[Python-checkins] r52625 - in tracker/vendor/roundup/current: .cvsignore BUILD.txt CHANGES.txt COPYING.txt ChangeLog MANIFEST.in README.txt cgi-bin cgi-bin/roundup.cgi demo.py detectors detectors/creator_resolution.py detectors/emailauditor.py detectors/newissuecopy.py doc doc/.cvsignore doc/FAQ.txt doc/Makefile doc/ZPL.txt doc/admin_guide.txt doc/announcement.txt doc/customizing.txt doc/debugging.txt doc/default.css doc/design.txt doc/developers.txt doc/features.txt doc/glossary.txt doc/images doc/images/edit.png doc/images/hyperdb.png doc/images/logo-acl-medium.png doc/images/logo-codesourcery-medium.png doc/images/logo-software-carpentry-standard.png doc/images/roundup-1.png doc/images/roundup.png doc/implementation.txt doc/index.txt doc/installation.txt doc/mysql.txt doc/original_overview.html doc/overview.txt doc/postgresql.txt doc/roundup-admin.1 doc/roundup-demo.1 doc/roundup-favicon.ico doc/roundup-mailgw.1 doc/roundup-server.1 doc/roundup-server.ini.example doc/spec.html doc/tracker_templates.txt doc/upgrading.txt doc/user_guide.txt doc/whatsnew-0.7.txt doc/whatsnew-0.8.txt frontends frontends/README.txt frontends/ZRoundup frontends/ZRoundup/.cvsignore frontends/ZRoundup/ZRoundup.py frontends/ZRoundup/__init__.py frontends/ZRoundup/dtml frontends/ZRoundup/dtml/manage_addZRoundupForm.dtml frontends/ZRoundup/icons frontends/ZRoundup/icons/tick_symbol.gif frontends/ZRoundup/refresh.txt locale locale/.cvsignore locale/GNUmakefile locale/de.po locale/en.po locale/es_AR.po locale/fr.po locale/lt.po locale/roundup.pot locale/ru.po locale/zh_CN.po locale/zh_TW.po patches patches/20020205.alternate_auth roundup roundup/.cvsignore roundup/__init__.py roundup/admin.py roundup/backends roundup/backends/.cvsignore 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 roundup/cgi/.cvsignore roundup/cgi/MultiMapping.py roundup/cgi/PageTemplates roundup/cgi/PageTemplates/.cvsignore roundup/cgi/PageTemplates/Expressions.py roundup/cgi/PageTemplates/GlobalTranslationService.py roundup/cgi/PageTemplates/MultiMapping.py roundup/cgi/PageTemplates/PageTemplate.py roundup/cgi/PageTemplates/PathIterator.py roundup/cgi/PageTemplates/PythonExpr.py roundup/cgi/PageTemplates/README.txt roundup/cgi/PageTemplates/TALES.py roundup/cgi/PageTemplates/__init__.py roundup/cgi/TAL roundup/cgi/TAL/.cvsignore roundup/cgi/TAL/DummyEngine.py roundup/cgi/TAL/HTMLParser.py roundup/cgi/TAL/HTMLTALParser.py roundup/cgi/TAL/README.txt roundup/cgi/TAL/TALDefs.py roundup/cgi/TAL/TALGenerator.py roundup/cgi/TAL/TALInterpreter.py roundup/cgi/TAL/TALParser.py roundup/cgi/TAL/TranslationContext.py roundup/cgi/TAL/XMLParser.py roundup/cgi/TAL/__init__.py roundup/cgi/TAL/markupbase.py roundup/cgi/TAL/talgettext.py roundup/cgi/TranslationService.py roundup/cgi/ZTUtils roundup/cgi/ZTUtils/.cvsignore roundup/cgi/ZTUtils/Batch.py roundup/cgi/ZTUtils/Iterator.py roundup/cgi/ZTUtils/__init__.py roundup/cgi/__init__.py roundup/cgi/accept_language.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/zLOG.py roundup/configuration.py roundup/date.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/msgfmt.py roundup/password.py roundup/rfc2822.py roundup/roundupdb.py roundup/scripts roundup/scripts/.cvsignore 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/support.py roundup/token.py roundup/version_check.py run_tests.py scripts scripts/README.txt scripts/add-issue scripts/copy-user.py scripts/hyperdb_example.py scripts/imapServer.py scripts/import_sf.py scripts/roundup-reminder scripts/roundup.rc-debian scripts/schema_diagram.py scripts/server-ctl setup.py templates templates/classic templates/classic/.cvsignore templates/classic/TEMPLATE-INFO.txt templates/classic/detectors templates/classic/detectors/.cvsignore templates/classic/detectors/messagesummary.py templates/classic/detectors/nosyreaction.py templates/classic/detectors/statusauditor.py templates/classic/detectors/userauditor.py templates/classic/extensions templates/classic/extensions/README.txt templates/classic/html templates/classic/html/_generic.calendar.html templates/classic/html/_generic.collision.html templates/classic/html/_generic.help.html templates/classic/html/_generic.index.html templates/classic/html/_generic.item.html templates/classic/html/file.index.html templates/classic/html/file.item.html templates/classic/html/help_controls.js templates/classic/html/home.classlist.html templates/classic/html/home.html templates/classic/html/issue.index.html templates/classic/html/issue.item.html templates/classic/html/issue.search.html templates/classic/html/keyword.item.html templates/classic/html/msg.index.html templates/classic/html/msg.item.html templates/classic/html/page.html templates/classic/html/query.edit.html templates/classic/html/query.item.html templates/classic/html/style.css templates/classic/html/user.forgotten.html templates/classic/html/user.index.html templates/classic/html/user.item.html templates/classic/html/user.register.html templates/classic/html/user.rego_progress.html templates/classic/initial_data.py templates/classic/schema.py templates/minimal templates/minimal/.cvsignore templates/minimal/TEMPLATE-INFO.txt templates/minimal/detectors templates/minimal/detectors/.cvsignore templates/minimal/detectors/userauditor.py templates/minimal/extensions templates/minimal/extensions/README.txt templates/minimal/html templates/minimal/html/_generic.calendar.html templates/minimal/html/_generic.collision.html templates/minimal/html/_generic.help.html templates/minimal/html/_generic.index.html templates/minimal/html/_generic.item.html templates/minimal/html/help_controls.js templates/minimal/html/home.classlist.html templates/minimal/html/home.html templates/minimal/html/page.html templates/minimal/html/style.css templates/minimal/html/user.index.html templates/minimal/html/user.item.html templates/minimal/html/user.register.html templates/minimal/html/user.rego_progress.html templates/minimal/initial_data.py templates/minimal/schema.py test test/.cvsignore test/README.txt test/__init__.py test/benchmark.py test/db_test_base.py test/mocknull.py test/session_common.py test/test_actions.py test/test_anydbm.py test/test_cgi.py test/test_dates.py test/test_hyperdbvals.py test/test_indexer.py test/test_locking.py test/test_mailgw.py test/test_mailsplit.py test/test_metakit.py test/test_multipart.py test/test_mysql.py test/test_postgresql.py test/test_rfc2822.py test/test_schema.py test/test_security.py test/test_sqlite.py test/test_templating.py test/test_token.py test/test_tsearch2.py tools tools/.cvsignore tools/base64 tools/fixroles.py tools/load_tracker.py tools/migrate-queries.py tools/pygettext.py
erik.forsberg
python-checkins at python.org
Sun Nov 5 21:30:53 CET 2006
Author: erik.forsberg
Date: Sun Nov 5 21:30:25 2006
New Revision: 52625
Added:
tracker/vendor/roundup/current/.cvsignore
tracker/vendor/roundup/current/BUILD.txt
tracker/vendor/roundup/current/CHANGES.txt
tracker/vendor/roundup/current/COPYING.txt
tracker/vendor/roundup/current/ChangeLog
tracker/vendor/roundup/current/MANIFEST.in
tracker/vendor/roundup/current/README.txt
tracker/vendor/roundup/current/cgi-bin/
tracker/vendor/roundup/current/cgi-bin/roundup.cgi (contents, props changed)
tracker/vendor/roundup/current/demo.py
tracker/vendor/roundup/current/detectors/
tracker/vendor/roundup/current/detectors/creator_resolution.py
tracker/vendor/roundup/current/detectors/emailauditor.py
tracker/vendor/roundup/current/detectors/newissuecopy.py
tracker/vendor/roundup/current/doc/
tracker/vendor/roundup/current/doc/.cvsignore
tracker/vendor/roundup/current/doc/FAQ.txt
tracker/vendor/roundup/current/doc/Makefile
tracker/vendor/roundup/current/doc/ZPL.txt
tracker/vendor/roundup/current/doc/admin_guide.txt
tracker/vendor/roundup/current/doc/announcement.txt
tracker/vendor/roundup/current/doc/customizing.txt
tracker/vendor/roundup/current/doc/debugging.txt
tracker/vendor/roundup/current/doc/default.css
tracker/vendor/roundup/current/doc/design.txt
tracker/vendor/roundup/current/doc/developers.txt
tracker/vendor/roundup/current/doc/features.txt
tracker/vendor/roundup/current/doc/glossary.txt
tracker/vendor/roundup/current/doc/images/
tracker/vendor/roundup/current/doc/images/edit.png (contents, props changed)
tracker/vendor/roundup/current/doc/images/hyperdb.png (contents, props changed)
tracker/vendor/roundup/current/doc/images/logo-acl-medium.png (contents, props changed)
tracker/vendor/roundup/current/doc/images/logo-codesourcery-medium.png (contents, props changed)
tracker/vendor/roundup/current/doc/images/logo-software-carpentry-standard.png (contents, props changed)
tracker/vendor/roundup/current/doc/images/roundup-1.png (contents, props changed)
tracker/vendor/roundup/current/doc/images/roundup.png (contents, props changed)
tracker/vendor/roundup/current/doc/implementation.txt
tracker/vendor/roundup/current/doc/index.txt
tracker/vendor/roundup/current/doc/installation.txt
tracker/vendor/roundup/current/doc/mysql.txt
tracker/vendor/roundup/current/doc/original_overview.html
tracker/vendor/roundup/current/doc/overview.txt
tracker/vendor/roundup/current/doc/postgresql.txt
tracker/vendor/roundup/current/doc/roundup-admin.1
tracker/vendor/roundup/current/doc/roundup-demo.1
tracker/vendor/roundup/current/doc/roundup-favicon.ico (contents, props changed)
tracker/vendor/roundup/current/doc/roundup-mailgw.1
tracker/vendor/roundup/current/doc/roundup-server.1
tracker/vendor/roundup/current/doc/roundup-server.ini.example
tracker/vendor/roundup/current/doc/spec.html
tracker/vendor/roundup/current/doc/tracker_templates.txt
tracker/vendor/roundup/current/doc/upgrading.txt
tracker/vendor/roundup/current/doc/user_guide.txt
tracker/vendor/roundup/current/doc/whatsnew-0.7.txt
tracker/vendor/roundup/current/doc/whatsnew-0.8.txt
tracker/vendor/roundup/current/frontends/
tracker/vendor/roundup/current/frontends/README.txt
tracker/vendor/roundup/current/frontends/ZRoundup/
tracker/vendor/roundup/current/frontends/ZRoundup/.cvsignore
tracker/vendor/roundup/current/frontends/ZRoundup/ZRoundup.py
tracker/vendor/roundup/current/frontends/ZRoundup/__init__.py
tracker/vendor/roundup/current/frontends/ZRoundup/dtml/
tracker/vendor/roundup/current/frontends/ZRoundup/dtml/manage_addZRoundupForm.dtml
tracker/vendor/roundup/current/frontends/ZRoundup/icons/
tracker/vendor/roundup/current/frontends/ZRoundup/icons/tick_symbol.gif (contents, props changed)
tracker/vendor/roundup/current/frontends/ZRoundup/refresh.txt
tracker/vendor/roundup/current/locale/
tracker/vendor/roundup/current/locale/.cvsignore
tracker/vendor/roundup/current/locale/GNUmakefile
tracker/vendor/roundup/current/locale/de.po
tracker/vendor/roundup/current/locale/en.po
tracker/vendor/roundup/current/locale/es_AR.po (contents, props changed)
tracker/vendor/roundup/current/locale/fr.po
tracker/vendor/roundup/current/locale/lt.po (contents, props changed)
tracker/vendor/roundup/current/locale/roundup.pot
tracker/vendor/roundup/current/locale/ru.po
tracker/vendor/roundup/current/locale/zh_CN.po
tracker/vendor/roundup/current/locale/zh_TW.po (contents, props changed)
tracker/vendor/roundup/current/patches/
tracker/vendor/roundup/current/patches/20020205.alternate_auth
tracker/vendor/roundup/current/roundup/
tracker/vendor/roundup/current/roundup/.cvsignore
tracker/vendor/roundup/current/roundup/__init__.py
tracker/vendor/roundup/current/roundup/admin.py
tracker/vendor/roundup/current/roundup/backends/
tracker/vendor/roundup/current/roundup/backends/.cvsignore
tracker/vendor/roundup/current/roundup/backends/__init__.py
tracker/vendor/roundup/current/roundup/backends/back_anydbm.py
tracker/vendor/roundup/current/roundup/backends/back_metakit.py (contents, props changed)
tracker/vendor/roundup/current/roundup/backends/back_mysql.py
tracker/vendor/roundup/current/roundup/backends/back_postgresql.py
tracker/vendor/roundup/current/roundup/backends/back_sqlite.py
tracker/vendor/roundup/current/roundup/backends/back_tsearch2.py
tracker/vendor/roundup/current/roundup/backends/blobfiles.py
tracker/vendor/roundup/current/roundup/backends/indexer_common.py
tracker/vendor/roundup/current/roundup/backends/indexer_dbm.py
tracker/vendor/roundup/current/roundup/backends/indexer_rdbms.py
tracker/vendor/roundup/current/roundup/backends/indexer_xapian.py
tracker/vendor/roundup/current/roundup/backends/locking.py
tracker/vendor/roundup/current/roundup/backends/portalocker.py
tracker/vendor/roundup/current/roundup/backends/rdbms_common.py
tracker/vendor/roundup/current/roundup/backends/sessions_dbm.py
tracker/vendor/roundup/current/roundup/backends/sessions_rdbms.py
tracker/vendor/roundup/current/roundup/backends/tsearch2_setup.py
tracker/vendor/roundup/current/roundup/cgi/
tracker/vendor/roundup/current/roundup/cgi/.cvsignore
tracker/vendor/roundup/current/roundup/cgi/MultiMapping.py
tracker/vendor/roundup/current/roundup/cgi/PageTemplates/
tracker/vendor/roundup/current/roundup/cgi/PageTemplates/.cvsignore
tracker/vendor/roundup/current/roundup/cgi/PageTemplates/Expressions.py
tracker/vendor/roundup/current/roundup/cgi/PageTemplates/GlobalTranslationService.py
tracker/vendor/roundup/current/roundup/cgi/PageTemplates/MultiMapping.py
tracker/vendor/roundup/current/roundup/cgi/PageTemplates/PageTemplate.py (contents, props changed)
tracker/vendor/roundup/current/roundup/cgi/PageTemplates/PathIterator.py
tracker/vendor/roundup/current/roundup/cgi/PageTemplates/PythonExpr.py
tracker/vendor/roundup/current/roundup/cgi/PageTemplates/README.txt
tracker/vendor/roundup/current/roundup/cgi/PageTemplates/TALES.py
tracker/vendor/roundup/current/roundup/cgi/PageTemplates/__init__.py
tracker/vendor/roundup/current/roundup/cgi/TAL/
tracker/vendor/roundup/current/roundup/cgi/TAL/.cvsignore
tracker/vendor/roundup/current/roundup/cgi/TAL/DummyEngine.py
tracker/vendor/roundup/current/roundup/cgi/TAL/HTMLParser.py
tracker/vendor/roundup/current/roundup/cgi/TAL/HTMLTALParser.py
tracker/vendor/roundup/current/roundup/cgi/TAL/README.txt
tracker/vendor/roundup/current/roundup/cgi/TAL/TALDefs.py
tracker/vendor/roundup/current/roundup/cgi/TAL/TALGenerator.py
tracker/vendor/roundup/current/roundup/cgi/TAL/TALInterpreter.py
tracker/vendor/roundup/current/roundup/cgi/TAL/TALParser.py
tracker/vendor/roundup/current/roundup/cgi/TAL/TranslationContext.py
tracker/vendor/roundup/current/roundup/cgi/TAL/XMLParser.py
tracker/vendor/roundup/current/roundup/cgi/TAL/__init__.py
tracker/vendor/roundup/current/roundup/cgi/TAL/markupbase.py
tracker/vendor/roundup/current/roundup/cgi/TAL/talgettext.py
tracker/vendor/roundup/current/roundup/cgi/TranslationService.py
tracker/vendor/roundup/current/roundup/cgi/ZTUtils/
tracker/vendor/roundup/current/roundup/cgi/ZTUtils/.cvsignore
tracker/vendor/roundup/current/roundup/cgi/ZTUtils/Batch.py
tracker/vendor/roundup/current/roundup/cgi/ZTUtils/Iterator.py
tracker/vendor/roundup/current/roundup/cgi/ZTUtils/__init__.py
tracker/vendor/roundup/current/roundup/cgi/__init__.py
tracker/vendor/roundup/current/roundup/cgi/accept_language.py (contents, props changed)
tracker/vendor/roundup/current/roundup/cgi/actions.py (contents, props changed)
tracker/vendor/roundup/current/roundup/cgi/apache.py
tracker/vendor/roundup/current/roundup/cgi/cgitb.py
tracker/vendor/roundup/current/roundup/cgi/client.py
tracker/vendor/roundup/current/roundup/cgi/exceptions.py (contents, props changed)
tracker/vendor/roundup/current/roundup/cgi/form_parser.py (contents, props changed)
tracker/vendor/roundup/current/roundup/cgi/templating.py
tracker/vendor/roundup/current/roundup/cgi/zLOG.py
tracker/vendor/roundup/current/roundup/configuration.py
tracker/vendor/roundup/current/roundup/date.py
tracker/vendor/roundup/current/roundup/exceptions.py
tracker/vendor/roundup/current/roundup/hyperdb.py
tracker/vendor/roundup/current/roundup/i18n.py
tracker/vendor/roundup/current/roundup/init.py
tracker/vendor/roundup/current/roundup/install_util.py
tracker/vendor/roundup/current/roundup/instance.py
tracker/vendor/roundup/current/roundup/mailer.py
tracker/vendor/roundup/current/roundup/mailgw.py
tracker/vendor/roundup/current/roundup/msgfmt.py
tracker/vendor/roundup/current/roundup/password.py
tracker/vendor/roundup/current/roundup/rfc2822.py
tracker/vendor/roundup/current/roundup/roundupdb.py
tracker/vendor/roundup/current/roundup/scripts/
tracker/vendor/roundup/current/roundup/scripts/.cvsignore
tracker/vendor/roundup/current/roundup/scripts/__init__.py
tracker/vendor/roundup/current/roundup/scripts/roundup_admin.py
tracker/vendor/roundup/current/roundup/scripts/roundup_demo.py
tracker/vendor/roundup/current/roundup/scripts/roundup_gettext.py
tracker/vendor/roundup/current/roundup/scripts/roundup_mailgw.py
tracker/vendor/roundup/current/roundup/scripts/roundup_server.py
tracker/vendor/roundup/current/roundup/security.py
tracker/vendor/roundup/current/roundup/support.py
tracker/vendor/roundup/current/roundup/token.py
tracker/vendor/roundup/current/roundup/version_check.py
tracker/vendor/roundup/current/run_tests.py
tracker/vendor/roundup/current/scripts/
tracker/vendor/roundup/current/scripts/README.txt
tracker/vendor/roundup/current/scripts/add-issue (contents, props changed)
tracker/vendor/roundup/current/scripts/copy-user.py (contents, props changed)
tracker/vendor/roundup/current/scripts/hyperdb_example.py
tracker/vendor/roundup/current/scripts/imapServer.py
tracker/vendor/roundup/current/scripts/import_sf.py
tracker/vendor/roundup/current/scripts/roundup-reminder (contents, props changed)
tracker/vendor/roundup/current/scripts/roundup.rc-debian
tracker/vendor/roundup/current/scripts/schema_diagram.py
tracker/vendor/roundup/current/scripts/server-ctl (contents, props changed)
tracker/vendor/roundup/current/setup.py
tracker/vendor/roundup/current/templates/
tracker/vendor/roundup/current/templates/classic/
tracker/vendor/roundup/current/templates/classic/.cvsignore
tracker/vendor/roundup/current/templates/classic/TEMPLATE-INFO.txt
tracker/vendor/roundup/current/templates/classic/detectors/
tracker/vendor/roundup/current/templates/classic/detectors/.cvsignore
tracker/vendor/roundup/current/templates/classic/detectors/messagesummary.py
tracker/vendor/roundup/current/templates/classic/detectors/nosyreaction.py
tracker/vendor/roundup/current/templates/classic/detectors/statusauditor.py
tracker/vendor/roundup/current/templates/classic/detectors/userauditor.py
tracker/vendor/roundup/current/templates/classic/extensions/
tracker/vendor/roundup/current/templates/classic/extensions/README.txt
tracker/vendor/roundup/current/templates/classic/html/
tracker/vendor/roundup/current/templates/classic/html/_generic.calendar.html
tracker/vendor/roundup/current/templates/classic/html/_generic.collision.html
tracker/vendor/roundup/current/templates/classic/html/_generic.help.html
tracker/vendor/roundup/current/templates/classic/html/_generic.index.html
tracker/vendor/roundup/current/templates/classic/html/_generic.item.html
tracker/vendor/roundup/current/templates/classic/html/file.index.html
tracker/vendor/roundup/current/templates/classic/html/file.item.html
tracker/vendor/roundup/current/templates/classic/html/help_controls.js
tracker/vendor/roundup/current/templates/classic/html/home.classlist.html
tracker/vendor/roundup/current/templates/classic/html/home.html
tracker/vendor/roundup/current/templates/classic/html/issue.index.html
tracker/vendor/roundup/current/templates/classic/html/issue.item.html
tracker/vendor/roundup/current/templates/classic/html/issue.search.html
tracker/vendor/roundup/current/templates/classic/html/keyword.item.html
tracker/vendor/roundup/current/templates/classic/html/msg.index.html
tracker/vendor/roundup/current/templates/classic/html/msg.item.html
tracker/vendor/roundup/current/templates/classic/html/page.html
tracker/vendor/roundup/current/templates/classic/html/query.edit.html
tracker/vendor/roundup/current/templates/classic/html/query.item.html
tracker/vendor/roundup/current/templates/classic/html/style.css
tracker/vendor/roundup/current/templates/classic/html/user.forgotten.html
tracker/vendor/roundup/current/templates/classic/html/user.index.html
tracker/vendor/roundup/current/templates/classic/html/user.item.html
tracker/vendor/roundup/current/templates/classic/html/user.register.html
tracker/vendor/roundup/current/templates/classic/html/user.rego_progress.html
tracker/vendor/roundup/current/templates/classic/initial_data.py
tracker/vendor/roundup/current/templates/classic/schema.py
tracker/vendor/roundup/current/templates/minimal/
tracker/vendor/roundup/current/templates/minimal/.cvsignore
tracker/vendor/roundup/current/templates/minimal/TEMPLATE-INFO.txt
tracker/vendor/roundup/current/templates/minimal/detectors/
tracker/vendor/roundup/current/templates/minimal/detectors/.cvsignore
tracker/vendor/roundup/current/templates/minimal/detectors/userauditor.py
tracker/vendor/roundup/current/templates/minimal/extensions/
tracker/vendor/roundup/current/templates/minimal/extensions/README.txt
tracker/vendor/roundup/current/templates/minimal/html/
tracker/vendor/roundup/current/templates/minimal/html/_generic.calendar.html
tracker/vendor/roundup/current/templates/minimal/html/_generic.collision.html
tracker/vendor/roundup/current/templates/minimal/html/_generic.help.html
tracker/vendor/roundup/current/templates/minimal/html/_generic.index.html
tracker/vendor/roundup/current/templates/minimal/html/_generic.item.html
tracker/vendor/roundup/current/templates/minimal/html/help_controls.js
tracker/vendor/roundup/current/templates/minimal/html/home.classlist.html
tracker/vendor/roundup/current/templates/minimal/html/home.html
tracker/vendor/roundup/current/templates/minimal/html/page.html
tracker/vendor/roundup/current/templates/minimal/html/style.css
tracker/vendor/roundup/current/templates/minimal/html/user.index.html
tracker/vendor/roundup/current/templates/minimal/html/user.item.html
tracker/vendor/roundup/current/templates/minimal/html/user.register.html
tracker/vendor/roundup/current/templates/minimal/html/user.rego_progress.html
tracker/vendor/roundup/current/templates/minimal/initial_data.py
tracker/vendor/roundup/current/templates/minimal/schema.py
tracker/vendor/roundup/current/test/
tracker/vendor/roundup/current/test/.cvsignore
tracker/vendor/roundup/current/test/README.txt
tracker/vendor/roundup/current/test/__init__.py
tracker/vendor/roundup/current/test/benchmark.py
tracker/vendor/roundup/current/test/db_test_base.py
tracker/vendor/roundup/current/test/mocknull.py
tracker/vendor/roundup/current/test/session_common.py
tracker/vendor/roundup/current/test/test_actions.py (contents, props changed)
tracker/vendor/roundup/current/test/test_anydbm.py
tracker/vendor/roundup/current/test/test_cgi.py
tracker/vendor/roundup/current/test/test_dates.py
tracker/vendor/roundup/current/test/test_hyperdbvals.py
tracker/vendor/roundup/current/test/test_indexer.py
tracker/vendor/roundup/current/test/test_locking.py
tracker/vendor/roundup/current/test/test_mailgw.py
tracker/vendor/roundup/current/test/test_mailsplit.py
tracker/vendor/roundup/current/test/test_metakit.py
tracker/vendor/roundup/current/test/test_multipart.py
tracker/vendor/roundup/current/test/test_mysql.py
tracker/vendor/roundup/current/test/test_postgresql.py
tracker/vendor/roundup/current/test/test_rfc2822.py
tracker/vendor/roundup/current/test/test_schema.py
tracker/vendor/roundup/current/test/test_security.py
tracker/vendor/roundup/current/test/test_sqlite.py
tracker/vendor/roundup/current/test/test_templating.py
tracker/vendor/roundup/current/test/test_token.py
tracker/vendor/roundup/current/test/test_tsearch2.py
tracker/vendor/roundup/current/tools/
tracker/vendor/roundup/current/tools/.cvsignore
tracker/vendor/roundup/current/tools/base64 (contents, props changed)
tracker/vendor/roundup/current/tools/fixroles.py
tracker/vendor/roundup/current/tools/load_tracker.py (contents, props changed)
tracker/vendor/roundup/current/tools/migrate-queries.py
tracker/vendor/roundup/current/tools/pygettext.py
Log:
Importing roundup 1.1.2.
Added: tracker/vendor/roundup/current/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,9 @@
+*.pyc
+*.pyo
+localconfig.py
+build
+demo
+dist
+MANIFEST
+_test_*
+*.cover
Added: tracker/vendor/roundup/current/BUILD.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/BUILD.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,51 @@
+Building Releases
+=================
+
+Roundup is currently a source-only release - it has no binary components. I
+want it to stay that way, too. This document describes how to build a
+source release. Users of Roundup should read the doc/installation.txt file
+to find out how to install this software.
+
+Building and distributing a release of Roundup is done by running:
+
+1. Make sure the unit tests run! "./run_tests.py"
+2. Tag the CVS for the release, eg. "cvs tag -R release-0-6-3"
+3. Edit roundup/__init__.py and doc/announcement.txt to reflect the new
+ version and appropriate announcements. Add truncated announcement to
+ setup.py description field.
+4. Clean out all *.orig, *.rej, .#* files from the source.
+5. python setup.py clean --all
+6. Edit setup.py to ensure that all information therein (version, contact
+ information etc) is correct.
+7. python setup.py sdist --manifest-only
+8. Check the MANIFEST to make sure that any new files are included. If
+ they are not, edit MANIFEST.in to include them. "Documentation" for
+ MANIFEST.in may be found in disutils.filelist._parse_template_line.
+9. python setup.py sdist
+ (if you find sdist a little verbose, add "--quiet" to the end of the
+ command)
+10. Unpack the new dist file in /tmp then a) run_test.py and b) demo.py
+ with all available Python versions.
+11. Generate gpg signature with "gpg -a --detach-sign"
+12. python setup.py bdist_rpm
+13. python setup.py bdist_wininst
+14. Send doc/announcement.txt to python-announce at python.org
+15. Notify any other news services as appropriate...
+
+ http://freshmeat.net/projects/roundup/
+
+
+So, those commands in a nice, cut'n'pasteable form::
+
+ find . -name '*.orig' -exec rm {} \;
+ find . -name '*.rej' -exec rm {} \;
+ find . -name '.#*' -exec rm {} \;
+ python setup.py clean --all
+ python setup.py sdist --manifest-only
+ python setup.py sdist --quiet
+ python setup.py bdist_rpm
+ python setup.py bdist_wininst
+ python setup.py register
+ python2.5 setup.py sdist upload --sign
+
+
Added: tracker/vendor/roundup/current/CHANGES.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/CHANGES.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,1873 @@
+This file contains the changes to the Roundup system over time. The entries
+are given with the most recent entry first.
+
+2006-04-27 1.1.2
+Feature:
+- server-ctl script uses server configuration file (sf bug 1443805)
+
+Fixed:
+- progress display in roundup-admin reindex
+- bug in menu() permission filter (sf bug 1444440)
+- indexing may be turned off for FileClass "content" now
+ ("content" and "type" properties are now automatically included in the
+ FileClass schema where previously the "content" property was faked and
+ "type" was optional)
+- verbose output during import is optional now (sf bug 1475624)
+- escape *all* uses of "schema" in mysql backend (sf bug 1472120)
+- responses to user rego email (sf bug 1470254)
+- dangling connections in session handling (sf bug 1463359)
+- reduced frequency of session timestamp update
+- classhelp popup pagination forgot about "type" (sf bug 1465836)
+- umask is now configurable (with the same 0002 default)
+- sorting of entries in classhelp popup (sf bug 1449000)
+- allow single digit seconds in date spec (sf bug 1447141)
+- prevent generation of new single-digit seconds dates (sf bug 1429390)
+- implement close() on all indexers (sf bug 1242477)
+
+
+2006-03-03 1.1.1
+Fixed:
+- failure with browsers not sending "Accept-Language" header
+ (sf bugs 1429646 and 1435335)
+- translate class name in "required property not supplied" error message
+ (sf bug 1429669)
+- error in link property lookups with numeric-alike key values (sf bug 1424550)
+- ignore UTF-8 BOM in .po files
+- add permission filter to menu() implementations (sf bug 1431188)
+- lithuanian translation updated by Nerijus Baliunas (sf patch 1411175)
+- incompatibility with python2.3 in the mailer module (sf bug 1432602)
+- typo in SMTP TLS option name: "MAIL_TLS_CERFILE" (sf bug 1435452)
+- email obfuscation code in html templating is more robust
+- blank-title subject line handling (sf bug 1442121)
+- "All users may only view and edit issues, files and messages they
+ create" example in docs (sf bug 1439086)
+- saving of queries (sf bug 1436169)
+- "Adding a new constrained field to the classic schema" example in docs
+ (sf bug 1433118)
+- security check in mailgw (sf bug 1442145)
+- "clear this message" (sf bug 1429367)
+- escape all uses of "schema" in mysql backend (sf bug 1397569)
+- date spec wasn't allowing week intervals
+
+
+2006-02-10 1.1.0
+Feature:
+- trackers may configure custom stop-words for the full-text indexer
+- login may now be for a single session (and this is the default)
+- trackers may hide exceptions from web users (they will be mailed to the
+ tracker admin) (hiding is the default)
+- include "clear this message" link in the "ok" message bar
+
+Fixed:
+- fixes in scripts/import_sf.py
+- fix some unicode bugs in roundup-admin import
+- Xapian indexer wasn't actually being used and its reindexing of existing
+ data was busted to boot
+- roundup-admin import wasn't indexing message content
+- allow dispname to be passed to renderWith (sf bug 1424587)
+- rename dispname to @dispname to avoid name clashes in the future
+- fixed schema migration problem when Class keys were removed
+
+
+2006-02-03 1.0.1
+Feature:
+- scripts/import_sf.py will import a tracker from Sourceforge.NET
+- added hasRole() to HTMLUser
+
+Fixed:
+- SQL generation for sort/group by separate Link properties (sf bug
+ 1417565)
+- fix timezone offsetting in email Date: header
+- fix security check for hasPermission('Permission', None)
+
+
+2006-01-27 1.0
+Feature:
+- Lithuanian translation by Aiste Kesminaite
+- Web User Interface language selection by form variable @language,
+ browser cookie or HTTP header Accept-Language (sf patch 1360321)
+- initial values for configuration options may be passed on
+ 'roundup-admin install' command line (based on sf patch 1237110)
+- favicon.ico image may be changed with server config option (sf patch 1355661)
+- Password objects initialized from plaintext remember plaintext value
+ (sf rfe 1379447)
+- Roundup installation document includes configuration example
+ for Exim Internet Mailer (sf bug 1393860)
+- enable registration confirmation by web only (sf bug 1381675)
+- allow preselection of values in templating menu()s (sf patch 1396085)
+- display the query name in the header (sf feature 1298535 / patch 1349387)
+- classhelp works with Link properties now (sf bug 1410290)
+- added setorderprop() and setlabelprop() to Class (sf features 1379534,
+ 1379490)
+- CSV encoding support (sf bug 1240848)
+- fields rendered with StructuredText are hyperlinked by default
+- additional attributes for input element may be passed to the 'field'
+ method of a property wrapper
+- added "copy_url" method to generate a URL for copying an item
+
+Fixed:
+- MySQL now creates String columns using the TEXT column type
+- password.crypt won't work with md5 passwords (sf bug 1372253)
+- use quoted printable encoding for nosy attachments that have MIME
+ type 'text/plain' but contain 8-bit characters (sf bug 1381559)
+- login name and email address fields in the classic template
+ are highlighted as required fields (sf bug 1392364)
+- french translation updated by Patrick Decat (sf patch 1397059)
+- HTTP authorization takes precedence over session cookie (sf bug 1396134)
+- enforce correct encoding of PostgreSQL backend (sf bug 1374235)
+- grouping/sorting on link to same class fixed (sf bug 1404930)
+- all backends implement the retired check in getnodeids (sf bug 1290560)
+- fix detection of "missing" existing values in CGI form parser (sf bug
+ 1414149)
+- ZRoundup works again (sf bug 1263842)
+- default user template does not display password fields and submit button
+ when editing is not allowed
+- fix StructuredText import in cgi.templating
+- have "System Messages" be marked as such again (sf bug 1281907)
+- enable editing of public queries (sf bug 966144)
+
+
+2005-10-07 0.9.0b1
+Feature:
+- added "imapServer.py" script (sf patch 934567)
+- added date selection popup windows (thanks Marcus Priesch)
+- added Xapian indexer; replaces standard indexers if Xapian is available
+- mailgw subject parsing has configurable levels of strictness
+- nosy messages may be sent individually to all recipients
+- remember where we came from when logging in (sf patch 1312889)
+
+
+2006-??-?? 0.8.6
+Fixed:
+- french translation updated by Patrick Decat (sf patch 1397059)
+- tighten up Date parsing to not allow 'M/D/YY' (or 'D/M/YY) (sf bug
+ 1290550)
+- handle "schema" being reserved word in MySQL 5+ (sf bug 1397569)
+- fixed documentation of filter() in the case of multiple values in a
+ String search (sf bug 1373396)
+- fix comma-separated ID filter spec in web requests (sf bug 1396278)
+- fix Date: header generation to be LOCALE-agnostic (sf bug 1352624)
+- fix admin doc description of roundup-server config file
+- fix redirect after instant registration (sf bug 1381676)
+- fix permission checks in cgi interface (sf bug 1289557)
+- fix permission check on RetireAction (sf bug 1407342)
+- timezone now applied to date for pretty-format (sf bug 1406861)
+- fix mangling of "_" in mail Subject class name (sf bug 1413852)
+- catch bad classname in URL (related to sf bug 1240541)
+- clean up digested_file_types (sf bug 1268303)
+- fix permission checks in mailgw (sf bug 1263655)
+- fix encoding of subject in generated error message (sf bug 1414465)
+
+
+2005-10-07 0.8.5
+Feature:
+- Argentinian Spanish translation by Ramiro Morales
+
+Fixed:
+- Display of Multilinks where linked Class labelprop values are None
+- Fix references to the old * Registration Permissions
+- Fix missing merge of fix to sf bug 1177057
+- Fix RDBMS indexer indexing UTF-8 words that encode to > 30 chars
+- Handle invalidly-specified charsets in incoming email
+
+
+2005-07-18 0.8.4
+Fixed:
+- extra CRs in CSV export files on Windows platform (sf bug 1195742)
+- activity RDBMS columns were being reported in changes
+- fix name collision in roundup.cgi script (sf bug 1203795)
+- fix handling of invalid interval input
+- search locale files relative ro roundup installation path (sf bug 1219689)
+- use translation for boolean property rendering (sf bug 1225152)
+- enabled disabling of REMOTE_USER for when it's not a valid username (sf
+ bug 1190187)
+- fix invocation of hasPermission from templating code (sf bug 1224172)
+- have 'roundup-admin security' display property restrictions (sf bug
+ 1222135)
+- fixed templating menu() sort_on handling (sf bug 1221936)
+- allow specification of pagesize, sorting and filtering in "classhelp"
+ popups (sf bug 1211800)
+- handle dropped properies in rdbms/metakit journal export (sf bug 1203569)
+- handle missing Subject lines better (sf bug 1198729)
+- sort/group by missing values correctly (sf bugs 1198623, 1176897)
+- discard, don't bounce messages to the mailgw when the messages's sender
+ is invalid (ie. when we try to bounce, we get a 550 "unknown user
+ account" response from the SMTP server) (sf bug 1190906)
+- removed debugging code from cgi/actions.py
+- refactored hyperdb.rawToHyperdb, allowing a number of improvements
+ (thanks Ralf Schlatterbeck)
+- don't try to set a timeout for IMAPS (thanks Paul Jimenez)
+- present Reject exception messages to web users (sf bug 1237685)
+
+
+2005-05-02 0.8.3
+Feature:
+- chinese translation by limodou
+
+Fixed:
+- fix reference to The Zope Book in Roundup FAQ
+- disabled file logging in Roundup test suite (sf bug 1155649)
+- return original string if message issue xref isn't valid
+- fix nosyreaction.py to stop it setting the nosy list unnecessarily
+ (see doc/upgrading.txt for how to fix in your trackers)
+- after logout, always display tracker home page
+- web forms don't create new items if no item properties are set from UI
+- item creation failed if multilink fields had invalid entries (sf bug
+ 1177602)
+- fix bdist_rpm (sf bug 1164328)
+- fix checking of "Email Access" for Anonymous email registration (sf bug
+ 1177057)
+- disable "Email Access" for Anonymous by default to stop spam regsitering
+ users on public trackers
+- send errors in the web interface to a logfile by default. Use the
+ "debug" multiprocess mode (roundup-server) or the DEBUG_TO_CLIENT var
+ (roundup.cgi) to have the errors appear in your browser
+- fix setgid typo (sf bug 1171346)
+- fix faulty find_template filename facility (sf bug 1163629)
+- fix roundup-admin "export" so it creates the target dir if needed
+- "fix" roundup-admin "import" to not use "universal newline support" since
+ the csv module appears to have its own ideas about such things (sf bug
+ 1163890)
+- fix installation docs referring to old-style configuration variables
+- fix roundup-admin "find" for searching Multilinks (sf bug 1189465)
+
+
+2005-03-03 0.8.2
+Feature:
+- roundup-server automatically redirects from trackers list
+ to the tracker page if there is only one tracker
+
+Fixed:
+- added content to ZRoundup refresh.txt file (sf bug 1147622)
+- fix invalid reference to csv.colon_separated
+- correct URL to What's New in setup.py meta-data
+- change AUTOCOMMIT=OFF to AUTOCOMMIT=0 for MySQL (sf bug 1143707)
+- compile message objects in 'setup.py build'
+- use backend datatype for journal timestamps in RDBMSes
+- fixes to the "Using an external password validation source"
+ customisation example (sf bugs 1153640 and 1155108)
+
+
+2005-02-17 0.8.1
+Fixed:
+- replaced MutlilinkIterator with multilinkGenerator (thanks Bob Ippolito)
+- fixed broken csv import in roundup.admin module
+- fixed braino in HTMLClass.filter() (sf bug 1124213)
+- change ZTUtils Iterator to always iter() its sequence argument
+
+
+2005-01-16 0.8.0
+Fixed:
+- fix roundup-server log and PID file paths to be absolute
+- fix initialisation of roundup-server in daemon mode so initialisation
+ errors are visible
+- fix: 'Logout' link was enabled on issue index page only
+- have Permissions only test the check function if itemid is suppled
+- modify cgi templating system to check item-level permissions in listings
+- enable batching in message and file listings
+- more documentation of security mechanisms (incl. sf patches 1117932,
+ 1117860)
+- better unit tests for security mechanisms
+- code cleanup (sf patch 1115329 and additional)
+- issue search page allows setting of no sorting / grouping (sf bug
+ 1119475)
+- better edit conflict handling (sf bug 1118790)
+- consistent text searching behaviour (AND everywhere) (sf bug 1101036)
+- fix handling of invalid date input (sf bug 1102165)
+- retain Boolean selections in edit error handling (sf bug 1101492)
+- fix initialisation of logging module from config file (sf bug 1108577)
+- removed rlog module (py 2.3 is minimum version now)
+- fixed class "help" listing paging (sf bug 1106329)
+- nicer error looking up values of None (response to sf bug 1108697)
+- fallback for (list) popups if javascript disabled (sf patch 1101626)
+
+
+2005-01-13 0.8.0b2
+Fixed:
+- note about how to run roundup demo in Windows (sf bug 1082090)
+- fix API for templating utils extensions - remove "utils" arg (sf bug 1081981)
+- back_sqlite.py is missing "import time" (sf bug 1081959)
+- fix (list) popup (sf bug 1083570)
+- fix some security assertions (sf bug 1085481)
+- 'roundup-server -S' always writes [trackers] section heading (sf bug 1088878)
+- fix port number as int in mysql connection info (sf bug 1082530)
+- fix setup.py to work with <Python2.3 (sf bug 1082801)
+- fix permissions checks in cgi templating (sf bug 1082755)
+- fix "Users may only edit their issues" example in docs
+- handle ~/.my.cnf files for MySQL defaults (sf bug 1096031)
+
+
+2004-12-08 0.8.0b1
+Feature:
+- added MD5 scheme for password hiding
+- added support for HTTP charset selection
+- implement __nonzero__ for HTMLProperty
+- remove "manual" locking of sqlite database
+- create a new RDBMS cursor after committing
+- added basic logging, and configuration of it and python's logging module
+- roundup-mailgw now logs fatal exceptions rather than mailing them to admin
+- add a default argument to the DateHTMLProperty.field method, and an
+ optional Interval (string or object) to the DateHTMLProperty.now (patch
+ from Vickenty Fesunov)
+- hide "(list)" popup links when issue is only viewable
+- roundup-server options -g and -u accept both ids and names (sf bug 983769)
+- roundup-server now has a configuration file (-C option)
+- added mod_python interface (see installation.txt)
+- reorganised tracker configuration, using ConfigParser config, cleaned-up
+ schema definition and implementing easier extension writing (sf rfe 661301)
+- Permissions may now be defined on a per-property basis
+- added "Create" Permission. Replaces the "Web"- and "Email Registration"
+ Permissions.
+- added option to turn off registration confirmation via email
+ ("instant_registration" in config) (sf rfe 922209)
+- roundup-admin reindex command may now work on single items or classes
+- multiple selection Link/Multilink search field (thanks Marlon van den Berg)
+- relaxed hyperlinking in web interface (accept "issue123" or "Issue 123")
+- record journaltag lookup ("fixes" sf bug 998140)
+- allow listing popup to be used in query forms (thanks Marcus Priesch)
+- roundup windows service may be installed with command line options
+ recognized by roundup-server (but not tracker specification arguments).
+ Use this to specify server configuration file for the service.
+- added experimental multi-thread server
+- don't try to import all backends in backends.__init__ unless we *want* to
+- unless in debug mode, keep a single persistent connection through a
+ single web or mailgw request.
+- HTTP Basic Authentication (sf patch 1067690)
+- extended security.addPermissionToRole to allow skipping the separate
+ getPermission call
+
+Fixed:
+- postgres backend open doesn't hide corruption in schema (sf bug 956375)
+- *dbm-style backends nuke() method now clear id counters
+- removed safeget() from the API (sf bug 994750)
+- demo tracker is always set up on localhost (sf bug 1049101)
+- relaxed URL designator syntax to allow issue[0]*1 (sf bug 1054523)
+
+
+2005-05-02 0.7.12
+Fixed:
+- handle capitalisation of class names in text hyperlinking (sf bug
+ 1101043)
+- quote full-text search text in URL generation
+- fixed problem migrating mysql databases
+- fix search_checkboxes macro (sf patch 1113828)
+- fix bug in date editing in Metakit
+- allow suppression of search_text in indexargs_form (sf bug 1101548)
+- hack to fix some anydbm export problems (sf bug 1081454)
+- ignore AutoReply messages (sf patch 1085051)
+- fix ZRoundup syntax error (sf bug 1122335)
+- fix RDBMS clear() so it resets all class itemid counters
+
+
+2005-01-06 0.7.11
+Fixed:
+- index args URL generation broken in .10 (sf bug 1096027)
+- handle NotModified for non-static files (sf patch 1095790)
+- fix permission lookup in query editing
+
+
+2005-01-04 0.7.10
+Fixed:
+- reset ID counters if the database is cleared (thanks William)
+- apply IE caching "fix" to automatically serve up all pages expired
+- fix typo (sf patch 1076629)
+- fix hyperlinking of items (sf bug 1080251)
+- fix roundup-admin find command handling of Multilinks
+- fix some security assertions (sf bug 1085481)
+- don't set the title to nothing from incoming mail (thanks Bruce Guenter)
+- fix py2.4 strftime() API change bug (sf bug 1087746)
+- fix indexer searching with no valid words (sf bug 1086787)
+- updated searching / indexing docs
+- fix "(list)" popup when list is one item long (sf bug 1064716)
+- have RDBMS full-text indexer do AND searching (sf bug 1055435)
+- handle spaces in String index params in batching (sf bug 1054224)
+
+
+2004-10-26 0.7.9
+Feature:
+- DateHTMLProperty.field() accepts format string (thanks Wil Cooley)
+
+Fixed:
+- popup listing uses filter args (thanks Marlon van den Berg)
+- fixed editing of message contents
+- loosened the detection of issue cross-references in messages
+- open CSV files in "universal newline" mode
+- s/Modifed/Modified (thanks donfu)
+- applied patch fixing some form handling issues in ZRoundup (sf bug 995565)
+- enforce View Permission when serving file content (sf bug 1050470)
+- don't index common words (sf bug 1046612)
+- don't wrap query.item.html in a <span> (thanks Roch'e Compaan)
+- TAL expressions like 'request/show/whatever' return True
+ if the request does not contain explicit @columns list
+- NumberHTMLProperty should return '' not "None" if not set (thanks
+ William)
+- ensure multilink ordering in RDBMS backends (thanks Marcus Priesch, sf
+ bug 950963)
+- always honor indexme property on Strings (sf patch 1063711)
+- make hyperdb value parsing errors readable in mailgw errors
+- make anydbm journal export handle removed properties
+- allow use of XML templates again
+
+
+2004-10-15 0.7.8
+Fixed:
+- Clean out sessions / otks tables when migrating
+
+
+2004-10-11 0.7.7
+Fixed:
+- ZRoundup's search interface works now (sf bug 994957)
+- fixed history display when "ascending"
+- removed references to py2.3+ boolean values (sf bug 995682)
+- fix static file path normalisation in security check (thanks David Linke)
+- less specific messages for login failures (thanks Chris Withers)
+- Reject raised against email messages should result in email rejection, not
+ discarding of the message
+- mailgw can override the MAIL_DEFAULT_CLASS
+- handle Py2.3+ datetime objects as Date specs (sf bug 971300)
+- use row locking in MySQL newid() (sf bug 1034211)
+- add sanity check for sort and group on same property (sf bug 1033477)
+- extend OTK and session table value cols to TEXT (sf bug 1031271)
+- fix lookup of REMOTE_USER (sf bug 1002923)
+- new Interval props weren't created properly in rdbms
+- date.Interval() now accepts an Interval as a spec (sf bug 1041266)
+- handle deleted properties in RDBMS history
+- apply timezone in correct direction in user input (sf bug 1013097)
+- more efficient find() in RDBMS (sf bug 1012781)
+
+
+2004-07-21 0.7.6
+Fixed:
+- rdbms backend full text search failure after import (sf bug 980314)
+- rdbms backends not filtering correctly on link=None
+- fix anydbm journal import (sf bug 983166)
+- handle postgresql bug in SQL generation (sf bug 984591)
+- fix dates-from-Dates (sf bug 984604)
+- fix messageid generated when msgid is None for send_message (sf bug 987933)
+- make user permissions check more sane (fix search page for anonymous)
+- fixed RDBMS filter() for no matches from full-text search (sf bug 990778)
+- fixed DateHTMLProperty for invalid date entry (sf bug 986538)
+- fixed external password source example (sf bug 986601)
+- document the STATIC_FILES config var
+- implement the HTTP HEAD command (sf bug 992544)
+- fix journal export of files to remove content from CSV files
+- API clarification. Previously, the anydbm/bsddb/metakit filter() methods
+ had required exact matches to Multilink argument lists. The RDBMS
+ backends treated Multilink matches like all other data types - matching
+ any of the Multilink argument list is good enough. The latter behaviour
+ is implemented across the board now.
+- fix metakit handling of filter on Link==None
+
+
+2004-06-24 0.7.5
+Fixed:
+- force lookup of journal props in anydbm filtering
+- fixed lookup of "missing" Link values for new props in anydbm backend
+- allow list of values for id, Number and Boolean filtering in anydbm
+ backend
+- fixed some more mysql 0.6->0.7 upgrade bugs (sf bug 950410)
+- fixed Boolean values in postgresql (sf bugs 972546 and 972600)
+- fixed -g arg to roundup-server (sf bug 973946)
+- better roundup-server usage string (sf bug 973352)
+- include "context" always, as documented (sf bug 965447)
+- fixed REMOTE_USER (external HTTP Basic auth) (sf bug 977309)
+- fixed roundup-admin "find" to use better value parsing
+- fixed RDBMS Class.find() to handle None value in multiple find
+- export now stores file "content" in separate files in export directory
+
+
+2004-06-10 0.7.4
+Fixed:
+- re-acquire the OTK manager when we re-open the database
+- mailgw handler can close the database on us
+- fixed grouping by a NULL Link value
+- fixed anydbm import/export (sf bugs 965216, 964457, 964450)
+- fix python 2.3.3 strftime deprecation warning (sf patch 968398)
+- fix some column datatypes in postgresql and mysql (sf bugs 962611,
+ 959177 and 964231)
+- fixed RDBMS journal packing (sf bug 959177)
+- fixed filtering by floats in anydbm (sf bug 963584)
+
+
+2004-05-28 0.7.3
+Fixed:
+- add "checked" to truth values for Boolean input
+- fixed import in metakit backend
+- fix SearchAction use of Class.filter(), and clarify API docs for same
+- ensure static files may only be served out of the tracker's "static
+ files" directory
+
+
+2004-05-17 0.7.2
+Fixed:
+- anydbm sorting with None values (sf bug 952853)
+- roundup-server -g option not recognised (sf bug 952310)
+- HTML templating isset() inverted (sf bug 951779)
+- otks manager missing (sf bug 952931)
+- mention DEFAULT_TIMEZONE requirement in upgrading doc (sf bug 952932)
+- fix DateHTMLProperty so local() can override user timezone (sf bug
+ 953678)
+- fix anydbm sort/group direction handling, and make RDBMS sort/group use
+ Link'ed "order" properties (sf bug 953148)
+- fix Interval editing (sf bug 954891)
+
+
+2004-05-10 0.7.1
+Fixed:
+- several temp files made it into the source distribution (sf bug 949243)
+- typo in roundup/instance.py
+- missing CRLF var in rfc822.py (sf patch 949471)
+- fix user creation page
+- have roundup server pass though the cause of a "403 Forbidden" response
+- fix schema mutation in sqlite backends (thanks Tamer Fahmy)
+- make popup Javascript IE 5.0 friendly (thanks Marlon van den Berg)
+- fix RDBMS import (thanks Tamer Fahmy)
+
+
+2004-05-06 0.7.0
+Fixed:
+- sqlite migration drops some journal information (thanks David Linke)
+- user editing Role entry help text always appears
+- disable forking server when os.fork() not available (sf bug 938586)
+- removed Boolean from source to make py <2.3 happy (sf bug 938790)
+- fix nested scope bug in rdbms multilink sorting
+- re-seed the random number generator for each request
+- postgresql backend altered to not use popen (thanks Georges Martin)
+- fixed journal marshalling in RDBMS backends (sf bug 943627)
+- fixed handling of key values starting with numbers (sf bug 941363)
+- fixed journal "param" column size in RDBMS backends
+- fixed static file serving
+- fixed rego from email address (sf bug 947414)
+- fixed sqlite journal ordering issue
+- fixed mysql date range filtering
+
+
+2004-04-18 0.7.0b3
+Feature:
+- added a favicon
+- added url_quote and html_quote methods to the utils object
+- added isset method to HTMLProperty
+- database export now exports full journals too
+- tracker name at end of page title (sf rfe 926840)
+- roundup-server now uses the ForkingMixin
+- added another sample detector "creator_resolution"
+- added search_checkboxes as an option for the search form
+- added IMAP support to mail gateway (sf rfe 934000)
+- check MANIFEST against the files actually unpacked
+- roundupdb nosymessage() takes an optional bcc list
+
+Fixed:
+- mysql and postgresql schema mutation now handle added Multilinks
+- web CSV export was busted (as was any action returning a result)
+- MultiMapping deviated from the Zope C implementation in a number of
+ places (thanks Toby Sargeant)
+- MySQL and Postgresql use BOOL/BOOLEAN for Boolean types
+- OTK generation was busted (thanks Stuart D. Gathman)
+- export and import now include journals (incompatible with export < 0.7)
+- added "download_url" method to generate a correctly quoted URL for file
+ download links (sf bug 927745)
+- all uses of TRACKER_WEB now ensure it ends with a '/'
+- roundup-admin install checks for existing tracker in target home
+- grouping (and sorting) by multilink in RDBMS backends (sf bug 655702)
+- roundup scripts may now be asked for their version (sf rfe 798657)
+- sqlite backend had stopped using the global lock
+- better check for anonymous viewing of user items (sf bug 933510)
+- stop Interval from displaying an empty string (sf bug 934022)
+- fixed storage of some datatypes in some RDBMS backends
+
+
+2004-03-27 0.7.0b2
+Feature:
+- added CSV export to index pages
+- added emailauditor.py which works around a bug in IE. See
+ "detectors/emailauditor.py" for more info.
+- added dispatcher functionality - see upgrading.txt for more info
+- added Reject exception which may be raised by auditors. This is trapped
+ by mailgw and may be used to veto creation of file attachments or
+ messages. (sf bug 700265)
+- queries on a per-user basis, and public queries (sf "bug" 891798 :)
+- added DEFAULT_TIMEZONE (sf rfe 895139)
+- added HTML page template to the templating context as "template"
+- added is_retired to HTMLItems in templating
+
+Fixed:
+- Boolean, Date and Link HTML templating was broken
+- fix reporting of test inclusion in postgresql test
+- EditAction was confused about who "self" was
+- edit collision detection was broken for index-page edits
+- sqlite backend wasn't migrating multilink tables correctly
+- use SimpleCookie instead of Cookie (is an alias for the evil SmartCookie)
+- handle older sessions in session dbm
+- make presetunread more resilient to status Class changes
+- HTMLDatabase classes() was broken
+
+
+2004-03-24 0.7.0b1
+Major new features:
+- added postgresql backend (originally from sf patch 761740, many changes
+ since)
+- added new "actor" automatic property (indicates user who cause the last
+ "activity")
+- RDBMS backends implement their session and one-time-key stores and
+ full-text indexers; thus they are now performing their own locking
+ internally
+- all RDBMS backends now have indexes on several columns
+- support confirming registration by replying to the email (sf bug 763668)
+- all HTML templating methods now automatically check for permissions
+ (either view or edit as appropriate), greatly simplifying templates
+
+Other new features:
+- simple support for collision detection (sf rfe 648763)
+- support setgid and running on port < 1024 (sf patch 777528)
+- using Zope3's test runner now, allowing GC checks, nicer controls and
+ coverage analysis
+- change nosymessage and send_message to accept msgid=None (RFE #707235)
+- handle Resent-From: headers (sf bug 841151)
+- always sort MultilinkHTMLProperty in the correct order, usually
+ alphabetically (sf feature 790512)
+- added script for copying user(s) ("scripts/copy-user.py") from tracker
+ to tracker (sf patch 828963)
+- ignore incoming email with "Precedence: bulk" (sf patch 843489)
+- use HTTP 'Content-Length' header (modified sf patch 844577)
+- HTML generated is now HTML4 (or optionally XHTML) compliant (sf feature
+ 814314 and sf patch 834620)
+- default stylesheet turns off sidebar when printing
+- allow direct supply of filter() arguments in templating (thanks Godefroid
+ Chapelle)
+- improved body_title slot in HTML templating (sf patch 873502)
+- HTMLLinkProperty field() method renders as a field now (thanks darryl)
+- cgi Action handlers may now return the actual content to be sent back to
+ the user (rather than using some template)
+- date.Date now handles fractional seconds
+
+Fixed:
+- mysql documentation fixed to note requirement of 4.0+ and InnoDB
+- added testing of schema mutation, fixed rdbms backends handling of a
+ couple of cases
+- HTML 4.01 validation on the 'classic' backend
+- messages to the mailgw can be about classes other than issues now.
+- signature matching is more precise (sf bug 827775).
+- anonymous user can no longer edit or view itself (sf bug 828901).
+- corrected typo in installation.html (sf bug 822967).
+- clarified listTemplates docstring.
+- print a nicer error message when the address is already in use
+ (sf bug 798659).
+- remove empty lines before sending strings off to the csv parser
+ (sf bug 821364).
+- centralised conversion of user-input data to hyperdb values (sf bug
+ 802405, sf bug 817217, sf rfe 816994)
+- recalculate SHA on template files when installed tracker used as
+ template (sf bug 827510)
+- fixed ZRoundup (sf bug 624380)
+- the mail gateway now searches recursively for the text/plain and the
+ attachments of a message (sf bug 841241).
+- fixed display of feedback messages in some situations (sf bug 739545)
+- fixed ability to edit "content" property (sf bug 914062)
+
+Cleanup:
+- replace curuserid attribute on Database with the extended getuid() method
+- extract a new 'mailer' module for sending mail
+- extract a '_send_mail' method for testing mail sending
+- simplify backend importing
+- use roundup_server in demo.py
+- implement newItemAction using editItemAction
+- use FormError in client.py, moving the handling up to inner_main()
+- implemented semantic comparison of Message objects in test_mailgw
+- tidied up forms in default stylesheet
+- force textareas to use monospace fonts, lessening surprise on the user
+- moved out parts of client.py to new modules:
+ * actions.py - the xxxAction and xxxPermission functions refactored into
+ Action classes
+ * exceptions.py - all exceptions
+ * form_parser.py - parsePropsFromForm & extractFormList in a FormParser
+ class
+
+
+2004-05-17 0.6.10
+Fixed:
+- mysql backend wasn't locking tracker
+- ensure static files may only be served out of the tracker's "static
+ files" directory
+
+
+2004-04-18 0.6.9
+Fixed:
+- paging in classhelp popup was broken
+- socket timeout error logging can fail
+- hyperlink designators in message display (sf bug 931828)
+- don't match retired items in RDBMS stringFind
+
+
+2004-04-01 0.6.8
+Fixed:
+- existing trackers (ie. live ones) may be used as templates for new
+ trackers - the TEMPLATE-INFO.txt name entry has the tracker's dir name
+ appended (so the demo tracker's template name is "classic-demo")
+- handle bad multilink input at item creation time better (sf bug 917834)
+- make sure email signature starts on a newline (sf bug 919759)
+- add line to rego email to help URL detection (sf bug 906247)
+- look harder for text/plain in email
+- fixed fallback excel writer in rcsv so it has a delimiter
+- fixed setup.py's use of listTemplates (!)
+- make rdbms serialise() less trusting
+- handle Boolean values in history HTML display
+
+
+2004-03-01 0.6.7
+Fixed:
+- be more backward-compatible when asking for EMAIL_CHARSET
+- made error on create consistent with edit when user enters invalid data
+ for Multilink and Link form fields (sf bug 904072)
+- made errors from bad input in the quick "Show issue:" form more
+ user-friendly (sf bug 904064)
+- don't add a query to a user's list if it's already there
+- nicer invalid property error in HTML templating
+- use EMAIL_CHARSET for message body too (still sf bug 900046)
+
+
+2004-02-25 0.6.6
+Fixed:
+- don't insert spaces into designators, it just confuses users (sf bug
+ 898087)
+- Eudora can't handle utf-8 headers. We love Eudora. (sf bug 900046)
+- fixed bug in args to new DateHTMLProperty in the local() method (sf bug
+ 901444)
+- fixed registration (sf bug 903283)
+- also changed rego to not use a 302 during confirmation, as this seems to
+ confuse some email clients or browsers.
+
+
+2004-02-16 0.6.5
+Fixed:
+- mailgw handling of subject-line errors
+- allow serving of FileClass file content when the class isn't called
+ "file" (eg. messages and other FileClasses)
+- allowed negative ids (ie. new item markers) in HTMLClass.getItem,
+ allowing "db/file_with_status/-1/status/menu" to generate a useful
+ widget
+- fixed content-type when templates are serving up xml (thanks Godefroid
+ Chapelle)
+- fixed IE double-submit when it shouldn't (sf bug 842254)
+- fixed check for JS pop()/push() to make more general (sf bug 877504)
+- fix re-enabling queries (sf bug 861940)
+- use supplied content-type on file uploads before trying filename)
+- fixed roundup-reminder script to use default schema (thanks Klamer Schutte)
+- fixed edit action / parsePropsFromForm to handle index-page edits better
+- safer logging from HTTP server (sf bug 896917)
+
+
+2003-12-17 0.6.4
+Fixed:
+- fixed date arithmetic to not allow day-of-month == 0 (sf bug 853306)
+- fixed date arithmetic to limit hours-per-day to 24, not 60
+- hard-coded python2.3-ism (socket.timeout) fixed
+- fixed activity displaying as future because of Date arithmetic fix in 0.6.3
+ (sf bug 842027).
+- fix Windows service mode for roundup-server (sf bug 819890)
+- fixed #white in cgitb (thanks Henrik Levkowetz)
+
+
+2003-11-14 0.6.3
+Fixed:
+- fixed detectors fix incorrectly fixed in bugfix release 0.6.2
+- added note to upgrading doc for detectors fix in 0.6.2
+- added script to help migrating queries from pre-0.6 trackers
+- fixed "documentation" of getnodeids in roundup.hyperdb
+- added flush() to DevNull (sf bug 835365)
+- fixed javascript for help window for only one checkbox case
+- date arithmetic was utterly broken, and has been for a long time.
+ Date +/- Interval now works, and Date - Date also works (produces
+ an Interval.
+- handle socket timeout exception (thanks Marcus Priesch)
+- fixed retirement of items in rdbms imports (sf bug 841355)
+- fixed bug in looking up journal of newly-created items in *dbm backends
+
+
+2003-09-29 0.6.2
+Fixed:
+- cleaned up, clarified internal caching API in *dbm backends
+- stopped pyc writing to current directory! yay! (patch 800718 with changes)
+- fixed file leak in detector initialisation (patch 800715)
+- commented out example tracker homes (patch 800720)
+- added note about hidden :template var in user.item (bug 799842)
+- fixed Apply Error that was raised, when property was deleted from class and
+ we are trying to edit an instance
+
+
+2003-08-31 0.6.1
+Fixed:
+- Add note about installing cgi-bin with a different interpreter
+- Importing wasn't setting None values explicitly when it should have been
+- Fixed import warning regarding 0xffff0000 literal, finally, really this
+ time. Checked on win2k. (sf bug 786711)
+- fix CGI editCSV action to handle metakit's integer itemids
+- apply fix for "remove" links from Klamer Schutte
+- added permission check on "remove" link while I was there..
+- applied CSV fix for python2.3 (sf bug 790363)
+- fixed form padding in LHS menu (sf bug 790502)
+- fixed upgrading docs for timezones (sf bug 790498)
+- set the content type on page templates (can have XML templates now)
+- various cosmetic fixes (thanks James Kew for being persistent :)
+- applied patch 739314 (sorry John!)
+
+
+2003-08-08 0.6.0
+- Fixed editing attributes on FileClass nodes.
+- Query editing now works correctly (sf bug 621248)
+- roundup-server now logs IP addresses by default (sf bug 778795)
+- logfile must be specified if pidfile is (sf bug 772820)
+- timelog editing via csv interface crashes (sf bug 699837)
+- sort multilinks a little better for grouping (sf bug 772935)
+- batch the (list) listings at 500 entries per page (sf bug 759906)
+- don't have RDBMS backends list retired nodes (sf bug 767319)
+- fix file downloading
+- add action attribute to issue.item form tag
+
+
+2003-07-29 0.6.0b4
+- plugged cross-site-scripting hole (thanks Jeff Epler)
+- handle deprecation of FCNTL in python2.2+ (sf bug 756756)
+- handle missing Subject: line (sf bug 755331)
+- fix New User creation (sf bug 754510)
+- fix hackish message escaping (sf bug 757128)
+- fix :required ordering problem (sf bug 740214)
+- audit some user properties for valid values (roles, address) (sf bugs
+ 742968 and 739653)
+- fix HTML file detection (hence history xref linking) (sf bug 741478)
+- session database caches it's type, rather than calling whichdb each time
+ around.
+- changed rdbms_common to fix sql backends for new Boolean types under Py2.3
+
+
+2003-06-10 0.6.0b3
+Fixed:
+- cgi client was broken during b2 fixing
+
+
+2003-06-09 0.6.0b2
+Feature:
+- added the start/stop/restart/condstart/status roundup-server control
+ script
+
+Fixed:
+- handle non-existant demo dir (thanks Ollie Rutherfurd)
+- strip whitespace from Role names so "User, Admin" will work
+- fixed template searching on Windows (thanks J Vickroy)
+
+
+2003-05-09 0.6.0b1
+Removed:
+- having served its purpose as a template for other relational database
+ implementations, the gadfly backend has now been removed from the Roundup
+ distribution.
+
+Feature:
+- new instant-gratification Demo Mode
+- support setting of properties on message and file through web and
+ email interface (thanks John Rouillard)
+- allow additional control over the roundupdb email sending (explicit
+ cc addresses, different from address and different nosy list property)
+ (thanks John Rouillard)
+- applied patch for nicer history display (sf feature 638280)
+- cleaning old unused sessions only once per hour, not on every cgi
+ request. It is greatly improves web interface performance, especially
+ on trackers under high load
+- added mysql backend (see doc/mysql.txt for details)
+- switch metakit to use "compressed" multilink journal change representation
+- metakit now handles "unset" for most types (not Number and Boolean)
+- fixed bug in metakit search-by-ID
+- added ability to display localized dates in web interface. User input is
+ convered to GMT (see doc/upgrading.txt).
+- added a form to show a specific issue
+- more proper sorting/grouping on mulitilink properties. Sorting is performed
+ not only by number of links, but also by links itself. This makes usable
+ grouping e.g. by topic multilink
+- add "ago" to intervals in the past (sf bug 679232)
+- included UN*X manual pages from Bastian Kleineidam
+- implemented extension to form parsing to allow editing of multiple items
+ and creation of multiple items (but only one per class)
+- the colon ":" special form variable designator may now be any of : + @
+- trackers' templates directory can contain subdirectories with static files
+ (e.g. images). They are accessible naturally: _file/images/img.gif
+- altered Class.create() and FileClass.create() methods to make "content"
+ property available in auditors
+- can now configure CC to author only for messages creating issues (sf
+ feature 625808)
+- registration is now a two-step process, with confirmation from the email
+ address supplied in the registration form
+- added password reset feature for forgotten password / login
+- added support for last-modified and if-modified-since headers for static
+ file serving
+- added Node.get() method
+- nicer page titles (sf feature 65197)
+- relaxed CVS importing (sf feature 693277)
+- added support for searching on ranges of dates and intervals (see
+ doc/user_guide.txt in chapter "Searching Page" for details) (closes sf
+ feature 700178)
+- role names made case insensitive
+- added ability to restore retired nodes
+- more lenient date input and addition Interval input support (sf bug 677764)
+- roundup mailgw now handles apop
+- implemented ability to search for multilink properties with no value
+- Class.find() may now find unset Links (sf bug 700620)
+- more flexibility in classhelp link labelling (sf feature 608204)
+- added command-line functionality for roundup-admin (sf feature 687664)
+- added nicer popup windows for topic, nosy, etc (has add/remove buttons)
+ thanks Gus Gollings
+- HTML templating files now have a .html extension
+- Roundup templates are now distributed much more sanely, allowing for
+ 3rd-party templates.
+- extended date syntax to make range searches even more useful
+- SMTP login and TLS support added (sf bug 710853 with extras ;)
+ Note: requires python 2.2+
+- added Windows Service mode for roundup-server when daemonification is
+ attempted on Windows.
+- sort HTMLClass.properties results by name (sf feature 724738)
+- nicer index navigation (sf feature 676866)
+
+Fixed:
+- applied unicode patch. All data is stored in utf-8. Incoming messages
+ converted from any encoding to utf-8, outgoing messages are encoded
+ according to rfc2822 (sf bug 568873)
+- fixed layout issues with forms in sidebar
+- fixed timelog example so it handles new issues (sf bug 678908)
+- handle missing os.fork() (sf bug 681046)
+- added warning filter for "FutureWarning: hex/oct constants > sys.maxint will
+ return positive values..." (literal 0xffff0000 in portalocker.py)
+- fixed ZPT code generating SyntaxWarning for assignment to None
+- open static files using binary mode (sf bug 693208)
+- fixed deja-vu bug 692910
+- don't display "Editing" on read-only pages (sf bug 651967)
+- re-worked detectors initialisation - woohoo, no more cross-importing!
+- fixed export/import of retired nodes (sf bug 685273)
+- remember the display template specified during edit (sf bug 701815)
+- added example HTML tempating for vacation flag (sf bug 701722)
+- finally, tables autosize columns (sf bug 609070)
+- added creation to index columns (sf bug 708247)
+- fixed missing (pre-commit) journal entries in *dbm backends (sf bug 679217)
+- URL cited in roundup email confusing dumb Email clients (sf bug 716585)
+- set title on issues even when the email body is empty (sf bug 727430)
+- under the heading of "questionable whether it's a fix or not"
+ (sf "bug" 621226 for the users of the "standards compliant" browser IE)
+
+
+2003-05-08 0.5.7
+- fixed Interval maths (sf bug 665357)
+- fixed sqlite rollback/caching bug (sf bug 689383)
+- fixed rdbms table update detection logic (sf bug 703297)
+- fixed detection of bad date specs (sf bug 691439)
+- required String properties not being flagged (thanks Ajit George)
+- only look for CSV files when importing (thanks Dan Grassi)
+- can now unset values in CSV editing (sf bug 704788)
+- fixed rdbms email address lookup (case insensitivity)
+- email file attachments added to issue files list (sf bug 711501)
+- added socket timeout to attempt to prevent stuck processes (sf bug 665487)
+- email registered users shouldn't be able to log in (sf bug 714673)
+- handle missing addresses on users (sf bug 724537)
+
+
+2003-02-27 0.5.6
+- fixed templating filter function arguments (sf bug 678911)
+- fixed multiselect in searching (sf bug 676874)
+- fixed parsing of content-disposition filenames (sf bug 675116)
+- added 'h' to roundup-server optarg list (sf bug 674070)
+- fixed doc for db.history in anydbm and rdbms_common (sf bug 679221)
+- fixed roundup-reminder (sf bug 681042)
+- fixed int assumptions about Number values (sf bug 677762)
+- clarified licensing
+- another attempt to fix cookie misbehaviour - customise cookie name using
+ tracker name
+- fixed error in indexargs_url (thanks Patrick Ohly)
+- fixed getnode (sf bug 684531)
+- fixed args to some date templating methods (sf bug 689670)
+- fixed database corruption in rdbms property mutation
+
+
+2003-01-24 0.5.5
+- fixed rdbms searching by ID (sf bug 666615)
+- fixed metakit searching by ID
+- detect corrupted index and raise semi-useful exception (sf bug 666767)
+- open server logfile unbuffered
+- revert StringHTMLProperty to not hyperlink text by default
+- fixes to CGI form handling
+- fix unlink bug in metakit backend
+- fixed hyperlinking ambiguity (sf bug 669777)
+- fixed cookie path to use TRACKER_WEB (sf bug 667020) (thanks Nathaniel Smith
+ for helping chase it down and Luke Opperman for confirming fix)
+
+
+2003-01-10 0.5.4
+- key the templates cache off full path, not filename
+- implemented whole-database locking
+- hyperlinking of special text (url, email, item designator) in messages
+- fixed time default in date.py
+- fixed error in cgi/templates.py (sf bug 652089)
+- fixed handling of missing password (sf bug 655632)
+- applied patches for handling Outlook quirks (thanks Andrey Lebedev)
+ (multipart/alternative, "fw" and content-type "name")
+- fire auditors and reactors in rdbms retire (thanks Sheila King)
+- better match for mailgw help "command" text
+- handle :add: better in cgi form parsing (sf bug 663235)
+- handle all-whitespace multilink values in forms (sf bug 663855)
+- fixed searching on date / interval fields (sf bug 658157)
+- fixed form elements names in search form to allow grouping and sorting
+ on "creation" field
+- display of saved queries is now performed correctly
+
+
+2002-12-11 0.5.3
+- added mention of how to give users multiple Roles
+- mention needed trailing "/" in TRACKER_WEB
+- fixed upgrading doc to have CGI changes in the correct order
+- fixed double-close of anydbm backend (sf bug 639030)
+- removed use of string/strop from TAL/TALInterpreter
+- handle KeyboardInterrupt nicely
+- fixed Date and Interval form value handling
+- fixed Date.local()
+- email quoted text stripping is controllable again (sf bug 650742)
+- extract attachment name from content-disposition if name is missing (sf
+ bug 637278)
+- removed FILTER_POSITION from bundled configs
+- reverse message listing in issue display (reversion of recent change)
+- bad entries for multilink editing in cgi don't traceback now (sf bug 640310)
+- detect and break email loops (sf bug 640854)
+- finished of handling of retired flag in filter() (sf bug 635260)
+- allow StringHTMLProperty in MultilinkHTMLProperty test to work
+- don't set explicit None Link properties in web create
+- fixed nasty sorting bug that was lowercasing properties
+- allow multiple :remove and :add elements per property being edited
+- added date header to emails (sf bug 651358)
+
+
+2002-11-07 0.5.2
+- added quotes around python interpreter in windows bat (sf bug 623963)
+- fixed link at end of installation doc (sf bug 623957)
+- handle "classname" URL path errors cleaner (generate a 404)
+- added CGI :remove:<propname> and :add:<propname> which specify item ids to
+ remove / add in <propname> multilink.
+- bugfix in boolean templating
+- remember the change note on bad submissions (sf bug 625989)
+- highlight required form fields (sf bug 625989)
+- force non-word boundary to match re: in subject (sf bug 626303)
+- handle sqlite bug (<2.7.2) (sf bug 630828)
+- handle missing props in anydbm stringFind
+- updated email package address formatting (deprecation)
+- copied email address quoting from email v2.4.3 so we're consistent with 2.2
+- email summary extraction now takes the first whole sentence or line -
+ whichever is longer
+- documented dependency on Active State (sf bug 623959)
+- ensured there's no zero-length files in source (sf bug 633622)
+- added ID to the search page (sf bug 631601)
+- fixed filtering by id in anydbm
+- show issue ID in the headings (sf bug 631598)
+- show entire messages by default in issues (sf bug 625995)
+- fixed journalling to save old values instead of new (sorry it took so long GM)
+- handle missing REQUEST_URI for cgi-bin users (sf bug 620163)
+
+
+2002-10-16 0.5.1
+- highlight rows in groups of three
+- metakit cleanups
+- nicer "navigation" style in index views
+- handle missing Link values in anydbm backend set() operation
+- fixed filter() with no sort/group (sf bug 618614)
+- fixed register with no session (sf bug 618611)
+- fixed log / pid file path handling in roundup-server (sf bug 617981)
+- fixed old gadfly compatibiltiy problem, for sure this time (sf bug 612873)
+- https URLs from config now recognised as valid (sf bug 619829)
+- nicer display of tracker list in roundup-server (sf bug 619769)
+- fixed some missed renaming instance -> tracker (sf bug 619769)
+- allow blank passwords again (sf bug 619714)
+- expose the tracker config as a variable for templating
+- homogenise newlines in CGI text submissions (sf bug 614072)
+- merged Zope Collector #372 fix from ZPT CVS trunk
+- fixed history to display username instead of userid
+- shipped templates didn't import all hyperdb types in dbinit.py
+- fixed bug in Interval serialisation
+- handle "unset" status in status auditor (sf bug 621250)
+- issues in 'done-cbb' are now also moved to 'chatting' on new messages
+- implemented the missing Interval.__add__
+- added ability to implement new templating utility methods
+- expose the Date.pretty method to templating
+- made form table cell alignment consistent (sf bug 621887)
+- include stylesheet in docs (sf bug 623183)
+- store PIPE messages so we can re-send them on errors (sf bug 623082)
+- implemented "retire" cgi action, added to user index (sf bug 618612)
+- included doc ideas from Bernhard Reiter (sf feature 621941)
+
+
+2002-10-02 0.5.0
+- fixed style for alternating rows in user lists
+- fixed query edit form so it doesn't barf
+- #617133 ] 0.5.0pr1 uses nonexistent renderTemplate
+- merged Zope Collector #539 fix from ZPT CVS trunk
+
+
+2002-09-27 0.5.0 pr1
+- handling of None for Date/Interval/Password values in export/import
+- handling of journal values in export/import
+- password edit now has a confirmation field
+- registration error punts back to register page
+- gadfly backend now handles changes to the schema - but only one property
+ at a time
+- cgi.client base URL is now obtained from the config TRACKER_WEB
+- request.url has gone away - there's too much magic in trying to figure
+ what it should be
+- cgi-bin script redirects to https now if the request was https
+- FileClass "content" property wasn't being returned by getprops() in most
+ backends
+- we now verify instance attributes on instance open and throw a useful error
+ if they're not all there
+- sf 611217 ] menu() has problems when labelprop==None
+- verify contents of tracker module when the tracker is opened
+- many performance improvements in *dbm and sql backends
+- mailgw was missing an "import sys"
+- setup now installs scripts with python -O flag, doubling performance in some
+ cases (there's a lot of __debug__ use)
+- fix :required for Link menus
+- import wasn't setting the ID to maxid+1
+- added getItem to HTMLClass so you can access arbitrary items in templates
+- index filtering form values may now be key values too
+- replaced the content() callback ickiness with Page Template macro usage
+- changed the default CSS style to be less offensive to some ;)
+- better handling of Page Template compilation errors
+- handle multiple unrelated indexed classes
+- #614188 ] Exception in mailgw.py
+- #613310 ] traceback on onexistant items
+- #613291 ] typos in nosy list
+- handle stupid mailers that QUOTE their Re; 'Re: "[issue1] bla blah"'
+- giving a user a Role that doesn't exist doesn't break stuff any more
+- revamped user guide, customisation guide, added maintenance guide
+- merge Zope Collector #538 fix from ZPT CVS trunk (path expressions with a
+ non-path final alternate no longer try to call a value returned by that
+ alternate)
+- merge Zope Collector #573 fix from ZPT CVS trunk
+- merge Zope Collector #580 fix from ZPT CVS trunk
+- added "crypt" password encoding and ability to set password with
+ already encrypted password through roundup-admin
+- fixed the mailgw so that anonymous users may still access it
+- add hook to allow external password verification, overridable in the
+ tracker interfaces module
+- fixed login attempt by user that doesn't exist
+
+
+2002-09-13 0.5.0 beta2
+- all backends now have a .close() method, and it's used everywhere
+- fixed bug in detectors __init__
+- switched the default issue item display to only show issue summary
+ (added instructions to doc to make it display entire content)
+- MANIFEST.in was missing a lot of template files
+- added generic item editing
+- much nicer layout of template rendering errors
+- added context/is_edit_ok and context/is_view_ok convenience methods and
+ implemented use of them in the classic template
+
+
+2002-09-11 0.5.0 beta1
+Fixed:
+- #576086 ] dumb copying mistake (frontends/ZRoundup.py)
+- installation instructions now mention "python2" in "testing your python".
+- made the unit tests run again - they were quite b0rken
+- #571170 ] gdbm deadlock
+- #576241 ] MultiLink problems in parsePropsFromForm
+- fixed the date module so that Date(". - 2d") works
+- web forms may now unset Link values (like assignedto)
+- cleanup: moved roundup.templatebuilder to roundup.templates.builder
+- instance __init__ no longer silently traps dbinit import errors
+
+Feature:
+- new backend for metakit (thanks Gordon McMillan)
+- new backend for gadfly (it's as done as it's going to get)
+- further split the dbm backends from the core code, allowing easier
+ non-dict-like backends (eg metakit, RDB)
+- implemented and used the new access control mechanisms (Permissions, Roles)
+ (see doc/security.txt)
+- switched templating to use Zope's PageTemplates (yay!)
+- switched to sessions for web authentication
+- added Boolean and Number types
+- fixed the journal bloat
+- updated design document for new access controls
+- updated customisation document, including more examples
+- entire database export and import (incl files)
+- better mailgw help message (feature request #558562)
+- re-enabled link backrefs from messages (feature request #568714)
+- the page layout is now templatable
+- re-worked cgi interface to abstract out the explicit "issue" interface
+- have index page handle mid-page errors better so header and footer are
+ still visible
+- we handle "not found", access and item page render errors better
+- fixed double-submit by having new-item-submit redirect at end
+- daemonify roundup-server (fork, logfile, pidfile)
+- modify cgitb to display PageTemplate errors better
+- rename to "instance" to "tracker"
+- have roundup.cgi pick up tracker config from the environment
+- revamped look and feel in web interface
+- cleaned up stylesheet usage
+- several bug fixes and documentation fixes
+- added is_retired test to hyperdb.Class
+- added capability to save queries:
+ - a query Class with name, klass (to search) and url (query string)
+ properties
+ - a Multilink to query on user called queries
+ - html templates for query, and a list of queries in user.item
+ - search form has Save button & name input
+ - saved queries put in menu in pagehead
+ - for migration, none of the above is required and old behavior preserved.
+ - showquery translates search form <-> query string
+- cleaned up the indexer code:
+ - it splits more words out
+ - removed code we'll never use (roundup.roundup_indexer has the full
+ implementation, and replaces roundup.indexer)
+ - only index text/plain and rfc822/message (ideas for other text formats to
+ index are welcome)
+ - added simple unit test for indexer. Needs more tests for regression.
+ - all String properties may now be indexed too. Currently there's a bit of
+ "issue" specific code in the actual searching which needs to be
+ addressed. In a nutshell:
+ + pass 'indexme="yes"' as a String() property initialisation arg, eg:
+ file = FileClass(db, "file", name=String(), type=String(),
+ comment=String(indexme="yes"))
+ + the comment will then be indexed and be searchable, with the results
+ related back to the issue that the file is linked to
+ - as a result of this work, the FileClass has a default MIME type that may
+ be overridden in a subclass, or by the use of a "type" property as is
+ done in the default templates.
+ - the regeneration of the indexes (if necessary) is done once the schema is
+ set up in the dbinit.
+ - new "reindex" command in roundup-admin used to force regeneration of the
+ index
+- added email display function - mangles email addrs so they're not so easily
+ scraped from the web
+- switched to using a session-based web login
+- made mailgw handle set and modify operations on multilinks (bug #579094)
+- fixed the journal bloat from multilink changes - we just log the add or
+ remove operations, not the whole list
+
+
+2002-06-24 0.4.2
+Fixed:
+- Cleaned up the hyperdb unit tests.
+- Applied patch from Andrew W. Nosenko to give nicer Unauthorised message
+ when anonymous user tries to edit. Should've been applied in 0.4.2pr1. Oops.
+- Added more detailed note to MIGRATION regarding the detectors changes.
+
+
+2002-06-19 0.4.2pr1
+Feature:
+- added a "detectors" directory for people to put their useful auditors and
+ reactors in. Note - the roundupdb.IssueClass.sendmessage method has been
+ split and renamed "nosymessage" specifically for things like the nosy
+ reactor, and "send_message" which just sends the message.
+- link() htmltemplate function now has a "showid" option for links and
+ multilinks. When true, it only displays the linked node id as the anchor
+ text. The link value is displayed as a tooltip using the title anchor
+ attribute.
+ To use in eg. the superseder field, have something like this:
+ <td>
+ <display call="field('superseder', showid=1)">
+ <display call="classhelp('issue', 'id,title', label='list', width=500)">
+ <property name="superseder">
+ <br>View: <display call="link('superseder', showid=1)">
+ </property>
+ </td>
+- stripping of the email message body can now be controlled through the
+ config variables EMAIL_KEEP_QUOTED_TEXT and EMAIL_LEAVE_BODY_UNCHANGED.
+- all database files created are now group readable and writable.
+- added option to automatically add the authors and recipients of messages
+ to the nosy lists with the options ADD_AUTHOR_TO_NOSY (default 'new') and
+ ADD_RECIPIENTS_TO_NOSY (default 'new'). These settings emulate the current
+ behaviour. Setting them to 'yes' will add the author/recipients to the nosy
+ on messages that create issues and followup messages.
+- reverting to dates for intervals > 2 months sucks
+- changed the default message list in issues to display the message body
+- applied patch #558876 ] cgi client customization
+- split instance initialisation into two steps, allowing config changes
+ before the database is initialised.
+- don't create an empty message on email issue creation if the email is empty
+- may now display additional fields in Multilink form menus
+- #541941 ] changing multilink properties by mail
+- #526730 ] search for messages capability
+- #505180 ] split MailGW.handle_Message
+ - also changed cgi client since it was duplicating the functionality
+
+Fixed:
+- stop sending blank (whitespace-only) notes
+- cleanup of serialisation for database storage
+- node ids are now generated from a lockable store - no more race conditions
+- sorting was applied to all nodes of the MultiLink class instead of
+ to the nodes that are actually linked to in the "field" template
+ function. This adds about 20+ seconds in the display of an issue if
+ your database has a 1000 or more issues in it.
+- added missing documentation for a few of the config option values
+- file upload broke if you didn't supply a change note
+- fixed SCRIPT_NAME in ZRoundup for instances not at top level of Zope
+ (thanks dman)
+- fixed some sorting issues that were breaking some unit tests under py2.2
+- mailgw test output dir was confusing the init test (but only on 2.2 *shrug*)
+- node caching now works, and gives a small boost in performance
+- #449374 ] re-enable bsddb3 backend
+ bsddb3 backend now works, reinstating
+- #551483 ] assignedto in Client.make_index_link
+- made backends.__init__ be more specific about which ImportErrors it really
+ wants to ignore
+- fixed the example addresses in the templates to use correct example domains
+- cleaned out the template stylesheets, removing a bunch of junk that really
+ wasn't necessary (font specs, styles never used) and added a style for
+ message content
+- build htmlbase if tests are run using CVS checkout
+- #565979 ] code error in hyperdb.Class.find
+- #565996 ] The "Attach a File to this Issue" fails
+- #564271 ] find() and new properties
+- #562130 ] cookie path generated from ZRoundup was wrong in some situations
+- remove CR characters embedded in messages (ZRoundup)
+- properly quote the email address and "real name" in all situations using the
+ 'email' module if it is available and 'rfc822' otherwise
+- #565992 ] if ISSUE_TRACKER_WEB doesn't have the trailing '/', add it
+- use the rfc822 module to ensure that every (oddball) email address and
+ real-name is properly quoted
+- #558867 ] ZRoundup redirect /instance requests to /instance/
+- #569415 ] {version}
+- #569178 ] type error
+ was fixed as part of the general cleanup of reactors
+
+
+2002-03-25 - 0.4.1
+Feature:
+- use blobfiles in back_anydbm which is used in back_bsddb.
+ change test_db as dirlist does not work for subdirectories.
+ ATTENTION: blobfiles now creates subdirectories for files.
+- add module blobfiles in backends with file access functions.
+- roundup db catch only IOError in getfile.
+- roundup db catches retrieving not existing files.
+- #503204 ] mailgw needs a default class
+ - partially done - the setting of additional properties can wait for a
+ better configuration system.
+- Alternate email addresses are now available for users. See the MIGRATION
+ file for info on how to activate the feature.
+- #511168 ] Web interface: Adding new products
+ Classes that don't provide template html get a default edit interface now:
+ - access using the admin "class list" interface
+ - limited to admin-only
+ - requires the csv module from object-craft (url given if it's missing)
+- Added popup help for classes using the classhelp html template function.
+ - add <display call="classhelp('priority', 'id,name,description')">
+ to an item page, and it generates a link to a popup window which displays
+ the id, name and description for the priority class. The description
+ field won't exist in most installations, but it will be added to the
+ default templates.
+- #517734 ] web header customisation is obscure
+- All messages sent to the nosy list are now encoded as
+ quoted-printable before they are sent.
+- Fixed display of mutlilink properties when using the template
+ functions, menu and plain.
+
+Fixed:
+- Clean up mail handling, multipart handling.
+- respect encodings in non multipart messages.
+- makeHtmlBase: re.sub under python 2.2 did not replace '.', string.replace
+ does it.
+- preamble in tepmlateBuilder mentioned htmldata
+- mailgw checks encoding on first part too.
+- #511586 ] unittest FAIL: testReldate_date
+- Added a uniquely Roundup header to email, "X-Roundup-Name"
+- All forms now have "double-submit" protection when Javascript is enabled
+ on the client-side.
+- #516883 ] mail interface + ANONYMOUS_REGISTER
+- #516854 ] "My Issues" and redisplay
+- #517906 ] Attribute order in "View customisation"
+- #514854 ] History: "User" is always ticket creator
+- wasn't handling cvs parser feeding correctly
+- fixed some problems in date calculations (calendar.py doesn't handle over-
+ and under-flow). Also, hour/minute/second intervals may now be more than
+ 99 each.
+- #527416 ] roundup-admin uses undefined value
+- #527503 ] unfriendly init blowup when parent dir
+ (also handles UsageError correctly now in init)
+- #524129 ] roundup-admin gets python path wrong
+
+
+2002-01-24 - 0.4.0
+Feature:
+- much nicer history display (actualy real handling of property types etc)
+- journal entries for link and mutlilink properties can be switched on or
+ off
+- properties in change note are now sorted
+- you can now use the roundup-admin tool pack the database
+
+Fixed:
+- the mail gateway now responds with an error message when invalid values
+ for arguments are specified for link or mutlilink properties
+- modified unit test to check nosy and assignedto when specified as arguments
+- handle attachments with no name (eg tnef)
+- fixed setting nosy as argument in subject line
+- fixed back_bsddb so it passed the journal tests
+- fixed status changes in mail gateway (eg. unread -> chatting)
+- we'll actually distribute the frontends directory now, as advertised...
+- handle stripping of "AW:" from subject line
+- htmltemplate list() wasn't sorting...
+- unit tests for html templating (and re-enabled the listbox field for
+ multilinks)
+- allow abbreviation of "help" in admin tool too.
+- run_tests testReldate_date failed if LANG is 'german'
+- mailgw failures (unexpected ones) are forwarded to the roundup admin
+
+
+2002-01-16 - 0.4.0b2
+Fixed:
+- #495392 ] empty nosy -patch
+- #500574 ] messageid must have format <part1 at part2>
+- fixed some problems with web editing and change detection
+- mail splitting wasn't detecting responses in the same "section" as quoted
+ text
+- missed a "from i18n import _" in date.py
+- #501690 ] MIGRATION.txt incomplete
+- #502342 ] pipe interface
+- #502437 ] rogue reactor and unittest
+- re-enabled dumbdbm when using python >2.1.1 (ie 2.1.2, 2.2)
+- changed all config accesses so they access either the instance or the
+ config attriubute on the db. This means that all config is obtained from
+ instance_config instead of the mish-mash of classes. This will make
+ switching to a ConfigParser setup easier too, I hope.
+- #502951 ] adding new properties to old database
+- #502953 ] nosy-like treatment of other multilinks
+- #503164 ] create and passwords
+- plain rendering of links in the htmltemplate now generate a hyperlink to
+ the linked node's page.
+- #503330 ] ANONYMOUS_REGISTER now applies to mail
+- #503353 ] setting properties in initial email
+- #502956 ] filtering by multilink not supported
+- #503340 ] creating issue with [asignedto=p.ohly]
+- #502949 ] index view for non-issues and redisplay
+- #503793 ] changing assignedto resets nosy list
+- lots of date/interval related changes:
+ - more relaxed date format for input
+ - handle None for date/interval properties
+
+
+2002-01-08 - 0.4.0b1
+Feature:
+- Added INSTANCE_NAME to configuration - used in web and email to identify
+ the instance.
+- Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
+ signature info in e-mails.
+- Some more flexibility in the mail gateway and more error handling.
+- Login now takes you to the page you back to the were denied access to.
+- Admin user now can has a user index link on their web interface.
+- We now have basic transaction support. Information is only written to
+ the database when the commit() method is called. Only the anydbm and
+ bsddb3 backends are modified in this way - the bsddb3 backend needs a
+ lot more work anyway...
+ - the CGI and mailgw automatically commit() at the end of processing a
+ single transaction
+ - the admin tool requires an explicit "commit" - it will prompt at exit
+ if there are unsaved changes. A "rollback" removes all changes made
+ during the session (up to the last commit).
+- Added the "display" command to the admin tool - displays a node's values
+- Message author's name appears in From: instead of roundup instance name
+ (which still appears in the Reply-To:)
+- Added a Zope frontend for roundup.
+- Centralised the python version check code, bumped version to 2.1.1 (really
+ needs to be 2.1.2, but that isn't released yet :)
+- much better attaching of erroneous messages in the mail gateway
+- #496356 ] Use threading in messages
+ This adds the tracking of messages by message-id and allows threading
+ using in-reply-to. Most e-mail clients support threading using this
+ feature, and we hope to add support for it to the web gateway.
+
+Fixed:
+- Lots of bugs, thanks Roché and others on the devel mailing list!
+- login_action and newuser_action return values were being ignored
+- Woohoo! Found that bloody re-login bug that was killing the mail
+ gateway.
+- Fixed login/registration forwarding the user to the right page (or not,
+ on a failure)
+- We now use weakrefs in the Classes to keep the database reference, so
+ the close() method on the database is no longer needed.
+- #487480 ] roundup-server
+- #487476 ] INSTALL.txt
+- #489760 ] [issue] only subject
+- fixed doc/index.html to include the quoting in the mail alias.
+- fixed the backends __init__ so we can pydoc the backend modules
+- web i/f reports "note added" if there are no changes but a note is entered
+- we were assuming database files created by anydbm had the same name, but
+ this is not the case for dbm. We now perform a much better check _and_
+ cope with the anydbm implementation module changing too!
+- envelope-from is now set to the roundup-admin and not roundup itself so
+ delivery reports aren't sent to roundup (thanks Patrick Ohly)
+- #495400 ] entering blanks
+ Values with spaces are now accepted in roundup-admin - check the long help
+ for details.
+- #496360 ] table width does not work
+- detectors were being registered multiple times
+- added tests for mailgw
+
+
+2001-11-23 - 0.3.0
+Feature:
+- #467129 ] Lossage when username=e-mail-address
+- #473123 ] Change message generation for author
+- MailGW now moves 'resolved' to 'chatting' on receiving e-mail for an issue.
+- Added Structured Text rendering to htmltemplate, thanks Brad Clements.
+- Added CGI configuration via env vars (see roundup.cgi for details)
+- "roundup.cgi" is now installed to "<python-prefix>/share/roundup/cgi-bin"
+- roundup-admin now accepts abbreviated commands (eg. l = li = lis = list)
+- roundup-mailgw now supports unix mailbox and POP as sources of mail.
+- roundup-admin now handles all hyperdb exceptions
+- users may attach files to issues (and support in ext) through the web now
+- incorporated patch from Roch'e Compaan implementing attachments in nosy
+ e-mail
+- added a target version field to the extended issue schema
+- added dummy hooks for I18N and some preliminary (test) markup of
+ translatable messages
+
+Fixed:
+- Fixed a bug in HTMLTemplate changes.
+- 'unread' to 'chatting' automagic status change was b0rken.
+- Anonymous user lockout wasn't working.
+- roundup-server now works on Windows, thanks Juergen Hermann.
+- Fixed install documentation, also thanks Juergen Hermann.
+- Fixed some URL issues in roundup.cgi, again thanks Juergen Hermann.
+- bug #475347 ] WindowsError still not caught (patch from Juergen Hermann)
+- bug #474749 ] indentations lost
+- bug #477104 ] HTML tag error in roundup-server
+- bug #477107 ] HTTP header problem
+- bug #477687 ] conforming html
+- bug #474372 ] Netscape 4.77 do not render Support form
+- bug #477685 ] base64.decodestring breaks
+- bug #477837 ] lynx does not like the cookie
+- bug #477892 ] Password edit doesn't fix login cookie
+- newuser_action now presents error messages rather than tracebacks.
+- bug #479511 ] mailgw to pop
+- bug #479508 ] roundup-admin crash on wrong class
+- bad error report in hyperdb
+- roundup.mailgw now handles errors on the set() and create() at the end
+ of processing
+- roundup.mailgw also handles messages that are passed to it that don't
+ contain a From: line - apparently some POP servers can do this. It punts
+ an error message to the roundup admin.
+- fixed nosy reaction and author copy handling
+- errors in nosy reaction will be propogated now (were effectively being
+ squashed)
+- re-open the database as the author in mail handling
+- missing "return" in filter_section (thanks Roch'e Compaan)
+
+
+2001-10-23 - 0.3.0 pre 3
+Feature:
+- MailGW now moves 'unread' to 'chatting' on receiving e-mail for an issue.
+- feature #473127: Filenames. I modified the file.index and htmltemplate
+ source so that the filename is used in the link and the creation
+ information is displayed.
+ Admin Tool (roundup-admin):
+ - Interactive mode for running multiple (independant at present) commands.
+ - Tabular display of nodes.
+ - Import and export via colon-separated files.
+
+Changed:
+- re-organised the html templating code. Fixed some bugs, probably
+ introduced some more. Hopefully not too many.
+
+Fixed:
+- Stand-alone server now has a configurable setuid user.
+- CGI interface wasn't handling checkboxes at all.
+- Fixed quopri usage in mailgw from bug reports on mailing list.
+- Remove the "freshen" command from the roundup-admin tool.
+- Catch errors in login - no username or password supplied.
+- Fixed editing of password (Password property type) thanks Roch'e Compaan.
+- Fixed grouping of non-str properties thanks Roch'e Compaan.
+- bug #473121: The customisation view and filters (CGI interface view
+ customisation section may now be hidden (patch from Roch'e Compaan.)
+- bug #473122: Issue id sorting (hyperdb sorts strings-that-look-like-numbers
+ as numbers now.
+- bug #473124: UI inconsistency with Link fields.
+ This also prompted me to fix a fairly long-standing usability issue -
+ that of being able to turn off certain filters.
+- bug #473125: Paragraph in e-mails
+- bug #473126: Sender unknown
+- bug #473130: Nosy list not set correctly
+
+
+2001-10-11 - 0.3.0 pre 2
+Fixed:
+- Hyperdatabase was inserting empty strings instead of None for missing
+ property values. This broke a lot of things.
+
+
+2001-10-10 - 0.3.0 pre 1
+Feature:
+- roundup-admin create now prompts for property info if none is supplied
+ on the command-line.
+- hyperdb Class getprops() method may now return only the mutable
+ properties.
+- CGI interfaces now generate a top-level index of their known instances.
+
+Changed:
+- Login now uses cookies, which makes it a whole lot more flexible. We can
+ now support anonymous user access (read-only, unless there's an
+ "anonymous" user, in which case write access is permitted). Login
+ handling has been moved into cgi_client.Client.main()
+- The "extended" schema is now the default in roundup init.
+- The schemas have had their page headings modified to cope with the new
+ login handling. Existing installations should copy the interfaces.py
+ file from the roundup lib directory to their instance home.
+- Passwords are now encoded by default (except exising databases which
+ will only be encoded when the passwords are changed). The scheme used
+ at the moment is SHA - but the code is flexible enough to take any
+ number of encoding systems.
+- The roundup-admin tool always operates as the "admin" user now. Database
+ protection should be achieved using file system protections (see the
+ documentation for details.)
+
+Fixed:
+- Incorrectly had a Bizar Software copyright on the cgitb.py module from
+ Ping - has been removed.
+- Pretty time interval wasn't handling > 1 month properly.
+- Generation of links to Link/Multilink in indexes. (thanks Hubert Hoegl)
+- AssignedTo wasn't in the "classic" schema's item page.
+- Fixed a whole bunch of places in the CGI interface where we should have
+ been returning Not Found instead of throwing an exception.
+- Fixed a deviation from the spec: trying to modify the 'id' property of
+ an item now throws an exception.
+- The plain() template function now html-escapes the content.
+- Change message was stuffing up for multilinks with no key property.
+
+
+
+--------------
+
+2001-08-30 - 0.2.8
+Fixed:
+- Wasn't handling unguessable mime types for file uploads.
+- Missing import in mailgw.
+
+
+2001-08-29 - 0.2.7
+Feature:
+- Text searches are now case insensitive. All forms of text search use
+ regular expressions now.
+
+Fixed:
+- Had another 2.1-ism in the unit tests
+- Made the mail parser a little more robust w.r.t missing Subject:
+ (both thanks Mikhail Sobolev)
+- Missed some isFooType usages (thanks Mikhail Sobolev for spotting them)
+- Reverted back to sending change messages to the web editor of a node so
+ that the change note message is actually genrated.
+- CGI interface wasn't generating correct change messages.
+- Notes entered during a change are saved to the messages list even if
+ there's no nosy list. No message is generated if there's no nosy list and
+ there's no change note (since it would just duplicates the journal).
+- Completely removed the bsddb3 module from the tests - will be reinstated
+ when the http://bsddb.sourceforge.net/'s bugs #439959 and #456408 are
+ dealt with. One is fixed in CVS, the other pending.
+
+
+2001-08-08 - 0.2.6
+Note:
+- Roundup is now released under the same terms as the Python License.
+
+Feature:
+- Added tests for instance initialisation. No more releasing the software
+ with bugs in roundup.init!
+- Now bundling unittest with the package so that python 2.0 users can use
+ the tests.
+- Much better error handling and messages generated by the mail gateway.
+
+Fixed:
+- Implemented correct mail splitting. Added unit tests. Also snips
+ signatures now too.
+- Bug #447671 - typo in roundup/init.py
+- Changed date.Date to use regular string formatting instead of strftime -
+ win32 seems to have problems with %T and no hour... or something...
+- Bug #448484 - now catching correct exception from makedirs.
+- Instances are now opened by a special function that generates a unique
+ module name for the instances on import time.
+
+
+2001-08-03 - 0.2.5
+Note:
+- The bsddb3 module has a bug that renders it non-functional. Users should
+ select the anydbm or bsddb backend instead.
+
+Fixed:
+- Python 2.0 does not contain the unittest module. The setup.py module now
+ checks for unittest before attempting to run the unit tests.
+
+
+2001-08-03 - 0.2.4
+Features:
+- Added ability for cgi newblah forms to indicate that the new node
+ should be linked somewhere.
+- Added time logging and file uploading to the templates.
+- Added "My Issues" and "My Support" to extended template. Changed "Your
+ Details" to "My Details". Changed the "New Foo" links to "Add Foo".
+ Added links for unassigned support and issues. Generally reorganised and
+ cleanup the header up.
+- Changed the order of the information in the message generated by web edits.
+- Extended the range of intervals that are pretty-printed before actual dates
+ are displayed.
+- Added more BUILD instructions including the "clean" command to force
+ rebuild.
+- 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.
+- 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).
+
+Fixed:
+- Argument handling for the roundup-admin find command.
+- Handling of summary when no note supplied for newblah. Again.
+- Detection of no form in htmltemplate Field display.
+- Checklist html template command was setting wrong name.
+- 2.1-specific gmtime() (no arg) call in roundup.date. (thanks Paul Wright)
+- mailgw was making naughty assumptions about the schema of the classes it
+ was creating nodes for.
+- remove the $Foo$ from the HTML files stored in the htmlbase modules.
+- 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-07-30 - 0.2.3
+Big change:
+- I've split off the support class from the issue class in "extended".
+ Anyone who has any support entries, sorry. It should be possible to
+ write a scipt that moves the entries over pretty easily. If this causes
+ you pain, I'll do so. You'll want to update your instance with the new
+ code in "extended" either way.
+
+Features:
+- Added the unit tests to the start of setup.py so they're run whenever
+ we do anything distutils'y.
+- Added nicer prompting to the roundup-admin "init" command.
+- Actually, the roundup-admin code is totally revamped, and has command
+ help and better command-line arg handling.
+- The cgi_client.Client base class now reflects the structure of "classic"
+ rather than "extended" since "classic" is more of a "base" template.
+- Added more DB to test. Skips tests where imports fail.
+
+Fixed:
+- One of the tests in test_date had the wrong expected result.
+- Fixed IssueClass so that superseders links to its classname rather than
+ hard-coded to "issue".
+- templatebuilder was catching IOError instead of OSError.
+- The cgi_client newblah method wasn't detecting the __note form field
+ properly.
+- The History command in htmltemplate didn't handle a new node (None
+ nodeid) properly.
+
+
+2001-07-29 - 0.2.2
+Features:
+- Added implementation.txt to the doc directory. Contains implementation
+ notes specific to this implementations of Roundup.
+- Cleaned up mailgw some (subclass Message for getPart) and added some
+ tests for multipart splitting.
+- Better checking for html dir in templatebuilder.
+- Base hyperdb.Class now fakes the "id" property.
+- Made the classic roundup look more like the original prototype.
+- Made cgi_client and templating slightly more generic.
+- Moved some code around in cgi_client allowing for subclassing to change
+ behaviour.
+- Added the fabricated property "id" to all hyperdb classes.
+- Cleanup of the link label generation (new method on hyperdb.Class to do
+ it).
+
+Fixed:
+- Everything uses errno module now to check errno values.
+- New issue form handles lack of note better now.
+- HTML templating uses section-bar style for index group headers now.
+- Fixed problem in link display when Link value is None.
+- Form handling in cgi client wasn't propogating through the previous
+ query elements.
+- Fixed sort arguments generated for column headings so sorting can be
+ changed now.
+
+
+2001-07-28 - 0.2.1
+Features:
+- Added docstring to roundup package so pydoc reports useful information.
+- Added the roundup 1 software carpentry submission HTML to the doc
+ directory as "overview.html".
+
+Fixes:
+- Fixed bug in init command - templatebuilder was assuming existence of
+ "html" directory in instance home.
+- Fixed INSTALL.txt to reflect some changes in the installation and test
+ procedure. Whatdya know, "setup.py install" does the script install.
+ There you go...
+- Fixed some non-string node ids in cgi_client now that the hyperdb is
+ strict about such things.
+
+2001-07-26 - 0.2.0
+Features:
+- Major reorganisation of code to allow multiple roundup instances and a
+ single, site-packages -based installation. Also allows multiple database
+ back-ends.
+- Moved the bin/ proggies into the top dir, so that it all works
+ out-of-the-box
+- Added the "classic" template - a direct implementation of the Roundup
+ spec. Well, as close as we're going to get, anyway.
+- Added an issue priority of support to "extended"
+- Added command-line arg handling to roundup-server so it's more useful
+ out-of-the-box.
+- Added distutils-style installation of "lib" files.
+- Added some unit tests.
+
+Fixes:
+- Fixed bug in re generation in the filter
+- Fixed handling of None String property in grouped list headings
+- Fixed adding new issue with no change note
+- Fixed values in text input fields which contained quotes (") are now
+ quoted.
+- Fixed a bug in the hyperdb filter - wrong variable names in the error
+ message.
+
+2001-07-19 - 0.1.3
+- Reldate now takes an argument "pretty" - when true, it pretty-prints the
+ interval generated up to 5 days, then pretty-prints the date of last
+ activity. The issue index and item now use the pretty format.
+- Classes list for admin user in CGI interface.
+- Made the view configuration more accessible, neater and more realistic.
+- Fixed list view grouping handling grouping by a Multilink or String or Link
+ value of None or Date, ... (mind you, sorting by Date???)
+- Fixed bug in the plain formatter when a Link was None.
+- Fixed ordering of list view column headings.
+- Fixed list view column heading sort links - and limited the number of
+ columns to sort by to 2.
+- Added searching by glob to StringType filtering -
+ ^text - search for text at start of fields
+ text$ - search for text at end of fields
+ ^text$ - exactly match text in fields
+ te*xt - search for text matching "te"<any characters>"xt"
+ te?xt - search for text matching "te"<any one character>"xt"
+- Added more fields to the issue.filter and issue.index templates
+
+
+2001-07-18 - 0.1.2
+- Set default index to ?:group=priority&:columns=activity,status,title so
+ the priority column isn't displayed.
+- Thanks Anthony:
+ - added notes to the README about Python prerequisites
+ - added check to roundup.py, roundup.cgi, server.py and roundup-mailgw.py
+ for python 2+ - and made the file itself parseable by 1.5.2 ;)
+ - python 2.0 didn't have the default args for the time module functions.
+ - better handling of db directory in initDB
+- Sorting on the extra properties defined by roundupdb classes was broken
+ due to the caching used. May now sort on activity and creation
+ properties, etc.
+- Set the default index to sort on activity
+
+
+2001-07-18 - 0.1.1
+- Initial version release with consent of Roundup spec author, Ka-Ping Yee:
+ "Amazing! Nice work. I'll watch for the source code on your website."
+
+2001-07-11 - 0.1.0
+- Needed a bug tracking system. Looked around. Tried to install many
+ Perl-based systems, to no avail. Got tired of waiting for Roundup to be
+ released. Had just finished major product project, so needed something
+ different for a while. Roundup here I come...
+
+
Added: tracker/vendor/roundup/current/COPYING.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/COPYING.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,106 @@
+Roundup Licensing
+-----------------
+
+Copyright (c) 2003 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/)
+
+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.
+
+
+PageTemplates Licensing
+-----------------------
+
+Portions of this code (roundup.cgi.PageTemplates, roundup.cgi.TAL and
+roundup.cgi.ZTUtils) have been copied from Zope. They have been modified in
+the following manner:
+
+- removal of unit tests, Zope-specific code and support files from
+ PageTemplates: PageTemplateFile.py, ZPythonExpr.py, ZRPythonExpr.py,
+ ZopePageTemplate.py, examples, help, tests, CHANGES.txt, HISTORY.txt,
+ version.txt and www. From TAL: DummyEngine.py, HISTORY.txt, CHANGES.txt,
+ benchmark, driver.py, markbench.py, ndiff.py, runtest.py, setpath.py,
+ tests and timer.py. From ZTUtils: SimpleTree.py, Zope.py, CHANGES.txt and
+ HISTORY.txt.
+- editing to remove dependencies on Zope modules (see files for change notes)
+
+The license for this code is the `Zope Public License (ZPL) Version 2.0`_,
+included below.
+
+
+Zope Public License (ZPL) Version 2.0
+-------------------------------------
+
+This software is Copyright (c) Zope Corporation (tm) and
+Contributors. All rights reserved.
+
+This license has been certified as open source. It has also
+been designated as GPL compatible by the Free Software
+Foundation (FSF).
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the
+following conditions are met:
+
+1. Redistributions in source code must retain the above
+ copyright notice, this list of conditions, and the following
+ disclaimer.
+
+2. Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions, and the following
+ disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+3. The name Zope Corporation (tm) must not be used to
+ endorse or promote products derived from this software
+ without prior written permission from Zope Corporation.
+
+4. The right to distribute this software or to use it for
+ any purpose does not give you the right to use Servicemarks
+ (sm) or Trademarks (tm) of Zope Corporation. Use of them is
+ covered in a separate agreement (see
+ http://www.zope.com/Marks).
+
+5. If any files are modified, you must cause the modified
+ files to carry prominent notices stating that you changed
+ the files and the date of any change.
+
+Disclaimer
+
+ THIS SOFTWARE IS PROVIDED BY ZOPE CORPORATION ``AS IS''
+ AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+ NO EVENT SHALL ZOPE CORPORATION OR ITS CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ DAMAGE.
+
+
+This software consists of contributions made by Zope
+Corporation and many individuals on behalf of Zope
+Corporation. Specific attributions are listed in the
+accompanying credits file.
+
Added: tracker/vendor/roundup/current/ChangeLog
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/ChangeLog Sun Nov 5 21:30:25 2006
@@ -0,0 +1,918 @@
+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.7 2001/08/03 02:12:07 anthonybaxter Exp $ and $Log: ChangeLog,v $
+ Added CVS keywords $Id$ and Revision 1.7 2001/08/03 02:12:07 anthonybaxter
+ Added CVS keywords $Id$ and regenerated on Fri Aug 3 12:12:00 EST 2001
+ Added CVS keywords $Id$ 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.
+
Added: tracker/vendor/roundup/current/MANIFEST.in
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/MANIFEST.in Sun Nov 5 21:30:25 2006
@@ -0,0 +1,14 @@
+recursive-include roundup *.*
+recursive-include frontends *.*
+recursive-include scripts *.* *-*
+recursive-include tools *.*
+recursive-include cgi-bin *.cgi
+recursive-include test *.py *.txt
+recursive-include doc *.html *.png *.txt *.css *.1
+recursive-include detectors *.py
+recursive-include templates *.* home* page*
+global-exclude .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
+include locale/*.po locale/*.mo locale/roundup.pot
Added: tracker/vendor/roundup/current/README.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/README.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,39 @@
+=======================================================
+Roundup: an Issue-Tracking System for Knowledge Workers
+=======================================================
+
+Copyright (c) 2003 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/)
+
+
+INSTANT GRATIFICATION
+=====================
+
+The impatient may try Roundup immediately by typing at the console::
+
+ python demo.py
+
+To start anew (a fresh demo instance)::
+
+ python demo.py nuke
+
+Installation
+============
+For installation instructions, please see installation.txt in the "doc"
+directory.
+
+
+Upgrading
+=========
+For upgrading instructions, please see upgrading.txt in the "doc" directory.
+
+
+Usage and Other Information
+===========================
+See the index.txt file in the "doc" directory.
+
+
+License
+=======
+See COPYING.txt
Added: tracker/vendor/roundup/current/cgi-bin/roundup.cgi
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/cgi-bin/roundup.cgi Sun Nov 5 21:30:25 2006
@@ -0,0 +1,229 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: roundup.cgi,v 1.42 2005/05/18 05:39:21 richard Exp $
+
+# python version check
+from roundup import version_check
+from roundup.i18n import _
+import sys, time
+
+#
+## Configuration
+#
+
+# Configuration can also be provided through the OS environment (or via
+# the Apache "SetEnv" configuration directive). If the variables
+# documented below are set, they _override_ any configuation defaults
+# given in this file.
+
+# TRACKER_HOMES is a list of trackers, in the form
+# "NAME=DIR<sep>NAME2=DIR2<sep>...", where <sep> is the directory path
+# separator (";" on Windows, ":" on Unix).
+
+# Make sure the NAME part doesn't include any url-unsafe characters like
+# spaces, as these confuse the cookie handling in browsers like IE.
+
+# ROUNDUP_LOG is the name of the logfile; if it's empty or does not exist,
+# logging is turned off (unless you changed the default below).
+
+# DEBUG_TO_CLIENT specifies whether debugging goes to the HTTP server (via
+# stderr) or to the web client (via cgitb).
+DEBUG_TO_CLIENT = False
+
+# This indicates where the Roundup tracker lives
+TRACKER_HOMES = {
+# 'example': '/path/to/example',
+}
+
+# Where to log debugging information to. Use an instance of DevNull if you
+# don't want to log anywhere.
+class DevNull:
+ def write(self, info):
+ pass
+ def close(self):
+ pass
+ def flush(self):
+ pass
+#LOG = open('/var/log/roundup.cgi.log', 'a')
+LOG = DevNull()
+
+#
+## end configuration
+#
+
+
+#
+# Set up the error handler
+#
+try:
+ import traceback, StringIO, cgi
+ from roundup.cgi import cgitb
+except:
+ print "Content-Type: text/plain\n"
+ print _("Failed to import cgitb!\n\n")
+ s = StringIO.StringIO()
+ traceback.print_exc(None, s)
+ print s.getvalue()
+
+
+#
+# Check environment for config items
+#
+def checkconfig():
+ import os, string
+ global TRACKER_HOMES, LOG
+
+ # see if there's an environment var. ROUNDUP_INSTANCE_HOMES is the
+ # old name for it.
+ if os.environ.has_key('ROUNDUP_INSTANCE_HOMES'):
+ homes = os.environ.get('ROUNDUP_INSTANCE_HOMES')
+ else:
+ homes = os.environ.get('TRACKER_HOMES', '')
+ if homes:
+ TRACKER_HOMES = {}
+ for home in string.split(homes, os.pathsep):
+ try:
+ name, dir = string.split(home, '=', 1)
+ except ValueError:
+ # ignore invalid definitions
+ continue
+ if name and dir:
+ TRACKER_HOMES[name] = dir
+
+ logname = os.environ.get('ROUNDUP_LOG', '')
+ if logname:
+ LOG = open(logname, 'a')
+
+ # ROUNDUP_DEBUG is checked directly in "roundup.cgi.client"
+
+
+#
+# Provide interface to CGI HTTP response handling
+#
+class RequestWrapper:
+ '''Used to make the CGI server look like a BaseHTTPRequestHandler
+ '''
+ def __init__(self, wfile):
+ self.wfile = wfile
+ def write(self, data):
+ self.wfile.write(data)
+ def send_response(self, code):
+ self.write('Status: %s\r\n'%code)
+ def send_header(self, keyword, value):
+ self.write("%s: %s\r\n" % (keyword, value))
+ def end_headers(self):
+ self.write("\r\n")
+
+#
+# Main CGI handler
+#
+def main(out, err):
+ import os, string
+ import roundup.instance
+ path = string.split(os.environ.get('PATH_INFO', '/'), '/')
+ request = RequestWrapper(out)
+ request.path = os.environ.get('PATH_INFO', '/')
+ tracker = path[1]
+ os.environ['TRACKER_NAME'] = tracker
+ os.environ['PATH_INFO'] = string.join(path[2:], '/')
+ if TRACKER_HOMES.has_key(tracker):
+ # redirect if we need a trailing '/'
+ if len(path) == 2:
+ request.send_response(301)
+ # redirect
+ if os.environ.get('HTTPS', '') == 'on':
+ protocol = 'https'
+ else:
+ protocol = 'http'
+ absolute_url = '%s://%s%s/'%(protocol, os.environ['HTTP_HOST'],
+ os.environ.get('REQUEST_URI', ''))
+ request.send_header('Location', absolute_url)
+ request.end_headers()
+ out.write('Moved Permanently')
+ else:
+ tracker_home = TRACKER_HOMES[tracker]
+ tracker = roundup.instance.open(tracker_home)
+ import roundup.cgi.client
+ if hasattr(tracker, 'Client'):
+ client = tracker.Client(tracker, request, os.environ)
+ else:
+ client = roundup.cgi.client.Client(tracker, request, os.environ)
+ try:
+ client.main()
+ except roundup.cgi.client.Unauthorised:
+ request.send_response(403)
+ request.send_header('Content-Type', 'text/html')
+ request.end_headers()
+ out.write('Unauthorised')
+ except roundup.cgi.client.NotFound:
+ request.send_response(404)
+ request.send_header('Content-Type', 'text/html')
+ request.end_headers()
+ out.write('Not found: %s'%client.path)
+
+ else:
+ import urllib
+ request.send_response(200)
+ request.send_header('Content-Type', 'text/html')
+ request.end_headers()
+ w = request.write
+ w(_('<html><head><title>Roundup trackers index</title></head>\n'))
+ w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
+ homes = TRACKER_HOMES.keys()
+ homes.sort()
+ for tracker in homes:
+ w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
+ 'tracker_url': os.environ['SCRIPT_NAME']+'/'+
+ urllib.quote(tracker),
+ 'tracker_name': cgi.escape(tracker)})
+ w(_('</ol></body></html>'))
+
+#
+# Now do the actual CGI handling
+#
+out, err = sys.stdout, sys.stderr
+try:
+ # force input/output to binary (important for file up/downloads)
+ if sys.platform == "win32":
+ import os, msvcrt
+ msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
+ msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
+ checkconfig()
+ sys.stdout = sys.stderr = LOG
+ main(out, err)
+except SystemExit:
+ pass
+except:
+ sys.stdout, sys.stderr = out, err
+ out.write('Content-Type: text/html\n\n')
+ if DEBUG_TO_CLIENT:
+ cgitb.handler()
+ else:
+ out.write(cgitb.breaker())
+ ts = time.ctime()
+ out.write('''<p>%s: An error occurred. Please check
+ the server log for more infomation.</p>'''%ts)
+ print >> sys.stderr, 'EXCEPTION AT', ts
+ traceback.print_exc(0, sys.stderr)
+
+sys.stdout.flush()
+sys.stdout, sys.stderr = out, err
+LOG.close()
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/demo.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/demo.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,129 @@
+#! /usr/bin/env python
+#
+# Copyright (c) 2003 Richard Jones (richard at mechanicalcat.net)
+#
+# $Id: demo.py,v 1.24 2006/02/08 04:03:54 richard Exp $
+
+import errno
+import os
+import socket
+import sys
+import urlparse
+from glob import glob
+
+from roundup import configuration
+from roundup.scripts import roundup_server
+
+def install_demo(home, backend, template):
+ """Install a demo tracker
+
+ Parameters:
+ home:
+ tracker home directory path
+ backend:
+ database backend name
+ template:
+ full path to the tracker template directory
+
+ """
+ from roundup import init, instance, password, backends
+
+ # set up the config for this tracker
+ config = configuration.CoreConfig()
+ config['TRACKER_HOME'] = home
+ config['MAIL_DOMAIN'] = 'localhost'
+ config['DATABASE'] = 'db'
+ config['WEB_DEBUG'] = True
+ if backend in ('mysql', 'postgresql'):
+ config['RDBMS_HOST'] = 'localhost'
+ config['RDBMS_USER'] = 'rounduptest'
+ config['RDBMS_PASSWORD'] = 'rounduptest'
+ config['RDBMS_NAME'] = 'rounduptest'
+
+ # see if we have further db nuking to perform
+ module = backends.get_backend(backend)
+ if module.db_exists(config):
+ module.db_nuke(config)
+
+ init.install(home, template)
+ # don't have email flying around
+ os.remove(os.path.join(home, 'detectors', 'nosyreaction.py'))
+ try:
+ os.remove(os.path.join(home, 'detectors', 'nosyreaction.pyc'))
+ except os.error, error:
+ if error.errno != errno.ENOENT:
+ raise
+ init.write_select_db(home, backend)
+
+ # figure basic params for server
+ hostname = 'localhost'
+ # pick a fairly odd, random port
+ port = 8917
+ while 1:
+ print 'Trying to set up web server on port %d ...'%port,
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ try:
+ s.connect((hostname, port))
+ except socket.error, e:
+ if not hasattr(e, 'args') or e.args[0] != errno.ECONNREFUSED:
+ raise
+ print 'should be ok.'
+ break
+ else:
+ s.close()
+ print 'already in use.'
+ port += 100
+ config['TRACKER_WEB'] = 'http://%s:%s/demo/'%(hostname, port)
+
+ # write the config
+ config['INSTANT_REGISTRATION'] = 1
+ config.save(os.path.join(home, config.INI_FILE))
+
+ # open the tracker and initialise
+ tracker = instance.open(home)
+ tracker.init(password.Password('admin'))
+
+ # add the "demo" user
+ db = tracker.open('admin')
+ db.user.create(username='demo', password=password.Password('demo'),
+ realname='Demo User', roles='User')
+ db.commit()
+ db.close()
+
+def run_demo(home):
+ """Run the demo tracker installed in ``home``"""
+ cfg = configuration.CoreConfig(home)
+ url = cfg["TRACKER_WEB"]
+ hostname, port = urlparse.urlparse(url)[1].split(':')
+ port = int(port)
+ success_message = '''Server running - connect to:
+ %s
+1. Log in as "demo"/"demo" or "admin"/"admin".
+2. Hit Control-C to stop the server.
+3. Re-start the server by running "python demo.py" again.
+4. Re-initialise the server by running "python demo.py nuke".
+''' % url
+
+ # disable command line processing in roundup_server
+ sys.argv = sys.argv[:1] + ['-p', str(port), 'demo=' + home]
+ roundup_server.run(success_message=success_message)
+
+def demo_main():
+ """Run a demo server for users to play with for instant gratification.
+
+ Sets up the web service on localhost. Disables nosy lists.
+ """
+ home = os.path.abspath('demo')
+ if not os.path.exists(home) or (sys.argv[-1] == 'nuke'):
+ if len(sys.argv) > 2:
+ backend = sys.argv[-2]
+ else:
+ backend = 'anydbm'
+ install_demo(home, backend, os.path.join('templates', 'classic'))
+ run_demo(home)
+
+if __name__ == '__main__':
+ demo_main()
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/detectors/creator_resolution.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/detectors/creator_resolution.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,43 @@
+# 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 $
+
+from roundup.exceptions import Reject
+
+def creator_resolution(db, cl, nodeid, newvalues):
+ '''Catch attempts to set the status to "resolved" - if the assignedto
+ user isn't the creator, then set the status to "in-progress" (try
+ "confirm-done" first though, but "classic" Roundup doesn't have that
+ status)
+ '''
+ if not newvalues.has_key('status'):
+ return
+
+ # get the resolved state ID
+ resolved_id = db.status.lookup('resolved')
+
+ if newvalues['status'] != resolved_id:
+ return
+
+ # check the assignedto
+ assignedto = newvalues.get('assignedto', cl.get(nodeid, 'assignedto'))
+ creator = cl.get(nodeid, 'creator')
+ if assignedto == creator:
+ if db.getuid() != creator:
+ name = db.user.get(creator, 'username')
+ raise Reject, 'Only the creator (%s) may close this issue'%name
+ return
+
+ # set the assignedto and status
+ newvalues['assignedto'] = creator
+ try:
+ status = db.status.lookup('confirm-done')
+ except KeyError:
+ status = db.status.lookup('in-progress')
+ newvalues['status'] = status
+
+def init(db):
+ db.issue.audit('set', creator_resolution)
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/detectors/emailauditor.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/detectors/emailauditor.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,42 @@
+
+def eml_to_mht(db, cl, nodeid, newvalues):
+ '''This auditor fires whenever a new file entity is created.
+
+ If the file is of type message/rfc822, we tack onthe extension .eml.
+
+ The reason for this is that Microsoft Internet Explorer will not open
+ things with a .eml attachment, as they deem it 'unsafe'. Worse yet,
+ they'll just give you an incomprehensible error message. For more
+ information, please see:
+
+ http://support.microsoft.com/default.aspx?scid=kb;EN-US;825803
+
+ Their suggested work around is (excerpt):
+
+ WORKAROUND
+
+ To work around this behavior, rename the .EML file that the URL
+ links to so that it has a .MHT file name extension, and then update
+ the URL to reflect the change to the file name. To do this:
+
+ 1. In Windows Explorer, locate and then select the .EML file that
+ the URL links.
+ 2. Right-click the .EML file, and then click Rename.
+ 3. Change the file name so that the .EML file uses a .MHT file name
+ extension, and then press ENTER.
+ 4. Updated the URL that links to the file to reflect the new file
+ name extension.
+
+ So... we do that. :)'''
+ if newvalues.get('type', '').lower() == "message/rfc822":
+ if not newvalues.has_key('name'):
+ newvalues['name'] = 'email.mht'
+ return
+ name = newvalues['name']
+ if name.endswith('.eml'):
+ name = name[:-4]
+ newvalues['name'] = name + '.mht'
+
+def init(db):
+ db.file.audit('create', eml_to_mht)
+
Added: tracker/vendor/roundup/current/detectors/newissuecopy.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/detectors/newissuecopy.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,22 @@
+# copied from nosyreaction
+
+from roundup import roundupdb
+
+def newissuecopy(db, cl, nodeid, oldvalues):
+ ''' Copy a message about new issues to a team address.
+ '''
+ # so use all the messages in the create
+ change_note = cl.generateCreateNote(nodeid)
+
+ # send a copy to the nosy list
+ for msgid in cl.get(nodeid, 'messages'):
+ try:
+ # note: last arg must be a list
+ cl.send_message(nodeid, msgid, change_note, ['team at team.host'])
+ except roundupdb.MessageSendError, message:
+ raise roundupdb.DetectorError, message
+
+def init(db):
+ db.issue.react('create', newissuecopy)
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/doc/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,21 @@
+announcement.html
+customizing.html
+developers.html
+implementation.html
+index.html
+installation.html
+user_guide.html
+FAQ.html
+security.html
+features.html
+upgrading.html
+glossary.html
+design.html
+admin_guide.html
+overview.html
+mysql.html
+postgresql.html
+tracker_templates.html
+whatsnew-0.7.html
+whatsnew-0.8.html
+*.ht
Added: tracker/vendor/roundup/current/doc/FAQ.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/FAQ.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,212 @@
+===========
+Roundup FAQ
+===========
+
+:Version: $Revision: 1.22 $
+
+.. contents::
+
+
+Installation
+------------
+
+Living without a mailserver
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Remove the nosy reactor, means delete the tracker file
+``detectors/nosyreactor.py`` from your tracker home.
+
+
+The cgi-bin is very slow!
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Yep, it sure is. It has to start up Python and load all of the support
+libraries for *every* request.
+
+The solution is to use the built in server.
+
+To make Roundup more seamless with your website, you may place the built
+in server behind apache and link it into your web tree
+
+
+How do I put Roundup behind Apache
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We have a project (foo) running on ``tracker.example:8080``.
+We want ``http://tracker.example/issues`` to use the roundup server, so we
+set that up on port 8080 on ``tracker.example`` with the ``config.ini`` line::
+
+ [tracker]
+ ...
+ web = 'http://tracker.example/issues/'
+
+We have a "foo_issues" tracker and we run the server with::
+
+ roundup-server -p 8080 issues=/home/roundup/trackers/issues
+
+Then, on the Apache machine (eg. redhat 7.3 with apache 1.3), in
+``/etc/httpd/conf/httpd.conf`` uncomment::
+
+ LoadModule proxy_module modules/libproxy.so
+
+and::
+
+ AddModule mod_proxy.c
+
+Then add::
+
+ # roundup stuff (added manually)
+ <IfModule mod_proxy.c>
+ # proxy through one tracker
+ ProxyPass /issues/ http://tracker.example:8080/issues/
+ # proxy through all tracker(*)
+ #ProxyPass /roundup/ http://tracker.example:8080/
+ </IfModule>
+
+Then restart Apache. Now Apache will proxy the request on to the
+roundup-server.
+
+Note that if you're proxying multiple trackers, you'll need to use the
+second ProxyPass rule described above. It will mean that your TRACKER_WEB
+will change to::
+
+ TRACKER_WEB = 'http://tracker.example/roundup/issues/'
+
+Once you're done, you can firewall off port 8080 from the rest of the world.
+
+Note that in some situations (eg. virtual hosting) you might need to use a
+more complex rewrite rule instead of the simpler ProxyPass above. The
+following should be useful as a starting template::
+
+ # roundup stuff (added manually)
+ <IfModule mod_proxy.c>
+
+ RewriteEngine on
+
+ # General Roundup
+ RewriteRule ^/roundup$ roundup/ [R]
+ RewriteRule ^/roundup/(.*)$ http://tracker.example:8080/$1 [P,L]
+
+ # Handle Foo Issues
+ RewriteRule ^/issues$ issues/ [R]
+ RewriteRule ^/issues/(.*)$ http://tracker.example:8080/issues/$1 [P,L]
+
+ </IfModule>
+
+
+How do I run Roundup through SSL (HTTPS)?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You should proxy through apache and use its SSL service. See the previous
+question on how to proxy through apache.
+
+
+Roundup runs very slowly on my XP machine when accessed from the Internet
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The issue is probably related to host name resolution for the client
+performing the request. You can turn off the resolution of the names
+when it's so slow like this. To do so, edit the module
+roundup/scripts/roundup_server.py around line 77 to add the following
+to the RoundupRequestHandler class:
+
+ def address_string(self):
+ return self.client_address[0]
+
+
+Templates
+---------
+
+What is that stuff in the tracker html directory?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This is the template code that Roundup uses to display the various pages.
+This is based upon the template markup language in Zope called, oddly
+enough "Zope Page Templates". There's documentation in the Roundup
+customisation_ documentation. For more information have a look at:
+
+ http://www.zope.org/Documentation/Books/ZopeBook/2_6Edition/
+
+specifically chapter 10 "Using Zope Page Templates" and chapter 14 "Advanced
+Page Templates".
+
+
+But I just want a select/option list for ....
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Really easy... edit ``html/issue.item``. For 'nosy', change line 53 from::
+
+ <span tal:replace="structure context/nosy/field" />
+
+to::
+
+ <span tal:replace="structure context/nosy/menu" />
+
+For 'assigned to', change line 61 from::
+
+ <td tal:content="structure context/assignedto/field">assignedto menu</td>
+
+to::
+
+ <td tal:content="structure context/assignedto/menu">assignedto menu</td>
+
+
+
+Great! But now the select/option list is too big
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Thats a little harder (but only a little ;^)
+
+Again, edit ``html/issue.item``. For nosy, change line 53 from:
+
+ <span tal:replace="structure context/nosy/field" />
+
+to::
+
+ <span tal:replace="structure python:context.nosy.menu(height=3)" />
+
+for more information, go and read about Zope Page Templates.
+
+
+Using Roundup
+-------------
+
+I got an error and I cant reload it!
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you're using Netscape/Mozilla, try holding shift and pressing reload.
+If you're using IE then install Mozilla and try again ;^)
+
+
+I keep getting logged out
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Make sure that the ``tracker`` -> ``web`` setting in your tracker's
+config.ini is set to the URL of the tracker.
+
+
+How is sorting performed, and why does it seem to fail sometimes?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When we sort items in the hyperdb, we use one of a number of methods,
+depending on the properties being sorted on:
+
+1. If it's a String, Number, Date or Interval property, we just sort the
+ scalar value of the property. Strings are sorted case-sensitively.
+2. If it's a Link property, we sort by either the linked item's "order"
+ property (if it has one) or the linked item's "id".
+3. Mulitlinks sort similar to #2, but we start with the first
+ Multilink list item, and if they're the same, we sort by the second item,
+ and so on.
+
+Note that if an "order" property is defined on a Class that is used for
+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/vendor/roundup/current/doc/Makefile
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/Makefile Sun Nov 5 21:30:25 2006
@@ -0,0 +1,25 @@
+STXTOHTML = rst2html.py
+STXTOHT = rst2ht.py
+WEBDIR = ../../htdocs/htdocs/doc-1.0
+
+SOURCE = announcement.txt customizing.txt developers.txt FAQ.txt features.txt \
+ glossary.txt implementation.txt index.txt design.txt mysql.txt \
+ installation.txt upgrading.txt user_guide.txt admin_guide.txt \
+ postgresql.txt tracker_templates.txt
+
+COMPILED := $(SOURCE:.txt=.html)
+WEBHT := $(SOURCE:.txt=.ht)
+
+all: ${COMPILED} ${WEBHT}
+
+website: ${WEBHT}
+ cp *.ht ${WEBDIR}
+
+%.html: %.txt
+ ${STXTOHTML} --report=warning -d $< $@
+
+%.ht: %.txt
+ ${STXTOHT} --report=warning -d $< $@
+
+clean:
+ rm -f ${COMPILED}
Added: tracker/vendor/roundup/current/doc/ZPL.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/ZPL.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,59 @@
+Zope Public License (ZPL) Version 2.0
+-----------------------------------------------
+
+This software is Copyright (c) Zope Corporation (tm) and
+Contributors. All rights reserved.
+
+This license has been certified as open source. It has also
+been designated as GPL compatible by the Free Software
+Foundation (FSF).
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the
+following conditions are met:
+
+1. Redistributions in source code must retain the above
+ copyright notice, this list of conditions, and the following
+ disclaimer.
+
+2. Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions, and the following
+ disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+3. The name Zope Corporation (tm) must not be used to
+ endorse or promote products derived from this software
+ without prior written permission from Zope Corporation.
+
+4. The right to distribute this software or to use it for
+ any purpose does not give you the right to use Servicemarks
+ (sm) or Trademarks (tm) of Zope Corporation. Use of them is
+ covered in a separate agreement (see
+ http://www.zope.com/Marks).
+
+5. If any files are modified, you must cause the modified
+ files to carry prominent notices stating that you changed
+ the files and the date of any change.
+
+Disclaimer
+
+ THIS SOFTWARE IS PROVIDED BY ZOPE CORPORATION ``AS IS''
+ AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+ NO EVENT SHALL ZOPE CORPORATION OR ITS CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ DAMAGE.
+
+
+This software consists of contributions made by Zope
+Corporation and many individuals on behalf of Zope
+Corporation. Specific attributions are listed in the
+accompanying credits file.
Added: tracker/vendor/roundup/current/doc/admin_guide.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/admin_guide.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,357 @@
+====================
+Administration Guide
+====================
+
+:Version: $Revision: 1.20 $
+
+.. contents::
+
+What does Roundup install?
+==========================
+
+There's two "installations" that we talk about when using Roundup:
+
+1. The installation of the software and its support files. This uses the
+ standard Python mechanism called "distutils" and thus Roundup's core code,
+ executable scripts and support data files are installed in Python's
+ directories. On Windows, this is typically:
+
+ Scripts
+ ``<python dir>\scripts\...``
+ Core code
+ ``<python dir>\lib\site-packages\roundup\...``
+ Support files
+ ``<python dir>\share\roundup\...``
+
+ and on Unix-like systems (eg. Linux):
+
+ Scripts
+ ``<python root>/bin/...``
+ Core code
+ ``<python root>/lib-<python version>/site-packages/roundup/...``
+ Support files
+ ``<python root>/share/roundup/...``
+
+2. The installation of a specific tracker. When invoking the roundup-admin
+ "inst" (and "init") commands, you're creating a new Roundup tracker. This
+ installs configuration files, HTML templates, detector code and a new
+ database. You have complete control over where this stuff goes through
+ both choosing your "tracker home" and the ``main`` -> ``database`` variable
+ in the tracker's config.ini.
+
+
+Configuring Roundup's Logging of Messages For Sysadmins
+=======================================================
+
+You may configure where Roundup logs messages in your tracker's config.ini
+file. Roundup will use the standard Python (2.3+) logging implementation
+when available. If not, then a very basic logging implementation will be used
+(see BasicLogging in the roundup.rlog module for details).
+
+Configuration for standard "logging" module:
+ - tracker configuration file specifies the location of a logging
+ configration file as ``logging`` -> ``config``
+ - ``roundup-server`` specifies the location of a logging configuration
+ file on the command line
+Configuration for "BasicLogging" implementation:
+ - tracker configuration file specifies the location of a log file
+ ``logging`` -> ``filename``
+ - tracker configuration file specifies the level to log to as
+ ``logging`` -> ``level``
+ - ``roundup-server`` specifies the location of a log file on the command
+ line
+ - ``roundup-server`` specifies the level to log to on the command line
+
+(``roundup-mailgw`` always logs to the tracker's log file)
+
+In both cases, if no logfile is specified then logging will simply be sent
+to sys.stderr with only logging of ERROR messages.
+
+
+Configuring roundup-server
+==========================
+
+The basic configuration file layout is as follows (take from the
+``roundup-server.ini.example`` file in the "doc" directory)::
+
+ [main]
+ port = 8080
+ ;hostname =
+ ;user =
+ ;group =
+ ;log_ip = yes
+ ;pidfile =
+ ;logfile =
+
+ [trackers]
+ ; Add one of these per tracker being served
+ name = /path/to/tracker/name
+
+Values ";commented out" are optional. The meaning of the various options
+are as follows:
+
+**port**
+ Defines the local TCP port to listen for clients on.
+**hostname**
+ Defines the local hostname to listen for clients on. Only required if
+ "localhost" is not sufficient.
+**user** and **group**
+ Defines the Unix user and group to run the server as. Only work if the
+ server is started as root.
+**log_ip**
+ If ``yes`` then we log IP addresses against accesses. If ``no`` then we
+ log the hostname of the client. The latter can be much slower.
+**pidfile**
+ If specified, the server will fork at startup and write its new PID to
+ the file.
+**logfile**
+ Any unhandled exception messages or other output from Roundup will be
+ written to this file. It must be specified if **pidfile** is specified.
+ If per-tracker logging is specified, then very little will be written to
+ this file.
+**trackers** section
+ Each line denotes a mapping from a URL component to a tracker home.
+ Make sure the name part doesn't include any url-unsafe characters like
+ spaces. Stick to alphanumeric characters and you'll be ok.
+
+
+Users and Security
+==================
+
+Roundup holds its own user database which primarily contains a username,
+password and email address for the user. Roundup *must* have its own user
+listing, in order to maintain internal consistency of its data. It is a
+relatively simple exercise to update this listing on a regular basis, or on
+demand, so that it matches an external listing (eg. unix passwd file, LDAP,
+etc.)
+
+Roundup identifies users in a number of ways:
+
+1. Through the web, users may be identified by either HTTP Basic
+ Authentication or cookie authentication. If you are running the web
+ server (roundup-server) through another HTTP server (eg. apache or IIS)
+ then that server may require HTTP Basic Authentication, and it will pass
+ the ``REMOTE_USER`` variable through to Roundup. If this variable is not
+ present, then Roundup defaults to using its own cookie-based login
+ mechanism.
+2. In email messages handled by roundup-mailgw, users are identified by the
+ From address in the message.
+
+In both cases, Roundup's behaviour when dealing with unknown users is
+controlled by Permissions defined in the "SECURITY SETTINGS" section of the
+tracker's ``schema.py`` module:
+
+Web Registration
+ If granted to the Anonymous Role, then anonymous users will be able to
+ register through the web.
+Email Registration
+ If granted to the Anonymous Role, then email messages from unknown users
+ will result in those users being registered with the tracker.
+
+More information about how to customise your tracker's security settings
+may be found in the `customisation documentation`_.
+
+
+Tasks
+=====
+
+Maintenance of Roundup can involve one of the following:
+
+1. `tracker backup`_
+2. `software upgrade`_
+3. `migrating backends`_
+4. `moving a tracker`_
+5. `migrating from other software`_
+6. `adding a user from the command-line`_
+
+
+Tracker Backup
+--------------
+
+Stop the web and email frontends and to copy the contents of the tracker home
+directory to some other place using standard backup tools.
+
+
+Software Upgrade
+----------------
+
+Always make a backup of your tracker before upgrading software. Steps you may
+take:
+
+1. Ensure that the unit tests run on your system::
+
+ python run_tests.py
+
+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
+ the software in the copied.
+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.
+
+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::
+
+ python setup.py install
+
+
+Migrating Backends
+------------------
+
+1. stop the existing tracker web and email frontends (preventing changes)
+2. use the roundup-admin tool "export" command to export the contents of
+ your tracker to disk
+3. copy the tracker home to a new directory
+4. delete the "db" directory from the new directory
+5. enter the new backend name in the tracker home ``db/backend_name`` file
+6. use the roundup-admin "import" command to import the previous export with
+ the new tracker home
+7. test each of the admin tool, web interface and mail gateway using the new
+ backend
+8. move the old tracker home out of the way (rename to "tracker.old") and
+ move the new tracker home into its place
+9. restart web and email frontends
+
+
+Moving a Tracker
+----------------
+
+If you're moving the tracker to a similar machine, you should:
+
+1. install Roundup on the new machine and test that it works there,
+2. stop the existing tracker web and email frontends (preventing changes),
+3. copy the tracker home directory over to the new machine, and
+4. start the tracker web and email frontends on the new machine.
+
+Most of the backends are actually portable across platforms (ie. from Unix to
+Windows to Mac). If this isn't the case (ie. the tracker doesn't work when
+moved using the above steps) then you'll need to:
+
+1. install Roundup on the new machine and test that it works there,
+2. stop the existing tracker web and email frontends (preventing changes),
+3. use the roundup-admin tool "export" command to export the contents of
+ the existing tracker,
+4. copy the export to the new machine,
+5. use the roundup-admin "import" command to import the tracker on the new
+ machine, and
+6. start the tracker web and email frontends on the new machine.
+
+
+Migrating From Other Software
+-----------------------------
+
+You have a couple of choices. You can either use a CSV import into Roundup,
+or you can write a simple Python script which uses the Roundup API
+directly. The latter is almost always simpler -- see the "scripts"
+directory in the Roundup source for some example uses of the API.
+
+"roundup-admin import" will import data into your tracker from a
+directory containing files with the following format:
+
+- one colon-separated-values file per Class with columns for each property,
+ named <classname>.csv
+- one colon-separated-values file per Class with journal information,
+ named <classname>-journals.csv (this is required, even if it's empty)
+- if the Class is a FileClass, you may have the "content" property
+ stored in separate files from the csv files. This goes in a directory
+ structure::
+
+ <classname>-files/<N>/<designator>
+
+ where ``<designator>`` is the item's ``<classname><id>`` combination.
+ The ``<N>`` value is ``int(<id> / 1000)``.
+
+
+Adding A User From The Command-Line
+-----------------------------------
+
+The ``roundup-admin`` program can create any data you wish to in the
+database. To create a new user, use::
+
+ roundup-admin create user
+
+To figure out what good values might be for some of the fields (eg. Roles)
+you can just display another user::
+
+ roundup-admin list user
+
+(or if you know their username, and it happens to be "richard")::
+
+ roundup-admin find username=richard
+
+then using the user id you get from one of the above commands, you may
+display the user's details::
+
+ roundup-admin display <userid>
+
+
+Running the Servers
+===================
+
+Unix
+----
+
+On Unix systems, use the scripts/server-ctl script to control the
+roundup-server server. Copy it somewhere and edit the variables at the top
+to reflect your specific installation.
+
+
+Windows
+-------
+
+On Windows, the roundup-server program runs as a Windows Service, and
+therefore may be controlled through the Services control panel. The
+roundup-server program may also control the service directly:
+
+**install the service**
+ ``roundup-server -c install``
+**start the service**
+ ``roundup-server -c start``
+**stop the service**
+ ``roundup-server -c stop``
+
+To bring up the services panel:
+
+Windows 2000 and later
+ Start/Control Panel/Administrative Tools/Services
+Windows NT4
+ Start/Control Panel/Services
+
+Running the Mail Gateway Script
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The mail gateway script should be scheduled to run regularly on your
+Windows server. Normally this will result in a window popping up. The
+solution to this is to:
+
+1. Create a new local account on the Roundup server
+2. Set the scheduled task to run in the context of this user instead
+ of your normal login
+
+
+-------------------
+
+Back to `Table of Contents`_
+
+.. _`Table of Contents`: index.html
+.. _`customisation documentation`: customizing.html
+.. _`upgrading documentation`: upgrading.html
+
Added: tracker/vendor/roundup/current/doc/announcement.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/announcement.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,72 @@
+I'm proud to release version 1.1.2 of Roundup.
+
+Feature:
+
+- server-ctl script uses server configuration file (sf bug 1443805)
+
+Fixed:
+
+- indexing may be turned off for FileClass "content" now
+ ("content" and "type" properties are now automatically included in the
+ FileClass schema where previously the "content" property was faked and
+ "type" was optional)
+- reduced frequency of session timestamp update
+- progress display in roundup-admin reindex
+- bug in menu() permission filter (sf bug 1444440)
+- verbose output during import is optional now (sf bug 1475624)
+- escape *all* uses of "schema" in mysql backend (sf bug 1472120)
+- responses to user rego email (sf bug 1470254)
+- dangling connections in session handling (sf bug 1463359)
+- classhelp popup pagination forgot about "type" (sf bug 1465836)
+- umask is now configurable (with the same 0002 default)
+- sorting of entries in classhelp popup (sf bug 1449000)
+- allow single digit seconds in date spec (sf bug 1447141)
+- prevent generation of new single-digit seconds dates (sf bug 1429390)
+- implement close() on all indexers (sf bug 1242477)
+
+
+If you're upgrading from an older version of Roundup you *must* follow
+the "Software Upgrade" guidelines given in the maintenance documentation.
+
+Roundup requires python 2.3 or later for correct operation.
+
+To give Roundup a try, just download (see below), unpack and run::
+
+ python demo.py
+
+Release info and download page:
+ http://cheeseshop.python.org/pypi/roundup
+Source and documentation is available at the website:
+ http://roundup.sourceforge.net/
+Mailing lists - the place to ask questions:
+ http://sourceforge.net/mail/?group_id=31577
+
+
+About Roundup
+=============
+
+Roundup is a simple-to-use and -install issue-tracking system with
+command-line, web and e-mail interfaces. It is based on the winning design
+from Ka-Ping Yee in the Software Carpentry "Track" design competition.
+
+Note: Ping is not responsible for this project. The contact for this
+project is richard at users.sourceforge.net.
+
+Roundup manages a number of issues (with flexible properties such as
+"description", "priority", and so on) and provides the ability to:
+
+(a) submit new issues,
+(b) find and edit existing issues, and
+(c) discuss issues with other participants.
+
+The system will facilitate communication among the participants by managing
+discussions and notifying interested parties when issues are edited. One of
+the major design goals for Roundup that it be simple to get going. Roundup
+is therefore usable "out of the box" with any python 2.3+ installation. It
+doesn't even need to be "installed" to be operational, though a
+disutils-based install script is provided.
+
+It comes with two issue tracker templates (a classic bug/feature tracker and
+a minimal skeleton) and five database back-ends (anydbm, sqlite, metakit,
+mysql and postgresql).
+
Added: tracker/vendor/roundup/current/doc/customizing.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/customizing.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,4549 @@
+===================
+Customising Roundup
+===================
+
+:Version: $Revision: 1.197 $
+
+.. This document borrows from the ZopeBook section on ZPT. The original is at:
+ http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx
+
+.. contents::
+ :depth: 1
+
+What You Can Do
+===============
+
+Before you get too far, it's probably worth having a quick read of the Roundup
+`design documentation`_.
+
+Customisation of Roundup can take one of six forms:
+
+1. `tracker configuration`_ changes
+2. database, or `tracker schema`_ changes
+3. "definition" class `database content`_ changes
+4. behavioural changes, through detectors_
+5. `security / access controls`_
+6. change the `web interface`_
+
+The third case is special because it takes two distinctly different forms
+depending upon whether the tracker has been initialised or not. The other two
+may be done at any time, before or after tracker initialisation. Yes, this
+includes adding or removing properties from classes.
+
+
+Trackers in a Nutshell
+======================
+
+Trackers have the following structure:
+
+=================== ========================================================
+Tracker File Description
+=================== ========================================================
+config.ini Holds the basic `tracker configuration`_
+schema.py Holds the `tracker schema`_
+initial_data.py Holds any data to be entered into the database when the
+ tracker is initialised.
+db/ Holds the tracker's database
+db/files/ Holds the tracker's upload files and messages
+db/backend_name Names the database back-end for the tracker
+detectors/ Auditors and reactors for this tracker
+extensions/ Additional web actions and templating utilities.
+html/ Web interface templates, images and style sheets
+=================== ========================================================
+
+
+Tracker Configuration
+=====================
+
+The ``config.ini`` located in your tracker home contains the basic
+configuration for the web and e-mail components of roundup's interfaces.
+
+Changes to the data captured by your tracker is controlled by the `tracker
+schema`_. Some configuration is also performed using permissions - see the
+`security / access controls`_ section. For example, to allow users to
+automatically register through the email interface, you must grant the
+"Anonymous" Role the "Email Access" Permission.
+
+The following is taken from the `Python Library Reference`__ (May 20, 2004)
+section "ConfigParser -- Configuration file parser":
+
+ The configuration file consists of sections, led by a "[section]" header
+ and followed by "name = value" entries, with line continuations on a
+ newline with leading whitespace. Note that leading whitespace is removed
+ from values. The optional values can contain format strings which
+ refer to other values in the same section. Lines beginning with "#" or ";"
+ are ignored and may be used to provide comments.
+
+ For example::
+
+ [My Section]
+ foodir = %(dir)s/whatever
+ dir = frob
+
+ would resolve the "%(dir)s" to the value of "dir" ("frob" in this case)
+ resulting in "foodir" being "frob/whatever".
+
+__ http://docs.python.org/lib/module-ConfigParser.html
+
+Section **main**
+ database -- ``db``
+ Database directory path. The path may be either absolute or relative
+ to the directory containig this config file.
+
+ templates -- ``html``
+ Path to the HTML templates directory. The path may be either absolute
+ or relative to the directory containig this config file.
+
+ admin_email -- ``roundup-admin``
+ Email address that roundup will complain to if it runs into trouble. If
+ the email address doesn't contain an ``@`` part, the MAIL_DOMAIN defined
+ below is used.
+
+ dispatcher_email -- ``roundup-admin``
+ The 'dispatcher' is a role that can get notified of new items to the
+ database. It is used by the ERROR_MESSAGES_TO config setting. If the
+ email address doesn't contain an ``@`` part, the MAIL_DOMAIN defined
+ below is used.
+
+ email_from_tag -- default *blank*
+ Additional text to include in the "name" part of the From: address used
+ in nosy messages. If the sending user is "Foo Bar", the From: line
+ is usually: ``"Foo Bar" <issue_tracker at tracker.example>``
+ the EMAIL_FROM_TAG goes inside the "Foo Bar" quotes like so:
+ ``"Foo Bar EMAIL_FROM_TAG" <issue_tracker at tracker.example>``
+
+ new_web_user_roles -- ``User``
+ Roles that a user gets when they register with Web User Interface.
+ This is a comma-separated list of role names (e.g. ``Admin,User``).
+
+ new_email_user_roles -- ``User``
+ Roles that a user gets when they register with Email Gateway.
+ This is a comma-separated string of role names (e.g. ``Admin,User``).
+
+ error_messages_to -- ``user``
+ Send error message emails to the ``dispatcher``, ``user``, or ``both``?
+ The dispatcher is configured using the DISPATCHER_EMAIL setting.
+ Allowed values: ``dispatcher``, ``user``, or ``both``
+
+ html_version -- ``html4``
+ HTML version to generate. The templates are ``html4`` by default.
+ If you wish to make them xhtml, then you'll need to change this
+ var to ``xhtml`` too so all auto-generated HTML is compliant.
+ Allowed values: ``html4``, ``xhtml``
+
+ timezone -- ``0``
+ Numeric timezone offset used when users do not choose their own
+ in their settings.
+
+ instant_registration -- ``yes``
+ Register new users instantly, or require confirmation via
+ email?
+ Allowed values: ``yes``, ``no``
+
+ email_registration_confirmation -- ``yes``
+ Offer registration confirmation by email or only through the web?
+ Allowed values: ``yes``, ``no``
+
+ indexer_stopwords -- default *blank*
+ Additional stop-words for the full-text indexer specific to
+ your tracker. See the indexer source for the default list of
+ stop-words (e.g. ``A,AND,ARE,AS,AT,BE,BUT,BY, ...``).
+
+Section **tracker**
+ name -- ``Roundup issue tracker``
+ A descriptive name for your roundup instance.
+
+ web -- ``http://host.example/demo/``
+ The web address that the tracker is viewable at.
+ This will be included in information sent to users of the tracker.
+ The URL MUST include the cgi-bin part or anything else
+ that is required to get to the home page of the tracker.
+ You MUST include a trailing '/' in the URL.
+
+ email -- ``issue_tracker``
+ Email address that mail to roundup should go to.
+
+Section **web**
+ http_auth -- ``yes``
+ Whether to use HTTP Basic Authentication, if present.
+ Roundup will use either the REMOTE_USER or HTTP_AUTHORIZATION
+ variables supplied by your web server (in that order).
+ Set this option to 'no' if you do not wish to use HTTP Basic
+ Authentication in your web interface.
+
+ use_browser_language -- ``yes``
+ Whether to use HTTP Accept-Language, if present.
+ Browsers send a language-region preference list.
+ It's usually set in the client's browser or in their
+ Operating System.
+ Set this option to 'no' if you want to ignore it.
+
+ debug -- ``no``
+ Setting this option makes Roundup display error tracebacks
+ in the user's browser rather than emailing them to the
+ tracker admin."),
+
+Section **rdbms**
+ Settings in this section are used by Postgresql and MySQL backends only
+
+ name -- ``roundup``
+ Name of the database to use.
+
+ host -- ``localhost``
+ Database server host.
+
+ port -- default *blank*
+ TCP port number of the database server. Postgresql usually resides on
+ port 5432 (if any), for MySQL default port number is 3306. Leave this
+ option empty to use backend default.
+
+ user -- ``roundup``
+ Database user name that Roundup should use.
+
+ password -- ``roundup``
+ Database user password.
+
+Section **logging**
+ config -- default *blank*
+ Path to configuration file for standard Python logging module. If this
+ option is set, logging configuration is loaded from specified file;
+ options 'filename' and 'level' in this section are ignored. The path may
+ be either absolute or relative to the directory containig this config file.
+
+ filename -- default *blank*
+ Log file name for minimal logging facility built into Roundup. If no file
+ name specified, log messages are written on stderr. If above 'config'
+ option is set, this option has no effect. The path may be either absolute
+ or relative to the directory containig this config file.
+
+ level -- ``ERROR``
+ Minimal severity level of messages written to log file. If above 'config'
+ option is set, this option has no effect.
+ Allowed values: ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``
+
+Section **mail**
+ Outgoing email options. Used for nosy messages, password reset and
+ registration approval requests.
+
+ domain -- ``localhost``
+ Domain name used for email addresses.
+
+ host -- default *blank*
+ SMTP mail host that roundup will use to send mail
+
+ username -- default *blank*
+ SMTP login name. Set this if your mail host requires authenticated access.
+ If username is not empty, password (below) MUST be set!
+
+ password -- default *blank*
+ SMTP login password.
+ Set this if your mail host requires authenticated access.
+
+ tls -- ``no``
+ If your SMTP mail host provides or requires TLS (Transport Layer Security)
+ then you may set this option to 'yes'.
+ Allowed values: ``yes``, ``no``
+
+ tls_keyfile -- default *blank*
+ If TLS is used, you may set this option to the name of a PEM formatted
+ file that contains your private key. The path may be either absolute or
+ relative to the directory containig this config file.
+
+ tls_certfile -- default *blank*
+ If TLS is used, you may set this option to the name of a PEM formatted
+ certificate chain file. The path may be either absolute or relative
+ to the directory containig this config file.
+
+ charset -- utf-8
+ Character set to encode email headers with. We use utf-8 by default, as
+ it's the most flexible. Some mail readers (eg. Eudora) can't cope with
+ that, so you might need to specify a more limited character set
+ (eg. iso-8859-1).
+
+ debug -- default *blank*
+ Setting this option makes Roundup to write all outgoing email messages
+ to this file *instead* of sending them. This option has the same effect
+ as environment variable SENDMAILDEBUG. Environment variable takes
+ precedence. The path may be either absolute or relative to the directory
+ containig this config file.
+
+Section **mailgw**
+ Roundup Mail Gateway options
+
+ keep_quoted_text -- ``yes``
+ Keep email citations when accepting messages. Setting this to ``no`` strips
+ out "quoted" text from the message. Signatures are also stripped.
+ Allowed values: ``yes``, ``no``
+
+ leave_body_unchanged -- ``no``
+ Preserve the email body as is - that is, keep the citations *and*
+ signatures.
+ Allowed values: ``yes``, ``no``
+
+ default_class -- ``issue``
+ Default class to use in the mailgw if one isn't supplied in email subjects.
+ To disable, leave the value blank.
+
+ subject_prefix_parsing -- ``strict``
+ Controls the parsing of the [prefix] on subject lines in incoming emails.
+ ``strict`` will return an error to the sender if the [prefix] is not
+ recognised. ``loose`` will attempt to parse the [prefix] but just
+ pass it through as part of the issue title if not recognised. ``none``
+ will always pass any [prefix] through as part of the issue title.
+
+ subject_suffix_parsing -- ``strict``
+ Controls the parsing of the [suffix] on subject lines in incoming emails.
+ ``strict`` will return an error to the sender if the [suffix] is not
+ recognised. ``loose`` will attempt to parse the [suffix] but just
+ pass it through as part of the issue title if not recognised. ``none``
+ will always pass any [suffix] through as part of the issue title.
+
+ subject_suffix_delimiters -- ``[]``
+ Defines the brackets used for delimiting the commands suffix in a subject
+ line.
+
+ subject_content_match -- ``always``
+ Controls matching of the incoming email subject line against issue titles
+ in the case where there is no designator [prefix]. ``never`` turns off
+ matching. ``creation + interval`` or ``activity + interval`` will match
+ an issue for the interval after the issue's creation or last activity.
+ The interval is a standard Roundup interval.
+
+Section **nosy**
+ Nosy messages sending
+
+ messages_to_author -- ``no``
+ Send nosy messages to the author of the message.
+ Allowed values: ``yes``, ``no``, ``new``
+
+ signature_position -- ``bottom``
+ Where to place the email signature.
+ Allowed values: ``top``, ``bottom``, ``none``
+
+ add_author -- ``new``
+ Does the author of a message get placed on the nosy list automatically?
+ If ``new`` is used, then the author will only be added when a message
+ creates a new issue. If ``yes``, then the author will be added on
+ followups too. If ``no``, they're never added to the nosy.
+ Allowed values: ``yes``, ``no``, ``new``
+
+ add_recipients -- ``new``
+ Do the recipients (``To:``, ``Cc:``) of a message get placed on the nosy
+ list? If ``new`` is used, then the recipients will only be added when a
+ message creates a new issue. If ``yes``, then the recipients will be added
+ on followups too. If ``no``, they're never added to the nosy.
+ Allowed values: ``yes``, ``no``, ``new``
+
+ email_sending -- ``single``
+ Controls the email sending from the nosy reactor. If ``multiple`` then
+ a separate email is sent to each recipient. If ``single`` then a single
+ email is sent with each recipient as a CC address.
+
+You may generate a new default config file using the ``roundup-admin
+genconfig`` command.
+
+Configuration variables may be referred to in lower or upper case. In code,
+variables not in the "main" section are referred to using their section and
+name, so "domain" in the section "mail" becomes MAIL_DOMAIN. The
+configuration variables available are:
+
+
+Tracker Schema
+==============
+
+.. note::
+ if you modify the schema, you'll most likely need to edit the
+ `web interface`_ HTML template files and `detectors`_ to reflect
+ your changes.
+
+A tracker schema defines what data is stored in the tracker's database.
+Schemas are defined using Python code in the ``schema.py`` module of your
+tracker.
+
+The ``schema.py`` module
+------------------------
+
+The ``schema.py`` module contains two functions:
+
+**open**
+ This function defines what your tracker looks like on the inside, the
+ **schema** of the tracker. It defines the **Classes** and **properties**
+ on each class. It also defines the **security** for those Classes. The
+ next few sections describe how schemas work and what you can do with
+ them.
+**init**
+ This function is responsible for setting up the initial state of your
+ tracker. It's called exactly once - but the ``roundup-admin initialise``
+ command. See the start of the section on `database content`_ for more
+ info about how this works.
+
+
+The "classic" schema
+--------------------
+
+The "classic" schema looks like this (see section `setkey(property)`_
+below for the meaning of ``'setkey'`` -- you may also want to look into
+the sections `setlabelprop(property)`_ and `setorderprop(property)`_ for
+specifying (default) labelling and ordering of classes.)::
+
+ pri = Class(db, "priority", name=String(), order=String())
+ pri.setkey("name")
+
+ stat = Class(db, "status", name=String(), order=String())
+ stat.setkey("name")
+
+ keyword = Class(db, "keyword", name=String())
+ keyword.setkey("name")
+
+ user = Class(db, "user", username=String(), organisation=String(),
+ password=String(), address=String(), realname=String(),
+ phone=String())
+ user.setkey("username")
+
+ msg = FileClass(db, "msg", author=Link("user"), summary=String(),
+ date=Date(), recipients=Multilink("user"),
+ files=Multilink("file"))
+
+ file = FileClass(db, "file", name=String(), type=String())
+
+ issue = IssueClass(db, "issue", topic=Multilink("keyword"),
+ status=Link("status"), assignedto=Link("user"),
+ priority=Link("priority"))
+ issue.setkey('title')
+
+
+What you can't do to the schema
+-------------------------------
+
+You must never:
+
+**Remove the users class**
+ This class is the only *required* class in Roundup.
+
+**Remove the "username", "address", "password" or "realname" user properties**
+ Various parts of Roundup require these properties. Don't remove them.
+
+**Change the type of a property**
+ Property types must *never* be changed - the database simply doesn't take
+ this kind of action into account. Note that you can't just remove a
+ property and re-add it as a new type either. If you wanted to make the
+ assignedto property a Multilink, you'd need to create a new property
+ assignedto_list and remove the old assignedto property.
+
+
+What you can do to the schema
+-----------------------------
+
+Your schema may be changed at any time before or after the tracker has been
+initialised (or used). You may:
+
+**Add new properties to classes, or add whole new classes**
+ This is painless and easy to do - there are generally no repurcussions
+ from adding new information to a tracker's schema.
+
+**Remove properties**
+ Removing properties is a little more tricky - you need to make sure that
+ the property is no longer used in the `web interface`_ *or* by the
+ detectors_.
+
+
+
+Classes and Properties - creating a new information store
+---------------------------------------------------------
+
+In the tracker above, we've defined 7 classes of information:
+
+ priority
+ Defines the possible levels of urgency for issues.
+
+ status
+ Defines the possible states of processing the issue may be in.
+
+ keyword
+ Initially empty, will hold keywords useful for searching issues.
+
+ user
+ Initially holding the "admin" user, will eventually have an entry
+ for all users using roundup.
+
+ msg
+ Initially empty, will hold all e-mail messages sent to or
+ generated by roundup.
+
+ file
+ Initially empty, will hold all files attached to issues.
+
+ issue
+ Initially empty, this is where the issue information is stored.
+
+We define the "priority" and "status" classes to allow two things:
+reduction in the amount of information stored on the issue and more
+powerful, accurate searching of issues by priority and status. By only
+requiring a link on the issue (which is stored as a single number) we
+reduce the chance that someone mis-types a priority or status - or
+simply makes a new one up.
+
+
+Class and Items
+~~~~~~~~~~~~~~~
+
+A Class defines a particular class (or type) of data that will be stored
+in the database. A class comprises one or more properties, which gives
+the information about the class items.
+
+The actual data entered into the database, using ``class.create()``, are
+called items. They have a special immutable property called ``'id'``. We
+sometimes refer to this as the *itemid*.
+
+
+Properties
+~~~~~~~~~~
+
+A Class is comprised of one or more properties of the following types:
+
+* String properties are for storing arbitrary-length strings.
+* Password properties are for storing encoded arbitrary-length strings.
+ The default encoding is defined on the ``roundup.password.Password``
+ class.
+* Date properties store date-and-time stamps. Their values are Timestamp
+ objects.
+* Number properties store numeric values.
+* Boolean properties store on/off, yes/no, true/false values.
+* A Link property refers to a single other item selected from a
+ specified class. The class is part of the property; the value is an
+ integer, the id of the chosen item.
+* A Multilink property refers to possibly many items in a specified
+ class. The value is a list of integers.
+
+All Classes automatically have a number of properties by default:
+
+*creator*
+ Link to the user that created the item.
+*creation*
+ Date the item was created.
+*actor*
+ Link to the user that last modified the item.
+*activity*
+ Date the item was last modified.
+
+
+FileClass
+~~~~~~~~~
+
+FileClasses save their "content" attribute off in a separate file from
+the rest of the database. This reduces the number of large entries in
+the database, which generally makes databases more efficient, and also
+allows us to use command-line tools to operate on the files. They are
+stored in the files sub-directory of the ``'db'`` directory in your
+tracker.
+
+
+IssueClass
+~~~~~~~~~~
+
+IssueClasses automatically include the "messages", "files", "nosy", and
+"superseder" properties.
+
+The messages and files properties list the links to the messages and
+files related to the issue. The nosy property is a list of links to
+users who wish to be informed of changes to the issue - they get "CC'ed"
+e-mails when messages are sent to or generated by the issue. The nosy
+reactor (in the ``'detectors'`` directory) handles this action. The
+superseder link indicates an issue which has superseded this one.
+
+They also have the dynamically generated "creation", "activity" and
+"creator" properties.
+
+The value of the "creation" property is the date when an item was
+created, and the value of the "activity" property is the date when any
+property on the item was last edited (equivalently, these are the dates
+on the first and last records in the item's journal). The "creator"
+property holds a link to the user that created the issue.
+
+
+setkey(property)
+~~~~~~~~~~~~~~~~
+
+Select a String property of the class to be the key property. The key
+property must be unique, and allows references to the items in the class
+by the content of the key property. That is, we can refer to users by
+their username: for example, let's say that there's an issue in roundup,
+issue 23. There's also a user, richard, who happens to be user 2. To
+assign an issue to him, we could do either of::
+
+ roundup-admin set issue23 assignedto=2
+
+or::
+
+ roundup-admin set issue23 assignedto=richard
+
+Note, the same thing can be done in the web and e-mail interfaces.
+
+setlabelprop(property)
+~~~~~~~~~~~~~~~~~~~~~~
+
+Select a property of the class to be the label property. The label
+property is used whereever an item should be uniquely identified, e.g.,
+when displaying a link to an item. If setlabelprop is not specified for
+a class, the following values are tried for the label:
+
+ * the key of the class (see the `setkey(property)`_ section above)
+ * the "name" property
+ * the "title" property
+ * the first property from the sorted property name list
+
+So in most cases you can get away without specifying setlabelprop
+explicitly.
+
+setorderprop(property)
+~~~~~~~~~~~~~~~~~~~~~~
+
+Select a property of the class to be the order property. The order
+property is used whenever using a default sort order for the class,
+e.g., when grouping or sorting class A by a link to class B in the user
+interface, the order property of class B is used for sorting. If
+setorderprop is not specified for a class, the following values are tried
+for the order property:
+
+ * the property named "order"
+ * the label property (see `setlabelprop(property)`_ above)
+
+So in most cases you can get away without specifying setorderprop
+explicitly.
+
+create(information)
+~~~~~~~~~~~~~~~~~~~
+
+Create an item in the database. This is generally used to create items
+in the "definitional" classes like "priority" and "status".
+
+
+A note about ordering
+~~~~~~~~~~~~~~~~~~~~~
+
+When we sort items in the hyperdb, we use one of a number of methods,
+depending on the properties being sorted on:
+
+1. If it's a String, Number, Date or Interval property, we just sort the
+ scalar value of the property. Strings are sorted case-sensitively.
+2. If it's a Link property, we sort by either the linked item's "order"
+ property (if it has one) or the linked item's "id".
+3. Mulitlinks sort similar to #2, but we start with the first Multilink
+ list item, and if they're the same, we sort by the second item, and
+ so on.
+
+Note that if an "order" property is defined on a Class that is used for
+sorting, all items of that Class *must* have a value against the "order"
+property, or sorting will result in random ordering.
+
+
+Examples of adding to your schema
+---------------------------------
+
+TODO
+
+
+Detectors - adding behaviour to your tracker
+============================================
+.. _detectors:
+
+Detectors are initialised every time you open your tracker database, so
+you're free to add and remove them any time, even after the database is
+initialised via the ``roundup-admin initialise`` command.
+
+The detectors in your tracker fire *before* (**auditors**) and *after*
+(**reactors**) changes to the contents of your database. They are Python
+modules that sit in your tracker's ``detectors`` directory. You will
+have some installed by default - have a look. You can write new
+detectors or modify the existing ones. The existing detectors installed
+for you are:
+
+**nosyreaction.py**
+ This provides the automatic nosy list maintenance and email sending.
+ The nosy reactor (``nosyreaction``) fires when new messages are added
+ to issues. The nosy auditor (``updatenosy``) fires when issues are
+ changed, and figures out what changes need to be made to the nosy list
+ (such as adding new authors, etc.)
+**statusauditor.py**
+ This provides the ``chatty`` auditor which changes the issue status
+ from ``unread`` or ``closed`` to ``chatting`` if new messages appear.
+ It also provides the ``presetunread`` auditor which pre-sets the
+ status to ``unread`` on new items if the status isn't explicitly
+ defined.
+**messagesummary.py**
+ Generates the ``summary`` property for new messages based on the message
+ content.
+**userauditor.py**
+ Verifies the content of some of the user fields (email addresses and
+ roles lists).
+
+If you don't want this default behaviour, you're completely free to change
+or remove these detectors.
+
+See the detectors section in the `design document`__ for details of the
+interface for detectors.
+
+__ design.html
+
+
+Detector API
+------------
+
+Auditors are called with the arguments::
+
+ audit(db, cl, itemid, newdata)
+
+where ``db`` is the database, ``cl`` is an instance of Class or
+IssueClass within the database, and ``newdata`` is a dictionary mapping
+property names to values.
+
+For a ``create()`` operation, the ``itemid`` argument is None and
+newdata contains all of the initial property values with which the item
+is about to be created.
+
+For a ``set()`` operation, newdata contains only the names and values of
+properties that are about to be changed.
+
+For a ``retire()`` or ``restore()`` operation, newdata is None.
+
+Reactors are called with the arguments::
+
+ react(db, cl, itemid, olddata)
+
+where ``db`` is the database, ``cl`` is an instance of Class or
+IssueClass within the database, and ``olddata`` is a dictionary mapping
+property names to values.
+
+For a ``create()`` operation, the ``itemid`` argument is the id of the
+newly-created item and ``olddata`` is None.
+
+For a ``set()`` operation, ``olddata`` contains the names and previous
+values of properties that were changed.
+
+For a ``retire()`` or ``restore()`` operation, ``itemid`` is the id of
+the retired or restored item and ``olddata`` is None.
+
+
+Additional Detectors Ready For Use
+----------------------------------
+
+Sample additional detectors that have been found useful will appear in
+the ``'detectors'`` directory of the Roundup distribution. If you want
+to use one, copy it to the ``'detectors'`` of your tracker instance:
+
+**newissuecopy.py**
+ This detector sends an email to a team address whenever a new issue is
+ created. The address is hard-coded into the detector, so edit it
+ before you use it (look for the text 'team at team.host') or you'll get
+ email errors!
+**creator_resolution.py**
+ Catch attempts to set the status to "resolved" - if the assignedto
+ user isn't the creator, then set the status to "confirm-done". Note that
+ "classic" Roundup doesn't have that status, so you'll have to add it. If
+ you don't want to though, it'll just use "in-progress" instead.
+**email_auditor.py**
+ If a file added to an issue is of type message/rfc822, we tack on the
+ extension .eml.
+ The reason for this is that Microsoft Internet Explorer will not open
+ things with a .eml attachment, as they deem it 'unsafe'. Worse yet,
+ they'll just give you an incomprehensible error message. For more
+ information, see the detector code - it has a length explanation.
+
+
+Auditor or Reactor?
+-------------------
+
+Generally speaking, the following rules should be observed:
+
+**Auditors**
+ Are used for `vetoing creation of or changes to items`_. They might
+ also make automatic changes to item properties.
+**Reactors**
+ Detect changes in the database and react accordingly. They should avoid
+ making changes to the database where possible, as this could create
+ detector loops.
+
+
+Vetoing creation of or changes to items
+---------------------------------------
+
+Auditors may raise the ``Reject`` exception to prevent the creation of
+or changes to items in the database. The mail gateway, for example, will
+not attach files or messages to issues when the creation of those files or
+messages are prevented through the ``Reject`` exception. It'll also not create
+users if that creation is ``Reject``'ed too.
+
+To use, simply add at the top of your auditor::
+
+ from roundup.exceptions import Reject
+
+And then when your rejection criteria have been detected, simply::
+
+ raise Reject
+
+
+Generating email from Roundup
+-----------------------------
+
+The module ``roundup.mailer`` contains most of the nuts-n-bolts required
+to generate email messages from Roundup.
+
+In addition, the ``IssueClass`` methods ``nosymessage()`` and
+``send_message()`` are used to generate nosy messages, and may generate
+messages which only consist of a change note (ie. the message id parameter
+is not required - this is referred to as a "System Message" because it
+comes from "the system" and not a user).
+
+
+Database Content
+================
+
+.. note::
+ if you modify the content of definitional classes, you'll most
+ likely need to edit the tracker `detectors`_ to reflect your changes.
+
+Customisation of the special "definitional" classes (eg. status,
+priority, resolution, ...) may be done either before or after the
+tracker is initialised. The actual method of doing so is completely
+different in each case though, so be careful to use the right one.
+
+**Changing content before tracker initialisation**
+ Edit the initial_data.py module in your tracker to alter the items
+ created using the ``create( ... )`` methods.
+
+**Changing content after tracker initialisation**
+ As the "admin" user, click on the "class list" link in the web
+ interface to bring up a list of all database classes. Click on the
+ name of the class you wish to change the content of.
+
+ You may also use the ``roundup-admin`` interface's create, set and
+ retire methods to add, alter or remove items from the classes in
+ question.
+
+See "`adding a new field to the classic schema`_" for an example that
+requires database content changes.
+
+
+Security / Access Controls
+==========================
+
+A set of Permissions is built into the security module by default:
+
+- Create (everything)
+- Edit (everything)
+- View (everything)
+
+These are assigned to the "Admin" Role by default, and allow a user to do
+anything. Every Class you define in your `tracker schema`_ also gets an
+Create, Edit and View Permission of its own. The web and email interfaces
+also define:
+
+*Email Access*
+ If defined, the user may use the email interface. Used by default to deny
+ Anonymous users access to the email interface. When granted to the
+ Anonymous user, they will be automatically registered by the email
+ interface (see also the ``new_email_user_roles`` configuration option).
+*Web Access*
+ If defined, the user may use the web interface. All users are able to see
+ the login form, regardless of this setting (thus enabling logging in).
+*Web Roles*
+ Controls user access to editing the "roles" property of the "user" class.
+ TODO: deprecate in favour of a property-based control.
+
+These are hooked into the default Roles:
+
+- Admin (Create, Edit, View and everything; Web Roles)
+- User (Web Access; Email Access)
+- Anonymous (Web Access)
+
+And finally, the "admin" user gets the "Admin" Role, and the "anonymous"
+user gets "Anonymous" assigned when the tracker is installed.
+
+For the "User" Role, the "classic" tracker defines:
+
+- Create, Edit and View issue, file, msg, query, keyword
+- View priority, status
+- View user
+- Edit their own user record
+
+And the "Anonymous" Role is defined as:
+
+- Web interface access
+- Create user (for registration)
+- View issue, file, msg, query, keyword, priority, status
+
+Put together, these settings appear in the tracker's ``schema.py`` file::
+
+ #
+ # 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', 'query', '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)
+
+ #
+ # 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)
+
+
+Automatic Permission Checks
+---------------------------
+
+Permissions are automatically checked when information is rendered
+through the web. This includes:
+
+1. View checks for properties when being rendered via the ``plain()`` or
+ similar methods. If the check fails, the text "[hidden]" will be
+ displayed.
+2. Edit checks for properties when the edit field is being rendered via
+ the ``field()`` or similar methods. If the check fails, the property
+ will be rendered via the ``plain()`` method (see point 1. for subsequent
+ checking performed)
+3. View checks are performed in index pages for each item being displayed
+ such that if the user does not have permission, the row is not rendered.
+4. View checks are performed at the top of item pages for the Item being
+ displayed. If the user does not have permission, the text "You are not
+ allowed to view this page." will be displayed.
+5. View checks are performed at the top of index pages for the Class being
+ displayed. If the user does not have permission, the text "You are not
+ allowed to view this page." will be displayed.
+
+
+New User Roles
+--------------
+
+New users are assigned the Roles defined in the config file as:
+
+- NEW_WEB_USER_ROLES
+- NEW_EMAIL_USER_ROLES
+
+The `users may only edit their issues`_ example shows customisation of
+these parameters.
+
+
+Changing Access Controls
+------------------------
+
+You may alter the configuration variables to change the Role that new
+web or email users get, for example to not give them access to the web
+interface if they register through email.
+
+You may use the ``roundup-admin`` "``security``" command to display the
+current Role and Permission configuration in your tracker.
+
+
+Adding a new Permission
+~~~~~~~~~~~~~~~~~~~~~~~
+
+When adding a new Permission, you will need to:
+
+1. add it to your tracker's ``schema.py`` so it is created, using
+ ``security.addPermission``, for example::
+
+ self.security.addPermission(name="View", klass='frozzle',
+ description="User is allowed to access frozzles")
+
+ will set up a new "View" permission on the Class "frozzle".
+2. enable it for the Roles that should have it (verify with
+ "``roundup-admin security``")
+3. add it to the relevant HTML interface templates
+4. add it to the appropriate xxxPermission methods on in your tracker
+ interfaces module
+
+The ``addPermission`` method takes a couple of optional parameters:
+
+**properties**
+ A sequence of property names that are the only properties to apply the
+ new Permission to (eg. ``... klass='user', properties=('name',
+ 'email') ...``)
+**check**
+ A function to be execute which returns boolean determining whether the
+ Permission is allowed. The function has the signature ``check(db, userid,
+ itemid)`` where ``db`` is a handle on the open database, ``userid`` is
+ the user attempting access and ``itemid`` is the specific item being
+ accessed.
+
+Example Scenarios
+~~~~~~~~~~~~~~~~~
+
+See the `examples`_ section for longer examples of customisation.
+
+**anonymous access through the e-mail gateway**
+ Give the "anonymous" user the "Email Access", ("Edit", "issue") and
+ ("Create", "msg") Permissions but do not not give them the ("Create",
+ "user") Permission. This means that when an unknown user sends email
+ into the tracker, they're automatically logged in as "anonymous".
+ Since they don't have the ("Create", "user") Permission, they won't
+ be automatically registered, but since "anonymous" has permission to
+ use the gateway, they'll still be able to submit issues. Note that
+ the Sender information - their email address - will not be available
+ - they're *anonymous*.
+
+**automatic registration of users in the e-mail gateway**
+ By giving the "anonymous" user the ("Create", "user" Permission, any
+ unidentified user will automatically be registered with the tracker
+ (with no password, so they won't be able to log in through
+ the web until an admin sets their password). This is the default
+ behaviour in the tracker templates that ship with Roundup. The new user
+ is given the Roles list defined in the "new_email_user_roles" config
+ variable.
+
+**only developers may be assigned issues**
+ Create a new Permission called "Fixer" for the "issue" class. Create a
+ new Role "Developer" which has that Permission, and assign that to the
+ appropriate users. Filter the list of users available in the assignedto
+ list to include only those users. Enforce the Permission with an
+ auditor. See the example
+ `restricting the list of users that are assignable to a task`_.
+
+**only managers may sign off issues as complete**
+ Create a new Permission called "Closer" for the "issue" class. Create a
+ new Role "Manager" which has that Permission, and assign that to the
+ appropriate users. In your web interface, only display the "resolved"
+ issue state option when the user has the "Closer" Permissions. Enforce
+ the Permission with an auditor. This is very similar to the previous
+ example, except that the web interface check would look like::
+
+ <option tal:condition="python:request.user.hasPermission('Closer')"
+ value="resolved">Resolved</option>
+
+**don't give web access to users who register through email**
+ Create a new Role called "Email User" which has all the Permissions of
+ the normal "User" Role minus the "Web Access" Permission. This will
+ allow users to send in emails to the tracker, but not access the web
+ interface.
+
+**let some users edit the details of all users**
+ Create a new Role called "User Admin" which has the Permission for
+ editing users::
+
+ db.security.addRole(name='User Admin', description='Managing users')
+ p = db.security.getPermission('Edit', 'user')
+ db.security.addPermissionToRole('User Admin', p)
+
+ and assign the Role to the users who need the permission.
+
+
+Web Interface
+=============
+
+.. contents::
+ :local:
+
+The web interface is provided by the ``roundup.cgi.client`` module and
+is used by ``roundup.cgi``, ``roundup-server`` and ``ZRoundup``
+(``ZRoundup`` is broken, until further notice). In all cases, we
+determine which tracker is being accessed (the first part of the URL
+path inside the scope of the CGI handler) and pass control on to the
+``roundup.cgi.client.Client`` class - which handles the rest of the
+access through its ``main()`` method. This means that you can do pretty
+much anything you want as a web interface to your tracker.
+
+
+
+Repercussions of changing the tracker schema
+---------------------------------------------
+
+If you choose to change the `tracker schema`_ you will need to ensure
+the web interface knows about it:
+
+1. Index, item and search pages for the relevant classes may need to
+ have properties added or removed,
+2. The "page" template may require links to be changed, as might the
+ "home" page's content arguments.
+
+
+How requests are processed
+--------------------------
+
+The basic processing of a web request proceeds as follows:
+
+1. figure out who we are, defaulting to the "anonymous" user
+2. figure out what the request is for - we call this the "context"
+3. handle any requested action (item edit, search, ...)
+4. render the template requested by the context, resulting in HTML
+ output
+
+In some situations, exceptions occur:
+
+- HTTP Redirect (generally raised by an action)
+- SendFile (generally raised by ``determine_context``)
+ here we serve up a FileClass "content" property
+- SendStaticFile (generally raised by ``determine_context``)
+ here we serve up a file from the tracker "html" directory
+- Unauthorised (generally raised by an action)
+ here the action is cancelled, the request is rendered and an error
+ message is displayed indicating that permission was not granted for
+ the action to take place
+- NotFound (raised wherever it needs to be)
+ this exception percolates up to the CGI interface that called the
+ client
+
+
+Determining web context
+-----------------------
+
+To determine the "context" of a request, we look at the URL and the
+special request variable ``@template``. The URL path after the tracker
+identifier is examined. Typical URL paths look like:
+
+1. ``/tracker/issue``
+2. ``/tracker/issue1``
+3. ``/tracker/@@file/style.css``
+4. ``/cgi-bin/roundup.cgi/tracker/file1``
+5. ``/cgi-bin/roundup.cgi/tracker/file1/kitten.png``
+
+where the "tracker identifier" is "tracker" in the above cases. That means
+we're looking at "issue", "issue1", "@@file/style.css", "file1" and
+"file1/kitten.png" in the cases above. The path is generally only one
+entry long - longer paths are handled differently.
+
+a. if there is no path, then we are in the "home" context. See `the "home"
+ context`_ below for more information about how it may be used.
+b. if the path starts with "@@file" (as in example 3,
+ "/tracker/@@file/style.css"), then the additional path entry,
+ "style.css" specifies the filename of a static file we're to serve up
+ from the tracker TEMPLATES (or STATIC_FILES, if configured) directory.
+ This is usually the tracker's "html" directory. Raises a SendStaticFile
+ exception.
+c. if there is something in the path (as in example 1, "issue"), it
+ identifies the tracker class we're to display.
+d. if the path is an item designator (as in examples 2 and 4, "issue1"
+ and "file1"), then we're to display a specific item.
+e. if the path starts with an item designator and is longer than one
+ entry (as in example 5, "file1/kitten.png"), then we're assumed to be
+ handling an item of a ``FileClass``, and the extra path information
+ gives the filename that the client is going to label the download
+ with (i.e. "file1/kitten.png" is nicer to download than "file1").
+ This raises a ``SendFile`` exception.
+
+Both b. and e. stop before we bother to determine the template we're
+going to use. That's because they don't actually use templates.
+
+The template used is specified by the ``@template`` CGI variable, which
+defaults to:
+
+- only classname suplied: "index"
+- full item designator supplied: "item"
+
+
+The "home" Context
+------------------
+
+The "home" context is special because it allows you to add templated
+pages to your tracker that don't rely on a class or item (ie. an issues
+list or specific issue).
+
+Let's say you wish to add frames to control the layout of your tracker's
+interface. You'd probably have:
+
+- A top-level frameset page. This page probably wouldn't be templated, so
+ it could be served as a static file (see `serving static content`_)
+- A sidebar frame that is templated. Let's call this page
+ "home.navigation.html" in your tracker's "html" directory. To load that
+ page up, you use the URL:
+
+ <tracker url>/home?@template=navigation
+
+
+Serving static content
+----------------------
+
+See the previous section `determining web context`_ where it describes
+``@@file`` paths.
+
+
+Performing actions in web requests
+----------------------------------
+
+When a user requests a web page, they may optionally also request for an
+action to take place. As described in `how requests are processed`_, the
+action is performed before the requested page is generated. Actions are
+triggered by using a ``@action`` CGI variable, where the value is one
+of:
+
+**login**
+ Attempt to log a user in.
+
+**logout**
+ Log the user out - make them "anonymous".
+
+**register**
+ Attempt to create a new user based on the contents of the form and then
+ log them in.
+
+**edit**
+ Perform an edit of an item in the database. There are some `special form
+ variables`_ you may use.
+
+**new**
+ Add a new item to the database. You may use the same `special form
+ variables`_ as in the "edit" action.
+
+**retire**
+ Retire the item in the database.
+
+**editCSV**
+ Performs an edit of all of a class' items in one go. See also the
+ *class*.csv templating method which generates the CSV data to be
+ edited, and the ``'_generic.index'`` template which uses both of these
+ features.
+
+**search**
+ Mangle some of the form variables:
+
+ - Set the form ":filter" variable based on the values of the filter
+ variables - if they're set to anything other than "dontcare" then add
+ them to :filter.
+
+ - Also handle the ":queryname" variable and save off the query to the
+ user's query list.
+
+Each of the actions is implemented by a corresponding ``*XxxAction*`` (where
+"Xxx" is the name of the action) class in the ``roundup.cgi.actions`` module.
+These classes are registered with ``roundup.cgi.client.Client``. If you need
+to define new actions, you may add them there (see `defining new
+web actions`_).
+
+Each action class also has a ``*permission*`` method which determines whether
+the action is permissible given the current user. The base permission checks
+for each action are:
+
+**login**
+ Determine whether the user has the "Web Access" Permission.
+**logout**
+ No permission checks are made.
+**register**
+ Determine whether the user has the ("Create", "user") Permission.
+**edit**
+ Determine whether the user has permission to edit this item. If we're
+ editing the "user" class, users are allowed to edit their own details -
+ unless they try to edit the "roles" property, which requires the
+ special Permission "Web Roles".
+**new**
+ Determine whether the user has permission to create this item. No
+ additional property checks are made. Additionally, new user items may
+ be created if the user has the ("Create", "user") Permission.
+**editCSV**
+ Determine whether the user has permission to edit this class.
+**search**
+ Determine whether the user has permission to view this class.
+
+
+Special form variables
+----------------------
+
+Item properties and their values are edited with html FORM
+variables and their values. You can:
+
+- Change the value of some property of the current item.
+- Create a new item of any class, and edit the new item's
+ properties,
+- Attach newly created items to a multilink property of the
+ current item.
+- Remove items from a multilink property of the current item.
+- Specify that some properties are required for the edit
+ operation to be successful.
+- Set up user interface locale.
+
+These operations will only take place if the form action (the
+``@action`` variable) is "edit" or "new".
+
+In the following, <bracketed> values are variable, "@" may be
+either ":" or "@", and other text "required" is fixed.
+
+Two special form variables are used to specify user language preferences:
+
+``@language``
+ value may be locale name or ``none``. If this variable is set to
+ locale name, web interface language is changed to given value
+ (provided that appropriate translation is available), the value
+ is stored in the browser cookie and will be used for all following
+ requests. If value is ``none`` the cookie is removed and the
+ language is changed to the tracker default, set up in the tracker
+ configuration or OS environment.
+
+``@charset``
+ value may be character set name or ``none``. Character set name
+ is stored in the browser cookie and sets output encoding for all
+ HTML pages generated by Roundup. If value is ``none`` the cookie
+ is removed and HTML output is reset to Roundup internal encoding
+ (UTF-8).
+
+Most properties are specified as form variables:
+
+``<propname>``
+ property on the current context item
+
+``<designator>"@"<propname>``
+ property on the indicated item (for editing related information)
+
+Designators name a specific item of a class.
+
+``<classname><N>``
+ Name an existing item of class <classname>.
+
+``<classname>"-"<N>``
+ Name the <N>th new item of class <classname>. If the form
+ submission is successful, a new item of <classname> is
+ created. Within the submitted form, a particular
+ designator of this form always refers to the same new
+ item.
+
+Once we have determined the "propname", we look at it to see
+if it's special:
+
+``@required``
+ The associated form value is a comma-separated list of
+ property names that must be specified when the form is
+ submitted for the edit operation to succeed.
+
+ When the <designator> is missing, the properties are
+ for the current context item. When <designator> is
+ present, they are for the item specified by
+ <designator>.
+
+ The "@required" specifier must come before any of the
+ properties it refers to are assigned in the form.
+
+``@remove@<propname>=id(s)`` or ``@add@<propname>=id(s)``
+ The "@add@" and "@remove@" edit actions apply only to
+ Multilink properties. The form value must be a
+ comma-separate list of keys for the class specified by
+ the simple form variable. The listed items are added
+ to (respectively, removed from) the specified
+ property.
+
+``@link@<propname>=<designator>``
+ If the edit action is "@link@", the simple form
+ variable must specify a Link or Multilink property.
+ The form value is a comma-separated list of
+ designators. The item corresponding to each
+ designator is linked to the property given by simple
+ form variable.
+
+None of the above (ie. just a simple form value)
+ The value of the form variable is converted
+ appropriately, depending on the type of the property.
+
+ For a Link('klass') property, the form value is a
+ single key for 'klass', where the key field is
+ specified in schema.py.
+
+ For a Multilink('klass') property, the form value is a
+ comma-separated list of keys for 'klass', where the
+ key field is specified in schema.py.
+
+ Note that for simple-form-variables specifiying Link
+ and Multilink properties, the linked-to class must
+ have a key field.
+
+ For a String() property specifying a filename, the
+ file named by the form value is uploaded. This means we
+ try to set additional properties "filename" and "type" (if
+ they are valid for the class). Otherwise, the property
+ is set to the form value.
+
+ For Date(), Interval(), Boolean(), and Number()
+ properties, the form value is converted to the
+ appropriate
+
+Any of the form variables may be prefixed with a classname or
+designator.
+
+Two special form values are supported for backwards compatibility:
+
+ at note
+ This is equivalent to::
+
+ @link at messages=msg-1
+ msg-1 at content=value
+
+ except that in addition, the "author" and "date" properties of
+ "msg-1" are set to the userid of the submitter, and the current
+ time, respectively.
+
+ at file
+ This is equivalent to::
+
+ @link at files=file-1
+ file-1 at content=value
+
+ The String content value is handled as described above for file
+ uploads.
+
+If both the "@note" and "@file" form variables are
+specified, the action::
+
+ @link at msg-1@files=file-1
+
+is also performed.
+
+We also check that FileClass items have a "content" property with
+actual content, otherwise we remove them from all_props before
+returning.
+
+
+Default templates
+-----------------
+
+The default templates are html4 compliant. If you wish to change them to be
+xhtml compliant, you'll need to change the ``html_version`` configuration
+variable in ``config.ini`` to ``'xhtml'`` instead of ``'html4'``.
+
+Most customisation of the web view can be done by modifying the
+templates in the tracker ``'html'`` directory. There are several types
+of files in there. The *minimal* template includes:
+
+**page.html**
+ This template usually defines the overall look of your tracker. When
+ you view an issue, it appears inside this template. When you view an
+ index, it also appears inside this template. This template defines a
+ macro called "icing" which is used by almost all other templates as a
+ coating for their content, using its "content" slot. It also defines
+ the "head_title" and "body_title" slots to allow setting of the page
+ title.
+**home.html**
+ the default page displayed when no other page is indicated by the user
+**home.classlist.html**
+ a special version of the default page that lists the classes in the
+ tracker
+**classname.item.html**
+ displays an item of the *classname* class
+**classname.index.html**
+ displays a list of *classname* items
+**classname.search.html**
+ displays a search page for *classname* items
+**_generic.index.html**
+ used to display a list of items where there is no
+ ``*classname*.index`` available
+**_generic.help.html**
+ used to display a "class help" page where there is no
+ ``*classname*.help``
+**user.register.html**
+ a special page just for the user class, that renders the registration
+ page
+**style.css.html**
+ a static file that is served up as-is
+
+The *classic* template has a number of additional templates.
+
+Remember that you can create any template extension you want to,
+so if you just want to play around with the templating for new issues,
+you can copy the current "issue.item" template to "issue.test", and then
+access the test template using the "@template" URL argument::
+
+ http://your.tracker.example/tracker/issue?@template=test
+
+and it won't affect your users using the "issue.item" template.
+
+
+How the templates work
+----------------------
+
+
+Basic Templating Actions
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Roundup's templates consist of special attributes on the HTML tags.
+These attributes form the `Template Attribute Language`_, or TAL.
+The basic TAL commands are:
+
+**tal:define="variable expression; variable expression; ..."**
+ Define a new variable that is local to this tag and its contents. For
+ example::
+
+ <html tal:define="title request/description">
+ <head><title tal:content="title"></title></head>
+ </html>
+
+ In this example, the variable "title" is defined as the result of the
+ expression "request/description". The "tal:content" command inside the
+ <html> tag may then use the "title" variable.
+
+**tal:condition="expression"**
+ Only keep this tag and its contents if the expression is true. For
+ example::
+
+ <p tal:condition="python:request.user.hasPermission('View', 'issue')">
+ Display some issue information.
+ </p>
+
+ In the example, the <p> tag and its contents are only displayed if
+ the user has the "View" permission for issues. We consider the number
+ zero, a blank string, an empty list, and the built-in variable
+ nothing to be false values. Nearly every other value is true,
+ including non-zero numbers, and strings with anything in them (even
+ spaces!).
+
+**tal:repeat="variable expression"**
+ Repeat this tag and its contents for each element of the sequence
+ that the expression returns, defining a new local variable and a
+ special "repeat" variable for each element. For example::
+
+ <tr tal:repeat="u user/list">
+ <td tal:content="u/id"></td>
+ <td tal:content="u/username"></td>
+ <td tal:content="u/realname"></td>
+ </tr>
+
+ The example would iterate over the sequence of users returned by
+ "user/list" and define the local variable "u" for each entry. Using
+ the repeat command creates a new variable called "repeat" which you
+ may access to gather information about the iteration. See the section
+ below on `the repeat variable`_.
+
+**tal:replace="expression"**
+ Replace this tag with the result of the expression. For example::
+
+ <span tal:replace="request/user/realname" />
+
+ The example would replace the <span> tag and its contents with the
+ user's realname. If the user's realname was "Bruce", then the
+ resultant output would be "Bruce".
+
+**tal:content="expression"**
+ Replace the contents of this tag with the result of the expression.
+ For example::
+
+ <span tal:content="request/user/realname">user's name appears here
+ </span>
+
+ The example would replace the contents of the <span> tag with the
+ user's realname. If the user's realname was "Bruce" then the
+ resultant output would be "<span>Bruce</span>".
+
+**tal:attributes="attribute expression; attribute expression; ..."**
+ Set attributes on this tag to the results of expressions. For
+ example::
+
+ <a tal:attributes="href string:user${request/user/id}">My Details</a>
+
+ In the example, the "href" attribute of the <a> tag is set to the
+ value of the "string:user${request/user/id}" expression, which will
+ be something like "user123".
+
+**tal:omit-tag="expression"**
+ Remove this tag (but not its contents) if the expression is true. For
+ example::
+
+ <span tal:omit-tag="python:1">Hello, world!</span>
+
+ would result in output of::
+
+ Hello, world!
+
+Note that the commands on a given tag are evaulated in the order above,
+so *define* comes before *condition*, and so on.
+
+Additionally, you may include tags such as <tal:block>, which are
+removed from output. Its content is kept, but the tag itself is not (so
+don't go using any "tal:attributes" commands on it). This is useful for
+making arbitrary blocks of HTML conditional or repeatable (very handy
+for repeating multiple table rows, which would othewise require an
+illegal tag placement to effect the repeat).
+
+.. _TAL:
+.. _Template Attribute Language:
+ http://dev.zope.org/Wikis/DevSite/Projects/ZPT/TAL%20Specification%201.4
+
+
+Templating Expressions
+~~~~~~~~~~~~~~~~~~~~~~
+
+Templating Expressions are covered by `Template Attribute Language
+Expression Syntax`_, or TALES. The expressions you may use in the
+attribute values may be one of the following forms:
+
+**Path Expressions** - eg. ``item/status/checklist``
+ These are object attribute / item accesses. Roughly speaking, the
+ path ``item/status/checklist`` is broken into parts ``item``,
+ ``status`` and ``checklist``. The ``item`` part is the root of the
+ expression. We then look for a ``status`` attribute on ``item``, or
+ failing that, a ``status`` item (as in ``item['status']``). If that
+ fails, the path expression fails. When we get to the end, the object
+ we're left with is evaluated to get a string - if it is a method, it
+ is called; if it is an object, it is stringified. Path expressions
+ may have an optional ``path:`` prefix, but they are the default
+ expression type, so it's not necessary.
+
+ If an expression evaluates to ``default``, then the expression is
+ "cancelled" - whatever HTML already exists in the template will
+ remain (tag content in the case of ``tal:content``, attributes in the
+ case of ``tal:attributes``).
+
+ If an expression evaluates to ``nothing`` then the target of the
+ expression is removed (tag content in the case of ``tal:content``,
+ attributes in the case of ``tal:attributes`` and the tag itself in
+ the case of ``tal:replace``).
+
+ If an element in the path may not exist, then you can use the ``|``
+ operator in the expression to provide an alternative. So, the
+ expression ``request/form/foo/value | default`` would simply leave
+ the current HTML in place if the "foo" form variable doesn't exist.
+
+ You may use the python function ``path``, as in
+ ``path("item/status")``, to embed path expressions in Python
+ expressions.
+
+**String Expressions** - eg. ``string:hello ${user/name}``
+ These expressions are simple string interpolations - though they can
+ be just plain strings with no interpolation if you want. The
+ expression in the ``${ ... }`` is just a path expression as above.
+
+**Python Expressions** - eg. ``python: 1+1``
+ These expressions give the full power of Python. All the "root level"
+ variables are available, so ``python:item.status.checklist()`` would
+ be equivalent to ``item/status/checklist``, assuming that
+ ``checklist`` is a method.
+
+Modifiers:
+
+**structure** - eg. ``structure python:msg.content.plain(hyperlink=1)``
+ The result of expressions are normally *escaped* to be safe for HTML
+ display (all "<", ">" and "&" are turned into special entities). The
+ ``structure`` expression modifier turns off this escaping - the
+ result of the expression is now assumed to be HTML, which is passed
+ to the web browser for rendering.
+
+**not:** - eg. ``not:python:1=1``
+ This simply inverts the logical true/false value of another
+ expression.
+
+.. _TALES:
+.. _Template Attribute Language Expression Syntax:
+ http://dev.zope.org/Wikis/DevSite/Projects/ZPT/TALES%20Specification%201.3
+
+
+Template Macros
+~~~~~~~~~~~~~~~
+
+Macros are used in Roundup to save us from repeating the same common
+page stuctures over and over. The most common (and probably only) macro
+you'll use is the "icing" macro defined in the "page" template.
+
+Macros are generated and used inside your templates using special
+attributes similar to the `basic templating actions`_. In this case,
+though, the attributes belong to the `Macro Expansion Template
+Attribute Language`_, or METAL. The macro commands are:
+
+**metal:define-macro="macro name"**
+ Define that the tag and its contents are now a macro that may be
+ inserted into other templates using the *use-macro* command. For
+ example::
+
+ <html metal:define-macro="page">
+ ...
+ </html>
+
+ defines a macro called "page" using the ``<html>`` tag and its
+ contents. Once defined, macros are stored on the template they're
+ defined on in the ``macros`` attribute. You can access them later on
+ through the ``templates`` variable, eg. the most common
+ ``templates/page/macros/icing`` to access the "page" macro of the
+ "page" template.
+
+**metal:use-macro="path expression"**
+ Use a macro, which is identified by the path expression (see above).
+ This will replace the current tag with the identified macro contents.
+ For example::
+
+ <tal:block metal:use-macro="templates/page/macros/icing">
+ ...
+ </tal:block>
+
+ will replace the tag and its contents with the "page" macro of the
+ "page" template.
+
+**metal:define-slot="slot name"** and **metal:fill-slot="slot name"**
+ To define *dynamic* parts of the macro, you define "slots" which may
+ be filled when the macro is used with a *use-macro* command. For
+ example, the ``templates/page/macros/icing`` macro defines a slot like
+ so::
+
+ <title metal:define-slot="head_title">title goes here</title>
+
+ In your *use-macro* command, you may now use a *fill-slot* command
+ like this::
+
+ <title metal:fill-slot="head_title">My Title</title>
+
+ where the tag that fills the slot completely replaces the one defined
+ as the slot in the macro.
+
+Note that you may not mix `METAL`_ and `TAL`_ commands on the same tag, but
+TAL commands may be used freely inside METAL-using tags (so your
+*fill-slots* tags may have all manner of TAL inside them).
+
+.. _METAL:
+.. _Macro Expansion Template Attribute Language:
+ http://dev.zope.org/Wikis/DevSite/Projects/ZPT/METAL%20Specification%201.0
+
+Information available to templates
+----------------------------------
+
+This is implemented by ``roundup.cgi.templating.RoundupPageTemplate``
+
+The following variables are available to templates.
+
+**context**
+ The current context. This is either None, a `hyperdb class wrapper`_
+ or a `hyperdb item wrapper`_
+**request**
+ Includes information about the current request, including:
+ - 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*
+ The current CGI form information as a mapping of form argument name
+ to value
+**config**
+ This variable holds all the values defined in the tracker config.ini
+ file (eg. TRACKER_NAME, etc.)
+**db**
+ The current database, used to access arbitrary database items.
+**templates**
+ Access to all the tracker templates by name. Used mainly in
+ *use-macro* commands.
+**utils**
+ This variable makes available some utility functions like batching.
+**nothing**
+ This is a special variable - if an expression evaluates to this, then
+ the tag (in the case of a ``tal:replace``), its contents (in the case
+ of ``tal:content``) or some attributes (in the case of
+ ``tal:attributes``) will not appear in the the output. So, for
+ example::
+
+ <span tal:attributes="class nothing">Hello, World!</span>
+
+ would result in::
+
+ <span>Hello, World!</span>
+
+**default**
+ Also a special variable - if an expression evaluates to this, then the
+ existing HTML in the template will not be replaced or removed, it will
+ remain. So::
+
+ <span tal:replace="default">Hello, World!</span>
+
+ would result in::
+
+ <span>Hello, World!</span>
+
+**true**, **false**
+ Boolean constants that may be used in `templating expressions`_
+ instead of ``python:1`` and ``python:0``.
+**i18n**
+ Internationalization service, providing two string translation methods:
+
+ **gettext** (*message*)
+ Return the localized translation of message
+ **ngettext** (*singular*, *plural*, *number*)
+ Like ``gettext()``, but consider plural forms. If a translation
+ is found, apply the plural formula to *number*, and return the
+ resulting message (some languages have more than two plural forms).
+ If no translation is found, return singular if *number* is 1;
+ return plural otherwise.
+
+ This function requires python2.3; in earlier python versions
+ may not work as expected.
+
+The context variable
+~~~~~~~~~~~~~~~~~~~~
+
+The *context* variable is one of three things based on the current
+context (see `determining web context`_ for how we figure this out):
+
+1. if we're looking at a "home" page, then it's None
+2. if we're looking at a specific hyperdb class, it's a
+ `hyperdb class wrapper`_.
+3. if we're looking at a specific hyperdb item, it's a
+ `hyperdb item wrapper`_.
+
+If the context is not None, we can access the properties of the class or
+item. The only real difference between cases 2 and 3 above are:
+
+1. the properties may have a real value behind them, and this will
+ appear if the property is displayed through ``context/property`` or
+ ``context/property/field``.
+2. the context's "id" property will be a false value in the second case,
+ but a real, or true value in the third. Thus we can determine whether
+ we're looking at a real item from the hyperdb by testing
+ "context/id".
+
+Hyperdb class wrapper
+:::::::::::::::::::::
+
+This is implemented by the ``roundup.cgi.templating.HTMLClass``
+class.
+
+This wrapper object provides access to a hyperb class. It is used
+primarily in both index view and new item views, but it's also usable
+anywhere else that you wish to access information about a class, or the
+items of a class, when you don't have a specific item of that class in
+mind.
+
+We allow access to properties. There will be no "id" property. The value
+accessed through the property will be the current value of the same name
+from the CGI form.
+
+There are several methods available on these wrapper objects:
+
+=========== =============================================================
+Method Description
+=========== =============================================================
+properties return a `hyperdb property wrapper`_ for all of this class's
+ properties.
+list lists all of the active (not retired) items in the class.
+csv return the items of this class as a chunk of CSV text.
+propnames lists the names of the properties of this class.
+filter lists of items from this class, filtered and sorted. Two
+ options are avaible for sorting:
+
+ 1. by the current *request* filterspec/filter/sort/group args
+ 2. by the "filterspec", "sort" and "group" keyword args.
+ "filterspec" is ``{propname: value(s)}``. "sort" and
+ "group" are ``(dir, prop)`` where dir is '+', '-' or None
+ and prop is a prop name or None.
+
+ eg. ``issue.filter(filterspec={"priority": "1"},
+ sort=('activity', '+'))``
+
+classhelp display a link to a javascript popup containing this class'
+ "help" template.
+
+ This generates a link to a popup window which displays the
+ properties indicated by "properties" of the class named by
+ "classname". The "properties" should be a comma-separated list
+ (eg. 'id,name,description'). Properties defaults to all the
+ properties of a class (excluding id, creator, created and
+ activity).
+
+ You may optionally override the "label" displayed, the "width",
+ the "height", the number of items per page ("pagesize") and
+ the field on which the list is sorted ("sort").
+
+ With the "filter" arg it is possible to specify a filter for
+ which items are supposed to be displayed. It has to be of
+ the format "<field>=<values>;<field>=<values>;...".
+
+ The popup window will be resizable and scrollable.
+
+ If the "property" arg is given, it's passed through to the
+ javascript help_window function. This allows updating of a
+ property in the calling HTML page.
+
+ If the "form" arg is given, it's passed through to the
+ javascript help_window function - it's the name of the form
+ the "property" belongs to.
+
+submit generate a submit button (and action hidden element)
+renderWith render this class with the given template.
+history returns 'New node - no history' :)
+is_edit_ok is the user allowed to Edit the current class?
+is_view_ok is the user allowed to View the current class?
+=========== =============================================================
+
+Note that if you have a property of the same name as one of the above
+methods, you'll need to access it using a python "item access"
+expression. For example::
+
+ python:context['list']
+
+will access the "list" property, rather than the list method.
+
+
+Hyperdb item wrapper
+::::::::::::::::::::
+
+This is implemented by the ``roundup.cgi.templating.HTMLItem``
+class.
+
+This wrapper object provides access to a hyperb item.
+
+We allow access to properties. There will be no "id" property. The value
+accessed through the property will be the current value of the same name
+from the CGI form.
+
+There are several methods available on these wrapper objects:
+
+=============== ========================================================
+Method Description
+=============== ========================================================
+submit generate a submit button (and action hidden element)
+journal return the journal of the current item (**not
+ implemented**)
+history render the journal of the current item as HTML
+renderQueryForm specific to the "query" class - render the search form
+ for the query
+hasPermission specific to the "user" class - determine whether the
+ user has a Permission. The signature is::
+
+ hasPermission(self, permission, [classname=],
+ [property=], [itemid=])
+
+ where the classname defaults to the current context.
+hasRole specific to the "user" class - determine whether the
+ user has a Role. The signature is::
+
+ hasRole(self, rolename)
+
+is_edit_ok is the user allowed to Edit the current item?
+is_view_ok is the user allowed to View the current item?
+is_retired is the item retired?
+download_url generate a url-quoted link for download of FileClass
+ item contents (ie. file<id>/<name>)
+copy_url generate a url-quoted link for creating a copy
+ of this item. By default, the copy will acquire
+ all properties of the current item except for
+ ``messages`` and ``files``. This can be overridden
+ by passing ``exclude`` argument which contains a list
+ (or any iterable) of property names that shall not be
+ copied. Database-driven properties like ``id`` or
+ ``activity`` cannot be copied.
+=============== ========================================================
+
+Note that if you have a property of the same name as one of the above
+methods, you'll need to access it using a python "item access"
+expression. For example::
+
+ python:context['journal']
+
+will access the "journal" property, rather than the journal method.
+
+
+Hyperdb property wrapper
+::::::::::::::::::::::::
+
+This is implemented by subclasses of the
+``roundup.cgi.templating.HTMLProperty`` class (``HTMLStringProperty``,
+``HTMLNumberProperty``, and so on).
+
+This wrapper object provides access to a single property of a class. Its
+value may be either:
+
+1. if accessed through a `hyperdb item wrapper`_, then it's a value from
+ the hyperdb
+2. if access through a `hyperdb class wrapper`_, then it's a value from
+ the CGI form
+
+
+The property wrapper has some useful attributes:
+
+=============== ========================================================
+Attribute Description
+=============== ========================================================
+_name the name of the property
+_value the value of the property if any - this is the actual
+ value retrieved from the hyperdb for this property
+=============== ========================================================
+
+There are several methods available on these wrapper objects:
+
+=========== ================================================================
+Method Description
+=========== ================================================================
+plain render a "plain" representation of the property. This method
+ may take two arguments:
+
+ escape
+ If true, escape the text so it is HTML safe (default: no). The
+ reason this defaults to off is that text is usually escaped
+ at a later stage by the TAL commands, unless the "structure"
+ option is used in the template. The following ``tal:content``
+ expressions are all equivalent::
+
+ "structure python:msg.content.plain(escape=1)"
+ "python:msg.content.plain()"
+ "msg/content/plain"
+ "msg/content"
+
+ Usually you'll only want to use the escape option in a
+ complex expression.
+
+ hyperlink
+ If true, turn URLs, email addresses and hyperdb item
+ designators in the text into hyperlinks (default: no). Note
+ that you'll need to use the "structure" TAL option if you
+ want to use this ``tal:content`` expression::
+
+ "structure python:msg.content.plain(hyperlink=1)"
+
+ The text is automatically HTML-escaped before the hyperlinking
+ transformation done in the plain() method.
+
+hyperlinked The same as msg.content.plain(hyperlink=1), but nicer::
+
+ "structure msg/content/hyperlinked"
+
+field render an appropriate form edit field for the property - for
+ most types this is a text entry box, but for Booleans it's a
+ tri-state yes/no/neither selection. This method may take some
+ arguments:
+
+ size
+ Sets the width in characters of the edit field
+
+ format (Date properties only)
+ Sets the format of the date in the field - uses the same
+ format string argument as supplied to the ``pretty`` method
+ below.
+
+stext only on String properties - render the value of the property
+ as StructuredText (requires the StructureText module to be
+ installed separately)
+multiline only on String properties - render a multiline form edit
+ field for the property
+email only on String properties - render the value of the property
+ as an obscured email address
+confirm only on Password properties - render a second form edit field
+ for the property, used for confirmation that the user typed
+ the password correctly. Generates a field with name
+ "name:confirm".
+now only on Date properties - return the current date as a new
+ property
+reldate only on Date properties - render the interval between the date
+ and now
+local only on Date properties - return this date as a new property
+ with some timezone offset, for example::
+
+ python:context.creation.local(10)
+
+ will render the date with a +10 hour offset.
+pretty Date properties - render the date as "dd Mon YYYY" (eg. "19
+ Mar 2004"). Takes an optional format argument, for example::
+
+ python:context.activity.pretty('%Y-%m-%d')
+
+ Will format as "2004-03-19" instead.
+
+ Interval properties - render the interval in a pretty
+ format (eg. "yesterday"). The format arguments are those used
+ in the standard ``strftime`` call (see the `Python Library
+ Reference: time module`__)
+popcal Generate a link to a popup calendar which may be used to
+ edit the date field, for example::
+
+ <span tal:replace="structure context/due/popcal" />
+
+menu only on Link and Multilink properties - render a form select
+ list for this property. Takes a number of optional arguments
+
+ size
+ is used to limit the length of the list labels
+ height
+ is used to set the <select> tag's "size" attribute
+ showid
+ includes the item ids in the list labels
+ additional
+ lists properties which should be included in the label
+ sort_on
+ indicates the property to sort the list on as (direction,
+ (direction, property) where direction is '+' or '-'. A
+ single string with the direction prepended may be used.
+ For example: ('-', 'order'), '+name'.
+ value
+ gives a default value to preselect in the menu
+
+ The remaining keyword arguments are used as conditions for
+ filtering the items in the list - they're passed as the
+ "filterspec" argument to a Class.filter() call. For example::
+
+ <span tal:replace="structure context/status/menu" />
+
+ <span tal:replace="python:context.status.menu(order='+name",
+ value='chatting',
+ filterspec={'status': '1,2,3,4'}" />
+
+sorted only on Multilink properties - produce a list of the linked
+ items sorted by some property, for example::
+
+ python:context.files.sorted('creation')
+
+ Will list the files by upload date.
+reverse only on Multilink properties - produce a list of the linked
+ items in reverse order
+isset returns True if the property has been set to a value
+=========== ================================================================
+
+__ http://docs.python.org/lib/module-time.html
+
+All of the above functions perform checks for permissions required to
+display or edit the data they are manipulating. The simplest case is
+editing an issue title. Including the expression::
+
+ context/title/field
+
+Will present the user with an edit field, if they have edit permission. If
+not, then they will be presented with a static display if they have view
+permission. If they don't even have view permission, then an error message
+is raised, preventing the display of the page, indicating that they don't
+have permission to view the information.
+
+
+The request variable
+~~~~~~~~~~~~~~~~~~~~
+
+This is implemented by the ``roundup.cgi.templating.HTMLRequest``
+class.
+
+The request variable is packed with information about the current
+request.
+
+.. taken from ``roundup.cgi.templating.HTMLRequest`` docstring
+
+=========== ============================================================
+Variable Holds
+=========== ============================================================
+form the CGI form as a cgi.FieldStorage
+env the CGI environment variables
+base the base URL for this tracker
+user a HTMLUser instance for this user
+classname the current classname (possibly None)
+template the current template (suffix, also possibly None)
+form the current CGI form variables in a FieldStorage
+=========== ============================================================
+
+**Index page specific variables (indexing arguments)**
+
+=========== ============================================================
+Variable Holds
+=========== ============================================================
+columns dictionary of the columns to display in an index page
+show a convenience access to columns - request/show/colname will
+ be true if the columns should be displayed, false otherwise
+sort index sort column (direction, column name)
+group index grouping property (direction, column name)
+filter properties to filter the index on
+filterspec values to filter the index on
+search_text text to perform a full-text search on for an index
+=========== ============================================================
+
+There are several methods available on the request variable:
+
+=============== ========================================================
+Method Description
+=============== ========================================================
+description render a description of the request - handle for the
+ page title
+indexargs_form render the current index args as form elements
+indexargs_url render the current index args as a URL
+base_javascript render some javascript that is used by other components
+ of the templating
+batch run the current index args through a filter and return a
+ list of items (see `hyperdb item wrapper`_, and
+ `batching`_)
+=============== ========================================================
+
+The form variable
+:::::::::::::::::
+
+The form variable is a bit special because it's actually a python
+FieldStorage object. That means that you have two ways to access its
+contents. For example, to look up the CGI form value for the variable
+"name", use the path expression::
+
+ request/form/name/value
+
+or the python expression::
+
+ python:request.form['name'].value
+
+Note the "item" access used in the python case, and also note the
+explicit "value" attribute we have to access. That's because the form
+variables are stored as MiniFieldStorages. If there's more than one
+"name" value in the form, then the above will break since
+``request/form/name`` is actually a *list* of MiniFieldStorages. So it's
+best to know beforehand what you're dealing with.
+
+
+The db variable
+~~~~~~~~~~~~~~~
+
+This is implemented by the ``roundup.cgi.templating.HTMLDatabase``
+class.
+
+Allows access to all hyperdb classes as attributes of this variable. If
+you want access to the "user" class, for example, you would use::
+
+ db/user
+ python:db.user
+
+Also, the current id of the current user is available as
+``db.getuid()``. This isn't so useful in templates (where you have
+``request/user``), but it can be useful in detectors or interfaces.
+
+The access results in a `hyperdb class wrapper`_.
+
+
+The templates variable
+~~~~~~~~~~~~~~~~~~~~~~
+
+This is implemented by the ``roundup.cgi.templating.Templates``
+class.
+
+This variable doesn't have any useful methods defined. It supports being
+used in expressions to access the templates, and consequently the
+template macros. You may access the templates using the following path
+expression::
+
+ templates/name
+
+or the python expression::
+
+ templates[name]
+
+where "name" is the name of the template you wish to access. The
+template has one useful attribute, namely "macros". To access a specific
+macro (called "macro_name"), use the path expression::
+
+ templates/name/macros/macro_name
+
+or the python expression::
+
+ templates[name].macros[macro_name]
+
+The repeat variable
+~~~~~~~~~~~~~~~~~~~
+
+The repeat variable holds an entry for each active iteration. That is, if
+you have a ``tal:repeat="user db/users"`` command, then there will be a
+repeat variable entry called "user". This may be accessed as either::
+
+ repeat/user
+ python:repeat['user']
+
+The "user" entry has a number of methods available for information:
+
+=============== =========================================================
+Method Description
+=============== =========================================================
+first True if the current item is the first in the sequence.
+last True if the current item is the last in the sequence.
+even True if the current item is an even item in the sequence.
+odd True if the current item is an odd item in the sequence.
+number Current position in the sequence, starting from 1.
+letter Current position in the sequence as a letter, a through
+ z, then aa through zz, and so on.
+Letter Same as letter(), except uppercase.
+roman Current position in the sequence as lowercase roman
+ numerals.
+Roman Same as roman(), except uppercase.
+=============== =========================================================
+
+
+The utils variable
+~~~~~~~~~~~~~~~~~~
+
+This is implemented by the
+``roundup.cgi.templating.TemplatingUtils`` class, but it may be extended
+as described below.
+
+=============== ========================================================
+Method Description
+=============== ========================================================
+Batch return a batch object using the supplied list
+url_quote quote some text as safe for a URL (ie. space, %, ...)
+html_quote quote some text as safe in HTML (ie. <, >, ...)
+html_calendar renders an HTML calendar used by the
+ ``_generic.calendar.html`` template (itself invoked by
+ the popupCalendar DateHTMLProperty method
+=============== ========================================================
+
+You may add additional utility methods by writing them in your tracker
+``extensions`` directory and registering them with the templating system
+using ``instance.registerUtil`` (see `adding a time log to your issues`_ for
+an example of this).
+
+
+Batching
+::::::::
+
+Use Batch to turn a list of items, or item ids of a given class, into a
+series of batches. Its usage is::
+
+ python:utils.Batch(sequence, size, start, end=0, orphan=0,
+ overlap=0)
+
+or, to get the current index batch::
+
+ request/batch
+
+The parameters are:
+
+========= ==============================================================
+Parameter Usage
+========= ==============================================================
+sequence a list of HTMLItems
+size how big to make the sequence.
+start where to start (0-indexed) in the sequence.
+end where to end (0-indexed) in the sequence.
+orphan if the next batch would contain less items than this value,
+ then it is combined with this batch
+overlap the number of items shared between adjacent batches
+========= ==============================================================
+
+All of the parameters are assigned as attributes on the batch object. In
+addition, it has several more attributes:
+
+=============== ========================================================
+Attribute Description
+=============== ========================================================
+start indicates the start index of the batch. *Unlike
+ the argument, is a 1-based index (I know, lame)*
+first indicates the start index of the batch *as a 0-based
+ index*
+length the actual number of elements in the batch
+sequence_length the length of the original, unbatched, sequence.
+=============== ========================================================
+
+And several methods:
+
+=============== ========================================================
+Method Description
+=============== ========================================================
+previous returns a new Batch with the previous batch settings
+next returns a new Batch with the next batch settings
+propchanged detect if the named property changed on the current item
+ when compared to the last item
+=============== ========================================================
+
+An example of batching::
+
+ <table class="otherinfo">
+ <tr><th colspan="4" class="header">Existing Keywords</th></tr>
+ <tr tal:define="keywords db/keyword/list"
+ tal:repeat="start python:range(0, len(keywords), 4)">
+ <td tal:define="batch python:utils.Batch(keywords, 4, start)"
+ tal:repeat="keyword batch" tal:content="keyword/name">
+ keyword here</td>
+ </tr>
+ </table>
+
+... which will produce a table with four columns containing the items of
+the "keyword" class (well, their "name" anyway).
+
+
+Displaying Properties
+---------------------
+
+Properties appear in the user interface in three contexts: in indices,
+in editors, and as search arguments. For each type of property, there
+are several display possibilities. For example, in an index view, a
+string property may just be printed as a plain string, but in an editor
+view, that property may be displayed in an editable field.
+
+
+Index Views
+-----------
+
+This is one of the class context views. It is also the default view for
+classes. The template used is "*classname*.index".
+
+
+Index View Specifiers
+~~~~~~~~~~~~~~~~~~~~~
+
+An index view specifier (URL fragment) looks like this (whitespace has
+been added for clarity)::
+
+ /issue?status=unread,in-progress,resolved&
+ topic=security,ui&
+ @group=priority&
+ @sort=-activity&
+ @filters=status,topic&
+ @columns=title,status,fixer
+
+The index view is determined by two parts of the specifier: the layout
+part and the filter part. The layout part consists of the query
+parameters that begin with colons, and it determines the way that the
+properties of selected items are displayed. The filter part consists of
+all the other query parameters, and it determines the criteria by which
+items are selected for display. The filter part is interactively
+manipulated with the form widgets displayed in the filter section. The
+layout part is interactively manipulated by clicking on the column
+headings in the table.
+
+The filter part selects the union of the sets of items with values
+matching any specified Link properties and the intersection of the sets
+of items with values matching any specified Multilink properties.
+
+The example specifies an index of "issue" items. Only items with a
+"status" of either "unread" or "in-progress" or "resolved" are
+displayed, and only items with "topic" values including both "security"
+and "ui" are displayed. The items are grouped by priority, arranged in
+ascending order; and within groups, sorted by activity, arranged in
+descending order. The filter section shows filters for the "status" and
+"topic" properties, and the table includes columns for the "title",
+"status", and "fixer" properties.
+
+============ =============================================================
+Argument Description
+============ =============================================================
+ at sort sort by prop name, optionally preceeded with '-' to give
+ descending or nothing for ascending sorting.
+ at group group by prop name, optionally preceeded with '-' or to sort
+ in descending or nothing for ascending order.
+ at columns selects the columns that should be displayed. Default is
+ all.
+ at filter indicates which properties are being used in filtering.
+ Default is none.
+propname selects the values the item properties given by propname must
+ have (very basic search/filter).
+ at search_text if supplied, performs a full-text search (message bodies,
+ issue titles, etc)
+============ =============================================================
+
+
+Searching Views
+---------------
+
+.. note::
+ if you add a new column to the ``@columns`` form variable potentials
+ then you will need to add the column to the appropriate `index views`_
+ template so that it is actually displayed.
+
+This is one of the class context views. The template used is typically
+"*classname*.search". The form on this page should have "search" as its
+``@action`` variable. The "search" action:
+
+- sets up additional filtering, as well as performing indexed text
+ searching
+- sets the ``@filter`` variable correctly
+- saves the query off if ``@query_name`` is set.
+
+The search page should lay out any fields that you wish to allow the
+user to search on. If your schema contains a large number of properties,
+you should be wary of making all of those properties available for
+searching, as this can cause confusion. If the additional properties are
+Strings, consider having their value indexed, and then they will be
+searchable using the full text indexed search. This is both faster, and
+more useful for the end user.
+
+If the search view does specify the "search" ``@action``, then it may also
+provide an additional argument:
+
+============ =============================================================
+Argument Description
+============ =============================================================
+ at query_name if supplied, the index parameters (including @search_text)
+ will be saved off as a the query item and registered against
+ the user's queries property. Note that the *classic* template
+ schema has this ability, but the *minimal* template schema
+ does not.
+============ =============================================================
+
+
+Item Views
+----------
+
+The basic view of a hyperdb item is provided by the "*classname*.item"
+template. It generally has three sections; an "editor", a "spool" and a
+"history" section.
+
+
+Editor Section
+~~~~~~~~~~~~~~
+
+The editor section is used to manipulate the item - it may be a static
+display if the user doesn't have permission to edit the item.
+
+Here's an example of a basic editor template (this is the default
+"classic" template issue item edit form - from the "issue.item.html"
+template)::
+
+ <table class="form">
+ <tr>
+ <th>Title</th>
+ <td colspan="3" tal:content="structure python:context.title.field(size=60)">title</td>
+ </tr>
+
+ <tr>
+ <th>Priority</th>
+ <td tal:content="structure context/priority/menu">priority</td>
+ <th>Status</th>
+ <td tal:content="structure context/status/menu">status</td>
+ </tr>
+
+ <tr>
+ <th>Superseder</th>
+ <td>
+ <span tal:replace="structure python:context.superseder.field(showid=1, size=20)" />
+ <span tal:replace="structure python:db.issue.classhelp('id,title')" />
+ <span tal:condition="context/superseder">
+ <br>View: <span tal:replace="structure python:context.superseder.link(showid=1)" />
+ </span>
+ </td>
+ <th>Nosy List</th>
+ <td>
+ <span tal:replace="structure context/nosy/field" />
+ <span tal:replace="structure python:db.user.classhelp('username,realname,address,phone')" />
+ </td>
+ </tr>
+
+ <tr>
+ <th>Assigned To</th>
+ <td tal:content="structure context/assignedto/menu">
+ assignedto menu
+ </td>
+ <td> </td>
+ <td> </td>
+ </tr>
+
+ <tr>
+ <th>Change Note</th>
+ <td colspan="3">
+ <textarea name=":note" wrap="hard" rows="5" cols="60"></textarea>
+ </td>
+ </tr>
+
+ <tr>
+ <th>File</th>
+ <td colspan="3"><input type="file" name=":file" size="40"></td>
+ </tr>
+
+ <tr>
+ <td> </td>
+ <td colspan="3" tal:content="structure context/submit">
+ submit button will go here
+ </td>
+ </tr>
+ </table>
+
+
+When a change is submitted, the system automatically generates a message
+describing the changed properties. As shown in the example, the editor
+template can use the ":note" and ":file" fields, which are added to the
+standard changenote message generated by Roundup.
+
+
+Form values
+:::::::::::
+
+We have a number of ways to pull properties out of the form in order to
+meet the various needs of:
+
+1. editing the current item (perhaps an issue item)
+2. editing information related to the current item (eg. messages or
+ attached files)
+3. creating new information to be linked to the current item (eg. time
+ spent on an issue)
+
+In the following, ``<bracketed>`` values are variable, ":" may be one of
+":" or "@", and other text ("required") is fixed.
+
+Properties are specified as form variables:
+
+``<propname>``
+ property on the current context item
+
+``<designator>:<propname>``
+ property on the indicated item (for editing related information)
+
+``<classname>-<N>:<propname>``
+ property on the Nth new item of classname (generally for creating new
+ items to attach to the current item)
+
+Once we have determined the "propname", we check to see if it is one of
+the special form values:
+
+``@required``
+ The named property values must be supplied or a ValueError will be
+ raised.
+
+``@remove@<propname>=id(s)``
+ The ids will be removed from the multilink property.
+
+``:add:<propname>=id(s)``
+ The ids will be added to the multilink property.
+
+``:link:<propname>=<designator>``
+ Used to add a link to new items created during edit. These are
+ collected and returned in ``all_links``. This will result in an
+ additional linking operation (either Link set or Multilink append)
+ after the edit/create is done using ``all_props`` in ``_editnodes``.
+ The <propname> on the current item will be set/appended the id of the
+ newly created item of class <designator> (where <designator> must be
+ <classname>-<N>).
+
+Any of the form variables may be prefixed with a classname or
+designator.
+
+Two special form values are supported for backwards compatibility:
+
+``:note``
+ create a message (with content, author and date), linked to the
+ context item. This is ALWAYS designated "msg-1".
+``:file``
+ create a file, attached to the current item and any message created by
+ :note. This is ALWAYS designated "file-1".
+
+
+Spool Section
+~~~~~~~~~~~~~
+
+The spool section lists related information like the messages and files
+of an issue.
+
+TODO
+
+
+History Section
+~~~~~~~~~~~~~~~
+
+The final section displayed is the history of the item - its database
+journal. This is generally generated with the template::
+
+ <tal:block tal:replace="structure context/history" />
+
+*To be done:*
+
+*The actual history entries of the item may be accessed for manual
+templating through the "journal" method of the item*::
+
+ <tal:block tal:repeat="entry context/journal">
+ a journal entry
+ </tal:block>
+
+*where each journal entry is an HTMLJournalEntry.*
+
+
+Defining new web actions
+------------------------
+
+You may define new actions to be triggered by the ``@action`` form variable.
+These are added to the tracker ``extensions`` directory and registered
+using ``instance.registerAction``.
+
+All the existing Actions are defined in ``roundup.cgi.actions``.
+
+Adding action classes takes three steps; first you `define the new
+action class`_, then you `register the action class`_ with the cgi
+interface so it may be triggered by the ``@action`` form variable.
+Finally you `use the new action`_ in your HTML form.
+
+See "`setting up a "wizard" (or "druid") for controlled adding of
+issues`_" for an example.
+
+
+Define the new action class
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create a new action class in your tracker's ``extensions`` directory, for
+example ``myaction.py``::
+
+ from roundup.cgi.actions import Action
+
+ class MyAction(Action):
+ def handle(self):
+ ''' Perform some action. No return value is required.
+ '''
+
+The *self.client* attribute is an instance of ``roundup.cgi.client.Client``.
+See the docstring of that class for details of what it can do.
+
+The method will typically check the ``self.form`` variable's contents.
+It may then:
+
+- add information to ``self.client.ok_message`` or ``self.client.error_message``
+- change the ``self.client.template`` variable to alter what the user will see
+ next
+- raise Unauthorised, SendStaticFile, SendFile, NotFound or Redirect
+ exceptions (import them from roundup.cgi.exceptions)
+
+
+Register the action class
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The class is now written, but isn't available to the user until you register
+it with the following code appended to your ``myaction.py`` file::
+
+ def init(instance):
+ instance.registerAction('myaction', myActionClass)
+
+This maps the action name "myaction" to the action class we defined.
+
+
+Use the new action
+~~~~~~~~~~~~~~~~~~
+
+In your HTML form, add a hidden form element like so::
+
+ <input type="hidden" name="@action" value="myaction">
+
+where "myaction" is the name you registered in the previous step.
+
+Actions may return content to the user
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Actions generally perform some database manipulation and then pass control
+on to the rendering of a template in the current context (see `Determining
+web context`_ for how that works.) Some actions will want to generate the
+actual content returned to the user. Action methods may return their own
+content string to be displayed to the user, overriding the templating step.
+In this situation, we assume that the content is HTML by default. You may
+override the content type indicated to the user by calling ``setHeader``::
+
+ self.client.setHeader('Content-Type', 'text/csv')
+
+This example indicates that the value sent back to the user is actually
+comma-separated value content (eg. something to be loaded into a
+spreadsheet or database).
+
+
+8-bit character set support in Web interface
+--------------------------------------------
+
+The web interface uses UTF-8 default. It may be overridden in both forms
+and a browser cookie.
+
+- In forms, use the ``@charset`` variable.
+- To use the cookie override, have the ``roundup_charset`` cookie set.
+
+In both cases, the value is a valid charset name (eg. ``utf-8`` or
+``kio8-r``).
+
+Inside Roundup, all strings are stored and processed in utf-8.
+Unfortunately, some older browsers do not work properly with
+utf-8-encoded pages (e.g. Netscape Navigator 4 displays wrong
+characters in form fields). This version allows one to change
+the character set for http transfers. To do so, you may add
+the following code to your ``page.html`` template::
+
+ <tal:block define="uri string:${request/base}${request/env/PATH_INFO}">
+ <a tal:attributes="href python:request.indexargs_url(uri,
+ {'@charset':'utf-8'})">utf-8</a>
+ <a tal:attributes="href python:request.indexargs_url(uri,
+ {'@charset':'koi8-r'})">koi8-r</a>
+ </tal:block>
+
+(substitute ``koi8-r`` with appropriate charset for your language).
+Charset preference is kept in the browser cookie ``roundup_charset``.
+
+``meta http-equiv`` lines added to the tracker templates in version 0.6.0
+should be changed to include actual character set name::
+
+ <meta http-equiv="Content-Type"
+ tal:attributes="content string:text/html;; charset=${request/client/charset}"
+ />
+
+The charset is also sent in the http header.
+
+
+Examples
+========
+
+.. contents::
+ :local:
+ :depth: 2
+
+
+Changing what's stored in the database
+--------------------------------------
+
+The following examples illustrate ways to change the information stored in
+the database.
+
+
+Adding a new field to the classic schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This example shows how to add a simple field (a due date) to the default
+classic schema. It does not add any additional behaviour, such as enforcing
+the due date, or causing automatic actions to fire if the due date passes.
+
+You add new fields by editing the ``schema.py`` file in you tracker's home.
+Schema changes are automatically applied to the database on the next
+tracker access (note that roundup-server would need to be restarted as it
+caches the schema).
+
+1. modify the schema::
+
+ issue = IssueClass(db, "issue",
+ assignedto=Link("user"), topic=Multilink("keyword"),
+ priority=Link("priority"), status=Link("status"),
+ due_date=Date())
+
+2. add an edit field to the issue.item.html template::
+
+ <tr>
+ <th>Due Date</th>
+ <td tal:content="structure context/due_date/field" />
+ </tr>
+
+3. add the property to the issue.index.html page::
+
+ (in the heading row)
+ <th tal:condition="request/show/due_date">Due Date</th>
+ (in the data row)
+ <td tal:condition="request/show/due_date" tal:content="i/due_date" />
+
+4. add the property to the issue.search.html page::
+
+ <tr tal:define="name string:due_date">
+ <th i18n:translate="">Due Date:</th>
+ <td metal:use-macro="search_input"></td>
+ <td metal:use-macro="column_input"></td>
+ <td metal:use-macro="sort_input"></td>
+ <td metal:use-macro="group_input"></td>
+ </tr>
+
+
+Adding a new constrained field to the classic schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This example shows how to add a new constrained property (i.e. a
+selection of distinct values) to your tracker.
+
+
+Introduction
+::::::::::::
+
+To make the classic schema of Roundup useful as a TODO tracking system
+for a group of systems administrators, it needs an extra data field per
+issue: a category.
+
+This would let sysadmins quickly list all TODOs in their particular area
+of interest without having to do complex queries, and without relying on
+the spelling capabilities of other sysadmins (a losing proposition at
+best).
+
+
+Adding a field to the database
+::::::::::::::::::::::::::::::
+
+This is the easiest part of the change. The category would just be a
+plain string, nothing fancy. To change what is in the database you need
+to add some lines to the ``schema.py`` file of your tracker instance.
+Under the comment::
+
+ # add any additional database schema configuration here
+
+add::
+
+ category = Class(db, "category", name=String())
+ category.setkey("name")
+
+Here we are setting up a chunk of the database which we are calling
+"category". It contains a string, which we are refering to as "name" for
+lack of a more imaginative title. (Since "name" is one of the properties
+that Roundup looks for on items if you do not set a key for them, it's
+probably a good idea to stick with it for new classes if at all
+appropriate.) Then we are setting the key of this chunk of the database
+to be that "name". This is equivalent to an index for database types.
+This also means that there can only be one category with a given name.
+
+Adding the above lines allows us to create categories, but they're not
+tied to the issues that we are going to be creating. It's just a list of
+categories off on its own, which isn't much use. We need to link it in
+with the issues. To do that, find the lines
+in ``schema.py`` which set up the "issue" class, and then add a link to
+the category::
+
+ issue = IssueClass(db, "issue", ... ,
+ category=Multilink("category"), ... )
+
+The ``Multilink()`` means that each issue can have many categories. If
+you were adding something with a one-to-one relationship to issues (such
+as the "assignedto" property), use ``Link()`` instead.
+
+That is all you need to do to change the schema. The rest of the effort
+is fiddling around so you can actually use the new category.
+
+
+Populating the new category class
+:::::::::::::::::::::::::::::::::
+
+If you haven't initialised the database with the ``roundup-admin``
+"initialise" command, then you can add the following to the tracker
+``initial_data.py`` under the comment::
+
+ # add any additional database creation steps here - but only if you
+ # haven't initialised the database with the admin "initialise" command
+
+Add::
+
+ category = db.getclass('category')
+ category.create(name="scipy")
+ category.create(name="chaco")
+ category.create(name="weave")
+
+If the database has already been initalised, then you need to use the
+``roundup-admin`` tool::
+
+ % roundup-admin -i <tracker home>
+ Roundup <version> ready for input.
+ Type "help" for help.
+ roundup> create category name=scipy
+ 1
+ roundup> create category name=chaco
+ 2
+ roundup> create category name=weave
+ 3
+ roundup> exit...
+ There are unsaved changes. Commit them (y/N)? y
+
+
+Setting up security on the new objects
+::::::::::::::::::::::::::::::::::::::
+
+By default only the admin user can look at and change objects. This
+doesn't suit us, as we want any user to be able to create new categories
+as required, and obviously everyone needs to be able to view the
+categories of issues for it to be useful.
+
+We therefore need to change the security of the category objects. This
+is also done in ``schema.py``.
+
+There are currently two loops which set up permissions and then assign
+them to various roles. Simply add the new "category" to both lists::
+
+ # Assign the access and edit permissions for issue, file and message
+ # to regular users now
+ for cl in 'issue', 'file', 'msg', 'category':
+ p = db.security.getPermission('View', cl)
+ db.security.addPermissionToRole('User', 'View', cl)
+ db.security.addPermissionToRole('User', 'Edit', cl)
+ db.security.addPermissionToRole('User', 'Create', cl)
+
+These lines assign the "View" and "Edit" Permissions to the "User" role,
+so that normal users can view and edit "category" objects.
+
+This is all the work that needs to be done for the database. It will
+store categories, and let users view and edit them. Now on to the
+interface stuff.
+
+
+Changing the web left hand frame
+::::::::::::::::::::::::::::::::
+
+We need to give the users the ability to create new categories, and the
+place to put the link to this functionality is in the left hand function
+bar, under the "Issues" area. The file that defines how this area looks
+is ``html/page.html``, which is what we are going to be editing next.
+
+If you look at this file you can see that it contains a lot of
+"classblock" sections which are chunks of HTML that will be included or
+excluded in the output depending on whether the condition in the
+classblock is met. We are going to add the category code at the end of
+the classblock for the *issue* class::
+
+ <p class="classblock"
+ tal:condition="python:request.user.hasPermission('View', 'category')">
+ <b>Categories</b><br>
+ <a tal:condition="python:request.user.hasPermission('Edit', 'category')"
+ href="category?@template=item">New Category<br></a>
+ </p>
+
+The first two lines is the classblock definition, which sets up a
+condition that only users who have "View" permission for the "category"
+object will have this section included in their output. Next comes a
+plain "Categories" header in bold. Everyone who can view categories will
+get that.
+
+Next comes the link to the editing area of categories. This link will
+only appear if the condition - that the user has "Edit" permissions for
+the "category" objects - is matched. If they do have permission then
+they will get a link to another page which will let the user add new
+categories.
+
+Note that if you have permission to *view* but not to *edit* categories,
+then all you will see is a "Categories" header with nothing underneath
+it. This is obviously not very good interface design, but will do for
+now. I just claim that it is so I can add more links in this section
+later on. However, to fix the problem you could change the condition in
+the classblock statement, so that only users with "Edit" permission
+would see the "Categories" stuff.
+
+
+Setting up a page to edit categories
+::::::::::::::::::::::::::::::::::::
+
+We defined code in the previous section which let users with the
+appropriate permissions see a link to a page which would let them edit
+conditions. Now we have to write that page.
+
+The link was for the *item* template of the *category* object. This
+translates into Roundup looking for a file called ``category.item.html``
+in the ``html`` tracker directory. This is the file that we are going to
+write now.
+
+First, we add an info tag in a comment which doesn't affect the outcome
+of the code at all, but is useful for debugging. If you load a page in a
+browser and look at the page source, you can see which sections come
+from which files by looking for these comments::
+
+ <!-- category.item -->
+
+Next we need to add in the METAL macro stuff so we get the normal page
+trappings::
+
+ <tal:block metal:use-macro="templates/page/macros/icing">
+ <title metal:fill-slot="head_title">Category editing</title>
+ <td class="page-header-top" metal:fill-slot="body_title">
+ <h2>Category editing</h2>
+ </td>
+ <td class="content" metal:fill-slot="content">
+
+Next we need to setup up a standard HTML form, which is the whole
+purpose of this file. We link to some handy javascript which sends the
+form through only once. This is to stop users hitting the send button
+multiple times when they are impatient and thus having the form sent
+multiple times::
+
+ <form method="POST" onSubmit="return submit_once()"
+ enctype="multipart/form-data">
+
+Next we define some code which sets up the minimum list of fields that
+we require the user to enter. There will be only one field - "name" - so
+they better put something in it, otherwise the whole form is pointless::
+
+ <input type="hidden" name="@required" value="name">
+
+To get everything to line up properly we will put everything in a table,
+and put a nice big header on it so the user has an idea what is
+happening::
+
+ <table class="form">
+ <tr><th class="header" colspan="2">Category</th></tr>
+
+Next, we need the field into which the user is going to enter the new
+category. The ``context.name.field(size=60)`` bit tells Roundup to
+generate a normal HTML field of size 60, and the contents of that field
+will be the "name" variable of the current context (namely "category").
+The upshot of this is that when the user types something in
+to the form, a new category will be created with that name::
+
+ <tr>
+ <th>Name</th>
+ <td tal:content="structure python:context.name.field(size=60)">
+ name</td>
+ </tr>
+
+Then a submit button so that the user can submit the new category::
+
+ <tr>
+ <td> </td>
+ <td colspan="3" tal:content="structure context/submit">
+ submit button will go here
+ </td>
+ </tr>
+
+Finally we finish off the tags we used at the start to do the METAL
+stuff::
+
+ </td>
+ </tal:block>
+
+So putting it all together, and closing the table and form we get::
+
+ <!-- category.item -->
+ <tal:block metal:use-macro="templates/page/macros/icing">
+ <title metal:fill-slot="head_title">Category editing</title>
+ <td class="page-header-top" metal:fill-slot="body_title">
+ <h2>Category editing</h2>
+ </td>
+ <td class="content" metal:fill-slot="content">
+ <form method="POST" onSubmit="return submit_once()"
+ enctype="multipart/form-data">
+
+ <table class="form">
+ <tr><th class="header" colspan="2">Category</th></tr>
+
+ <tr>
+ <th>Name</th>
+ <td tal:content="structure python:context.name.field(size=60)">
+ name</td>
+ </tr>
+
+ <tr>
+ <td>
+
+ <input type="hidden" name="@required" value="name">
+ </td>
+ <td colspan="3" tal:content="structure context/submit">
+ submit button will go here
+ </td>
+ </tr>
+ </table>
+ </form>
+ </td>
+ </tal:block>
+
+This is quite a lot to just ask the user one simple question, but there
+is a lot of setup for basically one line (the form line) to do its work.
+To add another field to "category" would involve one more line (well,
+maybe a few extra to get the formatting correct).
+
+
+Adding the category to the issue
+::::::::::::::::::::::::::::::::
+
+We now have the ability to create issues to our heart's content, but
+that is pointless unless we can assign categories to issues. Just like
+the ``html/category.item.html`` file was used to define how to add a new
+category, the ``html/issue.item.html`` is used to define how a new issue
+is created.
+
+Just like ``category.issue.html``, this file defines a form which has a
+table to lay things out. It doesn't matter where in the table we add new
+stuff, it is entirely up to your sense of aesthetics::
+
+ <th>Category</th>
+ <td>
+ <span tal:replace="structure context/category/field" />
+ <span tal:replace="structure python:db.category.classhelp('name',
+ property='category', width='200')" />
+ </td>
+
+First, we define a nice header so that the user knows what the next
+section is, then the middle line does what we are most interested in.
+This ``context/category/field`` gets replaced by a field which contains
+the category in the current context (the current context being the new
+issue).
+
+The classhelp lines generate a link (labelled "list") to a popup window
+which contains the list of currently known categories.
+
+
+Searching on categories
+:::::::::::::::::::::::
+
+Now we can add categories, and create issues with categories. The next
+obvious thing that we would like to be able to do, would be to search
+for issues based on their category, so that, for example, anyone working
+on the web server could look at all issues in the category "Web".
+
+If you look for "Search Issues" in the ``html/page.html`` file, you will
+find that it looks something like
+``<a href="issue?@template=search">Search Issues</a>``. This shows us
+that when you click on "Search Issues" it will be looking for a
+``issue.search.html`` file to display. So that is the file that we will
+change.
+
+If you look at this file it should begin to seem familiar, although it
+does use some new macros. You can add the new category search code anywhere you
+like within that form::
+
+ <tr tal:define="name string:category;
+ db_klass string:category;
+ db_content string:name;">
+ <th>Priority:</th>
+ <td metal:use-macro="search_select"></td>
+ <td metal:use-macro="column_input"></td>
+ <td metal:use-macro="sort_input"></td>
+ <td metal:use-macro="group_input"></td>
+ </tr>
+
+The definitions in the ``<tr>`` opening tag are used by the macros:
+
+- ``search_select`` expands to a drop-down box with all categories using
+ ``db_klass`` and ``db_content``.
+- ``column_input`` expands to a checkbox for selecting what columns
+ should be displayed.
+- ``sort_input`` expands to a radio button for selecting what property
+ should be sorted on.
+- ``group_input`` expands to a radio button for selecting what property
+ should be grouped on.
+
+The category search code above would expand to the following::
+
+ <tr>
+ <th>Category:</th>
+ <td>
+ <select name="category">
+ <option value="">don't care</option>
+ <option value="">------------</option>
+ <option value="1">scipy</option>
+ <option value="2">chaco</option>
+ <option value="3">weave</option>
+ </select>
+ </td>
+ <td><input type="checkbox" name=":columns" value="category"></td>
+ <td><input type="radio" name=":sort" value="category"></td>
+ <td><input type="radio" name=":group" value="category"></td>
+ </tr>
+
+Adding category to the default view
+:::::::::::::::::::::::::::::::::::
+
+We can now add categories, add issues with categories, and search for
+issues based on categories. This is everything that we need to do;
+however, there is some more icing that we would like. I think the
+category of an issue is important enough that it should be displayed by
+default when listing all the issues.
+
+Unfortunately, this is a bit less obvious than the previous steps. The
+code defining how the issues look is in ``html/issue.index.html``. This
+is a large table with a form down at the bottom for redisplaying and so
+forth.
+
+Firstly we need to add an appropriate header to the start of the table::
+
+ <th tal:condition="request/show/category">Category</th>
+
+The *condition* part of this statement is to avoid displaying the
+Category column if the user has selected not to see it.
+
+The rest of the table is a loop which will go through every issue that
+matches the display criteria. The loop variable is "i" - which means
+that every issue gets assigned to "i" in turn.
+
+The new part of code to display the category will look like this::
+
+ <td tal:condition="request/show/category"
+ tal:content="i/category"></td>
+
+The condition is the same as above: only display the condition when the
+user hasn't asked for it to be hidden. The next part is to set the
+content of the cell to be the category part of "i" - the current issue.
+
+Finally we have to edit ``html/page.html`` again. This time, we need to
+tell it that when the user clicks on "Unassigned Issues" or "All Issues",
+the category column should be included in the resulting list. If you
+scroll down the page file, you can see the links with lots of options.
+The option that we are interested in is the ``:columns=`` one which
+tells roundup which fields of the issue to display. Simply add
+"category" to that list and it all should work.
+
+Adding a time log to your issues
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We want to log the dates and amount of time spent working on issues, and
+be able to give a summary of the total time spent on a particular issue.
+
+1. Add a new class to your tracker ``schema.py``::
+
+ # storage for time logging
+ timelog = Class(db, "timelog", period=Interval())
+
+ Note that we automatically get the date of the time log entry
+ creation through the standard property "creation".
+
+2. Link to the new class from your issue class (again, in
+ ``schema.py``)::
+
+ issue = IssueClass(db, "issue",
+ assignedto=Link("user"), topic=Multilink("keyword"),
+ priority=Link("priority"), status=Link("status"),
+ times=Multilink("timelog"))
+
+ the "times" property is the new link to the "timelog" class.
+
+3. We'll need to let people add in times to the issue, so in the web
+ interface we'll have a new entry field. This is a special field
+ because unlike the other fields in the ``issue.item`` template, it
+ affects a different item (a timelog item) and not the template's
+ item (an issue). We have a special syntax for form fields that affect
+ items other than the template default item (see the cgi
+ documentation on `special form variables`_). In particular, we add a
+ field to capture a new timelog item's period::
+
+ <tr>
+ <th>Time Log</th>
+ <td colspan=3><input type="text" name="timelog-1 at period" />
+ <br />(enter as '3y 1m 4d 2:40:02' or parts thereof)
+ </td>
+ </tr>
+
+ and another hidden field that links that new timelog item (new
+ because it's marked as having id "-1") to the issue item. It looks
+ like this::
+
+ <input type="hidden" name="@link at times" value="timelog-1" />
+
+ On submission, the "-1" timelog item will be created and assigned a
+ real item id. The "times" property of the issue will have the new id
+ added to it.
+
+4. We want to display a total of the timelog times that have been
+ accumulated for an issue. To do this, we'll need to actually write
+ some Python code, since it's beyond the scope of PageTemplates to
+ perform such calculations. We do this by adding a module ``timespent.py``
+ to the ``extensions`` directory in our tracker. The contents of this
+ file is as follows::
+
+ def totalTimeSpent(times):
+ ''' Call me with a list of timelog items (which have an
+ Interval "period" property)
+ '''
+ total = Interval('0d')
+ for time in times:
+ total += time.period._value
+ return total
+
+ def init(instance):
+ instance.registerUtil('totalTimeSpent', totalTimeSpent)
+
+ We will now be able to access the ``totalTimeSpent`` function via the
+ ``utils`` variable in our templates, as shown in the next step.
+
+5. Display the timelog for an issue::
+
+ <table class="otherinfo" tal:condition="context/times">
+ <tr><th colspan="3" class="header">Time Log
+ <tal:block
+ tal:replace="python:utils.totalTimeSpent(context.times)" />
+ </th></tr>
+ <tr><th>Date</th><th>Period</th><th>Logged By</th></tr>
+ <tr tal:repeat="time context/times">
+ <td tal:content="time/creation"></td>
+ <td tal:content="time/period"></td>
+ <td tal:content="time/creator"></td>
+ </tr>
+ </table>
+
+ I put this just above the Messages log in my issue display. Note our
+ use of the ``totalTimeSpent`` method which will total up the times
+ for the issue and return a new Interval. That will be automatically
+ displayed in the template as text like "+ 1y 2:40" (1 year, 2 hours
+ and 40 minutes).
+
+8. If you're using a persistent web server - ``roundup-server`` or
+ ``mod_python`` for example - then you'll need to restart that to pick up
+ the code changes. When that's done, you'll be able to use the new
+ time logging interface.
+
+
+Tracking different types of issues
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes you will want to track different types of issues - developer,
+customer support, systems, sales leads, etc. A single Roundup tracker is
+able to support multiple types of issues. This example demonstrates adding
+a system support issue class to a tracker.
+
+1. Figure out what information you're going to want to capture. OK, so
+ this is obvious, but sometimes it's better to actually sit down for a
+ while and think about the schema you're going to implement.
+
+2. Add the new issue class to your tracker's ``schema.py``. Just after the
+ "issue" class definition, add::
+
+ # list our systems
+ system = Class(db, "system", name=String(), order=Number())
+ system.setkey("name")
+
+ # store issues related to those systems
+ support = IssueClass(db, "support",
+ assignedto=Link("user"), topic=Multilink("keyword"),
+ status=Link("status"), deadline=Date(),
+ affects=Multilink("system"))
+
+3. Copy the existing ``issue.*`` (item, search and index) templates in the
+ tracker's ``html`` to ``support.*``. Edit them so they use the properties
+ defined in the ``support`` class. Be sure to check for hidden form
+ variables like "required" to make sure they have the correct set of
+ required properties.
+
+4. Edit the modules in the ``detectors``, adding lines to their ``init``
+ functions where appropriate. Look for ``audit`` and ``react`` registrations
+ on the ``issue`` class, and duplicate them for ``support``.
+
+5. Create a new sidebar box for the new support class. Duplicate the
+ existing issues one, changing the ``issue`` class name to ``support``.
+
+6. Re-start your tracker and start using the new ``support`` class.
+
+
+Optionally, you might want to restrict the users able to access this new
+class to just the users with a new "SysAdmin" Role. To do this, we add
+some security declarations::
+
+ db.security.addPermissionToRole('SysAdmin', 'View', 'support')
+ db.security.addPermissionToRole('SysAdmin', 'Create', 'support')
+ db.security.addPermissionToRole('SysAdmin', 'Edit', 'support')
+
+You would then (as an "admin" user) edit the details of the appropriate
+users, and add "SysAdmin" to their Roles list.
+
+Alternatively, you might want to change the Edit/View permissions granted
+for the ``issue`` class so that it's only available to users with the "System"
+or "Developer" Role, and then the new class you're adding is available to
+all with the "User" Role.
+
+
+Using External User Databases
+-----------------------------
+
+Using an external password validation source
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. note:: You will need to either have an "admin" user in your external
+ password source *or* have one of your regular users have
+ the Admin Role assigned. If you need to assign the Role *after*
+ making the changes below, you may use the ``roundup-admin``
+ program to edit a user's details.
+
+We have a centrally-managed password changing system for our users. This
+results in a UN*X passwd-style file that we use for verification of
+users. Entries in the file consist of ``name:password`` where the
+password is encrypted using the standard UN*X ``crypt()`` function (see
+the ``crypt`` module in your Python distribution). An example entry
+would be::
+
+ admin:aamrgyQfDFSHw
+
+Each user of Roundup must still have their information stored in the Roundup
+database - we just use the passwd file to check their password. To do this, we
+need to override the standard ``verifyPassword`` method defined in
+``roundup.cgi.actions.LoginAction`` and register the new class. The
+following is added as ``externalpassword.py`` in the tracker ``extensions``
+directory::
+
+ import os, crypt
+ from roundup.cgi.actions import LoginAction
+
+ class ExternalPasswordLoginAction(LoginAction):
+ def verifyPassword(self, userid, password):
+ '''Look through the file, line by line, looking for a
+ name that matches.
+ '''
+ # get the user's username
+ username = self.db.user.get(userid, 'username')
+
+ # the passwords are stored in the "passwd.txt" file in the
+ # tracker home
+ file = os.path.join(self.db.config.TRACKER_HOME, 'passwd.txt')
+
+ # see if we can find a match
+ for ent in [line.strip().split(':') for line in
+ open(file).readlines()]:
+ if ent[0] == username:
+ return crypt.crypt(password, ent[1][:2]) == ent[1]
+
+ # user doesn't exist in the file
+ return 0
+
+ def init(instance):
+ instance.registerAction('login', ExternalPasswordLoginAction)
+
+You should also remove the redundant password fields from the ``user.item``
+template.
+
+
+Using a UN*X passwd file as the user database
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+On some systems the primary store of users is the UN*X passwd file. It
+holds information on users such as their username, real name, password
+and primary user group.
+
+Roundup can use this store as its primary source of user information,
+but it needs additional information too - email address(es), roundup
+Roles, vacation flags, roundup hyperdb item ids, etc. Also, "retired"
+users must still exist in the user database, unlike some passwd files in
+which the users are removed when they no longer have access to a system.
+
+To make use of the passwd file, we therefore synchronise between the two
+user stores. We also use the passwd file to validate the user logins, as
+described in the previous example, `using an external password
+validation source`_. We keep the user lists in sync using a fairly
+simple script that runs once a day, or several times an hour if more
+immediate access is needed. In short, it:
+
+1. parses the passwd file, finding usernames, passwords and real names,
+2. compares that list to the current roundup user list:
+
+ a. entries no longer in the passwd file are *retired*
+ b. entries with mismatching real names are *updated*
+ c. entries only exist in the passwd file are *created*
+
+3. send an email to administrators to let them know what's been done.
+
+The retiring and updating are simple operations, requiring only a call
+to ``retire()`` or ``set()``. The creation operation requires more
+information though - the user's email address and their Roundup Roles.
+We're going to assume that the user's email address is the same as their
+login name, so we just append the domain name to that. The Roles are
+determined using the passwd group identifier - mapping their UN*X group
+to an appropriate set of Roles.
+
+The script to perform all this, broken up into its main components, is
+as follows. Firstly, we import the necessary modules and open the
+tracker we're to work on::
+
+ import sys, os, smtplib
+ from roundup import instance, date
+
+ # open the tracker
+ tracker_home = sys.argv[1]
+ tracker = instance.open(tracker_home)
+
+Next we read in the *passwd* file from the tracker home::
+
+ # read in the users from the "passwd.txt" file
+ file = os.path.join(tracker_home, 'passwd.txt')
+ users = [x.strip().split(':') for x in open(file).readlines()]
+
+Handle special users (those to ignore in the file, and those who don't
+appear in the file)::
+
+ # users to not keep ever, pre-load with the users I know aren't
+ # "real" users
+ ignore = ['ekmmon', 'bfast', 'csrmail']
+
+ # users to keep - pre-load with the roundup-specific users
+ keep = ['comment_pool', 'network_pool', 'admin', 'dev-team',
+ 'cs_pool', 'anonymous', 'system_pool', 'automated']
+
+Now we map the UN*X group numbers to the Roles that users should have::
+
+ roles = {
+ '501': 'User,Tech', # tech
+ '502': 'User', # finance
+ '503': 'User,CSR', # customer service reps
+ '504': 'User', # sales
+ '505': 'User', # marketing
+ }
+
+Now we do all the work. Note that the body of the script (where we have
+the tracker database open) is wrapped in a ``try`` / ``finally`` clause,
+so that we always close the database cleanly when we're finished. So, we
+now do all the work::
+
+ # open the database
+ db = tracker.open('admin')
+ try:
+ # store away messages to send to the tracker admins
+ msg = []
+
+ # loop over the users list read in from the passwd file
+ for user,passw,uid,gid,real,home,shell in users:
+ if user in ignore:
+ # this user shouldn't appear in our tracker
+ continue
+ keep.append(user)
+ try:
+ # see if the user exists in the tracker
+ uid = db.user.lookup(user)
+
+ # yes, they do - now check the real name for correctness
+ if real != db.user.get(uid, 'realname'):
+ db.user.set(uid, realname=real)
+ msg.append('FIX %s - %s'%(user, real))
+ except KeyError:
+ # nope, the user doesn't exist
+ db.user.create(username=user, realname=real,
+ address='%s at ekit-inc.com'%user, roles=roles[gid])
+ msg.append('ADD %s - %s (%s)'%(user, real, roles[gid]))
+
+ # now check that all the users in the tracker are also in our
+ # "keep" list - retire those who aren't
+ for uid in db.user.list():
+ user = db.user.get(uid, 'username')
+ if user not in keep:
+ db.user.retire(uid)
+ msg.append('RET %s'%user)
+
+ # if we did work, then send email to the tracker admins
+ if msg:
+ # create the email
+ msg = '''Subject: %s user database maintenance
+
+ %s
+ '''%(db.config.TRACKER_NAME, '\n'.join(msg))
+
+ # send the email
+ smtp = smtplib.SMTP(db.config.MAILHOST)
+ addr = db.config.ADMIN_EMAIL
+ smtp.sendmail(addr, addr, msg)
+
+ # now we're done - commit the changes
+ db.commit()
+ finally:
+ # always close the database cleanly
+ db.close()
+
+And that's it!
+
+
+Using an LDAP database for user information
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A script that reads users from an LDAP store using
+http://python-ldap.sf.net/ and then compares the list to the users in the
+roundup user database would be pretty easy to write. You'd then have it run
+once an hour / day (or on demand if you can work that into your LDAP store
+workflow). See the example `Using a UN*X passwd file as the user database`_
+for more information about doing this.
+
+To authenticate off the LDAP store (rather than using the passwords in the
+Roundup user database) you'd use the same python-ldap module inside an
+extension to the cgi interface. You'd do this by overriding the method called
+``verifyPassword`` on the ``LoginAction`` class in your tracker's
+``extensions`` directory (see `using an external password validation
+source`_). The method is implemented by default as::
+
+ def verifyPassword(self, userid, password):
+ ''' Verify the password that the user has supplied
+ '''
+ stored = self.db.user.get(self.userid, 'password')
+ if password == stored:
+ return 1
+ if not password and not stored:
+ return 1
+ return 0
+
+So you could reimplement this as something like::
+
+ def verifyPassword(self, userid, password):
+ ''' Verify the password that the user has supplied
+ '''
+ # look up some unique LDAP information about the user
+ username = self.db.user.get(self.userid, 'username')
+ # now verify the password supplied against the LDAP store
+
+
+Changes to Tracker Behaviour
+----------------------------
+
+Stop "nosy" messages going to people on vacation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When users go on vacation and set up vacation email bouncing, you'll
+start to see a lot of messages come back through Roundup "Fred is on
+vacation". Not very useful, and relatively easy to stop.
+
+1. add a "vacation" flag to your users::
+
+ user = Class(db, "user",
+ username=String(), password=Password(),
+ address=String(), realname=String(),
+ phone=String(), organisation=String(),
+ alternate_addresses=String(),
+ roles=String(), queries=Multilink("query"),
+ vacation=Boolean())
+
+2. So that users may edit the vacation flags, add something like the
+ following to your ``user.item`` template::
+
+ <tr>
+ <th>On Vacation</th>
+ <td tal:content="structure context/vacation/field">vacation</td>
+ </tr>
+
+3. edit your detector ``nosyreactor.py`` so that the ``nosyreaction()``
+ consists of::
+
+ def nosyreaction(db, cl, nodeid, oldvalues):
+ users = db.user
+ messages = db.msg
+ # send a copy of all new messages to the nosy list
+ for msgid in determineNewMessages(cl, nodeid, oldvalues):
+ try:
+ # figure the recipient ids
+ sendto = []
+ seen_message = {}
+ recipients = messages.get(msgid, 'recipients')
+ for recipid in messages.get(msgid, 'recipients'):
+ seen_message[recipid] = 1
+
+ # figure the author's id, and indicate they've received
+ # the message
+ authid = messages.get(msgid, 'author')
+
+ # possibly send the message to the author, as long as
+ # they aren't anonymous
+ if (db.config.MESSAGES_TO_AUTHOR == 'yes' and
+ users.get(authid, 'username') != 'anonymous'):
+ sendto.append(authid)
+ seen_message[authid] = 1
+
+ # now figure the nosy people who weren't recipients
+ nosy = cl.get(nodeid, 'nosy')
+ for nosyid in nosy:
+ # Don't send nosy mail to the anonymous user (that
+ # user shouldn't appear in the nosy list, but just
+ # in case they do...)
+ if users.get(nosyid, 'username') == 'anonymous':
+ continue
+ # make sure they haven't seen the message already
+ if not seen_message.has_key(nosyid):
+ # send it to them
+ sendto.append(nosyid)
+ recipients.append(nosyid)
+
+ # generate a change note
+ if oldvalues:
+ note = cl.generateChangeNote(nodeid, oldvalues)
+ else:
+ note = cl.generateCreateNote(nodeid)
+
+ # we have new recipients
+ if sendto:
+ # filter out the people on vacation
+ sendto = [i for i in sendto
+ if not users.get(i, 'vacation', 0)]
+
+ # map userids to addresses
+ sendto = [users.get(i, 'address') for i in sendto]
+
+ # update the message's recipients list
+ messages.set(msgid, recipients=recipients)
+
+ # send the message
+ cl.send_message(nodeid, msgid, note, sendto)
+ except roundupdb.MessageSendError, message:
+ raise roundupdb.DetectorError, message
+
+ Note that this is the standard nosy reaction code, with the small
+ addition of::
+
+ # filter out the people on vacation
+ sendto = [i for i in sendto if not users.get(i, 'vacation', 0)]
+
+ which filters out the users that have the vacation flag set to true.
+
+Adding in state transition control
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes tracker admins want to control the states to which users may
+move issues. You can do this by following these steps:
+
+1. make "status" a required variable. This is achieved by adding the
+ following to the top of the form in the ``issue.item.html``
+ template::
+
+ <input type="hidden" name="@required" value="status">
+
+ This will force users to select a status.
+
+2. add a Multilink property to the status class::
+
+ stat = Class(db, "status", ... , transitions=Multilink('status'),
+ ...)
+
+ and then edit the statuses already created, either:
+
+ a. through the web using the class list -> status class editor, or
+ b. using the ``roundup-admin`` "set" command.
+
+3. add an auditor module ``checktransition.py`` in your tracker's
+ ``detectors`` directory, for example::
+
+ def checktransition(db, cl, nodeid, newvalues):
+ ''' Check that the desired transition is valid for the "status"
+ property.
+ '''
+ if not newvalues.has_key('status'):
+ return
+ current = cl.get(nodeid, 'status')
+ new = newvalues['status']
+ if new == current:
+ return
+ ok = db.status.get(current, 'transitions')
+ if new not in ok:
+ raise ValueError, 'Status not allowed to move from "%s" to "%s"'%(
+ db.status.get(current, 'name'), db.status.get(new, 'name'))
+
+ def init(db):
+ db.issue.audit('set', checktransition)
+
+4. in the ``issue.item.html`` template, change the status editing bit
+ from::
+
+ <th>Status</th>
+ <td tal:content="structure context/status/menu">status</td>
+
+ to::
+
+ <th>Status</th>
+ <td>
+ <select tal:condition="context/id" name="status">
+ <tal:block tal:define="ok context/status/transitions"
+ tal:repeat="state db/status/list">
+ <option tal:condition="python:state.id in ok"
+ tal:attributes="
+ value state/id;
+ selected python:state.id == context.status.id"
+ tal:content="state/name"></option>
+ </tal:block>
+ </select>
+ <tal:block tal:condition="not:context/id"
+ tal:replace="structure context/status/menu" />
+ </td>
+
+ which displays only the allowed status to transition to.
+
+
+Blocking issues that depend on other issues
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We needed the ability to mark certain issues as "blockers" - that is,
+they can't be resolved until another issue (the blocker) they rely on is
+resolved. To achieve this:
+
+1. Create a new property on the ``issue`` class:
+ ``blockers=Multilink("issue")``. To do this, edit the definition of
+ this class in your tracker's ``schema.py`` file. Change this::
+
+ issue = IssueClass(db, "issue",
+ assignedto=Link("user"), topic=Multilink("keyword"),
+ priority=Link("priority"), status=Link("status"))
+
+ to this, adding the blockers entry::
+
+ issue = IssueClass(db, "issue",
+ blockers=Multilink("issue"),
+ assignedto=Link("user"), topic=Multilink("keyword"),
+ priority=Link("priority"), status=Link("status"))
+
+2. Add the new ``blockers`` property to the ``issue.item.html`` edit
+ page, using something like::
+
+ <th>Waiting On</th>
+ <td>
+ <span tal:replace="structure python:context.blockers.field(showid=1,
+ size=20)" />
+ <span tal:replace="structure python:db.issue.classhelp('id,title')" />
+ <span tal:condition="context/blockers"
+ tal:repeat="blk context/blockers">
+ <br>View: <a tal:attributes="href string:issue${blk/id}"
+ tal:content="blk/id"></a>
+ </span>
+
+ You'll need to fiddle with your item page layout to find an
+ appropriate place to put it - I'll leave that fun part up to you.
+ Just make sure it appears in the first table, possibly somewhere near
+ the "superseders" field.
+
+3. Create a new detector module (see below) which enforces the rules:
+
+ - issues may not be resolved if they have blockers
+ - when a blocker is resolved, it's removed from issues it blocks
+
+ The contents of the detector should be something like this::
+
+
+ def blockresolution(db, cl, nodeid, newvalues):
+ ''' If the issue has blockers, don't allow it to be resolved.
+ '''
+ if nodeid is None:
+ blockers = []
+ else:
+ blockers = cl.get(nodeid, 'blockers')
+ blockers = newvalues.get('blockers', blockers)
+
+ # don't do anything if there's no blockers or the status hasn't
+ # changed
+ if not blockers or not newvalues.has_key('status'):
+ return
+
+ # get the resolved state ID
+ resolved_id = db.status.lookup('resolved')
+
+ # format the info
+ u = db.config.TRACKER_WEB
+ s = ', '.join(['<a href="%sissue%s">%s</a>'%(
+ u,id,id) for id in blockers])
+ if len(blockers) == 1:
+ s = 'issue %s is'%s
+ else:
+ s = 'issues %s are'%s
+
+ # ok, see if we're trying to resolve
+ if newvalues['status'] == resolved_id:
+ raise ValueError, "This issue can't be resolved until %s resolved."%s
+
+
+ def resolveblockers(db, cl, nodeid, oldvalues):
+ ''' When we resolve an issue that's a blocker, remove it from the
+ blockers list of the issue(s) it blocks.
+ '''
+ newstatus = cl.get(nodeid,'status')
+
+ # no change?
+ if oldvalues.get('status', None) == newstatus:
+ return
+
+ resolved_id = db.status.lookup('resolved')
+
+ # interesting?
+ if newstatus != resolved_id:
+ return
+
+ # yes - find all the blocked issues, if any, and remove me from
+ # their blockers list
+ issues = cl.find(blockers=nodeid)
+ for issueid in issues:
+ blockers = cl.get(issueid, 'blockers')
+ if nodeid in blockers:
+ blockers.remove(nodeid)
+ cl.set(issueid, blockers=blockers)
+
+ def init(db):
+ # might, in an obscure situation, happen in a create
+ db.issue.audit('create', blockresolution)
+ db.issue.audit('set', blockresolution)
+
+ # can only happen on a set
+ db.issue.react('set', resolveblockers)
+
+ Put the above code in a file called "blockers.py" in your tracker's
+ "detectors" directory.
+
+4. Finally, and this is an optional step, modify the tracker web page
+ URLs so they filter out issues with any blockers. You do this by
+ adding an additional filter on "blockers" for the value "-1". For
+ example, the existing "Show All" link in the "page" template (in the
+ tracker's "html" directory) looks like this::
+
+ <a href="issue?@sort=-activity&@group=priority&@filter=status&
+ @columns=id,activity,title,creator,assignedto,status&
+ status=-1,1,2,3,4,5,6,7">Show All</a><br>
+
+ modify it to add the "blockers" info to the URL (note, both the
+ "@filter" *and* "blockers" values must be specified)::
+
+ <a href="issue?@sort=-activity&@group=priority&@filter=status,blockers&
+ blockers=-1&@columns=id,activity,title,creator,assignedto,status&
+ status=-1,1,2,3,4,5,6,7">Show All</a><br>
+
+ The above examples are line-wrapped on the trailing & and should
+ be unwrapped.
+
+That's it. You should now be able to set blockers on your issues. Note
+that if you want to know whether an issue has any other issues dependent
+on it (i.e. it's in their blockers list) you can look at the journal
+history at the bottom of the issue page - look for a "link" event to
+another issue's "blockers" property.
+
+Add users to the nosy list based on the topic
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Let's say we need the ability to automatically add users to the nosy
+list based
+on the occurance of a topic. Every user should be allowed to edit their
+own list of topics for which they want to be added to the nosy list.
+
+Below, we'll show that this change can be done with minimal
+understanding of the Roundup system, using only copy and paste.
+
+This requires three changes to the tracker: a change in the database to
+allow per-user recording of the lists of topics for which he wants to
+be put on the nosy list, a change in the user view allowing them to edit
+this list of topics, and addition of an auditor which updates the nosy
+list when a topic is set.
+
+Adding the nosy topic list
+::::::::::::::::::::::::::
+
+The change to make in the database, is that for any user there should be
+a list of topics for which he wants to be put on the nosy list. Adding
+a ``Multilink`` of ``keyword`` seems to fullfill this (note that within
+the code, topics are called ``keywords``.) As such, all that has to be
+done is to add a new field to the definition of ``user`` within the
+file ``schema.py``. We will call this new field ``nosy_keywords``, and
+the updated definition of user will be::
+
+ user = Class(db, "user",
+ username=String(), password=Password(),
+ address=String(), realname=String(),
+ phone=String(), organisation=String(),
+ alternate_addresses=String(),
+ queries=Multilink('query'), roles=String(),
+ timezone=String(),
+ nosy_keywords=Multilink('keyword'))
+
+Changing the user view to allow changing the nosy topic list
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+We want any user to be able to change the list of topics for which
+he will by default be added to the nosy list. We choose to add this
+to the user view, as is generated by the file ``html/user.item.html``.
+We can easily
+see that the topic field in the issue view has very similar editing
+requirements as our nosy topics, both being lists of topics. As
+such, we look for Topics in ``issue.item.html``, and extract the
+associated parts from there. We add this to ``user.item.html`` at the
+bottom of the list of viewed items (i.e. just below the 'Alternate
+E-mail addresses' in the classic template)::
+
+ <tr>
+ <th>Nosy Topics</th>
+ <td>
+ <span tal:replace="structure context/nosy_keywords/field" />
+ <span tal:replace="structure python:db.keyword.classhelp(property='nosy_keywords')" />
+ </td>
+ </tr>
+
+
+Addition of an auditor to update the nosy list
+::::::::::::::::::::::::::::::::::::::::::::::
+
+The more difficult part is the logic to add
+the users to the nosy list when required.
+We choose to perform this action whenever the topics on an
+item are set (this includes the creation of items).
+Here we choose to start out with a copy of the
+``detectors/nosyreaction.py`` detector, which we copy to the file
+``detectors/nosy_keyword_reaction.py``.
+This looks like a good start as it also adds users
+to the nosy list. A look through the code reveals that the
+``nosyreaction`` function actually sends the e-mail.
+We don't need this. Therefore, we can change the ``init`` function to::
+
+ def init(db):
+ db.issue.audit('create', update_kw_nosy)
+ db.issue.audit('set', update_kw_nosy)
+
+After that, we rename the ``updatenosy`` function to ``update_kw_nosy``.
+The first two blocks of code in that function relate to setting
+``current`` to a combination of the old and new nosy lists. This
+functionality is left in the new auditor. The following block of
+code, which handled adding the assignedto user(s) to the nosy list in
+``updatenosy``, should be replaced by a block of code to add the
+interested users to the nosy list. We choose here to loop over all
+new topics, than looping over all users,
+and assign the user to the nosy list when the topic occurs in the user's
+``nosy_keywords``. The next part in ``updatenosy`` -- adding the author
+and/or recipients of a message to the nosy list -- is obviously not
+relevant here and is thus deleted from the new auditor. The last
+part, copying the new nosy list to ``newvalues``, can stay as is.
+This results in the following function::
+
+ def update_kw_nosy(db, cl, nodeid, newvalues):
+ '''Update the nosy list for changes to the topics
+ '''
+ # nodeid will be None if this is a new node
+ current = {}
+ 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:
+ if not current.has_key(value):
+ current[value] = 1
+
+ # 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
+ if not current.has_key(value):
+ current[value] = 1
+
+ # add users with topic in nosy_keywords to the nosy list
+ if newvalues.has_key('topic') and newvalues['topic'] is not None:
+ topic_ids = newvalues['topic']
+ for topic in topic_ids:
+ # loop over all users,
+ # and assign user to nosy when topic in nosy_keywords
+ for user_id in db.user.list():
+ nosy_kw = db.user.get(user_id, "nosy_keywords")
+ found = 0
+ for kw in nosy_kw:
+ if kw == topic:
+ found = 1
+ if found:
+ current[user_id] = 1
+
+ # that's it, save off the new nosy list
+ newvalues['nosy'] = current.keys()
+
+These two function are the only ones needed in the file.
+
+TODO: update this example to use the ``find()`` Class method.
+
+Caveats
+:::::::
+
+A few problems with the design here can be noted:
+
+Multiple additions
+ When a user, after automatic selection, is manually removed
+ from the nosy list, he is added to the nosy list again when the
+ topic list of the issue is updated. A better design might be
+ to only check which topics are new compared to the old list
+ of topics, and only add users when they have indicated
+ interest on a new topic.
+
+ The code could also be changed to only trigger on the ``create()``
+ event, rather than also on the ``set()`` event, thus only setting
+ the nosy list when the issue is created.
+
+Scalability
+ In the auditor, there is a loop over all users. For a site with
+ only few users this will pose no serious problem; however, with
+ many users this will be a serious performance bottleneck.
+ A way out would be to link from the topics to the users who
+ selected these topics as nosy topics. This will eliminate the
+ loop over all users.
+
+Changes to Security and Permissions
+-----------------------------------
+
+Restricting the list of users that are assignable to a task
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+1. In your tracker's ``schema.py``, create a new Role, say "Developer"::
+
+ db.security.addRole(name='Developer', description='A developer')
+
+2. Just after that, create a new Permission, say "Fixer", specific to
+ "issue"::
+
+ p = db.security.addPermission(name='Fixer', klass='issue',
+ description='User is allowed to be assigned to fix issues')
+
+3. Then assign the new Permission to your "Developer" Role::
+
+ db.security.addPermissionToRole('Developer', p)
+
+4. In the issue item edit page (``html/issue.item.html`` in your tracker
+ directory), use the new Permission in restricting the "assignedto"
+ list::
+
+ <select name="assignedto">
+ <option value="-1">- no selection -</option>
+ <tal:block tal:repeat="user db/user/list">
+ <option tal:condition="python:user.hasPermission(
+ 'Fixer', context._classname)"
+ tal:attributes="
+ value user/id;
+ selected python:user.id == context.assignedto"
+ tal:content="user/realname"></option>
+ </tal:block>
+ </select>
+
+For extra security, you may wish to setup an auditor to enforce the
+Permission requirement (install this as ``assignedtoFixer.py`` in your
+tracker ``detectors`` directory)::
+
+ def assignedtoMustBeFixer(db, cl, nodeid, newvalues):
+ ''' Ensure the assignedto value in newvalues is used with the
+ Fixer Permission
+ '''
+ if not newvalues.has_key('assignedto'):
+ # don't care
+ return
+
+ # get the userid
+ userid = newvalues['assignedto']
+ if not db.security.hasPermission('Fixer', userid, cl.classname):
+ raise ValueError, 'You do not have permission to edit %s'%cl.classname
+
+ def init(db):
+ db.issue.audit('set', assignedtoMustBeFixer)
+ db.issue.audit('create', assignedtoMustBeFixer)
+
+So now, if an edit action attempts to set "assignedto" to a user that
+doesn't have the "Fixer" Permission, the error will be raised.
+
+
+Users may only edit their issues
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In this case, users registering themselves are granted Provisional
+access, meaning they
+have access to edit the issues they submit, but not others. We create a new
+Role called "Provisional User" which is granted to newly-registered users,
+and has limited access. One of the Permissions they have is the new "Edit
+Own" on issues (regular users have "Edit".)
+
+First up, we create the new Role and Permission structure in
+``schema.py``::
+
+ #
+ # New users not approved by the admin
+ #
+ db.security.addRole(name='Provisional User',
+ description='New user registered via web or email')
+
+ # These users need to be able to view and create issues but only edit
+ # and view their own
+ db.security.addPermissionToRole('Provisional User', 'Create', 'issue')
+ def own_issue(db, userid, itemid):
+ '''Determine whether the userid matches the creator of the issue.'''
+ return userid == db.issue.get(itemid, 'creator')
+ p = db.security.addPermission(name='Edit', klass='issue',
+ check=own_issue, description='Can only edit own issues')
+ db.security.addPermissionToRole('Provisional User', p)
+ p = db.security.addPermission(name='View', klass='issue',
+ check=own_issue, description='Can only view own issues')
+ db.security.addPermissionToRole('Provisional User', p)
+
+ # Assign the Permissions for issue-related classes
+ for cl in 'file', 'msg', 'query', 'keyword':
+ db.security.addPermissionToRole('Provisional User', 'View', cl)
+ db.security.addPermissionToRole('Provisional User', 'Edit', cl)
+ db.security.addPermissionToRole('Provisional User', 'Create', cl)
+ for cl in 'priority', 'status':
+ db.security.addPermissionToRole('Provisional User', 'View', cl)
+
+ # and give the new users access to the web and email interface
+ db.security.addPermissionToRole('Provisional User', 'Web Access')
+ db.security.addPermissionToRole('Provisional User', 'Email Access')
+
+ # make sure they can view & edit their own user record
+ 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('Provisional 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('Provisional User', p)
+
+Then, in ``config.ini``, we change the Role assigned to newly-registered
+users, replacing the existing ``'User'`` values::
+
+ [main]
+ ...
+ new_web_user_roles = 'Provisional User'
+ new_email_user_roles = 'Provisional User'
+
+
+All users may only view and edit issues, files and messages they create
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Replace the standard "classic" tracker View and Edit Permission assignments
+for the "issue", "file" and "msg" classes with the following::
+
+ def checker(klass):
+ def check(db, userid, itemid, klass=klass):
+ return db.getclass(klass).get(itemid, 'creator') == userid
+ return check
+ for cl in 'issue', 'file', 'msg':
+ p = db.security.addPermission(name='View', klass=cl,
+ check=checker(cl))
+ db.security.addPermissionToRole('User', p)
+ p = db.security.addPermission(name='Edit', klass=cl,
+ check=checker(cl))
+ db.security.addPermissionToRole('User', p)
+ db.security.addPermissionToRole('User', 'Create', cl)
+
+
+
+Changes to the Web User Interface
+---------------------------------
+
+Adding action links to the index page
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Add a column to the ``item.index.html`` template.
+
+Resolving the issue::
+
+ <a tal:attributes="href
+ string:issue${i/id}?:status=resolved&:action=edit">resolve</a>
+
+"Take" the issue::
+
+ <a tal:attributes="href
+ string:issue${i/id}?:assignedto=${request/user/id}&:action=edit">take</a>
+
+... and so on.
+
+Colouring the rows in the issue index according to priority
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A simple ``tal:attributes`` statement will do the bulk of the work here. In
+the ``issue.index.html`` template, add this to the ``<tr>`` that
+displays the rows of data::
+
+ <tr tal:attributes="class string:priority-${i/priority/plain}">
+
+and then in your stylesheet (``style.css``) specify the colouring for the
+different priorities, as follows::
+
+ tr.priority-critical td {
+ background-color: red;
+ }
+
+ tr.priority-urgent td {
+ background-color: orange;
+ }
+
+and so on, with far less offensive colours :)
+
+Editing multiple items in an index view
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To edit the status of all items in the item index view, edit the
+``issue.item.html``:
+
+1. add a form around the listing table (separate from the existing
+ index-page form), so at the top it reads::
+
+ <form method="POST" tal:attributes="action request/classname">
+ <table class="list">
+
+ and at the bottom of that table::
+
+ </table>
+ </form
+
+ making sure you match the ``</table>`` from the list table, not the
+ navigation table or the subsequent form table.
+
+2. in the display for the issue property, change::
+
+ <td tal:condition="request/show/status"
+ tal:content="python:i.status.plain() or default"> </td>
+
+ to::
+
+ <td tal:condition="request/show/status"
+ tal:content="structure i/status/field"> </td>
+
+ this will result in an edit field for the status property.
+
+3. after the ``tal:block`` which lists the index items (marked by
+ ``tal:repeat="i batch"``) add a new table row::
+
+ <tr>
+ <td tal:attributes="colspan python:len(request.columns)">
+ <input type="submit" value=" Save Changes ">
+ <input type="hidden" name="@action" value="edit">
+ <tal:block replace="structure request/indexargs_form" />
+ </td>
+ </tr>
+
+ which gives us a submit button, indicates that we are performing an edit
+ on any changed statuses. The final ``tal:block`` will make sure that the
+ current index view parameters (filtering, columns, etc) will be used in
+ rendering the next page (the results of the editing).
+
+
+Displaying only message summaries in the issue display
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Alter the ``issue.item`` template section for messages to::
+
+ <table class="messages" tal:condition="context/messages">
+ <tr><th colspan="5" class="header">Messages</th></tr>
+ <tr tal:repeat="msg context/messages">
+ <td><a tal:attributes="href string:msg${msg/id}"
+ tal:content="string:msg${msg/id}"></a></td>
+ <td tal:content="msg/author">author</td>
+ <td class="date" tal:content="msg/date/pretty">date</td>
+ <td tal:content="msg/summary">summary</td>
+ <td>
+ <a tal:attributes="href string:?@remove at messages=${msg/id}&@action=edit">
+ remove</a>
+ </td>
+ </tr>
+ </table>
+
+
+Enabling display of either message summaries or the entire messages
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This is pretty simple - all we need to do is copy the code from the
+example `displaying only message summaries in the issue display`_ into
+our template alongside the summary display, and then introduce a switch
+that shows either the one or the other. We'll use a new form variable,
+``@whole_messages`` to achieve this::
+
+ <table class="messages" tal:condition="context/messages">
+ <tal:block tal:condition="not:request/form/@whole_messages/value | python:0">
+ <tr><th colspan="3" class="header">Messages</th>
+ <th colspan="2" class="header">
+ <a href="?@whole_messages=yes">show entire messages</a>
+ </th>
+ </tr>
+ <tr tal:repeat="msg context/messages">
+ <td><a tal:attributes="href string:msg${msg/id}"
+ tal:content="string:msg${msg/id}"></a></td>
+ <td tal:content="msg/author">author</td>
+ <td class="date" tal:content="msg/date/pretty">date</td>
+ <td tal:content="msg/summary">summary</td>
+ <td>
+ <a tal:attributes="href string:?@remove at messages=${msg/id}&@action=edit">remove</a>
+ </td>
+ </tr>
+ </tal:block>
+
+ <tal:block tal:condition="request/form/@whole_messages/value | python:0">
+ <tr><th colspan="2" class="header">Messages</th>
+ <th class="header">
+ <a href="?@whole_messages=">show only summaries</a>
+ </th>
+ </tr>
+ <tal:block tal:repeat="msg context/messages">
+ <tr>
+ <th tal:content="msg/author">author</th>
+ <th class="date" tal:content="msg/date/pretty">date</th>
+ <th style="text-align: right">
+ (<a tal:attributes="href string:?@remove at messages=${msg/id}&@action=edit">remove</a>)
+ </th>
+ </tr>
+ <tr><td colspan="3" tal:content="msg/content"></td></tr>
+ </tal:block>
+ </tal:block>
+ </table>
+
+
+Setting up a "wizard" (or "druid") for controlled adding of issues
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+1. Set up the page templates you wish to use for data input. My wizard
+ is going to be a two-step process: first figuring out what category
+ of issue the user is submitting, and then getting details specific to
+ that category. The first page includes a table of help, explaining
+ what the category names mean, and then the core of the form::
+
+ <form method="POST" onSubmit="return submit_once()"
+ enctype="multipart/form-data">
+ <input type="hidden" name="@template" value="add_page1">
+ <input type="hidden" name="@action" value="page1_submit">
+
+ <strong>Category:</strong>
+ <tal:block tal:replace="structure context/category/menu" />
+ <input type="submit" value="Continue">
+ </form>
+
+ The next page has the usual issue entry information, with the
+ addition of the following form fragments::
+
+ <form method="POST" onSubmit="return submit_once()"
+ enctype="multipart/form-data"
+ tal:condition="context/is_edit_ok"
+ tal:define="cat request/form/category/value">
+
+ <input type="hidden" name="@template" value="add_page2">
+ <input type="hidden" name="@required" value="title">
+ <input type="hidden" name="category" tal:attributes="value cat">
+ .
+ .
+ .
+ </form>
+
+ Note that later in the form, I use the value of "cat" to decide which
+ form elements should be displayed. For example::
+
+ <tal:block tal:condition="python:cat in '6 10 13 14 15 16 17'.split()">
+ <tr>
+ <th>Operating System</th>
+ <td tal:content="structure context/os/field"></td>
+ </tr>
+ <tr>
+ <th>Web Browser</th>
+ <td tal:content="structure context/browser/field"></td>
+ </tr>
+ </tal:block>
+
+ ... the above section will only be displayed if the category is one
+ of 6, 10, 13, 14, 15, 16 or 17.
+
+3. Determine what actions need to be taken between the pages - these are
+ usually to validate user choices and determine what page is next. Now encode
+ those actions in a new ``Action`` class (see `defining new web actions`_)::
+
+ from roundup.cgi.actions import Action
+
+ class Page1SubmitAction(Action):
+ def handle(self):
+ ''' Verify that the user has selected a category, and then move
+ on to page 2.
+ '''
+ category = self.form['category'].value
+ if category == '-1':
+ self.error_message.append('You must select a category of report')
+ return
+ # everything's ok, move on to the next page
+ self.template = 'add_page2'
+
+ def init(instance):
+ instance.registerAction('page1_submit', Page1SubmitAction)
+
+4. Use the usual "new" action as the ``@action`` on the final page, and
+ you're done (the standard context/submit method can do this for you).
+
+
+Debugging Trackers
+==================
+
+There are three switches in tracker configs that turn on debugging in
+Roundup:
+
+1. web :: debug
+2. mail :: debug
+3. logging :: level
+
+See the config.ini file or the `tracker configuration`_ section above for
+more information.
+
+Additionally, the ``roundup-server.py`` script has its own debugging mode
+in which it reloads edited templates immediately when they are changed,
+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
+
Added: tracker/vendor/roundup/current/doc/debugging.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/debugging.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,31 @@
+Debugging Flags
+---------------
+
+Roundup uses a number of debugging environment variables to help you
+figure out what the heck it's doing.
+
+HYPERDBDEBUG
+============
+
+This environment variable should be set to a filename - the hyperdb will
+write debugging information for various events (including, for instance,
+the SQL used).
+
+This is only obeyed when python is _not_ running in -O mode.
+
+HYPERDBTRACE
+============
+
+This environment variable should be set to a filename - the hyperdb will
+write a timestamp entry for various events. This appears to be suffering
+rather extreme bit-rot and may go away soon.
+
+This is only obeyed when python is _not_ running in -O mode.
+
+SENDMAILDEBUG
+=============
+
+Set to a filename and roundup will write a copy of each email message
+that it sends to that file. This environment variable is independent of
+the python -O flag.
+
Added: tracker/vendor/roundup/current/doc/default.css
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/default.css Sun Nov 5 21:30:25 2006
@@ -0,0 +1,239 @@
+/*
+:Author: David Goodger
+:Contact: goodger at users.sourceforge.net
+:date: $Date: 2004/06/09 00:25:32 $
+:version: $Revision: 1.13 $
+:copyright: This stylesheet has been placed in the public domain.
+
+Default cascading style sheet for the HTML output of Docutils.
+*/
+
+a.target {
+ color: blue }
+
+a.toc-backref {
+ text-decoration: none ;
+ color: black }
+
+dd {
+ margin-bottom: 0.5em }
+
+div.abstract {
+ margin: 2em 5em }
+
+div.abstract p.topic-title {
+ font-weight: bold ;
+ text-align: center }
+
+div.attention, div.caution, div.danger, div.error,
+div.important, div.tip, div.warning {
+ margin: 2em ;
+ border: medium outset ;
+ padding: 1em }
+
+div.hint, div.note {
+ font-size: 80%;
+ float: right;
+ width: 15em;
+ margin: 0.5em;
+ margin-left: 1em ;
+ border: solid #aaa;
+ background: #eee;
+ padding: 1em;
+}
+
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title {
+ color: red ;
+ font-weight: bold ;
+ font-family: sans-serif }
+
+div.hint p.admonition-title, div.important p.admonition-title,
+div.note p.admonition-title, div.tip p.admonition-title {
+ font-weight: bold ;
+ font-family: sans-serif }
+
+div.dedication {
+ margin: 2em 5em ;
+ text-align: center ;
+ font-style: italic }
+
+div.dedication p.topic-title {
+ font-weight: bold ;
+ font-style: normal }
+
+div.figure {
+ margin-left: 2em }
+
+div.footer, div.header {
+ font-size: smaller }
+
+div.system-messages {
+ margin: 5em }
+
+div.system-messages h1 {
+ color: red }
+
+div.system-message {
+ border: medium outset ;
+ padding: 1em }
+
+div.system-message p.system-message-title {
+ color: red ;
+ font-weight: bold }
+
+div.topic {
+ margin: 2em }
+
+h1 {
+ margin-top: 2em;
+ text-decoration: underline;
+}
+
+h1.title {
+ text-align: center;
+ margin-top: .5em;
+}
+
+h2.subtitle {
+ text-align: center }
+
+hr {
+ width: 75% }
+
+ol.simple, ul.simple {
+ margin-top: 0;
+ margin-bottom: 1em }
+
+ol.arabic {
+ list-style: decimal }
+
+ol.loweralpha {
+ list-style: lower-alpha }
+
+ol.upperalpha {
+ list-style: upper-alpha }
+
+ol.lowerroman {
+ list-style: lower-roman }
+
+ol.upperroman {
+ list-style: upper-roman }
+
+p.caption {
+ font-style: italic }
+
+p.credits {
+ font-style: italic ;
+ font-size: smaller }
+
+p.first {
+ margin-top: 0 }
+
+p.label {
+ white-space: nowrap }
+
+p.topic-title {
+ font-weight: bold }
+
+pre.address {
+ margin-bottom: 0 ;
+ margin-top: 0 ;
+ font-family: serif ;
+ font-size: 100% }
+
+pre.line-block {
+ font-family: serif ;
+ font-size: 100% }
+
+pre.literal-block, pre.doctest-block {
+ margin-left: 2em ;
+ margin-right: 2em ;
+ background-color: #eeeeee }
+
+span.classifier {
+ font-family: sans-serif ;
+ font-style: oblique }
+
+span.classifier-delimiter {
+ font-family: sans-serif ;
+ font-weight: bold }
+
+span.field-argument {
+ font-style: italic }
+
+span.interpreted {
+ font-family: sans-serif }
+
+span.option-argument {
+ font-style: italic }
+
+span.pre {
+ white-space: pre }
+
+span.problematic {
+ color: red }
+
+table {
+ margin-top: 0.5em ;
+ margin-bottom: 0.5em ;
+ }
+
+table.citation {
+ border-top: 0;
+ border-bottom: 0;
+ border-right: 0;
+ border-left: solid thin gray ;
+ padding-left: 0.5ex }
+
+table.docinfo {
+ margin: 2em 4em }
+
+table.footnote {
+ border-left: solid thin black ;
+ padding-left: 0.5ex }
+
+td, th {
+ padding-left: 0.5em ;
+ padding-right: 0.5em ;
+ vertical-align: baseline;
+}
+
+table.table {
+ border-spacing: 0px;
+ border-collapse: separate;
+}
+table.table td {
+ text-align: left;
+ border: solid thin gray;
+}
+
+table.table th {
+ text-align: left;
+ border: solid thin gray;
+}
+
+td > p:first-child, th > p:first-child {
+ margin-top: 0em }
+
+th.docinfo-name {
+ font-weight: bold ;
+ text-align: right }
+
+th.field-name {
+ font-weight: bold ;
+ text-align: right }
+
+h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
+ font-size: 100% }
+
+tt {
+ background-color: #eeeeee }
+
+tt.literal span.pre {
+ background-color: #eeeeee
+}
+
+ul.auto-toc {
+ list-style-type: none }
Added: tracker/vendor/roundup/current/doc/design.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/design.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,1639 @@
+========================================================
+Roundup - An Issue-Tracking System for Knowledge Workers
+========================================================
+
+:Authors: Ka-Ping Yee (original), Richard Jones (implementation)
+
+.. contents::
+
+Introduction
+---------------
+
+This document presents a description of the components of the Roundup
+system and specifies their interfaces and behaviour in sufficient detail
+to guide an implementation. For the philosophy and rationale behind the
+Roundup design, see the first-round Software Carpentry `submission for
+Roundup`__. This document fleshes out that design as well as specifying
+interfaces so that the components can be developed separately.
+
+__ spec.html
+
+
+The Layer Cake
+-----------------
+
+Lots of software design documents come with a picture of a cake.
+Everybody seems to like them. I also like cakes (i think they are
+tasty). So I, too, shall include a picture of a cake here::
+
+ ________________________________________________________________
+ | E-mail Client | Web Browser | Detector Scripts | Shell |
+ |---------------+---------------+--------------------+-----------|
+ | E-mail User | Web User | Detector | Command |
+ |----------------------------------------------------------------|
+ | Roundup Database Layer |
+ |----------------------------------------------------------------|
+ | Hyperdatabase Layer |
+ |----------------------------------------------------------------|
+ | Storage Layer |
+ ----------------------------------------------------------------
+
+The colourful parts of the cake are part of our system; the faint grey
+parts of the cake are external components.
+
+I will now proceed to forgo all table manners and eat from the bottom of
+the cake to the top. You may want to stand back a bit so you don't get
+covered in crumbs.
+
+
+Hyperdatabase
+-------------
+
+The lowest-level component to be implemented is the hyperdatabase. The
+hyperdatabase is a flexible data store that can hold configurable data
+in records which we call items.
+
+The hyperdatabase is implemented on top of the storage layer, an
+external module for storing its data. The "batteries-includes" distribution
+implements the hyperdatabase on the standard anydbm module. The storage
+layer could be a third-party RDBMS; for a low-maintenance solution,
+implementing the hyperdatabase on the SQLite RDBMS is suggested.
+
+
+Dates and Date Arithmetic
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Before we get into the hyperdatabase itself, we need a way of handling
+dates. The hyperdatabase module provides Timestamp objects for
+representing date-and-time stamps and Interval objects for representing
+date-and-time intervals.
+
+As strings, date-and-time stamps are specified with the date in
+international standard format (``yyyy-mm-dd``) joined to the time
+(``hh:mm:ss``) by a period "``.``". Dates in this form can be easily
+compared and are fairly readable when printed. An example of a valid
+stamp is "``2000-06-24.13:03:59``". We'll call this the "full date
+format". When Timestamp objects are printed as strings, they appear in
+the full date format with the time always given in GMT. The full date
+format is always exactly 19 characters long.
+
+For user input, some partial forms are also permitted: the whole time or
+just the seconds may be omitted; and the whole date may be omitted or
+just the year may be omitted. If the time is given, the time is
+interpreted in the user's local time zone. The Date constructor takes
+care of these conversions. In the following examples, suppose that
+``yyyy`` is the current year, ``mm`` is the current month, and ``dd`` is
+the current day of the month; and suppose that the user is on Eastern
+Standard Time.
+
+- "2000-04-17" means <Date 2000-04-17.00:00:00>
+- "01-25" means <Date yyyy-01-25.00:00:00>
+- "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
+- "08-13.22:13" means <Date yyyy-08-14.03:13:00>
+- "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
+- "14:25" means
+- <Date yyyy-mm-dd.19:25:00>
+- "8:47:11" means
+- <Date yyyy-mm-dd.13:47:11>
+- the special date "." means "right now"
+
+
+Date intervals are specified using the suffixes "y", "m", and "d". The
+suffix "w" (for "week") means 7 days. Time intervals are specified in
+hh:mm:ss format (the seconds may be omitted, but the hours and minutes
+may not).
+
+- "3y" means three years
+- "2y 1m" means two years and one month
+- "1m 25d" means one month and 25 days
+- "2w 3d" means two weeks and three days
+- "1d 2:50" means one day, two hours, and 50 minutes
+- "14:00" means 14 hours
+- "0:04:33" means four minutes and 33 seconds
+
+
+The Date class should understand simple date expressions of the form
+*stamp* ``+`` *interval* and *stamp* ``-`` *interval*. When adding or
+subtracting intervals involving months or years, the components are
+handled separately. For example, when evaluating "``2000-06-25 + 1m
+10d``", we first add one month to get 2000-07-25, then add 10 days to
+get 2000-08-04 (rather than trying to decide whether 1m 10d means 38 or
+40 or 41 days).
+
+Here is an outline of the Date and Interval classes::
+
+ class Date:
+ def __init__(self, spec, offset):
+ """Construct a date given a specification and a time zone
+ offset.
+
+ 'spec' is a full date or a partial form, with an optional
+ added or subtracted interval. 'offset' is the local time
+ zone offset from GMT in hours.
+ """
+
+ def __add__(self, interval):
+ """Add an interval to this date to produce another date."""
+
+ def __sub__(self, interval):
+ """Subtract an interval from this date to produce another
+ date.
+ """
+
+ def __cmp__(self, other):
+ """Compare this date to another date."""
+
+ def __str__(self):
+ """Return this date as a string in the yyyy-mm-dd.hh:mm:ss
+ format.
+ """
+
+ def local(self, offset):
+ """Return this date as yyyy-mm-dd.hh:mm:ss in a local time
+ zone.
+ """
+
+ class Interval:
+ def __init__(self, spec):
+ """Construct an interval given a specification."""
+
+ def __cmp__(self, other):
+ """Compare this interval to another interval."""
+
+ def __str__(self):
+ """Return this interval as a string."""
+
+
+
+Here are some examples of how these classes would behave in practice.
+For the following examples, assume that we are on Eastern Standard Time
+and the current local time is 19:34:02 on 25 June 2000::
+
+ >>> Date(".")
+ <Date 2000-06-26.00:34:02>
+ >>> _.local(-5)
+ "2000-06-25.19:34:02"
+ >>> Date(". + 2d")
+ <Date 2000-06-28.00:34:02>
+ >>> Date("1997-04-17", -5)
+ <Date 1997-04-17.00:00:00>
+ >>> Date("01-25", -5)
+ <Date 2000-01-25.00:00:00>
+ >>> Date("08-13.22:13", -5)
+ <Date 2000-08-14.03:13:00>
+ >>> Date("14:25", -5)
+ <Date 2000-06-25.19:25:00>
+ >>> Interval(" 3w 1 d 2:00")
+ <Interval 22d 2:00>
+ >>> Date(". + 2d") - Interval("3w")
+ <Date 2000-06-07.00:34:02>
+
+
+Items and Classes
+~~~~~~~~~~~~~~~~~
+
+Items contain data in properties. To Python, these properties are
+presented as the key-value pairs of a dictionary. Each item belongs to a
+class which defines the names and types of its properties. The database
+permits the creation and modification of classes as well as items.
+
+
+Identifiers and Designators
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Each item has a numeric identifier which is unique among items in its
+class. The items are numbered sequentially within each class in order
+of creation, starting from 1. The designator for an item is a way to
+identify an item in the database, and consists of the name of the item's
+class concatenated with the item's numeric identifier.
+
+For example, if "spam" and "eggs" are classes, the first item created in
+class "spam" has id 1 and designator "spam1". The first item created in
+class "eggs" also has id 1 but has the distinct designator "eggs1". Item
+designators are conventionally enclosed in square brackets when
+mentioned in plain text. This permits a casual mention of, say,
+"[patch37]" in an e-mail message to be turned into an active hyperlink.
+
+
+Property Names and Types
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Property names must begin with a letter.
+
+A property may be one of five basic types:
+
+- String properties are for storing arbitrary-length strings.
+
+- Boolean properties are for storing true/false, or yes/no values.
+
+- Number properties are for storing numeric values.
+
+- Date properties store date-and-time stamps. Their values are Timestamp
+ objects.
+
+- A Link property refers to a single other item selected from a
+ specified class. The class is part of the property; the value is an
+ integer, the id of the chosen item.
+
+- A Multilink property refers to possibly many items in a specified
+ class. The value is a list of integers.
+
+*None* is also a permitted value for any of these property types. An
+attempt to store None into a Multilink property stores an empty list.
+
+A property that is not specified will return as None from a *get*
+operation.
+
+
+Hyperdb Interface Specification
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+TODO: replace the Interface Specifications with links to the pydoc
+
+The hyperdb module provides property objects to designate the different
+kinds of properties. These objects are used when specifying what
+properties belong in classes::
+
+ class String:
+ def __init__(self, indexme='no'):
+ """An object designating a String property."""
+
+ class Boolean:
+ def __init__(self):
+ """An object designating a Boolean property."""
+
+ class Number:
+ def __init__(self):
+ """An object designating a Number property."""
+
+ class Date:
+ def __init__(self):
+ """An object designating a Date property."""
+
+ class Link:
+ def __init__(self, classname, do_journal='yes'):
+ """An object designating a Link property that links to
+ items in a specified class.
+
+ If the do_journal argument is not 'yes' then changes to
+ the property are not journalled in the linked item.
+ """
+
+ class Multilink:
+ def __init__(self, classname, do_journal='yes'):
+ """An object designating a Multilink property that links
+ to items in a specified class.
+
+ If the do_journal argument is not 'yes' then changes to
+ the property are not journalled in the linked item(s).
+ """
+
+
+Here is the interface provided by the hyperdatabase::
+
+ class Database:
+ """A database for storing records containing flexible data
+ types.
+ """
+
+ def __init__(self, config, journaltag=None):
+ """Open a hyperdatabase given a specifier to some storage.
+
+ The 'storagelocator' is obtained from config.DATABASE. The
+ meaning of 'storagelocator' depends on the particular
+ implementation of the hyperdatabase. It could be a file
+ name, a directory path, a socket descriptor for a connection
+ to a database over the network, etc.
+
+ The 'journaltag' is a token that will be attached to the
+ journal entries for any edits done on the database. If
+ 'journaltag' is None, the database is opened in read-only
+ mode: the Class.create(), Class.set(), Class.retire(), and
+ Class.restore() methods are disabled.
+ """
+
+ def __getattr__(self, classname):
+ """A convenient way of calling self.getclass(classname)."""
+
+ def getclasses(self):
+ """Return a list of the names of all existing classes."""
+
+ def getclass(self, classname):
+ """Get the Class object representing a particular class.
+
+ If 'classname' is not a valid class name, a KeyError is
+ raised.
+ """
+
+ class Class:
+ """The handle to a particular class of items in a hyperdatabase.
+ """
+
+ def __init__(self, db, classname, **properties):
+ """Create a new class with a given name and property
+ specification.
+
+ 'classname' must not collide with the name of an existing
+ class, or a ValueError is raised. The keyword arguments in
+ 'properties' must map names to property objects, or a
+ TypeError is raised.
+
+ A proxied reference to the database is available as the
+ 'db' attribute on instances. For example, in
+ 'IssueClass.send_message', the following is used to lookup
+ users, messages and files::
+
+ users = self.db.user
+ messages = self.db.msg
+ files = self.db.file
+ """
+
+ # Editing items:
+
+ def create(self, **propvalues):
+ """Create a new item of this class and return its id.
+
+ The keyword arguments in 'propvalues' map property names to
+ values. The values of arguments must be acceptable for the
+ types of their corresponding properties or a TypeError is
+ raised. If this class has a key property, it must be
+ present and its value must not collide with other key
+ strings or a ValueError is raised. Any other properties on
+ this class that are missing from the 'propvalues' dictionary
+ are set to None. If an id in a link or multilink property
+ does not refer to a valid item, an IndexError is raised.
+ """
+
+ def get(self, itemid, propname):
+ """Get the value of a property on an existing item of this
+ class.
+
+ 'itemid' must be the id of an existing item of this class or
+ an IndexError is raised. 'propname' must be the name of a
+ property of this class or a KeyError is raised.
+ """
+
+ def set(self, itemid, **propvalues):
+ """Modify a property on an existing item of this class.
+
+ 'itemid' must be the id of an existing item of this class or
+ an IndexError is raised. Each key in 'propvalues' must be
+ the name of a property of this class or a KeyError is
+ raised. All values in 'propvalues' must be acceptable types
+ for their corresponding properties or a TypeError is raised.
+ If the value of the key property is set, it must not collide
+ with other key strings or a ValueError is raised. If the
+ value of a Link or Multilink property contains an invalid
+ item id, a ValueError is raised.
+ """
+
+ def retire(self, itemid):
+ """Retire an item.
+
+ The properties on the item remain available from the get()
+ method, and the item's id is never reused. Retired items
+ are not returned by the find(), list(), or lookup() methods,
+ and other items may reuse the values of their key
+ properties.
+ """
+
+ def restore(self, nodeid):
+ '''Restore a retired node.
+
+ Make node available for all operations like it was before
+ retirement.
+ '''
+
+ def history(self, itemid):
+ """Retrieve the journal of edits on a particular item.
+
+ 'itemid' must be the id of an existing item of this class or
+ an IndexError is raised.
+
+ The returned list contains tuples of the form
+
+ (date, tag, action, params)
+
+ 'date' is a Timestamp object specifying the time of the
+ change and 'tag' is the journaltag specified when the
+ database was opened. 'action' may be:
+
+ 'create' or 'set' -- 'params' is a dictionary of
+ property values
+ 'link' or 'unlink' -- 'params' is (classname, itemid,
+ propname)
+ 'retire' -- 'params' is None
+ """
+
+ # Locating items:
+
+ def setkey(self, propname):
+ """Select a String property of this class to be the key
+ property.
+
+ 'propname' must be the name of a String property of this
+ class or None, or a TypeError is raised. The values of the
+ key property on all existing items must be unique or a
+ ValueError is raised.
+ """
+
+ def getkey(self):
+ """Return the name of the key property for this class or
+ None.
+ """
+
+ def lookup(self, keyvalue):
+ """Locate a particular item 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 items in this class, the matching item's
+ id is returned; otherwise a KeyError is raised.
+ """
+
+ def find(self, **propspec):
+ """Get the ids of items in this class which link to the
+ given items.
+
+ 'propspec' consists of keyword args propname=itemid or
+ propname={<itemid 1>:1, <itemid 2>: 1, ...}
+ 'propname' must be the name of a property in this class,
+ or a KeyError is raised. That property must
+ be a Link or Multilink property, or a TypeError
+ is raised.
+
+ Any item in this class whose 'propname' property links to
+ any of the itemids will be returned. Examples::
+
+ db.issue.find(messages='1')
+ db.issue.find(messages={'1':1,'3':1}, files={'7':1})
+ """
+
+ def filter(self, search_matches, filterspec, sort, group):
+ """ Return a list of the ids of the active items in this
+ class that match the 'filter' spec, sorted by the group spec
+ and then the sort spec.
+ """
+
+ def list(self):
+ """Return a list of the ids of the active items in this
+ class.
+ """
+
+ def count(self):
+ """Get the number of items in this class.
+
+ If the returned integer is 'numitems', the ids of all the
+ items in this class run from 1 to numitems, and numitems+1
+ will be the id of the next item to be created in this class.
+ """
+
+ # Manipulating properties:
+
+ def getprops(self):
+ """Return a dictionary mapping property names to property
+ objects.
+ """
+
+ def addprop(self, **properties):
+ """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.
+ """
+
+ def getitem(self, itemid, cache=1):
+ """ Return a Item convenience wrapper for the item.
+
+ 'itemid' must be the id of an existing item of this class or
+ an IndexError is raised.
+
+ 'cache' indicates whether the transaction cache should be
+ queried for the item. If the item has been modified and you
+ need to determine what its values prior to modification are,
+ you need to set cache=0.
+ """
+
+ class Item:
+ """ A convenience wrapper for the given item. It provides a
+ mapping interface to a single item's properties
+ """
+
+Hyperdatabase Implementations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Hyperdatabase implementations exist to create the interface described in
+the `hyperdb interface specification`_ over an existing storage
+mechanism. Examples are relational databases, \*dbm key-value databases,
+and so on.
+
+Several implementations are provided - they belong in the
+``roundup.backends`` package.
+
+
+Application Example
+~~~~~~~~~~~~~~~~~~~
+
+Here is an example of how the hyperdatabase module would work in
+practice::
+
+ >>> import hyperdb
+ >>> db = hyperdb.Database("foo.db", "ping")
+ >>> db
+ <hyperdb.Database "foo.db" opened by "ping">
+ >>> hyperdb.Class(db, "status", name=hyperdb.String())
+ <hyperdb.Class "status">
+ >>> _.setkey("name")
+ >>> db.status.create(name="unread")
+ 1
+ >>> db.status.create(name="in-progress")
+ 2
+ >>> db.status.create(name="testing")
+ 3
+ >>> db.status.create(name="resolved")
+ 4
+ >>> db.status.count()
+ 4
+ >>> db.status.list()
+ [1, 2, 3, 4]
+ >>> db.status.lookup("in-progress")
+ 2
+ >>> db.status.retire(3)
+ >>> db.status.list()
+ [1, 2, 4]
+ >>> hyperdb.Class(db, "issue", title=hyperdb.String(), status=hyperdb.Link("status"))
+ <hyperdb.Class "issue">
+ >>> db.issue.create(title="spam", status=1)
+ 1
+ >>> db.issue.create(title="eggs", status=2)
+ 2
+ >>> db.issue.create(title="ham", status=4)
+ 3
+ >>> db.issue.create(title="arguments", status=2)
+ 4
+ >>> db.issue.create(title="abuse", status=1)
+ 5
+ >>> hyperdb.Class(db, "user", username=hyperdb.Key(),
+ ... password=hyperdb.String())
+ <hyperdb.Class "user">
+ >>> db.issue.addprop(fixer=hyperdb.Link("user"))
+ >>> db.issue.getprops()
+ {"title": <hyperdb.String>, "status": <hyperdb.Link to "status">,
+ "user": <hyperdb.Link to "user">}
+ >>> db.issue.set(5, status=2)
+ >>> db.issue.get(5, "status")
+ 2
+ >>> db.status.get(2, "name")
+ "in-progress"
+ >>> db.issue.get(5, "title")
+ "abuse"
+ >>> db.issue.find("status", db.status.lookup("in-progress"))
+ [2, 4, 5]
+ >>> db.issue.history(5)
+ [(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse",
+ "status": 1}),
+ (<Date 2000-06-28.19:11:04>, "ping", "set", {"status": 2})]
+ >>> db.status.history(1)
+ [(<Date 2000-06-28.19:09:43>, "ping", "link", ("issue", 5, "status")),
+ (<Date 2000-06-28.19:11:04>, "ping", "unlink", ("issue", 5, "status"))]
+ >>> db.status.history(2)
+ [(<Date 2000-06-28.19:11:04>, "ping", "link", ("issue", 5, "status"))]
+
+
+For the purposes of journalling, when a Multilink property is set to a
+new list of items, the hyperdatabase compares the old list to the new
+list. The journal records "unlink" events for all the items that appear
+in the old list but not the new list, and "link" events for all the
+items that appear in the new list but not in the old list.
+
+
+Roundup Database
+----------------
+
+The Roundup database layer is implemented on top of the hyperdatabase
+and mediates calls to the database. Some of the classes in the Roundup
+database are considered issue classes. The Roundup database layer adds
+detectors and user items, and on issues it provides mail spools, nosy
+lists, and superseders.
+
+
+Reserved Classes
+~~~~~~~~~~~~~~~~
+
+Internal to this layer we reserve three special classes of items that
+are not issues.
+
+Users
+"""""
+
+Users are stored in the hyperdatabase as items of class "user". The
+"user" class has the definition::
+
+ hyperdb.Class(db, "user", username=hyperdb.String(),
+ password=hyperdb.String(),
+ address=hyperdb.String())
+ db.user.setkey("username")
+
+Messages
+""""""""
+
+E-mail messages are represented by hyperdatabase items of class "msg".
+The actual text content of the messages is stored in separate files.
+(There's no advantage to be gained by stuffing them into the
+hyperdatabase, and if messages are stored in ordinary text files, they
+can be grepped from the command line.) The text of a message is saved
+in a file named after the message item designator (e.g. "msg23") for the
+sake of the command interface (see below). Attachments are stored
+separately and associated with "file" items. The "msg" class has the
+definition::
+
+ hyperdb.Class(db, "msg", author=hyperdb.Link("user"),
+ recipients=hyperdb.Multilink("user"),
+ date=hyperdb.Date(),
+ summary=hyperdb.String(),
+ files=hyperdb.Multilink("file"))
+
+The "author" property indicates the author of the message (a "user" item
+must exist in the hyperdatabase for any messages that are stored in the
+system). The "summary" property contains a summary of the message for
+display in a message index.
+
+
+Files
+"""""
+
+Submitted files are represented by hyperdatabase items of class "file".
+Like e-mail messages, the file content is stored in files outside the
+database, named after the file item designator (e.g. "file17"). The
+"file" class has the definition::
+
+ hyperdb.Class(db, "file", user=hyperdb.Link("user"),
+ name=hyperdb.String(),
+ type=hyperdb.String())
+
+The "user" property indicates the user who submitted the file, the
+"name" property holds the original name of the file, and the "type"
+property holds the MIME type of the file as received.
+
+
+Issue Classes
+~~~~~~~~~~~~~
+
+All issues have the following standard properties:
+
+=========== ==========================
+Property Definition
+=========== ==========================
+title hyperdb.String()
+messages hyperdb.Multilink("msg")
+files hyperdb.Multilink("file")
+nosy hyperdb.Multilink("user")
+superseder hyperdb.Multilink("issue")
+=========== ==========================
+
+Also, two Date properties named "creation" and "activity" are fabricated
+by the Roundup database layer. Two user Link properties, "creator" and
+"actor" are also fabricated. By "fabricated" we mean that no such
+properties are actually stored in the hyperdatabase, but when properties
+on issues are requested, the "creation"/"creator" and "activity"/"actor"
+properties are made available. The value of the "creation"/"creator"
+properties relate to issue creation, and the value of the "activity"/
+"actor" properties relate to the last editing of any property on the issue
+(equivalently, these are the dates on the first and last records in the
+issue's journal).
+
+
+Roundupdb Interface Specification
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The interface to a Roundup database delegates most method calls to the
+hyperdatabase, except for the following changes and additional methods::
+
+ class Database:
+ def getuid(self):
+ """Return the id of the "user" item associated with the user
+ that owns this connection to the hyperdatabase."""
+
+ class Class:
+ # Overridden methods:
+
+ def create(self, **propvalues):
+ def set(self, **propvalues):
+ def retire(self, itemid):
+ """These operations trigger detectors and can be vetoed.
+ Attempts to modify the "creation", "creator", "activity"
+ properties or "actor" cause a KeyError.
+ """
+
+ class IssueClass(Class):
+ # Overridden methods:
+
+ def __init__(self, db, classname, **properties):
+ """The newly-created class automatically includes the
+ "messages", "files", "nosy", and "superseder" properties.
+ If the 'properties' dictionary attempts to specify any of
+ these properties or a "creation", "creator", "activity" or
+ "actor" property, a ValueError is raised."""
+
+ def get(self, itemid, propname):
+ def getprops(self):
+ """In addition to the actual properties on the item, these
+ methods provide the "creation", "creator", "activity" and
+ "actor" properties."""
+
+ # New methods:
+
+ def addmessage(self, itemid, summary, text):
+ """Add a message to an issue's mail spool.
+
+ A new "msg" item is constructed using the current date, the
+ user that owns the database connection as the author, and
+ the specified summary text. The "files" and "recipients"
+ fields are left empty. The given text is saved as the body
+ of the message and the item is appended to the "messages"
+ field of the specified issue.
+ """
+
+ def nosymessage(self, itemid, msgid):
+ """Send a message to the members of an issue's nosy list.
+
+ The message is sent only to users on the nosy list who are
+ not already on the "recipients" list for the message. These
+ users are then added to the message's "recipients" list.
+ """
+
+
+Default Schema
+~~~~~~~~~~~~~~
+
+The default schema included with Roundup turns it into a typical
+software bug tracker. The database is set up like this::
+
+ pri = Class(db, "priority", name=hyperdb.String(),
+ order=hyperdb.String())
+ pri.setkey("name")
+ 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 = Class(db, "status", name=hyperdb.String(),
+ order=hyperdb.String())
+ stat.setkey("name")
+ 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")
+
+ Class(db, "keyword", name=hyperdb.String())
+
+ Class(db, "issue", fixer=hyperdb.Multilink("user"),
+ topic=hyperdb.Multilink("keyword"),
+ priority=hyperdb.Link("priority"),
+ status=hyperdb.Link("status"))
+
+(The "order" property hasn't been explained yet. It gets used by the
+Web user interface for sorting.)
+
+The above isn't as pretty-looking as the schema specification in the
+first-stage submission, but it could be made just as easy with the
+addition of a convenience function like Choice for setting up the
+"priority" and "status" classes::
+
+ def Choice(name, *options):
+ cl = Class(db, name, name=hyperdb.String(),
+ order=hyperdb.String())
+ for i in range(len(options)):
+ cl.create(name=option[i], order=i)
+ return hyperdb.Link(name)
+
+
+Detector Interface
+------------------
+
+Detectors are Python functions that are triggered on certain kinds of
+events. The definitions of the functions live in Python modules placed
+in a directory set aside for this purpose. Importing the Roundup
+database module also imports all the modules in this directory, and the
+``init()`` function of each module is called when a database is opened
+to provide it a chance to register its detectors.
+
+There are two kinds of detectors:
+
+1. an auditor is triggered just before modifying an item
+2. a reactor is triggered just after an item has been modified
+
+When the Roundup database is about to perform a ``create()``, ``set()``,
+``retire()``, or ``restore`` operation, it first calls any *auditors*
+that have been registered for that operation on that class. Any auditor
+may raise a *Reject* exception to abort the operation.
+
+If none of the auditors raises an exception, the database proceeds to
+carry out the operation. After it's done, it then calls all of the
+*reactors* that have been registered for the operation.
+
+
+Detector Interface Specification
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``audit()`` and ``react()`` methods register detectors on a given
+class of items::
+
+ class Class:
+ def audit(self, event, detector, priority=100):
+ """Register an auditor on this class.
+
+ 'event' should be one of "create", "set", "retire", or
+ "restore". 'detector' should be a function accepting four
+ arguments. Detectors are called in priority order, execution
+ order is undefined for detectors with the same priority.
+ """
+
+ def react(self, event, detector, priority=100):
+ """Register a reactor on this class.
+
+ 'event' should be one of "create", "set", "retire", or
+ "restore". 'detector' should be a function accepting four
+ arguments. Detectors are called in priority order, execution
+ order is undefined for detectors with the same priority.
+ """
+
+Auditors are called with the arguments::
+
+ audit(db, cl, itemid, newdata)
+
+where ``db`` is the database, ``cl`` is an instance of Class or
+IssueClass within the database, and ``newdata`` is a dictionary mapping
+property names to values.
+
+For a ``create()`` operation, the ``itemid`` argument is None and
+newdata contains all of the initial property values with which the item
+is about to be created.
+
+For a ``set()`` operation, newdata contains only the names and values of
+properties that are about to be changed.
+
+For a ``retire()`` or ``restore()`` operation, newdata is None.
+
+Reactors are called with the arguments::
+
+ react(db, cl, itemid, olddata)
+
+where ``db`` is the database, ``cl`` is an instance of Class or
+IssueClass within the database, and ``olddata`` is a dictionary mapping
+property names to values.
+
+For a ``create()`` operation, the ``itemid`` argument is the id of the
+newly-created item and ``olddata`` is None.
+
+For a ``set()`` operation, ``olddata`` contains the names and previous
+values of properties that were changed.
+
+For a ``retire()`` or ``restore()`` operation, ``itemid`` is the id of
+the retired or restored item and ``olddata`` is None.
+
+
+Detector Example
+~~~~~~~~~~~~~~~~
+
+Here is an example of detectors written for a hypothetical
+project-management application, where users can signal approval of a
+project by adding themselves to an "approvals" list, and a project
+proceeds when it has three approvals::
+
+ # Permit users only to add themselves to the "approvals" list.
+
+ def check_approvals(db, cl, id, newdata):
+ if newdata.has_key("approvals"):
+ if cl.get(id, "status") == db.status.lookup("approved"):
+ raise Reject, "You can't modify the approvals list " \
+ "for a project that has already been approved."
+ old = cl.get(id, "approvals")
+ new = newdata["approvals"]
+ for uid in old:
+ if uid not in new and uid != db.getuid():
+ raise Reject, "You can't remove other users from " \
+ "the approvals list; you can only remove " \
+ "yourself."
+ for uid in new:
+ if uid not in old and uid != db.getuid():
+ raise Reject, "You can't add other users to the " \
+ "approvals list; you can only add yourself."
+
+ # When three people have approved a project, change its status from
+ # "pending" to "approved".
+
+ def approve_project(db, cl, id, olddata):
+ if (olddata.has_key("approvals") and
+ len(cl.get(id, "approvals")) == 3):
+ if cl.get(id, "status") == db.status.lookup("pending"):
+ cl.set(id, status=db.status.lookup("approved"))
+
+ def init(db):
+ db.project.audit("set", check_approval)
+ db.project.react("set", approve_project)
+
+Here is another example of a detector that can allow or prevent the
+creation of new items. In this scenario, patches for a software project
+are submitted by sending in e-mail with an attached file, and we want to
+ensure that there are text/plain attachments on the message. The
+maintainer of the package can then apply the patch by setting its status
+to "applied"::
+
+ # Only accept attempts to create new patches that come with patch
+ # files.
+
+ def check_new_patch(db, cl, id, newdata):
+ if not newdata["files"]:
+ raise Reject, "You can't submit a new patch without " \
+ "attaching a patch file."
+ for fileid in newdata["files"]:
+ if db.file.get(fileid, "type") != "text/plain":
+ raise Reject, "Submitted patch files must be " \
+ "text/plain."
+
+ # When the status is changed from "approved" to "applied", apply the
+ # patch.
+
+ def apply_patch(db, cl, id, olddata):
+ if (cl.get(id, "status") == db.status.lookup("applied") and
+ olddata["status"] == db.status.lookup("approved")):
+ # ...apply the patch...
+
+ def init(db):
+ db.patch.audit("create", check_new_patch)
+ db.patch.react("set", apply_patch)
+
+
+Command Interface
+-----------------
+
+The command interface is a very simple and minimal interface, intended
+only for quick searches and checks from the shell prompt. (Anything more
+interesting can simply be written in Python using the Roundup database
+module.)
+
+
+Command Interface Specification
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A single command, roundup, provides basic access to the hyperdatabase
+from the command line::
+
+ roundup-admin help
+ roundup-admin get [-list] designator[, designator,...] propname
+ roundup-admin set designator[, designator,...] propname=value ...
+ roundup-admin find [-list] classname propname=value ...
+
+See ``roundup-admin help commands`` for a complete list of commands.
+
+Property values are represented as strings in command arguments and in
+the printed results:
+
+- Strings are, well, strings.
+
+- Numbers are displayed the same as strings.
+
+- Booleans are displayed as 'Yes' or 'No'.
+
+- Date values are printed in the full date format in the local time
+ zone, and accepted in the full format or any of the partial formats
+ explained above.
+
+- Link values are printed as item designators. When given as an
+ argument, item designators and key strings are both accepted.
+
+- Multilink values are printed as lists of item designators joined by
+ commas. When given as an argument, item designators and key strings
+ are both accepted; an empty string, a single item, or a list of items
+ joined by commas is accepted.
+
+When multiple items are specified to the roundup get or roundup set
+commands, the specified properties are retrieved or set on all the
+listed items.
+
+When multiple results are returned by the roundup get or roundup find
+commands, they are printed one per line (default) or joined by commas
+(with the -list) option.
+
+
+Usage Example
+~~~~~~~~~~~~~
+
+To find all messages regarding in-progress issues that contain the word
+"spam", for example, you could execute the following command from the
+directory where the database dumps its files::
+
+ shell% for issue in `roundup find issue status=in-progress`; do
+ > grep -l spam `roundup get $issue messages`
+ > done
+ msg23
+ msg49
+ msg50
+ msg61
+ shell%
+
+Or, using the -list option, this can be written as a single command::
+
+ shell% grep -l spam `roundup get \
+ \`roundup find -list issue status=in-progress\` messages`
+ msg23
+ msg49
+ msg50
+ msg61
+ shell%
+
+
+E-mail User Interface
+---------------------
+
+The Roundup system must be assigned an e-mail address at which to
+receive mail. Messages should be piped to the Roundup mail-handling
+script by the mail delivery system (e.g. using an alias beginning with
+"|" for sendmail).
+
+
+Message Processing
+~~~~~~~~~~~~~~~~~~
+
+Incoming messages are examined for multiple parts. In a multipart/mixed
+message or part, each subpart is extracted and examined. In a
+multipart/alternative message or part, we look for a text/plain subpart
+and ignore the other parts. The text/plain subparts are assembled to
+form the textual body of the message, to be stored in the file
+associated with a "msg" class item. Any parts of other types are each
+stored in separate files and given "file" class items that are linked to
+the "msg" item.
+
+The "summary" property on message items is taken from the first
+non-quoting section in the message body. The message body is divided
+into sections by blank lines. Sections where the second and all
+subsequent lines begin with a ">" or "|" character are considered
+"quoting sections". The first line of the first non-quoting section
+becomes the summary of the message.
+
+All of the addresses in the To: and Cc: headers of the incoming message
+are looked up among the user items, and the corresponding users are
+placed in the "recipients" property on the new "msg" item. The address
+in the From: header similarly determines the "author" property of the
+new "msg" item. The default handling for addresses that don't have
+corresponding users is to create new users with no passwords and a
+username equal to the address. (The web interface does not permit
+logins for users with no passwords.) If we prefer to reject mail from
+outside sources, we can simply register an auditor on the "user" class
+that prevents the creation of user items with no passwords.
+
+The subject line of the incoming message is examined to determine
+whether the message is an attempt to create a new issue or to discuss an
+existing issue. A designator enclosed in square brackets is sought as
+the first thing on the subject line (after skipping any "Fwd:" or "Re:"
+prefixes).
+
+If an issue designator (class name and id number) is found there, the
+newly created "msg" item is added to the "messages" property for that
+issue, and any new "file" items are added to the "files" property for
+the issue.
+
+If just an issue class name is found there, we attempt to create a new
+issue of that class with its "messages" property initialized to contain
+the new "msg" item and its "files" property initialized to contain any
+new "file" items.
+
+Both cases may trigger detectors (in the first case we are calling the
+set() method to add the message to the issue's spool; in the second case
+we are calling the create() method to create a new item). If an auditor
+raises an exception, the original message is bounced back to the sender
+with the explanatory message given in the exception.
+
+
+Nosy Lists
+~~~~~~~~~~
+
+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.
+
+
+Setting Properties
+~~~~~~~~~~~~~~~~~~
+
+The e-mail interface also provides a simple way to set properties on
+issues. At the end of the subject line, ``propname=value`` pairs can be
+specified in square brackets, using the same conventions as for the
+roundup ``set`` shell command.
+
+
+Web User Interface
+------------------
+
+The web interface is provided by a CGI script that can be run under any
+web server. A simple web server can easily be built on the standard
+CGIHTTPServer module, and should also be included in the distribution
+for quick out-of-the-box deployment.
+
+The user interface is constructed from a number of template files
+containing mostly HTML. Among the HTML tags in templates are
+interspersed some nonstandard tags, which we use as placeholders to be
+replaced by properties and their values.
+
+
+Views and View Specifiers
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are two main kinds of views: *index* views and *issue* views. An
+index view displays a list of issues of a particular class, optionally
+sorted and filtered as requested. An issue view presents the properties
+of a particular issue for editing and displays the message spool for the
+issue.
+
+A view specifier is a string that specifies all the options needed to
+construct a particular view. It goes after the URL to the Roundup CGI
+script or the web server to form the complete URL to a view. When the
+result of selecting a link or submitting a form takes the user to a new
+view, the Web browser should be redirected to a canonical location
+containing a complete view specifier so that the view can be bookmarked.
+
+
+Displaying Properties
+~~~~~~~~~~~~~~~~~~~~~
+
+Properties appear in the user interface in three contexts: in indices,
+in editors, and as search filters. For each type of property, there are
+several display possibilities. For example, in an index view, a string
+property may just be printed as a plain string, but in an editor view,
+that property should be displayed in an editable field.
+
+The display of a property is handled by functions in the
+``cgi.templating`` module.
+
+Displayer functions are triggered by ``tal:content`` or ``tal:replace``
+tag attributes in templates. The value of the attribute provides an
+expression for calling the displayer function. For example, the
+occurrence of::
+
+ tal:content="context/status/plain"
+
+in a template triggers a call to::
+
+ context['status'].plain()
+
+where the context would be an item of the "issue" class. The displayer
+functions can accept extra arguments to further specify details about
+the widgets that should be generated.
+
+Some of the standard displayer functions include:
+
+========= ==============================================================
+Function Description
+========= ==============================================================
+plain display a String property directly;
+ display a Date property in a specified time zone with an
+ option to omit the time from the date stamp; for a Link or
+ Multilink property, display the key strings of the linked
+ items (or the ids if the linked class has no key property)
+field display a property like the plain displayer above, but in a
+ text field to be edited
+menu for a Link property, display a menu of the available choices
+========= ==============================================================
+
+See the `customisation`_ documentation for the complete list.
+
+
+Index Views
+~~~~~~~~~~~
+
+An index view contains two sections: a filter section and an index
+section. The filter section provides some widgets for selecting which
+issues appear in the index. The index section is a table of issues.
+
+
+Index View Specifiers
+"""""""""""""""""""""
+
+An index view specifier looks like this (whitespace has been added for
+clarity)::
+
+ /issue?status=unread,in-progress,resolved&
+ topic=security,ui&
+ :group=priority&
+ :sort=-activity&
+ :filters=status,topic&
+ :columns=title,status,fixer
+
+
+The index view is determined by two parts of the specifier: the layout
+part and the filter part. The layout part consists of the query
+parameters that begin with colons, and it determines the way that the
+properties of selected items are displayed. The filter part consists of
+all the other query parameters, and it determines the criteria by which
+items are selected for display.
+
+The filter part is interactively manipulated with the form widgets
+displayed in the filter section. The layout part is interactively
+manipulated by clicking on the column headings in the table.
+
+The filter part selects the union of the sets of issues with values
+matching any specified Link properties and the intersection of the sets
+of issues with values matching any specified Multilink properties.
+
+The example specifies an index of "issue" items. Only issues with a
+"status" of either "unread" or "in-progres" or "resolved" are displayed,
+and only issues with "topic" values including both "security" and "ui"
+are displayed. The issues are grouped by priority, arranged in
+ascending order; and within groups, sorted by activity, arranged in
+descending order. The filter section shows filters for the "status" and
+"topic" properties, and the table includes columns for the "title",
+"status", and "fixer" properties.
+
+Associated with each issue class is a default layout specifier. The
+layout specifier in the above example is the default layout to be
+provided with the default bug-tracker schema described above in section
+4.4.
+
+Index Section
+"""""""""""""
+
+The template for an index section describes one row of the index table.
+Fragments protected by a ``tal:condition="request/show/<property>"`` are
+included or omitted depending on whether the view specifier requests a
+column for a particular property. The table cells are filled by the
+``tal:content="context/<property>"`` directive, which displays the value
+of the property.
+
+Here's a simple example of an index template::
+
+ <tr>
+ <td tal:condition="request/show/title"
+ tal:content="contex/title"></td>
+ <td tal:condition="request/show/status"
+ tal:content="contex/status"></td>
+ <td tal:condition="request/show/fixer"
+ tal:content="contex/fixer"></td>
+ </tr>
+
+Sorting
+"""""""
+
+String and Date values are sorted in the natural way. Link properties
+are sorted according to the value of the "order" property on the linked
+items if it is present; or otherwise on the key string of the linked
+items; or finally on the item ids. Multilink properties are sorted
+according to how many links are present.
+
+Issue Views
+~~~~~~~~~~~
+
+An issue view contains an editor section and a spool section. At the top
+of an issue view, links to superseding and superseded issues are always
+displayed.
+
+Issue View Specifiers
+"""""""""""""""""""""
+
+An issue view specifier is simply the issue's designator::
+
+ /patch23
+
+
+Editor Section
+""""""""""""""
+
+The editor section is generated from a template containing
+``tal:content="context/<property>/<widget>"`` directives to insert the
+appropriate widgets for editing properties.
+
+Here's an example of a basic editor template::
+
+ <table>
+ <tr>
+ <td colspan=2
+ tal:content="python:context.title.field(size='60')"></td>
+ </tr>
+ <tr>
+ <td tal:content="context/fixer/field"></td>
+ <td tal:content="context/status/menu"></td>
+ </tr>
+ <tr>
+ <td tal:content="context/nosy/field"></td>
+ <td tal:content="context/priority/menu"></td>
+ </tr>
+ <tr>
+ <td colspan=2>
+ <textarea name=":note" rows=5 cols=60></textarea>
+ </td>
+ </tr>
+ </table>
+
+As shown in the example, the editor template can also include a ":note"
+field, which is a text area for entering a note to go along with a
+change.
+
+When a change is submitted, the system automatically generates a message
+describing the changed properties. The message displays all of the
+property values on the issue and indicates which ones have changed. An
+example of such a message might be this::
+
+ title: Polly Parrot is dead
+ priority: critical
+ status: unread -> in-progress
+ fixer: (none)
+ keywords: parrot,plumage,perch,nailed,dead
+
+If a note is given in the ":note" field, the note is appended to the
+description. The message is then added to the issue's message spool
+(thus triggering the standard detector to react by sending out this
+message to the nosy list).
+
+
+Spool Section
+"""""""""""""
+
+The spool section lists messages in the issue's "messages" property.
+The index of messages displays the "date", "author", and "summary"
+properties on the message items, and selecting a message takes you to
+its content.
+
+Access Control
+--------------
+
+At each point that requires an action to be performed, the security
+mechanisms are asked if the current user has permission. This permission
+is defined as a Permission.
+
+Individual assignment of Permission to user is unwieldy. The concept of
+a Role, which encompasses several Permissions and may be assigned to
+many Users, is quite well developed in many projects. Roundup will take
+this path, and allow the multiple assignment of Roles to Users, and
+multiple Permissions to Roles. These definitions are not persistent -
+they're defined when the application initialises.
+
+There will be three levels of Permission. The Class level permissions
+define logical permissions associated with all items of a particular
+class (or all classes). The Item level permissions define logical
+permissions associated with specific items by way of their user-linked
+properties. The Property level permissions define logical permissions
+associated with a specific property of an item.
+
+
+Access Control Interface Specification
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The security module defines::
+
+ class Permission:
+ ''' Defines a Permission with the attributes
+ - name
+ - description
+ - klass (optional)
+ - properties (optional)
+ - check function (optional)
+
+ The klass may be unset, indicating that this permission is
+ not locked to a particular hyperdb class. There may be
+ multiple Permissions for the same name for different
+ classes.
+
+ If property names are set, permission is restricted to those
+ properties only.
+
+ If check function is set, permission is granted only when
+ the function returns value interpreted as boolean true.
+ The function is called with arguments db, userid, itemid.
+ '''
+
+ class Role:
+ ''' Defines a Role with the attributes
+ - name
+ - description
+ - permissions
+ '''
+
+ class Security:
+ def __init__(self, db):
+ ''' Initialise the permission and role stores, and add in
+ the base roles (for admin user).
+ '''
+
+ def getPermission(self, permission, classname=None, properties=None,
+ check=None):
+ ''' Find the Permission exactly matching the name, class,
+ properties list and check function.
+
+ Raise ValueError if there is no exact match.
+ '''
+
+ def hasPermission(self, permission, userid, classname=None,
+ property=None, itemid=None):
+ ''' Look through all the Roles, and hence Permissions, and
+ see if "permission" exists given the constraints of
+ classname, property and itemid.
+
+ If classname is specified (and only classname) then the
+ search will match if there is *any* Permission for that
+ classname, even if the Permission has additional
+ constraints.
+
+ If property is specified, the Permission matched must have
+ either no properties listed or the property must appear in
+ the list.
+
+ If itemid is specified, the Permission matched must have
+ either no check function defined or the check function,
+ when invoked, must return a True value.
+
+ Note that this functionality is actually implemented by the
+ Permission.test() method.
+ '''
+
+ def addPermission(self, **propspec):
+ ''' Create a new Permission with the properties defined in
+ 'propspec'. See the Permission class for the possible
+ keyword args.
+ '''
+
+ def addRole(self, **propspec):
+ ''' Create a new Role with the properties defined in
+ 'propspec'
+ '''
+
+ def addPermissionToRole(self, rolename, permission):
+ ''' Add the permission to the role's permission list.
+
+ 'rolename' is the name of the role to add permission to.
+ '''
+
+Modules such as ``cgi/client.py`` and ``mailgw.py`` define their own
+permissions like so (this example is ``cgi/client.py``)::
+
+ def initialiseSecurity(security):
+ ''' Create some Permissions and Roles on the security object
+
+ This function is directly invoked by
+ security.Security.__init__() as a part of the Security
+ object instantiation.
+ '''
+ p = security.addPermission(name="Web Registration",
+ description="Anonymous users may register through the web")
+ security.addToRole('Anonymous', p)
+
+Detectors may also define roles in their init() function::
+
+ def init(db):
+ # register an auditor that checks that a user has the "May
+ # Resolve" Permission before allowing them to set an issue
+ # status to "resolved"
+ db.issue.audit('set', checkresolvedok)
+ p = db.security.addPermission(name="May Resolve", klass="issue")
+ security.addToRole('Manager', p)
+
+The tracker dbinit module then has in ``open()``::
+
+ # open the database - it must be modified to init the Security class
+ # from security.py as db.security
+ db = Database(config, name)
+
+ # add some extra permissions and associate them with roles
+ ei = db.security.addPermission(name="Edit", klass="issue",
+ description="User is allowed to edit issues")
+ db.security.addPermissionToRole('User', ei)
+ ai = db.security.addPermission(name="View", klass="issue",
+ description="User is allowed to access issues")
+ db.security.addPermissionToRole('User', ai)
+
+In the dbinit ``init()``::
+
+ # create the two default users
+ user.create(username="admin", password=Password(adminpw),
+ address=config.ADMIN_EMAIL, roles='Admin')
+ user.create(username="anonymous", roles='Anonymous')
+
+Then in the code that matters, calls to ``hasPermission`` and
+``hasItemPermission`` are made to determine if the user has permission
+to perform some action::
+
+ if db.security.hasPermission('issue', 'Edit', userid):
+ # all ok
+
+ if db.security.hasItemPermission('issue', itemid,
+ assignedto=userid):
+ # all ok
+
+Code in the core will make use of these methods, as should code in
+auditors in custom templates. The HTML templating may access the access
+controls through the *user* attribute of the *request* variable. It
+exposes a ``hasPermission()`` method::
+
+ tal:condition="python:request.user.hasPermission('Edit', 'issue')"
+
+or, if the *context* is *issue*, then the following is the same::
+
+ tal:condition="python:request.user.hasPermission('Edit')"
+
+
+Authentication of Users
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Users must be authenticated correctly for the above controls to work.
+This is not done in the current mail gateway at all. Use of digital
+signing of messages could alleviate this problem.
+
+The exact mechanism of registering the digital signature should be
+flexible, with perhaps a level of trust. Users who supply their
+signature through their first message into the tracker should be at a
+lower level of trust to those who supply their signature to an admin for
+submission to their user details.
+
+
+Anonymous Users
+~~~~~~~~~~~~~~~
+
+The "anonymous" user must always exist, and defines the access
+permissions for anonymous users. Unknown users accessing Roundup through
+the web or email interfaces will be logged in as the "anonymous" user.
+
+
+Use Cases
+~~~~~~~~~
+
+public - end users can submit bugs, request new features, request
+ support
+ The Users would be given the default "User" Role which gives "View"
+ and "Edit" Permission to the "issue" class.
+developer - developers can fix bugs, implement new features, provide
+ support
+ A new Role "Developer" is created with the Permission "Fixer" which
+ is checked for in custom auditors that see whether the issue is
+ being resolved with a particular resolution ("fixed", "implemented",
+ "supported") and allows that resolution only if the permission is
+ available.
+manager - approvers/managers can approve new features and signoff bug
+ fixes
+ A new Role "Manager" is created with the Permission "Signoff" which
+ is checked for in custom auditors that see whether the issue status
+ is being changed similar to the developer example. admin -
+ administrators can add users and set user's roles The existing Role
+ "Admin" has the Permissions "Edit" for all classes (including
+ "user") and "Web Roles" which allow the desired actions.
+system - automated request handlers running various report/escalation
+ scripts
+ A combination of existing and new Roles, Permissions and auditors
+ could be used here.
+privacy - issues that are only visible to some users
+ A new property is added to the issue which marks the user or group
+ of users who are allowed to view and edit the issue. An auditor will
+ check for edit access, and the template user object can check for
+ view access.
+
+
+Deployment Scenarios
+--------------------
+
+The design described above should be general enough to permit the use of
+Roundup for bug tracking, managing projects, managing patches, or
+holding discussions. By using items of multiple types, one could deploy
+a system that maintains requirement specifications, catalogs bugs, and
+manages submitted patches, where patches could be linked to the bugs and
+requirements they address.
+
+
+Acknowledgements
+----------------
+
+My thanks are due to Christy Heyl for reviewing and contributing
+suggestions to this paper and motivating me to get it done, and to Jesse
+Vincent, Mark Miller, Christopher Simons, Jeff Dunmall, Wayne Gramlich,
+and Dean Tribble for their assistance with the first-round submission.
+
+
+Changes to this document
+------------------------
+
+- Added Boolean and Number types
+- Added section Hyperdatabase Implementations
+- "Item" has been renamed to "Issue" to account for the more specific
+ nature of the Class.
+- New Templating
+- Access Controls
+- Added "actor" property
+
+------------------
+
+Back to `Table of Contents`_
+
+.. _`Table of Contents`: index.html
+.. _customisation: customizing.html
+
Added: tracker/vendor/roundup/current/doc/developers.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/developers.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,477 @@
+==================
+Developing Roundup
+==================
+
+:Version: $Revision: 1.14 $
+
+.. 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
+ installation, see `customising roundup`_.
+
+.. contents::
+
+Getting Started
+---------------
+
+Anyone wishing to help in the development of Roundup must read `Roundup's
+Design Document`_ and the `implementation notes`_.
+
+All development is coordinated through two resources:
+
+- roundup-dev mailing list at
+ http://lists.sourceforge.net/mailman/listinfo/roundup-devel
+- Sourceforge's issue trackers at
+ https://sourceforge.net/tracker/?group_id=31577
+
+Small Changes
+-------------
+
+Most small changes can be submitted through the Feature tracker, with patches
+attached that give context diffs of the affected source.
+
+
+CVS Access
+----------
+
+To get CVS access, contact richard at users.sourceforge.net.
+
+CVS stuff:
+
+1. to tag a release (eg. the pre-release of 0.5.0)::
+
+ cvs tag release-0-5-0-pr1
+
+1. to make a branch (eg. branching for code freeze/release)::
+
+ cvs co -d maint-0-5 -r release-0-5-0-pr1 roundup
+ cd maint-0-5
+ cvs tag -b maint-0-5
+
+2. to check out a branch (eg. the maintenance branch for 0.5.x)::
+
+ cvs co -d maint-0-5 -r maint-0-5
+
+3. to merge changes from the maintenance branch to the trunk, in the
+ directory containing the HEAD checkout::
+
+ cvs up -j maint-0-5
+
+ though this is highly discouraged, as it generally creates a whole swag
+ of conflicts :(
+
+Standard tag names:
+
+*release-maj-min-patch[-sub]*
+ Release of the major.minor.patch release, possibly a beta or pre-release,
+ in which case *sub* will be one of "b*N*" or "pr*N*".
+*maint-maj-min*
+ Maintenance branch for the major.minor release. Patch releases are tagged in
+ this branch.
+
+Typically, release happen like this:
+
+1. work progresses in the HEAD branch until milestones are met,
+2. a series of beta releases are tagged in the HEAD until the code is
+ stable enough to freeze,
+3. the pre-release is tagged in the HEAD, with the resultant code branched
+ to the maintenance branch for that release,
+4. bugs in the release are patched in the maintenance branch, and the final
+ and patch releases are tagged there, and
+5. further major work happens in the HEAD.
+
+Project Rules
+-------------
+
+Mostly the project follows Guido's Style (though naming tends to be a little
+relaxed sometimes). In short:
+
+- 80 column width code
+- 4-space indentations
+- All modules must have a CVS Id line near the top
+
+Other project rules:
+
+- New functionality must be documented, even briefly (so at least we know
+ where there's missing documentation) and changes to tracker configuration
+ must be logged in the upgrading document.
+- subscribe to roundup-checkins to receive checkin notifications from the
+ other developers with CVS access
+- discuss any changes with the other developers on roundup-dev. If nothing
+ else, this makes sure there's no rude shocks
+- write unit tests for changes you make (where possible), and ensure that
+ all unit tests run before committing changes
+- run pychecker over changed code
+
+The administrators of the project reserve the right to boot developers who
+consistently check in code which is either broken or takes the codebase in
+directions that have not been agreed to.
+
+
+Debugging Aids
+--------------
+
+Try turning on logging of DEBUG level messages. This may be done a number
+of ways, depending on what it is you're testing:
+
+1. If you're testing the database unit tests, then set the environment
+ variable ``LOGGING_LEVEL=DEBUG``. This may be done like so:
+
+ LOGGING_LEVEL=DEBUG python run_tests.py
+
+ This variable replaces the older HYPERDBDEBUG environment var.
+
+2. If you're testing a particular tracker, then set the logging level in
+ your tracker's ``config.ini``.
+
+
+Internationalization Notes
+--------------------------
+
+How stuff works:
+
+1. Strings that may require translation (messages in human language)
+ are marked in the source code. This step is discussed in
+ `Marking Strings for Translation`_ section.
+
+2. These strings are all extracted into Message Template File
+ ``locale/roundup.pot`` (_`POT` file). See `Extracting Translatable
+ Messages`_ below.
+
+3. Language teams use POT file to make Message Files for national
+ languages (_`PO` files). All PO files for Roundup are kept in
+ the ``locale`` directory. Names of these files are target
+ locale names, usually just 2-letter language codes. `Translating
+ Messages`_ section of this chapter gives useful hints for
+ message translators.
+
+4. Translated Message Files are compiled into binary form (_`MO` files)
+ and stored in ``locale`` directory (but not kept in the `Roundup
+ CVS`_ repository, as they may be easily made from PO files).
+ See `Compiling Message Catalogs`_ section.
+
+5. Roundup installer creates runtime locale structure on the file
+ system, putting MO files in their appropriate places.
+
+6. Runtime internationalization (_`I18N`) services use these MO files
+ to translate program messages into language selected by current
+ Roundup user. Roundup command line interface uses locale name
+ set in OS environment variable ``LANGUAGE``, ``LC_ALL``,
+ ``LC_MESSAGES``, or ``LANG`` (in that order). Roundup Web User
+ Interface uses language selected by currently authenticated user.
+
+Additional details may be found in `GNU gettext`_ and Python `gettext
+module`_ documentation.
+
+`Roundup source distribution`_ includes POT and PO files for message
+translators, and also pre-built MO files to facilitate installations
+from source. Roundup binary distribution includes MO files only.
+
+.. _GNU gettext:
+
+GNU gettext package
+^^^^^^^^^^^^^^^^^^^
+
+This chapter is full of references to GNU `gettext package`_.
+GNU gettext is a "must have" for nearly all steps of internationalizing
+any program, and it's manual is definetely a recommended reading
+for people involved in `I18N`_.
+
+There are GNU gettext ports to all major OS platforms.
+Windows binaries are available from `GNU mirror sites`_.
+
+Roundup does not use GNU gettext at runtime, but it's tools
+are used for `extracting translatable messages`_, `compiling
+message catalogs`_ and, optionally, for `translating messages`_.
+
+Note that ``gettext`` package in some OS distributions means just
+runtime tools and libraries. In such cases gettext development tools
+are usually distributed in separate package named ``gettext-devel``.
+
+Marking Strings for Translation
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Strings that need translation must be marked in the source code.
+Following subsections explain how this is done in different cases.
+
+If translatable string is used as a format string, it is recommended
+to always use *named* format specifiers::
+
+ _('Index of %(classname)s') % locals()
+
+This helps translators to better understand the context of the
+message and, with Python formatting, remove format specifier altogether
+(which is sometimes useful, especially in singular cases of `Plural Forms`_).
+
+When there is more than one format specifier in the translatable
+format string, named format specifiers **must** be used almost always,
+because translation may require different order of items.
+
+It is better to *not* mark for translation strings that are not
+locale-dependent, as this makes it more difficult to keep track
+of translation completeness. For example, string ``</ol></body></html>``
+(in ``index()`` method of the request handler in ``roundup_server``
+script) has no human readable parts at all, and needs no translations.
+Such strings are left untranslated in PO files, and are reported
+as such by PO status checkers (e.g. ``msgfmt --statistics``).
+
+Command Line Interfaces
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Scripts and routines run from the command line use "static" language
+defined by environment variables recognized by ``gettext`` module
+from Python library (``LANGUAGE``, ``LC_ALL``, ``LC_MESSAGES``, and
+``LANG``). Primarilly, these are ``roundup-admin`` script and
+``admin.py`` module, but also help texts and startup error messages
+in other scripts and their supporting modules.
+
+For these interfaces, Python ``gettext`` engine must be initialized
+to use Roundup message catalogs. This is normally done by including
+the following line in the module imports::
+
+ from i18n import _, ngettext
+
+Simple translations are automatically marked by calls to builtin
+message translation function ``_()``::
+
+ print _("This message is translated")
+
+Translations for messages whose grammatical depends on a number
+must be done by ``ngettext()`` function::
+
+ print ngettext("Nuked %i file", "Nuked %i files", number_of_files_nuked)
+
+Deferred Translations
+~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes translatable strings appear in the source code in untranslated
+form [#note_admin.py]_ and must be translated elsewhere.
+Example::
+
+ for meal in ("spam", "egg", "beacon"):
+ print _(meal)
+
+In such cases, strings must be marked for translation without actual
+call to the translating function. To mark these strings, we use Python
+feature of automatic concatenation of adjacent strings and different
+types of string quotes::
+
+ strings_to_translate = (
+ ''"This string will be translated",
+ ""'me too',
+ ''r"\raw string",
+ ''"""
+ multiline string"""
+ )
+
+.. [#note_admin.py] In current Roundup sources, this feature is
+ extensively used in the ``admin`` module using method docstrings
+ as help messages.
+
+Web User Interface
+~~~~~~~~~~~~~~~~~~
+
+For Web User Interface, translation services are provided by Client
+object. Action classes have methods ``_()`` and ``gettext()``,
+delegating translation to the Client instance. In HTML templates,
+translator object is available as context variable ``i18n``.
+
+HTML templates have special markup for translatable strings.
+The syntax for this markup is defined on `ZPTInternationalizationSupport`_
+page. Roundup translation service currently ignores values for
+``i18n:domain``, ``i18n:source`` and ``i18n:target``.
+
+Template markup examples:
+
+* simplest case::
+
+ <div i18n:translate="">
+ Say
+ no
+ more!
+ </div>
+
+ this will result in msgid ``"Say no more!"``, with all leading and
+ trailing whitespace stripped, and inner blanks replaced with single
+ space character.
+
+* using variable slots::
+
+ <div i18n:translate="">
+ And now...<br/>
+ No.<span tal:replace="number" i18n:name="slideNo" /><br/>
+ THE LARCH
+ </div>
+
+ Msgid will be: ``"And now...<br /> No.${slideNo}<br /> THE LARCH"``.
+ Template rendering will use context variable ``number`` (you may use
+ any expression) to put instead of ``${slideNo}`` in translation.
+
+* attribute translation::
+
+ <button name="btn_wink" value=" Wink " i18n:attributes="value" />
+
+ will translate the caption (and return value) for the "wink" button.
+
+* explicit msgids. Sometimes it may be useful to specify msgid
+ for the element translation explicitely, like this::
+
+ <span i18n:translate="know what i mean?">this text is ignored</span>
+
+ When rendered, element contents will be replaced by translation
+ of the string specified in ``i18n:translate`` attribute.
+
+* ``i18n`` in `TALES`_. You may translate strings in `TALES`_ python
+ expressions::
+
+ <span tal:replace="python: i18n.gettext('Oh, wicked.')" />
+
+* plural forms. There is no markup for plural forms in `TAL`_ i18n.
+ You must use python expression for that::
+
+ <span tal:replace="python: i18n.ngettext(
+ 'Oh but it\'s only %i shilling.',
+ 'Oh but it\'s only %i shillings.',
+ fine) % fine"
+ />
+
+Extracting Translatable Messages
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The most common tool for message extraction is ``xgettext`` utility
+from `GNU gettext package`_. Unfortunately, this utility has no means
+of `Deferred Translations`_ in Python sources. There is ``xpot`` tool
+from Francois Pinard free `PO utilities`_ that allows to mark strings
+for deferred translations, but it does not handle `plural forms`_.
+
+Roundup overcomes these limitations by using both of these utilities.
+This means that you need both `GNU gettext`_ tools and `PO utilities`_
+to build the Message Template File yourself.
+
+Latest Message Template File is kept in `Roundup CVS`_ and distributed
+with `Roundup Source`_. If you wish to rebuild the template yourself,
+make sure that you have both ``xpot`` and ``xgettext`` installed and
+just run ``gmake`` (or ``make``, if you are on a `GNU`_ system like
+`linux`_ or `cygwin`_) in the ``locale`` directory.
+
+For on-site i18n, Roundup provides command-line utility::
+
+ roundup-gettext <tracker_home>
+
+extracting translatable messages from tracker's html templates.
+This utility creates message template file ``messages.pot`` in
+``locale`` subdirectory of the tracker home directory. Translated
+messages may be put in *locale*.po files (where *locale* is selected
+locale name) in the same directory, e.g.: ``locale/ru.po``.
+These message catalogs are searched prior to system-wide translations
+kept in the ``share`` directory.
+
+Translating Messages
+^^^^^^^^^^^^^^^^^^^^
+
+Gettext Message File (`PO`_ file) is a plain text file, that can be created
+by simple copying ``roundup.pot`` to new .po file, like this::
+
+ $ cp roundup.pot ru.po
+
+The name of PO file is target locale name, usually just 2-letter language
+code (``ru`` for Russian in the above example). Alternatively, PO file
+may be initialized by ``msginit`` utility from `GNU gettext`_ tools::
+
+ $ msginit -i roundup.pot
+
+``msginit`` will check your current locale, and initialize the header
+entry, setting language name, rules for `plural forms`_ and, if available,
+translator's name and email address. The name for PO file is also chosen
+based on current locale.
+
+Next, you will need to edit this file, filling all ``msgstr`` lines with
+translations of the above ``msgid`` entries. PO file is a plain text
+file that can be edited with any text editor. However, there are several
+tools that may help you with this process:
+
+ - ``po-mode`` for `emacs`_. One of `GNU gettext`_ tools. Very handy,
+ definitely recommended if you are comfortable with emacs. Cannot
+ handle `plural forms`_ per se, but allows to edit them in simple
+ text mode.
+
+ - `po filetype plugin`_ for `vim`_. Does not do as much as ``po-mode``,
+ but helps in finding untranslated and fuzzy strings, and checking
+ code references. Please contact `alexander smishlajev`_ if you
+ prefer this, as i have patched this plugin a bit. I have also
+ informed the original plugin author about these changes, but got
+ no reply so far.
+
+ - `poEdit`_ by Vaclav Slavik. Nice cross-platform GUI editor.
+ Unfortunately, it does not handle `plural forms`_. Even worse,
+ it deletes all messages with plural forms when the file is saved.
+ Still, it may be useful to initially translate most of the messages
+ and add plural form messages later.
+
+ - `KBabel`_. Being part of `KDE`_, it works in X windows only.
+ At the first glance looks pretty hairy, with all bells and whistles.
+ Haven't had much experience with it, though.
+
+Compiling Message Catalogs
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Message catalogs (`PO`_ files) must be compiled into binary form
+(`MO`_ files) before they can be used in the application. This
+compilation is handled by ``msgfmt`` utility from `GNU gettext`_
+tools. ``GNUmakefile`` in the ``locale`` directory automatically
+compiles all existing message catalogs after updating them from
+Roundup source files. If you wish to rebuild an individual `MO`_
+file without making everything else, you may, for example::
+
+ $ msgfmt --statistics -o ru.mo ru.po
+
+This way, message translators can check their `PO`_ files without
+extracting strings from source. (Note: String extraction requires
+additional utility that is not part of `GNU gettext`_. See `Extracting
+Translatable Messages`_.)
+
+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
+
+
+.. _External hyperlink targets:
+
+.. _alexander smishlajev:
+.. _als: http://sourceforge.net/users/a1s/
+.. _cygwin: http://www.cygwin.com/
+.. _emacs: http://www.gnu.org/software/emacs/
+.. _gettext package: http://www.gnu.org/software/gettext/
+.. _gettext module: http://docs.python.org/lib/module-gettext.html
+.. _GNU: http://www.gnu.org/
+.. _GNU mirror sites: http://www.gnu.org/prep/ftp.html
+.. _KBabel: http://i18n.kde.org/tools/kbabel/
+.. _KDE: http://www.kde.org/
+.. _linux: http://www.linux.org/
+.. _Plural Forms:
+ http://www.gnu.org/software/gettext/manual/html_node/gettext_150.html
+.. _po filetype plugin:
+ http://vim.sourceforge.net/scripts/script.php?script_id=695
+.. _PO utilities: http://po-utils.progiciels-bpi.ca/
+.. _poEdit: http://poedit.sourceforge.net/
+.. _Roundup CVS: http://sourceforge.net/cvs/?group_id=31577
+.. _Roundup Source:
+.. _Roundup source distribution:
+.. _Roundup binary distribution:
+ http://sourceforge.net/project/showfiles.php?group_id=31577
+.. _TAL:
+.. _Template Attribute Language:
+ http://dev.zope.org/Wikis/DevSite/Projects/ZPT/TAL%20Specification%201.4
+.. _TALES:
+.. _Template Attribute Language Expression Syntax:
+ http://dev.zope.org/Wikis/DevSite/Projects/ZPT/TALES%20Specification%201.3
+.. _vim: http://www.vim.org/
+.. _ZPTInternationalizationSupport: http://dev.zope.org/Wikis/DevSite/Projects/ComponentArchitecture/ZPTInternationalizationSupport
Added: tracker/vendor/roundup/current/doc/features.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/features.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,109 @@
+================
+Roundup Features
+================
+
+Roundup is a simple-to-use and -install issue-tracking system with
+web, e-mail and command-line interfaces. It is based on the winning design
+from Ka-Ping Yee in the Software Carpentry "Track" design competition.
+
+*simple to install*
+ - installation (including web interface) takes about 30 minutes
+ - instant-gratification ``python demo.py`` :)
+ - two templates included in the distribution for you to base your tracker on
+ - play with the demo, customise it and then use *it* as the template for
+ your production tracker
+ - requires *no* additional support software - python (2.3+) is
+ enough to get you going
+ - easy to set up higher-performance storage backends like sqlite_,
+ metakit_, mysql_ and postgresql_
+
+*simple to use*
+ - accessible through the web, email, command-line or Python programs
+ - may be used to track bugs, features, user feedback, sales opportunities,
+ milestones, ...
+ - automatically keeps a full history of changes to issues with
+ configurable verbosity and easy access to information about who created
+ or last modified *any* item in the database
+ - issues have their own mini mailing list (nosy list)
+ - users may sign themselves up, there may be automatic signup for
+ incoming email and users may handle their own password reset requests
+
+*highly configurable*
+ - web interface HTML is fully editable
+ - database schema is also fully editable (only the "user" class is required)
+ with a full set of data types (including dates and many-to-many relations)
+ across all storages available
+ - customised automatic auditors and reactors may be written that perform
+ actions before and after changes are made to entries in the database,
+ or may veto the creation or modification of items int he database
+ - samples are provided for all manner of configuration changes and
+ customisations
+
+*fast, scalable*
+ - with the sqlite, metakit, mysql and postgresql backends, roundup is
+ also fast and scalable, easily handling thousands of issues and users
+ with decent response times
+ - database indexes are automatically added for those backends that
+ support them (sqlite, metakit, mysql and postgresql)
+ - indexed text searching giving fast responses to searches across all
+ messages and indexed string properties
+ - support for the Xapian full-text indexing engine for large trackers
+
+*documented*
+ - documentation exists for installation, upgrading, maintenance, users and
+ customisation
+
+*web interface*
+ - fully editable interfaces for listing and display of items
+ - extendable to include wizards, parent/meta bug displays, ...
+ - differentiates between anonymous, known and admin users
+ - may be set up to require login, and may also only allow admin users
+ to register new users
+ - authentication of user registration and user-driven password resetting
+ using email and one time keys
+ - may be run through CGI as a normal cgi script, as a stand-alone
+ web server, or through Zope
+ - searching may be performed using many constraints, including a full-text
+ search of messages attached to issues
+ - file attachments (added through the web or email) are served up with the
+ correct content-type and filename
+ - email change messages generated by roundup appear to be sent by the
+ person who made the change, but responses will go back through the nosy
+ list by default
+ - flexible access control built around Permissions and Roles with assigned
+ Permissions
+ - generates valid HTML4 or XHTML
+ - detects concurrent user changes
+ - saving and editing of user-defined queries which may optionally be
+ shared with other users
+
+*e-mail interface*
+ - may be set up using sendmail-like delivery alias, POP polling or mailbox
+ polling
+ - may auto-register users who send in mail and aren't known to roundup
+ - nosy list configuration controls how people are added and when messages
+ are sent to the list
+ - auto-filing of "unformatted" messages into a particular class
+ - e-mail attachments are handled sanely, being attached to the issue they're
+ intended for, and forwarded on to the nosy list
+ - sane handling of content-type and content-encoding of messages (text/plain
+ preferred in all situations)
+ - email packages that display threading will show issue messages correctly
+ threaded
+ - users may send in messages from multiple addresses and be associated
+ with the same roundup username
+ - built-in security features like TLS and APOP
+
+*command-line*
+ - may be used to interactively manage roundup databases
+ - may be scripted using standard shell scripting
+ - roundup's API may also be used by other Python programs - a sample is
+ provided that runs once a day and emails people their assigned issues
+ - a variety of sample shell scripts are provided (weekly reports, issue
+ generation, ...)
+
+.. _sqlite: http://www.hwaci.com/sw/sqlite/
+.. _metakit: http://www.equi4.com/metakit/
+.. _mysql: http://sourceforge.net/projects/mysql-python
+.. _postgresql: http://initd.org/software/initd/psycopg
+
Added: tracker/vendor/roundup/current/doc/glossary.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/glossary.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,37 @@
+================
+Roundup Glossary
+================
+
+:Version: $Revision: 1.5 $
+
+.. contents::
+
+
+class
+ a definition of the properties and behaviour of a set of items
+db (or hyperdb)
+ a collection of items
+designator
+ a combined class + itemid reference to any item in the hyperdb
+itemid
+ a numeric reference to a particular item of one class
+item
+ a collection of data that forms one entry in the hyperdb.
+property
+ one element of data that makes up an item. In Roundup, item
+ properties may be changed as needed - even after the tracker
+ has been initialised and used in production.
+schema
+ the definition of all the classes that make up an tracker
+tracker
+ the schema and hyperdb that forms one issue tracker
+tracker home
+ the physical location on disk of a tracker
+
+
+-----------------
+
+Back to `Table of Contents`_
+
+.. _`Table of Contents`: index.html
+
Added: tracker/vendor/roundup/current/doc/images/edit.png
==============================================================================
Binary file. No diff available.
Added: tracker/vendor/roundup/current/doc/images/hyperdb.png
==============================================================================
Binary file. No diff available.
Added: tracker/vendor/roundup/current/doc/images/logo-acl-medium.png
==============================================================================
Binary file. No diff available.
Added: tracker/vendor/roundup/current/doc/images/logo-codesourcery-medium.png
==============================================================================
Binary file. No diff available.
Added: tracker/vendor/roundup/current/doc/images/logo-software-carpentry-standard.png
==============================================================================
Binary file. No diff available.
Added: tracker/vendor/roundup/current/doc/images/roundup-1.png
==============================================================================
Binary file. No diff available.
Added: tracker/vendor/roundup/current/doc/images/roundup.png
==============================================================================
Binary file. No diff available.
Added: tracker/vendor/roundup/current/doc/implementation.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/implementation.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,55 @@
+====================
+Implementation notes
+====================
+
+:Version: $Revision: 1.6 $
+
+[see also the roundup package docstring]
+
+There have been some modifications to the spec. I've marked these in the
+source with 'XXX' comments when I remember to.
+
+In short:
+ Class.find() - may match multiple properties, uses keyword args.
+
+ Class.filter() - isn't in the spec and it's very useful to have at the
+ Class level.
+
+ CGI interface index view specifier layout part - lose the '+' from the
+ sorting arguments (it's a reserved URL character ;). Just made no
+ prefix mean ascending and '-' prefix descending.
+
+ ItemClass - renamed to IssueClass to better match it only having one
+ hypderdb class "issue". Allowing > 1 hyperdb class breaks the
+ "superseder" multilink (since it can only link to one thing, and
+ we'd want bugs to link to support and vice-versa).
+
+ template - the call="link()" is handled by special-case mechanisms in
+ my top-level CGI handler. In a nutshell, the handler looks for a
+ method on itself called 'index%s' or 'item%s' where %s is a class.
+ Most items pass on to the templating mechanism, but the file class
+ _always_ does downloading. It'll probably stay this way too...
+
+ template - call="link(property)" may be used to link "the current item"
+ (from an index) - the link text is the property specified.
+
+ template - added functions that I found very useful: List, History and
+ Submit.
+
+ template - items must specify the message lists, history, etc. Having
+ them by default was sometimes not wanted.
+
+ template - index view determines its default columns from the
+ template's ``tal:condition="request/show/<property>"`` directives.
+
+ template - menu() and field() look awfully similar now .... ;)
+
+ roundup_admin.py - the command-line tool has a lot more commands at its
+ disposal
+
+-----------------
+
+Back to `Table of Contents`_
+
+.. _`Table of Contents`: index.html
+
Added: tracker/vendor/roundup/current/doc/index.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/index.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,170 @@
+=======================================================
+Roundup: an Issue-Tracking System for Knowledge Workers
+=======================================================
+
+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,
+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,
+Godefroid Chapelle,
+Roch'e Compaan,
+Wil Cooley,
+Joe Cooper,
+Kelley Dagley,
+Paul F. Dubois,
+Andrew Eland,
+Jeff Epler,
+Tom Epperly,
+Tamer Fahmy,
+Vickenty Fesunov,
+Hernan Martinez Foffani,
+Stuart D. Gathman,
+Ajit George,
+Frank Gibbons,
+Johannes Gijsbers,
+Gus Gollings,
+Dan Grassi,
+Robin Green,
+Jason Grout,
+Charles Groves,
+Engelbert Gruber,
+Bruce Guenter,
+Thomas Arendsen Hein,
+Juergen Hermann,
+Uwe Hoffmann,
+Tobias Hunger,
+Simon Hyde,
+Paul Jimenez,
+Christophe Kalt,
+Brian Kelley,
+James Kew,
+Sheila King,
+Michael Klatt,
+Bastian Kleineidam,
+Axel Kollmorgen,
+Detlef Lannert,
+Andrey Lebedev,
+Henrik Levkowetz,
+David Linke,
+Fredrik Lundh,
+Georges Martin,
+Gordon McMillan,
+John F Meinel Jr,
+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,
+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,
+Maciej Starzyk,
+Mitchell Surface,
+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.
+
Added: tracker/vendor/roundup/current/doc/installation.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/installation.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,891 @@
+==================
+Installing Roundup
+==================
+
+:Version: 1.76
+
+.. contents::
+ :depth: 2
+
+
+Overview
+========
+
+Broken out separately, there are several conceptual pieces to a
+Roundup installation:
+
+Roundup trackers
+ Trackers consist of issues (be they bug reports or otherwise), tracker
+ configuration file(s), web HTML files etc. Roundup trackers are initialised
+ with a "Template" which defines the fields usable/assignable on a
+ per-issue basis. Descriptions of the provided templates are given in
+ `choosing your template`_.
+
+Roundup support code
+ Installed into your Python install's lib directory.
+
+Roundup scripts
+ These include the email gateway, the roundup
+ HTTP server, the roundup administration command-line interface, etc.
+
+
+Prerequisites
+=============
+
+Roundup requires Python 2.3 or newer with a functioning anydbm
+module. Download the latest version from http://www.python.org/.
+It is highly recommended that users install the latest patch version
+of python as these contain many fixes to serious bugs.
+
+Some variants of Linux will need an additional "python dev" package
+installed for Roundup installation to work. Debian and derivatives, are
+known to require this.
+
+If you're on windows, you will either need to be using the ActiveState python
+distribution (at http://www.activestate.com/Products/ActivePython/), or you'll
+have to install the win32all package separately (get it from
+http://starship.python.net/crew/mhammond/win32/).
+
+
+Optional Components
+===================
+
+You may optionally install and use:
+
+An RDBMS
+ Sqlite, MySQL and Postgresql are all supported by Roundup and will be
+ used if available. One of these is recommended if you are anticipating a
+ large user base (see `choosing your backend`_ below).
+
+Xapian full-text indexer
+ The Xapian_ full-text indexer is also supported and will be used by
+ default if it is available. This is strongly recommended if you are
+ anticipating a large number of issues (> 5000).
+
+ You may install Xapian at any time, even after a tracker has been
+ installed and used. You will need to run the "roundup-admin reindex"
+ command if the tracker has existing data.
+
+ Roundup requires Xapian *newer* than 0.9.2 - it may be necessary for
+ you to install a snapshot. Snapshot "0.9.2_svn6532" has been tried
+ successfully.
+
+.. _Xapian: http://www.xapian.org/
+
+
+Getting Roundup
+===============
+
+.. note::
+ Some systems, such as Debian and NetBSD, already have Roundup
+ installed. Try running the command "roundup-admin" with no arguments,
+ and if it runs you may skip the `Basic Installation Steps`_
+ below and go straight to `configuring your first tracker`_.
+
+Download the latest version from http://roundup.sf.net/.
+
+If you're using WinZIP's "classic" interface, make sure the "Use
+folder names" check box is checked before you extract the files.
+
+
+For The Really Impatient
+========================
+
+If you just want to give Roundup a whirl Right Now, then simply run
+``python demo.py``. If you used the Windows installer, you should run the
+``roundup-demo`` program instead. Users of other binary distributions or
+pre-installed Roundup will need to download the source to use it.
+
+This will set up a simple demo tracker on your machine. [1]_
+When it's done, it'll print out a URL to point your web browser at
+so you may start playing. Three users will be set up:
+
+1. anonymous - the "default" user with permission to do very little
+2. demo (password "demo") - a normal user who may create issues
+3. admin (password "admin") - an administrative user who has complete
+ access to the tracker
+
+.. [1] Demo tracker is set up to be accessed by localhost browser.
+ If you run demo on a server host, please stop the demo when
+ it has shown startup notice, open file ``demo/config.ini`` with
+ your editor, change host name in the ``web`` option in section
+ ``[tracker]``, save the file, then re-run the demo program.
+
+Installation
+============
+
+Set aside 15-30 minutes. There's several steps to follow in your
+installation:
+
+1. `basic installation steps`_ if Roundup is not installed on your system
+2. `configuring your first tracker`_ that all installers must follow
+3. then optionally `configure a web interface`_
+4. and optionally `configure an email interface`_
+5. `UNIX environment steps`_ to take if you're installing on a shared
+ UNIX machine and want to restrict local access to roundup
+6. `additional language codecs`_
+
+For information about how Roundup installs, see the `administration
+guide`_.
+
+
+Basic Installation Steps
+------------------------
+
+To install the Roundup support code into your Python tree and
+Roundup scripts into /usr/bin (substitute that path for whatever is
+appropriate on your system). You need to have write permissions
+for these locations, eg. being root on unix::
+
+ python setup.py install
+
+If you would like to place the Roundup scripts in a directory other
+than ``/usr/bin``, then specify the preferred location with
+``--install-script``. For example, to install them in
+``/opt/roundup/bin``::
+
+ python setup.py install --install-scripts=/opt/roundup/bin
+
+You can also use the ``--prefix`` option to use a completely different
+base directory, if you do not want to use administrator rights. If you
+choose to do this, you may have to change Python's search path (sys.path)
+yourself.
+
+
+Configuring your first tracker
+------------------------------
+
+1. To create a Roundup tracker (necessary to do before you can
+ use the software in any real fashion), you need to set up a "tracker
+ home":
+
+ a. (Optional) If you intend to keep your roundup trackers
+ under one top level directory which does not exist yet,
+ you should create that directory now. Example::
+
+ mkdir /opt/roundup/trackers
+
+ b. Either add the Roundup script location to your ``PATH``
+ environment variable or specify the full path to
+ the command in the next step.
+
+ c. Install a new tracker with the command ``roundup-admin install``.
+ You will be asked a series of questions. Descriptions of the provided
+ templates can be found in `choosing your template`_ below. Descriptions
+ of the available backends can be found in `choosing your backend`_
+ below. The questions will be something like (you may have more
+ templates or backends available)::
+
+ Enter tracker home: /opt/roundup/trackers/support
+ Templates: classic
+ Select template [classic]: classic
+ Back ends: anydbm, mysql, sqlite
+ Select backend [anydbm]: anydbm
+
+ Note: "Back ends" selection list depends on availability of
+ third-party database modules. Standard python distribution
+ includes anydbm module only.
+
+ The "support" part of the tracker name can be anything you want - it
+ is going to be used as the directory that the tracker information
+ will be stored in.
+
+ You will now be directed to edit the tracker configuration and
+ initial schema. At a minimum, you must set "main :: admin_email"
+ (that's the "admin_email" option in the "main" section) "mail ::
+ host", "tracker :: web" and "mail :: domain". If you get stuck,
+ and get configuration file errors, then see the `tracker
+ configuration`_ section of the `customisation documentation`_.
+
+ If you just want to get set up to test things quickly (and follow
+ the instructions in step 3 below), you can even just set the
+ "tracker :: web" variable to::
+
+ web = http://localhost:8080/support/
+
+ The URL *must* end in a '/', or your web interface *will not work*.
+ See `Customising Roundup`_ for details on configuration and schema
+ changes. You may change any of the configuration after
+ you've initialised the tracker - it's just better to have valid values
+ for this stuff now.
+
+ d. Initialise the tracker database with ``roundup-admin initialise``.
+ You will need to supply an admin password at this step. You will be
+ prompted::
+
+ Admin Password:
+ Confirm:
+
+ Note: running this command will *destroy any existing data in the
+ database*. In the case of MySQL and PostgreSQL, any exsting database
+ will be dropped and re-created.
+
+ Once this is done, the tracker has been created.
+
+2. At this point, your tracker is set up, but doesn't have a nice user
+ interface. To set that up, we need to `configure a web interface`_ and
+ optionally `configure an email interface`_. If you want to try your
+ new tracker out, assuming "tracker :: web" is set to
+ ``'http://localhost:8080/support/'``, run::
+
+ roundup-server support=/opt/roundup/trackers/support
+
+ then direct your web browser at:
+
+ http://localhost:8080/support/
+
+ and you should see the tracker interface.
+
+
+Choosing Your Template
+----------------------
+
+Classic Template
+~~~~~~~~~~~~~~~~
+
+The classic template is the one defined in the `Roundup Specification`_. It
+holds issues which have priorities and statuses. Each issue may also have a
+set of messages which are disseminated to the issue's list of nosy users.
+
+Minimal Template
+~~~~~~~~~~~~~~~~
+
+The minimal template has the minimum setup required for a tracker
+installation. That is, it has the configuration files, defines a user database
+and the basic HTML interface to that. It's a completely clean slate for you to
+create your tracker on.
+
+
+Choosing Your Backend
+---------------------
+
+The actual storage of Roundup tracker information is handled by backends.
+There's several to choose from, each with benefits and limitations:
+
+========== =========== ===== ==============================
+Name Speed Users Support
+========== =========== ===== ==============================
+anydbm Slowest Few Always available
+sqlite Fastest(*) Few Needs install (PySQLite_)
+metakit Fastest(*) Few Needs install (metakit_)
+postgresql Fast Many Needs install/admin (psycopg_)
+mysql Fast Many Needs install/admin (MySQLdb_)
+========== =========== ===== ==============================
+
+**sqlite** and **metakit**
+ These use the embedded database engines PySQLite_ and metakit_ to provide
+ very fast backends. They are not suitable for trackers which will have
+ many simultaneous users, but require much less installation and
+ maintenance effort than more scalable postgresql and mysql backends.
+ If you are choosing from these two, please select sqlite.
+**postgresql**
+ Backend for popular RDBMS PostgreSQL. You must read doc/postgresql.txt for
+ additional installation steps and requirements. You must also configure
+ the ``rdbms`` section of your tracker's ``config.ini``
+**mysql**
+ Backend for popular RDBMS MySQL. You must read doc/mysql.txt for additional
+ installation steps and requirements. You must also configure the ``rdbms``
+ section of your tracker's ``config.ini``
+
+You may defer your decision by setting your tracker up with the anydbm
+backend (which is guaranteed to be available) and switching to one of the
+other backends at any time using the instructions in the `administration
+guide`_.
+
+Regardless of which backend you choose, Roundup will attempt to initialise
+a new database for you when you run the roundup-admin "initialise" command.
+In the case of MySQL and PostgreSQL you will need to have the appropriate
+privileges to create databases.
+
+
+Configure a Web Interface
+-------------------------
+
+There are three web interfaces to choose from:
+
+1. `web server cgi-bin`_
+2. `stand-alone web server`_
+3. `Zope product - ZRoundup`_
+4. `Apache HTTP Server with mod_python`_
+
+You may need to give the web server user permission to access the tracker home
+- see the `UNIX environment steps`_ for information. You may also need to
+configure your system in some way - see `platform-specific notes`_.
+
+
+Web Server cgi-bin
+~~~~~~~~~~~~~~~~~~
+
+A benefit of using the cgi-bin approach is that it's the easiest way to
+restrict access to your tracker to only use HTTPS. Access will be slower
+than through the `stand-alone web server`_ though.
+
+If your Python isn't install as "python" then you'll need to edit
+the ``roundup.cgi`` script to fix the first line.
+
+If you're using IIS on a Windows platform, you'll need to run this command
+for the cgi to work (it turns on the PATH_INFO cgi variable)::
+
+ adsutil.vbs set w3svc/AllowPathInfoForScriptMappings TRUE
+
+The ``adsutil.vbs`` file can be found in either ``c:\inetpub\adminscripts``
+or ``c:\winnt\system32\inetsrv\adminsamples\`` or
+``c:\winnt\system32\inetsrv\adminscripts\`` depending on your installation.
+
+More information about ISS setup may be found at:
+
+ http://support.microsoft.com/default.aspx?scid=kb%3Ben-us%3B276494
+
+Copy the ``cgi-bin/roundup.cgi`` file to your web server's ``cgi-bin``
+directory. You will need to configure it to tell it where your tracker home
+is. You can do this either:
+
+Through an environment variable
+ Set the variable TRACKER_HOMES to be a colon (":") separated list of
+ name=home pairs (if you're using apache, the SetEnv directive can do this)
+
+Directly in the ``roundup.cgi`` file itself
+ Add your instance to the TRACKER_HOMES variable as ``'name': 'home'``
+
+The "name" part of the configuration will appear in the URL and identifies the
+tracker (so you may have more than one tracker per cgi-bin script). Make sure
+there are no spaces or other illegal characters in it (to be safe, stick to
+letters and numbers). The "name" forms part of the URL that appears in the
+tracker config "tracker :: web" variable, so make sure they match. The "home"
+part of the configuration is the tracker home directory.
+
+If you're using Apache, you can use an additional trick to hide the
+``.cgi`` extension of the cgi script. Place the ``roundup.cgi`` script
+wherever you want it to be, renamed it to just ``roundup``, and add a
+couple lines to your Apache configuration::
+
+ <Location /path/to/roundup>
+ SetHandler cgi-script
+ </Location>
+
+
+Stand-alone Web Server
+~~~~~~~~~~~~~~~~~~~~~~
+
+This approach will give you the fastest of the three web interfaces. You may
+investigate using ProxyPass or similar configuration in apache to have your
+tracker accessed through the same URL as other systems.
+
+The stand-alone web server is started with the command ``roundup-server``. It
+has several options - display them with ``roundup-server -h``.
+
+The tracker home configuration is similar to the cgi-bin - you may either edit
+the script to change the TRACKER_HOMES variable or you may supply the
+name=home values on the command-line after all the other options.
+
+To make the server run in the background, use the "-d" option, specifying the
+name of a file to write the server process id (pid) to.
+
+
+Zope Product - ZRoundup
+~~~~~~~~~~~~~~~~~~~~~~~
+
+ZRoundup installs as a regular Zope product. Copy the ZRoundup directory to
+your Products directory either in INSTANCE_HOME/Products or the Zope
+code tree lib/python/Products.
+
+When you next (re)start up Zope, you will be able to add a ZRoundup object
+that interfaces to your new tracker.
+
+Apache HTTP Server with mod_python
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`Mod_python`_ is an `Apache`_ module that embeds the Python interpreter
+within the server. Running Roundup this way is much faster than all
+above options and, like `web server cgi-bin`_, allows to use HTTPS
+protocol. The drawback is that this setup is more complicated.
+
+The following instructions were tested on apache 2.0 with mod_python 3.1.
+If you are using older versions, your mileage may vary.
+
+Mod_python uses OS threads. If your apache was built without threads
+(quite commonly), you must load the threading library to run mod_python.
+This is done by setting ``LD_PRELOAD`` to your threading library path
+in apache ``envvars`` file. Example for gentoo linux (``envvars`` file
+is located in ``/usr/lib/apache2/build/``)::
+
+ LD_PRELOAD=/lib/libpthread.so.0
+ export LD_PRELOAD
+
+Example for FreeBSD (``envvars`` is in ``/usr/local/sbin/``)::
+
+ LD_PRELOAD=/usr/lib/libc_r.so
+ export LD_PRELOAD
+
+Next, you have to add Roundup trackers configuration to apache config.
+Roundup apache interface uses two options specified with ``PythonOption``
+directives:
+
+ TrackerHome:
+ defines the tracker home directory - the directory that was specified
+ when you did ``roundup-admin init``. This option is required.
+
+ TrackerLaguage:
+ defines web user interface language. mod_python applications do not
+ receive OS environment variables in the same way as command-line
+ programs, so the language cannot be selected by setting commonly
+ used variables like ``LANG`` or ``LC_ALL``. ``TrackerLanguage``
+ value has the same syntax as values of these environment variables.
+ This option may be omitted.
+
+ TrackerDebug:
+ run the tracker in debug mode. Setting this option to ``yes`` or
+ ``true`` has the same effect as running ``roundup-server -t debug``:
+ the database schema and used html templates are rebuilt for each
+ HTTP request. Values ``no`` or ``false`` mean that all html
+ templates for the tracker are compiled and the database schema is
+ checked once at startup. This is the default behaviour.
+
+ TrackerTiming:
+ has nearly the same effect as environment variable ``CGI_SHOW_TIMING``
+ for standalone roundup server. The difference is that setting this
+ option to ``no`` or ``false`` disables timings display. Value
+ ``comment`` writes request handling times in html comment, and
+ any other non-empty value makes timing report visible. By default,
+ timing display is disabled.
+
+In the following example we have two trackers set up in
+``/var/db/roundup/support`` and ``var/db/roundup/devel`` and accessed
+as ``https://my.host/roundup/support/`` and ``https://my.host/roundup/devel/``
+respectively. Having them share same parent directory allows us to
+reduce the number of configuration directives. Support tracker has
+russian user interface. The other tracker (devel) has english user
+interface (default).
+
+Static files from ``html`` directory are served by apache itself - this
+is quickier and generally more robust than doing that from python.
+Everything else is aliased to dummy (non-existing) ``py`` file,
+which is handled by mod_python and our roundup module.
+
+Example mod_python configuration::
+
+ #################################################
+ # Roundup Issue tracker
+ #################################################
+ # enable Python optimizations (like 'python -O')
+ PythonOptimize On
+ # let apache handle static files from 'html' directories
+ AliasMatch /roundup/(.+)/@@file/(.*) /var/db/roundup/$1/html/$2
+ # everything else is handled by roundup web UI
+ AliasMatch /roundup/([^/]+)/(?!@@file/)(.*) /var/db/roundup/$1/dummy.py/$2
+ # roundup requires a slash after tracker name - add it if missing
+ RedirectMatch permanent /roundup/([^/]+)$ /roundup/$1/
+ # common settings for all roundup trackers
+ <Directory /var/db/roundup/*>
+ Order allow,deny
+ Allow from all
+ AllowOverride None
+ Options None
+ AddHandler python-program .py
+ PythonHandler roundup.cgi.apache
+ # uncomment the following line to see tracebacks in the browser
+ # (note that *some* tracebacks will be displayed anyway)
+ #PythonDebug On
+ </Directory>
+ # roundup tracker homes
+ <Directory /var/db/roundup/support>
+ PythonOption TrackerHome /var/db/roundup/support
+ PythonOption TrackerLanguage ru
+ </Directory>
+ <Directory /var/db/roundup/devel>
+ PythonOption TrackerHome /var/db/roundup/devel
+ </Directory>
+
+
+Configure an Email Interface
+----------------------------
+
+If you don't want to use the email component of Roundup, then remove the
+"``nosyreaction.py``" module from your tracker "``detectors``" directory.
+
+See `platform-specific notes`_ for steps that may be needed on your system.
+
+There are three supported ways to get emailed issues into the
+Roundup tracker. You should pick ONE of the following, all
+of which will continue my example setup from above:
+
+As a mail alias pipe process
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Set up a mail alias called "issue_tracker" as (include the quote marks):
+"``|/usr/bin/python /usr/bin/roundup-mailgw <tracker_home>``"
+(substitute ``/usr/bin`` for wherever roundup-mailgw is installed).
+
+In some installations (e.g. RedHat 6.2 I think) you'll need to set up smrsh so
+sendmail will accept the pipe command. In that case, symlink
+``/etc/smrsh/roundup-mailgw`` to "``/usr/bin/roundup-mailgw``" and change
+the command to::
+
+ |roundup-mailgw /opt/roundup/trackers/support
+
+To test the mail gateway on unix systems, try::
+
+ echo test |mail -s '[issue] test' support at YOUR_DOMAIN_HERE
+
+As a custom router/transport using a pipe process (Exim4 specific)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The following configuration snippets for `Exim 4`_ configuration
+implement a custom router & transport to accomplish mail delivery to
+roundup-mailgw. A configuration for Exim3 is similar but not
+included, since Exim3 is considered obsolete.
+
+.. _Exim 4: http://www.exim.org/
+
+This configuration is similar to the previous section, in that it uses
+a pipe process. However, there are advantages to using a custom
+router/transport process, if you are using Exim.
+
+* This avoids privilege escalation, since otherwise the pipe process
+ will run as the mail user, typically mail. The transport can be
+ configured to run as the user appropriate for the task at hand. In the
+ transport described in this section, Exim4 runs as the unprivileged
+ user ``roundup``.
+
+* Separate configuration is not required for each tracker
+ instance. When a email arrives at the server, Exim passes it through
+ the defined routers. The roundup_router looks for a match with one of
+ the roundup directories, and if there is one it is passed to the
+ roundup_transport, which uses the pipe process described in the
+ previous section (`As a mail alias pipe process`_).
+
+The matching is done in the line::
+
+ require_files = /usr/bin/roundup-mailgw:ROUNDUP_HOME/$local_part/schema.py
+
+The following configuration has been tested on Debian Sarge with
+Exim4.
+
+.. note::
+ Note that the Debian Exim4 packages don't allow pipes in alias files
+ by default, so the method described in the section `As a mail alias
+ pipe process`_ will not work with the default configuration. However,
+ the method described in this section does. See the discussion in
+ ``/usr/share/doc/exim4-config/README.system_aliases`` on any Debian
+ system with Exim4 installed.
+
+ For more Debian-specific information, see suggested addition to
+ README.Debian in
+ http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=343283, which will
+ hopefully be merged into the Debian package eventually.
+
+This config makes a few assumptions:
+
+* That the mail address corresponding to the tracker instance has the
+ same name as the directory of the tracker instance, i.e. the mail
+ interface address corresponding to a Roundup instance called
+ ``/var/lib/roundup/trackers/mytracker`` is ``mytracker at your.host``.
+
+* That (at least) all the db subdirectories of all the tracker
+ instances (ie. ``/var/lib/roundup/trackers/*/db``) are owned by the same
+ user, in this case, 'roundup'.
+
+* That if the ``schema.py`` file exists, then the tracker is ready for
+ use. Another option is to use the ``config.ini`` file, but this recently
+ changed (in 0.8) from ``config.py``.
+
+Macros for Roundup router/transport. Should be placed in the macros
+section of the Exim4 config::
+
+ # Home dir for your Roundup installation
+ ROUNDUP_HOME=/var/lib/roundup/trackers
+
+ # User and group for Roundup.
+ ROUNDUP_USER=roundup
+ ROUNDUP_GROUP=roundup
+
+Custom router for Roundup. This will (probably) work if placed at the
+beginning of the router section of the Exim4 config::
+
+ roundup_router:
+ driver = accept
+ # The config file config.ini seems like a more natural choice, but the
+ # file config.py was replaced by config.ini in 0.8, and schema.py needs
+ # to be present too.
+ require_files = /usr/bin/roundup-mailgw:ROUNDUP_HOME/$local_part/schema.py
+ transport = roundup_transport
+
+Custom transport for Roundup. This will (probably) work if placed at
+the beginning of the router section of the Exim4 config::
+
+ roundup_transport:
+ driver = pipe
+ command = /usr/bin/python /usr/bin/roundup-mailgw ROUNDUP_HOME/$local_part/
+ current_directory = ROUNDUP_HOME
+ home_directory = ROUNDUP_HOME
+ user = ROUNDUP_USER
+ group = ROUNDUP_GROUP
+
+As a regular job using a mailbox source
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Set ``roundup-mailgw`` up to run every 10 minutes or so. For example
+(substitute ``/usr/bin`` for wherever roundup-mailgw is installed)::
+
+ 0,10,20,30,40,50 * * * * /usr/bin/roundup-mailgw /opt/roundup/trackers/support mailbox <mail_spool_file>
+
+Where the ``mail_spool_file`` argument is the location of the roundup submission
+user's mail spool. On most systems, the spool for a user "issue_tracker"
+will be "``/var/mail/issue_tracker``".
+
+As a regular job using a POP source
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To retrieve from a POP mailbox, use a *cron* entry similar to the mailbox
+one (substitute ``/usr/bin`` for wherever roundup-mailgw is
+installed)::
+
+ 0,10,20,30,40,50 * * * * /usr/bin/roundup-mailgw /opt/roundup/trackers/support pop <pop_spec>
+
+where pop_spec is "``username:password at server``" that specifies the roundup
+submission user's POP account name, password and server.
+
+On windows, you would set up the command using the windows scheduler.
+
+As a regular job using an IMAP source
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To retrieve from an IMAP mailbox, use a *cron* entry similar to the
+POP one (substitute ``/usr/bin`` for wherever roundup-mailgw is
+installed)::
+
+ 0,10,20,30,40,50 * * * * /usr/bin/roundup-mailgw /opt/roundup/trackers/support imap <imap_spec>
+
+where imap_spec is "``username:password at server``" that specifies the roundup
+submission user's IMAP account name, password and server. You may
+optionally include a mailbox to use other than the default ``INBOX`` with
+"``imap username:password at server mailbox``".
+
+If you have a secure (ie. HTTPS) IMAP server then you may use ``imaps``
+in place of ``imap`` in the command to use a secure connection.
+
+As with the POP job, on windows, you would set up the command using the
+windows scheduler.
+
+
+UNIX Environment Steps
+----------------------
+
+Each tracker ideally should have its own UNIX group, so create
+a UNIX group (edit ``/etc/group`` or your appropriate NIS map if
+you're using NIS). To continue with my examples so far, I would
+create the UNIX group 'support', although the name of the UNIX
+group does not have to be the same as the tracker name. To this
+'support' group I then add all of the UNIX usernames who will be
+working with this Roundup tracker. In addition to 'real' users,
+the Roundup email gateway will need to have permissions to this
+area as well, so add the user your mail service runs as to the
+group (typically "mail" or "daemon"). The UNIX group might then
+look like::
+
+ support:*:1002:jblaine,samh,geezer,mail
+
+If you intend to use the web interface (as most people do), you
+should also add the username your web server runs as to the group.
+My group now looks like this::
+
+ support:*:1002:jblaine,samh,geezer,mail,apache
+
+The tracker "db" directory should be chmod'ed g+sw so that the group can
+write to the database, and any new files created in the database will be owned
+by the group.
+
+If you're using the mysql or postgresql backend then you'll need to ensure
+that the tracker user has appropriate permissions to create/modify the
+database. If you're using roundup.cgi, the apache user needs permissions
+to modify the database. Alternatively, explicitly specify a database login
+in ``rdbms`` -> ``user`` and ``password`` in ``config.ini``.
+
+An alternative to the above is to create a new user who has the sole
+responsibility of running roundup. This user:
+
+1. runs the CGI interface daemon
+2. runs regular polls for email
+3. runs regular checks (using cron) to ensure the daemon is up
+4. optionally has no login password so that nobody but the "root" user
+ may actually login and play with the roundup setup.
+
+
+Additional Language Codecs
+--------------------------
+
+If you intend to send messages to Roundup that use Chinese, Japanese or
+Korean encodings the you'll need to obtain CJKCodecs from
+http://cjkpython.berlios.de/
+
+
+
+Maintenance
+===========
+
+Read the separate `administration guide`_ for information about how to
+perform common maintenance tasks with Roundup.
+
+
+Upgrading
+=========
+
+Read the separate `upgrading document`_, which describes the steps needed to
+upgrade existing tracker trackers for each version of Roundup that is
+released.
+
+
+Further Reading
+===============
+
+If you intend to use Roundup with anything other than the default
+templates, if you would like to hack on Roundup, or if you would
+like implementation details, you should read `Customising Roundup`_.
+
+
+Running Multiple Trackers
+=========================
+
+Things to think about before you jump off the deep end and install
+multiple trackers, which involve additional URLs, user databases, email
+addresses, databases to back up, etc.
+
+1. Do you want a tracker per product you sell/support? You can just add
+ a new property to your issues called Product, and filter by that. See
+ the customisation example `adding a new field to the classic schema`_.
+2. Do you want to track internal software development issues and customer
+ support issues separately? You can just set up an additional "issue"
+ class called "cust_issues" in the same tracker, mimicing the normal
+ "issue" class, but with different properties. See the customisation
+ example `tracking different types of issues`_.
+
+
+Platform-Specific Notes
+=======================
+
+Windows command-line tools
+--------------------------
+
+To make the command-line tools accessible in Windows, you need to update
+the "Path" environment variable in the Registry via a dialog box.
+
+On Windows 2000 and later:
+
+1) Press the "Start" button.
+2) Choose "Settings"
+3) Choose "Control Panel"
+4) Choose "System"
+5) Choose "Advanced"
+6) Choose "Environmental Variables"
+7) Add: "<dir>\Scripts" to the "Path" environmental variable.
+
+Where <dir> in 7) is the root directory (e.g., ``C:\Python22\Scripts``)
+of your Python installation.
+
+I understand that in XP, 2) above is not needed as "Control
+Panel" is directly accessible from "Start".
+
+I do not believe this is possible to do in previous versions of Windows.
+
+
+Windows Server
+--------------
+
+To have the Roundup web server start up when your machine boots up, set the
+following up in Scheduled Tasks (note, the following is for a cygwin setup):
+
+Run
+ ``c:\cygwin\bin\bash.exe -c "roundup-server TheProject=/opt/roundup/trackers/support"``
+Start In
+ ``C:\cygwin\opt\roundup\bin``
+Schedule
+ At System Startup
+
+To have the Roundup mail gateway run periodically to poll a POP email address,
+set the following up in Scheduled Tasks:
+
+Run
+ ``c:\cygwin\bin\bash.exe -c "roundup-mailgw /opt/roundup/trackers/support pop roundup:roundup at mail-server"``
+Start In
+ ``C:\cygwin\opt\roundup\bin``
+Schedule
+ Every 10 minutes from 5:00AM for 24 hours every day
+ Stop the task if it runs for 8 minutes
+
+
+Sendmail smrsh
+--------------
+
+If you use Sendmail's ``smrsh`` mechanism, you will need to tell
+smrsh that roundup-mailgw is a valid/trusted mail handler
+before it will work.
+
+This is usually done via the following 2 steps:
+
+1. make a symlink in ``/etc/smrsh`` called ``roundup-mailgw``
+ which points to the full path of your actual ``roundup-mailgw``
+ script.
+
+2. change your alias to ``"|roundup-mailgw <tracker_home>"``
+
+
+Linux
+-----
+
+Make sure you read the instructions under `UNIX environment steps`_.
+
+
+Solaris
+-------
+
+You'll need to build Python.
+
+Make sure you read the instructions under `UNIX environment steps`_.
+
+
+Problems? Testing your Python...
+================================
+
+.. note::
+ The ``run_tests.py`` script is packaged in Roundup's source distribution
+ - users of the Windows installer, other binary distributions or
+ pre-installed Roundup will need to download the source to use it.
+
+Once you've unpacked roundup's source, run ``python run_tests.py`` in the
+source directory and make sure there are no errors. If there are errors,
+please let us know!
+
+If the above fails, you may be using the wrong version of python. Try
+``python2 run_tests.py``. If that works, you will need to substitute
+``python2`` for ``python`` in all further commands you use in relation to
+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
+.. _`tracker configuration`: customizing.html#tracker-configuration
+.. _`customisation documentation`: customizing.html
+.. _`Adding a new field to the classic schema`:
+ customizing.html#adding-a-new-field-to-the-classic-schema
+.. _`Tracking different types of issues`:
+ customizing.html#tracking-different-types-of-issues
+.. _`customising roundup`: customizing.html
+.. _`upgrading document`: upgrading.html
+.. _`administration guide`: admin_guide.html
+
+
+.. _External hyperlink targets:
+
+.. _apache: http://httpd.apache.org/
+.. _metakit: http://www.equi4.com/metakit/
+.. _mod_python: http://www.modpython.org/
+.. _MySQLdb: http://sourceforge.net/projects/mysql-python
+.. _Psycopg: http://initd.org/software/initd/psycopg
+.. _pysqlite: http://pysqlite.org/
Added: tracker/vendor/roundup/current/doc/mysql.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/mysql.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,66 @@
+=============
+MySQL Backend
+=============
+
+:version: $Revision: 1.12 $
+
+This notes detail the MySQL backend for the Roundup issue tracker.
+
+
+Prerequisites
+=============
+
+To use MySQL as the backend for storing roundup data, you also need
+to install:
+
+1. MySQL RDBMS 4.0.16 or higher - http://www.mysql.com. Your MySQL
+ installation MUST support InnoDB tables (or Berkeley DB (BDB) tables
+ if you have no other choice). If you're running < 4.0.16 (but not <4.0)
+ then you'll need to use BDB to pass all unit tests. Edit the
+ ``roundup/backends/back_mysql.py`` file to enable DBD instead of InnoDB.
+2. Python MySQL interface - http://sourceforge.net/projects/mysql-python
+
+.. note::
+ The InnoDB implementation has a bug__ that Roundup tickles. See
+
+__ http://bugs.mysql.com/bug.php?id=1810
+
+
+Running the MySQL tests
+=======================
+
+Roundup tests expect an empty MySQL database. Two alternate ways to provide
+this:
+
+1. If you have root permissions on the MySQL server, you can create
+ the necessary database entries using the follwing SQL sequence. Use
+ ``mysql`` on the command line to enter::
+
+ CREATE DATABASE rounduptest;
+ USE rounduptest;
+ GRANT ALL PRIVILEGES ON rounduptest.* TO rounduptest at localhost
+ IDENTIFIED BY 'rounduptest';
+ FLUSH PRIVILEGES;
+
+2. If your administrator has provided you with database connection info,
+ you can modify MYSQL_* constants in the file test/test_db.py with
+ the correct values.
+
+The MySQL database should not contain any tables. Tests will not
+drop the database with existing data.
+
+
+Showing MySQL who's boss
+========================
+
+If things ever get to the point where that test database is totally hosed,
+just::
+
+ $ su -
+ # /etc/init.d/mysql stop
+ # rm -rf /var/lib/mysql/rounduptest
+ # /etc/init.d/mysql start
+
+and all will be better (note that on some systems, ``mysql`` is spelt
+``mysqld``).
+
Added: tracker/vendor/roundup/current/doc/original_overview.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/original_overview.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,962 @@
+<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html><head>
+<title>Roundup: an Issue-Tracking System for Knowledge Workers</title>
+<link rev=made href="mailto:ping at lfw.org">
+</head><body>
+
+<table width="100%">
+<tr>
+
+<td align="left">
+<a href="http://www.software-carpentry.com">
+<img src="images/logo-software-carpentry-standard.png" alt="[Software Carpentry logo]" border="0">
+</a>
+</td>
+
+<td align="right">
+<table>
+<tr><td>
+<a href="http://www.acl.lanl.gov">
+<img src="images/logo-acl-medium.png" alt="[ACL Logo]" border="0">
+</a>
+</td></tr>
+<tr><td><hr></td></tr>
+<tr><td>
+<a href="http://www.codesourcery.com">
+<img src="images/logo-codesourcery-medium.png" alt="[CodeSourcery Logo]" border="0">
+</a>
+</td></tr>
+</table>
+</td>
+
+</tr>
+
+<tr>
+
+<td colspan="2"><em>
+Copyright (c) 2000 Ka-Ping Yee. This material may
+be distributed only subject to the terms and conditions set forth in
+the Software Carpentry Open Publication License, which is available at:
+<center>
+<a href="http://www.software-carpentry.com/openpub-license.html">http://www.software-carpentry.com/openpub-license.html</a>
+</center>
+</em></td>
+
+</tr>
+</table>
+
+<p><hr><p>
+
+
+<h1 align=center>Roundup</h1>
+<h3 align=center>An Issue-Tracking System for Knowledge Workers</h3>
+<h4 align=center>Ka-Ping Yee</h4>
+<h4 align=center><a href="http://www.lfw.org/">lfw discorporated</a><br>
+<a href="mailto:ping at lfw.org">ping at lfw.org</a></h4>
+
+<!-- the following line will start a comment in lynx -soft_dquotes mode -->
+<p style="><!--">
+
+<p><hr>
+<h2>Contents</h2>
+
+<ul>
+<li><a href="#overview">Overview</a>
+<li><a href="#background">Background</a>
+ <ul>
+ <li><a href="#principles">Guiding Principles</a>
+ </ul>
+<li><a href="#data">Data Model</a>
+ <ul>
+ <li><a href="#hyperdb">The Hyperdatabase</a>
+ <li><a href="#rationale">Rationale</a>
+ <li><a href="#roundupdb">Roundup's Hyperdatabase</a>
+ <li><a href="#schema">The Default Schema</a>
+ </ul>
+<li><a href="#ui">User Interface</a>
+ <ul>
+ <li><a href="#discuss">Submission and Discussion (Nosy Lists)</a>
+ <li><a href="#edit">Editing (Templated UI)</a>
+ <li><a href="#browse">Browsing and Searching</a>
+ </ul>
+<li><a href="#devplan">Development Plan</a>
+<li><a href="#issues">Open Issues</a>
+<li><a href="#summary">Summary</a>
+<li><a href="#ack">Acknowledgements</a>
+</ul>
+
+<!-- this comment will end the comment started in lynx -soft_dquotes mode -->
+
+<p><hr>
+<h2><a name="overview">Overview</a></h2>
+
+<p>We propose an issue-tracking system called
+<em>Roundup</em>, which will manage a number of issues
+(with properties such as "description", "priority", and so on)
+and provide the ability to
+(a) submit new issues,
+(b) find and edit existing issues,
+and
+(c) discuss issues with other participants.
+The system will facilitate communication
+among the participants by managing discussions and
+notifying interested parties when issues are edited.
+
+<p>This design draws on experience from
+<a href="http://www.lfw.org/ping/roundup.html">an existing
+implementation</a> which we will refer to
+as "the Roundup prototype".
+The graphical interface we have in mind will resemble
+<a href="http://www.lfw.org/ping/roundup-1.png">
+the main display of the prototype</a>.
+
+<p align=center>
+<a href="images/roundup-1.png">
+<img src="images/roundup.png" width=358 height=205 border=0 alt=""></a>
+
+<p><hr>
+<h2><a name="background">Background</a></h2>
+
+<p>A typical software project requires the management of
+many tasks, usually distributed among several collaborators.
+In fact, any project team
+could use a tool for sorting out and discussing all the
+relevant issues. A common approach is to set up some kind
+of "to-do" list that people can share.
+
+<p>However, to address the overall problem we need much more
+than just a shared to-do list; we need to
+manage a growing body of knowledge and experience to help a
+team collaborate effectively on a project. The issue-tracking
+tool becomes a nexus for communication: the Grand Central
+Station of the group intelligence.
+
+<p>The primary focus of this design is to help
+developers work together well, not to provide a customer
+service interface to the developers. This is not to say that
+the design is to be made unsuitable for customers to use.
+Rather, it is assumed that many of the same qualities
+that are good for supporting development (see below)
+are also good for non-developers using the system.
+Additional niceties
+for providing a safe or simplified interface to clients are
+intentionally deferred for later consideration.
+
+<p>A good issue-tracking system should have at least the
+following properties:
+
+<p><table align=right width="40%" bgcolor="#808080"
+cellspacing=0 cellpadding=0 border=0><tr><td
+><table bgcolor="#e8e8e8" width="100%"
+cellspacing=0 cellpadding=5 border=0><tr><td
+><p><font color="#808080"><small>
+With a nod to the time-honoured computer science tradition
+of "filling in the fourth quadrant", we note that
+there are really four kinds of information flow
+going on here. The three mentioned qualities
+really address the first three quadrants of this 2-by-2 matrix,
+respectively:
+
+<ol>
+<li>User push: a user submits information to the system.
+<li>User pull: a user queries for information from the system.
+<li>System push: the system sends information out to users.
+<li>System pull: the system solicits information from users.
+</ol>
+
+An example of the fourth kind of flow is voting.
+Voting isn't described in this design,
+but it should be noted as a potential enhancement.
+</small></font></td></tr></table></td></tr></table>
+
+<ol>
+<li><strong>Low barrier to participation.</strong>
+The usefulness of the tool depends entirely on the
+information people contribute to it. It must be made
+as easy as possible to submit new issues and contribute
+information about existing issues.<p>
+
+<li><strong>Straightforward navigation.</strong>
+It should be easy for users to extract information they need
+from the system to direct their decisions and tasks.
+They should be able to get a decent overview of
+things as well as finding specific information when
+they know what they're after.<p>
+
+<li><strong>Controlled information flow.</strong>
+The users must have control over how much information and
+what information they get. A common flaw of some issue-tracking
+systems is that they inundate users with so much useless
+e-mail that people avoid the system altogether.
+</ol>
+<br clear=all>
+
+<p><br>
+<h3><a name="principles">Guiding Principles</a></h3>
+
+<p><strong>Simplicity.</strong> It is a strong requirement
+that the tool be accessible and understandable. It should
+be fairly obvious what different parts of the interface do,
+and the inner mechanisms should operate in ways that most
+users can easily predict.
+
+<p><strong>Efficiency.</strong>
+We aim to optimize for minimum effort to do the most common
+operations, and best use of resources like screen real estate
+to maximize the amount of information that we summarize and present.
+
+<p><strong>Generality.</strong> We try to avoid making
+unnecessary assumptions that would restrict the applicability
+of the tool. For example, there is no reason why one might
+not also want to use this tool to manage a design process,
+non-software projects, or organizational decisions.
+
+<p><strong>Persistence.</strong> We
+prefer hiding or reclassifying information to deleting it.
+This helps support the collection of statistics later.
+If records are never destroyed, there is little danger
+in providing access to a larger community, and logging yields
+accountability, which may encourage better behaviour.
+
+<p><hr>
+<p><table align=right width="40%" bgcolor="#808080"
+cellspacing=0 cellpadding=0 border=0><tr><td
+><table bgcolor="#e8e8e8" width="100%"
+cellspacing=0 cellpadding=5 border=0><tr><td
+><font color="#808080"><small>
+Okay, enough ranting. Let's get down to business.
+</small></font></td></tr></table></td></tr></table>
+<h2><a name="data">Data Model</a></h2>
+
+<p>Roundup stores a number of <em>items</em>, each of
+which can have several properties and an associated discussion.
+The properties can be used to classify or search for items.
+The discussion is a sequence of e-mail messages.
+Each item is identified by a unique number, and has
+an activity log which
+records the time and content of edits made on its properties.
+The log stays fairly small since the design intentionally
+provides only small data types as item properties, and
+encourages anything large to be attached to
+e-mail where it becomes part of the discussion.
+The next section explains how items are organized.
+
+<h3><a name="hyperdb">The Hyperdatabase</a></h3>
+
+<p><table align=right width="40%" bgcolor="#808080"
+cellspacing=0 cellpadding=0 border=0><tr><td
+><table bgcolor="#e8e8e8" width="100%"
+cellspacing=0 cellpadding=5 border=0><tr><td
+><font color="#808080"><small>
+In my opinion, forcing
+items into fixed categories is one of the most
+serious problems with the Roundup prototype.
+The hyperdatabase is an <em>experimental</em> attempt to
+address the problem of information organization,
+whose scope goes beyond just Roundup.
+</small></font></td></tr></table></td></tr></table>
+
+Often when classifying information we are
+asked to select exactly one of a number of categories
+or to fit it into a rigid hierarchy. Yet things
+only sometimes fall into one category; often,
+a piece of information may be related to several concepts.
+
+For example, forcing each item into a single topic
+category is not just suboptimal but counterproductive:
+seekers of that
+item may expect to find it in a different category
+and conclude that the item is not present in the
+database -- which has them <em>worse</em> off
+than if the items were not categorized at all.
+
+<p>Some systems try to alleviate this problem by
+allowing nodes to appear at multiple locations
+in a tree, as with "aliases" or "symbolic links" in
+a filesystem, for example. This does help somewhat,
+but we want to be even more flexible
+by allowing the
+organization of nodes into sets that may freely
+intersect. Rather than putting each node at exactly
+one place in an overall "grand scheme", a node can
+belong to as many sets as are appropriate.
+
+If we choose to represent the sets themselves as nodes
+and set membership as a link between nodes,
+we're now ready to present the definition of a
+hyperdatabase.
+
+<p><table align=right width="40%" bgcolor="#808080"
+cellpadding=0 cellspacing=0 border=0><tr><td
+><table bgcolor="#e8e8e8" width="100%"
+cellspacing=0 cellpadding=5 border=0><tr><td
+><font color="#808080"><small>
+Perhaps it's too pretentious a name?
+You could say this is just like an object database.
+The hyperdatabase is hardly much of an invention; the
+intent is merely to emphasize querying on links
+rather than properties.
+(I haven't heard of this being done with
+object databases before, but plead ignorance if
+there's already a good name for this idea.)
+</small></font></td></tr></table></td></tr></table>
+A <em>hyperdatabase</em> is a collection of <em>nodes</em>
+that may be hyperlinked to
+each other (hence the name "hyperdatabase").
+Each node carries a collection of key-value pairs,
+where some of the values may be links to other nodes.
+Any node may have an arbitrary number of outgoing and
+incoming links. Hyperdatabases are able to efficiently
+answer queries such as "what nodes link to this node?"
+and "what nodes does this node link to?"
+
+<h3><a name="rationale">Rationale</a></h3>
+
+<p>There are several reasons for building our
+own kind of database for Roundup rather than using an existing one.
+
+Requiring the installation of a full-blown third-party
+SQL database system would probably deter many potential
+users from attempting to set up Roundup;
+yet a real relational database would be too
+complicated to implement on our own.
+
+On the other hand, a hyperdatabase can be implemented fairly easily
+using one of the Python DBM modules, so we can
+take the "batteries-included" approach and provide it
+as part of the system. It's easier to build and understand
+than a true relational database (in accordance with our guiding
+principle of <em>simplicity</em>), but provides
+most of the query functionality we want.
+
+<p>A hyperdatabase is well suited for finding the intersection
+of a number of sets in which items belong. We expect that
+most of the queries people want to do will be of this
+form, rather than complicated SQL queries. For example, a
+typical request might be
+"show me all critical items related to security".
+The ability to store arbitrary key-value pairs and links
+on nodes gives it more flexibility than an RDBMS.
+
+Users are not going to be making thousands of queries
+per second, so it makes sense to optimize for simplicity
+and flexibility rather than performance.
+
+<p align=center><img src="images/hyperdb.png" width=433 height=352 alt=""></a>
+
+
+<h3><a name="roundupdb">Roundup's Hyperdatabase</a></h3>
+
+<p>For our application, we store each item as a node in a
+hyperdatabase. The item's properties are stored
+as key-value pairs on its node.
+Four types of properties are allowed:
+<em>string</em>, <em>date</em>,
+<em>choice</em>, and <em>reference</em>.
+
+<p>The <em>string</em> type is for short, free-form strings.
+String properties are not intended to contain large
+amounts of text, and it is recommended that they be presented
+as one-line fields to encourage brevity.
+
+<p>The <em>date</em> type is for calendar dates and times.
+
+<p>The <em>choice</em> type denotes a single selection
+from a number of options. A <em>choice</em> property
+entails a link from the node possessing the property to
+the node representing the chosen option.
+
+<p>The <em>reference</em> type is for a list of links to any
+number of other nodes in the in the database. A <em>reference</em>
+property, for example, can be used to refer to related items
+or topic categories relevant to an item.
+
+<p>For Roundup, all items have five properties
+that are not customizable:
+
+<ul>
+<li>a <em>string</em> property named <strong>description</strong>
+<li>a <em>reference</em> property named <strong>superseder</strong>
+<li>a <em>reference</em> property named <strong>nosy</strong>
+<li>a <em>date</em> property named <strong>creation</strong>
+<li>a <em>date</em> property named <strong>activity</strong>
+</ul>
+
+<p>The <strong>description</strong> property is a short
+one-line description of the item.
+The detailed description can go in the
+first e-mail message of the item's discussion spool.
+
+<p>The <strong>superseder</strong> property is used to
+support the splitting, joining, or replacing of items.
+When several items need to be
+joined into a single item, all the old items
+link to the new item in their <strong>superseder</strong>
+property.
+When an item needs to be split apart, the item
+references all the new items in its <strong>superseder</strong>
+propety.
+We can easily list all active items just by checking
+for an empty <strong>superseder</strong> property, and trace
+the path of an item's origins by querying the hyperdatabase
+for links.
+
+<p>The <strong>nosy</strong> property contains a list of
+the people who are interested in an item. This
+mechanism is explained in
+<a href="#discuss">the section on Nosy Lists</a>.
+
+<p>The <strong>creation</strong> property records the
+item's creation time. The <strong>activity</strong>
+property records the last time that the item was edited or
+a mail message was added to its discussion spool. These two
+properties are managed by Roundup and are not available to
+be edited like other properties.
+
+<p>Users of the system are also represented by nodes in the
+hyperdatabase, containing properties
+like the user's e-mail address, login name, and password.
+
+<h3><a name="schema">The Default Schema</a></h3>
+
+<p><table align=right width="40%" bgcolor="#808080"
+cellpadding=0 cellspacing=0 border=0><tr><td
+><table bgcolor="#e8e8e8" width="100%"
+cellspacing=0 cellpadding=5 border=0><tr><td
+><font color="#808080"><small>
+Roundup could be distributed with a few
+suggested schemas for different purposes.
+One possible enhancement to the
+software-development schema is
+a <em>reference</em> property
+named <strong>implements</strong> for connecting
+development items to design requirements which
+they satisfy, which should
+be enough to provide basic support for
+<a href="http://software-carpentry.codesourcery.com/lists/sc-discuss/msg00046.html">traceability</a>.
+Clearly there is also potential for adding
+properties for related source files, check-ins,
+test results, regression tests for resolved items,
+and so on, though these have not yet been
+sufficiently well thought out to specify here.
+</small></font></td></tr></table></td></tr></table>
+<p>It is hoped that the hyperdatabase together with the
+specializations mentioned above for Roundup will be
+applicable in a variety of situations
+(in accordance with our guiding principle of <em>generality</em>).
+
+<p>To address the problem at hand, we need
+a specific schema for items applied particularly to software development.
+Again, we are trying to keep the schema simple: too many
+options make it tougher for someone to make a good choice.
+The schema is written here in the same form that it would
+appear in a configuration file.
+<br clear=all>
+
+<pre>
+ fixer = Reference() # people who will fix the problem
+
+ topic = Reference() # relevant topic keywords
+
+ priority = Choice("critical", # panic: work is stopped!
+ "urgent", # important, but not deadly
+ "bug", # lost work or incorrect results
+ "feature", # want missing functionality
+ "wish") # avoidable bugs, missing conveniences
+
+ status = Choice("unread", # submitted but no action yet
+ "deferred", # intentionally set aside
+ "chatting", # under review or seeking clarification
+ "need-eg", # need a reproducible example of a bug
+ "in-progress", # understood; development in progress
+ "testing", # we think it's done; others, please test
+ "done-cbb", # okay for now, but could be better
+ "resolved") # fix has been released
+</pre>
+
+<p>The <strong>fixer</strong> property assigns
+responsibility for an item to a person or a list of people.
+The <strong>topic</strong> property places the
+item in an arbitrary number of relevant topic sets (see
+<a href="#browse">the section on Browsing and Searching</a>).
+
+<p>As previously mentioned, each item gets an activity log.
+Whenever a property on an item is changed, the log
+records the time of the change, the user making the change,
+and the old and new values of the property. This permits
+the later gathering of statistics (for example, the average time
+from submission to resolution).
+
+<p>We do not specify or enforce a state transition graph,
+since making the system rigid in that fashion is probably more
+trouble than it's worth.
+Experience has shown that there are probably
+two convenient automatic state transitions:
+
+<ul>
+<li>from <strong>unread</strong> to <strong>chatting</strong>
+when e-mail is written about an item
+<li>from <strong>testing</strong> to <strong>resolved</strong>
+when a new release of the software is made
+</ul>
+
+Beyond these, in accordance with our principle of <em>generality</em>,
+we allow access to the hyperdatabase
+API so that scripts can automate transitions themselves or
+be triggered by changes in the database.
+
+<p><hr>
+<h2><a name="ui">User Interface</a></h2>
+
+<p>Roundup provides its services through two main interfaces:
+e-mail and the Web.
+This division is chosen to optimize the most common tasks.
+
+<p>E-mail is best suited for
+the submission of new items since most people are most comfortable
+with composing long messages in their own favourite e-mail client.
+E-mail also permits them to mention URLs or attach files relevant
+to their submission. Indeed, in many cases people are already
+used to making requests by sending e-mail to a mailing list
+of people; they can do exactly the same thing to use Roundup
+without even thinking about it.
+Similarly, people are already
+familiar with holding discussions in e-mail, and plenty of
+valuable usage conventions and software tools already exist for that medium.
+
+<p>The Web, on the other hand, is best suited for summarizing
+and seeking information, because it can present an interactive
+overview of items. Since the Web has forms, it's also
+the best place to edit items.
+
+<h3><a name="discuss">Submission and Discussion</a></h3>
+
+<p><table align=right width="40%" bgcolor="#808080" cellpadding=0 border=0
+><tr><td><table bgcolor="#e8e8e8" width="100%" cellspacing=0 cellpadding=5
+border=0><tr><td><font color="#808080"><small>
+Nosy lists have actually been tried in practice,
+and their emergent properties have
+turned out to be very effective.
+They are one of the key strengths of the Roundup prototype,
+and often cause me to wonder if all mailing lists ought to work this way.
+Roundup could even replace Hypermail.
+</small></font></td></tr></table></td></tr></table>
+
+<p>The system needs an address for receiving mail
+and an address that forwards mail to all participants.
+Each item has its own list
+of interested parties, known as its <em>nosy list</em>.
+Here's how nosy lists work:
+
+<p><ol type="a">
+<li>New items are always submitted by sending an e-mail message
+to Roundup. The "Subject:" field becomes the description
+of the new item.
+The message is saved in the mail spool of the new item,
+and copied to the list of all participants
+so everyone knows that a new item has been added.
+The new item's nosy list initially contains the submitter.
+
+<li>All e-mail messages sent by Roundup have their "Reply-To:"
+field set to Roundup's address, and have the item's
+number in the "Subject:" field. Thus, any replies to the
+initial announcement and subsequent threads are all received
+by Roundup. Roundup notes the item number in the "Subject:"
+field of each incoming message and appends the message
+to the appropriate spool.
+
+<li>Any incoming e-mail tagged with an item number is copied
+to all the people on the item's nosy list,
+and any users found in the "From:", "To:", or "Cc:" fields
+are automatically added to the nosy list. Whenever a user
+edits an item's properties in the Web interface, they are
+also added to the nosy list.
+</ol>
+
+<p>The effect
+is like each item having its own little mailing list,
+except that no one ever has to worry about subscribing to
+anything. Indicating interest in an issue is sufficient, and if you
+want to bring someone new into the conversation, all you need to do
+is Cc: a message to them. It turns out that no one ever has to worry
+about unsubscribing, either: the nosy lists are so specific in scope
+that the conversation tends to die down by itself when the issue is
+resolved or people no longer find it sufficiently important.
+
+<p>Each nosy list is like an asynchronous chat room,
+lasting only a short time (typically five or ten messages)
+and involving a small group of people.
+However, that
+group is the <em>right</em> group of people:
+only those who express interest in an item in some way
+ever end up on
+the list, so no one gets spammed with mail they
+don't care about, and no one who <em>wants</em>
+to see mail about a particular item needs to be left
+out, for they can easily join in, and just as easily
+look at the mail spool on an item to catch up on any
+messages they might have missed.
+
+<p>We can take this a step further and
+permit users to monitor particular topics or
+classifications of items
+by allowing other kinds of nodes to
+also have their own nosy lists.
+For example, a manager could be on the
+nosy list of the priority value node for "critical", or a
+developer could be on the nosy list of the
+topic value node for "security".
+The recipients are then
+determined by the union of the nosy lists on the
+item and all the nodes it links to.
+
+<p>Using many small, specific mailing lists results
+in much more effective communication than one big list.
+Taking away the effort of subscribing and unsubscribing
+gives these lists the "feel" of being cheap and
+disposable.
+
+The transparent capture of the mail spool attached to each
+issue also yields a nice knowledge repository over time.
+
+
+<h3><a name="edit">Editing</a></h3>
+
+<p>
+<img src="images/edit.png" align=right width=171 height=471 alt="">
+Since Roundup is intended to support arbitrary user-defined
+schema for item properties, the editing interface must be
+automatically generated from the schema. The configuration for
+Roundup will include a template describing how to lay out the
+properties to present a UI for inspecting and editing items.
+For example:
+
+<pre>
+ <table width="100%">
+ <tr><td align=right>Description:</td>
+ <td><?property description size=70></td></tr>
+ <tr><td align=right>Status:</td>
+ <td><?property status></td></tr>
+ </table>
+</pre>
+
+<p>To display the editing form for an item, Roundup substitutes
+an HTML form widget for each <tt><?property </tt>...<tt>></tt>
+tag, and transfers attributes
+(such as <tt>size=70</tt> in the above example)
+from the processing tag to the form widget's tag.
+Each type has its own appropriate editing widget:
+<ul>
+<li><em>string</em> properties appear as text fields
+<li><em>date</em> properties appear as text fields
+<li><em>choice</em> properties appear as selection lists
+<li><em>reference</em> properties appear as multiple-selection lists
+with a text field for adding a new option
+</ul>
+
+<p>We foresee the use of custom date fields for things like deadlines,
+so input fields for <em>date</em> properties should support some
+simple way of specifying relative dates (such as "three weeks from now").
+
+<p>The <strong>superseder</strong> property is a special case:
+although it is more efficient to store a <strong>superseder</strong>
+property in the superseded item, it makes more sense to provide
+a "supersedes" edit field on the superseding item. So we need
+a special widget on items for this purpose (perhaps something
+as simple as a text field containing a comma-separated list of
+item numbers will do). Links in the <strong>superseder</strong> property
+should appear on both the superseding and superseded items to
+facilitate navigating an item's pedigree.
+
+<p>After the editing widgets, the item inspection page shows
+a "note" text box and then a display of the messages in the
+discussion spool, like the Roundup prototype. This field
+lets you enter a note explaining your change when you edit the
+item, and the note is included in the notification message that
+goes out to tell the interested parties on the nosy list of
+your edits.
+
+<h3><a name="browse">Browsing and Searching</a></h3>
+
+<p>The ideal we would like to achieve is to make searching as
+much like browsing as possible: the user simply clicks about
+on things that seem interesting, and the information narrows
+down comfortably until the goal is in sight. This is preferable
+to trying to digest a screen filled with widgets and buttons
+or entering a search expression in some arcane algebraic syntax.
+
+<p><table align=right width="40%" bgcolor="#808080" cellpadding=0 border=0
+><tr><td><table bgcolor="#e8e8e8" width="100%" cellspacing=0 cellpadding=5
+border=0><tr><td><font color="#808080"><small>
+Though the generation of each page amounts to a database query,
+so that the underlying mechanism is still a series of queries and
+responses, the user interface never separates the query from
+the response, so the <em>experience</em> is one of stepwise
+refinement.
+</small></font></td></tr></table></td></tr></table>
+While a one-shot search may be appropriate when you're
+looking for a single item and you know exactly what you want, it's
+not very helpful when you want an overview of
+things ("Gee, there are a lot more high-priority items than
+there were last week!") or trying to do comparisons ("I have
+some time today, so who is busiest and could most use some help?")
+<br clear=all>
+
+<p>The browsing interface presents filtering
+functionality for each of the properties in the schema. As with
+editing, the interface is generated from a template
+describing how to lay out the properties.
+Each type of property has its own appropriate filtering widget:
+<ul>
+<li><em>string</em> properties appear as text fields supporting
+case-insensitive substring match
+<li><em>date</em> properties appear as a text field with an
+option to choose dates after or before the specified date
+<li><em>choice</em> properties appear as a group of
+selectable options
+(the filter selects the <em>union</em> of the sets of items
+associated with the active options)
+<li><em>reference</em> properties appear as a group of
+selectable options
+(the filter selects the <em>intersection</em> of the sets of items
+associated with the active options)
+</ul>
+
+<p>For a <em>reference</em> property like <strong>topic</strong>,
+one possibility is to show, as hyperlinks, the keywords whose
+sets have non-empty intersections with the currently displayed set of
+items. Sorting the keywords by popularity seems
+reasonable. Clicking on a keyword then narrows both the list of items
+and the list of keywords. This gives some of the feel of walking
+around a directory tree -- but without the restriction of having
+to select keywords in a particular hierarchical order, and without
+the need to travel all the way to the leaves of the tree before
+any items are visible.
+
+<p>Below the filtering form is a listing of items, with their
+properties displayed in a table. Rows in the table can also be
+generated from a template, as with the editing interface.
+This listing is the central overview of the system, and it
+should aim to maximize the density of
+useful information in accordance with our guiding principle of
+<em>efficiency</em>.
+For example,
+<a href="http://www.lfw.org/ping/bugzilla-4.gif">Bugzilla
+initially displays seven or eight items of the index</a>, but only
+after the user has
+<a href="http://www.lfw.org/ping/bugzilla-1.gif">waded</a>
+through
+<a href="http://www.lfw.org/ping/bugzilla-2.gif">three</a>
+bewildering
+<a href="http://www.lfw.org/ping/bugzilla-3.gif">screens</a> of
+form widgets.
+<a href="http://www.lfw.org/ping/jitterbug-1.gif">Jitterbug can't
+even fit any items at all in the first screenful</a>, as it's
+taken up by artwork and adminstrative debris. In contrast,
+<a href="http://www.lfw.org/ping/roundup-1.gif">in the
+Roundup prototype,
+25 high-priority issues are immediately visible</a>, with
+most of the screen space devoted to their descriptions.
+Colour indicates
+the status of each item to help the eye sift through the index quickly.
+
+<p>In both Jitterbug and Bugzilla, items are sorted by default by ID,
+a meaningless field. Sorting by ID puts the issues in order by
+ascending submission date, which banishes recent issues far away
+at the bottom of the list.
+The Roundup prototype sorts items
+in sections by priority, and then within sections by the date
+of last activity. This reveals at a glance where discussion is
+most active, and provides an easy way for anyone to move an issue
+up in the list.
+
+<p>The page produced by a given set of browsing options constitutes
+a <em>view</em>. The options should all be part of the query
+parameters in the URL so that views may be bookmarked. A view
+specifies:
+
+<ul>
+<li>search strings for string properties
+<li>date ranges for date properties
+<li>acceptable values for choice properties
+<li>required values for reference properties
+<li>one or more sort keys
+<li>a list of properties for which to display filtering widgets
+</ul>
+
+<p>On each sort key there is the option to use sections -- that is,
+instead of making the property's value a column of the table, each
+possible value for the property
+is displayed at the top of a section and all the items having
+that value for that property are grouped underneath. This avoids
+wasting screen space with redundant information.
+
+<p>We propose that our default view should be:
+
+<ul>
+<li>all options on for <strong>priority</strong> and <strong>fixer</strong>
+<li>all options on except "resolved" for <strong>status</strong>
+<li>no options on for <strong>topic</strong>
+<li>primary sort by <strong>priority</strong> in sections
+<li>secondary sort by decreasing <strong>activity</strong> date
+</ul>
+
+<p>The starting URL for Roundup should immediately present the listing of
+items generated by this default view, with no
+preceding query screen.
+
+<p><hr>
+<h2><a name="devplan">Development Plan</a></h2>
+
+<p>The hyperdatabase is clearly a separable component which
+can be developed and tested independently to an API specification.
+
+<p>As soon as the API to the hyperdatabase is nailed down,
+the implementation of the Roundup database layer
+on top of the hyperdatabase can begin.
+(This refers to the data types and five fixed properties
+specific to Roundup.) This layer can also be tested separately.
+
+<p>When the interface to the Roundup hyperdatabase is ready,
+development can begin on the user interface. The mail handler
+and the Web interface can be developed in parallel and mostly
+independently of each other.
+
+<p>The mail handler can be set up for testing fairly easily:
+mail messages on its standard input can be synthesized;
+its output is outgoing mail, which can be
+captured by replacing the implementation of the
+"send mail" function; and its side effects appear in the
+hyperdatabase, which has a Python API.
+
+<p>The Web interface is not easily testable in its entirety,
+though the most important components of it can be unit tested,
+such as the component that translates a view specification
+into a list of items for display, and
+the component that performs replacements on templates
+to produce an editing or filtering interface.
+
+<p><hr>
+<h2><a name="issues">Open Issues</a></h2>
+
+<p>The description of the hyperdatabase above avoids some
+issues regarding node typing that need to be better specified.
+It is conceivable that eventually Roundup
+could support multiple kinds of items with their own schemas.
+
+<p>To permit integration with external tools, it is probably
+a good idea to provide a command-line tool that exposes the
+hyperdatabase API. This tool will be left for a later phase
+of development and so isn't specified in detail here.
+
+<p>Generating the user interface from a template is like
+applying an XSL stylesheet to XML, and if there's a standard
+Python module for performing these transformations, we could
+use XML instead.
+
+<p>More thinking is needed to determine the best filtering
+interface for <em>reference</em> properties.
+The proposed interface works well for topic keywords, but
+it isn't clear what to do when there are too many keywords
+to display them all.
+
+<p>There has been a variety of reactions to the hyperdatabase
+from reviewers: some like it, some are neutral, and some
+would prefer a "standard" RDBMS solution.
+For those in the latter camp, note
+that it's still possible to build the Roundup database layer
+around an RDBMS if we really need to. The rest of the design, in
+particular the "nosy list" mechanism, remains intact.
+
+<p>The possibility of malice by registered users has been disregarded.
+The system is intended to be used by a co-operative group.
+
+<p>This design tries to address as many as possible of the
+suggested requirements mentioned on
+<a href="http://software-carpentry.codesourcery.com/sc_track">the contest page</a>:
+
+<ul>
+<li>configuring states: Edit the schema.
+<li>setting state transition rules: We don't enforce any rules.
+<li>assigning responsibility: Set the <strong>fixer</strong> property.
+<li>splitting and joining: Use the <strong>superseder</strong> property.
+<li>hiding information: Add
+a property and a pre-defined view that filters on it.
+<li>secure protocols: Naturally HTTPS would be nice, though it's largely
+a webserver configuration issue; secure e-mail is not addressed.
+<li>archiving old issues: Tag them with a property.
+<li>identifying repeated issues: Use the <strong>superseder</strong> property.
+<li>connecting state changes to external operations: We provide an
+API to the database and the notification mechanism so it can be scripted.
+<li>non-Latin alphabets: Unicode in Python 1.6 will handle
+this for string properties, and we can leverage existing standards for
+internationalizing e-mail messages.
+<li>images and other binaries: Attach them to e-mail messages.
+<li>inspecting item state: Use the editing interface.
+<li>translation between system-dependent formats: This is not addressed.
+<li>performing searches: Use the browsing and filtering interface.
+<li>collecting statistics: Information is gathered in the activity log,
+though tools to summarize it are not described here.
+</ul>
+
+<p><hr>
+<h2><a name="summary">Summary</a></h2>
+
+<p>Roundup is an issue-tracking system that also functions as
+a communications center and a knowledge repository. It combines
+the strengths of e-mail and the Web to try to provide the best
+possible user interaction.
+
+<ul>
+<li>The submission and discussion of items by e-mail, permitting
+participants to use an easy and familiar tool, achieves our goal
+of <em>low barrier to participation</em>.
+<li>The generic link-based structuring of data and use of
+incremental filtering rather than one-shot querying makes for
+<em>straightforward navigation</em>.
+<li>The use of <em>nosy lists</em> (a powerful replacement for
+e-mail discussion lists) to manage communication on
+a fine-grained level provides <em>controlled information flow</em>.
+</ul>
+
+<p>The use of a "hyperdatabase" as the core model for
+the knowledge repository gives us the flexibility to extend
+Roundup and apply it to a variety of domains by
+providing new item schemas and user-interface templates.
+
+<p>Roundup is self-contained and easy to set up, requiring
+only a webserver and a mailbox. No one needs to be root to
+configure the webserver or to install database software.
+
+<p>This design is based on an existing deployed
+prototype which has proven its strengths and revealed its
+weaknesses in heavy day-to-day use by a real development team.
+
+<p><hr>
+<h2><a name="ack">Acknowledgements</a></h2>
+
+<p>My thanks are due to
+Christina Heyl, Jesse Vincent, Mark Miller, Christopher Simons,
+Jeff Dunmall, Wayne Gramlich, and Dean Tribble
+for reviewing this paper and contributing their suggestions.
+
+<p><hr><p>
+
+<center>
+<table>
+<tr>
+<td> <a href="http://www.software-carpentry.com/index.html"><b>[Home]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/faq.html"><b>[FAQ]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/license.html"><b>[License]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/contest-rules.html"><b>[Rules]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/biblio.html"><b>[Resources]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/lists/"><b>[Archives]</b></a> </td>
+</tr>
+</table>
+</center>
+
+<p><hr>
+<center>
+Last modified 2001/04/06 11:50:59.9063 US/Mountain
+</center>
+</body></html>
Added: tracker/vendor/roundup/current/doc/overview.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/overview.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,603 @@
+=======================================================
+Roundup: an Issue-Tracking System for Knowledge Workers
+=======================================================
+
+:Authors: Ka-Ping Yee (original_), Richard Jones (implementation)
+
+.. _original: original_overview.html
+
+.. contents::
+
+
+Introduction
+============
+
+Roundup is an issue-tracking system called which will manage a
+number of issues (with properties such as "description", "priority",
+and so on) and provides the ability to:
+
+(a) submit new issues,
+(b) find and edit existing issues, and
+(c) discuss issues with other participants.
+
+Roundup facilitates communication among the participants by managing
+discussions and notifying interested parties when issues are edited.
+
+
+Background
+----------
+
+A typical software project requires the management of
+many tasks, usually distributed among several collaborators.
+In fact, any project team
+could use a tool for sorting out and discussing all the
+relevant issues. A common approach is to set up some kind
+of "to-do" list that people can share.
+
+However, to address the overall problem we need much more
+than just a shared to-do list; we need to
+manage a growing body of knowledge and experience to help a
+team collaborate effectively on a project. The issue-tracking
+tool becomes a nexus for communication: the Grand Central
+Station of the group intelligence.
+
+The primary focus of this design is to help
+developers work together well, not to provide a customer
+service interface to the developers. This is not to say that
+the design is to be made unsuitable for customers to use.
+Rather, it is assumed that many of the same qualities
+that are good for supporting development (see below)
+are also good for non-developers using the system.
+Additional niceties
+for providing a safe or simplified interface to clients are
+intentionally deferred for later consideration.
+
+A good issue-tracking system should have at least the
+following properties:
+
+**Low barrier to participation**
+ The usefulness of the tool depends entirely on the
+ information people contribute to it. It must be made
+ as easy as possible to submit new issues and contribute
+ information about existing issues.
+
+**Straightforward navigation**
+ It should be easy for users to extract information they need
+ from the system to direct their decisions and tasks.
+ They should be able to get a decent overview of
+ things as well as finding specific information when
+ they know what they're after.
+
+**Controlled information flow**
+ The users must have control over how much information and
+ what information they get. A common flaw of some issue-tracking
+ systems is that they inundate users with so much useless
+ e-mail that people avoid the system altogether.
+
+With a nod to the time-honoured computer science tradition
+of "filling in the fourth quadrant", we note that
+there are really four kinds of information flow
+going on here. The three mentioned qualities
+really address the first three quadrants of this 2-by-2 matrix,
+respectively:
+
+1. User push: a user submits information to the system.
+2. User pull: a user queries for information from the system.
+3. System push: the system sends information out to users.
+4. System pull: the system solicits information from users.
+
+An example of the fourth kind of flow is voting.
+Voting isn't described in this design,
+but it should be noted as a potential enhancement.
+
+
+Guiding Principles
+------------------
+
+**Simplicity**
+ It is a strong requirement
+ that the tool be accessible and understandable. It should
+ be fairly obvious what different parts of the interface do,
+ and the inner mechanisms should operate in ways that most
+ users can easily predict.
+
+**Efficiency**
+ We aim to optimize for minimum effort to do the most common
+ operations, and best use of resources like screen real estate
+ to maximize the amount of information that we summarize and present.
+
+**Generality**
+ We try to avoid making
+ unnecessary assumptions that would restrict the applicability
+ of the tool. For example, there is no reason why one might
+ not also want to use this tool to manage a design process,
+ non-software projects, or organizational decisions.
+
+**Persistence** We
+ prefer hiding or reclassifying information to deleting it.
+ This helps support the collection of statistics later.
+ If records are never destroyed, there is little danger
+ in providing access to a larger community, and logging yields
+ accountability, which may encourage better behaviour.
+
+
+Data Model
+==========
+
+Roundup stores a number of *items*, each of
+which can have several properties and an associated discussion.
+The properties can be used to classify or search for items.
+The discussion is a sequence of e-mail messages.
+Each item is identified by a unique number, and has
+an activity log which
+records the time and content of edits made on its properties.
+The log stays fairly small since the design intentionally
+provides only small data types as item properties, and
+encourages anything large to be attached to
+e-mail where it becomes part of the discussion.
+The next section explains how items are organized.
+
+
+The Hyperdatabase
+-----------------
+
+Often when classifying information we are
+asked to select exactly one of a number of categories
+or to fit it into a rigid hierarchy. Yet things
+only sometimes fall into one category; often,
+a piece of information may be related to several concepts.
+
+For example, forcing each item into a single topic
+category is not just suboptimal but counterproductive:
+seekers of that
+item may expect to find it in a different category
+and conclude that the item is not present in the
+database -- which has them *worse* off
+than if the items were not categorized at all.
+
+Some systems try to alleviate this problem by
+allowing items to appear at multiple locations
+in a tree, as with "aliases" or "symbolic links" in
+a filesystem, for example. This does help somewhat,
+but we want to be even more flexible
+by allowing the
+organization of items into sets that may freely
+intersect. Rather than putting each item at exactly
+one place in an overall "grand scheme", a item can
+belong to as many sets as are appropriate.
+
+If we choose to represent the sets themselves as items
+and set membership as a link between items,
+we're now ready to present the definition of a
+hyperdatabase.
+
+A *hyperdatabase* is a collection of *items*
+that may be hyperlinked to
+each other (hence the name "hyperdatabase").
+Each item carries a collection of key-value pairs,
+where some of the values may be links to other items.
+Any item may have an arbitrary number of outgoing and
+incoming links. Hyperdatabases are able to efficiently
+answer queries such as "what items link to this item?"
+and "what items does this item link to?"
+
+Rationale
+---------
+
+There are several reasons for building our
+own kind of database for Roundup rather than using an existing one.
+
+Requiring the installation of a full-blown third-party
+SQL database system would probably deter many potential
+users from attempting to set up Roundup;
+yet a real relational database would be too
+complicated to implement on our own.
+
+On the other hand, a hyperdatabase can be implemented fairly easily
+using one of the Python DBM modules, so we can
+take the "batteries-included" approach and provide it
+as part of the system. It's easier to build and understand
+than a true relational database (in accordance with our guiding
+principle of *simplicity*), but provides
+most of the query functionality we want.
+
+A hyperdatabase is well suited for finding the intersection
+of a number of sets in which items belong. We expect that
+most of the queries people want to do will be of this
+form, rather than complicated SQL queries. For example, a
+typical request might be
+"show me all critical items related to security".
+The ability to store arbitrary key-value pairs and links
+on items gives it more flexibility than an RDBMS.
+
+Users are not going to be making thousands of queries
+per second, so it makes sense to optimize for simplicity
+and flexibility rather than performance.
+
+.. img: images/hyperdb.png
+
+
+Roundup's Hyperdatabase
+-----------------------
+
+For our application, we store each item as a item in a
+hyperdatabase. The item's properties are stored
+as key-value pairs on its item.
+Several types of properties are allowed:
+*string*, *number*, *boolean*, *date*, *interval, *link*,
+and *multlink*. Another type, *password*, is a special type
+of string and it's only used internally to Roundup.
+
+The *string* type is for short, free-form strings.
+String properties are not intended to contain large
+amounts of text, and it is recommended that they be presented
+as one-line fields to encourage brevity. A *number* is a special
+type of string that represents a numeric value. A *boolean* is
+further constrained to be a *true* or *false* value.
+
+The *date* type is for calendar dates and times. An *interval*
+is the time between two dates.
+
+The *link* type denotes a single selection from a number of
+options. A *link* property entails a link from the item
+possessing the property to the item representing the chosen option.
+
+The *multilink* type is for a list of links to any
+number of other items in the in the database. A *multilink*
+property, for example, can be used to refer to related items
+or topic categories relevant to an item.
+
+For Roundup, all items have four properties that are not customizable:
+
+1. a *date* property named **creation**
+2. a *link* property named **creator**
+3. a *date* property named **activity**
+
+These properties represent the date of the creation of the item, who
+created it, and when the last change was made.
+
+Further, all *issue* items have an additional four properties:
+
+1. a *string* property named **title**
+2. a *multilink* property named **nosy**
+3. a *multilink* property named **messages**
+4. a *multilink* property named **files**
+5. a *multilink* property named **superseder**
+
+The **title** property is a short one-line description of the item.
+The detailed description can go in the first e-mail message of the
+item's messages spool.
+
+The **nosy** property contains a list of
+the people who are interested in an item. This
+mechanism is explained in the section on `Submission and Discussion`_.
+
+Each message added to the item goes in the **messages** spool - any
+attached files go in the **files** spool.
+
+The **superseder** property is used to
+support the splitting, joining, or replacing of items.
+When several items need to be
+joined into a single item, all the old items
+link to the new item in their **superseder**
+property.
+When an item needs to be split apart, the item
+references all the new items in its **superseder**
+propety.
+We can easily list all active items just by checking
+for an empty **superseder** property, and trace
+the path of an item's origins by querying the hyperdatabase
+for links.
+
+Users of the system are also represented by items in the
+hyperdatabase, containing properties
+like the user's e-mail address, login name, and password.
+
+The Default Schema
+------------------
+
+It is hoped that the hyperdatabase together with the
+specializations mentioned above for Roundup will be
+applicable in a variety of situations
+(in accordance with our guiding principle of *generality*).
+
+To address the problem at hand, we need
+a specific schema for items applied particularly to software development.
+Again, we are trying to keep the schema simple: too many
+options make it tougher for someone to make a good choice::
+
+ # IssueClass automatically gets these properties:
+ # title = String()
+ # messages = Multilink("msg")
+ # files = Multilink("file")
+ # nosy = Multilink("user")
+ # superseder = Multilink("issue")
+ # (it also gets the Class properties creation, activity and creator)
+ issue = IssueClass(db, "issue",
+ assignedto=Link("user"), topic=Multilink("keyword"),
+ priority=Link("priority"), status=Link("status"))
+
+The **assignedto** property assigns
+responsibility for an item to a person or a list of people.
+The **topic** property places the
+item in an arbitrary number of relevant topic sets (see
+the section on `Browsing and Searching`_).
+
+The **prority** and **status** values are initially:
+
+=========== =====================================
+Priority Description
+=========== =====================================
+"critical" panic: work is stopped!
+"urgent" important, but not deadly
+"bug" lost work or incorrect results
+"feature" want missing functionality
+"wish" avoidable bugs, missing conveniences
+=========== =====================================
+
+============= =====================================
+Status Description
+============= =====================================
+"unread" submitted but no action yet
+"deferred" intentionally set aside
+"chatting" under review or seeking clarification
+"need-eg" need a reproducible example of a bug
+"in-progress" understood; development in progress
+"testing" we think it's done; others, please test
+"done-cbb" okay for now, but could be better
+"resolved" fix has been released
+============= =====================================
+
+As previously mentioned, each item gets an activity log.
+Whenever a property on an item is changed, the log
+records the time of the change, the user making the change,
+and the old and new values of the property. This permits
+the later gathering of statistics (for example, the average time
+from submission to resolution).
+
+We do not specify or enforce a state transition graph,
+since making the system rigid in that fashion is probably more
+trouble than it's worth.
+Experience has shown that there are probably
+two convenient automatic state transitions:
+
+1. from **unread** to **chatting** when e-mail is written about an item
+2. from **testing** to **resolved** when a new release of the software is made
+
+Beyond these, in accordance with our principle of *generality*,
+we allow access to the hyperdatabase
+API so that scripts can automate transitions themselves or
+be triggered by changes in the database.
+
+
+User Interface
+==============
+
+Roundup provides its services through two main interfaces:
+e-mail and the Web.
+This division is chosen to optimize the most common tasks.
+
+E-mail is best suited for
+the submission of new items since most people are most comfortable
+with composing long messages in their own favourite e-mail client.
+E-mail also permits them to mention URLs or attach files relevant
+to their submission. Indeed, in many cases people are already
+used to making requests by sending e-mail to a mailing list
+of people; they can do exactly the same thing to use Roundup
+without even thinking about it.
+Similarly, people are already
+familiar with holding discussions in e-mail, and plenty of
+valuable usage conventions and software tools already exist for that medium.
+
+The Web, on the other hand, is best suited for summarizing
+and seeking information, because it can present an interactive
+overview of items. Since the Web has forms, it's also
+the best place to edit items.
+
+
+Submission and Discussion
+-------------------------
+
+The system needs an address for receiving mail
+and an address that forwards mail to all participants.
+Each item has its own list
+of interested parties, known as its *nosy list*.
+Here's how nosy lists work:
+
+1. New items are always submitted by sending an e-mail message
+ to Roundup. The "Subject:" field becomes the description
+ of the new item.
+ The message is saved in the mail spool of the new item,
+ and copied to the list of all participants
+ so everyone knows that a new item has been added.
+ The new item's nosy list initially contains the submitter.
+
+2. All e-mail messages sent by Roundup have their "Reply-To:"
+ field set to Roundup's address, and have the item's
+ number in the "Subject:" field. Thus, any replies to the
+ initial announcement and subsequent threads are all received
+ by Roundup. Roundup notes the item number in the "Subject:"
+ field of each incoming message and appends the message
+ to the appropriate spool.
+
+3. Any incoming e-mail tagged with an item number is copied
+ to all the people on the item's nosy list,
+ and any users found in the "From:", "To:", or "Cc:" fields
+ are automatically added to the nosy list. Whenever a user
+ edits an item's properties in the Web interface, they are
+ also added to the nosy list.
+
+The effect is like each item having its own little mailing list,
+except that no one ever has to worry about subscribing to
+anything. Indicating interest in an issue is sufficient, and if you
+want to bring someone new into the conversation, all you need to do
+is "Cc:" a message to them. It turns out that no one ever has to worry
+about unsubscribing, either: the nosy lists are so specific in scope
+that the conversation tends to die down by itself when the issue is
+resolved or people no longer find it sufficiently important.
+
+Each nosy list is like an asynchronous chat room,
+lasting only a short time (typically five or ten messages)
+and involving a small group of people. However, that
+group is the *right* group of people:
+only those who express interest in an item in some way
+ever end up on the list, so no one gets spammed with mail they
+don't care about, and no one who *wants*
+to see mail about a particular item needs to be left
+out, for they can easily join in, and just as easily
+look at the mail spool on an item to catch up on any
+messages they might have missed.
+
+We can take this a step further and
+permit users to monitor particular topics or classifications of items
+by allowing other kinds of items to also have their own nosy lists.
+For example, a manager could be on the
+nosy list of the priority value item for "critical", or a
+developer could be on the nosy list of the topic value item for "security".
+The recipients are then determined by the union of the nosy lists on the
+item and all the items it links to.
+
+Using many small, specific mailing lists results
+in much more effective communication than one big list.
+Taking away the effort of subscribing and unsubscribing
+gives these lists the "feel" of being cheap and
+disposable.
+
+The transparent capture of the mail spool attached to each
+issue also yields a nice knowledge repository over time.
+
+
+Editing
+-------
+
+Since Roundup is intended to support arbitrary user-defined
+schema for item properties, the editing interface must be
+automatically generated from the schema. The configuration for
+Roundup will include a template describing how to lay out the
+properties to present a UI for inspecting and editing items.
+For example::
+
+ <tr>
+ <th class="required">Priority</th>
+ <td tal:content="structure context/priority/menu">priority</td>
+ <th>Status</th>
+ <td tal:content="structure context/status/menu">status</td>
+ </tr>
+
+To display the editing form for an item, Roundup inserts
+an HTML form widget where it encounters an expression like
+``tal:content="structure context/priority/menu"``.
+Each type has its own appropriate editing widget:
+
+- *string* and *number* properties appear as text fields
+- *boolean* properties appear as a yes/no selection
+- *date* and *interval* properties appear as text fields
+- *link* properties appear as selection lists
+- *multilink* properties appear as multiple-selection lists
+ or text fields with pop-up widgets for larger selections.
+
+We foresee the use of custom date fields for things like deadlines,
+so input fields for *date* properties support a
+simple way of specifying relative dates (such as "3w" for
+"three weeks from now").
+
+The **superseder** property is a special case:
+although it is more efficient to store a **superseder**
+property in the superseded item, it makes more sense to provide
+a "supersedes" edit field on the superseding item. We use
+a special widget on items for this purpose (a text field containing
+a comma-separated list of items). Links in the **superseder** property
+appear on both the superseding and superseded items to
+facilitate navigating an item's pedigree.
+
+After the editing widgets, the item inspection page shows
+a "note" text box and then a display of the messages in the
+discussion spool. This field
+lets you enter a note explaining your change when you edit the
+item, and the note is included in the notification message that
+goes out to tell the interested parties on the nosy list of
+your edits.
+
+Browsing and Searching
+----------------------
+
+The ideal we would like to achieve is to make searching as
+much like browsing as possible: the user simply clicks about
+on things that seem interesting, and the information narrows
+down comfortably until the goal is in sight. This is preferable
+to trying to digest a screen filled with widgets and buttons
+or entering a search expression in some arcane algebraic syntax.
+
+While a one-shot search may be appropriate when you're
+looking for a single item and you know exactly what you want, it's
+not very helpful when you want an overview of
+things ("Gee, there are a lot more high-priority items than
+there were last week!") or trying to do comparisons ("I have
+some time today, so who is busiest and could most use some help?")
+
+The browsing interface presents filtering
+functionality for each of the properties in the schema. As with
+editing, the interface is generated from a template
+describing how to lay out the properties.
+Each type of property has its own appropriate filtering widget:
+
+- *string* properties appear as text fields supporting
+ case-insensitive substring match
+- *date* properties appear as a text field which accepts a date
+ range with start, end or both
+- *link* properties appear as a group of selectable options
+ (the filter selects the *union* of the sets of items
+ associated with the active options)
+- *multilink* properties appear as a group of selectable options
+ (the filter selects the *intersection* of the sets of items
+ associated with the active options)
+
+For a *multilink* property like **topic**,
+one possibility is to show, as hyperlinks, the keywords whose
+sets have non-empty intersections with the currently displayed set of
+items. Sorting the keywords by popularity seems
+reasonable. Clicking on a keyword then narrows both the list of items
+and the list of keywords. This gives some of the feel of walking
+around a directory tree -- but without the restriction of having
+to select keywords in a particular hierarchical order, and without
+the need to travel all the way to the leaves of the tree before
+any items are visible.
+
+Below the filtering form is a listing of items, with their
+properties displayed in a table. Rows in the table are
+generated from a template, as with the editing interface.
+This listing is the central overview of the system, and it
+should aim to maximize the density of
+useful information in accordance with our guiding principle of
+*efficiency*. Colour may be used to indicate
+the status of each item to help the eye sift through the index quickly.
+
+Roundup sorts items
+in groups by priority, and then within groups by the date
+of last activity. This reveals at a glance where discussion is
+most active, and provides an easy way for anyone to move an issue
+up in the list.
+
+The page produced by a given set of browsing options constitutes
+an *index*. The options should all be part of the query
+parameters in the URL so that views may be bookmarked. An index
+specifies:
+
+- search strings for string properties
+- date ranges for date properties
+- acceptable values for choice properties
+- required values for reference properties
+- a sorting key
+- a grouping key
+- a list of properties for which to display filtering widgets
+
+Our default index is:
+
+- all **status** values except "resolved"
+- show **priority** and **fixer**
+- grouping by **priority** in sections
+- sorting by decreasing **activity** date
+
+The starting URL for Roundup immediately presents the listing of
+items generated by this default index, with no preceding query screen.
+
Added: tracker/vendor/roundup/current/doc/postgresql.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/postgresql.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,40 @@
+==========================
+PostgreSQL/psycopg Backend
+==========================
+
+This are notes about PostqreSQL backend based on the psycopg adapter for
+Roundup issue tracker.
+
+Prerequisites
+=============
+
+To use PostgreSQL as backend for storing roundup data, you should
+additionally install:
+
+1. PostgreSQL 7.x - http://www.postgresql.org/
+
+2. The psycopg python interface to PostgreSQL:
+ http://initd.org/software/initd/psycopg
+
+Some advice on setting up the postgresql backend may be found at:
+
+ http://www.magma.com.ni/wiki/index.cgi?TipsRoundupPostgres
+
+
+Running the PostgreSQL unit tests
+=================================
+
+The user that you're running the tests as will need to be able to access
+the postgresql database on the local machine and create and drop
+databases. Edit the ``test/test_postgresql.py`` database connection info if
+you wish to test against a different database.
+
+The test database will be called "rounduptest".
+
+
+Credit
+======
+
+The postgresql backend was originally submitted by Federico Di Gregorio
+<fog at initd.org>
+
Added: tracker/vendor/roundup/current/doc/roundup-admin.1
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/roundup-admin.1 Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/doc/roundup-demo.1
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/roundup-demo.1 Sun Nov 5 21:30:25 2006
@@ -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).
+.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>.
Added: tracker/vendor/roundup/current/doc/roundup-favicon.ico
==============================================================================
Binary file. No diff available.
Added: tracker/vendor/roundup/current/doc/roundup-mailgw.1
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/roundup-mailgw.1 Sun Nov 5 21:30:25 2006
@@ -0,0 +1,59 @@
+.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>.
Added: tracker/vendor/roundup/current/doc/roundup-server.1
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/roundup-server.1 Sun Nov 5 21:30:25 2006
@@ -0,0 +1,43 @@
+.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-h\fP
+print help
+.TP
+\fBname=\fP\fItracker home\fP
+Sets the tracker home(s) to use. The name is how the tracker is
+identified in the URL (it's the first part of the URL path). The
+tracker home is the directory that was identified when you did
+"roundup-admin init". You may specify any number of these name=home
+pairs on the command-line. For convenience, you may edit the
+TRACKER_HOMES variable in the roundup-server file instead.
+Make sure the name part doesn't include any url-unsafe characters like
+spaces, as these confuse the cookie handling in browsers like IE.
+.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/vendor/roundup/current/doc/roundup-server.ini.example
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/roundup-server.ini.example Sun Nov 5 21:30:25 2006
@@ -0,0 +1,16 @@
+; This is a sample configuration file for roundup-server. See the
+; admin_guide for information about its contents.
+[server]
+port = 8080
+;hostname =
+;user =
+;group =
+;log_ip = yes
+;pidfile =
+;logfile =
+
+
+; Add one of these per tracker being served
+[tracker_url_component]
+home = /path/to/tracker
+
Added: tracker/vendor/roundup/current/doc/spec.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/spec.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,1546 @@
+<html>
+<head>
+<title>Software Carpentry Track: Roundup</title>
+</head>
+<body bgcolor=white>
+
+<table width="100%">
+<tr>
+
+<td align="left">
+<a href="http://www.software-carpentry.com"><img
+src="images/logo-software-carpentry-standard.png" alt="[Software Carpentry logo]" border="0"></a>
+</td>
+
+<td align="right">
+<table>
+<tr><td>
+<a href="http://www.acl.lanl.gov"><img src="images//logo-acl-medium.png" alt="[ACL Logo]" border="0"></a>
+</td></tr>
+<tr><td><hr></td></tr>
+<tr><td>
+<a href="http://www.codesourcery.com"><img
+src="images/logo-codesourcery-medium.png" alt="[CodeSourcery Logo]" border="0"></a>
+</td></tr>
+</table>
+</td>
+
+</tr>
+</table>
+
+<hr><p>
+
+<h1 align=center>Roundup</h1>
+<h3 align=center>An Issue-Tracking System for Knowledge Workers</h3>
+<h4 align=center><a href="http://www.lfw.org/ping/">Ka-Ping Yee</a><br>
+<a href="mailto:ping at lfw.org">ping at lfw.org</a></h4>
+<h3 align=center>Implementation Guide</h3>
+
+<h2>Contents</h2>
+
+<ol>
+<li>Introduction
+<li>The Layer Cake
+<li>Hyperdatabase
+ <ol>
+ <li>Dates and Date Arithmetic
+ <li>Items and Classes
+ <li>Identifiers and Designators
+ <li>Property Names and Types
+ <li>Interface Specification
+ <li>Application Example
+ </ol>
+<li>Roundup Database
+ <ol>
+ <li>Reserved Classes
+ <ol>
+ <li>Users
+ <li>Messages
+ <li>Files
+ </ol>
+ <li>Item Classes
+ <li>Interface Specification
+ <li>Default Schema
+ </ol>
+<li>Detector Interface
+ <ol>
+ <li>Interface Specification
+ <li>Detector Example
+ </ol>
+<li>Command Interface
+ <ol>
+ <li>Interface Specification
+ <li>Usage Example
+ </ol>
+<li>E-mail User Interface
+ <ol>
+ <li>Message Processing
+ <li>Nosy Lists
+ <li>Setting Properties
+ <li>Workflow Example
+ </ol>
+<li>Web User Interface
+ <ol>
+ <li>Views and View Specifiers
+ <li>Displaying Properties
+ <li>Index Views
+ <ol>
+ <li>Index View Specifiers
+ <li>Filter Section
+ <li>Index Section
+ <li>Sorting
+ </ol>
+ <li>Item Views
+ <ol>
+ <li>Item View Specifiers
+ <li>Editor Section
+ <li>Spool Section
+ </ol>
+ </ol>
+<li>Deployment Scenarios
+<li>Acknowledgements
+</ol>
+
+<p><hr>
+<h2>1. Introduction</h2>
+
+<p>This document presents a description of the components
+of the Roundup system and specifies their interfaces and
+behaviour in sufficient detail to guide an implementation.
+For the philosophy and rationale behind the Roundup design,
+see the first-round Software Carpentry submission for Roundup.
+This document fleshes out that design as well as specifying
+interfaces so that the components can be developed separately.
+
+<p><hr>
+<h2>2. The Layer Cake</h2>
+
+<p>Lots of software design documents come with a picture of
+a cake. Everybody seems to like them. I also like cakes
+(i think they are tasty). So i, too, shall include
+a picture of a cake here.
+
+<p align=center><table cellspacing=0 cellpadding=10 border=0 align=center>
+<tr>
+<td bgcolor="#e8e8e8" align=center>
+<p><font face="helvetica, arial"><small>
+E-mail Client
+</small></font>
+</td>
+<td bgcolor="#e0e0e0" align="center">
+<p><font face="helvetica, arial"><small>
+Web Browser
+</small></font>
+</td>
+<td bgcolor="#e8e8e8" align=center>
+<p><font face="helvetica, arial"><small>
+Detector Scripts
+</small></font>
+</td>
+<td bgcolor="#e0e0e0" align="center">
+<p><font face="helvetica, arial"><small>
+Shell
+</small></font>
+</td>
+<tr>
+<td bgcolor="#d0d0f0" align=center>
+<p><font face="helvetica, arial"><small>
+E-mail User Interface
+</small></font>
+</td>
+<td bgcolor="#f0d0d0" align=center>
+<p><font face="helvetica, arial"><small>
+Web User Interface
+</small></font>
+</td>
+<td bgcolor="#d0f0d0" align=center>
+<p><font face="helvetica, arial"><small>
+Detector Interface
+</small></font>
+</td>
+<td bgcolor="#f0d0f0" align=center>
+<p><font face="helvetica, arial"><small>
+Command Interface
+</small></font>
+</td>
+<tr>
+<td bgcolor="#f0f0d0" colspan=4 align=center>
+<p><font face="helvetica, arial"><small>
+Roundup Database Layer
+</small></font>
+</td>
+<tr>
+<td bgcolor="#d0f0f0" colspan=4 align=center>
+<p><font face="helvetica, arial"><small>
+Hyperdatabase Layer
+</small></font>
+</td>
+<tr>
+<td bgcolor="#e8e8e8" colspan=4 align=center>
+<p><font face="helvetica, arial"><small>
+Storage Layer
+</small></font>
+</td>
+</table>
+
+<p>The colourful parts of the cake are part of our system;
+the faint grey parts of the cake are external components.
+
+<p>I will now proceed to forgo all table manners and
+eat from the bottom of the cake to the top. You may want
+to stand back a bit so you don't get covered in crumbs.
+
+<p><hr>
+<h2>3. Hyperdatabase</h2>
+
+<p>The lowest-level component to be implemented is the hyperdatabase.
+The hyperdatabase is intended to be
+a flexible data store that can hold configurable data in
+records which we call <em>items</em>.
+
+<p>The hyperdatabase is implemented on top of the storage layer,
+an external module for storing its data. The storage layer could
+be a third-party RDBMS; for a "batteries-included" distribution,
+implementing the hyperdatabase on the standard <tt>bsddb</tt>
+module is suggested.
+
+<h3>3.1. Dates and Date Arithmetic</h3>
+
+<p>Before we get into the hyperdatabase itself, we need a
+way of handling dates. The hyperdatabase module provides
+Timestamp objects for
+representing date-and-time stamps and Interval objects for
+representing date-and-time intervals.
+
+<p>As strings, date-and-time stamps are specified with
+the date in international standard format
+(<em>yyyy</em>-<em>mm</em>-<em>dd</em>)
+joined to the time (<em>hh</em>:<em>mm</em>:<em>ss</em>)
+by a period ("."). Dates in
+this form can be easily compared and are fairly readable
+when printed. An example of a valid stamp is
+"<strong>2000-06-24.13:03:59</strong>".
+We'll call this the "full date format". When Timestamp objects are
+printed as strings, they appear in the full date format with
+the time always given in GMT. The full date format is always
+exactly 19 characters long.
+
+<p>For user input, some partial forms are also permitted:
+the whole time or just the seconds may be omitted; and the whole date
+may be omitted or just the year may be omitted. If the time is given,
+the time is interpreted in the user's local time zone.
+The <tt>Date</tt> constructor takes care of these conversions.
+In the following examples, suppose that <em>yyyy</em> is the current year,
+<em>mm</em> is the current month, and <em>dd</em> is the current
+day of the month; and suppose that the user is on Eastern Standard Time.
+
+<ul>
+<li>"<strong>2000-04-17</strong>" means <Date 2000-04-17.00:00:00>
+<li>"<strong>01-25</strong>" means <Date <em>yyyy</em>-01-25.00:00:00>
+<li>"<strong>2000-04-17.03:45</strong>" means <Date 2000-04-17.08:45:00>
+<li>"<strong>08-13.22:13</strong>" means <Date <em>yyyy</em>-08-14.03:13:00>
+<li>"<strong>11-07.09:32:43</strong>" means <Date <em>yyyy</em>-11-07.14:32:43>
+<li>"<strong>14:25</strong>" means
+<Date <em>yyyy</em>-<em>mm</em>-<em>dd</em>.19:25:00>
+<li>"<strong>8:47:11</strong>" means
+<Date <em>yyyy</em>-<em>mm</em>-<em>dd</em>.13:47:11>
+<li>the special date "<strong>.</strong>" means "right now"
+</ul>
+
+<p>Date intervals are specified using the suffixes
+"y", "m", and "d". The suffix "w" (for "week") means 7 days.
+Time intervals are specified in hh:mm:ss format (the seconds
+may be omitted, but the hours and minutes may not).
+
+<ul>
+<li>"<strong>3y</strong>" means three years
+<li>"<strong>2y 1m</strong>" means two years and one month
+<li>"<strong>1m 25d</strong>" means one month and 25 days
+<li>"<strong>2w 3d</strong>" means two weeks and three days
+<li>"<strong>1d 2:50</strong>" means one day, two hours, and 50 minutes
+<li>"<strong>14:00</strong>" means 14 hours
+<li>"<strong>0:04:33</strong>" means four minutes and 33 seconds
+</ul>
+
+<p>The Date class should understand simple date expressions of the form
+<em>stamp</em> + <em>interval</em> and <em>stamp</em> - <em>interval</em>.
+When adding or subtracting intervals involving months or years, the
+components are handled separately. For example, when evaluating
+"<strong>2000-06-25 + 1m 10d</strong>", we first add one month to
+get <strong>2000-07-25</strong>, then add 10 days to get
+<strong>2000-08-04</strong> (rather than trying to decide whether
+<strong>1m 10d</strong> means 38 or 40 or 41 days).
+
+<p>Here is an outline of the Date and Interval classes.
+
+<blockquote>
+<pre><small>class <strong>Date</strong>:
+ def <strong>__init__</strong>(self, spec, offset):
+ """Construct a date given a specification and a time zone offset.
+
+ 'spec' is a full date or a partial form, with an optional
+ added or subtracted interval. 'offset' is the local time
+ zone offset from GMT in hours.
+ """
+
+ def <strong>__add__</strong>(self, interval):
+ """Add an interval to this date to produce another date."""
+
+ def <strong>__sub__</strong>(self, interval):
+ """Subtract an interval from this date to produce another date."""
+
+ def <strong>__cmp__</strong>(self, other):
+ """Compare this date to another date."""
+
+ def <strong>__str__</strong>(self):
+ """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
+
+ def <strong>local</strong>(self, offset):
+ """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone."""
+
+class <strong>Interval</strong>:
+ def <strong>__init__</strong>(self, spec):
+ """Construct an interval given a specification."""
+
+ def <strong>__cmp__</strong>(self, other):
+ """Compare this interval to another interval."""
+
+ def <strong>__str__</strong>(self):
+ """Return this interval as a string."""
+</small></pre>
+</blockquote>
+
+<p>Here are some examples of how these classes would behave in practice.
+For the following examples, assume that we are on Eastern Standard
+Time and the current local time is 19:34:02 on 25 June 2000.
+
+<blockquote><pre><small
+>>>> <span class="input">Date(".")</span>
+<span class="output"><Date 2000-06-26.00:34:02></span>
+>>> <span class="input">_.local(-5)</span>
+<span class="output">"2000-06-25.19:34:02"</span>
+>>> <span class="input">Date(". + 2d")</span>
+<span class="output"><Date 2000-06-28.00:34:02></span>
+>>> <span class="input">Date("1997-04-17", -5)</span>
+<span class="output"><Date 1997-04-17.00:00:00></span>
+>>> <span class="input">Date("01-25", -5)</span>
+<span class="output"><Date 2000-01-25.00:00:00></span>
+>>> <span class="input">Date("08-13.22:13", -5)</span>
+<span class="output"><Date 2000-08-14.03:13:00></span>
+>>> <span class="input">Date("14:25", -5)</span>
+<span class="output"><Date 2000-06-25.19:25:00></span>
+>>> <span class="input">Interval(" 3w 1 d 2:00")</span>
+<span class="output"><Interval 22d 2:00></span>
+>>> <span class="input">Date(". + 2d") - Interval("3w")</span>
+<span class="output"><Date 2000-06-07.00:34:02></span
+></small></pre></blockquote>
+
+<h3>3.2. Items and Classes</h3>
+
+<p>Items contain data in <em>properties</em>. To Python, these
+properties are presented as the key-value pairs of a dictionary.
+Each item belongs to a <em>class</em> which defines the names
+and types of its properties. The database permits the creation
+and modification of classes as well as items.
+
+<h3>3.3. Identifiers and Designators</h3>
+
+<p>Each item has a numeric identifier which is unique among
+items in its class. The items are numbered sequentially
+within each class in order of creation, starting from 1.
+The <em>designator</em>
+for an item is a way to identify an item in the database, and
+consists of the name of the item's class concatenated with
+the item's numeric identifier.
+
+<p>For example, if "spam" and "eggs" are classes, the first
+item created in class "spam" has id 1 and designator "spam1".
+The first item created in class "eggs" also has id 1 but has
+the distinct designator "eggs1". Item designators are
+conventionally enclosed in square brackets when mentioned
+in plain text. This permits a casual mention of, say,
+"[patch37]" in an e-mail message to be turned into an active
+hyperlink.
+
+<h3>3.4. Property Names and Types</h3>
+
+<p>Property names must begin with a letter.
+
+<p>A property may be one of five <em>basic types</em>:
+
+<ul>
+<li><em>String</em> properties are for storing arbitrary-length
+strings.
+
+<li><em>Date</em> properties store date-and-time stamps.
+Their values are Timestamp objects.
+
+<li>A <em>Link</em> property refers to a single other item
+selected from a specified class. The class is part of the property;
+the value is an integer, the id of the chosen item.
+
+<li>A <em>Multilink</em> property refers to possibly many items
+in a specified class. The value is a list of integers.
+</ul>
+
+<p><tt>None</tt> is also a permitted value for any of these property
+types. An attempt to store <tt>None</tt> into a String property
+stores the empty string; an attempt to store <tt>None</tt>
+into a Multilink property stores an empty list.
+
+<h3>3.5. Interface Specification</h3>
+
+<p>The hyperdb module provides property objects to designate
+the different kinds of properties. These objects are used when
+specifying what properties belong in classes.
+
+<blockquote><pre><small
+>class <strong>String</strong>:
+ def <strong>__init__</strong>(self):
+ """An object designating a String property."""
+
+class <strong>Date</strong>:
+ def <strong>__init__</strong>(self):
+ """An object designating a Date property."""
+
+class <strong>Link</strong>:
+ def <strong>__init__</strong>(self, classname):
+ """An object designating a Link property that links to
+ items in a specified class."""
+
+class <strong>Multilink</strong>:
+ def <strong>__init__</strong>(self, classname):
+ """An object designating a Multilink property that links
+ to items in a specified class."""
+</small></pre></blockquote>
+
+<p>Here is the interface provided by the hyperdatabase.
+
+<blockquote><pre><small
+>class <strong>Database</strong>:
+ """A database for storing records containing flexible data types."""
+
+ def <strong>__init__</strong>(self, storagelocator, journaltag):
+ """Open a hyperdatabase given a specifier to some storage.
+
+ The meaning of 'storagelocator' depends on the particular
+ implementation of the hyperdatabase. It could be a file name,
+ a directory path, a socket descriptor for a connection to a
+ database over the network, etc.
+
+ The 'journaltag' is a token that will be attached to the journal
+ entries for any edits done on the database. If 'journaltag' is
+ None, the database is opened in read-only mode: the Class.create(),
+ Class.set(), and Class.retire() methods are disabled.
+ """
+
+ def <strong>__getattr__</strong>(self, classname):
+ """A convenient way of calling self.getclass(classname)."""
+
+ def <strong>getclasses</strong>(self):
+ """Return a list of the names of all existing classes."""
+
+ def <strong>getclass</strong>(self, classname):
+ """Get the Class object representing a particular class.
+
+ If 'classname' is not a valid class name, a KeyError is raised.
+ """
+
+class <strong>Class</strong>:
+ """The handle to a particular class of items in a hyperdatabase."""
+
+ def <strong>__init__</strong>(self, db, classname, **properties):
+ """Create a new class with a given name and property specification.
+
+ 'classname' must not collide with the name of an existing class,
+ or a ValueError is raised. The keyword arguments in 'properties'
+ must map names to property objects, or a TypeError is raised.
+ """
+
+ # Editing items:
+
+ def <strong>create</strong>(self, **propvalues):
+ """Create a new item of this class and return its id.
+
+ The keyword arguments in 'propvalues' map property names to values.
+ The values of arguments must be acceptable for the types of their
+ corresponding properties or a TypeError is raised. If this class
+ has a key property, it must be present and its value must not
+ collide with other key strings or a ValueError is raised. Any other
+ properties on this class that are missing from the 'propvalues'
+ dictionary are set to None. If an id in a link or multilink
+ property does not refer to a valid item, an IndexError is raised.
+ """
+
+ def <strong>get</strong>(self, itemid, propname):
+ """Get the value of a property on an existing item of this class.
+
+ 'itemid' must be the id of an existing item of this class or an
+ IndexError is raised. 'propname' must be the name of a property
+ of this class or a KeyError is raised.
+ """
+
+ def <strong>set</strong>(self, itemid, **propvalues):
+ """Modify a property on an existing item of this class.
+
+ 'itemid' must be the id of an existing item of this class or an
+ IndexError is raised. Each key in 'propvalues' must be the name
+ of a property of this class or a KeyError is raised. All values
+ in 'propvalues' must be acceptable types for their corresponding
+ properties or a TypeError is raised. If the value of the key
+ property is set, it must not collide with other key strings or a
+ ValueError is raised. If the value of a Link or Multilink
+ property contains an invalid item id, a ValueError is raised.
+ """
+
+ def <strong>retire</strong>(self, itemid):
+ """Retire an item.
+
+ The properties on the item remain available from the get() method,
+ and the item's id is never reused. Retired items are not returned
+ by the find(), list(), or lookup() methods, and other items may
+ reuse the values of their key properties.
+ """
+
+ def <strong>history</strong>(self, itemid):
+ """Retrieve the journal of edits on a particular item.
+
+ 'itemid' must be the id of an existing item of this class or an
+ IndexError is raised.
+
+ The returned list contains tuples of the form
+
+ (date, tag, action, params)
+
+ 'date' is a Timestamp object specifying the time of the change and
+ 'tag' is the journaltag specified when the database was opened.
+ 'action' may be:
+
+ 'create' or 'set' -- 'params' is a dictionary of property values
+ 'link' or 'unlink' -- 'params' is (classname, itemid, propname)
+ 'retire' -- 'params' is None
+ """
+
+ # Locating items:
+
+ def <strong>setkey</strong>(self, propname):
+ """Select a String property of this class to be the key property.
+
+ 'propname' must be the name of a String property of this class or
+ None, or a TypeError is raised. The values of the key property on
+ all existing items must be unique or a ValueError is raised.
+ """
+
+ def <strong>getkey</strong>(self):
+ """Return the name of the key property for this class or None."""
+
+ def <strong>lookup</strong>(self, keyvalue):
+ """Locate a particular item 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 items in this class, the matching item's id is returned;
+ otherwise a KeyError is raised.
+ """
+
+ def <strong>find</strong>(self, propname, itemid):
+ """Get the ids of items in this class which link to a given item.
+
+ 'propname' must be the name of a property in this class, or a
+ KeyError is raised. That property must be a Link or Multilink
+ property, or a TypeError is raised. 'itemid' must be the id of
+ an existing item in the class linked to by the given property,
+ or an IndexError is raised.
+ """
+
+ def <strong>list</strong>(self):
+ """Return a list of the ids of the active items in this class."""
+
+ def <strong>count</strong>(self):
+ """Get the number of items in this class.
+
+ If the returned integer is 'numitems', the ids of all the items
+ in this class run from 1 to numitems, and numitems+1 will be the
+ id of the next item to be created in this class.
+ """
+
+ # Manipulating properties:
+
+ def <strong>getprops</strong>(self):
+ """Return a dictionary mapping property names to property objects."""
+
+ def <strong>addprop</strong>(self, **properties):
+ """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.
+ """</small></pre></blockquote>
+
+<h3>3.6. Application Example</h3>
+
+<p>Here is an example of how the hyperdatabase module would work in practice.
+
+<blockquote><pre><small
+>>>> <span class="input">import hyperdb</span>
+>>> <span class="input">db = hyperdb.Database("foo.db", "ping")</span>
+>>> <span class="input">db</span>
+<span class="output"><hyperdb.Database "foo.db" opened by "ping"></span>
+>>> <span class="input">hyperdb.Class(db, "status", name=hyperdb.String())</span>
+<span class="output"><hyperdb.Class "status"></span>
+>>> <span class="input">_.setkey("name")</span>
+>>> <span class="input">db.status.create(name="unread")</span>
+<span class="output">1</span>
+>>> <span class="input">db.status.create(name="in-progress")</span>
+<span class="output">2</span>
+>>> <span class="input">db.status.create(name="testing")</span>
+<span class="output">3</span>
+>>> <span class="input">db.status.create(name="resolved")</span>
+<span class="output">4</span>
+>>> <span class="input">db.status.count()</span>
+<span class="output">4</span>
+>>> <span class="input">db.status.list()</span>
+<span class="output">[1, 2, 3, 4]</span>
+>>> <span class="input">db.status.lookup("in-progress")</span>
+<span class="output">2</span>
+>>> <span class="input">db.status.retire(3)</span>
+>>> <span class="input">db.status.list()</span>
+<span class="output">[1, 2, 4]</span>
+>>> <span class="input">hyperdb.Class(db, "issue", title=hyperdb.String(), status=hyperdb.Link("status"))</span>
+<span class="output"><hyperdb.Class "issue"></span>
+>>> <span class="input">db.issue.create(title="spam", status=1)</span>
+<span class="output">1</span>
+>>> <span class="input">db.issue.create(title="eggs", status=2)</span>
+<span class="output">2</span>
+>>> <span class="input">db.issue.create(title="ham", status=4)</span>
+<span class="output">3</span>
+>>> <span class="input">db.issue.create(title="arguments", status=2)</span>
+<span class="output">4</span>
+>>> <span class="input">db.issue.create(title="abuse", status=1)</span>
+<span class="output">5</span>
+>>> <span class="input">hyperdb.Class(db, "user", username=hyperdb.Key(), password=hyperdb.String())</span>
+<span class="output"><hyperdb.Class "user"></span>
+>>> <span class="input">db.issue.addprop(fixer=hyperdb.Link("user"))</span>
+>>> <span class="input">db.issue.getprops()</span>
+<span class="output"
+>{"title": <hyperdb.String>, "status": <hyperdb.Link to "status">,
+ "user": <hyperdb.Link to "user">}</span>
+>>> <span class="input">db.issue.set(5, status=2)</span>
+>>> <span class="input">db.issue.get(5, "status")</span>
+<span class="output">2</span>
+>>> <span class="input">db.status.get(2, "name")</span>
+<span class="output">"in-progress"</span>
+>>> <span class="input">db.issue.get(5, "title")</span>
+<span class="output">"abuse"</span>
+>>> <span class="input">db.issue.find("status", db.status.lookup("in-progress"))</span>
+<span class="output">[2, 4, 5]</span>
+>>> <span class="input">db.issue.history(5)</span>
+<span class="output"
+>[(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse", "status": 1}),
+ (<Date 2000-06-28.19:11:04>, "ping", "set", {"status": 2})]</span>
+>>> <span class="input">db.status.history(1)</span>
+<span class="output"
+>[(<Date 2000-06-28.19:09:43>, "ping", "link", ("issue", 5, "status")),
+ (<Date 2000-06-28.19:11:04>, "ping", "unlink", ("issue", 5, "status"))]</span>
+>>> <span class="input">db.status.history(2)</span>
+<span class="output"
+>[(<Date 2000-06-28.19:11:04>, "ping", "link", ("issue", 5, "status"))]</span>
+</small></pre></blockquote>
+
+<p>For the purposes of journalling, when a Multilink property is
+set to a new list of items, the hyperdatabase compares the old
+list to the new list.
+The journal records "unlink" events for all the items that appear
+in the old list but not the new list,
+and "link" events for
+all the items that appear in the new list but not in the old list.
+
+<p><hr>
+<h2>4. Roundup Database</h2>
+
+<p>The Roundup database layer is implemented on top of the
+hyperdatabase and mediates calls to the database.
+Some of the classes in the Roundup database are considered
+<em>item classes</em>.
+The Roundup database layer adds detectors and user items,
+and on items it provides mail spools, nosy lists, and superseders.
+
+<h3>4.1. Reserved Classes</h3>
+
+<p>Internal to this layer we reserve three special classes
+of items that are not items.
+
+<h4>4.1.1. Users</h4>
+
+<p>Users are stored in the hyperdatabase as items of
+class "user". The "user" class has the definition:
+
+<blockquote><pre><small
+>hyperdb.Class(db, "user", username=hyperdb.String(),
+ password=hyperdb.String(),
+ address=hyperdb.String())
+db.user.setkey("username")</small></pre></blockquote>
+
+<h4>4.1.2. Messages</h4>
+
+<p>E-mail messages are represented by hyperdatabase items of class "msg".
+The actual text content of the messages is stored in separate files.
+(There's no advantage to be gained by stuffing them into the
+hyperdatabase, and if messages are stored in ordinary text files,
+they can be grepped from the command line.) The text of a message is
+saved in a file named after the message item designator (e.g. "msg23")
+for the sake of the command interface (see below). Attachments are
+stored separately and associated with "file" items.
+The "msg" class has the definition:
+
+<blockquote><pre><small
+>hyperdb.Class(db, "msg", author=hyperdb.Link("user"),
+ recipients=hyperdb.Multilink("user"),
+ date=hyperdb.Date(),
+ summary=hyperdb.String(),
+ files=hyperdb.Multilink("file"))</small
+></pre></blockquote>
+
+<p>The "author" property indicates the author of the message
+(a "user" item must exist in the hyperdatabase for any messages
+that are stored in the system).
+The "summary" property contains a summary of the message for display
+in a message index.
+
+<h4>4.1.3. Files</h4>
+
+<p>Submitted files are represented by hyperdatabase
+items of class "file". Like e-mail messages, the file content
+is stored in files outside the database,
+named after the file item designator (e.g. "file17").
+The "file" class has the definition:
+
+<blockquote><pre><small
+>hyperdb.Class(db, "file", user=hyperdb.Link("user"),
+ name=hyperdb.String(),
+ type=hyperdb.String())</small></pre></blockquote>
+
+<p>The "user" property indicates the user who submitted the
+file, the "name" property holds the original name of the file,
+and the "type" property holds the MIME type of the file as received.
+
+<h3>4.2. Item Classes</h3>
+
+<p>All items have the following standard properties:
+
+<blockquote><pre><small
+>title=hyperdb.String()
+messages=hyperdb.Multilink("msg")
+files=hyperdb.Multilink("file")
+nosy=hyperdb.Multilink("user")
+superseder=hyperdb.Multilink("item")</small></pre></blockquote>
+
+<p>Also, two Date properties named "creation" and "activity" are
+fabricated by the Roundup database layer. By "fabricated" we
+mean that no such properties are actually stored in the
+hyperdatabase, but when properties on items are requested, the
+"creation" and "activity" properties are made available.
+The value of the "creation" property is the date when an item was
+created, and the value of the "activity" property is the
+date when any property on the item was last edited (equivalently,
+these are the dates on the first and last records in the item's journal).
+
+<h3>4.3. Interface Specification</h3>
+
+<p>The interface to a Roundup database delegates most method
+calls to the hyperdatabase, except for the following
+changes and additional methods.
+
+<blockquote><pre><small
+>class <strong>Database</strong>:
+ # Overridden methods:
+
+ def <strong>__init__</strong>(self, storagelocator, journaltag):
+ """When the Roundup database is opened by a particular user,
+ the 'journaltag' is the id of the user's "user" item."""
+
+ def <strong>getclass</strong>(self, classname):
+ """This method now returns an instance of either Class or
+ ItemClass depending on whether an item class is specified."""
+
+ # New methods:
+
+ def <strong>getuid</strong>(self):
+ """Return the id of the "user" item associated with the user
+ that owns this connection to the hyperdatabase."""
+
+class <strong>Class</strong>:
+ # Overridden methods:
+
+ def <strong>create</strong>(self, **propvalues):
+ def <strong>set</strong>(self, **propvalues):
+ def <strong>retire</strong>(self, itemid):
+ """These operations trigger detectors and can be vetoed. Attempts
+ to modify the "creation" or "activity" properties cause a KeyError.
+ """
+
+ # New methods:
+
+ def <strong>audit</strong>(self, event, detector):
+ def <strong>react</strong>(self, event, detector):
+ """Register a detector (see below for more details)."""
+
+class <strong>ItemClass</strong>(Class):
+ # Overridden methods:
+
+ def <strong>__init__</strong>(self, db, classname, **properties):
+ """The newly-created class automatically includes the "messages",
+ "files", "nosy", and "superseder" properties. If the 'properties'
+ dictionary attempts to specify any of these properties or a
+ "creation" or "activity" property, a ValueError is raised."""
+
+ def <strong>get</strong>(self, itemid, propname):
+ def <strong>getprops</strong>(self):
+ """In addition to the actual properties on the item, these
+ methods provide the "creation" and "activity" properties."""
+
+ # New methods:
+
+ def <strong>addmessage</strong>(self, itemid, summary, text):
+ """Add a message to an item's mail spool.
+
+ A new "msg" item is constructed using the current date, the
+ user that owns the database connection as the author, and
+ the specified summary text. The "files" and "recipients"
+ fields are left empty. The given text is saved as the body
+ of the message and the item is appended to the "messages"
+ field of the specified item.
+ """
+
+ def <strong>nosymessage</strong>(self, itemid, msgid):
+ """Send a message to the members of an item's nosy list.
+
+ The message is sent only to users on the nosy list who are not
+ already on the "recipients" list for the message. These users
+ are then added to the message's "recipients" list.
+ """
+</small></pre></blockquote>
+
+<h3>4.4. Default Schema</h3>
+
+<p>The default schema included with Roundup turns it into a
+typical software bug tracker. The database is set up like this:
+
+<blockquote><pre><small
+>pri = Class(db, "priority", name=hyperdb.String(), order=hyperdb.String())
+pri.setkey("name")
+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 = Class(db, "status", name=hyperdb.String(), order=hyperdb.String())
+stat.setkey("name")
+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")
+
+Class(db, "keyword", name=hyperdb.String())
+
+Class(db, "issue", fixer=hyperdb.Multilink("user"),
+ topic=hyperdb.Multilink("keyword"),
+ priority=hyperdb.Link("priority"),
+ status=hyperdb.Link("status"))
+</small></pre></blockquote>
+
+<p>(The "order" property hasn't been explained yet. It
+gets used by the Web user interface for sorting.)
+
+<p>The above isn't as pretty-looking as the schema specification
+in the first-stage submission, but it could be made just as easy
+with the addition of a convenience function like <tt>Choice</tt>
+for setting up the "priority" and "status" classes:
+
+<blockquote><pre><small
+>def Choice(name, *options):
+ cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
+ for i in range(len(options)):
+ cl.create(name=option[i], order=i)
+ return hyperdb.Link(name)
+</small></pre></blockquote>
+
+<p><hr>
+<h2>5. Detector Interface</h2>
+
+<p>Detectors are Python functions that are triggered on certain
+kinds of events. The definitions of the
+functions live in Python modules placed in a directory set aside
+for this purpose. Importing the Roundup database module also
+imports all the modules in this directory, and the <tt>init()</tt>
+function of each module is called when a database is opened to
+provide it a chance to register its detectors.
+
+<p>There are two kinds of detectors:
+
+<ul>
+<li>an <em>auditor</em> is triggered just before modifying an item
+<li>a <em>reactor</em> is triggered just after an item has been modified
+</ul>
+
+<p>When the Roundup database is about to perform a
+<tt>create()</tt>, <tt>set()</tt>, or <tt>retire()</tt>
+operation, it first calls any auditors that
+have been registered for that operation on that class.
+Any auditor may raise a <tt>Reject</tt> exception
+to abort the operation.
+
+<p>If none of the auditors raises an exception, the database
+proceeds to carry out the operation. After it's done, it
+then calls all of the reactors that have been registered
+for the operation.
+
+<h3>5.1. Interface Specification</h3>
+
+<p>The <tt>audit()</tt> and <tt>react()</tt> methods
+register detectors on a given class of items.
+
+<blockquote><pre><small
+>class Class:
+ def <strong>audit</strong>(self, event, detector):
+ """Register an auditor on this class.
+
+ 'event' should be one of "create", "set", or "retire".
+ 'detector' should be a function accepting four arguments.
+ """
+
+ def <strong>react</strong>(self, event, detector):
+ """Register a reactor on this class.
+
+ 'event' should be one of "create", "set", or "retire".
+ 'detector' should be a function accepting four arguments.
+ """
+</small></pre></blockquote>
+
+<p>Auditors are called with the arguments:
+
+<blockquote><pre><small
+>audit(db, cl, itemid, newdata)</small></pre></blockquote>
+
+where <tt>db</tt> is the database, <tt>cl</tt> is an
+instance of Class or ItemClass within the database, and <tt>newdata</tt>
+is a dictionary mapping property names to values.
+
+For a <tt>create()</tt>
+operation, the <tt>itemid</tt> argument is <tt>None</tt> and <tt>newdata</tt>
+contains all of the initial property values with which the item
+is about to be created.
+
+For a <tt>set()</tt> operation, <tt>newdata</tt>
+contains only the names and values of properties that are about
+to be changed.
+
+For a <tt>retire()</tt> operation, <tt>newdata</tt> is <tt>None</tt>.
+
+<p>Reactors are called with the arguments:
+
+<blockquote><pre><small
+>react(db, cl, itemid, olddata)</small></pre></blockquote>
+
+where <tt>db</tt> is the database, <tt>cl</tt> is an
+instance of Class or ItemClass within the database, and <tt>olddata</tt>
+is a dictionary mapping property names to values.
+
+For a <tt>create()</tt>
+operation, the <tt>itemid</tt> argument is the id of the
+newly-created item and <tt>olddata</tt> is None.
+
+For a <tt>set()</tt> operation, <tt>olddata</tt>
+contains the names and previous values of properties that were changed.
+
+For a <tt>retire()</tt> operation, <tt>itemid</tt> is the
+id of the retired item and <tt>olddata</tt> is <tt>None</tt>.
+
+<h3>5.2. Detector Example</h3>
+
+<p>Here is an example of detectors written for a hypothetical
+project-management application, where users can signal approval
+of a project by adding themselves to an "approvals" list, and
+a project proceeds when it has three approvals.
+
+<blockquote><pre><small
+># Permit users only to add themselves to the "approvals" list.
+
+def check_approvals(db, cl, id, newdata):
+ if newdata.has_key("approvals"):
+ if cl.get(id, "status") == db.status.lookup("approved"):
+ raise Reject, "You can't modify the approvals list " \
+ "for a project that has already been approved."
+ old = cl.get(id, "approvals")
+ new = newdata["approvals"]
+ for uid in old:
+ if uid not in new and uid != db.getuid():
+ raise Reject, "You can't remove other users from the "
+ "approvals list; you can only remove yourself."
+ for uid in new:
+ if uid not in old and uid != db.getuid():
+ raise Reject, "You can't add other users to the approvals "
+ "list; you can only add yourself."
+
+# When three people have approved a project, change its
+# status from "pending" to "approved".
+
+def approve_project(db, cl, id, olddata):
+ if olddata.has_key("approvals") and len(cl.get(id, "approvals")) == 3:
+ if cl.get(id, "status") == db.status.lookup("pending"):
+ cl.set(id, status=db.status.lookup("approved"))
+
+def init(db):
+ db.project.audit("set", check_approval)
+ db.project.react("set", approve_project)</small
+></pre></blockquote>
+
+<p>Here is another example of a detector that can allow or prevent
+the creation of new items. In this scenario, patches for a software
+project are submitted by sending in e-mail with an attached file,
+and we want to ensure that there are <tt>text/plain</tt> attachments on
+the message. The maintainer of the package can then apply the
+patch by setting its status to "applied".
+
+<blockquote><pre><small
+># Only accept attempts to create new patches that come with patch files.
+
+def check_new_patch(db, cl, id, newdata):
+ if not newdata["files"]:
+ raise Reject, "You can't submit a new patch without " \
+ "attaching a patch file."
+ for fileid in newdata["files"]:
+ if db.file.get(fileid, "type") != "text/plain":
+ raise Reject, "Submitted patch files must be text/plain."
+
+# When the status is changed from "approved" to "applied", apply the patch.
+
+def apply_patch(db, cl, id, olddata):
+ if cl.get(id, "status") == db.status.lookup("applied") and \
+ olddata["status"] == db.status.lookup("approved"):
+ # ...apply the patch...
+
+def init(db):
+ db.patch.audit("create", check_new_patch)
+ db.patch.react("set", apply_patch)</small
+></pre></blockquote>
+
+<p><hr>
+<h2>6. Command Interface</h2>
+
+<p>The command interface is a very simple and minimal interface,
+intended only for quick searches and checks from the shell prompt.
+(Anything more interesting can simply be written in Python using
+the Roundup database module.)
+
+<h3>6.1. Interface Specification</h3>
+
+<p>A single command, <tt>roundup</tt>, provides basic access to
+the hyperdatabase from the command line.
+
+<ul>
+<li><tt>roundup get </tt>[<tt>-list</tt>]<tt> </tt
+><em>designator</em>[<tt>,</tt
+><em>designator</em><tt>,</tt>...]<tt> </tt><em>propname</em>
+<li><tt>roundup set </tt><em>designator</em>[<tt>,</tt
+><em>designator</em><tt>,</tt>...]<tt> </tt><em>propname</em
+><tt>=</tt><em>value</em> ...
+<li><tt>roundup find </tt>[<tt>-list</tt>]<tt> </tt
+><em>classname</em><tt> </tt><em>propname</em>=<em>value</em> ...
+</ul>
+
+<p>Property values are represented as strings in command arguments
+and in the printed results:
+
+<ul>
+<li>Strings are, well, strings.
+
+<li>Date values are printed in the full date format in the local
+time zone, and accepted in the full format or any of the partial
+formats explained above.
+
+<li>Link values are printed as item designators. When given as
+an argument, item designators and key strings are both accepted.
+
+<li>Multilink values are printed as lists of item designators
+joined by commas. When given as an argument, item designators
+and key strings are both accepted; an empty string, a single item,
+or a list of items joined by commas is accepted.
+</ul>
+
+<p>When multiple items are specified to the
+<tt>roundup get</tt> or <tt>roundup set</tt>
+commands, the specified properties are retrieved or set
+on all the listed items.
+
+<p>When multiple results are returned by the <tt>roundup get</tt>
+or <tt>roundup find</tt> commands, they are printed one per
+line (default) or joined by commas (with the <tt>-list</tt>) option.
+
+<h3>6.2. Usage Example</h3>
+
+<p>To find all messages regarding in-progress issues that
+contain the word "spam", for example, you could execute the
+following command from the directory where the database
+dumps its files:
+
+<blockquote><pre><small
+>shell% <span class="input">for issue in `roundup find issue status=in-progress`; do</span>
+> <span class="input">grep -l spam `roundup get $issue messages`</span>
+> <span class="input">done</span>
+<span class="output">msg23
+msg49
+msg50
+msg61</span>
+shell%</small></pre></blockquote>
+
+<p>Or, using the <tt>-list</tt> option, this can be written as a single command:
+
+<blockquote><pre><small
+>shell% <span class="input">grep -l spam `roundup get \
+ \`roundup find -list issue status=in-progress\` messages`</span>
+<span class="output">msg23
+msg49
+msg50
+msg61</span>
+shell%</small></pre></blockquote>
+
+<p><hr>
+<h2>7. E-mail User Interface</h2>
+
+<p>The Roundup system must be assigned an e-mail address
+at which to receive mail. Messages should be piped to
+the Roundup mail-handling script by the mail delivery
+system (e.g. using an alias beginning with "|" for sendmail).
+
+<h3>7.1. Message Processing</h3>
+
+<p>Incoming messages are examined for multiple parts.
+In a <tt>multipart/mixed</tt> message or part, each subpart is
+extracted and examined. In a <tt>multipart/alternative</tt>
+message or part, we look for a <tt>text/plain</tt> subpart and
+ignore the other parts. The <tt>text/plain</tt> subparts are
+assembled to form the textual body of the message, to
+be stored in the file associated with a "msg" class item.
+Any parts of other types are each stored in separate
+files and given "file" class items that are linked to
+the "msg" item.
+
+<p>The "summary" property on message items is taken from
+the first non-quoting section in the message body.
+The message body is divided into sections by blank lines.
+Sections where the second and all subsequent lines begin
+with a ">" or "|" character are considered "quoting
+sections". The first line of the first non-quoting
+section becomes the summary of the message.
+
+<p>All of the addresses in the To: and Cc: headers of the
+incoming message are looked up among the user items, and
+the corresponding users are placed in the "recipients"
+property on the new "msg" item. The address in the From:
+header similarly determines the "author" property of the
+new "msg" item.
+The default handling for
+addresses that don't have corresponding users is to create
+new users with no passwords and a username equal to the
+address. (The web interface does not permit logins for
+users with no passwords.) If we prefer to reject mail from
+outside sources, we can simply register an auditor on the
+"user" class that prevents the creation of user items with
+no passwords.
+
+<p>The subject line of the incoming message is examined to
+determine whether the message is an attempt to create a new
+item or to discuss an existing item. A designator enclosed
+in square brackets is sought as the first thing on the
+subject line (after skipping any "Fwd:" or "Re:" prefixes).
+
+<p>If an item designator (class name and id number) is found
+there, the newly created "msg" item is added to the "messages"
+property for that item, and any new "file" items are added to
+the "files" property for the item.
+
+<p>If just an item class name is found there, we attempt to
+create a new item of that class with its "messages" property
+initialized to contain the new "msg" item and its "files"
+property initialized to contain any new "file" items.
+
+<p>Both cases may trigger detectors (in the first case we
+are calling the <tt>set()</tt> method to add the message to the
+item's spool; in the second case we are calling the
+<tt>create()</tt> method to create a new item). If an auditor
+raises an exception, the original message is bounced back to
+the sender with the explanatory message given in the exception.
+
+<h3>7.2. Nosy Lists</h3>
+
+<p>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
+item 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.
+
+<h3>7.3. Setting Properties</h3>
+
+<p>The e-mail interface also provides a simple way to set
+properties on items. At the end of the subject line,
+<em>propname</em><tt>=</tt><em>value</em> pairs can be
+specified in square brackets, using the same conventions
+as for the <tt>roundup set</tt> shell command.
+
+<p><hr>
+<h2>8. Web User Interface</h2>
+
+<p>The web interface is provided by a CGI script that can be
+run under any web server. A simple web server can easily be
+built on the standard <tt>CGIHTTPServer</tt> module, and
+should also be included in the distribution for quick
+out-of-the-box deployment.
+
+<p>The user interface is constructed from a number of template
+files containing mostly HTML. Among the HTML tags in templates
+are interspersed some nonstandard tags, which we use as
+placeholders to be replaced by properties and their values.
+
+<h3>8.1. Views and View Specifiers</h3>
+
+<p>There are two main kinds of views: index views and item views.
+An index view displays a list of items of a particular class,
+optionally sorted and filtered as requested. An item view
+presents the properties of a particular item for editing
+and displays the message spool for the item.
+
+<p>A <em>view specifier</em> is a string that specifies
+all the options needed to construct a particular view.
+It goes after the URL to the Roundup CGI script or the
+web server to form the complete URL to a view. When the
+result of selecting a link or submitting a form takes
+the user to a new view, the Web browser should be redirected
+to a canonical location containing a complete view specifier
+so that the view can be bookmarked.
+
+<h3>8.2. Displaying Properties</h3>
+
+<p>Properties appear in the user interface in three contexts:
+in indices, in editors, and as filters. For each type of
+property, there are several display possibilities. For example,
+in an index view, a string property may just be printed as
+a plain string, but in an editor view, that property should
+be displayed in an editable field.
+
+<p>The display of a property is handled by functions in
+a <tt>displayers</tt> module. Each function accepts at
+least three standard arguments -- the database, class name,
+and item id -- and returns a chunk of HTML.
+
+<p>Displayer functions are triggered by <tt><display></tt>
+tags in templates. The <tt>call</tt> attribute of the tag
+provides a Python expression for calling the displayer
+function. The three standard arguments are inserted in
+front of the arguments given. For example, the occurrence of
+
+<blockquote><pre><small
+> <display call="plain('status', max=30)">
+</small></pre></blockquote>
+
+in a template triggers a call to
+
+<blockquote><pre><small
+> plain(db, "issue", 13, "status", max=30)
+</small></pre></blockquote>
+
+when displaying item 13 in the "issue" class. The displayer
+functions can accept extra arguments to further specify
+details about the widgets that should be generated. By defining new
+displayer functions, the user interface can be highly customized.
+
+<p>Some of the standard displayer functions include:
+
+<ul>
+<li><strong>plain</strong>: display a String property directly;
+display a Date property in a specified time zone with an option
+to omit the time from the date stamp; for a Link or Multilink
+property, display the key strings of the linked items (or the
+ids if the linked class has no key property)
+
+<li><strong>field</strong>: display a property like the
+<strong>plain</strong> displayer above, but in a text field
+to be edited
+
+<li><strong>menu</strong>: for a Link property, display
+a menu of the available choices
+
+<li><strong>link</strong>: for a Link or Multilink property,
+display the names of the linked items, hyperlinked to the
+item views on those items
+
+<li><strong>count</strong>: for a Multilink property, display
+a count of the number of links in the list
+
+<li><strong>reldate</strong>: display a Date property in terms
+of an interval relative to the current date (e.g. "+ 3w", "- 2d").
+
+<li><strong>download</strong>: show a Link("file") or Multilink("file")
+property using links that allow you to download files
+
+<li><strong>checklist</strong>: for a Link or Multilink property,
+display checkboxes for the available choices to permit filtering
+</ul>
+
+<h3>8.3. Index Views</h3>
+
+<p>An index view contains two sections: a filter section
+and an index section.
+The filter section provides some widgets for selecting
+which items appear in the index. The index section is
+a table of items.
+
+<h4>8.3.1. Index View Specifiers</h4>
+
+<p>An index view specifier looks like this (whitespace
+has been added for clarity):
+
+<blockquote><pre><small
+>/issue?status=unread,in-progress,resolved&
+ topic=security,ui&
+ :group=+priority&
+ :sort=-activity&
+ :filters=status,topic&
+ :columns=title,status,fixer
+</small></pre></blockquote>
+
+<p>The index view is determined by two parts of the
+specifier: the layout part and the filter part.
+The layout part consists of the query parameters that
+begin with colons, and it determines the way that the
+properties of selected items are displayed.
+The filter part consists of all the other query parameters,
+and it determines the criteria by which items
+are selected for display.
+
+<p>The filter part is interactively manipulated with
+the form widgets displayed in the filter section. The
+layout part is interactively manipulated by clicking
+on the column headings in the table.
+
+<p>The filter part selects the <em>union</em> of the
+sets of items with values matching any specified Link
+properties and the <em>intersection</em> of the sets
+of items with values matching any specified Multilink
+properties.
+
+<p>The example specifies an index of "issue" items.
+Only items with a "status" of <em>either</em>
+"unread" or "in-progres" or "resolved" are displayed,
+and only items with "topic" values including <em>both</em>
+"security" <em>and</em> "ui" are displayed. The items
+are grouped by priority, arranged in ascending order;
+and within groups, sorted by activity, arranged in
+descending order. The filter section shows filters
+for the "status" and "topic" properties, and the
+table includes columns for the "title", "status", and
+"fixer" properties.
+
+<p>Associated with each item class is a default
+layout specifier. The layout specifier in the above
+example is the default layout to be provided with
+the default bug-tracker schema described above in
+section 4.4.
+
+<h4>8.3.2. Filter Section</h4>
+
+<p>The template for a filter section provides the
+filtering widgets at the top of the index view.
+Fragments enclosed in <tt><property></tt>...<tt></property></tt>
+tags are included or omitted depending on whether the
+view specifier requests a filter for a particular property.
+
+<p>Here's a simple example of a filter template.
+
+<blockquote><pre><small
+><property name=status>
+ <display call="checklist('status')">
+</property>
+<br>
+<property name=priority>
+ <display call="checklist('priority')">
+</property>
+<br>
+<property name=fixer>
+ <display call="menu('fixer')">
+</property></small></pre></blockquote>
+
+<h4>8.3.3. Index Section</h4>
+
+<p>The template for an index section describes one row of
+the index table.
+Fragments enclosed in <tt><property></tt>...<tt></property></tt>
+tags are included or omitted depending on whether the
+view specifier requests a column for a particular property.
+The table cells should contain <tt><display></tt> tags
+to display the values of the item's properties.
+
+<p>Here's a simple example of an index template.
+
+<blockquote><pre><small
+><tr>
+ <property name=title>
+ <td><display call="plain('title', max=50)"></td>
+ </property>
+ <property name=status>
+ <td><display call="plain('status')"></td>
+ </property>
+ <property name=fixer>
+ <td><display call="plain('fixer')"></td>
+ </property>
+</tr></small></pre></blockquote>
+
+<h4>8.3.4. Sorting</h4>
+
+<p>String and Date values are sorted in the natural way.
+Link properties are sorted according to the value of the
+"order" property on the linked items if it is present; or
+otherwise on the key string of the linked items; or
+finally on the item ids. Multilink properties are
+sorted according to how many links are present.
+
+<h3>8.4. Item Views</h3>
+
+<p>An item view contains an editor section and a spool section.
+At the top of an item view, links to superseding and superseded
+items are always displayed.
+
+<h4>8.4.1. Item View Specifiers</h4>
+
+<p>An item view specifier is simply the item's designator:
+
+<blockquote><pre><small
+>/patch23
+</small></pre></blockquote>
+
+<h4>8.4.2. Editor Section</h4>
+
+<p>The editor section is generated from a template
+containing <tt><display></tt> tags to insert
+the appropriate widgets for editing properties.
+
+<p>Here's an example of a basic editor template.
+
+<blockquote><pre><small
+><table>
+<tr>
+ <td colspan=2>
+ <display call="field('title', size=60)">
+ </td>
+</tr>
+<tr>
+ <td>
+ <display call="field('fixer', size=30)">
+ </td>
+ <td>
+ <display call="menu('status')>
+ </td>
+</tr>
+<tr>
+ <td>
+ <display call="field('nosy', size=30)">
+ </td>
+ <td>
+ <display call="menu('priority')>
+ </td>
+</tr>
+<tr>
+ <td colspan=2>
+ <display call="note()">
+ </td>
+</tr>
+</table>
+</small></pre></blockquote>
+
+<p>As shown in the example, the editor template can also
+request the display of a "note" field, which is a
+text area for entering a note to go along with a change.
+
+<p>When a change is submitted, the system automatically
+generates a message describing the changed properties.
+The message displays all of the property values on the
+item and indicates which ones have changed.
+An example of such a message might be this:
+
+<blockquote><pre><small
+>title: Polly Parrot is dead
+priority: critical
+status: unread -> in-progress
+fixer: (none)
+keywords: parrot,plumage,perch,nailed,dead
+</small></pre></blockquote>
+
+<p>If a note is given in the "note" field, the note is
+appended to the description. The message is then added
+to the item's message spool (thus triggering the standard
+detector to react by sending out this message to the nosy list).
+
+<h4>8.4.3. Spool Section</h4>
+
+<p>The spool section lists messages in the item's "messages"
+property. The index of messages displays the "date", "author",
+and "summary" properties on the message items, and selecting a
+message takes you to its content.
+
+<p><hr>
+<h2>9. Deployment Scenarios</h2>
+
+<p>The design described above should be general enough
+to permit the use of Roundup for bug tracking, managing
+projects, managing patches, or holding discussions. By
+using items of multiple types, one could deploy a system
+that maintains requirement specifications, catalogs bugs,
+and manages submitted patches, where patches could be
+linked to the bugs and requirements they address.
+
+<p><hr>
+<h2>10. Acknowledgements</h2>
+
+<p>My thanks are due to Christy Heyl for
+reviewing and contributing suggestions to this paper
+and motivating me to get it done, and to
+Jesse Vincent, Mark Miller, Christopher Simons,
+Jeff Dunmall, Wayne Gramlich, and Dean Tribble for
+their assistance with the first-round submission.
+</td>
+</tr>
+</table>
+
+<p>
+
+<center>
+<table>
+<tr>
+<td> <a href="http://www.software-carpentry.com/index.html"><b>[Home]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/faq.html"><b>[FAQ]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/license.html"><b>[License]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/contest-rules.html"><b>[Rules]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/sc_config/"><b>[Configure]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/sc_build/"><b>[Build]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/sc_test/"><b>[Test]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/sc_track/"><b>[Track]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/biblio.html"><b>[Resources]</b></a> </td>
+<td> <a href="http://www.software-carpentry.com/lists/"><b>[Archives]</b></a> </td>
+</tr>
+</table>
+</center>
+
+<p><hr>
+<center>Last modified 2001/04/06 11:50:59.9063 US/Mountain</center>
+</BODY>
+</HTML>
Added: tracker/vendor/roundup/current/doc/tracker_templates.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/tracker_templates.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,42 @@
+=========================
+Roundup Tracker Templates
+=========================
+
+:Version: $Revision: 1.2 $
+
+The templates distributed with Roundup are stored in the "share" directory
+nominated by Python. On Unix this is typically
+``/usr/share/roundup/templates/`` (or ``/usr/local/share...``) and
+on Windows this is ``c:\python22\share\roundup\templates\``.
+
+The template loading looks in four places to find the templates:
+
+1. *share* - eg. ``<prefix>/share/roundup/templates/*``.
+ This should be the standard place to find them when Roundup is
+ installed.
+2. ``<roundup.admin.__file__>/../templates/*``.
+ This will be used if Roundup's run in the distro (aka. source)
+ directory.
+3. ``<current working dir>/*``.
+ This is for when someone unpacks a 3rd-party template.
+4. ``<current working dir>``.
+ This is for someone who "cd"s to the 3rd-party template dir.
+
+Templates contain:
+
+- modules ``schema.py`` and ``initial_data.py``
+- directories ``html``, ``detectors`` and ``extensions``
+ (with appropriate contents)
+- template "marker" file ``TEMPLATE-INFO.txt``, which contains
+ the name of the template, a description of the template
+ and its intended audience.
+
+An example TEMPLATE-INFO.txt::
+
+ 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/vendor/roundup/current/doc/upgrading.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/upgrading.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,1309 @@
+======================================
+Upgrading to newer versions of Roundup
+======================================
+
+Please read each section carefully and edit your tracker home files
+accordingly. Note that there is information about upgrade procedures in the
+`administration guide`_.
+
+If a specific version transition isn't mentioned here (eg. 0.6.7 to 0.6.8)
+then you don't need to do anything. If you're upgrading from 0.5.6 to
+0.6.8 though, you'll need to check the "0.5 to 0.6" and "0.6.x to 0.6.3"
+steps.
+
+.. contents::
+
+Migrating from 1.1.0 to 1.1.1
+=============================
+
+1.1.1 "Clear this message"
+--------------------------
+
+In 1.1.1, the standard ``page.html`` template includes a "clear this message"
+link in the green "ok" message bar that appears after a successful edit
+(or other) action.
+
+To include this in your tracker, change the following in your ``page.html``
+template::
+
+ <p tal:condition="options/ok_message | nothing" class="ok-message"
+ tal:repeat="m options/ok_message" tal:content="structure m">error</p>
+
+to be::
+
+ <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>
+
+
+If you implemented the "clear this message" in your 1.1.0 tracker, then you
+should change it to the above and it will work much better!
+
+
+Migrating from 1.0.x to 1.1.0
+=============================
+
+1.1 Login "For Session Only"
+----------------------------
+
+In 1.1, web logins are alive for the length of a session only, *unless* you
+add the following to the login form in your tracker's ``page.html``::
+
+ <input type="checkbox" name="remember" id="remember">
+ <label for="remember" i18n:translate="">Remember me?</label><br>
+
+See the classic tracker ``page.html`` if you're unsure where this should
+go.
+
+
+1.1 Query Display Name
+----------------------
+
+The ``dispname`` web variable has been renamed ``@dispname`` to avoid
+clashing with other variables of the same name. If you are using the
+display name feature, you will need to edit your tracker's ``page.html``
+and ``issue.index.html`` pages to change ``dispname`` to ``@dispname``.
+
+A side-effect of this change is that the renderWith method used in the
+``home.html`` page may now take a dispname argument.
+
+
+1.1 "Clear this message"
+------------------------
+
+In 1.1, the standard ``page.html`` template includes a "clear this message"
+link in the green "ok" message bar that appears after a successful edit
+(or other) action.
+
+To include this in your tracker, change the following in your ``page.html``
+template::
+
+ <p tal:condition="options/ok_message | nothing" class="ok-message"
+ tal:repeat="m options/ok_message" tal:content="structure m">error</p>
+
+to be::
+
+ <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 string:issue${context/id}"
+ i18n:translate="">clear this message</a>
+ </p>
+
+
+Migrating from 0.8.x to 1.0
+===========================
+
+1.0 New Query Permissions
+-------------------------
+
+New permissions are defined for query editing and viewing. To include these
+in your tracker, you need to add these lines to your tracker's
+``schema.py``::
+
+ # 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='Create', klass='query',
+ description="User is allowed to create queries")
+ db.security.addPermissionToRole('User', p)
+
+and then remove 'query' from the line::
+
+ # Assign the access and edit Permissions for issue, file and message
+ # to regular users now
+ for cl in 'issue', 'file', 'msg', 'query', 'keyword':
+
+so it looks like::
+
+ for cl in 'issue', 'file', 'msg', 'keyword':
+
+
+Migrating from 0.8.0 to 0.8.3
+=============================
+
+0.8.3 Nosy Handling Changes
+---------------------------
+
+A change was made to fix a bug in the ``nosyreaction.py`` standard
+detector. To incorporate this fix in your trackers, you will need to copy
+the ``nosyreaction.py`` file from the ``templates/classic/detectors``
+directory of the source to your tracker's ``templates`` directory.
+
+If you have modified the ``nosyreaction.py`` file from the standard
+version, you will need to roll your changes into the new file.
+
+
+Migrating from 0.7.1 to 0.8.0
+=============================
+
+You *must* fully uninstall previous Roundup version before installing
+Roundup 0.8.0. If you don't do that, ``roundup-admin install``
+command may fail to function properly.
+
+0.8.0 Backend changes
+---------------------
+
+Backends 'bsddb' and 'bsddb3' are removed. If you are using one of these,
+you *must* migrate to another backend before upgrading.
+
+
+0.8.0 API changes
+-----------------
+
+Class.safeget() was removed from the API. Test your item ids before calling
+Class.get() instead.
+
+
+0.8.0 New tracker layout
+------------------------
+
+The ``config.py`` file has been replaced by ``config.ini``. You may use the
+roundup-admin command "genconfig" to generate a new config file::
+
+ roundup-admin genconfig <tracker home>/config.ini
+
+and modify the values therein based on the contents of your old config.py.
+In most cases, the names of the config variables are the same.
+
+The ``select_db.py`` file has been replaced by a file in the ``db``
+directory called ``backend_name``. As you might guess, this file contains
+just the name of the backend. To figure what the contents of yours should
+be, use the following table:
+
+ ================================ =========================
+ ``select_db.py`` contents ``backend_name`` contents
+ ================================ =========================
+ from back_anydbm import ... anydbm
+ from back_metakit import ... metakit
+ from back_sqlite import ... sqlite
+ from back_mysql import ... mysql
+ from back_postgresql import ... postgresql
+ ================================ =========================
+
+The ``dbinit.py`` file has been split into two new files,
+``initial_data.py`` and ``schema.py``. The contents of this file are:
+
+``initial_data.py``
+ You don't need one of these as your tracker is already initialised.
+
+``schema.py``
+ Copy the body of the ``def open(name=None)`` function from your old
+ tracker's ``dbinit.py`` file to this file. As the lines you're copying
+ aren't part of a function definition anymore, one level of indentation
+ needs to be removed (remove only the leading four spaces on each
+ line).
+
+ The first few lines -- those starting with ``from roundup.hyperdb
+ import ...`` and the ``db = Database(config, name)`` line -- don't
+ need to be copied. Neither do the last few lines -- those starting
+ with ``import detectors``, down to ``return db`` inclusive.
+
+You may remove the ``__init__.py`` module from the "detectors" directory as
+it is no longer used.
+
+There's a new way to write extension code for Roundup - the old
+``interfaces.py`` file will be ignored. See the `customisation
+documentation`_ for information about how extensions are now written.
+
+
+0.8.0 Permissions Changes
+-------------------------
+
+The creation of a new item in the user interfaces is now controlled by the
+"Create" Permission. You will need to add an assignment of this Permission
+to your users who are allowed to create items. The most common form of this
+is the following in your ``schema.py`` added just under the current
+assignation of the Edit Permission::
+
+ for cl in 'issue', 'file', 'msg', 'query', 'keyword':
+ p = db.security.getPermission('Create', cl)
+ db.security.addPermissionToRole('User', p)
+
+You will need to explicitly let anonymous users access the web interface so
+that regular users are able to see the login form. 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.
+It's enabled by adding to your ``schema.py``::
+
+ p = db.security.getPermission('Web Access')
+ db.security.addPermissionToRole('Anonymous', p)
+
+Finally, you will need to enable permission for your users to edit their
+own details by adding the following to ``schema.py``::
+
+ # Users should be able to edit their own details. Note that 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")
+ 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)
+
+
+0.8.0 Use of TemplatingUtils
+----------------------------
+
+If you used custom python functions in TemplatingUtils, they must
+be moved from interfaces.py to a new file in the ``extensions`` directory.
+
+Each Function that should be available through TAL needs to be defined
+as a toplevel function in the newly created file. Furthermore you
+add an inititialization function, that registers the functions with the
+tracker.
+
+If you find this too tedious, donfu wrote an automatic init function that
+takes an existing TemplatingUtils class, and registers all class methods
+that do not start with an underscore. The following hack should be placed
+in the ``extensions`` directory alongside other extensions::
+
+ class TemplatingUtils:
+ # copy from interfaces.py
+
+ def init(tracker):
+ util = TemplatingUtils()
+
+ def setClient(tu):
+ util.client = tu.client
+ return util
+
+ def execUtil(name):
+ return lambda tu, *args, **kwargs: \
+ getattr(setClient(tu), name)(*args, **kwargs)
+
+ for name in dir(util):
+ if callable(getattr(util, name)) and not name.startswith('_'):
+ tracker.registerUtil(name, execUtil(name))
+
+
+0.8.0 Logging Configuration
+---------------------------
+
+See the `administration guide`_ for information about configuring the new
+logging implemented in 0.8.0.
+
+
+Migrating from 0.7.2 to 0.7.3
+=============================
+
+0.7.3 Configuration
+-------------------
+
+If you choose, you may specify the directory from which static files are
+served (those which use the URL component ``@@file``). Currently the
+directory defaults to the ``TEMPLATES`` configuration variable. You may
+define a new variable, ``STATIC_FILES`` which overrides this value for
+static files.
+
+
+Migrating from 0.7.0 to 0.7.2
+=============================
+
+0.7.2 DEFAULT_TIMEZONE is now required
+--------------------------------------
+
+The DEFAULT_TIMEZONE configuration variable is now required. Add the
+following to your tracker's ``config.py`` file::
+
+ # You may specify a different default timezone, for use when users do not
+ # choose their own in their settings.
+ DEFAULT_TIMEZONE = 0 # specify as numeric hour offest
+
+
+Migrating from 0.7.0 to 0.7.1
+=============================
+
+0.7.1 Permission assignments
+----------------------------
+
+If you allow anonymous access to your tracker, you might need to assign
+some additional View (or Edit if your tracker is that open) permissions
+to the "anonymous" user. To do so, find the code in your ``dbinit.py`` that
+says::
+
+ for cl in 'issue', 'file', 'msg', 'query', 'keyword':
+ p = db.security.getPermission('View', cl)
+ db.security.addPermissionToRole('User', p)
+ p = db.security.getPermission('Edit', cl)
+ db.security.addPermissionToRole('User', p)
+ for cl in 'priority', 'status':
+ p = db.security.getPermission('View', cl)
+ db.security.addPermissionToRole('User', p)
+
+Add add a line::
+
+ db.security.addPermissionToRole('Anonymous', p)
+
+next to the existing ``'User'`` lines for the Permissions you wish to
+assign to the anonymous user.
+
+
+Migrating from 0.6 to 0.7
+=========================
+
+0.7.0 Permission assignments
+----------------------------
+
+Due to a change in the rendering of web widgets, permissions are now
+checked on Classes where they previously weren't (this is a good thing).
+
+You will need to add some additional Permission assignments for your
+regular users, or some displays will break. After the following in your
+tracker's ``dbinit.py``::
+
+ # Assign the access and edit Permissions for issue, file and message
+ # to regular users now
+ for cl in 'issue', 'file', 'msg', 'query', 'keyword':
+ p = db.security.getPermission('View', cl)
+ db.security.addPermissionToRole('User', p)
+ p = db.security.getPermission('Edit', cl)
+ db.security.addPermissionToRole('User', p)
+
+add::
+
+ for cl in 'priority', 'status':
+ p = db.security.getPermission('View', cl)
+ db.security.addPermissionToRole('User', p)
+
+
+0.7.0 Getting the current user id
+---------------------------------
+
+The Database.curuserid attribute has been removed.
+
+Any code referencing this attribute should be replaced with a
+call to Database.getuid().
+
+
+0.7.0 ZRoundup changes
+----------------------
+
+The templates in your tracker's html directory will need updating if you
+wish to use ZRoundup. If you've not modified those files (or some of them),
+you may just copy the new versions from the Roundup source in the
+templates/classic/html directory.
+
+If you have modified the html files, then you'll need to manually edit them
+to change all occurances of special form variables from using the colon ":"
+special character to the at "@" special character. That is, variables such
+as::
+
+ :action :required :template :remove:messages ...
+
+should become::
+
+ @action @required @template @remove at messages ...
+
+Note that ``tal:`` statements are unaffected. So are TAL expression type
+prefixes such as ``python:`` and ``string:``. Please ask on the
+roundup-users mailing list for help if you're unsure.
+
+
+0.7.0 Edit collision detection
+------------------------------
+
+Roundup now detects collisions with editing in the web interface (that is,
+two people editing the same item at the same time).
+
+You must copy the ``_generic.collision.html`` file from Roundup source in
+the ``templates/classic/html`` directory. to your tracker's ``html``
+directory.
+
+
+Migrating from 0.6.x to 0.6.3
+=============================
+
+0.6.3 Configuration
+-------------------
+
+You will need to copy the file::
+
+ templates/classic/detectors/__init__.py
+
+to your tracker's ``detectors`` directory, replacing the one already there.
+This fixes a couple of bugs in that file.
+
+
+
+Migrating from 0.5 to 0.6
+=========================
+
+
+0.6.0 Configuration
+-------------------
+
+Introduced EMAIL_FROM_TAG config variable. This value is inserted into
+the From: line of nosy email. If the sending user is "Foo Bar", the
+From: line is usually::
+
+ "Foo Bar" <issue_tracker at tracker.example>
+
+the EMAIL_FROM_TAG goes inside the "Foo Bar" quotes like so::
+
+ "Foo Bar EMAIL_FROM_TAG" <issue_tracker at tracker.example>
+
+I've altered the mechanism in the detectors __init__.py module so that it
+doesn't cross-import detectors from other trackers (if you run more than one
+in a single roundup-server). This change means that you'll need to copy the
+__init__.py from roundup/templates/classic/detectors/__init__.py to your
+<tracker home>/detectors/__init__.py. Don't worry, the "classic" __init__ is a
+one-size-fits-all, so it'll work even if you've added/removed detectors.
+
+0.6.0 Templating changes
+------------------------
+
+The ``user.item`` template (in the tracker home "templates" directory)
+needs to have the following hidden variable added to its form (between the
+``<form...>`` and ``</form>`` tags::
+
+ <input type="hidden" name=":template" value="item">
+
+
+0.6.0 Form handling changes
+---------------------------
+
+Roundup's form handling capabilities have been significantly expanded. This
+should not affect users of 0.5 installations - but if you find you're
+getting errors from form submissions, please ask for help on the Roundup
+users mailing list:
+
+ http://lists.sourceforge.net/lists/listinfo/roundup-users
+
+See the customisation doc section on `Form Values`__ for documentation of the
+new form variables possible.
+
+__ customizing.html#form-values
+
+
+0.6.0 Multilingual character set support
+----------------------------------------
+
+Added internationalization support. This is done via encoding all data
+stored in roundup database to utf-8 (unicode encoding). To support utf-8 in
+web interface you should add the folowing line to your tracker's html/page
+and html/_generic.help files inside <head> tag::
+
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+Since latin characters in utf-8 have the same codes as in ASCII table, this
+modification is optional for users who use only plain latin characters.
+
+After this modification, you will be able to see and enter any world
+character via web interface. Data received via mail interface also converted
+to utf-8, however only new messages will be converted. If your roundup
+database contains some of non-ASCII characters in one of 8-bit encoding,
+they will not be visible in new unicode environment. Some of such data (e.g.
+user names, keywords, etc) can be edited by administrator, the others
+(e.g. messages' contents) is not editable via web interface. Currently there
+is no tool for converting such data, the only solution is to close
+appropriate old issues and create new ones with the same content.
+
+
+0.6.0 User timezone support
+---------------------------
+
+From version 0.6.0 roundup supports displaying of Date data in user' local
+timezone if he/she has provided timezone information. To make it possible
+some modification to tracker's schema and HTML templates are required.
+First you must add string property 'timezone' to user class in dbinit.py
+like this::
+
+ user = Class(db, "user",
+ username=String(), password=Password(),
+ address=String(), realname=String(),
+ phone=String(), organisation=String(),
+ alternate_addresses=String(),
+ queries=Multilink('query'), roles=String(),
+ timezone=String())
+
+And second - html interface. Add following lines to
+$TRACKER_HOME/html/user.item template::
+
+ <tr>
+ <th>Timezone</th>
+ <td tal:content="structure context/timezone/field">timezone</td>
+ </tr>
+
+After that all users should be able to provide their timezone information.
+Timezone should be a positive or negative integer - offset from GMT.
+
+After providing timezone, roundup will show all dates values, found in web
+and mail interfaces in local time. It will also accept any Date info in
+local time, convert and store it in GMT.
+
+
+0.6.0 Search page structure
+---------------------------
+
+In order to accomodate query editing the search page has been restructured. If
+you want to provide your users with query editing, you should update your
+search page using the macros detailed in the customisation doc section
+`Searching on categories`__.
+
+__ customizing.html#searching-on-categories
+
+Also, the url field in the query class no longer starts with a '?'. You'll need
+to remove this question mark from the url field to support queries. There's
+a script in the "tools" directory called ``migrate-queries.py`` that should
+automatically change any existing queries for you. As always, make a backup
+of your database before running such a script.
+
+
+0.6.0 Notes for metakit backend users
+-------------------------------------
+
+Roundup 0.6.0 introduced searching on ranges of dates and intervals. To
+support it, some modifications to interval storing routine were made. So if
+your tracker uses metakit backend and your db schema contains intervals
+property, searches on that property will not be accurate for db items that
+was stored before roundup' upgrade. However all new records should be
+searchable on intervals.
+
+It is possible to convert your database to new format: you can export and
+import back all your data (consult "Migrating backends" in "Maintenance"
+documentation). After this operation all your interval properties should
+become searchable.
+
+Users of backends others than metakit should not worry about this issue.
+
+
+Migrating from 0.4.x to 0.5.0
+=============================
+
+This has been a fairly major revision of Roundup:
+
+1. Brand new, much more powerful, flexible, tasty and nutritious templating.
+ Unfortunately, this means all your current templates are useless. Hopefully
+ the new documentation and examples will be enough to help you make the
+ transition. Please don't hesitate to ask on roundup-users for help (or
+ complete conversions if you're completely stuck)!
+2. The database backed got a lot more flexible, allowing Metakit and SQL
+ databases! The only decent SQL database implemented at present is sqlite,
+ but others shouldn't be a whole lot more work.
+3. A brand new, highly flexible and much more robust security system including
+ a system of Permissions, Roles and Role assignments to users. You may now
+ define your own Permissions that may be checked in CGI transactions.
+4. Journalling has been made less storage-hungry, so has been turned on
+ by default *except* for author, recipient and nosy link/unlink events. You
+ are advised to turn it off in your trackers too.
+5. We've changed the terminology from "instance" to "tracker", to ease the
+ learning curve/impact for new users.
+6. Because of the above changes, the tracker configuration has seen some
+ major changes. See below for the details.
+
+Please, **back up your database** before you start the migration process. This
+is as simple as copying the "db" directory and all its contents from your
+tracker to somewhere safe.
+
+
+0.5.0 Configuration
+-------------------
+
+First up, rename your ``instance_config.py`` file to just ``config.py``.
+
+Then edit your tracker's ``__init__.py`` module. It'll currently look
+like this::
+
+ from instance_config import *
+ try:
+ from dbinit import *
+ except ImportError:
+ pass # in installdir (probably :)
+ from interfaces import *
+
+and it needs to be::
+
+ import config
+ from dbinit import open, init
+ from interfaces import Client, MailGW
+
+Due to the new templating having a top-level ``page`` that defines links for
+searching, indexes, adding items etc, the following variables are no longer
+used:
+
+- HEADER_INDEX_LINKS
+- HEADER_ADD_LINKS
+- HEADER_SEARCH_LINKS
+- SEARCH_FILTERS
+- DEFAULT_INDEX
+- UNASSIGNED_INDEX
+- USER_INDEX
+- ISSUE_FILTER
+
+The new security implementation will require additions to the dbinit module,
+but also removes the need for the following tracker config variables:
+
+- ANONYMOUS_ACCESS
+- ANONYMOUS_REGISTER
+
+but requires two new variables which define the Roles assigned to users who
+register through the web and e-mail interfaces:
+
+- NEW_WEB_USER_ROLES
+- NEW_EMAIL_USER_ROLES
+
+in both cases, 'User' is a good initial setting. To emulate
+``ANONYMOUS_ACCESS='deny'``, remove all "View" Permissions from the
+"Anonymous" Role. To emulate ``ANONYMOUS_REGISTER='deny'``, remove the "Web
+Registration" and/or the "Email Registration" Permission from the "Anonymous"
+Role. See the section on customising security in the `customisation
+documentation`_ for more information.
+
+Finally, the following config variables have been renamed to make more sense:
+
+- INSTANCE_HOME -> TRACKER_HOME
+- INSTANCE_NAME -> TRACKER_NAME
+- ISSUE_TRACKER_WEB -> TRACKER_WEB
+- ISSUE_TRACKER_EMAIL -> TRACKER_EMAIL
+
+
+0.5.0 Schema Specification
+--------------------------
+
+0.5.0 Database backend changes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Your select_db module in your tracker has changed a fair bit. Where it used
+to contain::
+
+ # WARNING: DO NOT EDIT THIS FILE!!!
+ from roundup.backends.back_anydbm import Database
+
+it must now contain::
+
+ # WARNING: DO NOT EDIT THIS FILE!!!
+ from roundup.backends.back_anydbm import Database, Class, FileClass, IssueClass
+
+Yes, I realise the irony of the "DO NOT EDIT THIS FILE" statement :)
+Note the addition of the Class, FileClass, IssueClass imports. These are very
+important, as they're going to make the next change work too. You now need to
+modify the top of the dbinit module in your tracker from::
+
+ import instance_config
+ from roundup import roundupdb
+ from select_db import Database
+
+ from roundup.roundupdb import Class, FileClass
+
+ class Database(roundupdb.Database, select_db.Database):
+ ''' Creates a hybrid database from:
+ . the selected database back-end from select_db
+ . the roundup extensions from roundupdb
+ '''
+ pass
+
+ class IssueClass(roundupdb.IssueClass):
+ ''' issues need the email information
+ '''
+ pass
+
+to::
+
+ import config
+ from select_db import Database, Class, FileClass, IssueClass
+
+Yes, remove the Database and IssueClass definitions and those other imports.
+They're not needed any more!
+
+Look for places in dbinit.py where ``instance_config`` is used too, and
+rename them ``config``.
+
+
+0.5.0 Journalling changes
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Journalling has been optimised for storage. Journalling of links has been
+turned back on by default. If your tracker has a large user base, you may wish
+to turn off journalling of nosy list, message author and message recipient
+link and unlink events. You do this by adding ``do_journal='no'`` to the Class
+initialisation in your dbinit. For example, your *msg* class initialisation
+probably looks like this::
+
+ msg = FileClass(db, "msg",
+ author=Link("user"), recipients=Multilink("user"),
+ date=Date(), summary=String(),
+ files=Multilink("file"),
+ messageid=String(), inreplyto=String())
+
+to turn off journalling of author and recipient link events, add
+``do_journal='no'`` to the ``author=Link("user")`` part of the statement,
+like so::
+
+ 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())
+
+Nosy list link event journalling is actually turned off by default now. If you
+want to turn it on, change to your issue class' nosy list, change its
+definition from::
+
+ issue = IssueClass(db, "issue",
+ assignedto=Link("user"), topic=Multilink("keyword"),
+ priority=Link("priority"), status=Link("status"))
+
+to::
+
+ issue = IssueClass(db, "issue", nosy=Multilink("user", do_journal='yes'),
+ assignedto=Link("user"), topic=Multilink("keyword"),
+ priority=Link("priority"), status=Link("status"))
+
+noting that your definition of the nosy Multilink will override the normal one.
+
+
+0.5.0 User schema changes
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Users have two more properties, "queries" and "roles". You'll have something
+like this in your dbinit module now::
+
+ user = Class(db, "user",
+ username=String(), password=Password(),
+ address=String(), realname=String(),
+ phone=String(), organisation=String(),
+ alternate_addresses=String())
+ user.setkey("username")
+
+and you'll need to add the new properties and the new "query" class to it
+like so::
+
+ query = Class(db, "query",
+ klass=String(), name=String(),
+ url=String())
+ query.setkey("name")
+
+ # Note: roles is a comma-separated string of Role names
+ user = Class(db, "user",
+ username=String(), password=Password(),
+ address=String(), realname=String(),
+ phone=String(), organisation=String(),
+ alternate_addresses=String(),
+ queries=Multilink('query'), roles=String())
+ user.setkey("username")
+
+The "queries" property is used to store off the user's favourite database
+queries. The "roles" property is explained below in `0.5.0 Security
+Settings`_.
+
+
+0.5.0 Security Settings
+~~~~~~~~~~~~~~~~~~~~~~~
+
+See the `security documentation`_ for an explanation of how the new security
+system works. In a nutshell though, the security is handled as a four step
+process:
+
+1. Permissions are defined as having a name and optionally a hyperdb class
+ they're specific to,
+2. Roles are defined that have one or more Permissions,
+3. Users are assigned Roles in their "roles" property, and finally
+4. Roundup checks that users have appropriate Permissions at appropriate times
+ (like editing issues).
+
+Your tracker dbinit module's *open* function now has to define any
+Permissions that are specific to your tracker, and also the assignment
+of Permissions to Roles. At the moment, your open function
+ends with::
+
+ import detectors
+ detectors.init(db)
+
+ return db
+
+and what we need to do is insert some commands that will set up the security
+parameters. Right above the ``import detectors`` line, you'll want to insert
+these lines::
+
+ #
+ # SECURITY SETTINGS
+ #
+ # new permissions for this schema
+ for cl in 'issue', 'file', 'msg', 'user':
+ db.security.addPermission(name="Edit", klass=cl,
+ description="User is allowed to edit "+cl)
+ db.security.addPermission(name="View", klass=cl,
+ description="User is allowed to access "+cl)
+
+ # Assign the access and edit permissions for issue, file and message
+ # to regular users now
+ for cl in 'issue', 'file', 'msg':
+ p = db.security.getPermission('View', cl)
+ db.security.addPermissionToRole('User', p)
+ p = db.security.getPermission('Edit', cl)
+ db.security.addPermissionToRole('User', p)
+ # and give the regular users access to the web and email interface
+ p = db.security.getPermission('Web Access')
+ db.security.addPermissionToRole('User', p)
+ p = db.security.getPermission('Email Access')
+ db.security.addPermissionToRole('User', p)
+
+ # May users view other user information? Comment these lines out
+ # if you don't want them to
+ p = db.security.getPermission('View', 'user')
+ db.security.addPermissionToRole('User', p)
+
+ # Assign the appropriate permissions to the anonymous user's Anonymous
+ # Role. Choices here are:
+ # - Allow anonymous users to register through the web
+ p = db.security.getPermission('Web Registration')
+ db.security.addPermissionToRole('Anonymous', p)
+ # - Allow anonymous (new) users to register through the email gateway
+ p = db.security.getPermission('Email Registration')
+ db.security.addPermissionToRole('Anonymous', p)
+ # - Allow anonymous users access to the "issue" class of data
+ # Note: this also grants access to related information like files,
+ # messages, statuses etc that are linked to issues
+ #p = db.security.getPermission('View', 'issue')
+ #db.security.addPermissionToRole('Anonymous', p)
+ # - Allow anonymous users access to edit the "issue" class of data
+ # Note: this also grants access to create related information like
+ # files and messages etc that are linked to issues
+ #p = db.security.getPermission('Edit', 'issue')
+ #db.security.addPermissionToRole('Anonymous', p)
+
+ # oh, g'wan, let anonymous access the web interface too
+ p = db.security.getPermission('Web Access')
+ db.security.addPermissionToRole('Anonymous', p)
+
+Note in the comments there the places where you might change the permissions
+to restrict users or grant users more access. If you've created additional
+classes that users should be able to edit and view, then you should add them
+to the "new permissions for this schema" section at the start of the security
+block. Then add them to the "Assign the access and edit permissions" section
+too, so people actually have the new Permission you've created.
+
+One final change is needed that finishes off the security system's
+initialisation. We need to add a call to ``db.post_init()`` at the end of the
+dbinit open() function. Add it like this::
+
+ import detectors
+ detectors.init(db)
+
+ # schema is set up - run any post-initialisation
+ db.post_init()
+ return db
+
+You may verify the setup of Permissions and Roles using the new
+"``roundup-admin security``" command.
+
+
+0.5.0 User changes
+~~~~~~~~~~~~~~~~~~
+
+To support all those schema changes, you'll need to massage your user database
+a little too, to:
+
+1. make sure there's an "anonymous" user - this user is mandatory now and is
+ the one that unknown users are logged in as.
+2. make sure all users have at least one Role.
+
+If you don't have the "anonymous" user, create it now with the command::
+
+ roundup-admin create user username=anonymous roles=Anonymous
+
+making sure the capitalisation is the same as above. Once you've done that,
+you'll need to set the roles property on all users to a reasonable default.
+The admin user should get "Admin", the anonymous user "Anonymous"
+and all other users "User". The ``fixroles.py`` script in the tools directory
+will do this. Run it like so (where python is your python 2+ binary)::
+
+ python tools/fixroles.py -i <tracker home> fixroles
+
+
+
+0.5.0 CGI interface changes
+---------------------------
+
+The CGI interface code was completely reorganised and largely rewritten. The
+end result is that this section of your tracker interfaces module will need
+changing from::
+
+ from roundup import cgi_client, mailgw
+ from roundup.i18n import _
+
+ class Client(cgi_client.Client):
+ ''' derives basic CGI implementation from the standard module,
+ with any specific extensions
+ '''
+ pass
+
+to::
+
+ from roundup import mailgw
+ from roundup.cgi import client
+
+ class Client(client.Client):
+ ''' derives basic CGI implementation from the standard module,
+ with any specific extensions
+ '''
+ pass
+
+You will also need to install the new version of roundup.cgi from the source
+cgi-bin directory if you're using it.
+
+
+0.5.0 HTML templating
+---------------------
+
+You'll want to make a backup of your current tracker html directory. You
+should then copy the html directory from the Roundup source "classic" template
+and modify it according to your local schema changes.
+
+If you need help with the new templating system, please ask questions on the
+roundup-users mailing list (available through the roundup project page on
+sourceforge, http://roundup.sf.net/)
+
+
+0.5.0 Detectors
+---------------
+
+The nosy reactor has been updated to handle the tracker not having an
+"assignedto" property on issues. You may want to copy it into your tracker's
+detectors directory. Chances are you've already fixed it though :)
+
+
+Migrating from 0.4.1 to 0.4.2
+=============================
+
+0.4.2 Configuration
+-------------------
+The USER_INDEX definition introduced in 0.4.1 was too restrictive in its
+allowing replacement of 'assignedto' with the user's userid. Users must change
+the None value of 'assignedto' to 'CURRENT USER' (the string, in quotes) for
+the replacement behaviour to occur now.
+
+The new configuration variables are:
+
+- EMAIL_KEEP_QUOTED_TEXT
+- EMAIL_LEAVE_BODY_UNCHANGED
+- ADD_RECIPIENTS_TO_NOSY
+
+See the sample configuration files in::
+
+ <roundup source>/roundup/templates/classic/instance_config.py
+
+and::
+
+ <roundup source>/roundup/templates/extended/instance_config.py
+
+and the `customisation documentation`_ for information on how they're used.
+
+
+0.4.2 Changes to detectors
+--------------------------
+You will need to copy the detectors from the distribution into your instance
+home "detectors" directory. If you used the classic schema, the detectors
+are in::
+
+ <roundup source>/roundup/templates/classic/detectors/
+
+If you used the extended schema, the detectors are in::
+
+ <roundup source>/roundup/templates/extended/detectors/
+
+The change means that schema-specific code has been removed from the
+mail gateway and cgi interface and made into auditors:
+
+- nosyreactor.py has now got an updatenosy auditor which updates the nosy
+ list with author, recipient and assignedto information.
+- statusauditor.py makes the unread or resolved -> chatting changes and
+ presets the status of an issue to unread.
+
+There's also a bug or two fixed in the nosyreactor code.
+
+0.4.2 HTML templating changes
+-----------------------------
+The link() htmltemplate function now has a "showid" option for links and
+multilinks. When true, it only displays the linked item id as the anchor
+text. The link value is displayed as a tooltip using the title anchor
+attribute. To use in eg. the superseder field, have something like this::
+
+ <td>
+ <display call="field('superseder', showid=1)">
+ <display call="classhelp('issue', 'id,title', label='list', width=500)">
+ <property name="superseder">
+ <br>View: <display call="link('superseder', showid=1)">
+ </property>
+ </td>
+
+The stylesheets have been cleaned up too. You may want to use the newer
+versions in::
+
+ <roundup source>/roundup/templates/<template>/html/default.css
+
+
+
+Migrating from 0.4.0 to 0.4.1
+=============================
+
+0.4.1 Files storage
+-------------------
+
+Messages and files from newly created issues will be put into subdierectories
+in thousands e.g. msg123 will be put into files/msg/0/msg123, file2003
+will go into files/file/2/file2003. Previous messages are still found, but
+could be put into this structure.
+
+0.4.1 Configuration
+-------------------
+
+To allow more fine-grained access control, the variable used to check
+permission to auto-register users in the mail gateway is now called
+ANONYMOUS_REGISTER_MAIL rather than overloading ANONYMOUS_REGISTER. If the
+variable doesn't exist, then ANONYMOUS_REGISTER is tested as before.
+
+Configuring the links in the web header is now easier too. The following
+variables have been added to the classic instance_config.py::
+
+ HEADER_INDEX_LINKS - defines the "index" links to be made available
+ HEADER_ADD_LINKS - defines the "add" links
+ DEFAULT_INDEX - specifies the index view for DEFAULT
+ UNASSIGNED_INDEX - specifies the index view for UNASSIGNED
+ USER_INDEX - specifies the index view for USER
+
+See the <roundup source>/roundup/templates/classic/instance_config.py for more
+information - including how the variables are to be set up. Most users will
+just be able to copy the variables from the source to their instance home. If
+you've modified the header by changing the source of the interfaces.py file in
+the instance home, you'll need to remove that customisation and move it into
+the appropriate variables in instance_config.py.
+
+The extended schema has similar variables added too - see the source for more
+info.
+
+0.4.1 Alternate E-Mail Addresses
+--------------------------------
+
+If you add the property "alternate_addresses" to your user class, your users
+will be able to register alternate email addresses that they may use to
+communicate with roundup as. All email from roundup will continue to be sent
+to their primary address.
+
+If you have not edited the dbinit.py file in your instance home directory,
+you may simply copy the new dbinit.py file from the core code. If you used
+the classic schema, the interfaces file is in::
+
+ <roundup source>/roundup/templates/classic/dbinit.py
+
+If you used the extended schema, the file is in::
+
+ <roundup source>/roundup/templates/extended/dbinit.py
+
+If you have modified your dbinit.py file, you need to edit the dbinit.py
+file in your instance home directory. Find the lines which define the user
+class::
+
+ user = Class(db, "msg",
+ username=String(), password=Password(),
+ address=String(), realname=String(),
+ phone=String(), organisation=String(),
+ alternate_addresses=String())
+
+You will also want to add the property to the user's details page. The
+template for this is the "user.item" file in your instance home "html"
+directory. Similar to above, you may copy the file from the roundup source if
+you haven't modified it. Otherwise, add the following to the template::
+
+ <display call="multiline('alternate_addresses')">
+
+with appropriate labelling etc. See the standard template for an idea.
+
+
+
+Migrating from 0.3.x to 0.4.0
+=============================
+
+0.4.0 Message-ID and In-Reply-To addition
+-----------------------------------------
+0.4.0 adds the tracking of messages by message-id and allows threading
+using in-reply-to. Most e-mail clients support threading using this
+feature, and we hope to add support for it to the web gateway. If you
+have not edited the dbinit.py file in your instance home directory, you may
+simply copy the new dbinit.py file from the core code. If you used the
+classic schema, the interfaces file is in::
+
+ <roundup source>/roundup/templates/classic/dbinit.py
+
+If you used the extended schema, the file is in::
+
+ <roundup source>/roundup/templates/extended/dbinit.py
+
+If you have modified your dbinit.py file, you need to edit the dbinit.py
+file in your instance home directory. Find the lines which define the msg
+class::
+
+ msg = FileClass(db, "msg",
+ author=Link("user"), recipients=Multilink("user"),
+ date=Date(), summary=String(),
+ files=Multilink("file"))
+
+and add the messageid and inreplyto properties like so::
+
+ msg = FileClass(db, "msg",
+ author=Link("user"), recipients=Multilink("user"),
+ date=Date(), summary=String(),
+ files=Multilink("file"),
+ messageid=String(), inreplyto=String())
+
+Also, configuration is being cleaned up. This means that your dbinit.py will
+also need to be changed in the open function. If you haven't changed your
+dbinit.py, the above copy will be enough. If you have, you'll need to change
+the line (round line 50)::
+
+ db = Database(instance_config.DATABASE, name)
+
+to::
+
+ db = Database(instance_config, name)
+
+
+0.4.0 Configuration
+--------------------
+``TRACKER_NAME`` and ``EMAIL_SIGNATURE_POSITION`` have been added to the
+instance_config.py. The simplest solution is to copy the default values
+from template in the core source.
+
+The mail gateway now checks ``ANONYMOUS_REGISTER`` to see if unknown users
+are to be automatically registered with the tracker. If it is set to "deny"
+then unknown users will not have access. If it is set to "allow" they will be
+automatically registered with the tracker.
+
+
+0.4.0 CGI script roundup.cgi
+----------------------------
+The CGI script has been updated with some features and a bugfix, so you should
+copy it from the roundup cgi-bin source directory again. Make sure you update
+the ROUNDUP_INSTANCE_HOMES after the copy.
+
+
+0.4.0 Nosy reactor
+------------------
+The nosy reactor has also changed - copy the nosyreactor.py file from the core
+source::
+
+ <roundup source>/roundup/templates/<template>/detectors/nosyreactor.py
+
+to your instance home "detectors" directory.
+
+
+0.4.0 HTML templating
+---------------------
+The field() function was incorrectly implemented - links and multilinks now
+display as text fields when rendered using field(). To display a menu (drop-
+down or select box) you need to use the menu() function.
+
+
+
+Migrating from 0.2.x to 0.3.x
+=============================
+
+0.3.x Cookie Authentication changes
+-----------------------------------
+0.3.0 introduces cookie authentication - you will need to copy the
+interfaces.py file from the roundup source to your instance home to enable
+authentication. If you used the classic schema, the interfaces file is in::
+
+ <roundup source>/roundup/templates/classic/interfaces.py
+
+If you used the extended schema, the file is in::
+
+ <roundup source>/roundup/templates/extended/interfaces.py
+
+If you have modified your interfaces.Client class, you will need to take
+note of the login/logout functionality provided in roundup.cgi_client.Client
+(classic schema) or roundup.cgi_client.ExtendedClient (extended schema) and
+modify your instance code apropriately.
+
+
+0.3.x Password encoding
+-----------------------
+This release also introduces encoding of passwords in the database. If you
+have not edited the dbinit.py file in your instance home directory, you may
+simply copy the new dbinit.py file from the core code. If you used the
+classic schema, the interfaces file is in::
+
+ <roundup source>/roundup/templates/classic/dbinit.py
+
+If you used the extended schema, the file is in::
+
+ <roundup source>/roundup/templates/extended/dbinit.py
+
+
+If you have modified your dbinit.py file, you may use encoded passwords:
+
+1. Edit the dbinit.py file in your instance home directory
+ a. At the first code line of the open() function::
+
+ from roundup.hyperdb import String, Date, Link, Multilink
+
+ alter to include Password, as so::
+
+ from roundup.hyperdb import String, Password, Date, Link, Multilink
+
+ b. Where the password property is defined (around line 66)::
+
+ user = Class(db, "user",
+ username=String(), password=String(),
+ address=String(), realname=String(),
+ phone=String(), organisation=String())
+ user.setkey("username")
+
+ alter the "password=String()" to "password=Password()"::
+
+ user = Class(db, "user",
+ username=String(), password=Password(),
+ address=String(), realname=String(),
+ phone=String(), organisation=String())
+ user.setkey("username")
+
+2. Any existing passwords in the database will remain cleartext until they
+ are edited. It is recommended that at a minimum the admin password be
+ changed immediately::
+
+ roundup-admin -i <instance home> set user1 password=<new password>
+
+
+0.3.x Configuration
+-------------------
+FILTER_POSITION, ANONYMOUS_ACCESS, ANONYMOUS_REGISTER have been added to
+the instance_config.py. Simplest solution is to copy the default values from
+template in the core source.
+
+MESSAGES_TO_AUTHOR has been added to the IssueClass in dbinit.py. Set to 'yes'
+to send nosy messages to the author. Default behaviour is to not send nosy
+messages to the author. You will need to add MESSAGES_TO_AUTHOR to your
+dbinit.py in your instance home.
+
+
+0.3.x CGI script roundup.cgi
+----------------------------
+There have been some structural changes to the roundup.cgi script - you will
+need to install it again from the cgi-bin directory of the source
+distribution. Make sure you update the ROUNDUP_INSTANCE_HOMES after the
+copy.
+
+
+.. _`customisation documentation`: customizing.html
+.. _`security documentation`: security.html
+.. _`administration guide`: admin_guide.html
Added: tracker/vendor/roundup/current/doc/user_guide.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/user_guide.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,787 @@
+==========
+User Guide
+==========
+
+:Version: $Revision: 1.32 $
+
+.. contents::
+
+.. hint::
+ This document will refer to *issues* as the primary store of
+ information in the tracker. This is the default of the classic template,
+ but may vary in any given installation.
+
+
+Your Tracker in a Nutshell
+==========================
+
+Your tracker holds information about issues in bundles we call *items*.
+An item may be an *issue* (a bug or feature request) or a *user*. The
+issue-ness or user-ness is called the item's *class*. So, for bug
+reports and features, the class is "issue", and for users the class is
+"user".
+
+Each item in the tracker has an id number that identifies it along with
+its item class. To identify a particular issue or user, we combine the
+class with the number to create a unique label, so that user 1 (who,
+incidentally, is *always* the "admin" user) is referred to as "user1".
+Issue number 315 is referred to as "issue315". We call that label the
+item's *designator*.
+
+Items in the database are never deleted, they're just "retired". You
+can still refer to them by ID - hence removing an item won't break
+references to the item. It's just that the item won't appear in any
+listings.
+
+
+Accessing the Tracker
+---------------------
+
+You may access your tracker through one of three ways:
+
+1. through the `web interface`_,
+2. through the `e-mail gateway`_, or
+3. using the `command line tool`_.
+
+The last is usually only used by administrators. Most users will use the
+web and e-mail interfaces. All three are explained below.
+
+
+Issue life cycles in Roundup
+----------------------------
+
+New issues may be submitted via the web or e-mail.
+
+By default, the issue will have the status "unread". If another message
+is received for the issue, its status will change to "chatting".
+
+The "home" page for a tracker will generally display all issues which
+are not "resolved".
+
+If an issue is closed, and a new message is received then it'll be
+reopened to the state of "chatting".
+
+
+Entering values in your Tracker
+-------------------------------
+
+All interfaces to your tracker use the same format for entering values.
+This means the web interface for entering a new issue, the web interface
+for searching issues, the e-mail interface and even the command-line
+administration tool.
+
+
+String and Numeric properties
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+These fields just take a simple text value, like ``It's broken``.
+
+
+Boolean properties
+~~~~~~~~~~~~~~~~~~
+
+These fields take a value which indicates "yes"/"no", "true"/"false",
+"1"/"0" or "on"/"off".
+
+
+Constrained (link and multilink) properties
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Fields like "Assigned To" and "Topics" hold references to items in other
+classes ("user" and "keyword" in those two cases.)
+
+Sometimes, the selection is done through a menu, like in the "Assigned
+To" field.
+
+Where the input is not a simple menu selection, we use a comma-separated
+list of values to indicated which values of "user" or "keyword" are
+interesting. The values may be either numeric ids or the names of items.
+The special value "-1" may be used to match items where the property is
+not set. For example, the following searches on the issues:
+
+``assignedto=richard,george``
+ match issues which are assigned to richard or george.
+``assignedto=-1``
+ match issues that are not assigned to a user.
+``assignedto=2,3,40``
+ match issues that are assigned to users 2, 3 or 40.
+``topic=user interface``
+ match issues with the keyword "user interface" in their topic list
+``topic=web interface,e-mail interface``
+ match issues with the keyword "web interface" or "e-mail interface" in
+ their topic list
+``topic=-1``
+ match issues with no topics set
+
+
+Date properties
+~~~~~~~~~~~~~~~
+
+Date-and-time stamps are specified with the date in
+international standard format (``yyyy-mm-dd``) joined to the time
+(``hh:mm:ss``) by a period ``.``. Dates in this form can be easily
+compared and are fairly readable when printed. An example of a valid
+stamp is ``2000-06-24.13:03:59``. We'll call this the "full date
+format". When Timestamp objects are printed as strings, they appear in
+the full date format.
+
+For user input, some partial forms are also permitted: the whole time or
+just the seconds may be omitted; and the whole date may be omitted or
+just the year may be omitted. If the time is given, the time is
+interpreted in the user's local time zone. The Date constructor takes
+care of these conversions. In the following examples, suppose that
+``yyyy`` is the current year, ``mm`` is the current month, and ``dd`` is
+the current day of the month.
+
+- "2000-04-17" means <Date 2000-04-17.00:00:00>
+- "01-25" means <Date yyyy-01-25.00:00:00>
+- "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
+- "08-13.22:13" means <Date yyyy-08-14.03:13:00>
+- "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
+- "14:25" means
+- <Date yyyy-mm-dd.19:25:00>
+- "8:47:11" means
+- <Date yyyy-mm-dd.13:47:11>
+- the special date "." means "right now"
+
+
+When searching, a plain date entered as a search field will match that date
+exactly in the database. We may also accept ranges of dates. You can
+specify range of dates in one of two formats:
+
+1. English syntax::
+
+ [From <value>][To <value>]
+
+ Keywords "From" and "To" are case insensitive. Keyword "From" is
+ optional.
+
+2. "Geek" syntax::
+
+ [<value>];[<value>]
+
+Either first or second ``<value>`` can be omitted in both syntaxes.
+
+For example, if you enter string "from 9:00" to "Creation date" field,
+roundup will find all issues, that were created today since 9 AM.
+
+The ``<value>`` may also be an interval, as described in the next section.
+Searching of "-2m; -1m" on activity field gives you issues which were
+active between period of time since 2 months up-till month ago.
+
+Other possible examples (consider local time is 2003-03-08.22:07:48):
+
+- "from 2-12 to 4-2" means
+ <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
+- "FROM 18:00 TO +2m" means
+ <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
+- "12:00;" means
+ <Range from 2003-03-08.12:00:00 to None>
+- "tO +3d" means
+ <Range from None to 2003-03-11.20:07:48>
+- "2002-11-10; 2002-12-12" means
+ <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
+- "; 20:00 +1d" means
+ <Range from None to 2003-03-09.20:00:00>
+- "2003" means
+ <Range from 2003-01-01.00:00:00 to 2003-12-31.23:59:59>
+- "2003-04" means
+ <Range from 2003-04-01.00:00:00 to 2003-04-30.23:59:59>
+
+
+Interval properties
+~~~~~~~~~~~~~~~~~~~
+
+Date intervals are specified using the suffixes "y", "m", and "d". The
+suffix "w" (for "week") means 7 days. Time intervals are specified in
+hh:mm:ss format (the seconds may be omitted, but the hours and minutes
+may not).
+
+- "3y" means three years
+- "2y 1m" means two years and one month
+- "1m 25d" means one month and 25 days
+- "2w 3d" means two weeks and three days
+- "1d 2:50" means one day, two hours, and 50 minutes
+- "14:00" means 14 hours
+- "0:04:33" means four minutes and 33 seconds
+
+
+Simple support for collision detection
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Item edit pages remember when the item was last edited. When a form is
+submitted, the user will be informed if someone else has edited the item
+at the same time they tried to.
+
+
+Web Interface
+=============
+
+.. note::
+ This document contains screenshots of the default look and feel.
+ Your site may have a slightly (or very) different look, but the
+ functionality will be very similar, and the concepts still hold.
+
+The web interface is broken up into the following parts:
+
+1. `lists of items`_,
+2. `display, edit or entry of an item`_, and
+3. `searching page`_.
+
+
+Lists of Items
+--------------
+
+The first thing you'll see when you log into Roundup will be a list of
+open (ie. not resolved) issues. This list has been generated by a bunch
+of controls `under the covers`_ but for now, you can see something like:
+
+.. img: images/index_logged_out.png
+
+The screen is divided up into three sections:
+
+.. img: images/page_layout.png
+
+you may either register or log in. Registration takes you to:
+
+.. img: images/registration.png
+
+Once you're logged in, the screen changes slightly to:
+
+.. img: images/index_logged_in.png
+
+Note that the sidebar menu has changed slightly, so you can now get to
+your "My Details" page:
+
+.. img: images/my_details.png
+
+Note the new information on this page - the history.
+
+
+Display, edit or entry of an item
+---------------------------------
+
+Create a new issue with "create new" under the issue subheading. This
+will take you to:
+
+.. img: images/new_issue.png
+
+The `nosy list`_ is explained below. Enter some information and click
+"submit new entry" and you'll be rewarded with:
+
+.. img: images/new_issue_created.png
+
+or, if you don't enter all the required information (or some other error
+occurs) you'll get something like:
+
+.. img: images/new_issue_error.png
+
+
+Searching Page
+--------------
+
+See `entering values in your tracker`_ for an explanation of what you
+may type into the search form.
+
+
+Saving queries
+~~~~~~~~~~~~~~
+
+You may save queries in the tracker by giving the query a name. Each user
+may only have one query with a given name - if a subsequent search is
+performed with the same query name supplied, then it will edit the
+existing query of the same name.
+
+Queries may be marked as "private". These queries are only visible to the
+user that created them. If they're not marked "private" then all other
+users may include the query in their list of "Your Queries". Marking it as
+private at a later date does not affect users already using the query, nor
+does deleting the query.
+
+If a user subsequently creates or edits a public query, a new personal
+version of that query is made, with the same editing rules as described
+above.
+
+
+Under the covers
+~~~~~~~~~~~~~~~~
+
+The searching page converts your selections into the following
+arguments:
+
+============ =============================================================
+Argument Description
+============ =============================================================
+ at sort sort by prop name, optionally preceeded with '-' to give
+ descending or nothing for ascending sorting.
+ at group group by prop name, optionally preceeded with '-' or to sort
+ in descending or nothing for ascending order.
+ at columns selects the columns that should be displayed. Default is
+ all.
+ at filter indicates which properties are being used in filtering.
+ Default is none.
+propname selects the values the item properties given by propname must
+ have (very basic search/filter).
+ at search_text performs a full-text search (message bodies, issue titles,
+ etc)
+============ =============================================================
+
+You may manually write URLS that contain these arguments, like so
+(whitespace has been added for clarity)::
+
+ /issue?status=unread,in-progress,resolved&
+ topic=security,ui&
+ @group=priority&
+ @sort=-activity&
+ @filters=status,topic&
+ @columns=title,status,fixer
+
+
+Access Controls
+---------------
+
+User access is controlled through Permissions. These are are grouped
+into Roles, and users have a comma-separated list of Roles assigned to
+them.
+
+Permissions divide access controls up into answering questions like:
+
+- may the user edit issues ("Edit", "issue")
+- is the user allowed to use the web interface ("Web Access")
+- may the user edit other user's Roles through the web ("Web Roles")
+
+Any number of new Permissions and Roles may be created as described in
+the customisation documentation. Examples of new access controls are:
+
+- only managers may sign off issues as complete
+- don't give users who register through e-mail web access
+- let some users edit the details of all users
+
+
+E-Mail Gateway
+==============
+
+Roundup trackers may be used to facilitate e-mail conversations around
+issues. The "nosy" list attached to each issue indicates the users who
+should receive e-mail when messages are added to the issue.
+
+When e-mail comes into a tracker that identifies an issue in the subject
+line, the content of the e-mail is attached to the issue.
+
+You may even create new issues from e-mail messages.
+
+E-mail sent to a tracker is examined for several pieces of information:
+
+1. `subject-line information`_ identifying the purpose of the e-mail
+2. `sender identification`_ using the sender of the message
+3. `e-mail message content`_ which is to be extracted
+4. e-mail attachments which should be associated with the message
+
+
+Subject-line information
+------------------------
+
+The subject line of the incoming message is examined to find one of:
+
+1. the item that the message is responding to,
+2. the type of item the message should create, or
+3. we default the item class and try some trickiness
+
+If the subject line contains a prefix in ``[square brackets]`` then
+we're looking at case 1 or 2 above. Any "re:" or "fwd:" prefixes are
+stripped off the subject line before we start looking for real
+information.
+
+If an item designator (class name and id number, for example
+``issue123``) is found there, a new "msg" item is added to the
+"messages" property for that item, and any new "file" items are added to
+the "files" property for the item.
+
+If just an item class name is found there, we attempt to create a new
+item of that class with its "messages" property initialized to contain
+the new "msg" item and its "files" property initialized to contain any
+new "file" items.
+
+The third case above - where no ``[information]`` is provided, the
+tracker's ``MAIL_DEFAULT_CLASS`` configuration variable defines what
+class of item the message relates to. We try to match the subject line
+to an existing item of the default class, and if there's a match, the
+message is related to that matched item. If not, then a new item of the
+default class is created.
+
+
+Setting Properties
+~~~~~~~~~~~~~~~~~~
+
+The e-mail interface also provides a simple way to set properties on
+items. At the end of the subject line, propname=value pairs can be
+specified in square brackets, using the same conventions as for the
+roundup set shell command.
+
+For example,
+
+- setting the priority of an issue::
+
+ Subject: Re: [issue1] the coffee machine is broken! [priority=urgent]
+
+- adding yourself to a nosy list::
+
+ Subject: Re: [issue2] we're out of widgets [nosy=+richard]
+
+- setting the nosy list to just you and cliff::
+
+ Subject: Re: [issue2] we're out of widgets [nosy=richard,cliff]
+
+- removing yourself from a nosy list and setting the priority::
+
+ Subject: Re: [issue2] we're out of widgets [nosy=-richard;priority=bug]
+
+In all cases, the message relates to issue 2. The ``Re:`` prefix is
+stripped off.
+
+
+Automatic Properties
+~~~~~~~~~~~~~~~~~~~~
+
+**status of new issues**
+ When a new message is received that is not identified as being related
+ to an existing issue, it creates a new issue. The status of the new
+ issue is defaulted to "unread".
+
+**reopening of resolved issues**
+ When a message is is received for a resolved issue, the issue status is
+ automatically reset to "chatting" to indicate new information has been
+ received.
+
+
+Sender identification
+---------------------
+
+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
+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.
+
+
+E-Mail Message Content
+----------------------
+
+Roundup only associates plain text (MIME type ``text/plain``) as
+messages for items. Any other parts of a message are associated as
+downloadable files. If no plain text part is found, the message is
+rejected.
+
+To do this, incoming messages are examined for multiple parts:
+
+* In a multipart/mixed message or part, each subpart is extracted and
+ examined. The text/plain subparts are assembled to form the textual
+ body of the message, to be stored in the file associated with a "msg"
+ class item. Any parts of other types are each stored in separate files
+ and given "file" class items that are linked to the "msg" item.
+* In a multipart/alternative message or part, we look for a text/plain
+ subpart and ignore the other parts.
+
+If the message is a response to a previous message, and contains quoted
+sections, then these will be stripped out of the message if the
+``EMAIL_KEEP_QUOTED_TEXT`` configuration variable is set to ``'no'``.
+
+Message summary
+~~~~~~~~~~~~~~~
+
+The "summary" property on message items is taken from the first
+non-quoting section in the message body. The message body is divided
+into sections by blank lines. Sections where the second and all
+subsequent lines begin with a ">" or "|" character are considered
+"quoting sections". The first line of the first non-quoting section
+becomes the summary of the message.
+
+
+Address handling
+----------------
+
+All of the addresses in the ``To:`` and ``Cc:`` headers of the incoming
+message are looked up among the tracker users, and the corresponding
+users are placed in the "recipients" property on the new "msg" item. The
+address in the ``From:`` header similarly determines the "author"
+property of the new "msg" item. The default handling for addresses that
+don't have corresponding users is to create new users with no passwords
+and a username equal to the address.
+
+The addresses mentioned in the ``To:``, ``From:`` and ``Cc:`` headers of
+the message may be added to the `nosy list`_ depending on:
+
+``ADD_AUTHOR_TO_NOSY``
+ Does the author of a message get placed on the nosy list automatically?
+ If 'new' is used, then the author will only be added when a message
+ creates a new issue. If 'yes', then the author will be added on
+ followups too. If 'no', they're never added to the nosy.
+
+``ADD_RECIPIENTS_TO_NOSY``
+ Do the recipients (To:, Cc:) of a message get placed on the nosy list?
+ If 'new' is used, then the recipients will only be added when a message
+ creates a new issue. If 'yes', then the recipients will be added on
+ followups too. If 'no', they're never added to the nosy.
+
+
+Nosy List
+~~~~~~~~~
+
+Roundup watches for additions to the "messages" property of items.
+
+When a new message is added, it is sent to all the users on the "nosy"
+list for the item 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.
+
+If the author of the message is also in the nosy list for the item that
+the message is attached to, then the config var ``MESSAGES_TO_AUTHOR``
+is queried to determine if they get a nosy list copy of the message too.
+
+
+Mail gateway script command line
+--------------------------------
+
+Usage::
+
+ roundup-mailgw [[-C class] -S field=value]* <instance home> [method]
+
+The roundup mail gateway may be called in one of three ways:
+
+ - with an instance home as the only argument,
+ - with both an instance home and a mail spool file, or
+ - with both an instance home and a pop server account.
+
+It also supports optional -C and -S arguments that allows you to set a
+fields for a class created by the roundup-mailgw. The default class if
+not specified is msg, but the other classes: issue, file, user can also
+be used. The -S or --set options uses the same
+property=value[;property=value] notation accepted by the command line
+roundup command or the commands that can be given on the Subject line of
+an e-mail message.
+
+It can let you set the type of the message on a per e-mail address basis.
+
+PIPE:
+ In the first case, the mail gateway reads a single message from the
+ standard input and submits the message to the roundup.mailgw module.
+
+UNIX mailbox:
+ In the second case, the gateway reads all messages from the mail spool
+ file and submits each in turn to the roundup.mailgw module. The file is
+ emptied once all messages have been successfully handled. The file is
+ specified as::
+
+ mailbox /path/to/mailbox
+
+POP:
+ In the third case, the gateway reads all messages from the POP server
+ specified and submits each in turn to the roundup.mailgw module. The
+ server is specified as::
+
+ pop username:password at server
+
+ The username and password may be omitted::
+
+ 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.
+
+APOP:
+ Same as POP, but using Authenticated POP::
+
+ apop username:password at server
+
+
+Command Line Tool
+=================
+
+The basic usage is::
+
+ Usage: roundup-admin [options] [<command> <arguments>]
+
+ Options:
+ -i instance home -- specify the issue tracker "home directory" to administer
+ -u -- the user[:password] to use for commands
+ -d -- print full designators not just class id numbers
+ -c -- when outputting lists of data, comma-separate them.
+ Same as '-S ","'.
+ -S <string> -- when outputting lists of data, string-separate them
+ -s -- when outputting lists of data, space-separate them.
+ Same as '-S " "'.
+
+ Only one of -s, -c or -S can be specified.
+
+ Help:
+ roundup-admin -h
+ roundup-admin help -- this help
+ roundup-admin help <command> -- command-specific help
+ roundup-admin help all -- all available help
+
+ Commands:
+ commit
+ create classname property=value ...
+ display designator[,designator]*
+ export [class[,class]] export_dir
+ find classname propname=value ...
+ get property designator[,designator]*
+ help topic
+ history designator
+ import import_dir
+ initialise [adminpw]
+ install [template [backend [admin password]]]
+ list classname [property]
+ pack period | date
+ reindex
+ retire designator[,designator]*
+ rollback
+ security [Role name]
+ set items property=value property=value ...
+ specification classname
+ table classname [property[,property]*]
+ Commands may be abbreviated as long as the abbreviation matches only one
+ command, e.g. l == li == lis == list.
+
+
+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 an
+issue tracker. It may be thought of as the issue tracker's "home
+directory". It may be specified in the environment variable
+``TRACKER_HOME`` or on the command line as "``-i tracker``".
+
+A designator is a classname and an itemid concatenated, eg. bug1,
+user10, ... Property values are represented as strings in command
+arguments and in the printed results:
+
+- Strings are, well, strings.
+- Password values will display as their encoded value.
+- Date values are printed in the full date format in the local time
+ zone, and accepted in the full format or any of the partial formats
+ explained below.::
+
+ Input of... Means...
+ "2000-04-17.03:45" 2000-04-17.03:45:00
+ "2000-04-17" 2000-04-17.00:00:00
+ "01-25" yyyy-01-25.00:00:00
+ "08-13.22:13" yyyy-08-13.22:13:00
+ "11-07.09:32:43" yyyy-11-07.09:32:43
+ "14:25" yyyy-mm-dd.14:25:00
+ "8:47:11" yyyy-mm-dd.08:47:11
+ "2003" 2003-01-01.00:00:00
+ "2003-04" 2003-04-01.00:00:00
+ "." "right now"
+
+- Link values are printed as item designators. When given as an
+ argument, item designators and key strings are both accepted.
+- Multilink values are printed as lists of item designators joined by
+ commas. When given as an argument, item designators and key strings
+ are both accepted; an empty string, a single item, or a list of items
+ joined by commas is accepted.
+
+When multiple items are specified to the roundup get or roundup set
+commands, the specified properties are retrieved or set on all the
+listed items. When multiple results are returned by the roundup get or
+roundup find commands, they are printed one per line (default) or joined
+by commas (with the "``-c``" option).
+
+Where the command changes data, a login name/password is required. The
+login may be specified as either "``name``" or "``name:password``".
+
+- ``ROUNDUP_LOGIN`` environment variable
+- the "``-u``" command-line option
+
+If either the name or password is not supplied, they are obtained from
+the command-line.
+
+
+Using with the shell
+--------------------
+
+With version 0.6.0 or newer of roundup which supports: multiple
+designators to display and the -d, -S and -s flags.
+
+To find all messages regarding chatting issues that contain the word
+"spam", for example, you could execute the following command from the
+directory where the database dumps its files::
+
+ shell% for issue in `roundup-admin -ds find issue status=chatting`; do
+ > grep -l spam `roundup-admin -ds ' ' get messages $issue`
+ > done
+ msg23
+ msg49
+ msg50
+ msg61
+ shell%
+
+Or, using the -dc option, this can be written as a single command::
+
+ shell% grep -l spam `roundup get messages \
+ \`roundup -dc find issue status=chatting\``
+ msg23
+ msg49
+ msg50
+ msg61
+ shell%
+
+You can also display issue contents::
+
+ shell% roundup-admin display `roundup-admin -dc get messages \
+ issue3,issue1`
+ files: []
+ inreplyto: None
+ recipients: []
+ author: 1
+ date: 2003-02-16.21:23:03
+ messageid: None
+ summary: jkdskldjf
+ files: []
+ inreplyto: None
+ recipients: []
+ author: 1
+ date: 2003-02-15.01:59:11
+ messageid: None
+ summary: jlkfjadsf
+
+or status::
+
+ shell% roundup-admin get name `/tools/roundup/bin/roundup-admin \
+ -dc -i /var/roundup/sysadmin get status issue3,issue1`
+ unread
+ deferred
+
+or status on a single line::
+
+ shell% echo `roundup-admin get name \`/tools/roundup/bin/roundup-admin \
+ -dc -i /var/roundup/sysadmin get status issue3,issue1\``
+ unread deferred
+
+which is the same as::
+
+ shell% roundup-admin -s get name `/tools/roundup/bin/roundup-admin \
+ -dc -i /var/roundup/sysadmin get status issue3,issue1`
+ unread deferred
+
+Also the tautological::
+
+ shell% roundup-admin get name \
+ `roundup-admin -dc get status \`roundup-admin -dc find issue \
+ status=chatting\``
+ chatting
+ chatting
+
+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/vendor/roundup/current/doc/whatsnew-0.7.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/whatsnew-0.7.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,481 @@
+=========================
+What's New in Roundup 0.7
+=========================
+
+For those completely new to Roundup, you might want to look over the very
+terse features__ page.
+
+__ features.html
+
+.. contents::
+
+Instant-Gratification script even more gratifying
+=================================================
+
+The immensely popular ``python demo.py`` instant-gratification script has
+been extended to allow you to choose the backend to use with the demo. To
+select the "sqlite" backend (assuming it is available) you use::
+
+ python demo.py sqlite nuke
+
+This will nuke any existing demo and reinitialise it with the sqlite
+backend. Remember folks, if you want to restart the demo at a later point,
+you just need to type::
+
+ python demo.py
+
+without the "sqlite nuke" part, or you'll clear out the demo again. The
+backend names are:
+
+ anydbm bsddb bsddb3 sqlite metakit mysql postgresql
+
+You will need support modules installed for all except the first two. If
+you're not sure whether you have support, run::
+
+ python run_tests.py
+
+and if you see a line saying "Including XXXX tests" where XXXX is the
+backend you wish to try, then you're on your way. The mysql and postgresql
+require their test environments to be set up. Read their respective
+documents in the "doc" directory to do that.
+
+
+Web Interface
+=============
+
+Saving and sharing of user queries
+----------------------------------
+
+Due to popular demand, the user query saving mechanisms have been
+overhauled.
+
+As before, you may save queries in the tracker by giving the query a
+name. Each user may only have one query with a given name - if a
+subsequent search is performed with the same query name supplied, then
+it will edit the existing query of the same name.
+
+Queries may be marked as "private". These queries are only visible to the
+user that created them. If they're not marked "private" then all other
+users may include the query in their list of "Your Queries". Marking it as
+private at a later date does not affect users already using the query, nor
+does deleting the query.
+
+If a user subsequently creates or edits a public query, a new personal
+version of that query is made, with the same editing rules as described
+above.
+
+You *are not required* to make these changes in your tracker. You only
+need to make them if you wish to use the new query editing features. It's
+highly recommended, as the effort is minimal.
+
+1. You will need to edit your tracker's ``dbinit.py`` to change the way
+ queries are stored. Change the lines::
+
+ query = Class(db, "query",
+ klass=String(), name=String(),
+ url=String())
+ query.setkey("name")
+
+ to::
+
+ query = Class(db, "query",
+ klass=String(), name=String(),
+ url=String(), private_for=Link('user'))
+
+ That is, add the "private_for" property, and remove the line that says
+ ``query.setkey("name")``.
+
+2. You will also need to copy the ``query.edit.html`` template page from the
+ ``templates/classic/html/`` directory of the source to your tracker's
+ ``html`` directory.
+
+3. Once you've done that, edit the tracker's ``page.html`` template to
+ change::
+
+ <td rowspan="2" valign="top" class="sidebar">
+ <p class="classblock" tal:condition="request/user/queries">
+ <b>Your Queries</b><br>
+ <tal:block tal:repeat="qs request/user/queries">
+
+ to::
+
+ <td rowspan="2" valign="top" class="sidebar">
+ <p class="classblock">
+ <b>Your Queries</b> (<a href="query?@template=edit">edit</a>)<br>
+ <tal:block tal:repeat="qs request/user/queries">
+
+ That is, you're removing the ``tal:condition`` and adding a link to the
+ new edit page.
+
+4. You might also wish to remove the redundant query editing section from the
+ ``user.item.html`` page.
+
+ZRoundup reinstated
+-------------------
+
+The Zope interface, ZRoundup, lives again!
+
+See the `upgrading documentation`__ if you wish to use it.
+
+__ upgrading.html#zroundup-changes
+
+
+Simple support for collision detection
+--------------------------------------
+
+Item edit pages that use the ``context/submit`` function to generate their
+submit buttons now automatically include a datestamp in the form. This
+datestamp is compared to the "activity" property of the item when the form
+is submitted. If the "actvity" property is younger than the datestamp in
+the form submission, then someone else has edited the item, and a page
+indicating this is displayed to the user.
+
+
+Extending the cgi interface
+---------------------------
+
+Before 0.7.0 adding or extending web actions was done by overriding or adding
+methods on the Client class. Though this approach still works to provide
+backwards compatibility, it is recommended you upgrade to the new approach, as
+described in the `Defining new web actions`__ section of the customization
+documentation. You might also want to take a look at the `Using an external
+password validation source`__ example.
+
+__ customizing.html#defining-new-web-actions
+__ customizing.html#using-an-external-password-validation-source
+
+Actions may also return the content that should return to the user, which
+causes the web interface to skip the normal template formatting step.
+This could be used to return an image to the user instead of HTML. Be sure
+to set the correct content-type header though! The default is still
+text/html. This is done with::
+
+ self.client.setHeader('Content-Type', 'image/png')
+
+if you were returning a PNG image.
+
+
+Roundup server
+--------------
+
+The roundup-server web interface now supports setgid and running on port
+< 1024.
+
+It also forks to handle new connections, which means that trackers using
+the postgresql or mysql backends will be able to have multiple users
+accessing the tracker simultaneously.
+
+
+HTML templating made easier
+---------------------------
+
+All HTML templating functions perform checks for permissions required to
+display or edit the data they are manipulating. The simplest case is
+editing an issue title. Including the expression::
+
+ context/title/field
+
+will present the user with an edit field if they have Edit Permission. If
+not, then they will be presented with a static display if they have View
+Permission. If they don't even have View Permission, then an error message
+is raised, preventing the display of the page, indicating that they don't
+have permission to view the information.
+
+This removes the need for the template to perform those checks, which was
+just plain messy.
+
+Some new permissions will need to be created in your trackers to cope with
+this change, as outlined in the `upgrading documentation`__.
+
+__ upgrading.html#permission-assignments
+
+
+Standards changes
+-----------------
+
+The HTTP Content-Length header when we serve up files, either
+static ones from the "html" folder or file content from the database.
+
+We also handle If-Modified-Since and supply Last-Modified for both types
+of file too.
+
+The HTML generated in the classic tracker is now HTML4 (or optionally
+XHTML) compliant. The ``config.py`` variable "HTML_VERSION" is used to
+control this behaviour.
+
+The stylesheet includes printer settings now too, so printed pages
+don't include the sidebar.
+
+
+Quoting of URLs and HTML
+------------------------
+
+Templates that wish to offer file downloads may now use a new
+``download_url`` method::
+
+ <tr tal:repeat="file context/files">
+ <td>
+ <a tal:attributes="href file/download_url"
+ tal:content="file/name">dld link</a>
+ </td>
+ ...
+
+The ``download_url`` method looks up the file's "id" and "name" and
+generates a correctly-quoted URL.
+
+Additionally, users wishing to URL- or HTML- quote text in their templates
+may use the new ``utils.url_quote(url)`` and ``utils.html_quote(html)``
+methods.
+
+
+CSV download of search results
+------------------------------
+
+A new CGI action, ``export_csv`` has been added which exports a given
+index page query as a comma-separated-value file.
+
+To use this new action, just add a link to your ``issue.index.html``
+page::
+
+ <a tal:attributes="href python:request.indexargs_url('issue',
+ {'@action':'export_csv'})">Download as CSV</a>
+
+You may use this for other classes by adding it to their index page and
+changing the ``'issue'`` part of the expression to the new class' name.
+
+
+Other changes
+-------------
+
+- we serve up a favicon now
+- the page titles have the tracker name at the end of the text instead
+ of the start
+- added url_quote and html_quote methods to the utils object
+- added isset method to HTMLProperty
+- added search_checkboxes as an option for the search form
+
+
+Email Interface
+===============
+
+Better handling of some email headers
+-------------------------------------
+
+We ignore messages with the header "Precedence: bulk".
+
+If a Resent-From: header is present, it is used in preference to the From:
+header when determining the author of the message. Useful for redirecting
+error messages from automated systems.
+
+
+Email character set
+-------------------
+
+The default character set for sending email is UTF-8 (ie. Unicode). If you
+have users whose email clients can't handle UTF-8 (eg. Eudora) then you
+will need to edit the new config.py variable ``EMAIL_CHARSET``.
+
+
+Dispatcher configuration
+------------------------
+
+A new config option has been added that specifies the email address of
+a "dispatcher" role. This email address acts as a central sentinel for
+issues coming into the system. You can configure it so that all e-mail
+error messages get bounced to them, them and the user in question, or
+just the user (default).
+
+To toggle these switches, add the "DISPATCHER_EMAIL" and
+"ERROR_MESSAGES_TO" configuration values to your tracker's ``config.py``.
+See the `customisation documentation`_ for how to use them.
+
+
+More flexible message generation
+--------------------------------
+
+The code for generating email messages in Roundup has been refactored. A
+new module, ``roundup.mailer`` contains most of the nuts-n-bolts required
+to generate email messages from Roundup.
+
+In addition, the ``IssueClass`` methods ``nosymessage()`` and
+``send_message()`` have both been altered so that they don't require the
+message id parameter. This means that change notes with no associated
+change message may now be generated much more easily.
+
+The roundupdb nosymessage() method also accepts a ``bcc`` argument which
+specifies additional userids to send the message to that will not be
+included in the To: header of the message.
+
+
+Registration confirmation by email
+----------------------------------
+
+Users may now reply to their registration confirmation email, and the
+roundup mail gateway will complete their registration.
+
+
+``roundup-mailgw`` now supports IMAP
+------------------------------------
+
+To retrieve from an IMAP mailbox, use a *cron* entry similar to the
+POP one::
+
+ 0,10,20,30,40,50 * * * * /usr/local/bin/roundup-mailgw /opt/roundup/trackers/support imap <imap_spec>
+
+where imap_spec is "``username:password at server``" that specifies the roundup
+submission user's IMAP account name, password and server. You may
+optionally include a mailbox to use other than the default ``INBOX`` with
+"``imap username:password at server mailbox``".
+
+If you have a secure (ie. HTTPS) IMAP server then you may use ``imaps``
+in place of ``imap`` in the command to use a secure connection.
+
+
+Database configuration
+======================
+
+Postgresql added as a backend option
+------------------------------------
+
+Trackers may now use the postgresql RDBMS as a database store.
+
+Postgresql is a good choice if you expect your tracker to grow very large,
+and are expecting many users.
+
+
+API change
+----------
+
+The Database.curuserid attribute was removed. Any code referencing this
+attribute should be replaced with a call to Database.getuid().
+
+
+New configuration options
+-------------------------
+
+- Added DEFAULT_TIMEZONE which allows the tracker to have a different
+ default to UTC when users don't specify their own preference.
+
+- Added EMAIL_CHARSET (in 0.6.6, but worth mentioning here) which hard-codes
+ the character set to be used when sending email from Roundup. This works
+ around some email clients' inability to cope well with UTF-8 (the
+ default).
+
+- ERROR_MESSAGES_TO and DISPATCHER_EMAIL as described above in `Dispatcher
+ configuration`_.
+
+
+Typed columns in RDBMS backends
+-------------------------------
+
+The SQLite, MySQL and Postgresql backends now create tables with
+appropriate column datatypes (not just varchar).
+
+Your database will be automatically migrated to use the new schemas, but
+it will take time. It's probably a good idea to make sure you do this as
+part of the upgrade when users are not expected to be using the system.
+
+
+Permission setup
+----------------
+
+0.7 automatically sets up the Edit and View Permissions for all classes,
+thus you don't need to do so. Feel free to remove the code::
+
+ # Add new Permissions for this schema
+ for cl in 'issue', 'file', 'msg', 'user', 'query', 'keyword':
+ db.security.addPermission(name="Edit", klass=cl,
+ description="User is allowed to edit "+cl)
+ db.security.addPermission(name="View", klass=cl,
+ description="User is allowed to access "+cl)
+
+from your ``dbinit.py``.
+
+
+New "actor" property
+--------------------
+
+Roundup's database has a new per-item property "actor" which reflects the
+user performing the last "actvitiy". See the classic template for ways to
+integrate this new property into your interface.
+
+The property will be automatically added to your existing database.
+
+
+New Reject exception for Auditors
+---------------------------------
+
+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
+handle this exception sanely. For example:
+
+- mailgw will trap and ignore Reject for file attachments and messages
+- cgi will trap and present the exception in a nice format
+
+
+New auditor fixes Outlook bug
+-----------------------------
+
+The new optional auditor ``detectors/emailauditor.py`` fires whenever a
+new file entity is created.
+
+If the file is of type message/rfc822, we tack on the extension .mht.
+
+The reason for this is that Microsoft Internet Explorer will not open
+things with a .eml attachment, as they deem it 'unsafe'. Worse yet,
+they'll just give you an incomprehensible error message. For more
+information, please see:
+
+http://support.microsoft.com/default.aspx?scid=kb;EN-US;825803
+
+Their suggested work around is (excerpt):
+
+ WORKAROUND
+
+ To work around this behavior, rename the .EML file that the URL
+ links to so that it has a .MHT file name extension, and then update
+ the URL to reflect the change to the file name. To do this:
+
+ 1. In Windows Explorer, locate and then select the .EML file that
+ the URL links.
+ 2. Right-click the .EML file, and then click Rename.
+ 3. Change the file name so that the .EML file uses a .MHT file name
+ extension, and then press ENTER.
+ 4. Updated the URL that links to the file to reflect the new file
+ name extension.
+
+
+New script for copying users
+----------------------------
+
+A new script, ``scripts/copy-user.py``, will copy users from one tracker
+to another. Example usage::
+
+ copy-user.py /roundup/tracker1 /roundup/tracker2 `seq 3 10` 14 16
+
+which copies users 3, 4, 5, 6, 7, 8, 9, 10, 14 and 16.
+
+
+Other improvements
+------------------
+
+- All RDBMS backends now have indexes automatically created on critical
+ table columns.
+
+- Additionally, the RDBMS backends also implement their own session,
+ one-time-key and full-text indexing stores. These were previously external
+ dbm stores. This change allows control of locking the database to be
+ completely handed over to the RDBMS.
+
+- Date values capture fractions of seconds now. Note that the MySQL backend
+ is not capable of storing this precision though, so it will be lost for
+ users of that backend.
+
+- The roundup-admin "export" and "import" commands now handle the database
+ journals too. This means that exports from previous versions of Roundup
+ will not work under 0.7!
+
+
+.. _`customisation documentation`: customizing.html
Added: tracker/vendor/roundup/current/doc/whatsnew-0.8.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/doc/whatsnew-0.8.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,187 @@
+=========================
+What's New in Roundup 0.8
+=========================
+
+For those completely new to Roundup, you might want to look over the very
+terse features__ page.
+
+__ features.html
+
+.. contents::
+
+In Summary
+==========
+
+(this information copied directly from the ``CHANGES.txt`` file)
+
+XXX this section needs more detail
+
+- create a new RDBMS cursor after committing
+- roundup-admin reindex command may now work on single items or classes
+
+- roundup-server options -g and -u accept both ids and names (sf bug 983769)
+- roundup-server now has a configuration file (-C option)
+- roundup windows service may be installed with command line options
+ recognized by roundup-server (but not tracker specification arguments).
+ Use this to specify server configuration file for the service.
+
+- added option to turn off registration confirmation via email
+ ("instant_registration" in config) (sf rfe 922209)
+
+
+
+Performance improvements
+========================
+
+We don't try to import all backends in backends.__init__ unless we *want*
+to.
+
+Roundup may now use the Apache mod_python interface (see installation.txt)
+which is much faster than the standard cgi-bin and a little faster than
+roundup-server.
+
+There is now an experimental multi-thread server which should allow faster
+concurrent access.
+
+In the hyperdb, a few other speedups were implemented, such as:
+
+- record journaltag lookup ("fixes" sf bug 998140)
+- unless in debug mode, keep a single persistent connection through a
+ single web or mailgw request.
+- remove "manual" locking of sqlite database
+
+
+Logging of internal messages
+============================
+
+Roundup's previously ad-hoc logging of events has been cleaned up and is
+now configured in a single place in the tracker configuration file.
+
+The `customization documentation`_ has more details on how this is
+configured.
+
+roundup-mailgw now logs fatal exceptions rather than mailing them to admin.
+
+
+Security Changes
+================
+
+``security.addPermissionToRole()`` has been extended to allow skipping the
+separate getPermission call.
+
+
+Password Storage
+----------------
+
+Added MD5 scheme for password hiding. This extends the existing SHA and
+crypt methods and is useful if you have an existing MD5 password database.
+
+
+Permission Definitions
+----------------------
+
+Permissions may now be defined on a per-property basis, allowing access to
+only specific properties on items.
+
+Permissions may also have code attached which is executed to check whether
+the Permission is valid for the current user and item.
+
+Permissions are now automatically checked when information is rendered
+through the web. This includes:
+
+1. View checks for properties when being rendered via the ``plain()`` or
+ similar methods. If the check fails, the text "[hidden]" will be
+ displayed.
+2. Edit checks for properties when the edit field is being rendered via
+ the ``field()`` or similar methods. If the check fails, the property
+ will be rendered via the ``plain()`` method (see point 1. for additional
+ checking performed)
+3. View checks are performed in index pages for each item being displayed
+ such that if the user does not have permission, the row is not rendered.
+
+
+Extending Roundup
+=================
+
+To write extension code for Roundup you place a file in the tracker home
+``extensions`` directory. See the `customisation documentation`_ for more
+information about how this is done.
+
+
+8-bit character set support in Web interface
+============================================
+
+This is used to override the UTF-8 default. It may be overridden in both
+forms and a browser cookie.
+
+- In forms, use the ``@charset`` variable.
+- To use the cookie override, have the ``roundup_charset`` cookie set.
+
+In both cases, the value is a valid charset name (eg. ``utf-8`` or
+``kio8-r``).
+
+Inside Roundup, all strings are stored and processed in utf-8.
+Unfortunately, some older browsers do not work properly with
+utf-8-encoded pages (e.g. Netscape Navigator 4 displays wrong
+characters in form fields). This version allows to change
+the character set for http transfers. To do so, you may add
+the following code to your ``page.html`` template::
+
+ <tal:block define="uri string:${request/base}${request/env/PATH_INFO}">
+ <a tal:attributes="href python:request.indexargs_url(uri,
+ {'@charset':'utf-8'})">utf-8</a>
+ <a tal:attributes="href python:request.indexargs_url(uri,
+ {'@charset':'koi8-r'})">koi8-r</a>
+ </tal:block>
+
+(substitute ``koi8-r`` with the appropriate charset for your language).
+Charset preference is kept in the browser cookie ``roundup_charset``.
+
+``meta http-equiv`` lines added to the tracker templates in version 0.6.0
+should be changed to include actual character set name::
+
+ <meta http-equiv="Content-Type"
+ tal:attributes="content string:text/html;; charset=${request/client/charset}"
+ />
+
+Actual charset is also sent in the http header.
+
+
+Web Interface Miscellanea
+=========================
+
+The web interface has seen some changes:
+
+Editing
+
+Templating
+ We implement __nonzero__ for HTMLProperty - properties may now be used in
+ boolean conditions (eg ``tal:condition="issue/nosy"`` will be false if
+ the nosy list is empty).
+
+ We added a default argument to the DateHTMLProperty.field method, and an
+ optional Interval (string or object) to the DateHTMLProperty.now
+
+ We've added a multiple selection Link/Multilink search field macro to the
+ default classic page.html template.
+
+ We relaxed hyperlinking in web interface (accept "issue123" or "Issue 123")
+
+ The listing popup may be used in query forms.
+
+Standard templates
+ We hide "(list)" popup links when issue is only viewable
+
+ The issue search page now has fields to allow no sorting / grouping of
+ the results.
+
+ The default page.html template now has a search box in the top right
+ corner which performs a full-text search of issues. The "show issue"
+ quick jump form in the sidebar has had its font size reduced to use less
+ space.
+
+Web server
+ The builtin web server may now perform HTTP Basic Authentication by
+ itself.
+
+.. _`customization documentation`: customizing.html
Added: tracker/vendor/roundup/current/frontends/README.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/frontends/README.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,8 @@
+This directory contains alternate front-ends for Roundup.
+
+Zope - ZRoundup
+---------------
+
+This installs as a regular Zope product. See Roundup's doc/installation.txt
+for more info.
+
Added: tracker/vendor/roundup/current/frontends/ZRoundup/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/frontends/ZRoundup/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2 @@
+*.pyc
+*.pyo
Added: tracker/vendor/roundup/current/frontends/ZRoundup/ZRoundup.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/frontends/ZRoundup/ZRoundup.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,218 @@
+# 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: ZRoundup.py,v 1.22 2006/01/25 03:43:04 richard Exp $
+#
+''' ZRoundup module - exposes the roundup web interface to Zope
+
+This frontend works by providing a thin layer that sits between Zope and the
+regular CGI interface of roundup, providing the web frontend with the minimum
+of effort.
+
+This means that the regular CGI interface does all authentication quite
+independently of Zope. The roundup code is kept in memory though, and it
+runs in the same server as all your other Zope stuff, so it does have _some_
+advantages over regular CGI :)
+'''
+
+import urlparse
+
+from Globals import InitializeClass, HTMLFile
+from OFS.SimpleItem import Item
+from OFS.PropertyManager import PropertyManager
+from Acquisition import Explicit, Implicit
+from Persistence import Persistent
+from AccessControl import ClassSecurityInfo
+from AccessControl import ModuleSecurityInfo
+modulesecurity = ModuleSecurityInfo()
+
+import roundup.instance
+from roundup.cgi import client
+
+modulesecurity.declareProtected('View management screens',
+ 'manage_addZRoundupForm')
+manage_addZRoundupForm = HTMLFile('dtml/manage_addZRoundupForm', globals())
+
+modulesecurity.declareProtected('Add Z Roundups', 'manage_addZRoundup')
+def manage_addZRoundup(self, id, instance_home, REQUEST):
+ """Add a ZRoundup product """
+ # validate the instance_home
+ roundup.instance.open(instance_home)
+ self._setObject(id, ZRoundup(id, instance_home))
+ return self.manage_main(self, REQUEST)
+
+class RequestWrapper:
+ '''Make the Zope RESPONSE look like a BaseHTTPServer
+ '''
+ def __init__(self, RESPONSE):
+ self.RESPONSE = RESPONSE
+ self.wfile = self.RESPONSE
+ def send_response(self, status):
+ self.RESPONSE.setStatus(status)
+ def send_header(self, header, value):
+ self.RESPONSE.addHeader(header, value)
+ def end_headers(self):
+ # not needed - the RESPONSE object handles this internally on write()
+ pass
+
+class FormItem:
+ '''Make a Zope form item look like a cgi.py one
+ '''
+ def __init__(self, value):
+ self.value = value
+ if hasattr(self.value, 'filename'):
+ self.filename = self.value.filename
+ self.value = self.value.read()
+
+class FormWrapper:
+ '''Make a Zope form dict look like a cgi.py one
+ '''
+ def __init__(self, form):
+ self.__form = form
+ def __getitem__(self, item):
+ entry = self.__form[item]
+ if isinstance(entry, type([])):
+ entry = map(FormItem, entry)
+ else:
+ entry = FormItem(entry)
+ return entry
+ def getvalue(self, key, default=None):
+ if self.__form.has_key(key):
+ return self.__form[key]
+ else:
+ return default
+ def has_key(self, item):
+ return self.__form.has_key(item)
+ def keys(self):
+ return self.__form.keys()
+
+ def __repr__(self):
+ return '<ZRoundup.FormWrapper %r>'%self.__form
+
+class ZRoundup(Item, PropertyManager, Implicit, Persistent):
+ '''An instance of this class provides an interface between Zope and
+ roundup for one roundup instance
+ '''
+ meta_type = 'Z Roundup'
+ security = ClassSecurityInfo()
+
+ def __init__(self, id, instance_home):
+ self.id = id
+ self.instance_home = instance_home
+
+ # define the properties that define this object
+ _properties = (
+ {'id':'id', 'type': 'string', 'mode': 'w'},
+ {'id':'instance_home', 'type': 'string', 'mode': 'w'},
+ )
+ property_extensible_schema__ = 0
+
+ # define the tabs for the management interface
+ manage_options= PropertyManager.manage_options + (
+ {'label': 'View', 'action':'index_html'},
+ ) + Item.manage_options
+
+ icon = "misc_/ZRoundup/icon"
+
+ security.declarePrivate('roundup_opendb')
+ def roundup_opendb(self):
+ '''Open the roundup instance database for a transaction.
+ '''
+ tracker = roundup.instance.open(self.instance_home)
+ request = RequestWrapper(self.REQUEST['RESPONSE'])
+ env = self.REQUEST.environ
+
+ # figure out the path components to set
+ url = urlparse.urlparse( self.absolute_url() )
+ path = url[2]
+ path_components = path.split( '/' )
+
+ # special case when roundup is '/' in this virtual host,
+ if path == "/" :
+ env['SCRIPT_NAME'] = "/"
+ env['TRACKER_NAME'] = ''
+ else :
+ # all but the last element is the path
+ env['SCRIPT_NAME'] = '/'.join( path_components[:-1] )
+ # the last element is the name
+ env['TRACKER_NAME'] = path_components[-1]
+
+ form = FormWrapper(self.REQUEST.form)
+ if hasattr(tracker, 'Client'):
+ return tracker.Client(tracker, request, env, form)
+ return client.Client(tracker, request, env, form)
+
+ security.declareProtected('View', 'index_html')
+ def index_html(self):
+ '''Alias index_html to roundup's index
+ '''
+ # Redirect misdirected requests -- bugs 558867 , 565992
+ # PATH_INFO, as defined by the CGI spec, has the *real* request path
+ orig_path = self.REQUEST.environ['PATH_INFO']
+ if orig_path[-1] != '/' :
+ url = urlparse.urlparse( self.absolute_url() )
+ url = list( url ) # make mutable
+ url[2] = url[2]+'/' # patch
+ url = urlparse.urlunparse( url ) # reassemble
+ RESPONSE = self.REQUEST.RESPONSE
+ RESPONSE.setStatus( "MovedPermanently" ) # 301
+ RESPONSE.setHeader( "Location" , url )
+ return RESPONSE
+
+ client = self.roundup_opendb()
+ # fake the path that roundup should use
+ client.split_path = ['index']
+ return client.main()
+
+ def __getitem__(self, item):
+ '''All other URL accesses are passed throuh to roundup
+ '''
+ return PathElement(self, item).__of__(self)
+
+class PathElement(Item, Implicit):
+ def __init__(self, zr, path):
+ self.zr = zr
+ self.path = path
+
+ def __getitem__(self, item):
+ ''' Get a subitem.
+ '''
+ return PathElement(self.zr, self.path + '/' + item).__of__(self)
+
+ def index_html(self, REQUEST=None):
+ ''' Actually call through to roundup to handle the request.
+ '''
+ try:
+ client = self.zr.roundup_opendb()
+ # fake the path that roundup should use
+ client.path = self.path
+ # and call roundup to do something
+ client.main()
+ return ''
+ except client.NotFound:
+ raise 'NotFound', REQUEST.URL
+ pass
+ except:
+ import traceback
+ traceback.print_exc()
+ # all other exceptions in roundup are valid
+ raise
+
+InitializeClass(ZRoundup)
+modulesecurity.apply(globals())
+
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/frontends/ZRoundup/__init__.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/frontends/ZRoundup/__init__.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,56 @@
+# 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: __init__.py,v 1.4 2002/10/10 03:47:27 richard Exp $
+#
+__version__='1.0'
+
+import os
+# figure where ZRoundup is installed
+here = None
+if os.environ.has_key('INSTANCE_HOME'):
+ here = os.environ['INSTANCE_HOME']
+ path = os.path.join(here, 'Products', 'ZRoundup')
+ if not os.path.exists(path):
+ path = os.path.join(here, 'lib', 'python', 'Products', 'ZRoundup')
+ if not os.path.exists(path):
+ here = None
+if here is None:
+ from __main__ import here
+ path = os.path.join(here, 'Products', 'ZRoundup')
+ if not os.path.exists(path):
+ path = os.path.join(here, 'lib', 'python', 'Products', 'ZRoundup')
+ if not os.path.exists(path):
+ raise ValueError, "Can't determine where ZRoundup is installed"
+
+# product initialisation
+import ZRoundup
+def initialize(context):
+ context.registerClass(
+ ZRoundup, meta_type = 'Z Roundup',
+ constructors = (
+ ZRoundup.manage_addZRoundupForm, ZRoundup.manage_addZRoundup
+ )
+ )
+
+# set up the icon
+from ImageFile import ImageFile
+misc_ = {
+ 'icon': ImageFile('icons/tick_symbol.gif', path),
+}
+
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/frontends/ZRoundup/dtml/manage_addZRoundupForm.dtml
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/frontends/ZRoundup/dtml/manage_addZRoundupForm.dtml Sun Nov 5 21:30:25 2006
@@ -0,0 +1,5 @@
+<form action="manage_addZRoundup">
+ID: <input type="text" name="id"><br>
+Instance Home: <input type="text" name="instance_home"><br>
+<input type="submit">
+</form>
Added: tracker/vendor/roundup/current/frontends/ZRoundup/icons/tick_symbol.gif
==============================================================================
Binary file. No diff available.
Added: tracker/vendor/roundup/current/frontends/ZRoundup/refresh.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/frontends/ZRoundup/refresh.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2 @@
+The existence of this file enables the Zope product refresh option.
+Read the Zope documentation for more info about product refresh.
Added: tracker/vendor/roundup/current/locale/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/locale/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,5 @@
+*.mo
+*.bak
+*.swp
+*.tmp
+*.poedit
\ No newline at end of file
Added: tracker/vendor/roundup/current/locale/GNUmakefile
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/locale/GNUmakefile Sun Nov 5 21:30:25 2006
@@ -0,0 +1,56 @@
+# Extract translatable strings from Roundup sources,
+# update and compile all existing translations
+#
+# $Id: GNUmakefile,v 1.10 2006/03/04 09:04:29 a1s Exp $
+
+# tool locations
+XPOT ?= xpot
+MSGFMT ?= msgfmt
+MSGMERGE ?= msgmerge
+XGETTEXT ?= xgettext
+PYTHON ?= python
+
+TEMPLATE=roundup.pot
+
+PACKAGES=$(shell find ../roundup -name '*.py'|sed -e 's,/[^/]*$$,,'|sort|uniq)
+SOURCES=$(PACKAGES:=/*.py)
+PO_FILES=$(wildcard *.po)
+MO_FILES=$(PO_FILES:.po=.mo)
+RUN_PYTHON=PYTHONPATH=../build/lib $(PYTHON) -O
+
+all: dist
+
+help:
+ @echo "$(MAKE) - build MO files. Run this before sdist"
+ @echo "$(MAKE) template - update message template from sources"
+ @echo "$(MAKE) locale.po - update message file from template"
+ @echo "$(MAKE) locale.mo - compile individual message file"
+ @echo "$(MAKE) help - this text"\
+
+# This will rebuild all MO files without updating their corresponding PO
+# files first. Run before creating Roundup distribution (hence the name).
+# PO files should be updated by their translators only, automatic update
+# adds unwanted fuzzy labels.
+dist:
+ for file in $(PO_FILES); do \
+ ${MSGFMT} -o `basename $$file .po`.mo $$file; \
+ done
+
+template:
+ ${XPOT} -n -o $(TEMPLATE) $(SOURCES)
+ ${RUN_PYTHON} ../roundup/cgi/TAL/talgettext.py -u $(TEMPLATE) \
+ ../templates/classic/html/*.html ../templates/minimal/html/*.html
+ ${XGETTEXT} -j -w 80 -F \
+ --msgid-bugs-address=roundup-devel at lists.sourceforge.net \
+ --copyright-holder="See Roundup README.txt" \
+ -o $(TEMPLATE) $(SOURCES)
+
+# helps to check template file before check in
+diff:
+ cvs diff roundup.pot|grep -v '^[-+]#'|vim -Rv -
+
+%.po: $(TEMPLATE)
+ ${MSGMERGE} -U --suffix=.bak $@ $<
+
+%.mo: %.po
+ ${MSGFMT} --statistics -o $@ $<
Added: tracker/vendor/roundup/current/locale/de.po
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/locale/de.po Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2989 @@
+# German message file for Roundup Issue Tracker
+# Stefan Niederhauser <stefan.niederhauser at unibas.ch>, 2004.
+#
+# $Id: de.po,v 1.3 2005/01/13 23:08:12 richard Exp $
+#
+# roundup.pot revision 1.8
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Roundup 0.7.0\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: 2004-07-05 15:00+0100\n"
+"Last-Translator: Stefan Niederhauser <stefan.niederhauser at unibas.ch>\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-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+
+# ../roundup/admin.py:83 :949 :998 :1020
+#: ../roundup/admin.py:84 ../roundup/admin.py:954 ../roundup/admin.py:1003
+#: ../roundup/admin.py:1025
+#, 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
+#, python-format
+msgid "argument \"%(arg)s\" not propname=value"
+msgstr ""
+"Der Parameter \"%(arg)s\" entspricht nicht dem Format Eigenschaft=Wert"
+
+#: ../roundup/admin.py:111
+#, python-format
+msgid ""
+"Problem: %(message)s\n"
+"\n"
+msgstr ""
+"Problem: %(message)s\n"
+"\n"
+
+#: ../roundup/admin.py:112
+#, 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"
+"\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 ""
+"%(message)sVerwendung: roundup-admin [Optionen] [<Befehl> <Parameter>]\n"
+"\n"
+"Optionen:\n"
+" -i <Instanzverzeichnis> -- Tracker-Instanz zur Administration auswählen\n"
+" -u -- Benutzer[:Password] für das Ausführen von Befehlen\n"
+" -d -- Lange Bezeichner anzeigen statt Klassen-Ids\n"
+" -c -- Komma-getrennte Listenausgabe (CSV).\n"
+" Analog zu '-S \",\"'.\n"
+" -S <Zeichenkette> -- Trennzeichen bei der Listenausgabe.\n"
+" -s -- Leerschlag als Trennzeichen verwenden.\n"
+" Analog zu '-S \" \"'.\n"
+"\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 Hilfe anzeigen\n"
+
+#: ../roundup/admin.py:137
+msgid "Commands:"
+msgstr "Befehle:"
+
+#: ../roundup/admin.py:144
+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"
+"z.B. l == li == lis == list."
+
+#: ../roundup/admin.py:174
+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 ""
+"\n"
+"Sie müssen für sämtliche Befehle - ausser 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"
+"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"
+" . 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"
+" 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"
+" angegeben.\n"
+" . Bei Mehrfach-Links werden die verlinkten Bezeichner mit Komma getrennt\n"
+" ausgegeben. Bei der Eingabe können Bezeichner oder Schlüssel\n"
+" mit Kommas getrennt eingegeben werden.\n"
+"\n"
+"Falls Eigenschaften Leerschläge enthalten, müssen die Werte in\n"
+"\"Anführungszeichen\" eingeschlossen werden. Leerschläge 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"
+" \"Hallo Welt\" (1 Wert: Hallo Welt)\n"
+" \"Sophie's\" Welt (2 Werte: Sophie's, 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"
+"Wenn bei einer Abfrage oder einer Änderung mehrere Einträge angegeben\n"
+"werden, so werden die gewünschten Eigenschaften aller Einträge angezeigt\n"
+"order 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 -c Option) kommagetrennet\n"
+"ausgegeben.\n"
+"\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:passwort\".\n"
+"\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"
+" \"08-13.22:13\" ergibt <Date yyyy-08-14.03:13:00>\n"
+" \"11-07.09:32:43\" ergibt <Date yyyy-11-07.14:32:43>\n"
+" \"14:25\" ergibt <Date yyyy-mm-dd.19:25:00>\n"
+" \"8:47:11\" ergibt <Date yyyy-mm-dd.13:47:11>\n"
+" \".\" ergibt \"jetzt\"\n"
+"\n"
+"Befehlshilfe:\n"
+
+#: ../roundup/admin.py:237
+#, python-format
+msgid "%s:"
+msgstr "%s:"
+
+#: ../roundup/admin.py:242
+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 ""
+"Verwendung: help Thema\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"
+" "
+
+#: ../roundup/admin.py:265
+#, 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
+msgid "Templates:"
+msgstr "Vorlagen:"
+
+# ../roundup/admin.py:339 :393
+#: ../roundup/admin.py:340 ../roundup/admin.py:397
+msgid "Back ends:"
+msgstr "Datenbanken:"
+
+#: ../roundup/admin.py:343
+msgid ""
+"Usage: install [template [backend [admin password]]]\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"
+"\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 ""
+"Verwendung: install [Vorlage [Datenbanktyp [Administratorpasswort]]]\n"
+" Installiert einen neuen Roundup-Tracker.\n"
+"\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"
+" passwort anzugeben.\n"
+" Sie können die Vorlage, den Datenbanktyp und das Passwort\n"
+" auch in dieser Reihenfolge auf der Kommandozeile angegen.\n"
+"\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"
+" Siehe auch unter dem Hilfethema \"initopts\".\n"
+" "
+
+# ../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
+msgid "Not enough arguments supplied"
+msgstr "Zu wenig Parameter übergeben"
+
+#: ../roundup/admin.py:365
+#, 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
+#, 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 ""
+"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: "
+
+#: ../roundup/admin.py:388
+msgid "Select template [classic]: "
+msgstr "Template auswählen [classic]:"
+
+#: ../roundup/admin.py:399
+msgid "Select backend [anydbm]: "
+msgstr "Datenbank auswählen [anydbm]"
+
+#: ../roundup/admin.py:408
+#, python-format
+msgid ""
+"\n"
+" You should now edit the tracker configuration file:\n"
+" %(config_file)s"
+msgstr ""
+"\n"
+" Sie sollten nun die Konfigurationsdatei des Trackers bearbeiten:\n"
+" %(config_file)s"
+
+#: ../roundup/admin.py:417
+msgid " ... at a minimum, you must set following options:"
+msgstr " ... passen sie zumindest folgende Optionen an:"
+
+#: ../roundup/admin.py:422
+#, 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"
+msgstr ""
+"\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"
+" %(database_init_file)s\n"
+" ... die Online-Dokumentation erhält ein eigenes Kapitel über Anpassungen.\n"
+
+#: ../roundup/admin.py:436
+msgid ""
+"Usage: genconfig <filename>\n"
+" Generate a new tracker config file (ini style) with default values\n"
+" in <filename>.\n"
+" "
+msgstr ""
+"Verwendung: genconfig <filename>\n"
+" Schreibt eine neue Tracker-Konfiguration (im \".ini\"-Format) mit \n"
+" Vorgabe-Werten in die Datei <filename>.\n"
+" "
+
+#. password
+#: ../roundup/admin.py:446
+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 ""
+"Verwendung: initialise [Administrationspasswort]\n"
+" Initialisieren eines neuen Roundup-Trackers.\n"
+"\n"
+" Der Administrator-Benutzer wird eingerichtet.\n"
+"\n"
+" Die Funktion dbinit.init() wirf aufgerufen\n"
+" "
+
+#: ../roundup/admin.py:460
+msgid "Admin Password: "
+msgstr "Admin Passwort: "
+
+#: ../roundup/admin.py:461
+msgid " Confirm: "
+msgstr " Bestätigen: "
+
+#: ../roundup/admin.py:465
+msgid "Instance home does not exist"
+msgstr "Tracker-Verzeichnis existiert nicht"
+
+#: ../roundup/admin.py:469
+msgid "Instance has not been installed"
+msgstr "Tracker-Instanz wurde nicht installiert"
+
+#: ../roundup/admin.py:474
+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: "
+
+#: ../roundup/admin.py:495
+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 ""
+"Verwendung: get Eigenschaft Bezeichner[,Bezeichner]*\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"
+" "
+
+# ../roundup/admin.py:516 :531
+#: ../roundup/admin.py:535 ../roundup/admin.py:550
+#, 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. Das Option -d trifft "
+"deshalb hier nicht zu."
+
+# ../roundup/admin.py:539 :951 :1000 :1022
+#: ../roundup/admin.py:558 ../roundup/admin.py:956 ../roundup/admin.py:1005
+#: ../roundup/admin.py:1027
+#, python-format
+msgid "no such %(classname)s node \"%(nodeid)s\""
+msgstr ""
+"Es existiert kein Eintrag der Klasse %(classname)s mit der ID \"%(nodeid)s\""
+
+#: ../roundup/admin.py:560
+#, python-format
+msgid "no such %(classname)s property \"%(propname)s\""
+msgstr ""
+"Die Eigenschaft \"%(propname)s\" ist nicht definiert für die Klasse \"%"
+"(classname)s\""
+
+#: ../roundup/admin.py:569
+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 ""
+"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 kommagetrennten Bezeichnern aufgeführen\n"
+" (\"Bezeichner[,Bezeichner]*\").\n"
+"\n"
+" Der Wert der Eigenschaft wird für alle angegebenen Eintrge 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 kommagetrennte ID-Nummern angegeben (\"1,2,3\").\n"
+" "
+
+#: ../roundup/admin.py:623
+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 ""
+"Verwendung: find Klassenname Eigenschaft=Wert ...\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"
+" 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
+#, python-format
+msgid "%(classname)s has no property \"%(propname)s\""
+msgstr "Die Klasse \"%(classname)s\" hat keine Eigenschaft \"%(propname)s\""
+
+#: ../roundup/admin.py:681
+msgid ""
+"Usage: specification classname\n"
+" Show the properties for a classname.\n"
+"\n"
+" This lists the properties for a given class.\n"
+" "
+msgstr ""
+"Verwendung: specification Klassenname\n"
+" Gibt die Spezifikation der Klasseneigenschaften aus.\n"
+"\n"
+" Listet sämtliche Eigenschaften der Klasse auf.\n"
+" "
+
+#: ../roundup/admin.py:696
+#, python-format
+msgid "%(key)s: %(value)s (key property)"
+msgstr "%(key)s: %(value)s (Schlüsseleigenschaft)"
+
+#: ../roundup/admin.py:698
+#, python-format
+msgid "%(key)s: %(value)s"
+msgstr "%(key)s: %(value)s"
+
+#: ../roundup/admin.py:701
+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 ""
+"Verwendung: display Bezeichner[,Bezeichner]*\n"
+" Zeigt alle Eigenschaften eines oder mehrerer Eintrage an.\n"
+"\n"
+" Der Befehl listet sämtliche Eigenschaften und Ihre Werte des\n"
+" Eintrages an.\n"
+" "
+
+#: ../roundup/admin.py:725
+#, python-format
+msgid "%(key)s: %(value)r"
+msgstr "%(key)s: %(value)r"
+
+#: ../roundup/admin.py:728
+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 ""
+"Verwendung: create Klassenname Eigenschaft=Wert ...\n"
+" Erstellt einen neuen Eintrag der angegebenen Klasse.\n"
+"\n"
+" Ein neuer Eintrag der Klasse wird erstellt und die Eigenschaften\n"
+" werden mit den Werten initialisiert\n"
+" "
+
+#: ../roundup/admin.py:755
+#, python-format
+msgid "%(propname)s (Password): "
+msgstr "%(propname)s (Passwort):"
+
+#: ../roundup/admin.py:757
+#, python-format
+msgid " %(propname)s (Again): "
+msgstr " %(propname)s (Wiederholen):"
+
+#: ../roundup/admin.py:759
+msgid "Sorry, try again..."
+msgstr "Bitte erneut versuchen..."
+
+#: ../roundup/admin.py:763
+#, python-format
+msgid "%(propname)s (%(proptype)s): "
+msgstr "%(propname)s (%(proptype)s): "
+
+#: ../roundup/admin.py:781
+#, python-format
+msgid "you must provide the \"%(propname)s\" property."
+msgstr "Sie müssen die Eigenschaft \"%(propname)s\" angeben."
+
+#: ../roundup/admin.py:792
+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 ""
+"Usage: list Klassenname [Eigenschaft]\n"
+" Listet sämtliche Einträge einer Klasse auf.\n"
+"\n"
+" Es werden sämtliche Einträge der Klasse ausgegeben. Wird keine\n"
+" Eigenschaft angegeben, so wird ein Bezeichner verwendet. Falls ein\n"
+" Schlüsselfeld existiert, wird dieses ausgegeben, sonst ein Feld "
+"namens \n"
+" \"name\" oder \"title\". Falls auch diese Felder nicht existieren, "
+"wird \n"
+" erste Feld alphabetisch sortiert angezeigt.\n"
+"\n"
+" Mit den Optionen -c, -S or -s wird eine Liste von ID's ausgegeben,\n"
+" falls keine Eigenschaft angegeben wird. Sonst werden die Werte\n"
+" dieser Eigenschaften sämtlicher Instanzen dieser Klasse "
+"aufgelistet.\n"
+" "
+
+#: ../roundup/admin.py:805
+msgid "Too many arguments supplied"
+msgstr "Sie haben zuviele Argumente übergeben"
+
+#: ../roundup/admin.py:841
+#, python-format
+msgid "%(nodeid)4s: %(value)s"
+msgstr "%(nodeid)4s: %(value)s"
+
+#: ../roundup/admin.py:845
+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 ""
+"Verwendung: table Klassenname [Eigenschaft[,Eigenschaft]*]\n"
+" Listet die Einträge einer Klasse in tabelarischer Form.\n"
+"\n"
+" 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"
+" angegeben als \"Eigenschaft:Breite\"\n"
+" Beispiel:\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"
+" Um die Spaltenbreite auf die Grösse des Spaltentitels zu "
+"bechränken,\n"
+" lassen Sie die Breitenangabe hinter dem : weg.\n"
+" Beispiel:\n"
+"\n"
+" roundup> table priority id,name:\n"
+" Id Name\n"
+" 1 fata\n"
+" 2 bug\n"
+" 3 usab\n"
+" 4 feat\n"
+"\n"
+" "
+
+#: ../roundup/admin.py:889
+#, python-format
+msgid "\"%(spec)s\" not name:width"
+msgstr "\"%(spec)s\" entspricht nicht dem Format Eigenschaft:Breite"
+
+#: ../roundup/admin.py:939
+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 ""
+"Verwendung: history Bezeichner\n"
+" Zeigt den Verlauf eines Eintrages an.\n"
+"\n"
+" Listet das Bearbeitungs-Journal des Eintrages mit dem angegebenen\n"
+" Bezeichner auf.\n"
+" "
+
+#: ../roundup/admin.py:960
+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 ""
+"Verwendung: commit\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"
+"\n"
+" Einzelbefehle über die Kommandozeile werden sofort in die Datenbank\n"
+" geschrieben.\n"
+" "
+
+#: ../roundup/admin.py:974
+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 ""
+"Verwendung: rollback\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"
+" verworfen.\n"
+" "
+
+#: ../roundup/admin.py:986
+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 ""
+"Verwendung: retire Bezeichner[,Bezeichner]*\n"
+" Verbirgt einen oder mehrere Einträge.\n"
+"\n"
+" Das Verbergen eines Eintrages bewirkt, dass dieser bei einer Suche\n"
+" nicht mehr angezeigt wird. Der Schlüssel des verborgenen Eintrages\n"
+" kann zudem wiederverwendet werden.\n"
+" "
+
+#: ../roundup/admin.py:1009
+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 ""
+"Verwendung: restore Bezeichner[,Bezeichner]*\n"
+" Ein oder mehrere verborgene Einträge werden wieder hergestellt.\n"
+"\n"
+" Ein verborgener Eintrag wird wieder hergestellt und ist danach\n"
+" für die Benutzer wieder sichtbar.\n"
+" "
+
+#. grab the directory to export to
+#: ../roundup/admin.py:1031
+msgid ""
+"Usage: export [class[,class]] export_dir\n"
+" Export the database to colon-separated-value files.\n"
+"\n"
+" Optionally limit the export to just the names classes.\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: export [Klasse[,Klasse]] Exportverzeichnis\n"
+" Exportiert die Datenbank in ein Verzeichnis mit CSV-Dateien.\n"
+"\n"
+" Optional kann der Export auf gewisse Klassen beschränkt werden.\n"
+"\n"
+" Die Daten werden als kommagetrennte Dateien in das angegebene\n"
+" Exportverzeichnis geschrieben.\n"
+" "
+
+#: ../roundup/admin.py:1084
+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 ""
+"Verwendung: import Importverzeichnis\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 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"
+" beschrieben.\n"
+"\n"
+" Importierte Einträge übernehmen die ID's, welche in den Dateien\n"
+" definiert sind. Existierende Einträge mit denselben ID's 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
+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 ""
+"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"
+"\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"
+"\n"
+" Das Datumsformat lautet \"JJJJ-MM-TT\", z.B:\n"
+" 2001-06-27\n"
+"\n"
+" "
+
+#: ../roundup/admin.py:1180
+msgid "Invalid format"
+msgstr "Ungültiges Format"
+
+#: ../roundup/admin.py:1190
+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 ""
+"Verwendung: reindex [Klasse|Bezeichner]*\n"
+" Der Volltext-Index eines Trackers wird neu erstellt.\n"
+"\n"
+" Der Volltext-Index wird neu generiert. Dieser Prozess geschieht \n"
+" normalerweise automatisch.\n"
+" "
+
+#: ../roundup/admin.py:1204
+#, python-format
+msgid "no such item \"%(designator)s\""
+msgstr "Der Eintrag \"%(designator)s\" existiert nicht"
+
+#: ../roundup/admin.py:1214
+msgid ""
+"Usage: security [Role name]\n"
+" Display the Permissions available to one or all Roles.\n"
+" "
+msgstr ""
+"Verwendung: security [Rollenname]\n"
+" Zeigt die Berechtigungen einer oder aller Rollen an.\n"
+" "
+
+#: ../roundup/admin.py:1222
+#, python-format
+msgid "No such Role \"%(role)s\""
+msgstr "Die Rolle \"%(role)s\" existiert nicht "
+
+#: ../roundup/admin.py:1228
+#, python-format
+msgid "New Web users get the Roles \"%(role)s\""
+msgstr "Neue Web-Benutzer erhalten die Rollen \"%(role)s\""
+
+#: ../roundup/admin.py:1230
+#, python-format
+msgid "New Web users get the Role \"%(role)s\""
+msgstr "Neue Web-Benutzer erhalten die Rolle \"%(role)s\""
+
+#: ../roundup/admin.py:1233
+#, python-format
+msgid "New Email users get the Roles \"%(role)s\""
+msgstr "Neue Email-Benutzer erhalten die Rollen \"%(role)s\""
+
+#: ../roundup/admin.py:1235
+#, python-format
+msgid "New Email users get the Role \"%(role)s\""
+msgstr "Neue Email-Benutzer erhalten die Rolle \"%(role)s\""
+
+#: ../roundup/admin.py:1238
+#, python-format
+msgid "Role \"%(name)s\":"
+msgstr "Rolle \"%(name)s\":"
+
+#: ../roundup/admin.py:1241
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
+msgstr "%(description)s (%(name)s einzig für \"%(klass)s\")"
+
+#: ../roundup/admin.py:1244
+#, python-format
+msgid " %(description)s (%(name)s)"
+msgstr " %(description)s (%(name)s)"
+
+#: ../roundup/admin.py:1273
+#, 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
+#, python-format
+msgid "Multiple commands match \"%(command)s\": %(list)s"
+msgstr "Zur Abkürzung \"%(command)s\" passen mehrere Befehle: %(list)s"
+
+#: ../roundup/admin.py:1286
+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
+#, python-format
+msgid "Error: %(message)s"
+msgstr "Fehler: %(message)s"
+
+#: ../roundup/admin.py:1307
+#, python-format
+msgid "Error: Couldn't open tracker: %(message)s"
+msgstr "Fehler: Die Tracker-Instanz konnte nicht geöffnet werden: %(message)s"
+
+#: ../roundup/admin.py:1332
+#, python-format
+msgid ""
+"Roundup %s ready for input.\n"
+"Type \"help\" for help."
+msgstr ""
+"Roundup %s ist bereit.\n"
+"Schreiben Sie \"help\", um zur Hilfe zu gelangen."
+
+#: ../roundup/admin.py:1337
+msgid "Note: command history and editing not available"
+msgstr "Bemerkung: Befehlsverlauf/-bearbeitung nicht verfügbar"
+
+#: ../roundup/admin.py:1341
+msgid "roundup> "
+msgstr "roundup> "
+
+#: ../roundup/admin.py:1343
+msgid "exit..."
+msgstr "beenden..."
+
+#: ../roundup/admin.py:1353
+msgid "There are unsaved changes. Commit them (y/N)? "
+msgstr "Es gibt noch ungespeicherte Änderungen. Änderungen speichern (y/N)?"
+
+#: ../roundup/backends/back_anydbm.py:2054
+#, python-format
+msgid "WARNING: invalid date tuple %r"
+msgstr "WARNUNG: ungültiges Datums-Tupel %r"
+
+#: ../roundup/backends/rdbms_common.py:1425
+msgid "create"
+msgstr "erstellt"
+
+#: ../roundup/backends/rdbms_common.py:1588
+msgid "unlink"
+msgstr "link gelöscht"
+
+#: ../roundup/backends/rdbms_common.py:1592
+msgid "link"
+msgstr "verlinkt"
+
+#: ../roundup/backends/rdbms_common.py:1702
+msgid "set"
+msgstr "geändert"
+
+#: ../roundup/backends/rdbms_common.py:1726
+msgid "retired"
+msgstr "verborgen"
+
+#: ../roundup/backends/rdbms_common.py:1756
+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 haben keine Berechtigung um die Aktion %(action)s auf die Klasse%"
+"(classname)s anzuwenden."
+
+#: ../roundup/cgi/actions.py:89
+msgid "No type specified"
+msgstr "Typ nicht spezifiziert"
+
+#: ../roundup/cgi/actions.py:91
+msgid "No ID entered"
+msgstr "Keine ID spezifiziert"
+
+#: ../roundup/cgi/actions.py:97
+#, 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:124
+#, python-format
+msgid "%(classname)s %(itemid)s has been retired"
+msgstr "%(classname)s %(itemid)s wurde gelöscht"
+
+#: ../roundup/cgi/actions.py:279
+#, python-format
+msgid "Not enough values on line %(line)s"
+msgstr "Nicht genügend Werte auf Zeile %(line)s"
+
+#: ../roundup/cgi/actions.py:326
+msgid "Items edited OK"
+msgstr "Die Einträge wurden aktualisiert"
+
+#: ../roundup/cgi/actions.py:386
+#, 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
+#, python-format
+msgid "%(class)s %(id)s - nothing changed"
+msgstr "%(class)s %(id)s - keine Änderungen"
+
+#: ../roundup/cgi/actions.py:401
+#, 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 haben keine Berechtigung die Einträge der Klasse \"%(class)s\" zu "
+"bearbeiten"
+
+#: ../roundup/cgi/actions.py:445
+#, python-format
+msgid "You do not have permission to create %(class)s"
+msgstr ""
+"Sie haben keine Berechtigung um 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 haben keine Berechtigung Benutzer-Rollen zu ändern"
+
+#: ../roundup/cgi/actions.py:530
+#, 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
+#, python-format
+msgid "Error: %s"
+msgstr "Fehler: %s"
+
+#: ../roundup/cgi/actions.py:598
+msgid ""
+"Invalid One Time Key!\n"
+"(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)"
+
+#: ../roundup/cgi/actions.py:640
+#, python-format
+msgid "Password reset and email sent to %s"
+msgstr "Ihr Passwort wurde zurückgesetzt und per Email an %s versandt"
+
+#: ../roundup/cgi/actions.py:649
+msgid "Unknown username"
+msgstr "Benutzername unbekannt"
+
+#: ../roundup/cgi/actions.py:657
+msgid "Unknown email address"
+msgstr "Email-Adresse unbekannt"
+
+#: ../roundup/cgi/actions.py:662
+msgid "You need to specify a username or address"
+msgstr "Sie müssen einen Benutzernamen oder eine Email-Adresse angeben"
+
+#: ../roundup/cgi/actions.py:687
+#, python-format
+msgid "Email sent to %s"
+msgstr "Eine Email wurde an %s versandt"
+
+#: ../roundup/cgi/actions.py:706
+msgid "You are now registered, welcome!"
+msgstr "Sie sind nun registriert. Willkommen!"
+
+#: ../roundup/cgi/actions.py:751
+msgid "It is not permitted to supply roles at registration."
+msgstr "Bei der Registrierung dürfen keine Rollen angegeben werden"
+
+#: ../roundup/cgi/actions.py:834
+msgid "You are logged out"
+msgstr "Sie wurden vom System abgemeldet"
+
+#: ../roundup/cgi/actions.py:845
+msgid "Username required"
+msgstr "Benutzername notwendig"
+
+#: ../roundup/cgi/actions.py:873 ../roundup/cgi/actions.py:877
+msgid "Invalid login"
+msgstr "Ungültiges Login"
+
+#: ../roundup/cgi/actions.py:883
+msgid "You do not have permission to login"
+msgstr "Sie haben keine Berechtigung sich anzumelden"
+
+#: ../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>Templating Fehler</h1>\n"
+"<p><b>%(exc_type)s</b>: %(exc_value)s</p>\n"
+"<p class=\"help\">Es folgen Informationen zum Fehler</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>Looking for \"%(name)s\", current path:<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 "Ein Problem ist im Template \"%s\" aufgetreten."
+
+#: ../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>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"
+" %(locals)s\n"
+"</table></li>\n"
+
+#: ../roundup/cgi/cgitb.py:103
+msgid "Full traceback:"
+msgstr "Vollständiger Traceback:"
+
+#: ../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>Ein Problem trat auf, als ein Python-Script ausgeführt wurde. Hier sehen "
+"Sie die Aufrufe, welche zu einem Fehler führten. Der letzten Aufruf erscheint "
+"zuoberst. Der Fehler hat folgende Attribute: "
+
+#: ../roundup/cgi/cgitb.py:129
+msgid "<file is None - probably inside <tt>eval</tt> or <tt>exec</tt>>"
+msgstr "<file ist None - Wahrscheinlich in einem <tt>eval</tt> oder einem "
+"<tt>exec</tt>>"
+
+#: ../roundup/cgi/cgitb.py:138
+#, python-format
+msgid "in <strong>%s</strong>"
+msgstr "in <strong>%s</strong>"
+
+# ../roundup/cgi/cgitb.py:145 :151
+#: ../roundup/cgi/cgitb.py:172 ../roundup/cgi/cgitb.py:178
+msgid "<em>undefined</em>"
+msgstr "<em>nicht definiert</em>"
+
+#: ../roundup/cgi/client.py:291
+msgid "Form Error: "
+msgstr "Formular-Fehler: "
+
+#: ../roundup/cgi/client.py:344
+#, python-format
+msgid "Unrecognized charset: %r"
+msgstr "Charset-Codierung nicht erkannt: %r"
+
+#: ../roundup/cgi/client.py:446
+msgid "Anonymous users are not allowed to use the web interface"
+msgstr ""
+"Gast-Benutzer haben nicht die Berechtigung, das Web-Interface zu benutzen."
+
+#: ../roundup/cgi/client.py:597
+msgid "You are not allowed to view this file."
+msgstr "Sie haben nicht die Berechtigung, diese Seite anzuzeigen."
+
+#: ../roundup/cgi/client.py:689
+#, python-format
+msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
+msgstr "%(starttag)sBenötigte Zeit: %(seconds)fs%(endtag)s\n"
+
+#: ../roundup/cgi/client.py:693
+#, 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"
+
+#: ../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\""
+
+#: ../roundup/cgi/form_parser.py:290
+#, 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
+#, 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
+#, 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"
+
+# ../roundup/cgi/form_parser.py:354 :360
+#: ../roundup/cgi/form_parser.py:354 ../roundup/cgi/form_parser.py:360
+msgid "Password and confirmation text do not match"
+msgstr "Die beiden Passwort-Felder stimmen nicht überein"
+
+#: ../roundup/cgi/form_parser.py:395
+#, 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\""
+
+#: ../roundup/cgi/form_parser.py:509
+#, 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\" 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
+msgid "File is empty"
+msgstr "Die ausgewählte Datei ist leer"
+
+#: ../roundup/cgi/templating.py:68
+#, python-format
+msgid "You are not allowed to %(action)s items of class %(class)s"
+msgstr ""
+"Sie haben keine Berechtigung, um die Aktion \"%(action)s\" auf Einträge der "
+"Klasse \"%(class)s\" anzuwenden"
+
+#: ../roundup/cgi/templating.py:612
+msgid "(list)"
+msgstr "(Liste)"
+
+#: ../roundup/cgi/templating.py:646
+msgid "Submit New Entry"
+msgstr "Eintrag speichern"
+
+#: ../roundup/cgi/templating.py:656
+msgid "New node - no history"
+msgstr "Neuer Eintrag - Noch kein Verlauf"
+
+#: ../roundup/cgi/templating.py:756
+msgid "Submit Changes"
+msgstr "Speichern"
+
+#: ../roundup/cgi/templating.py:837
+msgid "<em>The indicated property no longer exists</em>"
+msgstr "<em>Die gewählte Eigenschaft existiert nicht mehr</em>"
+
+#: ../roundup/cgi/templating.py:838
+#, python-format
+msgid "<em>%s: %s</em>\n"
+msgstr "<em>%s: %s</em>\n"
+
+#: ../roundup/cgi/templating.py:851
+#, 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
+msgid "<strike>The linked node no longer exists</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
+#, 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>Ereignis kann nicht im Verlauf angezeigt werden!</em></strong>"
+
+#: ../roundup/cgi/templating.py:979
+msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
+msgstr "<tr><td colspan=4><strong>Notiz:</strong></td></tr>"
+
+#: ../roundup/cgi/templating.py:988
+msgid "History"
+msgstr "Verlauf"
+
+#: ../roundup/cgi/templating.py:990
+msgid "<th>Date</th>"
+msgstr "<th>Datum</th>"
+
+#: ../roundup/cgi/templating.py:991
+msgid "<th>User</th>"
+msgstr "<th>Benutzer</th>"
+
+#: ../roundup/cgi/templating.py:992
+msgid "<th>Action</th>"
+msgstr "<th>Aktion</th>"
+
+#: ../roundup/cgi/templating.py:993
+msgid "<th>Args</th>"
+msgstr "<th>Argumente</th>"
+
+#: ../roundup/cgi/templating.py:1234
+msgid "*encrypted*"
+msgstr "*verschlüsselt*"
+
+#: ../roundup/cgi/templating.py:1412
+msgid ""
+"default value for DateHTMLProperty must be either DateHTMLProperty or string "
+"date representation."
+msgstr ""
+"Der voreingestellte Wert einer DateHTML-Eigenschaft muss entweder ein\n"
+"DateHTML Objekt sein oder ein Datum repräsentieren."
+
+#: ../roundup/cgi/templating.py:1600
+#, python-format
+msgid "<option %svalue=\"-1\">- no selection -</option>"
+msgstr "<option %svalue=\"-1\">- keine Auswahl -</option>"
+
+#: ../roundup/date.py:180
+#, python-format
+msgid "Not a date spec: %s"
+msgstr "Kein gültiges Datum: %s"
+
+#: ../roundup/date.py:231
+#, python-format
+msgid "%r not a date spec (%s)"
+msgstr "%r ist kein gültiges Datum (%s)"
+
+#: ../roundup/date.py:522
+msgid ""
+"Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
+msgstr ""
+"Fehler im Zeitperioden-Format: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date "
+"spec]"
+
+#: ../roundup/date.py:541
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+msgstr ""
+"Fehler im Intervall-Format: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date "
+"spec]"
+
+#: ../roundup/date.py:678
+#, 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
+#, 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
+#, 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
+#, 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
+msgid "tomorrow"
+msgstr "Morgen"
+
+#: ../roundup/date.py:696
+msgid "yesterday"
+msgstr "Gestern"
+
+#: ../roundup/date.py:699
+#, 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
+msgid "an hour"
+msgstr "eine Stunde"
+
+#: ../roundup/date.py:705
+msgid "1 1/2 hours"
+msgstr "1 1/2 Stunden"
+
+#: ../roundup/date.py:707
+#, python-format
+msgid "1 %(number)s/4 hours"
+msgid_plural "1 %(number)s/4 hours"
+msgstr[0] "1 %(number)s/4 Stunden"
+msgstr[1] "1 %(number)s/4 Stunden"
+
+#: ../roundup/date.py:711
+msgid "in a moment"
+msgstr "in Kürze"
+
+#: ../roundup/date.py:713
+msgid "just now"
+msgstr "Soeben"
+
+#: ../roundup/date.py:716
+msgid "1 minute"
+msgstr "1 Minute"
+
+#: ../roundup/date.py:719
+#, 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
+msgid "1/2 an hour"
+msgstr "1/2 Stunde"
+
+#: ../roundup/date.py:724
+#, python-format
+msgid "%(number)s/4 hour"
+msgid_plural "%(number)s/4 hours"
+msgstr[0] "%(number)s/4 Stunden"
+msgstr[1] "%(number)s/4 Stunden"
+
+#: ../roundup/date.py:728
+#, python-format
+msgid "%s ago"
+msgstr "vor %s"
+
+#: ../roundup/date.py:730
+#, python-format
+msgid "in %s"
+msgstr "in %s"
+
+#: ../roundup/init.py:132
+#, 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."
+
+#: ../roundup/roundupdb.py:141
+msgid "files"
+msgstr "Dateien"
+
+#: ../roundup/roundupdb.py:141
+msgid "messages"
+msgstr "Meldungen"
+
+#: ../roundup/roundupdb.py:141
+msgid "nosy"
+msgstr "Interessenten"
+
+#: ../roundup/roundupdb.py:141
+msgid "superseder"
+msgstr "Übergeordnet"
+
+#: ../roundup/roundupdb.py:141
+msgid "title"
+msgstr "Titel"
+
+#: ../roundup/roundupdb.py:142
+msgid "assignedto"
+msgstr "Zugewiesen"
+
+#: ../roundup/roundupdb.py:142
+msgid "priority"
+msgstr "Prioriät"
+
+#: ../roundup/roundupdb.py:142
+msgid "status"
+msgstr "Status"
+
+#: ../roundup/roundupdb.py:142
+msgid "topic"
+msgstr "Thema"
+
+#: ../roundup/roundupdb.py:145
+msgid "activity"
+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
+msgid "actor"
+msgstr "Akteur"
+
+#: ../roundup/roundupdb.py:145
+msgid "creation"
+msgstr "Erstellungsdatum"
+
+#: ../roundup/roundupdb.py:145
+msgid "creator"
+msgstr "Ersteller"
+
+#: ../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]: "
+
+#: ../roundup/scripts/roundup_gettext.py:22
+#, python-format
+msgid "Usage: %(program)s <tracker home>"
+msgstr "Verwendung: %(program)s <Tracker Verzeichnis>"
+
+#: ../roundup/scripts/roundup_gettext.py:37
+#, python-format
+msgid "No tracker templates found in directory %s"
+msgstr "Keine Tracker-Vorlage gefunden im Verzeichnis %s"
+
+#: ../roundup/scripts/roundup_mailgw.py:36
+#, python-format
+msgid ""
+"Usage: %(program)s [-v] [-c] [[-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"
+"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 ""
+"Verwendung: %(program)s [-v] [[-C Klasse] -S Eigenschaft=Wert]* <Tracker-"
+"Verzeichnis> [Methode]\n"
+"\n"
+"Optionen:\n"
+" -v: Versionsnummer ausgeben und beenden\n"
+" -c: Vorgegebene Klasse beim Erstellen eines Eintrages (sonst: MAIL_DEFAULT_CLASS)\n"
+" -C / -S: siehe Unten\n"
+"\n"
+"Das Roundup Mailgateway kann auf vier verschiedene Arten aufgerufen werden:\n"
+" . mit einem Tracker-Verzeichnis als einziges Argument,\n"
+" . mit einem Tracker-Verzeichnis und einer Mailbox Datei,\n"
+" . mit einem Tracker-Verzeichnis und einem POP/APOP Konto, oder\n"
+" . mit einem Tracker-Verzeichnis und einem IMAP/IMAPS Konto.\n"
+"\n"
+"Optional kann mit -C die Klasse des zu erstellenden Eintrages spezifiziert \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"
+"Voreingestellt ist die Klasse \"msg\", aber auch Klassen wie \"issue\",\n"
+"\"user\" oder \"file\" können verwendet werden.\n"
+"\n"
+"Sie können dadurch mehrere Email-Kontos für einen Tracker verwenden und\n"
+"unterschiedliche Eintragstypen aus den Meldungen erstellen.\n"
+"\n"
+"PIPE:\n"
+" Das Mail Gateway liest eine Meldung vom Standard-Input und übergibt die\n"
+" Meldung an das Modul roundup.mailgw.\n"
+"\n"
+"UNIX Mailbox:\n"
+" Die angegebene Mailbox-Datei wird ausgelesen und alle Meldungen werden\n"
+" an das Modul roundup.mailgw übergeben. Nach erfolgreicher Verarbeitung \n"
+" wird die Mail-Spool Datei geleert.\n"
+" Die Mailbox-Datei wird folgendermassen angegeben: mailbox /pfad/zur/"
+"mailbox\n"
+"\n"
+"POP:\n"
+" Das Gateway liest alle Meldungen vom POP3-Konto und leitet sie weiter an \n"
+" das Modul roundup.mailgw. \n"
+" Das Konto wird folgendermassen 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"
+"APOP:\n"
+" Wie POP aber unter Verwendung von authentifiziertem POP:\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:passwort at server\n"
+" Um eine andere Mailbox anstelle von \"INBOX\" zu verwenden, benutzen Sie:\n"
+" imap benutzername:passwort at server mailbox\n"
+"\n"
+"IMAPS:\n"
+" Verbindung zu einem IMAP-Server über eine sichere SSL-Verbindung.\n"
+" Die Syntax entspricht der IMAP-Spezifikation:\n"
+" imaps benutzername:passwort at server [mailbox]\n"
+"\n"
+
+#: ../roundup/scripts/roundup_mailgw.py:147
+msgid "Error: not enough source specification information"
+msgstr "Sie haben nicht genügend Angaben zur 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:164
+msgid "Error: apop specification not valid"
+msgstr "Fehler: apop Optionen ungültig"
+
+#: ../roundup/scripts/roundup_mailgw.py:178
+msgid ""
+"Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or "
+"\"imaps\""
+msgstr ""
+"Fehler: Als Mail-Quelle muss \"mailbox\", \"pop\", \"apop\", \"imap\" oder "
+"\"imaps\" gewählt werden"
+
+#: ../roundup/scripts/roundup_server.py:140
+msgid ""
+"<html><head><title>Roundup trackers index</title></head>\n"
+"<body><h1>Roundup trackers index</h1><ol>\n"
+msgstr ""
+"<html><head><title>Roundup Tracker-Liste</title></head>\n"
+"<body><h1>Roundup Tracker-Liste</h1><ol>\n"
+
+#: ../roundup/scripts/roundup_server.py:242
+#, python-format
+msgid "Error: %s: %s"
+msgstr "Fehler: %s: %s"
+
+#: ../roundup/scripts/roundup_server.py:252
+msgid "WARNING: ignoring \"-g\" argument, not root"
+msgstr ""
+"WARNUNG: die Option \"-g\" wird ignoriert, da Sie nicht Administrator sind"
+
+#: ../roundup/scripts/roundup_server.py:258
+msgid "Can't change groups - no grp module"
+msgstr "Die Gruppe kann nicht gewechselt werden - das grp Modul fehlt"
+
+#: ../roundup/scripts/roundup_server.py:267
+#, python-format
+msgid "Group %(group)s doesn't exist"
+msgstr "Die Gruppe %(group)s existiert nicht"
+
+#: ../roundup/scripts/roundup_server.py:278
+msgid "Can't run as root!"
+msgstr "Dieser Prozess kann nicht unter dem Administrator (\"root\") laufen!"
+
+#: ../roundup/scripts/roundup_server.py:281
+msgid "WARNING: ignoring \"-u\" argument, not root"
+msgstr ""
+"WARNUNG: die Option \"-u\" wird ignoriert, da Sie nicht Administrator sind"
+
+#: ../roundup/scripts/roundup_server.py:286
+msgid "Can't change users - no pwd module"
+msgstr "Der Benutzer kann nicht gewechselt werden - das pwd Modul fehlt"
+
+#: ../roundup/scripts/roundup_server.py:295
+#, python-format
+msgid "User %(user)s doesn't exist"
+msgstr "Der Benutzer %(user)s existiert nicht"
+
+#: ../roundup/scripts/roundup_server.py:417
+#, python-format
+msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
+msgstr "Der Multiprozess-Modus \"%s\" ist nicht verfügbar, Einprozess-Modus"
+"aktiviert"
+
+#: ../roundup/scripts/roundup_server.py:440
+#, 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
+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 ""
+" -c <Befehl> Windows Service Optionen.\n"
+" 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"
+" \"roundup-server -c help\" zeigt eine weitere Hilfe zum Thema."
+
+#: ../roundup/scripts/roundup_server.py:514
+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 ""
+" -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 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
+#, 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 ""
+"%(message)s"
+"Benutzung: roundup-server [Optionen] [Tracker-Name=Tracker-Verzeichnis]*\n"
+"\n"
+"Optionen:\n"
+" -v Versionsnummer ausgeben und beenden\n"
+" -h Diese Hilfe ausgeben und beenden\n"
+" -S Konfiguration erstellen oder aktualiseren und beenden\n"
+" -C <Datei> Konfiguration in <Datei> verwenden\n"
+" -n Hostname 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> Multiprozess-Modus (Voreinstellung: %(mp_def)s).\n"
+" Verfügbare Modi: %(mp_types)s.\n"
+"%(os_part)s\n"
+"\n"
+"Lange Optionen:\n"
+" --version Roundup Versionsnummer ausgeben und beenden\n"
+" --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"
+" auch in der Form --<Name>=<Wert> angegeben.\n"
+"\n"
+"Beispiele:\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"
+"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"
+"\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"
+" alternativ die Variable TRACKER_HOMES 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"
+" Zahlen und \"-_\".\n"
+
+#: ../roundup/scripts/roundup_server.py:669
+msgid "Instances must be name=home"
+msgstr "Instanzen müssen als Tracker-Name=Tracker-Verzeichnis angegeben werden"
+
+#: ../roundup/scripts/roundup_server.py:683
+#, python-format
+msgid "Configuration saved to %s"
+msgstr "Konfiguration in der Datei %s gespeichert"
+
+#: ../roundup/scripts/roundup_server.py:694
+msgid "Sorry, you can't run the server as a daemon on this Operating System"
+msgstr ""
+"Auf diesem Betriebssystem kann der Server nicht als Hintergrundprozess laufen"
+
+#: ../roundup/scripts/roundup_server.py:706
+#, 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
+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
+msgid "${class} Edit Collision"
+msgstr "Kollision bei der Bearbeitung"
+
+#: ../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"
+" 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}"
+
+#: ../templates/classic/html/_generic.help.html:30
+#: ../templates/minimal/html/_generic.help.html:30
+msgid " Cancel "
+msgstr " Abbrechen "
+
+#: ../templates/classic/html/_generic.help.html:33
+#: ../templates/minimal/html/_generic.help.html:33
+msgid " Apply "
+msgstr " Bestätigen "
+
+#: ../templates/classic/html/_generic.help.html:40
+#: ../templates/classic/html/issue.index.html:67
+#: ../templates/minimal/html/_generic.help.html:40
+msgid "<< previous"
+msgstr "<< zurück"
+
+#: ../templates/classic/html/_generic.help.html:50
+#: ../templates/classic/html/issue.index.html:75
+#: ../templates/minimal/html/_generic.help.html:50
+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
+msgid "next >>"
+msgstr "Weiter >>"
+
+#: ../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 "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
+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:10
+#: ../templates/classic/html/user.index.html:9
+#: ../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 haben nicht die Berechtigung, 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 die Einträge der Klasse \"${classname}\" "
+"mit diesem Formular bearbeiten. Kommas, Zeilenschaltungen und "
+"Anführungszeichen (\") mit Vorsicht verwenden. Kommas und Zeilenschaltungen "
+"dürfen nur Anführungszeichen (\") verwendet werden. Um Anführungszeichen in "
+"Werten zu verwenden, müssen Sie verdoppelt werden (\"\"). </p> <p class="
+"\"form-help\"> Mehrfachlinks werden durch Doppeltpunkt (\":\") getrennt "
+"(... ,\"eins:zwei:drei\", ...) </p> <p class=\"form-help\"> Einträge können "
+"gelöscht werden, indem Sie Zeilen entfernen. Fügen Sie Zeilen ein für neue "
+"Einträge und geben Sie bei der ID-Spalte ein X an. </p>"
+
+#: ../templates/classic/html/_generic.index.html:44
+#: ../templates/minimal/html/_generic.index.html:44
+msgid "Edit Items"
+msgstr "Einträge bearbeiten"
+
+#: ../templates/classic/html/file.index.html:4
+msgid "List of files - ${tracker}"
+msgstr "Dateiliste - ${tracker}"
+
+#: ../templates/classic/html/file.index.html:5
+msgid "List of files"
+msgstr "Dateiliste"
+
+#: ../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
+msgid "Content Type"
+msgstr "Inhaltstyp"
+
+#: ../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
+msgid "Date"
+msgstr "Datum"
+
+#: ../templates/classic/html/file.item.html:2
+msgid "File display - ${tracker}"
+msgstr "Datei anzeigen - ${tracker}"
+
+#: ../templates/classic/html/file.item.html:4
+msgid "File display"
+msgstr "Datei anzeigen"
+
+#: ../templates/classic/html/file.item.html:19
+#: ../templates/classic/html/file.item.html:47
+#: ../templates/classic/html/user.item.html:34
+#: ../templates/classic/html/user.register.html:17
+msgid "Name"
+msgstr "Name"
+
+#: ../templates/classic/html/file.item.html:41
+msgid "download"
+msgstr "herunterladen"
+
+#: ../templates/classic/html/home.classlist.html:2
+#: ../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
+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
+msgid "List of issues"
+msgstr "Aufgabenliste"
+
+#: ../templates/classic/html/issue.index.html:17
+#: ../templates/classic/html/issue.item.html:38
+msgid "Priority"
+msgstr "Priorität"
+
+#: ../templates/classic/html/issue.index.html:18
+msgid "ID"
+msgstr "ID"
+
+#: ../templates/classic/html/issue.index.html:19
+msgid "Creation"
+msgstr "Erstellungsdatum"
+
+#: ../templates/classic/html/issue.index.html:20
+msgid "Activity"
+msgstr "Aktivität"
+
+#: ../templates/classic/html/issue.index.html:21
+msgid "Actor"
+msgstr "Akteur"
+
+#: ../templates/classic/html/issue.index.html:22
+msgid "Topic"
+msgstr "Thema"
+
+#: ../templates/classic/html/issue.index.html:23
+#: ../templates/classic/html/issue.item.html:33
+msgid "Title"
+msgstr "Titel"
+
+#: ../templates/classic/html/issue.index.html:24
+#: ../templates/classic/html/issue.item.html:40
+msgid "Status"
+msgstr "Status"
+
+#: ../templates/classic/html/issue.index.html:25
+msgid "Creator"
+msgstr "Ersteller"
+
+#: ../templates/classic/html/issue.index.html:26
+msgid "Assigned To"
+msgstr "Zugewiesen"
+
+#: ../templates/classic/html/issue.index.html:90
+msgid "Download as CSV"
+msgstr "Als CSV-Datei herunterladen"
+
+#: ../templates/classic/html/issue.index.html:98
+msgid "Sort on:"
+msgstr "Sortieren:"
+
+#: ../templates/classic/html/issue.index.html:101
+#: ../templates/classic/html/issue.index.html:118
+msgid "- nothing -"
+msgstr "- nichts -"
+
+#: ../templates/classic/html/issue.index.html:109
+#: ../templates/classic/html/issue.index.html:126
+msgid "Descending:"
+msgstr "Absteigend:"
+
+#: ../templates/classic/html/issue.index.html:115
+msgid "Group on:"
+msgstr "Gruppieren:"
+
+#: ../templates/classic/html/issue.index.html:132
+msgid "Redisplay"
+msgstr "Aktualisieren"
+
+#: ../templates/classic/html/issue.item.html:7
+msgid "Issue ${id}: ${title} - ${tracker}"
+msgstr "Aufgabe ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:10
+msgid "New Issue - ${tracker}"
+msgstr "Neue Aufgabe - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:14
+msgid "New Issue"
+msgstr "Neue Aufgabe"
+
+#: ../templates/classic/html/issue.item.html:16
+msgid "New Issue Editing"
+msgstr "Neue Aufgabe bearbeiten"
+
+#: ../templates/classic/html/issue.item.html:19
+msgid "Issue${id}"
+msgstr "Aufgabe${id}"
+
+#: ../templates/classic/html/issue.item.html:22
+msgid "Issue${id} Editing"
+msgstr "Aufgabe${id} bearbeiten"
+
+#: ../templates/classic/html/issue.item.html:45
+msgid "Superseder"
+msgstr "Übergeordnete Aufgabe"
+
+#: ../templates/classic/html/issue.item.html:50
+msgid "View: ${link}"
+msgstr "Anzeigen: ${link}"
+
+#: ../templates/classic/html/issue.item.html:54
+msgid "Nosy List"
+msgstr "Interessenten"
+
+#: ../templates/classic/html/issue.item.html:63
+msgid "Assigned To"
+msgstr "Zugewiesen"
+
+#: ../templates/classic/html/issue.item.html:65
+msgid "Topics"
+msgstr "Themen"
+
+#: ../templates/classic/html/issue.item.html:73
+msgid "Change Note"
+msgstr "Änderungsnotiz"
+
+#: ../templates/classic/html/issue.item.html:81
+msgid "File"
+msgstr "Datei"
+
+#: ../templates/classic/html/issue.item.html:100
+msgid ""
+"<table class=\"form\"> <tr> <td>Note: </td> <th class=\"required"
+"\">highlighted</th> <td> fields are required.</td> </tr> </table>"
+msgstr ""
+"<table class=\"form\"> <tr> <td>Bemerkungen: </td> <th class=\"required"
+"\">Fett markierte</th> <td> 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>."
+
+#: ../templates/classic/html/issue.item.html:118
+#: ../templates/classic/html/msg.item.html:51
+msgid "Files"
+msgstr "Dateien"
+
+#: ../templates/classic/html/issue.item.html:120
+#: ../templates/classic/html/msg.item.html:53
+msgid "File name"
+msgstr "Dateiname"
+
+#: ../templates/classic/html/issue.item.html:121
+#: ../templates/classic/html/msg.item.html:54
+msgid "Uploaded"
+msgstr "Hochgeladen"
+
+#: ../templates/classic/html/issue.item.html:122
+msgid "Type"
+msgstr "Typ"
+
+#: ../templates/classic/html/issue.item.html:123
+#: ../templates/classic/html/query.edit.html:30
+msgid "Edit"
+msgstr "Bearbeiten"
+
+#: ../templates/classic/html/issue.item.html:124
+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
+msgid "remove"
+msgstr "Nein"
+
+#: ../templates/classic/html/issue.item.html:151
+#: ../templates/classic/html/msg.index.html:9
+msgid "Messages"
+msgstr "Meldungen"
+
+#: ../templates/classic/html/issue.item.html:155
+msgid "msg${id} (view)"
+msgstr "msg${id} (betrachten)"
+
+#: ../templates/classic/html/issue.item.html:156
+msgid "Author: ${author}"
+msgstr "Autor: ${author}"
+
+#: ../templates/classic/html/issue.item.html:158
+msgid "Date: ${date}"
+msgstr "Datum: ${date}"
+
+#: ../templates/classic/html/issue.search.html:2
+msgid "Issue searching - ${tracker}"
+msgstr "Aufgaben suchen - ${tracker}"
+
+#: ../templates/classic/html/issue.search.html:4
+msgid "Issue searching"
+msgstr "Aufgaben suchen"
+
+#: ../templates/classic/html/issue.search.html:25
+msgid "Filter on"
+msgstr "Filtern"
+
+#: ../templates/classic/html/issue.search.html:26
+msgid "Display"
+msgstr "Anzeigen"
+
+#: ../templates/classic/html/issue.search.html:27
+msgid "Sort on"
+msgstr "Sortieren"
+
+#: ../templates/classic/html/issue.search.html:28
+msgid "Group on"
+msgstr "Gruppieren"
+
+#: ../templates/classic/html/issue.search.html:32
+msgid "All text*:"
+msgstr "Volltext*:"
+
+#: ../templates/classic/html/issue.search.html:40
+msgid "Title:"
+msgstr "Titel:"
+
+#: ../templates/classic/html/issue.search.html:50
+msgid "Topic:"
+msgstr "Thema:"
+
+#: ../templates/classic/html/issue.search.html:58
+msgid "ID:"
+msgstr "ID:"
+
+#: ../templates/classic/html/issue.search.html:66
+msgid "Creation Date:"
+msgstr "Erstellungsdatum:"
+
+#: ../templates/classic/html/issue.search.html:77
+msgid "Creator:"
+msgstr "Ersteller:"
+
+#: ../templates/classic/html/issue.search.html:79
+msgid "created by me"
+msgstr "Durch mich erstellt"
+
+#: ../templates/classic/html/issue.search.html:88
+msgid "Activity:"
+msgstr "Aktivität:"
+
+#: ../templates/classic/html/issue.search.html:99
+msgid "Actor:"
+msgstr "Akteur:"
+
+#: ../templates/classic/html/issue.search.html:101
+msgid "done by me"
+msgstr "durch mich"
+
+#: ../templates/classic/html/issue.search.html:112
+msgid "Priority:"
+msgstr "Priorität:"
+
+#: ../templates/classic/html/issue.search.html:114
+#: ../templates/classic/html/issue.search.html:130
+msgid "not selected"
+msgstr "Nicht gewählt"
+
+#: ../templates/classic/html/issue.search.html:125
+msgid "Status:"
+msgstr "Status:"
+
+#: ../templates/classic/html/issue.search.html:128
+msgid "not resolved"
+msgstr "Ungelöst"
+
+#: ../templates/classic/html/issue.search.html:143
+msgid "Assigned to:"
+msgstr "Zugewiesen:"
+
+#: ../templates/classic/html/issue.search.html:146
+msgid "assigned to me"
+msgstr "Mir zugewiesen"
+
+#: ../templates/classic/html/issue.search.html:148
+msgid "unassigned"
+msgstr "Nicht zugewiesen"
+
+#: ../templates/classic/html/issue.search.html:158
+msgid "Pagesize:"
+msgstr "Pro Seite:"
+
+#: ../templates/classic/html/issue.search.html:164
+msgid "Start With:"
+msgstr "Starten bei:"
+
+#: ../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:184
+msgid "Query name**:"
+msgstr "Speichern unter**:"
+
+#: ../templates/classic/html/issue.search.html:194
+#: ../templates/classic/html/page.html:47
+msgid "Search"
+msgstr "Suchen"
+
+#: ../templates/classic/html/issue.search.html:198
+msgid ""
+"*: The \"all text\" field will look in message bodies and issue titles<br> "
+"**: If you supply a name, the query will be saved off and available as a "
+"link in the sidebar"
+msgstr ""
+"*: Das Feld \"Volltext\" durchsucht Titel von Aufgaben und Meldungstexte<br> "
+"**: Geben Sie einen Namen für diese Abfrage ein, um sie in der Seitenleiste "
+"zu speichern. "
+
+#: ../templates/classic/html/keyword.item.html:3
+msgid "Keyword editing - ${tracker}"
+msgstr "Stichwort bearbeiten - ${tracker}"
+
+#: ../templates/classic/html/keyword.item.html:5
+msgid "Keyword editing"
+msgstr "Stichwort bearbeiten"
+
+#: ../templates/classic/html/keyword.item.html:11
+msgid "Existing Keywords"
+msgstr "Vorhandene Stichworte"
+
+#: ../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 Stichwort zu bearbeiten, klicken Sie darauf."
+
+#: ../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 Stichwort hinzufügen, tragen Sie es hier ein und klicken Sie "
+"auf \"Eintrag speichern\"."
+
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
+msgstr "Stichwort"
+
+#: ../templates/classic/html/msg.index.html:3
+msgid "List of messages - ${tracker}"
+msgstr "Meldungsliste - ${tracker}"
+
+#: ../templates/classic/html/msg.index.html:5
+msgid "Message listing"
+msgstr "Meldungsliste"
+
+#: ../templates/classic/html/msg.item.html:6
+msgid "Message ${id} - ${tracker}"
+msgstr "Meldung ${id} - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:9
+msgid "New Message - ${tracker}"
+msgstr "Neue Meldung - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:13
+msgid "New Message"
+msgstr "Neue Meldung"
+
+#: ../templates/classic/html/msg.item.html:15
+msgid "New Message Editing"
+msgstr "Neue Meldung bearbeiten"
+
+#: ../templates/classic/html/msg.item.html:18
+msgid "Message${id}"
+msgstr "Message${id}"
+
+#: ../templates/classic/html/msg.item.html:21
+msgid "Message${id} Editing"
+msgstr "Meldung${id} bearbeiten"
+
+#: ../templates/classic/html/msg.item.html:28
+msgid "Author"
+msgstr "Autor"
+
+#: ../templates/classic/html/msg.item.html:33
+msgid "Recipients"
+msgstr "Empfänger"
+
+#: ../templates/classic/html/msg.item.html:44
+msgid "Content"
+msgstr "Inhalt"
+
+#: ../templates/classic/html/page.html:28
+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
+msgid "Issues"
+msgstr "Aufgaben"
+
+#: ../templates/classic/html/page.html:41
+#: ../templates/classic/html/page.html:60
+msgid "Create New"
+msgstr "Neuer Eintrag"
+
+#: ../templates/classic/html/page.html:43
+msgid "Show Unassigned"
+msgstr "Nicht zugewiesen"
+
+#: ../templates/classic/html/page.html:45
+msgid "Show All"
+msgstr "Alle anzeigen"
+
+#: ../templates/classic/html/page.html:48
+msgid "Show issue:"
+msgstr "Aufgabe zeigen:"
+
+#: ../templates/classic/html/page.html:58
+msgid "Keywords"
+msgstr "Stichworte"
+
+#: ../templates/classic/html/page.html:64
+msgid "Edit Existing"
+msgstr "Bearbeiten"
+
+#: ../templates/classic/html/page.html:70
+#: ../templates/minimal/html/page.html:48
+msgid "Administration"
+msgstr "Administration"
+
+#: ../templates/classic/html/page.html:72
+#: ../templates/minimal/html/page.html:49
+msgid "Class List"
+msgstr "Klassenliste"
+
+#: ../templates/classic/html/page.html:76
+#: ../templates/minimal/html/page.html:51
+msgid "User List"
+msgstr "Benutzerliste"
+
+#: ../templates/classic/html/page.html:78
+#: ../templates/minimal/html/page.html:54
+msgid "Add User"
+msgstr "Benutzer hinzufügen"
+
+#: ../templates/classic/html/page.html:85
+#: ../templates/classic/html/page.html:89
+#: ../templates/minimal/html/page.html:30
+msgid "Login"
+msgstr "Anmelden"
+
+#: ../templates/classic/html/page.html:91
+#: ../templates/classic/html/user.register.html:63
+#: ../templates/minimal/html/page.html:33
+#: ../templates/minimal/html/user.register.html:58
+msgid "Register"
+msgstr "Registrieren"
+
+#: ../templates/classic/html/page.html:94
+msgid "Lost your login?"
+msgstr "Passwort vergessen?"
+
+#: ../templates/classic/html/page.html:99
+msgid "Hello, ${user}"
+msgstr "Guten Tag, ${user}"
+
+#: ../templates/classic/html/page.html:101
+msgid "Your Issues"
+msgstr "Ihre Aufgaben"
+
+#: ../templates/classic/html/page.html:102
+#: ../templates/minimal/html/page.html:40
+msgid "Your Details"
+msgstr "Ihr Konto"
+
+#: ../templates/classic/html/page.html:104
+#: ../templates/minimal/html/page.html:42
+msgid "Logout"
+msgstr "Abmelden"
+
+#: ../templates/classic/html/page.html:108
+msgid "Help"
+msgstr "Hilfe"
+
+#: ../templates/classic/html/page.html:109
+msgid "Roundup docs"
+msgstr "Roundup Handbuch"
+
+#: ../templates/classic/html/page.html:160
+msgid "don't care"
+msgstr "egal"
+
+#: ../templates/classic/html/page.html:162
+msgid "------------"
+msgstr "------------"
+
+#: ../templates/classic/html/page.html:188
+msgid "no value"
+msgstr "kein Wert"
+
+#: ../templates/classic/html/query.edit.html:4
+msgid "\"Your Queries\" Editing - ${tracker}"
+msgstr "\"Abfragen\" berabeiten - ${tracker}"
+
+#: ../templates/classic/html/query.edit.html:6
+msgid "\"Your Queries\" Editing"
+msgstr "\"Abfragen\" berabeiten"
+
+#: ../templates/classic/html/query.edit.html:11
+msgid "You are not allowed to edit queries."
+msgstr "Sie haben keine Berechtigung um Abfragen zu bearbeiten."
+
+#: ../templates/classic/html/query.edit.html:28
+msgid "Query"
+msgstr "Abfrage"
+
+#: ../templates/classic/html/query.edit.html:29
+msgid "Include in \"Your Queries\""
+msgstr "Unter \"Abfragen\" aufführen"
+
+#: ../templates/classic/html/query.edit.html:31
+msgid "Private to you?"
+msgstr "Nur für Sie?"
+
+#: ../templates/classic/html/query.edit.html:44
+msgid "leave out"
+msgstr "Nein"
+
+#: ../templates/classic/html/query.edit.html:45
+msgid "include"
+msgstr "Ja"
+
+#: ../templates/classic/html/query.edit.html:49
+msgid "leave in"
+msgstr "Ja"
+
+#: ../templates/classic/html/query.edit.html:54
+msgid "[query is retired]"
+msgstr "[Abfrage ist verborgen]"
+
+#: ../templates/classic/html/query.edit.html:67
+msgid "edit"
+msgstr "bearbeiten"
+
+#: ../templates/classic/html/query.edit.html:71
+msgid "yes"
+msgstr "ja"
+
+#: ../templates/classic/html/query.edit.html:73
+msgid "no"
+msgstr "nein"
+
+#: ../templates/classic/html/query.edit.html:79
+msgid "Delete"
+msgstr "Löschen"
+
+#: ../templates/classic/html/query.edit.html:90
+msgid "[not yours to edit]"
+msgstr "[nicht Ihr Eintrag]"
+
+#: ../templates/classic/html/query.edit.html:96
+msgid "Save Selection"
+msgstr "Auswahl speichern"
+
+#: ../templates/classic/html/user.forgotten.html:3
+msgid "Password reset request - ${tracker}"
+msgstr "Passwort zurücksetzen - ${tracker}"
+
+#: ../templates/classic/html/user.forgotten.html:5
+msgid "Password reset request"
+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 Passwort zurückzusetzen, geben Sie entweder die Email-Adresse an, mit "
+"welcher Sie sich registriert haben..."
+
+#: ../templates/classic/html/user.forgotten.html:16
+msgid "Email Address:"
+msgstr "Email-Adresse"
+
+#: ../templates/classic/html/user.forgotten.html:24
+#: ../templates/classic/html/user.forgotten.html:34
+msgid "Request password reset"
+msgstr "Passwort zurücksetzen"
+
+#: ../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
+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 ein Bestätigungs-Email verschickt. Bitte folgen Sie den "
+"Anweisungen im Email, um ihr Passwort zurückzusetzen."
+
+#: ../templates/classic/html/user.index.html:3
+#: ../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
+msgid "User listing"
+msgstr "Benutzerliste"
+
+#: ../templates/classic/html/user.index.html:14
+#: ../templates/minimal/html/user.index.html:14
+msgid "Username"
+msgstr "Benutzername"
+
+#: ../templates/classic/html/user.index.html:15
+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
+msgid "Organisation"
+msgstr "Organisation"
+
+#: ../templates/classic/html/user.index.html:17
+#: ../templates/minimal/html/user.index.html:15
+msgid "Email address"
+msgstr "Email-Adresse"
+
+#: ../templates/classic/html/user.index.html:18
+msgid "Phone number"
+msgstr "Telefonnummer"
+
+#: ../templates/classic/html/user.index.html:19
+msgid "Retire"
+msgstr "Verbergen"
+
+#: ../templates/classic/html/user.index.html:32
+msgid "retire"
+msgstr "verbergen"
+
+#: ../templates/classic/html/user.item.html:7
+msgid "User ${id}: ${title} - ${tracker}"
+msgstr "Benutzer ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:10
+msgid "New User - ${tracker}"
+msgstr "Neuer Benutzer - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:14
+#: ../templates/minimal/html/user.item.html:6
+msgid "New User"
+msgstr "Neuer Benutzer"
+
+#: ../templates/classic/html/user.item.html:16
+#: ../templates/minimal/html/user.item.html:8
+msgid "New User Editing"
+msgstr "Neuen Benutzer bearbeiten"
+
+#: ../templates/classic/html/user.item.html:19
+#: ../templates/minimal/html/user.item.html:11
+msgid "User${id}"
+msgstr "Benutzer${id}"
+
+#: ../templates/classic/html/user.item.html:22
+#: ../templates/minimal/html/user.item.html:14
+msgid "User${id} Editing"
+msgstr "Benutzer${id} bearbeiten"
+
+#: ../templates/classic/html/user.item.html:38
+#: ../templates/classic/html/user.register.html:21
+#: ../templates/minimal/html/user.item.html:27
+#: ../templates/minimal/html/user.item.html:67
+#: ../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 "Passwort"
+
+#: ../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 "Passwort 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
+msgid "Roles"
+msgstr "Rollen"
+
+#: ../templates/classic/html/user.item.html:56
+msgid "(to give the user more than one role, enter a comma,separated,list)"
+msgstr "Verwenden,Sie,Kommas, 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"
+
+#: ../templates/classic/html/user.item.html:73
+msgid "(this is a numeric hour offset, the default is ${zone})"
+msgstr "Zeitverschiebung in Stunden - Voreinstellung: ${zone}"
+
+#: ../templates/classic/html/user.item.html:78
+#: ../templates/classic/html/user.register.html:49
+#: ../templates/minimal/html/user.item.html:47
+#: ../templates/minimal/html/user.item.html:71
+#: ../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
+msgid "Alternate E-mail addresses<br>One address per line"
+msgstr "Alternative Email-Adressen<br>Eine pro Zeile"
+
+#: ../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 "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 Registration is am laufen - ${tracker}"
+
+#: ../templates/classic/html/user.rego_progress.html:6
+#: ../templates/minimal/html/user.rego_progress.html:6
+msgid "Registration in progress..."
+msgstr "Die Registration ist am 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 ein Bestätigungs-Email erhalten. Um die Registrierung "
+"abzuschliessen, klicken Sie auf den Link im Email."
+
+#: ../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"
+
+#: ../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 "Guten Tag,<br>${user}"
+
+#: ../templates/minimal/html/user.item.html:3
+msgid "User editing - ${tracker}"
+msgstr "Benutzer bearbeiten - ${tracker}"
Added: tracker/vendor/roundup/current/locale/en.po
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/locale/en.po Sun Nov 5 21:30:25 2006
@@ -0,0 +1,20 @@
+# English message file for Roundup Issue Tracker
+#
+# $Id: en.po,v 1.2 2004/11/20 11:54:32 a1s Exp $
+#
+# roundup.pot revision 1.9
+#
+# Currently Roundup has no strings that need english translation.
+# This file is a dummy needed to provide the user with english UI
+# if 'en' is the first item in locale preference list and the list
+# also contains existing Roundup locale name.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Roundup 0.7.0\n"
+"Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
+"POT-Creation-Date: 2004-07-13 13:24+0300\n"
+"PO-Revision-Date: 2004-11-20 13:47+0200\n"
+"Language-Team: English\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=us-ascii\n"
Added: tracker/vendor/roundup/current/locale/es_AR.po
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/locale/es_AR.po Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2978 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR See Roundup README.txt
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Roundup 0.8.4\n"
+"Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
+"POT-Creation-Date: 2005-07-05 09:37+0300\n"
+"PO-Revision-Date: 2005-08-01 10:57:00-0300\n"
+"Last-Translator: Ramiro Morales <rm0 at gmx.net>\n"
+"Language-Team: Spanish Translators <roundup-devel at lists.sourceforge.net>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=iso-8859-1\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+# ../roundup/admin.py:85 :955 :1004 :1026
+#: ../roundup/admin.py:85
+#: ../roundup/admin.py:962
+#: ../roundup/admin.py:1011
+#: ../roundup/admin.py:1033
+#, python-format
+msgid "no such class \"%(classname)s\""
+msgstr "la clase \"%(classname)s\" no existe"
+
+# ../roundup/admin.py:95 :99
+#: ../roundup/admin.py:95
+#: ../roundup/admin.py:99
+#, python-format
+msgid "argument \"%(arg)s\" not propname=value"
+msgstr "el argumento \"%(arg)s\" no es de la forma nombrepropiedad=valor"
+
+#: ../roundup/admin.py:112
+#, 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"
+"\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 ""
+"%(message)sUso: roundup-admin [opciones] [<comando> <argumentos>]\n"
+"\n"
+"Opciones:\n"
+" -i base de instancia -- especifica el \"directorio base\" del issue tracker\n"
+" sobre el cual se va a actuar\n"
+" -u -- usuario[:contraseña] a usarse para los comandos\n"
+" -d -- imprime designadores completos, no solamente números\n"
+" de identificación de clases\n"
+" -c -- separa los elementos con comas cuando se generan listas de\n"
+" salida.\n"
+" Equivalente a '-S \",\"'.\n"
+" -S <string> -- separa los elementos con cadenas cuando se generan listas\n"
+" de salida\n"
+" -s -- separa los elementos con espacios cuando se generan listas\n"
+" de salida\n"
+" Equivalente a '-S \" \"'.\n"
+"\n"
+" Sólo puede especificarse una de las opciones -s, -c o -S.\n"
+"\n"
+"Ayuda:\n"
+" roundup-admin -h\n"
+" roundup-admin help -- esta ayuda\n"
+" roundup-admin help <comando> -- ayuda específica a un comando\n"
+" roundup-admin help all -- toda la ayuda disponible\n"
+
+#: ../roundup/admin.py:138
+msgid "Commands:"
+msgstr "Comandos:"
+
+#: ../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 ""
+"Los comandos pueden ser abreviados siempre y cuando la abreviación\n"
+"coincida con sólo un comando, ej. l == li == lis == list."
+
+#: ../roundup/admin.py:175
+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 ""
+"\n"
+"Todos los comandos (excepto ayuda) requieren un especificador de tracker. Este es\n"
+"simplemente la ruta al tracker roundup con el que se está trabajando. Un\n"
+"tracker roundup es donde roundup mantiene la base de datos y el archivo de\n"
+"configuración que define un issue tracker. Puede pensarse en el mismo como el\n"
+"\"directorio personal\" del issue tracker. Puede especificarse en la variable\n"
+"de entorno TRACKER_HOME o en la línea de comandos como \"-i tracker\".\n"
+"\n"
+"Un designador es un nombre de clase y un id de nodo concatenados, ej. bug1, user10, ...\n"
+"\n"
+"Los valores de propiedades se representan como cadenas en argumentos de comandos y en\n"
+"resultados visualizados:\n"
+" . Las cadenas son, ahem, cadenas.\n"
+" . Los valores de fechas se imprimen en el formato de fecha completo y en el huso\n"
+" horario local y se aceptan en el formato completo o cualquiera de los\n"
+" formatos parciales descriptos mas abajo.\n"
+" . Los valores Link se imprimen como designadores de nodos. Cuando se pasan como\n"
+" argumentos, se aceptan tanto los designadores de nodos como las cadenas clave.\n"
+" . Los valores Multilink se imprimen como listas de designadores de nodos unidos por\n"
+" comas. Cuando se pasan como argumentos, se aceptan tanto los designadores de\n"
+" nodos como las cadenas clave; tambien se aceptan una cadena vacía, un nodo\n"
+" individual, o una lista de nodos unidos por comas.\n"
+"\n"
+"Cuando los valores de las propiedades deben contener espacios, simplemente\n"
+"escriba el valor entre comillas simples (') o dobles (\"). Un caracter espacio\n"
+"individual puede ser tambien representado prefijándolo con un caracter barra\n"
+"invertida (\\). Si un valor debe incluir un caracter comillas, debe prefijarse\n"
+"el mismo con un caracter barra invertida o escribirse entre comillas. Ejemplos:\n"
+" hello world (2 unidades: hello, world)\n"
+" \"hello world\" (1 unidad: hello world)\n"
+" \"Roch'e\" Compaan (2 unidades: Roch'e Compaan)\n"
+" Roch\\'e Compaan (2 unidades: Roch'e Compaan)\n"
+" address=\"1 2 3\" (1 unidad: address=1 2 3)\n"
+" \\\\ (1 unidad: \\)\n"
+" \\n"
+"\\r\\t (1 unidad: una línea nueva, retorno de carro y tabulación)\n"
+"\n"
+"Cuando se especifican múltiples nodos a los comandos roundup get y roundup set,\n"
+"los valores de las propiedades especificadas son recuperados o asignados en\n"
+"todos los nodos listados.\n"
+"\n"
+"Cuando los comandos roundup get o roundup find retornan múltiples resultados\n"
+"los mismos son impresos uno por línea (comportamiento por omisión) o unidos\n"
+"por comas (con la opción -c).\n"
+"\n"
+"Cuando un comando modifica algún dato, es obligatorio el uso de nombre de\n"
+"usuario/contraseña. Esta información puede especificarse como \"nombre\" o\n"
+"\"nombre:contraseña\" en\n"
+" . La variable de entorno ROUNDUP_LOGIN\n"
+" . La opción de línea de comandos -u\n"
+"Si no se proveen ya sea el nombre de usuario o la contraseña, los mismos se\n"
+"obtendrán de la línea de comandos.\n"
+"\n"
+"Ejemplos de formatos de fecha:\n"
+" \"2000-04-17.03:45\" significa <Date 2000-04-17.08:45:00>\n"
+" \"2000-04-17\" significa <Date 2000-04-17.00:00:00>\n"
+" \"01-25\" significa <Date yyyy-01-25.00:00:00>\n"
+" \"08-13.22:13\" significa <Date yyyy-08-14.03:13:00>\n"
+" \"11-07.09:32:43\" significa <Date yyyy-11-07.14:32:43>\n"
+" \"14:25\" significa <Date yyyy-mm-dd.19:25:00>\n"
+" \"8:47:11\" significa <Date yyyy-mm-dd.13:47:11>\n"
+" \".\" significa \"right now\"\n"
+"\n"
+"Ayuda del comando:\n"
+
+#: ../roundup/admin.py:238
+#, python-format
+msgid "%s:"
+msgstr ""
+
+#: ../roundup/admin.py:243
+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 ""
+"Uso: help tópico\n"
+" Visualiza ayuda acerca del tópico.\n"
+"\n"
+" commands -- lista los comandos\n"
+" <comando> -- ayuda específica a un comando\n"
+" initopts -- opciones del comando init\n"
+" all -- toda la ayuda disponible\n"
+" "
+
+#: ../roundup/admin.py:266
+#, python-format
+msgid "Sorry, no help for \"%(topic)s\""
+msgstr "Lo siento, no hay ayuda para \"%(topic)s\""
+
+# ../roundup/admin.py:338 :387
+#: ../roundup/admin.py:338
+#: ../roundup/admin.py:387
+msgid "Templates:"
+msgstr "Plantillas:"
+
+# ../roundup/admin.py:341 :398
+#: ../roundup/admin.py:341
+#: ../roundup/admin.py:398
+msgid "Back ends:"
+msgstr "Motor de almacenamiento"
+
+#: ../roundup/admin.py:344
+msgid ""
+"Usage: install [template [backend [admin password]]]\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"
+"\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 ""
+"Uso: install [plantilla [backend [contraseña admin]]]\n"
+" Instala un nuevo tracker Roundup.\n"
+"\n"
+" El comando preguntará el directorio base del tracker\n"
+" (si el mismo no se provee vía TRACKER_HOME o la opción -i).\n"
+" La plantilla, el backend y la contraseña de admin pueden especificarse\n"
+" en la línea de comandos como argumentos, en ese orden.\n"
+"\n"
+" Luego de este comando debe usarse el comando initialise con el objetivo\n"
+" de inicializar la base de datos del tracker. Ud. puede editar los contenidos\n"
+" iniciales de la base de datos del tracker antes de ejecutar dicho comando\n"
+" editando la funcion init() del módulo dbinit.py del tracker.\n"
+"\n"
+" Vea también initopts help.\n"
+" "
+
+# ../roundup/admin.py:360 :442 :503 :582 :632 :688 :709 :737 :808 :875 :946
+# :994 :1016 :1043 :1106 :1173
+#: ../roundup/admin.py:360
+#: ../roundup/admin.py:447
+#: ../roundup/admin.py:508
+#: ../roundup/admin.py:587
+#: ../roundup/admin.py:637
+#: ../roundup/admin.py:695
+#: ../roundup/admin.py:716
+#: ../roundup/admin.py:744
+#: ../roundup/admin.py:815
+#: ../roundup/admin.py:882
+#: ../roundup/admin.py:953
+#: ../roundup/admin.py:1001
+#: ../roundup/admin.py:1023
+#: ../roundup/admin.py:1050
+#: ../roundup/admin.py:1117
+#: ../roundup/admin.py:1184
+msgid "Not enough arguments supplied"
+msgstr "No se proveyó una cantidad suficiente de argumentos"
+
+#: ../roundup/admin.py:366
+#, python-format
+msgid "Instance home parent directory \"%(parent)s\" does not exist"
+msgstr "El directorio padre \"%(parent)s\" del directorio base de la instancia no existe"
+
+#: ../roundup/admin.py:374
+#, 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 ""
+"ATENCIÓN: Aparentemente ya existe un tracker en \"%(tracker_home)s\"!\n"
+"Si Ud. lo reinstala, perderá toda la información relacionada al mismo!\n"
+"Elimino la misma? Y/N: "
+
+#: ../roundup/admin.py:389
+msgid "Select template [classic]: "
+msgstr "Seleccione la plantilla [classic]: "
+
+#: ../roundup/admin.py:400
+msgid "Select backend [anydbm]: "
+msgstr "Selecccione el motor de almacenamiento [anydbm]: "
+
+#: ../roundup/admin.py:409
+#, python-format
+msgid ""
+"\n"
+"---------------------------------------------------------------------------\n"
+" You should now edit the tracker configuration file:\n"
+" %(config_file)s"
+msgstr ""
+"\n"
+"---------------------------------------------------------------------------\n"
+" Ud. debe ahora editar el archivo de configuración del tracker:\n"
+" %(config_file)s"
+
+#: ../roundup/admin.py:419
+msgid " ... at a minimum, you must set following options:"
+msgstr " ... como mínimo, debe configurar las siguientes opciones:"
+
+#: ../roundup/admin.py:424
+#, 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 ""
+"\n"
+" Si desea modificar el esquema de la base de datos,\n"
+" debe tambien editar el archivo de esquema:\n"
+" %(database_config_file)s\n"
+" Puede también cambiar el archivo de inicialización de la base de datos:\n"
+" %(database_init_file)s\n"
+" ... vea la documentación sobre personalización si desea más información.\n"
+"\n"
+" Ud. DEBE ejecutar el comando \"roundup-admin initialise\" una vez que haya\n"
+" completado los pasos arriba descriptos.\n"
+"---------------------------------------------------------------------------\n"
+
+#: ../roundup/admin.py:442
+msgid ""
+"Usage: genconfig <filename>\n"
+" Generate a new tracker config file (ini style) with default values\n"
+" in <filename>.\n"
+" "
+msgstr ""
+"Uso: genconfig <archivo>\n"
+" Genera un nuevo archivo de configuración de tracker (en formato ini) con\n"
+" valores por defecto en el archivo <archivo>.\n"
+" "
+
+#. password
+#: ../roundup/admin.py:452
+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 ""
+"Uso: initialise [contraseña-admin]\n"
+" Inicializa un nuevo tracker Roundup.\n"
+"\n"
+" Es en este paso cuando se configuran los detalles del usuario administrador.\n"
+"\n"
+" Ejecuta la función de inicialización dbinit.init() del tracker\n"
+" "
+
+#: ../roundup/admin.py:466
+msgid "Admin Password: "
+msgstr "Contraseña de administración: "
+
+#: ../roundup/admin.py:467
+msgid " Confirm: "
+msgstr " Confirmar: "
+
+#: ../roundup/admin.py:471
+msgid "Instance home does not exist"
+msgstr "El directorio base de la instancia no existe"
+
+#: ../roundup/admin.py:475
+msgid "Instance has not been installed"
+msgstr "La instancia no ha sido instalada"
+
+#: ../roundup/admin.py:480
+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 ""
+"ATENCIÓN: La base de datos ya ha sido inicializada!\n"
+"Si la reinicializa, perderá toda la información!\n"
+"Eliminar la misma? Y/N: "
+
+#: ../roundup/admin.py:501
+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 ""
+"Uso: get propiedad designador[,designador]*\n"
+" Retorna la propiedad especificada de uno o mas designadores.\n"
+"\n"
+" Recupera el valor de la propiedad de los nodos especificados\n"
+" por los designadores.\n"
+" "
+
+# ../roundup/admin.py:536 :551
+#: ../roundup/admin.py:541
+#: ../roundup/admin.py:556
+#, python-format
+msgid "property %s is not of type Multilink or Link so -d flag does not apply."
+msgstr "la propiededad %s no es de tipo Multilink o Link asi que el modificador -d no puede usarse."
+
+# ../roundup/admin.py:559 :957 :1006 :1028
+#: ../roundup/admin.py:564
+#: ../roundup/admin.py:964
+#: ../roundup/admin.py:1013
+#: ../roundup/admin.py:1035
+#, python-format
+msgid "no such %(classname)s node \"%(nodeid)s\""
+msgstr "no existe nodo de clase %(classname)s llamado \"%(nodeid)s\""
+
+#: ../roundup/admin.py:566
+#, python-format
+msgid "no such %(classname)s property \"%(propname)s\""
+msgstr "no existe propiedad de clase %(classname)s llamado \"%(propname)s\""
+
+#: ../roundup/admin.py:575
+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 ""
+"Uso: set items propiedad=valor propiedad=valor ...\n"
+" Establece las propiedades especificadas de uno o más ítems.\n"
+"\n"
+" Los ítems se especifican como una clase o como una lista de designadores\n"
+" de ítems (\"designador[,designador,...]\") separados por comas.\n"
+"\n"
+" Este comando establece valores de las propiedades para todos los\n"
+" designadores especificados. Si los valores no se especifican (\"propiedad=\")\n"
+" entonces la propiedad se elimina. Si la propiedad es del tipo multilink, deben\n"
+" especificarse los identificadores asociados como números separados por comas\n"
+" (\"1,2,3\").\n"
+" "
+
+#: ../roundup/admin.py:629
+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 ""
+"Uso: find nombreclase nombreprop=valor ...\n"
+" Busca los nodos de la clase especificada que poseen un cierto valor de propiedad.\n"
+"\n"
+" Busca los nodos de la clase especificada que poseen un cierto valor de propiedad.\n"
+" El valor puede ser el identificador de nodo del nodo enlazado o su valor clave.\n"
+" "
+
+# ../roundup/admin.py:675 :828 :840 :894
+#: ../roundup/admin.py:682
+#: ../roundup/admin.py:835
+#: ../roundup/admin.py:847
+#: ../roundup/admin.py:901
+#, python-format
+msgid "%(classname)s has no property \"%(propname)s\""
+msgstr "%(classname)s no posee la propiedad \"%(propname)s\""
+
+#: ../roundup/admin.py:689
+msgid ""
+"Usage: specification classname\n"
+" Show the properties for a classname.\n"
+"\n"
+" This lists the properties for a given class.\n"
+" "
+msgstr ""
+"Uso: specification nombreclase\n"
+" Muestra las propiedades para un nombre de clase.\n"
+"\n"
+" Visualiza las propiedades para una cierta clase.\n"
+" "
+
+#: ../roundup/admin.py:704
+#, python-format
+msgid "%(key)s: %(value)s (key property)"
+msgstr "%(key)s: %(value)s (propiedad de clave)"
+
+#: ../roundup/admin.py:706
+#, python-format
+msgid "%(key)s: %(value)s"
+msgstr ""
+
+#: ../roundup/admin.py:709
+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 ""
+"Uso: display designador[,designador]*\n"
+" Muestra los valores de propiedeades para el/los nodo(s) especificados(s).\n"
+"\n"
+" Lista las propiedades y sus valores asociados para el nodo especificado.\n"
+" "
+
+#: ../roundup/admin.py:733
+#, python-format
+msgid "%(key)s: %(value)r"
+msgstr ""
+
+#: ../roundup/admin.py:736
+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 ""
+"Uso: create nombreclase propiedad=valor ...\n"
+" Crea una nueva entrada de una clase especificada.\n"
+"\n"
+" Crea una nueva entrada de la clase especificada usando los argumentos nombre=valor\n"
+" provistos en la línea de comandos luego del comando \"create\" para establecer\n"
+" valores de propiedad(es). "
+
+#: ../roundup/admin.py:763
+#, python-format
+msgid "%(propname)s (Password): "
+msgstr "%(propname)s (Contraseña): "
+
+#: ../roundup/admin.py:765
+#, python-format
+msgid " %(propname)s (Again): "
+msgstr " %(propname)s (Nuevamente): "
+
+#: ../roundup/admin.py:767
+msgid "Sorry, try again..."
+msgstr "Lo lamento, intente nuevamente..."
+
+#: ../roundup/admin.py:771
+#, python-format
+msgid "%(propname)s (%(proptype)s): "
+msgstr ""
+
+#: ../roundup/admin.py:789
+#, python-format
+msgid "you must provide the \"%(propname)s\" property."
+msgstr "debe proveer la propiedad \"%(propname)s\"."
+
+#: ../roundup/admin.py:800
+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 ""
+"Uso: list nombreclase [propiedad]\n"
+" Lista las instancias de una clase.\n"
+"\n"
+" Lista todas las instancias de la clase especificada. Si no se\n"
+" especifica la propiedad, se usará la propiedad \"etiqueta\". La propiedad\n"
+" etiqueta es obtenida siguiendo el siguiente orden: \"name\", \"title\"\n"
+" y luego la primera propiedad en orden alfabético.\n"
+"\n"
+" Cuando se usa -c, -S o -s imprime una lista de ids de items si no se ha especificado\n"
+" propiedad. Si se ha especificado propiedad, imprime una lista de dicha propiedad\n"
+" para cada instancia de la clase.\n"
+" "
+
+#: ../roundup/admin.py:813
+msgid "Too many arguments supplied"
+msgstr "Demasiados argumentos"
+
+#: ../roundup/admin.py:849
+#, python-format
+msgid "%(nodeid)4s: %(value)s"
+msgstr ""
+
+#: ../roundup/admin.py:853
+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 ""
+"Uso: table nombreclase [propiedad[,propiedad]*]\n"
+" Lista las instancias de una clase en formato tabular.\n"
+"\n"
+" Lista todas las instancias de la clase especificada. Si no se especifican\n"
+" las propiedades, se visualizan todas las propiedades. Por omisión, los anchos\n"
+" de las columnas son iguales a los anchos de valores mayores respectivos. Es\n"
+" posible definir el ancho de columna de una propiedad definiendo la misma en la\n"
+" forma \"nombre:ancho\".\n"
+" Por ejemplo::\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"
+" también, para obtener un ancho de columna igual al ancho de la etiqueta,\n"
+" deje un : al final sin un valor de ancho de la propiedad. Por ejemplo::\n"
+"\n"
+" roundup> table priority id,name:\n"
+" Id Name\n"
+" 1 fata\n"
+" 2 bug\n"
+" 3 usab\n"
+" 4 feat\n"
+"\n"
+" dará como resultado una columna \"Name\" con un ancho de 4 caracteres.\n"
+" "
+
+#: ../roundup/admin.py:897
+#, python-format
+msgid "\"%(spec)s\" not name:width"
+msgstr "\"%(spec)s\" no es de la forma nombre:longitud"
+
+#: ../roundup/admin.py:947
+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 ""
+"Uso: history designador\n"
+" Muestra las entradas en la historia de un designador.\n"
+"\n"
+" Lista las entradas del journal para el nodo identificado por el designador.\n"
+" "
+
+#: ../roundup/admin.py:968
+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 ""
+"Uso: commit\n"
+" Almacena definitivamente cambios realizados a la base de datos durante\n"
+" una sesión interactiva.\n"
+"\n"
+" Los cambios realizados durante una sesión interactiva no son automáticamente\n"
+" escritos en la base de datos - los mismos deben ser escritos usando este comando\n"
+"\n"
+" Comandos individuales realizados desde la línea de comandos son automáticamente\n"
+" escritos si resultan exitosos.\n"
+" "
+
+#: ../roundup/admin.py:982
+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 ""
+"Uso: rollback\n"
+" Deshace todos los cambios que están pendientes de ser escritos\n"
+" definitivamente en la base de datos.\n"
+"\n"
+" Los cambios hechos durante una sesión interactiva no son automáticamente\n"
+" escritos en la base de datos - los mismos deben ser grabados manualmente.\n"
+" Este comando deshace todos dichos cambios, de manera que un comando commit\n"
+" inmediatamente posterior no introduciría cambios en la base de datos.\n"
+" "
+
+#: ../roundup/admin.py:994
+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 ""
+"Uso: retire designador[,designador]*\n"
+" Retira el nodo especificado por designador.\n"
+"\n"
+" Esta acción indica que un nodo particular no se obtendrá cuando se\n"
+" usen los comandos list y find, y que su valor clave podrá ser reusado.\n"
+" "
+
+#: ../roundup/admin.py:1017
+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 ""
+"Uso: restore designador[,designador]*\n"
+" Restaura el nodo retirado especificado por designador.\n"
+"\n"
+" Los nodos especificados volverán a estar nuevamente disponibles para los usuarios.\n"
+" "
+
+#. grab the directory to export to
+#: ../roundup/admin.py:1039
+msgid ""
+"Usage: export [class[,class]] export_dir\n"
+" Export the database to colon-separated-value files.\n"
+"\n"
+" Optionally limit the export to just the names classes.\n"
+"\n"
+" This action exports the current data from the database into\n"
+" colon-separated-value files that are placed in the nominated\n"
+" destination directory.\n"
+" "
+msgstr ""
+"Uso: export [clase[,clase]] export_dir\n"
+" Exporta la base de datos a archivos de valores separados por comas.\n"
+"\n"
+" Opcionalmente limita la exportación sólo a las clases especificadas.\n"
+"\n"
+" Esta acción exporta los datos actuales desde la base de datos a\n"
+" archivos de valores separados por comas que se colocarán en el directorio\n"
+" de destino especificado (export_dir).\n"
+" "
+
+#: ../roundup/admin.py:1097
+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 ""
+"Uso: import import_dir\n"
+" Importa una base de datos desde el directorio conteniendo archivos CSV,\n"
+" dos por cada clase a importar.\n"
+"\n"
+" Los archivos usados en la importación son::\n"
+"\n"
+" <clase>.csv\n"
+" Este debe definir las mismas propiedades que la clase (esto incluye la\n"
+" existencia de una línea \"encabezado\" con los nombre de dichas propiedades.)\n"
+" <clase>-journals.csv\n"
+" Este define los journals para los items que se están importando.\n"
+"\n"
+" Los nodos importados tendrán los mismos id´s que los nodos según se encontraban\n"
+" definidos en el arvhivo importado, por lo tanto reemplazarán todo contenido\n"
+" preexistente.\n"
+"\n"
+" Los nuevos nodos son agregados a la base de datos existente - si Ud. desea\n"
+" crear una base de datos nueva usando los datos importados, entonces puede\n"
+" crear una nueva base de datos (o, tediosamente, retirar toda los datos viejos.)\n"
+" "
+
+#: ../roundup/admin.py:1166
+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 ""
+"Uso: pack período |fecha\n"
+"\n"
+" Elimina entradas de journal mas viejas que un período de tiempo\n"
+" especificado o anteriores a cierta fecha.\n"
+"\n"
+" Un período se especifica usando los sufijos \"y\", \"m\", and \"d\". El\n"
+" sufijo \"w\" (por \"week\") significa 7 días.\n"
+"\n"
+" \"3y\" significa tres años\n"
+" \"2y 1m\" significa dos años y un mes\n"
+" \"1m 25d\" significa un mes y 25 días\n"
+" \"2w 3d\" significa dos semanas y tres días\n"
+"\n"
+" El formato de fecha es \"YYYY-MM-DD\" ej.:\n"
+" 2001-01-01\n"
+"\n"
+" "
+
+#: ../roundup/admin.py:1194
+msgid "Invalid format"
+msgstr "Formato inválido"
+
+#: ../roundup/admin.py:1204
+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 ""
+"Uso: reindex [nombreclase|designador]*\n"
+" Regenera los índices de búsqueda de un tracker.\n"
+"\n"
+" Este comando regenerará los índices de búsqueda de un tracker.\n"
+" Es un comando que por lo general se ejecuta automáticamente.\n"
+" "
+
+#: ../roundup/admin.py:1218
+#, python-format
+msgid "no such item \"%(designator)s\""
+msgstr "no existe un ítem llamado \"%(designator)s\""
+
+#: ../roundup/admin.py:1228
+msgid ""
+"Usage: security [Role name]\n"
+" Display the Permissions available to one or all Roles.\n"
+" "
+msgstr ""
+"Uso: security [Nombre de rol]\n"
+" Muestra los permisos disponibles para uno o todos los Roles.\n"
+" "
+
+#: ../roundup/admin.py:1236
+#, python-format
+msgid "No such Role \"%(role)s\""
+msgstr "No existe un Rol llamado \"%(role)s\""
+
+#: ../roundup/admin.py:1242
+#, python-format
+msgid "New Web users get the Roles \"%(role)s\""
+msgstr "Los nuevos usuarios creados vía Web obtiene los Roles \"%(role)s\""
+
+#: ../roundup/admin.py:1244
+#, python-format
+msgid "New Web users get the Role \"%(role)s\""
+msgstr "Los nuevos usuarios creados vía Web obtienen el Rol \"%(role)s\""
+
+#: ../roundup/admin.py:1247
+#, python-format
+msgid "New Email users get the Roles \"%(role)s\""
+msgstr "Los nuevos usuarios creados vía e-mail obtienen los Roles \"%(role)s\""
+
+#: ../roundup/admin.py:1249
+#, python-format
+msgid "New Email users get the Role \"%(role)s\""
+msgstr "Los nuevos usuarios creados vía e-mail obtienen el Rol \"%(role)s\""
+
+#: ../roundup/admin.py:1252
+#, python-format
+msgid "Role \"%(name)s\":"
+msgstr "Rol \"%(name)s\":"
+
+#: ../roundup/admin.py:1257
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
+msgstr " %(description)s (%(name)s para \"%(klass)s\": %(properties)s solamente)"
+
+#: ../roundup/admin.py:1260
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
+msgstr " %(description)s (%(name)s para \"%(klass)s\" solamente)"
+
+#: ../roundup/admin.py:1263
+#, python-format
+msgid " %(description)s (%(name)s)"
+msgstr ""
+
+#: ../roundup/admin.py:1292
+#, python-format
+msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
+msgstr "Comando desconocido \"%(command)s\" (tipee \"help commands\" para obtener una lista)"
+
+#: ../roundup/admin.py:1298
+#, python-format
+msgid "Multiple commands match \"%(command)s\": %(list)s"
+msgstr "Coinciden mas de un comando \"%(command)s\": %(list)s"
+
+#: ../roundup/admin.py:1305
+msgid "Enter tracker home: "
+msgstr "Ingrese directorio base del tracker: "
+
+# ../roundup/admin.py:1296 :1302 :1322
+#: ../roundup/admin.py:1312
+#: ../roundup/admin.py:1318
+#: ../roundup/admin.py:1338
+#, python-format
+msgid "Error: %(message)s"
+msgstr ""
+
+#: ../roundup/admin.py:1326
+#, python-format
+msgid "Error: Couldn't open tracker: %(message)s"
+msgstr "Error: No se pudo abrir el tracker: %(message)s"
+
+#: ../roundup/admin.py:1351
+#, python-format
+msgid ""
+"Roundup %s ready for input.\n"
+"Type \"help\" for help."
+msgstr ""
+"Roundup %s listo para comandos.\n"
+"Tipee \"help\" para ayuda."
+
+#: ../roundup/admin.py:1356
+msgid "Note: command history and editing not available"
+msgstr "Nota: historia y edición de comandos no disponible"
+
+#: ../roundup/admin.py:1360
+msgid "roundup> "
+msgstr ""
+
+#: ../roundup/admin.py:1362
+msgid "exit..."
+msgstr "salir..."
+
+#: ../roundup/admin.py:1372
+msgid "There are unsaved changes. Commit them (y/N)? "
+msgstr "Hay cambios sin guardar. Debo guardar los mismos (y/N)? "
+
+#: ../roundup/backends/back_anydbm.py:2061
+#, python-format
+msgid "WARNING: invalid date tuple %r"
+msgstr "ATENCIÓN: tuple de fecha inválido %r"
+
+#: ../roundup/backends/rdbms_common.py:1431
+msgid "create"
+msgstr "crear"
+
+#: ../roundup/backends/rdbms_common.py:1597
+msgid "unlink"
+msgstr "desenlazar"
+
+#: ../roundup/backends/rdbms_common.py:1601
+msgid "link"
+msgstr "enlazar"
+
+#: ../roundup/backends/rdbms_common.py:1720
+msgid "set"
+msgstr ""
+
+#: ../roundup/backends/rdbms_common.py:1744
+msgid "retired"
+msgstr "retirado"
+
+#: ../roundup/backends/rdbms_common.py:1774
+msgid "restored"
+msgstr "restaurado"
+
+#: ../roundup/cgi/actions.py:58
+#, python-format
+msgid "You do not have permission to %(action)s the %(classname)s class."
+msgstr "Ud. no posee los permisos necesarios para %(action)s la clase %(classname)s."
+
+#: ../roundup/cgi/actions.py:89
+msgid "No type specified"
+msgstr "No se especificó un tipo"
+
+#: ../roundup/cgi/actions.py:91
+msgid "No ID entered"
+msgstr "No se ingresó un ID"
+
+#: ../roundup/cgi/actions.py:97
+#, python-format
+msgid "\"%(input)s\" is not an ID (%(classname)s ID required)"
+msgstr "\"%(input)s\" no es un ID (se requieren IDs %(classname)s)"
+
+#: ../roundup/cgi/actions.py:117
+msgid "You may not retire the admin or anonymous user"
+msgstr "Ni el usuario admin ni el usuario anónimo pueden ser retirados"
+
+#: ../roundup/cgi/actions.py:124
+#, python-format
+msgid "%(classname)s %(itemid)s has been retired"
+msgstr "%(classname)s %(itemid)s ha sido retirado"
+
+# ../roundup/cgi/actions.py:163 :191
+#: ../roundup/cgi/actions.py:163
+#: ../roundup/cgi/actions.py:191
+msgid "You do not have permission to edit queries"
+msgstr "Ud. no posee los permisos necesarios para editar consultas"
+
+# ../roundup/cgi/actions.py:169 :197
+#: ../roundup/cgi/actions.py:169
+#: ../roundup/cgi/actions.py:197
+msgid "You do not have permission to store queries"
+msgstr "Ud. no posee los permisos necesarios para grabar consultas"
+
+#: ../roundup/cgi/actions.py:286
+#, python-format
+msgid "Not enough values on line %(line)s"
+msgstr "No hay valores suficientes en la línea %(line)s"
+
+#: ../roundup/cgi/actions.py:333
+msgid "Items edited OK"
+msgstr "Items editados exitosamente"
+
+#: ../roundup/cgi/actions.py:393
+#, python-format
+msgid "%(class)s %(id)s %(properties)s edited ok"
+msgstr "%(properties)s de %(class)s %(id)s editados exitosamente"
+
+#: ../roundup/cgi/actions.py:396
+#, python-format
+msgid "%(class)s %(id)s - nothing changed"
+msgstr "%(class)s %(id)s - sin modificaciones"
+
+#: ../roundup/cgi/actions.py:408
+#, python-format
+msgid "%(class)s %(id)s created"
+msgstr "%(class)s %(id)s creado"
+
+#: ../roundup/cgi/actions.py:440
+#, python-format
+msgid "You do not have permission to edit %(class)s"
+msgstr "Ud. no posee los permisos necesarios para editar %(class)s"
+
+#: ../roundup/cgi/actions.py:452
+#, python-format
+msgid "You do not have permission to create %(class)s"
+msgstr "Ud. no posee los permisos necesarios para crear %(class)s"
+
+#: ../roundup/cgi/actions.py:475
+msgid "You do not have permission to edit user roles"
+msgstr "Ud. no posee los permisos necesarios para editar roles de usuario"
+
+#: ../roundup/cgi/actions.py:519
+#, 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 "Error de edición: Alguien más ha editado este %s (%s). Vea los <a target=\"new\" href=\"%s%s\">cambios</a> que dicha persona ha realizado en una ventana aparte."
+
+#: ../roundup/cgi/actions.py:548
+#, python-format
+msgid "Edit Error: %s"
+msgstr "Error de edición: %s"
+
+# ../roundup/cgi/actions.py:579 :590 :761 :780
+#: ../roundup/cgi/actions.py:579
+#: ../roundup/cgi/actions.py:590
+#: ../roundup/cgi/actions.py:761
+#: ../roundup/cgi/actions.py:780
+#, python-format
+msgid "Error: %s"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:616
+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 inválida!\n"
+"(un bug de Mozilla puede ser el causante de que se visualice este mensaje en forma errónea, por favor verifique su casilla de e-mail)"
+
+#: ../roundup/cgi/actions.py:658
+#, python-format
+msgid "Password reset and email sent to %s"
+msgstr "Contraseña reinicializada y mensaje de e-mail enviado a %s"
+
+#: ../roundup/cgi/actions.py:667
+msgid "Unknown username"
+msgstr "Usuario desconocido"
+
+#: ../roundup/cgi/actions.py:675
+msgid "Unknown email address"
+msgstr "Dirección de e-mail desconocida"
+
+#: ../roundup/cgi/actions.py:680
+msgid "You need to specify a username or address"
+msgstr "Debe especificar un nombre de usuario o dirección de e-mail"
+
+#: ../roundup/cgi/actions.py:705
+#, python-format
+msgid "Email sent to %s"
+msgstr "Se ha enviado un mensaje de e-mail a %s"
+
+#: ../roundup/cgi/actions.py:724
+msgid "You are now registered, welcome!"
+msgstr "Ud. se ha registrado exitosamente, bienvenido!"
+
+#: ../roundup/cgi/actions.py:769
+msgid "It is not permitted to supply roles at registration."
+msgstr "No está permitido especificar roles en el momento del registro."
+
+#: ../roundup/cgi/actions.py:852
+msgid "You are logged out"
+msgstr "Ha salido del sistema exitosamente"
+
+#: ../roundup/cgi/actions.py:869
+msgid "Username required"
+msgstr "Se requiere el ingreso de un nombre de usuario"
+
+# ../roundup/cgi/actions.py:891 :895
+#: ../roundup/cgi/actions.py:897
+#: ../roundup/cgi/actions.py:901
+msgid "Invalid login"
+msgstr "nombre de usuario ó contraseña inválidos"
+
+#: ../roundup/cgi/actions.py:907
+msgid "You do not have permission to login"
+msgstr "Ud. no tiene permiso para ingresar al sistema"
+
+#: ../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>Error de Templating</h1>\n"
+"<p><b>%(exc_type)s</b>: %(exc_value)s</p>\n"
+"<p class=\"help\">Información de depuración:</p>"
+
+#: ../roundup/cgi/cgitb.py:64
+#, python-format
+msgid "<li>\"%(name)s\" (%(info)s)</li>"
+msgstr ""
+
+#: ../roundup/cgi/cgitb.py:67
+#, python-format
+msgid "<li>Looking for \"%(name)s\", current path:<ol>%(path)s</ol></li>"
+msgstr "<li>Buscando \"%(name)s\", ruta actual:<ol>%(path)s</ol></li>"
+
+#: ../roundup/cgi/cgitb.py:71
+#, python-format
+msgid "<li>In %s</li>"
+msgstr "<li>En %s</li>"
+
+#: ../roundup/cgi/cgitb.py:76
+#, python-format
+msgid "A problem occurred in your template \"%s\"."
+msgstr "Ha ocurrido un problema en su template \"%s\"."
+
+#: ../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>Cuando se evaluaba la expresión %(info)r en la línea %(line)d\n"
+"<table class=\"otherinfo\" style=\"font-size: 90%%\">\n"
+" <tr><th colspan=\"2\" class=\"header\">Variables activas:</th></tr>\n"
+" %(globals)s\n"
+" %(locals)s\n"
+"</table></li>\n"
+
+#: ../roundup/cgi/cgitb.py:103
+msgid "Full traceback:"
+msgstr "Traza completa"
+
+#: ../roundup/cgi/cgitb.py:116
+#, python-format
+msgid "<font size=+1><strong>%(exc_type)s</strong>: %(exc_value)s</font>"
+msgstr ""
+
+#: ../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>Ha ocurrido un problema ejecutando un script Python. Esta es la secuencia de llamadas a funciones que llevaron al error, con la llamada mas reciente (la mas anidada) ubicada primera. Los atributos de la excepción son:"
+
+#: ../roundup/cgi/cgitb.py:129
+msgid "<file is None - probably inside <tt>eval</tt> or <tt>exec</tt>>"
+msgstr "<file es None - probablemente dentro de <tt>eval</tt> or <tt>exec</tt>>"
+
+#: ../roundup/cgi/cgitb.py:138
+#, python-format
+msgid "in <strong>%s</strong>"
+msgstr "en <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>indefinido/a</em>"
+
+#: ../roundup/cgi/client.py:297
+msgid "Form Error: "
+msgstr "Error de formulario"
+
+#: ../roundup/cgi/client.py:350
+#, python-format
+msgid "Unrecognized charset: %r"
+msgstr "Conjunto de caracteres desconocido: %r"
+
+#: ../roundup/cgi/client.py:453
+msgid "Anonymous users are not allowed to use the web interface"
+msgstr "Los usuarios anonimos no tienen permitido usar esta interfaz Web"
+
+#: ../roundup/cgi/client.py:604
+msgid "You are not allowed to view this file."
+msgstr "Ud. no tiene permitido ver este archivo"
+
+#: ../roundup/cgi/client.py:696
+#, python-format
+msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
+msgstr "%(starttag)sTiempo transcurrido: %(seconds)fs%(endtag)s\n"
+
+#: ../roundup/cgi/client.py:700
+#, 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)sAciertos Cache: %(cache_hits)d, no aciertos %(cache_misses)d. Cargando items: %(get_items)f secs. Filtrado: %(filtering)f secs.%(endtag)s\n"
+
+#: ../roundup/cgi/form_parser.py:283
+#, python-format
+msgid "link \"%(key)s\" value \"%(value)s\" not a designator"
+msgstr "el enlace \"%(key)s\" valor \"%(value)s\" no es un designador"
+
+#: ../roundup/cgi/form_parser.py:290
+#, python-format
+msgid "%(class)s %(property)s is not a link or multilink property"
+msgstr "%(property)s de %(class)s no es una propiedad enlace o multilink"
+
+#: ../roundup/cgi/form_parser.py:312
+#, python-format
+msgid "You have submitted a %(action)s action for the property \"%(property)s\" which doesn't exist"
+msgstr "Ha ingresado una acción %(action)s para la propiedad \"%(property)s\" que no existe"
+
+# ../roundup/cgi/form_parser.py:331 :357
+#: ../roundup/cgi/form_parser.py:331
+#: ../roundup/cgi/form_parser.py:357
+#, python-format
+msgid "You have submitted more than one value for the %s property"
+msgstr "Ha ingresado más de un valor para la propiedad %s"
+
+# ../roundup/cgi/form_parser.py:354 :360
+#: ../roundup/cgi/form_parser.py:354
+#: ../roundup/cgi/form_parser.py:360
+msgid "Password and confirmation text do not match"
+msgstr "La contraseña y el texto de confirmación no coinciden"
+
+#: ../roundup/cgi/form_parser.py:395
+#, python-format
+msgid "property \"%(propname)s\": \"%(value)s\" not currently in list"
+msgstr "propiedad \"%(propname)s\": \"%(value)s\" no se encuentra en este momento en la lista"
+
+#: ../roundup/cgi/form_parser.py:509
+#, python-format
+msgid "Required %(class)s property %(property)s not supplied"
+msgid_plural "Required %(class)s properties %(property)s not supplied"
+msgstr[0] "La propiedad %(property)s de la clase %(class)s es obligatoria y no se ha provisto"
+msgstr[1] "Las propiedades %(property)s de la clase %(class)s son obligatorias y no se han provisto"
+
+#: ../roundup/cgi/form_parser.py:532
+msgid "File is empty"
+msgstr "El archivo está vacío"
+
+#: ../roundup/cgi/templating.py:69
+#, python-format
+msgid "You are not allowed to %(action)s items of class %(class)s"
+msgstr "Ud. no tiene permitido %(action)s items de la clase %(class)s"
+
+#: ../roundup/cgi/templating.py:623
+msgid "(list)"
+msgstr "(lista)"
+
+#: ../roundup/cgi/templating.py:687
+msgid "Submit New Entry"
+msgstr "Crear nuevo elemento"
+
+# ../roundup/cgi/templating.py:673 :792 :1166 :1187 :1231 :1253 :1287 :1326
+# :1377 :1394 :1470 :1490 :1503 :1520 :1530 :1580 :1755
+#: ../roundup/cgi/templating.py:701
+#: ../roundup/cgi/templating.py:820
+#: ../roundup/cgi/templating.py:1194
+#: ../roundup/cgi/templating.py:1215
+#: ../roundup/cgi/templating.py:1259
+#: ../roundup/cgi/templating.py:1281
+#: ../roundup/cgi/templating.py:1315
+#: ../roundup/cgi/templating.py:1354
+#: ../roundup/cgi/templating.py:1405
+#: ../roundup/cgi/templating.py:1422
+#: ../roundup/cgi/templating.py:1498
+#: ../roundup/cgi/templating.py:1518
+#: ../roundup/cgi/templating.py:1531
+#: ../roundup/cgi/templating.py:1563
+#: ../roundup/cgi/templating.py:1573
+#: ../roundup/cgi/templating.py:1623
+#: ../roundup/cgi/templating.py:1810
+msgid "[hidden]"
+msgstr "[oculto]"
+
+#: ../roundup/cgi/templating.py:702
+msgid "New node - no history"
+msgstr "Nuevo nodo - sin historia"
+
+#: ../roundup/cgi/templating.py:802
+msgid "Submit Changes"
+msgstr "Enviar modificaciones"
+
+#: ../roundup/cgi/templating.py:884
+msgid "<em>The indicated property no longer exists</em>"
+msgstr "<em>La propiedad indicada ya no existe</em>"
+
+#: ../roundup/cgi/templating.py:885
+#, python-format
+msgid "<em>%s: %s</em>\n"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:898
+#, python-format
+msgid "The linked class %(classname)s no longer exists"
+msgstr "La clase relacionada %(classname)s ya no existe"
+
+# ../roundup/cgi/templating.py:903 :924
+#: ../roundup/cgi/templating.py:931
+#: ../roundup/cgi/templating.py:952
+msgid "<strike>The linked node no longer exists</strike>"
+msgstr "<strike>El nodo relacionado ya no existe</strike>"
+
+#: ../roundup/cgi/templating.py:994
+#: ../roundup/cgi/templating.py:1358
+#: ../roundup/cgi/templating.py:1379
+#: ../roundup/cgi/templating.py:1385
+msgid "No"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:994
+#: ../roundup/cgi/templating.py:1358
+#: ../roundup/cgi/templating.py:1377
+#: ../roundup/cgi/templating.py:1382
+msgid "Yes"
+msgstr "Si"
+
+#: ../roundup/cgi/templating.py:1005
+#, python-format
+msgid "%s: (no value)"
+msgstr "%s: (sin valor)"
+
+#: ../roundup/cgi/templating.py:1017
+msgid "<strong><em>This event is not handled by the history display!</em></strong>"
+msgstr "<strong><em>Este evento no es soportado por la visualización de historia!</em></strong>"
+
+#: ../roundup/cgi/templating.py:1029
+msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
+msgstr "<tr><td colspan=4><strong>Nota:</strong></td></tr>"
+
+#: ../roundup/cgi/templating.py:1038
+msgid "History"
+msgstr "Historia"
+
+#: ../roundup/cgi/templating.py:1040
+msgid "<th>Date</th>"
+msgstr "<th>Fecha</th>"
+
+#: ../roundup/cgi/templating.py:1041
+msgid "<th>User</th>"
+msgstr "<th>Usuario</th>"
+
+#: ../roundup/cgi/templating.py:1042
+msgid "<th>Action</th>"
+msgstr "<th>Acción</th>"
+
+#: ../roundup/cgi/templating.py:1043
+msgid "<th>Args</th>"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1285
+msgid "*encrypted*"
+msgstr "*cifrado*"
+
+#: ../roundup/cgi/templating.py:1466
+msgid "default value for DateHTMLProperty must be either DateHTMLProperty or string date representation."
+msgstr "el valor por defecto para DateHTMLProperty debe ser un DateHTMLProperty o una cadena que represente una fecha."
+
+#: ../roundup/cgi/templating.py:1614
+#, python-format
+msgid "Attempt to look up %(attr)s on a missing value"
+msgstr "Se intentó buscar %(attr)s en un valor faltante"
+
+#: ../roundup/cgi/templating.py:1688
+#, python-format
+msgid "<option %svalue=\"-1\">- no selection -</option>"
+msgstr "<option %svalue=\"-1\">- sin selección -</option>"
+
+#: ../roundup/date.py:180
+msgid "Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or \"yyyy-mm-dd.HH:MM:SS.SSS\""
+msgstr "No es una especificación de fecha: \"aaaa-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" o \"aaaa-mm-dd.HH:MM:SS.SSS\""
+
+#: ../roundup/date.py:234
+#, 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 no es una especificación de fecha / hora \"aaaa-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" o \"aaaa-mm-dd.HH:MM:SS.SSS\""
+
+#: ../roundup/date.py:532
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
+msgstr "No es una especificación de intervalo de tiempo: [+-] [#a] [#m] [#s] [#d] [[[H]H:MM]:SS] [especific. fecha]"
+
+#: ../roundup/date.py:551
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+msgstr "No es una especificación de intervalo de tiempo: [+-] [#a] [#m] [#s] [#d] [[[H]H:MM]:SS]"
+
+#: ../roundup/date.py:688
+#, python-format
+msgid "%(number)s year"
+msgid_plural "%(number)s years"
+msgstr[0] "%(number)s año"
+msgstr[1] "%(number)s años"
+
+#: ../roundup/date.py:692
+#, python-format
+msgid "%(number)s month"
+msgid_plural "%(number)s months"
+msgstr[0] "%(number)s mes"
+msgstr[1] "%(number)s meses"
+
+#: ../roundup/date.py:696
+#, python-format
+msgid "%(number)s week"
+msgid_plural "%(number)s weeks"
+msgstr[0] "%(number)s semana"
+msgstr[1] "%(number)s semanas"
+
+#: ../roundup/date.py:700
+#, python-format
+msgid "%(number)s day"
+msgid_plural "%(number)s days"
+msgstr[0] "%(number)s día"
+msgstr[1] "%(number)s días"
+
+#: ../roundup/date.py:704
+msgid "tomorrow"
+msgstr "mañana"
+
+#: ../roundup/date.py:706
+msgid "yesterday"
+msgstr "ayer"
+
+#: ../roundup/date.py:709
+#, python-format
+msgid "%(number)s hour"
+msgid_plural "%(number)s hours"
+msgstr[0] "%(number)s hora"
+msgstr[1] "%(number)s horas"
+
+#: ../roundup/date.py:713
+msgid "an hour"
+msgstr "una hora"
+
+#: ../roundup/date.py:715
+msgid "1 1/2 hours"
+msgstr "1 hora y 1/2"
+
+#: ../roundup/date.py:717
+#, python-format
+msgid "1 %(number)s/4 hours"
+msgid_plural "1 %(number)s/4 hours"
+msgstr[0] "1 %(number)s/4 de hora"
+msgstr[1] "1 %(number)s/4 de hora"
+
+#: ../roundup/date.py:721
+msgid "in a moment"
+msgstr "en un momento"
+
+#: ../roundup/date.py:723
+msgid "just now"
+msgstr "ahora"
+
+#: ../roundup/date.py:726
+msgid "1 minute"
+msgstr "1 minuto"
+
+#: ../roundup/date.py:729
+#, python-format
+msgid "%(number)s minute"
+msgid_plural "%(number)s minutes"
+msgstr[0] "%(number)s minuto"
+msgstr[1] "%(number)s minutos"
+
+#: ../roundup/date.py:732
+msgid "1/2 an hour"
+msgstr "media hora"
+
+#: ../roundup/date.py:734
+#, python-format
+msgid "%(number)s/4 hour"
+msgid_plural "%(number)s/4 hours"
+msgstr[0] "%(number)s/4 de hora"
+msgstr[1] "%(number)s/4s de hora"
+
+#: ../roundup/date.py:738
+#, python-format
+msgid "%s ago"
+msgstr "hace %s"
+
+#: ../roundup/date.py:740
+#, python-format
+msgid "in %s"
+msgstr "en %s"
+
+#: ../roundup/init.py:132
+#, python-format
+msgid ""
+"WARNING: directory '%s'\n"
+"\tcontains old-style template - ignored"
+msgstr ""
+"ATENCIÓN: El directorio '%s'\n"
+"\tcontiene una plantilla con el viejo formato - se ignorará"
+
+#: ../roundup/roundupdb.py:141
+msgid "files"
+msgstr "archivos"
+
+#: ../roundup/roundupdb.py:141
+msgid "messages"
+msgstr "mensajes"
+
+#: ../roundup/roundupdb.py:141
+msgid "nosy"
+msgstr "interesados"
+
+#: ../roundup/roundupdb.py:141
+msgid "superseder"
+msgstr "reemplazado por"
+
+#: ../roundup/roundupdb.py:141
+msgid "title"
+msgstr "título"
+
+#: ../roundup/roundupdb.py:142
+msgid "assignedto"
+msgstr "asignadoa"
+
+#: ../roundup/roundupdb.py:142
+msgid "priority"
+msgstr "prioridad"
+
+#: ../roundup/roundupdb.py:142
+msgid "status"
+msgstr "estado"
+
+#: ../roundup/roundupdb.py:142
+msgid "topic"
+msgstr "palabraclave"
+
+#: ../roundup/roundupdb.py:145
+msgid "activity"
+msgstr "actividad"
+
+#. following properties are common for all hyperdb classes
+#. they are listed here to keep things in one place
+#: ../roundup/roundupdb.py:145
+msgid "actor"
+msgstr "últimoactor"
+
+#: ../roundup/roundupdb.py:145
+msgid "creation"
+msgstr "creación"
+
+#: ../roundup/roundupdb.py:145
+msgid "creator"
+msgstr "creador"
+
+#: ../roundup/scripts/roundup_demo.py:32
+#, python-format
+msgid "Enter directory path to create demo tracker [%s]: "
+msgstr "Ingrese la ruta al directorio en el qeu se creará el tracker demo [%s]: "
+
+#: ../roundup/scripts/roundup_gettext.py:22
+#, python-format
+msgid "Usage: %(program)s <tracker home>"
+msgstr "Uso: %(program)s <directorio base de tracker>"
+
+#: ../roundup/scripts/roundup_gettext.py:37
+#, python-format
+msgid "No tracker templates found in directory %s"
+msgstr "No se encontraron templates de trackers en el directorio %s"
+
+#: ../roundup/scripts/roundup_mailgw.py:36
+#, python-format
+msgid ""
+"Usage: %(program)s [-v] [-c] [[-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"
+"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 ""
+"Uso: %(program)s [-v] [-c] [[-C clase] -S campo=valor]* <directorio base instancia> [método]\n"
+"\n"
+"Opciones:\n"
+" -v: imprime version y sale\n"
+" -c: clase por omisión del item a crear (sinó se usará MAIL_DEFAULT_CLASS del tracker)\n"
+" -C / -S: ver mas abajo\n"
+"\n"
+"La pasarela de correo de roundup puede ser invocada en una de cuatro formas:\n"
+" . con un directorio base de instancia como único argumento,\n"
+" . con un directorio base de instancia y un archivo de spool de correo,\n"
+" . con un directorio base de instancia y una cuenta de un servidor POP/APOP, o\n"
+" . con un directorio base de instancia y una cuenta de un servidor IMAP/IMAPS.\n"
+"\n"
+"También soporta los argumentos opcionales -C y -S que le permiten establecer\n"
+"campos para una clase creada por la pasarela de correo de Roundup roundup-mailgw.\n"
+"La clase por omisión es msg, pero las otras clases: issue, file, user tambien\n"
+"pueden usarse. Las opciones -S y --set usan la notación\n"
+"propiedad=valor[;propiedad=valor] aceptada por el comando roundup de línea de\n"
+"comandos o los comandos que pueden ser pasados en el campo Asunto: de un mensaje\n"
+"de correo electrónico.\n"
+"\n"
+"También le permite establecer el tipo de mensaje basado en la dirección de correo usada.\n"
+"\n"
+"PIPE:\n"
+" En el primer caso, la pasarela de correo lee un mensaje desde la entrada estándar\n"
+" y lo envía al módulo roundup.mailgw.\n"
+"\n"
+"UNIX mailbox:\n"
+" En el segundo caso, la pasarela lee todos los mensajes desde el archivo de\n"
+" spool de correo y envía los mismos de a uno al módulo roundup.mailgw. El archivo\n"
+" se vacía una vez que todos los mensajes han sido procesados exitosamente. El\n"
+" archivo se especifica como:\n"
+" mailbox /ruta/al/mailbox\n"
+"\n"
+"POP:\n"
+" En el tercer caso, la pasarela lee todos los mensajes en el servidor\n"
+" POP y envía los mismos de a uno al módulo roundup.mailgw. El servidor\n"
+" POP se especifica como:\n"
+" pop nombreusuario:contraseña at servidor\n"
+" El nombreusuario y la contraseña pueden omitirse:\n"
+" pop nombreusuario at servidor\n"
+" pop servidor\n"
+" son válidos. El nombre de usuario y/o la contraseña se solicitarán si no\n"
+" se proveen en la línea de comandos.\n"
+"\n"
+"APOP:\n"
+" Idéntico a POP, pero usando Authenticated POP:\n"
+" apop nombreusuario:contraseña at servidor\n"
+"\n"
+"IMAP:\n"
+" Se conecta a un servidor IMAP. Esta forma soporta la misma notación que correo POP\n"
+" imap nombreusuario:contraseña at servidor\n"
+" También le permite especificar una casilla distinta a INBOX usando\n"
+" el siguiente formato:\n"
+" imap nombreusuario:contraseña at servidor casilla\n"
+"\n"
+"IMAPS:\n"
+" Se conecta a un servidor IMAP usando ssl.\n"
+" Esta forma soporta la misma notación que IMAP.\n"
+" imaps nombreusuario:contraseña at servidor [casilla]\n"
+"\n"
+
+#: ../roundup/scripts/roundup_mailgw.py:147
+msgid "Error: not enough source specification information"
+msgstr "Error: no hay información de especificación de origen suficiente"
+
+#: ../roundup/scripts/roundup_mailgw.py:157
+msgid "Error: pop specification not valid"
+msgstr "Error: especification pop no válida"
+
+#: ../roundup/scripts/roundup_mailgw.py:164
+msgid "Error: apop specification not valid"
+msgstr "Error: especification apop no válida"
+
+#: ../roundup/scripts/roundup_mailgw.py:178
+msgid "Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or \"imaps\""
+msgstr "Error: EL origen debe ser \"mailbox\", \"pop\", \"apop\", \"imap\" o \"imaps\""
+
+#: ../roundup/scripts/roundup_server.py:156
+msgid ""
+"<html><head><title>Roundup trackers index</title></head>\n"
+"<body><h1>Roundup trackers index</h1><ol>\n"
+msgstr ""
+"<html><head><title>Índice de trackers Roundup</title></head>\n"
+"<body><h1>Índice de trackers Roundup</h1><ol>\n"
+
+#: ../roundup/scripts/roundup_server.py:257
+#, python-format
+msgid "Error: %s: %s"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:267
+msgid "WARNING: ignoring \"-g\" argument, not root"
+msgstr "ATENCIÓN: ignorando argumento \"-g\" , Ud. no es root"
+
+#: ../roundup/scripts/roundup_server.py:273
+msgid "Can't change groups - no grp module"
+msgstr "No puede cambiar grupos - el módulo grp no está presente"
+
+#: ../roundup/scripts/roundup_server.py:282
+#, python-format
+msgid "Group %(group)s doesn't exist"
+msgstr "El grupo %(group)s no existe"
+
+#: ../roundup/scripts/roundup_server.py:293
+msgid "Can't run as root!"
+msgstr "No puede ejecutarse como root!"
+
+#: ../roundup/scripts/roundup_server.py:296
+msgid "WARNING: ignoring \"-u\" argument, not root"
+msgstr "ATENCIÓN: ignorando argumento \"-u\", Ud. no es root"
+
+#: ../roundup/scripts/roundup_server.py:301
+msgid "Can't change users - no pwd module"
+msgstr "No puedo cambiar usuarios - no existe el módulo pwd"
+
+#: ../roundup/scripts/roundup_server.py:310
+#, python-format
+msgid "User %(user)s doesn't exist"
+msgstr "El usuario %(user)s no existe"
+
+#: ../roundup/scripts/roundup_server.py:437
+#, python-format
+msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
+msgstr "El modo multiproceso \"%s\" no está disponible, conmutado a proceso simple"
+
+#: ../roundup/scripts/roundup_server.py:460
+#, python-format
+msgid "Unable to bind to port %s, port already in use."
+msgstr "Imposible asociarse al puerto %s, el mismo ya está en uso."
+
+#: ../roundup/scripts/roundup_server.py:528
+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 ""
+" -c <Comando> Opciones de Servicio Windows.\n"
+" Si desdea ejecutar el servidor como un Servicio Windows, debe usar\n"
+" un archivo de configuración para especificar los directorios base\n"
+" de los trackers.\n"
+" Cuando ejecuta el Roundup Tracker como un servicio deb usar la\n"
+" opción para activar un archivo de registro.\n"
+" Tipee \"roundup-server -c help\" para ver ayuda específica para\n"
+" Servicios Web."
+
+#: ../roundup/scripts/roundup_server.py:535
+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 ""
+" -u <UID> ejecuta el servidor web de Roundup como este UID\n"
+" -g <GID> ejecuta el servidor web de Roundup como este GID\n"
+" -d <PIDfile> ejecuta el servidor web de Roundup en segundo plano y escribe el\n"
+" PID del servidor en el archivo especificado por PIDfile.\n"
+" La opción -l *debe* ser especificada si se usa la opción -d."
+
+#: ../roundup/scripts/roundup_server.py:542
+#, 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 ""
+"%(message)sUso: roundup-server [opciones] [nombre=directorio base de tracker]*\n"
+"\n"
+"Opciones:\n"
+" -v imprime el número de versión de Roundup y sale\n"
+" -h imprime este texto y sale\n"
+" -S crea o actualiza el archivo de configuración y sale\n"
+" -C <fname> usa el archivo de configuración <fname>\n"
+" -n <name> especifica el nombre de host de la instancia del servidor web de Roundup\n"
+" -p <port> especifica el puerto en el cual escuchará el servidor (por omisión: %(port)s)\n"
+" -l <fname> almacena bitácora en el archivo indicado por fname en lugar de hacerlo a stderr/stdout\n"
+" -N almacena en bitácora los nombres de los equipos clientes en lugar de direcciones IP (mucho mas lento)\n"
+" -t <mode> mod multiproceso (por omisión: %(mp_def)s).\n"
+" Valores permitidos: %(mp_types)s.\n"
+"%(os_part)s\n"
+"\n"
+"Opciones largas:\n"
+" --version imprime el número de versión de Roundup y sale\n"
+" --help imprime este texto y sale\n"
+" --save-config crea o actualiza el archivo de configuración y sale\n"
+" --config <fname> usa el archivo de configuración <fname>\n"
+" Todos las variables de la sección [main] del archivo de configuración\n"
+" pueden también especificarse usando la forma --<nombre>=<valor>\n"
+"\n"
+"Ejemplos:\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"
+"Formato de archivo de configuración:\n"
+" El archivo de configuración del Servidor Roundup tiene un formato de archivo.ini común.\n"
+" El archivo de configuración creado con 'roundup-server -S' contiene\n"
+" explicaciones detalladas para cada opción. Por favor vea dicho archivo para encontrar\n"
+" descripciones de las variables.\n"
+"\n"
+"Cómo usar \"nombre=directorio base de tracker\":\n"
+" Estos argumentos especifican el directorio base a usarse para el tracker.\n"
+" El nombre es cómo se identificará el tracker en la URL (será la primera parte\n"
+" en la ruta del la URL).\n"
+" El directorio base de tracker es el directorio que se identificó cuando se\n"
+" ejecutó \"roundup-admin init\". Pueden especificarse un número arbirario de dichos\n"
+" pares nombre=dirbase en la línea de comandos. Asegúrese de el nombre no contengan\n"
+" caracteres tales como espacios, dado que los mismos confunden a Internet Explorer.\n"
+
+#: ../roundup/scripts/roundup_server.py:690
+msgid "Instances must be name=home"
+msgstr "Las Instancias debe ser de la forma nombre=directorio base"
+
+#: ../roundup/scripts/roundup_server.py:704
+#, python-format
+msgid "Configuration saved to %s"
+msgstr "Configuración guardada en %s"
+
+#: ../roundup/scripts/roundup_server.py:722
+msgid "Sorry, you can't run the server as a daemon on this Operating System"
+msgstr "Lo siento, no puede ejecutar el servidor como un demonio en este Sistema Operativo"
+
+#: ../roundup/scripts/roundup_server.py:734
+#, python-format
+msgid "Roundup server started on %(HOST)s:%(PORT)s"
+msgstr "servidor Roundup iniciado en %(HOST)s:%(PORT)s"
+
+#: ../templates/classic/html/_generic.collision.html:4
+#: ../templates/minimal/html/_generic.collision.html:4
+msgid "${class} Edit Collision - ${tracker}"
+msgstr "Colisión de edición ${class} - ${tracker}"
+
+#: ../templates/classic/html/_generic.collision.html:7
+#: ../templates/minimal/html/_generic.collision.html:7
+msgid "${class} Edit Collision"
+msgstr "Colisión de edición ${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"
+" Se ha encontrado una colisión. Otro usuario ha actualizado este nodo\n"
+" mientras Ud. lo editaba. Por favor <a href='${context}'>revisualice</a>\n"
+" el nodo y revise sus modificaciones.\n"
+
+#: ../templates/classic/html/_generic.help.html:9
+#: ../templates/minimal/html/_generic.help.html:9
+msgid "${property} help - ${tracker}"
+msgstr "${property} ayuda - ${tracker}"
+
+#: ../templates/classic/html/_generic.help.html:31
+#: ../templates/minimal/html/_generic.help.html:31
+msgid " Cancel "
+msgstr " Cancelar "
+
+#: ../templates/classic/html/_generic.help.html:34
+#: ../templates/minimal/html/_generic.help.html:34
+msgid " Apply "
+msgstr " Aplicar "
+
+#: ../templates/classic/html/_generic.help.html:41
+#: ../templates/classic/html/issue.index.html:67
+#: ../templates/minimal/html/_generic.help.html:41
+msgid "<< previous"
+msgstr "<< anterior"
+
+#: ../templates/classic/html/_generic.help.html:52
+#: ../templates/classic/html/issue.index.html:75
+#: ../templates/minimal/html/_generic.help.html:52
+msgid "${start}..${end} out of ${total}"
+msgstr "${start}..${end} de un total de ${total}"
+
+#: ../templates/classic/html/_generic.help.html:56
+#: ../templates/classic/html/issue.index.html:78
+#: ../templates/minimal/html/_generic.help.html:56
+msgid "next >>"
+msgstr "próxima >>"
+
+#: ../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 "Edición 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 "Edición de ${class}"
+
+#: ../templates/classic/html/_generic.index.html:14
+#: ../templates/classic/html/_generic.item.html:12
+#: ../templates/classic/html/file.item.html:9
+#: ../templates/classic/html/issue.index.html:10
+#: ../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 "Ud. no posee los permisos necesarios para ver esta página."
+
+#: ../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\"> Puede editar el contenido de la clase ${classname} usando este formulario. Las comas, los saltos de línea y las comillas dobles (\") deben ser tratadas con cuidado. Para incluir comas y saltos de línea encierre los valores entre comillas dobles (\"). si quiere ingresar comillas dobles debe usarlas en grupos de a dos (\"\"). </p> <p class=\"form-help\"> Para las propiedades Multilink ingrese sus múltiples valores separados por dos puntos (\":\") (... ,\"uno:dos:tres\", ...) </p> <p class=\"form-help\"> Para eliminar elementos elimine la línea correspondiente. Para agregar nuevos elementos anéxelos a la tabla y coloque una X en la columna id. </p>"
+
+#: ../templates/classic/html/_generic.index.html:44
+#: ../templates/minimal/html/_generic.index.html:44
+msgid "Edit Items"
+msgstr "Editar Items"
+
+#: ../templates/classic/html/file.index.html:4
+msgid "List of files - ${tracker}"
+msgstr "Lista de archivos - ${tracker}"
+
+#: ../templates/classic/html/file.index.html:5
+msgid "List of files"
+msgstr "Lista de archivos"
+
+#: ../templates/classic/html/file.index.html:10
+msgid "Download"
+msgstr "Descargar"
+
+#: ../templates/classic/html/file.index.html:11
+#: ../templates/classic/html/file.item.html:22
+msgid "Content Type"
+msgstr "Tipo de Contenido"
+
+#: ../templates/classic/html/file.index.html:12
+msgid "Uploaded By"
+msgstr "Subido por"
+
+#: ../templates/classic/html/file.index.html:13
+#: ../templates/classic/html/msg.item.html:43
+msgid "Date"
+msgstr "Fecha"
+
+#: ../templates/classic/html/file.item.html:2
+msgid "File display - ${tracker}"
+msgstr "Visualización de archivos - ${tracker}"
+
+#: ../templates/classic/html/file.item.html:4
+msgid "File display"
+msgstr "Visualización de archivos"
+
+#: ../templates/classic/html/file.item.html:18
+#: ../templates/classic/html/user.item.html:39
+#: ../templates/classic/html/user.register.html:17
+msgid "Name"
+msgstr "Nombre"
+
+#: ../templates/classic/html/file.item.html:40
+msgid "download"
+msgstr "descargar"
+
+#: ../templates/classic/html/home.classlist.html:2
+#: ../templates/minimal/html/home.classlist.html:2
+msgid "List of classes - ${tracker}"
+msgstr "Lista de clases - ${tracker}"
+
+#: ../templates/classic/html/home.classlist.html:4
+#: ../templates/minimal/html/home.classlist.html:4
+msgid "List of classes"
+msgstr "Lista de clases"
+
+#: ../templates/classic/html/issue.index.html:4
+msgid "List of issues - ${tracker}"
+msgstr "Lista de issues - ${tracker}"
+
+#: ../templates/classic/html/issue.index.html:6
+msgid "List of issues"
+msgstr "Lista de issues"
+
+#: ../templates/classic/html/issue.index.html:16
+#: ../templates/classic/html/issue.item.html:44
+msgid "Priority"
+msgstr "Prioridad"
+
+#: ../templates/classic/html/issue.index.html:17
+msgid "ID"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:18
+msgid "Creation"
+msgstr "Creación"
+
+#: ../templates/classic/html/issue.index.html:19
+msgid "Activity"
+msgstr "Actividad"
+
+#: ../templates/classic/html/issue.index.html:20
+msgid "Actor"
+msgstr "ültimo actor"
+
+#: ../templates/classic/html/issue.index.html:21
+msgid "Topic"
+msgstr "Palabra clave"
+
+#: ../templates/classic/html/issue.index.html:22
+#: ../templates/classic/html/issue.item.html:39
+msgid "Title"
+msgstr "Título"
+
+#: ../templates/classic/html/issue.index.html:23
+#: ../templates/classic/html/issue.item.html:46
+msgid "Status"
+msgstr "Estado"
+
+#: ../templates/classic/html/issue.index.html:24
+msgid "Creator"
+msgstr "Creador"
+
+#: ../templates/classic/html/issue.index.html:25
+msgid "Assigned To"
+msgstr "Asignado a"
+
+#: ../templates/classic/html/issue.index.html:91
+msgid "Download as CSV"
+msgstr "Descargar como CSV"
+
+#: ../templates/classic/html/issue.index.html:99
+msgid "Sort on:"
+msgstr "Ordenar por:"
+
+#: ../templates/classic/html/issue.index.html:102
+#: ../templates/classic/html/issue.index.html:119
+msgid "- nothing -"
+msgstr "- nada -"
+
+#: ../templates/classic/html/issue.index.html:110
+#: ../templates/classic/html/issue.index.html:127
+msgid "Descending:"
+msgstr "Descendente:"
+
+#: ../templates/classic/html/issue.index.html:116
+msgid "Group on:"
+msgstr "Agrupar por:"
+
+#: ../templates/classic/html/issue.index.html:133
+msgid "Redisplay"
+msgstr "Revisualizar"
+
+#: ../templates/classic/html/issue.item.html:7
+msgid "Issue ${id}: ${title} - ${tracker}"
+msgstr "Issue ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:10
+#, fuzzy
+msgid "New Issue - ${tracker}"
+msgstr "Issue nuevo - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:14
+#, fuzzy
+msgid "New Issue"
+msgstr "Issue nuevo"
+
+#: ../templates/classic/html/issue.item.html:16
+msgid "New Issue Editing"
+msgstr "Edición de Nuevo Issue"
+
+#: ../templates/classic/html/issue.item.html:19
+msgid "Issue${id}"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:22
+msgid "Issue${id} Editing"
+msgstr "Edición de Issue${id}"
+
+#: ../templates/classic/html/issue.item.html:51
+msgid "Superseder"
+msgstr "Reemplazado por"
+
+#: ../templates/classic/html/issue.item.html:56
+msgid "View: ${link}"
+msgstr "Ver: ${link}"
+
+#: ../templates/classic/html/issue.item.html:60
+msgid "Nosy List"
+msgstr "Lista de interesados"
+
+#: ../templates/classic/html/issue.item.html:69
+msgid "Assigned To"
+msgstr "Asignado a"
+
+#: ../templates/classic/html/issue.item.html:71
+msgid "Topics"
+msgstr "Palabras clave"
+
+#: ../templates/classic/html/issue.item.html:79
+msgid "Change Note"
+msgstr "Nota de modificación"
+
+#: ../templates/classic/html/issue.item.html:87
+msgid "File"
+msgstr "Archivo"
+
+#: ../templates/classic/html/issue.item.html:106
+msgid "<table class=\"form\"> <tr> <td>Note: </td> <th class=\"required\">highlighted</th> <td> fields are required.</td> </tr> </table>"
+msgstr "<table class=\"form\"> <tr> <td>Nota: Los campos </td> <th class=\"required\">resaltados</th> <td> son obligatorios.</td> </tr> </table>"
+
+#: ../templates/classic/html/issue.item.html:120
+msgid "Created on <b>${creation}</b> by <b>${creator}</b>, last changed <b>${activity}</b> by <b>${actor}</b>."
+msgstr "Creado el <b>${creation}</b> por <b>${creator}</b>, última modificación el <b>${activity}</b> por <b>${actor}</b>."
+
+#: ../templates/classic/html/issue.item.html:124
+#: ../templates/classic/html/msg.item.html:56
+msgid "Files"
+msgstr "Archivos"
+
+#: ../templates/classic/html/issue.item.html:126
+#: ../templates/classic/html/msg.item.html:58
+msgid "File name"
+msgstr "Nombre de archivo"
+
+#: ../templates/classic/html/issue.item.html:127
+#: ../templates/classic/html/msg.item.html:59
+msgid "Uploaded"
+msgstr "Subido"
+
+#: ../templates/classic/html/issue.item.html:128
+msgid "Type"
+msgstr "Tipo"
+
+#: ../templates/classic/html/issue.item.html:129
+#: ../templates/classic/html/query.edit.html:30
+msgid "Edit"
+msgstr "Editar"
+
+#: ../templates/classic/html/issue.item.html:130
+msgid "Remove"
+msgstr "Eliminar"
+
+#: ../templates/classic/html/issue.item.html:150
+#: ../templates/classic/html/issue.item.html:171
+#: ../templates/classic/html/query.edit.html:50
+msgid "remove"
+msgstr "eliminar"
+
+#: ../templates/classic/html/issue.item.html:157
+#: ../templates/classic/html/msg.index.html:9
+msgid "Messages"
+msgstr "Mensajes"
+
+#: ../templates/classic/html/issue.item.html:161
+msgid "msg${id} (view)"
+msgstr "mensaje${id} (ver)"
+
+#: ../templates/classic/html/issue.item.html:162
+msgid "Author: ${author}"
+msgstr "Autor: ${author}"
+
+#: ../templates/classic/html/issue.item.html:164
+msgid "Date: ${date}"
+msgstr "Fecha: ${date}"
+
+#: ../templates/classic/html/issue.search.html:2
+msgid "Issue searching - ${tracker}"
+msgstr "Búsqueda de Issues - ${tracker}"
+
+#: ../templates/classic/html/issue.search.html:4
+msgid "Issue searching"
+msgstr "Búsqueda de Issues"
+
+#: ../templates/classic/html/issue.search.html:25
+msgid "Filter on"
+msgstr "Filtrar por"
+
+#: ../templates/classic/html/issue.search.html:26
+msgid "Display"
+msgstr "Visualizar"
+
+#: ../templates/classic/html/issue.search.html:27
+msgid "Sort on"
+msgstr "Ordenar por"
+
+#: ../templates/classic/html/issue.search.html:28
+msgid "Group on"
+msgstr "Agrupar por"
+
+#: ../templates/classic/html/issue.search.html:32
+msgid "All text*:"
+msgstr "Todo el texto*:"
+
+#: ../templates/classic/html/issue.search.html:40
+msgid "Title:"
+msgstr "Título:"
+
+#: ../templates/classic/html/issue.search.html:50
+msgid "Topic:"
+msgstr "Palabra clave:"
+
+#: ../templates/classic/html/issue.search.html:58
+msgid "ID:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:66
+msgid "Creation Date:"
+msgstr "Fecha de creación:"
+
+#: ../templates/classic/html/issue.search.html:77
+msgid "Creator:"
+msgstr "Creador:"
+
+#: ../templates/classic/html/issue.search.html:79
+msgid "created by me"
+msgstr "creado por mí"
+
+#: ../templates/classic/html/issue.search.html:88
+msgid "Activity:"
+msgstr "Actividad:"
+
+#: ../templates/classic/html/issue.search.html:99
+msgid "Actor:"
+msgstr "Último actor:"
+
+#: ../templates/classic/html/issue.search.html:101
+msgid "done by me"
+msgstr "hecho por mí"
+
+#: ../templates/classic/html/issue.search.html:112
+msgid "Priority:"
+msgstr "Prioridad:"
+
+#: ../templates/classic/html/issue.search.html:114
+#: ../templates/classic/html/issue.search.html:130
+msgid "not selected"
+msgstr "no seleccionado"
+
+#: ../templates/classic/html/issue.search.html:125
+msgid "Status:"
+msgstr "Estado:"
+
+#: ../templates/classic/html/issue.search.html:128
+msgid "not resolved"
+msgstr "sin resolver"
+
+#: ../templates/classic/html/issue.search.html:143
+msgid "Assigned to:"
+msgstr "Asignado a:"
+
+#: ../templates/classic/html/issue.search.html:146
+msgid "assigned to me"
+msgstr "asignado a mí"
+
+#: ../templates/classic/html/issue.search.html:148
+msgid "unassigned"
+msgstr "no asignado"
+
+#: ../templates/classic/html/issue.search.html:158
+msgid "No Sort or group:"
+msgstr "No ordenar o agrupar"
+
+#: ../templates/classic/html/issue.search.html:166
+msgid "Pagesize:"
+msgstr "Tamaño de página"
+
+#: ../templates/classic/html/issue.search.html:172
+msgid "Start With:"
+msgstr "Comenzar con:"
+
+#: ../templates/classic/html/issue.search.html:178
+msgid "Sort Descending:"
+msgstr "Ordenar en forma descendente:"
+
+#: ../templates/classic/html/issue.search.html:185
+msgid "Group Descending:"
+msgstr "Agrupar en forma descendente:"
+
+#: ../templates/classic/html/issue.search.html:192
+msgid "Query name**:"
+msgstr "Nombre de la consulta**:"
+
+#: ../templates/classic/html/issue.search.html:202
+#: ../templates/classic/html/page.html:31
+#: ../templates/classic/html/page.html:60
+#: ../templates/minimal/html/page.html:31
+msgid "Search"
+msgstr "Buscar"
+
+#: ../templates/classic/html/issue.search.html:206
+msgid "*: The \"all text\" field will look in message bodies and issue titles<br> **: If you supply a name, the query will be saved off and available as a link in the sidebar"
+msgstr "*: El campo \"Todo el texto\" busca en los cuerpos de los mensajes y los títulos de los issues<br> **: Si Ud. provee un nombre, la consulta será grabada y estará disponible como un enlace en la barra lateral"
+
+#: ../templates/classic/html/keyword.item.html:3
+msgid "Keyword editing - ${tracker}"
+msgstr "Edición de Palabras clave - ${tracker}"
+
+#: ../templates/classic/html/keyword.item.html:5
+msgid "Keyword editing"
+msgstr "Edición de Palabras clave"
+
+#: ../templates/classic/html/keyword.item.html:11
+msgid "Existing Keywords"
+msgstr "Palabras clave existentes"
+
+#: ../templates/classic/html/keyword.item.html:20
+msgid "To edit an existing keyword (for spelling or typing errors), click on its entry above."
+msgstr "Para editar una Palabra clave existente (para corregir errores de ortografía o errores de tipeo), haga click en la misma arriba."
+
+#: ../templates/classic/html/keyword.item.html:27
+msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
+msgstr "Para crear una nueva Palabra clave, ingrese la misma abajo y haga click en \"Crear nuevo elemento\"."
+
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
+msgstr "Palabra clave"
+
+#: ../templates/classic/html/msg.index.html:3
+msgid "List of messages - ${tracker}"
+msgstr "Lista de mensajes - ${tracker}"
+
+#: ../templates/classic/html/msg.index.html:5
+msgid "Message listing"
+msgstr "Listado de mensajes"
+
+#: ../templates/classic/html/msg.item.html:6
+msgid "Message ${id} - ${tracker}"
+msgstr "Mensaje ${id} - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:9
+msgid "New Message - ${tracker}"
+msgstr "Nuevo mensaje - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:13
+msgid "New Message"
+msgstr "Nuevo mensaje"
+
+#: ../templates/classic/html/msg.item.html:15
+msgid "New Message Editing"
+msgstr "Edición de nuevo mensaje"
+
+#: ../templates/classic/html/msg.item.html:18
+msgid "Message${id}"
+msgstr "Mensaje${id}"
+
+#: ../templates/classic/html/msg.item.html:21
+msgid "Message${id} Editing"
+msgstr "Edición de Mensaje${id}"
+
+#: ../templates/classic/html/msg.item.html:33
+msgid "Author"
+msgstr "Autor"
+
+#: ../templates/classic/html/msg.item.html:38
+msgid "Recipients"
+msgstr "Destinatarios"
+
+#: ../templates/classic/html/msg.item.html:49
+msgid "Content"
+msgstr "Contenido"
+
+#: ../templates/classic/html/page.html:41
+msgid "<b>Your Queries</b> (<a href=\"query?@template=edit\">edit</a>)"
+msgstr "<b>Sus consultas</b> (<a href=\"query?@template=edit\">editar</a>)"
+
+#: ../templates/classic/html/page.html:52
+msgid "Issues"
+msgstr ""
+
+#: ../templates/classic/html/page.html:54
+#: ../templates/classic/html/page.html:74
+msgid "Create New"
+msgstr "Crear"
+
+#: ../templates/classic/html/page.html:56
+msgid "Show Unassigned"
+msgstr "Mostrar no asignados"
+
+#: ../templates/classic/html/page.html:58
+msgid "Show All"
+msgstr "Mostrar todos"
+
+#: ../templates/classic/html/page.html:61
+msgid "Show issue:"
+msgstr "Mostrar issue:"
+
+#: ../templates/classic/html/page.html:72
+msgid "Keywords"
+msgstr "Palabras clave"
+
+#: ../templates/classic/html/page.html:78
+msgid "Edit Existing"
+msgstr "Editar existentes"
+
+#: ../templates/classic/html/page.html:84
+#: ../templates/minimal/html/page.html:62
+msgid "Administration"
+msgstr "Administración"
+
+#: ../templates/classic/html/page.html:86
+#: ../templates/minimal/html/page.html:63
+msgid "Class List"
+msgstr "Lista de clases"
+
+#: ../templates/classic/html/page.html:90
+#: ../templates/minimal/html/page.html:65
+msgid "User List"
+msgstr "Lista de usuarios"
+
+#: ../templates/classic/html/page.html:92
+#: ../templates/minimal/html/page.html:68
+msgid "Add User"
+msgstr "Agregar usuario"
+
+#: ../templates/classic/html/page.html:99
+#: ../templates/classic/html/page.html:103
+#: ../templates/minimal/html/page.html:44
+msgid "Login"
+msgstr "Ingresar"
+
+#: ../templates/classic/html/page.html:105
+#: ../templates/classic/html/user.register.html:63
+#: ../templates/minimal/html/page.html:47
+#: ../templates/minimal/html/user.register.html:58
+msgid "Register"
+msgstr "Registrarse"
+
+#: ../templates/classic/html/page.html:108
+msgid "Lost your login?"
+msgstr "Olvidó su contraseña?"
+
+#: ../templates/classic/html/page.html:113
+msgid "Hello, ${user}"
+msgstr "Hola, ${user}"
+
+#: ../templates/classic/html/page.html:115
+msgid "Your Issues"
+msgstr "Sus Issues"
+
+#: ../templates/classic/html/page.html:116
+#: ../templates/minimal/html/page.html:54
+msgid "Your Details"
+msgstr "Sus datos personales"
+
+#: ../templates/classic/html/page.html:118
+#: ../templates/minimal/html/page.html:56
+msgid "Logout"
+msgstr "Salir"
+
+#: ../templates/classic/html/page.html:122
+msgid "Help"
+msgstr "Ayuda"
+
+#: ../templates/classic/html/page.html:123
+msgid "Roundup docs"
+msgstr "Doc. de Roundup"
+
+#: ../templates/classic/html/page.html:174
+msgid "don't care"
+msgstr "cualquier(a)"
+
+#: ../templates/classic/html/page.html:176
+msgid "------------"
+msgstr ""
+
+#: ../templates/classic/html/page.html:203
+msgid "no value"
+msgstr "sin valor"
+
+#: ../templates/classic/html/query.edit.html:4
+msgid "\"Your Queries\" Editing - ${tracker}"
+msgstr "Edición de \"Sus consultas\" - ${tracker}"
+
+#: ../templates/classic/html/query.edit.html:6
+msgid "\"Your Queries\" Editing"
+msgstr "Edición de \"Sus consultas\""
+
+#: ../templates/classic/html/query.edit.html:11
+msgid "You are not allowed to edit queries."
+msgstr "Ud. no posee los permisos necesarios para editar consultas."
+
+#: ../templates/classic/html/query.edit.html:28
+msgid "Query"
+msgstr "Consulta"
+
+#: ../templates/classic/html/query.edit.html:29
+msgid "Include in \"Your Queries\""
+msgstr "Incluir en \"Sus consultas\""
+
+#: ../templates/classic/html/query.edit.html:31
+msgid "Private to you?"
+msgstr "Privada a Ud.?"
+
+#: ../templates/classic/html/query.edit.html:44
+msgid "leave out"
+msgstr "no incluir"
+
+#: ../templates/classic/html/query.edit.html:45
+msgid "include"
+msgstr "incluir"
+
+#: ../templates/classic/html/query.edit.html:49
+msgid "leave in"
+msgstr "incluir"
+
+#: ../templates/classic/html/query.edit.html:54
+msgid "[query is retired]"
+msgstr "[consulta retirada]"
+
+#: ../templates/classic/html/query.edit.html:67
+msgid "edit"
+msgstr "editar"
+
+#: ../templates/classic/html/query.edit.html:71
+msgid "yes"
+msgstr "si"
+
+#: ../templates/classic/html/query.edit.html:73
+msgid "no"
+msgstr "no"
+
+#: ../templates/classic/html/query.edit.html:79
+msgid "Delete"
+msgstr "Eliminar"
+
+#: ../templates/classic/html/query.edit.html:90
+msgid "[not yours to edit]"
+msgstr "[no puede editar una consulta que no le pertenece]"
+
+#: ../templates/classic/html/query.edit.html:96
+msgid "Save Selection"
+msgstr "Guardar selección"
+
+#: ../templates/classic/html/user.forgotten.html:3
+msgid "Password reset request - ${tracker}"
+msgstr "Solicitud de generación de nueva contraseña - ${tracker}"
+
+#: ../templates/classic/html/user.forgotten.html:5
+msgid "Password reset request"
+msgstr "solicitud de generación de nueva contraseña"
+
+#: ../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 "Si ha olvidado su contraseña dispone de dos opciones. Si recuerda la dirección de e-mail con la que se registró, ingrésela abajo."
+
+#: ../templates/classic/html/user.forgotten.html:16
+msgid "Email Address:"
+msgstr "Dirección de e-mail:"
+
+#: ../templates/classic/html/user.forgotten.html:24
+#: ../templates/classic/html/user.forgotten.html:34
+msgid "Request password reset"
+msgstr "Solicitar generación nueva contraseña"
+
+#: ../templates/classic/html/user.forgotten.html:30
+msgid "Or, if you know your username, then enter it below."
+msgstr "O, si conoce su nombre de usuario, ingréselo abajo."
+
+#: ../templates/classic/html/user.forgotten.html:33
+msgid "Username:"
+msgstr "Nombre de usuario:"
+
+#: ../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 "Se le enviará un mensaje de e-mail - por favor siga las instrucciones detalladas en el mismo para completar el proceso de generación de nueva una contraseña."
+
+#: ../templates/classic/html/user.index.html:3
+#: ../templates/minimal/html/user.index.html:3
+msgid "User listing - ${tracker}"
+msgstr "Listado de usuarios - ${tracker}"
+
+#: ../templates/classic/html/user.index.html:5
+#: ../templates/minimal/html/user.index.html:5
+msgid "User listing"
+msgstr "Listado de usuarios"
+
+#: ../templates/classic/html/user.index.html:14
+#: ../templates/minimal/html/user.index.html:14
+msgid "Username"
+msgstr "Nombre de usuario"
+
+#: ../templates/classic/html/user.index.html:15
+msgid "Real name"
+msgstr "Nombre real"
+
+#: ../templates/classic/html/user.index.html:16
+#: ../templates/classic/html/user.item.html:70
+#: ../templates/classic/html/user.register.html:45
+msgid "Organisation"
+msgstr "Organización"
+
+#: ../templates/classic/html/user.index.html:17
+#: ../templates/minimal/html/user.index.html:15
+msgid "Email address"
+msgstr "Dirección de e-mail"
+
+#: ../templates/classic/html/user.index.html:18
+msgid "Phone number"
+msgstr "Nro. telefónico"
+
+#: ../templates/classic/html/user.index.html:19
+msgid "Retire"
+msgstr "Retirar"
+
+#: ../templates/classic/html/user.index.html:32
+msgid "retire"
+msgstr "retirar"
+
+#: ../templates/classic/html/user.item.html:7
+msgid "User ${id}: ${title} - ${tracker}"
+msgstr "Usuario ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:10
+msgid "New User - ${tracker}"
+msgstr "Nuevo usuario - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:14
+#: ../templates/minimal/html/user.item.html:6
+msgid "New User"
+msgstr "Nuevo usuario"
+
+#: ../templates/classic/html/user.item.html:16
+#: ../templates/minimal/html/user.item.html:8
+msgid "New User Editing"
+msgstr "Edición de nuevo usuario"
+
+#: ../templates/classic/html/user.item.html:19
+#: ../templates/minimal/html/user.item.html:11
+msgid "User${id}"
+msgstr "Usuario${id}"
+
+#: ../templates/classic/html/user.item.html:22
+#: ../templates/minimal/html/user.item.html:14
+msgid "User${id} Editing"
+msgstr "Edición de Usuario${id}"
+
+#: ../templates/classic/html/user.item.html:43
+#: ../templates/classic/html/user.register.html:21
+#: ../templates/minimal/html/user.item.html:27
+#: ../templates/minimal/html/user.item.html:67
+#: ../templates/minimal/html/user.register.html:26
+msgid "Login Name"
+msgstr "Nombre para Login"
+
+#: ../templates/classic/html/user.item.html:47
+#: ../templates/classic/html/user.register.html:25
+#: ../templates/minimal/html/user.item.html:31
+#: ../templates/minimal/html/user.register.html:30
+msgid "Login Password"
+msgstr "Contraseña para Login"
+
+#: ../templates/classic/html/user.item.html:51
+#: ../templates/classic/html/user.register.html:29
+#: ../templates/minimal/html/user.item.html:35
+#: ../templates/minimal/html/user.register.html:34
+msgid "Confirm Password"
+msgstr "Confirmar contraseña"
+
+#: ../templates/classic/html/user.item.html:55
+#: ../templates/classic/html/user.register.html:33
+#: ../templates/minimal/html/user.item.html:39
+#: ../templates/minimal/html/user.register.html:38
+msgid "Roles"
+msgstr "Roles"
+
+#: ../templates/classic/html/user.item.html:61
+msgid "(to give the user more than one role, enter a comma,separated,list)"
+msgstr "(para asignar más de un rol al usuario, ingrese una lista de los mismos separados por comas)"
+
+#: ../templates/classic/html/user.item.html:66
+#: ../templates/classic/html/user.register.html:41
+msgid "Phone"
+msgstr "Teléfono"
+
+#: ../templates/classic/html/user.item.html:74
+msgid "Timezone"
+msgstr "Zona horaria"
+
+#: ../templates/classic/html/user.item.html:78
+msgid "(this is a numeric hour offset, the default is ${zone})"
+msgstr "(este es un valor numérico de diferencia horaria, el valor por defecto es ${zone})"
+
+#: ../templates/classic/html/user.item.html:83
+#: ../templates/classic/html/user.register.html:49
+#: ../templates/minimal/html/user.item.html:47
+#: ../templates/minimal/html/user.item.html:71
+#: ../templates/minimal/html/user.register.html:46
+msgid "E-mail address"
+msgstr "Dirección de e-mail"
+
+#: ../templates/classic/html/user.item.html:87
+#: ../templates/classic/html/user.register.html:53
+#: ../templates/minimal/html/user.item.html:51
+#: ../templates/minimal/html/user.register.html:50
+msgid "Alternate E-mail addresses<br>One address per line"
+msgstr "Direcciones de e-mail alternativas <br>Una dirección por línea"
+
+#: ../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 "Registrándose en ${tracker}"
+
+#: ../templates/classic/html/user.rego_progress.html:4
+#: ../templates/minimal/html/user.rego_progress.html:4
+msgid "Registration in progress - ${tracker}"
+msgstr "Registro en marcha - ${tracker}"
+
+#: ../templates/classic/html/user.rego_progress.html:6
+#: ../templates/minimal/html/user.rego_progress.html:6
+msgid "Registration in progress..."
+msgstr "Registro en marcha..."
+
+#: ../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 "En breve recibirá un mensaje de e-mail para confirmar su registro. Para completar el proceso, visite el enlace indicado en dicho mensaje."
+
+#: ../templates/minimal/html/home.html:2
+msgid "Tracker home - ${tracker}"
+msgstr "Directorio base del tracker - ${tracker}"
+
+#: ../templates/minimal/html/home.html:4
+msgid "Tracker home"
+msgstr "Directorio base del tracker"
+
+#: ../templates/minimal/html/home.html:16
+msgid "Please select from one of the menu options on the left."
+msgstr "Por favor seleccione entre las opciones del menú a la izquierda."
+
+#: ../templates/minimal/html/home.html:19
+msgid "Please log in or register."
+msgstr "Por favor ingrese al sistema o regístrese en el mismo."
+
+#: ../templates/minimal/html/page.html:52
+msgid "Hello,<br>${user}"
+msgstr "Hola,<br>${user}"
+
+#: ../templates/minimal/html/user.item.html:3
+msgid "User editing - ${tracker}"
+msgstr "Edición de usuario - ${tracker}"
+
Added: tracker/vendor/roundup/current/locale/fr.po
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/locale/fr.po Sun Nov 5 21:30:25 2006
@@ -0,0 +1,3111 @@
+# French message file for Roundup Issue Tracker
+# Georges Martin <georges.martin at pi.be>, 2004.
+# This file is distributed under the same license as the Roundup package.
+#
+# $Id: fr.po,v 1.4 2006/03/25 16:04:36 a1s Exp $
+# roundup.pot revision 1.17
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Roundup 1.1.1\n"
+"Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
+"POT-Creation-Date: 2006-02-28 07:44+0200\n"
+"PO-Revision-Date: 2006-03-22 12:50 GMT+1\n"
+"Last-Translator: Patrick Decat <pdecat at gmail.com>\n"
+"Language-Team: French Translators <roundup-devel at lists.sourceforge.net>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ISO-8859-1\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n !=1;\n"
+
+# ../roundup/admin.py:85 :979 :1028 :1050
+#: ../roundup/admin.py:85 ../roundup/admin.py:979 ../roundup/admin.py:1028
+#: ../roundup/admin.py:1050
+#, python-format
+msgid "no such class \"%(classname)s\""
+msgstr "la classe \"%(classname)s\" n'existe pas"
+
+# ../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"
+
+#: ../roundup/admin.py:112
+#, python-format
+msgid ""
+"Problem: %(message)s\n"
+"\n"
+msgstr ""
+"Problème: %(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"
+"\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 ""
+"%(message)sUsage: roundup-admin [options] [<commande> <arguments>]\n"
+"\n"
+"Options:\n"
+" -i base-du-pisteur -- spécifie le répertoire de base du pisteur à\n"
+" administrer.\n"
+" -u -- le nom-d'utilisateur[:mot-de-passe] à utiliser\n"
+" pour les commandes.\n"
+" -d -- imprime les indicateurs complets, pas seulement\n"
+" 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"
+" -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"
+"\n"
+" Une seule des options -s, -c ou -S peut être spécifiée.\n"
+"\n"
+"Aide:\n"
+" roundup-admin -h\n"
+" roundup-admin help -- cette aide\n"
+" roundup-admin help <commande> -- l'aide sur une commande\n"
+" roundup-admin help all -- toute l'aide disponible\n"
+
+#: ../roundup/admin.py:138
+msgid "Commands:"
+msgstr "Commandes:"
+
+#: ../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 ""
+"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."
+
+#: ../roundup/admin.py:175
+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 ""
+"\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"
+"\n"
+"Un indicateur est la concaténation d'un nom de classe et d'un\n"
+"identificateur de noeud, par 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"
+" 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"
+" 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"
+" . 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"
+"\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"
+" \"08-13.22:13\" donne <Date yyyy-08-14.03:13:00>\n"
+" \"11-07.09:32:43\" donne <Date yyyy-11-07.14:32:43>\n"
+" \"14:25\" donne <Date yyyy-mm-dd.19:25:00>\n"
+" \"8:47:11\" donne <Date yyyy-mm-dd.13:47:11>\n"
+" \".\" donne \"maintenant\"\n"
+"\n"
+"Aide sur les commandes:\n"
+
+#: ../roundup/admin.py:238
+#, python-format
+msgid "%s:"
+msgstr "%s:"
+
+#: ../roundup/admin.py:243
+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 ""
+"Usage: help sujet\n"
+" Donne de l'aide sur un sujet.\n"
+"\n"
+" commands -- liste les commandes\n"
+" <commande> -- aide spécifique à une commande\n"
+" initopts -- options des commandes d'initialisation\n"
+" all -- toute l'aide disponible\n"
+" "
+
+#: ../roundup/admin.py:266
+#, python-format
+msgid "Sorry, no help for \"%(topic)s\""
+msgstr "Désolé, aucune aide n'est disponible à propos de \"%(topic)s\""
+
+# ../roundup/admin.py:338 :394
+#: ../roundup/admin.py:338 ../roundup/admin.py:394
+msgid "Templates:"
+msgstr "Modèles:"
+
+# ../roundup/admin.py:341 :405
+#: ../roundup/admin.py:341 ../roundup/admin.py:405
+msgid "Back ends:"
+msgstr "Moteurs de stockage:"
+
+#: ../roundup/admin.py:344
+msgid ""
+"Usage: install [template [backend [admin password [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"
+"\n"
+" The last command line argument allows to pass initial values\n"
+" 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 ""
+"Usage: 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"
+"\n"
+" Voyez également l'aide sur \"initopts\".\n"
+" "
+
+# ../roundup/admin.py:367 :464 :525 :604 :654 :712 :733 :761 :832 :899 :970
+# :1018 :1040 :1067 :1134 :1204
+#: ../roundup/admin.py:367 ../roundup/admin.py:464 ../roundup/admin.py:525
+#: ../roundup/admin.py:604 ../roundup/admin.py:654 ../roundup/admin.py:712
+#: ../roundup/admin.py:733 ../roundup/admin.py:761 ../roundup/admin.py:832
+#: ../roundup/admin.py:899 ../roundup/admin.py:970 ../roundup/admin.py:1018
+#: ../roundup/admin.py:1040 ../roundup/admin.py:1067 ../roundup/admin.py:1134
+#: ../roundup/admin.py:1204
+msgid "Not enough arguments supplied"
+msgstr "Pas assez d'arguments fournis"
+
+#: ../roundup/admin.py:373
+#, 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"
+
+#: ../roundup/admin.py:381
+#, 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 ""
+"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: "
+
+#: ../roundup/admin.py:396
+msgid "Select template [classic]: "
+msgstr "Sélectionnez un modèle [classic]: "
+
+#: ../roundup/admin.py:407
+msgid "Select backend [anydbm]: "
+msgstr "Sélectionnez un moteur de stockage [anydbm]: "
+
+#: ../roundup/admin.py:417
+#, python-format
+msgid "Error in configuration settings: \"%s\""
+msgstr "Erreur dans les paramètres de la configuration : \"%s\""
+
+#: ../roundup/admin.py:426
+#, python-format
+msgid ""
+"\n"
+"---------------------------------------------------------------------------\n"
+" You should now edit the tracker configuration file:\n"
+" %(config_file)s"
+msgstr ""
+
+#: ../roundup/admin.py:436
+msgid " ... at a minimum, you must set following options:"
+msgstr ""
+
+#: ../roundup/admin.py:441
+#, 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:459
+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:469
+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 ""
+"Usage: 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"
+"\n"
+" Exécute la fonction d'initialisation dbinit.init() du pisteur\n"
+" "
+
+#: ../roundup/admin.py:483
+msgid "Admin Password: "
+msgstr "Mot de passe d'administrateur: "
+
+#: ../roundup/admin.py:484
+msgid " Confirm: "
+msgstr " Confirmez: "
+
+#: ../roundup/admin.py:488
+msgid "Instance home does not exist"
+msgstr "Le répertoire de base de l'instance n'existe pas"
+
+#: ../roundup/admin.py:492
+msgid "Instance has not been installed"
+msgstr "L'instance n'a pas été installée"
+
+#: ../roundup/admin.py:497
+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 ""
+"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: "
+
+#: ../roundup/admin.py:518
+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 ""
+"Usage: get property indicateur[,indicateur]*\n"
+" Donne la propriété spécifiée d'un ou plusieurs indicateurs.\n"
+"\n"
+" Donne la valeur de la propriété des noeuds spécifiés par\n"
+" les indicateurs.\n"
+" "
+
+# ../roundup/admin.py:558 :573
+#: ../roundup/admin.py:558 ../roundup/admin.py:573
+#, 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 ne"
+"s'applique pas."
+
+# ../roundup/admin.py:581 :981 :1030 :1052
+#: ../roundup/admin.py:581 ../roundup/admin.py:981 ../roundup/admin.py:1030
+#: ../roundup/admin.py:1052
+#, python-format
+msgid "no such %(classname)s node \"%(nodeid)s\""
+msgstr "le noeud \"%(nodeid)s\" de classe \"%(classname)s\" n'existe pas"
+
+#: ../roundup/admin.py:583
+#, python-format
+msgid "no such %(classname)s property \"%(propname)s\""
+msgstr ""
+"la propriété \"%(propname)s\" n'existe pas pour la classe \"%(classname)s\""
+
+#: ../roundup/admin.py:592
+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 ""
+"Usage: 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"
+" "
+
+#: ../roundup/admin.py:646
+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 ""
+"Usage: find nom-de-classe propriété=valeur ...\n"
+" Recherche les noeuds de la classe spécifié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"
+" lien donnée. La valeur peut être soit l'identificateur de noeud du\n"
+" noeud lié, ou sa valeur de clé.\n"
+" "
+
+# ../roundup/admin.py:699 :852 :864 :918
+#: ../roundup/admin.py:699 ../roundup/admin.py:852 ../roundup/admin.py:864
+#: ../roundup/admin.py:918
+#, python-format
+msgid "%(classname)s has no property \"%(propname)s\""
+msgstr "%(classname)s n'a pas de propriété \"%(propname)s\""
+
+#: ../roundup/admin.py:706
+msgid ""
+"Usage: specification classname\n"
+" Show the properties for a classname.\n"
+"\n"
+" 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"
+"\n"
+" Cette commande énumère les propriétés d'une classe donnée.\n"
+" "
+
+#: ../roundup/admin.py:721
+#, python-format
+msgid "%(key)s: %(value)s (key property)"
+msgstr "%(key)s: %(value)s (propriété clé)"
+
+#: ../roundup/admin.py:723
+#, python-format
+msgid "%(key)s: %(value)s"
+msgstr "%(key)s: %(value)s"
+
+#: ../roundup/admin.py:726
+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 ""
+"Usage: display indicateur[,indicateur]*\n"
+" Donne les valeurs des propriétés pour les noeuds spécifiés.\n"
+"\n"
+" Cette commande énumère les propriétés et leurs valeurs du ou\n"
+" des noeuds spécifiés.\n"
+" "
+
+#: ../roundup/admin.py:750
+#, python-format
+msgid "%(key)s: %(value)r"
+msgstr "%(key)s: %(value)r"
+
+#: ../roundup/admin.py:753
+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 ""
+"Usage: 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"
+" "
+
+#: ../roundup/admin.py:780
+#, python-format
+msgid "%(propname)s (Password): "
+msgstr "%(propname)s (Mot de passe): "
+
+#: ../roundup/admin.py:782
+#, python-format
+msgid " %(propname)s (Again): "
+msgstr " %(propname)s (À nouveau): "
+
+#: ../roundup/admin.py:784
+msgid "Sorry, try again..."
+msgstr "Désolé, essayez encore..."
+
+#: ../roundup/admin.py:788
+#, python-format
+msgid "%(propname)s (%(proptype)s): "
+msgstr "%(propname)s (%(proptype)s): "
+
+#: ../roundup/admin.py:806
+#, python-format
+msgid "you must provide the \"%(propname)s\" property."
+msgstr "Vous devez fournir la propriété \"%(propname)s\"."
+
+#: ../roundup/admin.py:817
+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 ""
+"Usage: list nom-de-classe [propriété]\n"
+" Donne 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"
+"\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"
+" "
+
+#: ../roundup/admin.py:830
+msgid "Too many arguments supplied"
+msgstr "Trop d'arguments fournis"
+
+#: ../roundup/admin.py:866
+#, python-format
+msgid "%(nodeid)4s: %(value)s"
+msgstr "%(nodeid)4s: %(value)s"
+
+#: ../roundup/admin.py:870
+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 ""
+"Usage: 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"
+" 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"
+"\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"
+" 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"
+"\n"
+" roundup> table priority id,name:\n"
+" Id Name\n"
+" 1 fata\n"
+" 2 bug\n"
+" 3 usab\n"
+" 4 feat\n"
+"\n"
+" donnera une colonne \"Name\" large de 4 caractères.\n"
+" "
+
+#: ../roundup/admin.py:914
+#, python-format
+msgid "\"%(spec)s\" not name:width"
+msgstr "\"%(spec)s\" ne correspond pas au format \"nom:largeur\""
+
+#: ../roundup/admin.py:964
+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 ""
+"Usage: history indicateur\n"
+" Donne l'historique pour un indicateur.\n"
+"\n"
+" Liste les entrées de journal pour le noeud identifié par\n"
+" l'indicateur.\n"
+" "
+
+#: ../roundup/admin.py:985
+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 ""
+"Usage: 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"
+"\n"
+" Les commandes \"one-off\" données en ligne de commande sont\n"
+" automatiquement validées si elles réussissent\n"
+" "
+
+#: ../roundup/admin.py:999
+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 ""
+"Usage: rollback\n"
+" Annule tout 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"
+" "
+
+#: ../roundup/admin.py:1011
+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 ""
+"Usage: retire indicateur[,indicateur]*\n"
+" Abandonne le noeud spécifié 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"
+" "
+
+#: ../roundup/admin.py:1034
+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 ""
+"Usage: restore indicateur[,indicateur]*\n"
+" Restaure le ou les noeuds abandonnés, spécifiés par le ou les\n"
+" indicateurs.\n"
+"\n"
+" Les noeuds spécifiés seront à nouveau acessibles aux utilisateurs.\n"
+" "
+
+#. grab the directory to export to
+#: ../roundup/admin.py:1056
+msgid ""
+"Usage: export [class[,class]] export_dir\n"
+" Export the database to colon-separated-value files.\n"
+"\n"
+" Optionally limit the export to just the names classes.\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 ""
+"Usage: 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"
+"\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"
+" format aux valeurs séparées par des doubles-points.\n"
+" "
+
+#: ../roundup/admin.py:1114
+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 ""
+"Usage: 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"
+"\n"
+" Les fichiers utilisés lors de l'importation sont:\n"
+"\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"
+" <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"
+" 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"
+" "
+
+#: ../roundup/admin.py:1186
+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 ""
+"Usage: pack période | date\n"
+"\n"
+" Efface les entrées de journaux antérieures à une période\n"
+" ou à 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"
+"\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"
+"\n"
+" Le format de date est \"AAAA-MM-JJ\", par ex.:\n"
+" 2001-01-01\n"
+"\n"
+" "
+
+#: ../roundup/admin.py:1214
+msgid "Invalid format"
+msgstr "Format invalide"
+
+#: ../roundup/admin.py:1224
+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 ""
+"Usage: 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"
+" "
+
+#: ../roundup/admin.py:1238
+#, python-format
+msgid "no such item \"%(designator)s\""
+msgstr ""
+
+#: ../roundup/admin.py:1248
+msgid ""
+"Usage: security [Role name]\n"
+" Display the Permissions available to one or all Roles.\n"
+" "
+msgstr ""
+"Usage: security [nom-de-rôle]\n"
+" Affiche les permissions disponible pour un ou plusieurs rôles.\n"
+" "
+
+#: ../roundup/admin.py:1256
+#, python-format
+msgid "No such Role \"%(role)s\""
+msgstr "Ce rôle \"%(role)s\" n'existe pas"
+
+#: ../roundup/admin.py:1262
+#, python-format
+msgid "New Web users get the Roles \"%(role)s\""
+msgstr "Les nouveaux utilisateurs Web ont les rôles \"%(role)s\""
+
+#: ../roundup/admin.py:1264
+#, python-format
+msgid "New Web users get the Role \"%(role)s\""
+msgstr "Les nouveaux utilisateurs Web ont le rôle \"%(role)s\""
+
+#: ../roundup/admin.py:1267
+#, python-format
+msgid "New Email users get the Roles \"%(role)s\""
+msgstr "Les nouveaux utilisateurs Email ont les rôles \"%(role)s\""
+
+#: ../roundup/admin.py:1269
+#, python-format
+msgid "New Email users get the Role \"%(role)s\""
+msgstr "Les nouveaux utilisateurs Email ont le rôle \"%(role)s\""
+
+#: ../roundup/admin.py:1272
+#, python-format
+msgid "Role \"%(name)s\":"
+msgstr "Rôle \"%(name)s\":"
+
+#: ../roundup/admin.py:1277
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
+msgstr ""
+
+#: ../roundup/admin.py:1280
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
+msgstr " %(description)s (%(name)s pour \"%(klass)s\" uniquement)"
+
+#: ../roundup/admin.py:1283
+#, python-format
+msgid " %(description)s (%(name)s)"
+msgstr " %(description)s (%(name)s)"
+
+#: ../roundup/admin.py:1312
+#, python-format
+msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
+msgstr "Commande inconnue \"%(command)s\" (\"help commands\" pour la liste)"
+
+#: ../roundup/admin.py:1318
+#, python-format
+msgid "Multiple commands match \"%(command)s\": %(list)s"
+msgstr "Plusieurs commandes correspondent à \"%(command)s\": %(list)s"
+
+#: ../roundup/admin.py:1325
+msgid "Enter tracker home: "
+msgstr "Entrez le répertoire de base du pisteur: "
+
+# ../roundup/admin.py:1332 :1338 :1358
+#: ../roundup/admin.py:1332 ../roundup/admin.py:1338 ../roundup/admin.py:1358
+#, python-format
+msgid "Error: %(message)s"
+msgstr "Erreur: %(message)s"
+
+#: ../roundup/admin.py:1346
+#, python-format
+msgid "Error: Couldn't open tracker: %(message)s"
+msgstr "Erreur: impossible d'ouvrir le pisteur: %(message)s"
+
+#: ../roundup/admin.py:1371
+#, python-format
+msgid ""
+"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/admin.py:1376
+msgid "Note: command history and editing not available"
+msgstr "Note: l'historique et l'édition des commandes n'est pas disponible"
+
+#: ../roundup/admin.py:1380
+msgid "roundup> "
+msgstr "roundup> "
+
+#: ../roundup/admin.py:1382
+msgid "exit..."
+msgstr "sortie..."
+
+#: ../roundup/admin.py:1392
+msgid "There are unsaved changes. Commit them (y/N)? "
+msgstr "Des changements n'ont pas été enregistrés. À valider ? (y/N)? "
+
+#: ../roundup/backends/back_anydbm.py:1997
+#, python-format
+msgid "WARNING: invalid date tuple %r"
+msgstr ""
+
+#: ../roundup/backends/rdbms_common.py:1434
+msgid "create"
+msgstr "créer"
+
+#: ../roundup/backends/rdbms_common.py:1600
+msgid "unlink"
+msgstr "détacher"
+
+#: ../roundup/backends/rdbms_common.py:1604
+msgid "link"
+msgstr "attacher"
+
+#: ../roundup/backends/rdbms_common.py:1724
+msgid "set"
+msgstr "assigner"
+
+#: ../roundup/backends/rdbms_common.py:1748
+msgid "retired"
+msgstr "abandonné"
+
+#: ../roundup/backends/rdbms_common.py:1778
+msgid "restored"
+msgstr "restauré"
+
+#: ../roundup/cgi/actions.py:58
+#, python-format
+msgid "You do not have permission to %(action)s the %(classname)s class."
+msgstr ""
+"Vous n'avez pas les permissions pour %(action)s la classe %(classname)s."
+
+#: ../roundup/cgi/actions.py:89
+msgid "No type specified"
+msgstr "Aucun type spécifié"
+
+#: ../roundup/cgi/actions.py:91
+msgid "No ID entered"
+msgstr "Aucun identifiant entré"
+
+#: ../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)"
+
+#: ../roundup/cgi/actions.py:117
+msgid "You may not retire the admin or anonymous user"
+msgstr "Vous ne pouvez pas abandonner les utilisateurs admin ou anonyme"
+
+#: ../roundup/cgi/actions.py:124
+#, python-format
+msgid "%(classname)s %(itemid)s has been retired"
+msgstr "%(classname)s %(itemid)s a été abandonné"
+
+# ../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 ""
+
+# ../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"
+
+#: ../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"
+
+#: ../roundup/cgi/actions.py:344
+msgid "Items edited OK"
+msgstr "Les éléments ont bien été modifié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)"
+
+#: ../roundup/cgi/actions.py:407
+#, python-format
+msgid "%(class)s %(id)s - nothing changed"
+msgstr "%(class)s %(id)s - aucun changement"
+
+#: ../roundup/cgi/actions.py:419
+#, python-format
+msgid "%(class)s %(id)s created"
+msgstr "%(class)s %(id)s créé"
+
+#: ../roundup/cgi/actions.py:451
+#, python-format
+msgid "You do not have permission to edit %(class)s"
+msgstr "Vous n'avez pas la permission de modifier %(class)s"
+
+#: ../roundup/cgi/actions.py:463
+#, python-format
+msgid "You do not have permission to create %(class)s"
+msgstr "Vous n'avez pas la permission de créer de %(class)s"
+
+#: ../roundup/cgi/actions.py:487
+msgid "You do not have permission to edit user roles"
+msgstr "Vous n'avez pas la permission de modifier les rôles d'un utilisateur"
+
+#: ../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 ""
+"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."
+
+#: ../roundup/cgi/actions.py:565
+#, python-format
+msgid "Edit Error: %s"
+msgstr "Erreur de modification: %s"
+
+# ../roundup/cgi/actions.py:596 :607 :778 :797
+#: ../roundup/cgi/actions.py:596 ../roundup/cgi/actions.py:607
+#: ../roundup/cgi/actions.py:778 ../roundup/cgi/actions.py:797
+#, python-format
+msgid "Error: %s"
+msgstr "Erreur: %s"
+
+#: ../roundup/cgi/actions.py:633
+msgid ""
+"Invalid One Time Key!\n"
+"(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)"
+
+#: ../roundup/cgi/actions.py:675
+#, python-format
+msgid "Password reset and email sent to %s"
+msgstr "Mot de passe réinitialisé et courriel envoyé à %s"
+
+#: ../roundup/cgi/actions.py:684
+msgid "Unknown username"
+msgstr "Nom d'utilisateur inconnu"
+
+#: ../roundup/cgi/actions.py:692
+msgid "Unknown email address"
+msgstr "Adresse courriel 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"
+
+#: ../roundup/cgi/actions.py:722
+#, python-format
+msgid "Email sent to %s"
+msgstr "Courriel envoyé à %s"
+
+#: ../roundup/cgi/actions.py:741
+msgid "You are now registered, welcome!"
+msgstr "Vous êtes désormais inscrit, bienvenue !"
+
+#: ../roundup/cgi/actions.py:786
+msgid "It is not permitted to supply roles at registration."
+msgstr ""
+
+#: ../roundup/cgi/actions.py:878
+msgid "You are logged out"
+msgstr "Vous êtes déconnecté"
+
+#: ../roundup/cgi/actions.py:895
+msgid "Username required"
+msgstr "Nom d'utilisateur requis"
+
+# ../roundup/cgi/actions.py:930 :934
+#: ../roundup/cgi/actions.py:930 ../roundup/cgi/actions.py:934
+msgid "Invalid login"
+msgstr "Tentative de connexion invalide"
+
+#: ../roundup/cgi/actions.py:940
+msgid "You do not have permission to login"
+msgstr "Vous n'avez la permission de vous connecter"
+
+#: ../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>Erreur de modèle</h1>\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>"
+
+#: ../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>"
+
+#: ../roundup/cgi/cgitb.py:71
+#, python-format
+msgid "<li>In %s</li>"
+msgstr "<li>Dans %s</li>"
+
+#: ../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\"."
+
+#: ../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>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"
+" %(globals)s\n"
+" %(locals)s\n"
+"</table></li>\n"
+
+#: ../roundup/cgi/cgitb.py:103
+msgid "Full traceback:"
+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>"
+
+#: ../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:"
+
+#: ../roundup/cgi/cgitb.py:129
+msgid "<file is None - probably inside <tt>eval</tt> or <tt>exec</tt>>"
+msgstr ""
+"<\"file\" est à \"None\" - probablement dans un <tt>eval</tt> ou un "
+"<tt>exec</tt>>"
+
+#: ../roundup/cgi/cgitb.py:138
+#, python-format
+msgid "in <strong>%s</strong>"
+msgstr "dans <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>non défini</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>Une erreur s'est produite</title></head>\n"
+"<body><h1>Une erreur s'est produite</h1>\n"
+"<p>Un problème a été rencontré lors du traitement de votre requête.\n"
+"Les administrateurs du pisteur ont été notifiés du problème.</p>\n"
+"</body></html>"
+
+#: ../roundup/cgi/client.py:308
+msgid "Form Error: "
+msgstr "Erreur de formulaire: "
+
+#: ../roundup/cgi/client.py:363
+#, python-format
+msgid "Unrecognized charset: %r"
+msgstr "Table de caractères non reconnue: %r"
+
+#: ../roundup/cgi/client.py:490
+msgid "Anonymous users are not allowed to use the web interface"
+msgstr ""
+"Les utilisateurs anonymes ne sont pas autorisés à utiliser l'interface Web"
+
+#: ../roundup/cgi/client.py:645
+msgid "You are not allowed to view this file."
+msgstr "Vous n'êtes pas autorisé à voir ce fichier"
+
+#: ../roundup/cgi/client.py:737
+#, python-format
+msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
+msgstr "%(starttag)sTemps écoulé: %(seconds)fs%(endtag)s\n"
+
+#: ../roundup/cgi/client.py:741
+#, 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)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"
+
+#: ../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"
+
+#: ../roundup/cgi/form_parser.py:290
+#, python-format
+msgid "%(class)s %(property)s is not a link or multilink property"
+msgstr "%(class)s %(property)s n'est pas une propriété lien ou lien multiple"
+
+#: ../roundup/cgi/form_parser.py:312
+#, python-format
+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"
+
+# ../roundup/cgi/form_parser.py:331 :357
+#: ../roundup/cgi/form_parser.py:331 ../roundup/cgi/form_parser.py:357
+#, python-format
+msgid "You have submitted more than one value for the %s property"
+msgstr "Vous avez fourni plus d'une valeur pour la propriété %s"
+
+# ../roundup/cgi/form_parser.py:354 :360
+#: ../roundup/cgi/form_parser.py:354 ../roundup/cgi/form_parser.py:360
+msgid "Password and confirmation text do not match"
+msgstr "Le mot de passe et le texte de confirmation ne correspondent pas"
+
+#: ../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"
+
+#: ../roundup/cgi/form_parser.py:512
+#, python-format
+msgid "Required %(class)s property %(property)s not supplied"
+msgid_plural "Required %(class)s properties %(property)s not supplied"
+msgstr[0] "La propriété requise %(property)s de %(class)s n'a pas été fournie"
+msgstr[1] ""
+"Les propriétés requises %(property)s de %(class)s n'ont pas été fournies"
+
+#: ../roundup/cgi/form_parser.py:535
+msgid "File is empty"
+msgstr "Le fichier est vide"
+
+#: ../roundup/cgi/templating.py:72
+#, python-format
+msgid "You are not allowed to %(action)s items of class %(class)s"
+msgstr "Vous ne pouvez pas %(action)s des éléments de classe %(class)s"
+
+#: ../roundup/cgi/templating.py:627
+msgid "(list)"
+msgstr "(liste)"
+
+#: ../roundup/cgi/templating.py:696
+msgid "Submit New Entry"
+msgstr "Soumettre un nouvelle entrée"
+
+# ../roundup/cgi/templating.py:710 :829 :1236 :1257 :1304 :1327 :1361 :1400
+# :1453 :1470 :1549 :1569 :1587 :1619 :1629 :1683 :1875
+#: ../roundup/cgi/templating.py:710 ../roundup/cgi/templating.py:829
+#: ../roundup/cgi/templating.py:1236 ../roundup/cgi/templating.py:1257
+#: ../roundup/cgi/templating.py:1304 ../roundup/cgi/templating.py:1327
+#: ../roundup/cgi/templating.py:1361 ../roundup/cgi/templating.py:1400
+#: ../roundup/cgi/templating.py:1453 ../roundup/cgi/templating.py:1470
+#: ../roundup/cgi/templating.py:1549 ../roundup/cgi/templating.py:1569
+#: ../roundup/cgi/templating.py:1587 ../roundup/cgi/templating.py:1619
+#: ../roundup/cgi/templating.py:1629 ../roundup/cgi/templating.py:1683
+#: ../roundup/cgi/templating.py:1875
+msgid "[hidden]"
+msgstr "[masqué]"
+
+#: ../roundup/cgi/templating.py:711
+msgid "New node - no history"
+msgstr "Nouveau noeud - pas d'historique"
+
+#: ../roundup/cgi/templating.py:811
+msgid "Submit Changes"
+msgstr "Soumettre les changements"
+
+#: ../roundup/cgi/templating.py:893
+msgid "<em>The indicated property no longer exists</em>"
+msgstr "<em>La propriété indiquée n'existe plus</em>"
+
+#: ../roundup/cgi/templating.py:894
+#, python-format
+msgid "<em>%s: %s</em>\n"
+msgstr "<em>%s: %s</em>\n"
+
+#: ../roundup/cgi/templating.py:907
+#, python-format
+msgid "The linked class %(classname)s no longer exists"
+msgstr "La classe liée %(classname)s n'existe plus"
+
+# ../roundup/cgi/templating.py:940 :964
+#: ../roundup/cgi/templating.py:940 ../roundup/cgi/templating.py:964
+msgid "<strike>The linked node no longer exists</strike>"
+msgstr "<strike>Le noeud lié n'existe plus</strike>"
+
+# ../roundup/cgi/templating.py:1006 :1404 :1425 :1431
+#: ../roundup/cgi/templating.py:1006 ../roundup/cgi/templating.py:1404
+#: ../roundup/cgi/templating.py:1425 ../roundup/cgi/templating.py:1431
+msgid "No"
+msgstr "Non"
+
+# ../roundup/cgi/templating.py:1006 :1404 :1423 :1428
+#: ../roundup/cgi/templating.py:1006 ../roundup/cgi/templating.py:1404
+#: ../roundup/cgi/templating.py:1423 ../roundup/cgi/templating.py:1428
+msgid "Yes"
+msgstr "Oui"
+
+#: ../roundup/cgi/templating.py:1017
+#, python-format
+msgid "%s: (no value)"
+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>"
+
+#: ../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>"
+
+#: ../roundup/cgi/templating.py:1050
+msgid "History"
+msgstr "Historique"
+
+#: ../roundup/cgi/templating.py:1052
+msgid "<th>Date</th>"
+msgstr "<th>Date</th>"
+
+#: ../roundup/cgi/templating.py:1053
+msgid "<th>User</th>"
+msgstr "<th>Utilisateur</th>"
+
+#: ../roundup/cgi/templating.py:1054
+msgid "<th>Action</th>"
+msgstr "<th>Action</th>"
+
+#: ../roundup/cgi/templating.py:1055
+msgid "<th>Args</th>"
+msgstr "<th>Arguments</th>"
+
+#: ../roundup/cgi/templating.py:1097
+#, python-format
+msgid "Copy of %(class)s %(id)s"
+msgstr "Copie de %(class)s %(id)s"
+
+#: ../roundup/cgi/templating.py:1331
+msgid "*encrypted*"
+msgstr "*encrypté*"
+
+#: ../roundup/cgi/templating.py:1514
+msgid ""
+"default value for DateHTMLProperty must be either DateHTMLProperty or string "
+"date representation."
+msgstr ""
+"la valeur par défaut pour DateHTMLProperty doit être soit DateHTMLProperty "
+"soit une représentation textuelle de la date."
+
+#: ../roundup/cgi/templating.py:1674
+#, python-format
+msgid "Attempt to look up %(attr)s on a missing value"
+msgstr "Tentative de recherche de %(attr)s sur une valeur manquante"
+
+#: ../roundup/cgi/templating.py:1750
+#, python-format
+msgid "<option %svalue=\"-1\">- no selection -</option>"
+msgstr "<option %svalue=\"-1\">- pas de sélection -</option>"
+
+#: ../roundup/date.py:186
+msgid ""
+"Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or \"yyyy-"
+"mm-dd.HH:MM:SS.SSS\""
+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\""
+
+#: ../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]"
+
+#: ../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]"
+
+#: ../roundup/date.py:694
+#, python-format
+msgid "%(number)s year"
+msgid_plural "%(number)s years"
+msgstr[0] "%(number)s année"
+msgstr[1] "%(number)s années"
+
+#: ../roundup/date.py:698
+#, python-format
+msgid "%(number)s month"
+msgid_plural "%(number)s months"
+msgstr[0] "%(number)s mois"
+msgstr[1] "%(number)s mois"
+
+#: ../roundup/date.py:702
+#, python-format
+msgid "%(number)s week"
+msgid_plural "%(number)s weeks"
+msgstr[0] "%(number)s semaine"
+msgstr[1] "%(number)s semaines"
+
+#: ../roundup/date.py:706
+#, python-format
+msgid "%(number)s day"
+msgid_plural "%(number)s days"
+msgstr[0] "%(number)s jour"
+msgstr[1] "%(number)s jours"
+
+#: ../roundup/date.py:710
+msgid "tomorrow"
+msgstr "demain"
+
+#: ../roundup/date.py:712
+msgid "yesterday"
+msgstr "hier"
+
+#: ../roundup/date.py:715
+#, python-format
+msgid "%(number)s hour"
+msgid_plural "%(number)s hours"
+msgstr[0] "%(number)s heure"
+msgstr[1] "%(number)s heures"
+
+#: ../roundup/date.py:719
+msgid "an hour"
+msgstr "une heure"
+
+#: ../roundup/date.py:721
+msgid "1 1/2 hours"
+msgstr "1 heure et demie"
+
+#: ../roundup/date.py:723
+#, python-format
+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"
+
+#: ../roundup/date.py:727
+msgid "in a moment"
+msgstr "dans un instant"
+
+#: ../roundup/date.py:729
+msgid "just now"
+msgstr "à l'instant"
+
+#: ../roundup/date.py:732
+msgid "1 minute"
+msgstr "une minute"
+
+#: ../roundup/date.py:735
+#, python-format
+msgid "%(number)s minute"
+msgid_plural "%(number)s minutes"
+msgstr[0] "%(number)s minute"
+msgstr[1] "%(number)s minutes"
+
+#: ../roundup/date.py:738
+msgid "1/2 an hour"
+msgstr "une demi-heure"
+
+#: ../roundup/date.py:740
+#, python-format
+msgid "%(number)s/4 hour"
+msgid_plural "%(number)s/4 hours"
+msgstr[0] "un quart d'heure"
+msgstr[1] "trois quarts d'heure"
+
+#: ../roundup/date.py:744
+#, python-format
+msgid "%s ago"
+msgstr "il y a %s"
+
+#: ../roundup/date.py:746
+#, python-format
+msgid "in %s"
+msgstr "dans %s"
+
+#: ../roundup/init.py:134
+#, python-format
+msgid ""
+"WARNING: directory '%s'\n"
+"\tcontains old-style template - ignored"
+msgstr ""
+"ATTENTION: le répertoire '%s'\n"
+"\tcontient des modèles obsolètes - ignoré"
+
+#: ../roundup/roundupdb.py:141
+msgid "files"
+msgstr "fichiers"
+
+#: ../roundup/roundupdb.py:141
+msgid "messages"
+msgstr "messages"
+
+#: ../roundup/roundupdb.py:141
+msgid "nosy"
+msgstr "curieux"
+
+#: ../roundup/roundupdb.py:141
+msgid "superseder"
+msgstr "remplaçant"
+
+#: ../roundup/roundupdb.py:141
+msgid "title"
+msgstr "titre"
+
+#: ../roundup/roundupdb.py:142
+msgid "assignedto"
+msgstr "assigné_à"
+
+#: ../roundup/roundupdb.py:142
+msgid "priority"
+msgstr "priorité"
+
+#: ../roundup/roundupdb.py:142
+msgid "status"
+msgstr "état"
+
+#: ../roundup/roundupdb.py:142
+msgid "topic"
+msgstr "sujet"
+
+#: ../roundup/roundupdb.py:145
+msgid "activity"
+msgstr "activité"
+
+#. following properties are common for all hyperdb classes
+#. they are listed here to keep things in one place
+#: ../roundup/roundupdb.py:145
+msgid "actor"
+msgstr "acteur"
+
+#: ../roundup/roundupdb.py:145
+msgid "creation"
+msgstr "création"
+
+#: ../roundup/roundupdb.py:145
+msgid "creator"
+msgstr "créateur"
+
+#: ../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]: "
+
+#: ../roundup/scripts/roundup_gettext.py:22
+#, python-format
+msgid "Usage: %(program)s <tracker home>"
+msgstr "Usage: %(program)s <répertoire du pisteur>"
+
+#: ../roundup/scripts/roundup_gettext.py:37
+#, python-format
+msgid "No tracker templates found in directory %s"
+msgstr "Aucun modèle de pisteur dans le répertoire %s"
+
+#: ../roundup/scripts/roundup_mailgw.py:36
+#, python-format
+msgid ""
+"Usage: %(program)s [-v] [-c] [[-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"
+"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 ""
+"Usage: %(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"
+"\n"
+"La passerelle de messagerie de Roundup peut être appelée de quatre "
+"manières:\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 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"
+"\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"
+" 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"
+" mailbox /chemin/vers/mailbox\n"
+"\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"
+" pop nom-d'utilisateur:mot-de-passe at serveur\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 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 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"
+" imap nom-d'utilisateur:mot-de-passe at serveur boîte-aux-lettres\n"
+"\n"
+"IMAPS:\n"
+" Se connecte avec SSL à un serveur IMAP.\n"
+" Supporte 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"
+
+#: ../roundup/scripts/roundup_mailgw.py:163
+msgid "Error: pop specification not valid"
+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"
+
+#: ../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\""
+
+#: ../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>Index des pisteurs Roundup</title></head>\n"
+"<body><h1>Index des pisteurs Roundup</h1><ol>\n"
+
+#: ../roundup/scripts/roundup_server.py:287
+#, python-format
+msgid "Error: %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\")"
+
+#: ../roundup/scripts/roundup_server.py:303
+msgid "Can't change groups - no grp module"
+msgstr "Impossible de changer les groupes - le module grp n'est pas présent"
+
+#: ../roundup/scripts/roundup_server.py:312
+#, python-format
+msgid "Group %(group)s doesn't exist"
+msgstr "Le groupe %(group)s n'existe pas"
+
+#: ../roundup/scripts/roundup_server.py:323
+msgid "Can't run as root!"
+msgstr "Impossible d'exécuter en tant que superutilisateur (\"root\")"
+
+#: ../roundup/scripts/roundup_server.py:326
+msgid "WARNING: ignoring \"-u\" argument, not root"
+msgstr ""
+"ATTENTION: le paramètre \"-u\" est ignoré, vous n'êtes pas superutilisateur "
+"(\"root\")"
+
+#: ../roundup/scripts/roundup_server.py:331
+msgid "Can't change users - no pwd module"
+msgstr ""
+"Impossible de changer les utilisateurs - le module pwd n'est pas présent"
+
+#: ../roundup/scripts/roundup_server.py:340
+#, python-format
+msgid "User %(user)s doesn't exist"
+msgstr "L'utilisateur %(user)s n'existe pas"
+
+#: ../roundup/scripts/roundup_server.py:471
+#, python-format
+msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
+msgstr ""
+"Le mode multiprocessus \"%s\" n'existe pas, passage en mode processus unique"
+
+#: ../roundup/scripts/roundup_server.py:494
+#, python-format
+msgid "Unable to bind to port %s, port already in use."
+msgstr "Impossible de s'attacher au port %s, le port est déjà utilisé"
+
+#: ../roundup/scripts/roundup_server.py:562
+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 ""
+" -c <Commande> \n"
+" Options des services Windows.\n"
+" Si vous désirez démarrer le serveur comme service Windows,\n"
+" vous devez utiliser le fichier de configuration pour\n"
+" 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 "
+" spécificités du service Windows."
+
+#: ../roundup/scripts/roundup_server.py:569
+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 ""
+" -u <UID> démarre le serveur Web de Roundup sous l'identificateur\n"
+" d'utilisateur UID\"\n"
+" -g <GID> démarre le serveur Web de Roundup sous l'identificateur\n"
+" de groupe GID\n"
+" -d <fichier-PID>\n"
+" démarre le serveur en tâche de fond et écrit "
+"l'identificateur\n"
+" de processus (\"PID\") dans le fichier spécifié par fichier-"
+"PID\n"
+" L'option -l option *doit* être spécifiée si -d est utilisé."
+
+#: ../roundup/scripts/roundup_server.py:576
+#, 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:724
+msgid "Instances must be name=home"
+msgstr "Les instances doivent être nom=base-du-pisteur"
+
+#: ../roundup/scripts/roundup_server.py:738
+#, python-format
+msgid "Configuration saved to %s"
+msgstr "Configuration sauvegardée dans %s"
+
+#: ../roundup/scripts/roundup_server.py:756
+msgid "Sorry, you can't run the server as a daemon on this Operating System"
+msgstr ""
+"Désolé, vous ne pouvez pas démarrer le serveur en tâche de fond avec ce "
+"système d'exploitation"
+
+#: ../roundup/scripts/roundup_server.py:768
+#, python-format
+msgid "Roundup server started on %(HOST)s:%(PORT)s"
+msgstr "Le serveur Roundup est démarré sur %(HOST)s:%(PORT)s"
+
+#: ../templates/classic/html/_generic.collision.html:4
+#: ../templates/minimal/html/_generic.collision.html:4
+msgid "${class} Edit Collision - ${tracker}"
+msgstr "Édition des collisions pour ${class} - ${tracker}"
+
+#: ../templates/classic/html/_generic.collision.html:7
+#: ../templates/minimal/html/_generic.collision.html:7
+msgid "${class} Edit Collision"
+msgstr "Édition des collisions pour ${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"
+" Une collision s'est produite. Un autre utilisateur a mis à jour ce\n"
+" noeud pendant que vous l'éditiez. Veuillez <a "
+"href='${context}'>recharger</a>\n"
+" ce noeud et vé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}"
+
+#: ../templates/classic/html/_generic.help.html:31
+#: ../templates/minimal/html/_generic.help.html:31
+msgid " Cancel "
+msgstr " Annuler "
+
+#: ../templates/classic/html/_generic.help.html:34
+#: ../templates/minimal/html/_generic.help.html:34
+msgid " Apply "
+msgstr " Appliquer "
+
+#: ../templates/classic/html/_generic.help.html:41
+#: ../templates/classic/html/issue.index.html:73
+#: ../templates/minimal/html/_generic.help.html:41
+msgid "<< previous"
+msgstr "<< précédents"
+
+#: ../templates/classic/html/_generic.help.html:52
+#: ../templates/classic/html/issue.index.html:81
+#: ../templates/minimal/html/_generic.help.html:52
+msgid "${start}..${end} out of ${total}"
+msgstr "${start}..${end} sur ${total}"
+
+#: ../templates/classic/html/_generic.help.html:56
+#: ../templates/classic/html/issue.index.html:84
+#: ../templates/minimal/html/_generic.help.html:56
+msgid "next >>"
+msgstr "suivants >>"
+
+#: ../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 "Édition 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 "Édition de ${class}"
+
+#: ../templates/classic/html/_generic.index.html:14
+#: ../templates/classic/html/_generic.item.html:12
+#: ../templates/classic/html/file.item.html:9
+#: ../templates/classic/html/issue.index.html:16
+#: ../templates/classic/html/issue.item.html:28
+#: ../templates/classic/html/msg.item.html:26
+#: ../templates/classic/html/user.index.html:9
+#: ../templates/classic/html/user.item.html:28
+#: ../templates/minimal/html/_generic.index.html:14
+#: ../templates/minimal/html/_generic.item.html:12
+#: ../templates/minimal/html/user.index.html:9
+#: ../templates/minimal/html/user.item.html:28
+#: ../templates/minimal/html/user.register.html:14
+msgid "You are not allowed to view this page."
+msgstr "Vous n'êtes pas autorisé à voir cette page."
+
+#: ../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\"> Vous pouvez éditer le contenu de la classe "
+"${classname} en utilisant ce formulaire. Les virgules, passages à la "
+"ligne guillemets doubles (\") doivent être gérés "
+"soigneusement. Vous pouvez insérer des virgules et des passage "
+"à la ligne en insérant les valeurs dans des guillemets doubles "
+"(\"). Les guillemets doubles elles-mêmes doivent être insé"
+"rées en les doublant (\"\").</p><p class=\"form-help\">Les "
+"propriétés des liens multiples doivent séparerleurs "
+"valeurs multiples par des double-points (\":\") (... ,\"un:deux:trois"
+"\", ...) </p><p class=\"form-help\"> Enlevez des entrées en "
+"effaçant leur ligne. Ajoutez de nouvelles entrées en les "
+"ajoutant à 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
+msgid "Edit Items"
+msgstr "Modifier des éléments"
+
+#: ../templates/classic/html/file.index.html:4
+msgid "List of files - ${tracker}"
+msgstr "Liste des fichiers - ${tracker}s"
+
+#: ../templates/classic/html/file.index.html:5
+msgid "List of files"
+msgstr "Liste des fichiers"
+
+#: ../templates/classic/html/file.index.html:10
+msgid "Download"
+msgstr "Télécharger"
+
+#: ../templates/classic/html/file.index.html:11
+#: ../templates/classic/html/file.item.html:22
+msgid "Content Type"
+msgstr "Type de contenu"
+
+#: ../templates/classic/html/file.index.html:12
+msgid "Uploaded By"
+msgstr "Téléchargé par"
+
+#: ../templates/classic/html/file.index.html:13
+#: ../templates/classic/html/msg.item.html:43
+msgid "Date"
+msgstr "Date"
+
+#: ../templates/classic/html/file.item.html:2
+msgid "File display - ${tracker}"
+msgstr "Affichage de fichier - ${tracker}"
+
+#: ../templates/classic/html/file.item.html:4
+msgid "File display"
+msgstr "Affichage de fichier"
+
+#: ../templates/classic/html/file.item.html:18
+#: ../templates/classic/html/user.item.html:39
+#: ../templates/classic/html/user.register.html:17
+msgid "Name"
+msgstr "Nom"
+
+#: ../templates/classic/html/file.item.html:40
+msgid "download"
+msgstr "téléchargement"
+
+#: ../templates/classic/html/home.classlist.html:2
+#: ../templates/minimal/html/home.classlist.html:2
+msgid "List of classes - ${tracker}"
+msgstr "Liste des classes - ${tracker}"
+
+#: ../templates/classic/html/home.classlist.html:4
+#: ../templates/minimal/html/home.classlist.html:4
+msgid "List of classes"
+msgstr "Liste des classes"
+
+#: ../templates/classic/html/issue.index.html:7
+msgid "List of issues - ${tracker}"
+msgstr "Liste des demandes - ${tracker}"
+
+#: ../templates/classic/html/issue.index.html:11
+msgid "List of issues"
+msgstr "Liste des demandes"
+
+#: ../templates/classic/html/issue.index.html:22
+#: ../templates/classic/html/issue.item.html:44
+msgid "Priority"
+msgstr "Priorité"
+
+#: ../templates/classic/html/issue.index.html:23
+msgid "ID"
+msgstr "ID"
+
+#: ../templates/classic/html/issue.index.html:24
+msgid "Creation"
+msgstr "Création"
+
+#: ../templates/classic/html/issue.index.html:25
+msgid "Activity"
+msgstr "Activité"
+
+#: ../templates/classic/html/issue.index.html:26
+msgid "Actor"
+msgstr "Acteur"
+
+#: ../templates/classic/html/issue.index.html:27
+msgid "Topic"
+msgstr "Sujet"
+
+#: ../templates/classic/html/issue.index.html:28
+#: ../templates/classic/html/issue.item.html:39
+msgid "Title"
+msgstr "Titre"
+
+#: ../templates/classic/html/issue.index.html:29
+#: ../templates/classic/html/issue.item.html:46
+msgid "Status"
+msgstr "État"
+
+#: ../templates/classic/html/issue.index.html:30
+msgid "Creator"
+msgstr "Créateur"
+
+#: ../templates/classic/html/issue.index.html:31
+msgid "Assigned To"
+msgstr "Assigné à"
+
+#: ../templates/classic/html/issue.index.html:97
+msgid "Download as CSV"
+msgstr "Télécharger comme CSV"
+
+#: ../templates/classic/html/issue.index.html:105
+msgid "Sort on:"
+msgstr "Trier par:"
+
+#: ../templates/classic/html/issue.index.html:108
+#: ../templates/classic/html/issue.index.html:125
+msgid "- nothing -"
+msgstr "- rien -"
+
+#: ../templates/classic/html/issue.index.html:116
+#: ../templates/classic/html/issue.index.html:133
+msgid "Descending:"
+msgstr "Descendant:"
+
+#: ../templates/classic/html/issue.index.html:122
+msgid "Group on:"
+msgstr "Grouper par:"
+
+#: ../templates/classic/html/issue.index.html:139
+msgid "Redisplay"
+msgstr "Ré-afficher"
+
+#: ../templates/classic/html/issue.item.html:7
+msgid "Issue ${id}: ${title} - ${tracker}"
+msgstr "Demande ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:10
+msgid "New Issue - ${tracker}"
+msgstr "Nouvelle demande - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:14
+msgid "New Issue"
+msgstr "Nouvelle demande"
+
+#: ../templates/classic/html/issue.item.html:16
+msgid "New Issue Editing"
+msgstr "Édition d'une nouvelle demande"
+
+#: ../templates/classic/html/issue.item.html:19
+msgid "Issue${id}"
+msgstr "Issue${id}"
+
+#: ../templates/classic/html/issue.item.html:22
+msgid "Issue${id} Editing"
+msgstr "Édition de la demande Issue${id}"
+
+#: ../templates/classic/html/issue.item.html:51
+msgid "Superseder"
+msgstr "Supplanté par"
+
+#: ../templates/classic/html/issue.item.html:56
+msgid "View: ${link}"
+msgstr "Voir: ${link}"
+
+#: ../templates/classic/html/issue.item.html:60
+msgid "Nosy List"
+msgstr "Liste des curieux"
+
+#: ../templates/classic/html/issue.item.html:69
+msgid "Assigned To"
+msgstr "Assigné à"
+
+#: ../templates/classic/html/issue.item.html:71
+msgid "Topics"
+msgstr "Sujets"
+
+#: ../templates/classic/html/issue.item.html:79
+msgid "Change Note"
+msgstr "Note de modification"
+
+#: ../templates/classic/html/issue.item.html:87
+msgid "File"
+msgstr "Fichier"
+
+#: ../templates/classic/html/issue.item.html:99
+msgid "Make a copy"
+msgstr "Réaliser une copie"
+
+#: ../templates/classic/html/issue.item.html:107
+#: ../templates/classic/html/user.item.html:106
+#: ../templates/classic/html/user.register.html:69
+#: ../templates/minimal/html/user.item.html:86
+msgid ""
+"<table class=\"form\"> <tr> <td>Note: </td> <th class=\"required"
+"\">highlighted</th> <td> fields are required.</td> </tr> </table>"
+msgstr ""
+"<table class=\"form\"> <tr> <td>Note: Les champs </td> <th class="
+"\"required\">mis en évidence</th> <td> sont requis.</td> </tr> </"
+"table>"
+
+#: ../templates/classic/html/issue.item.html:121
+msgid ""
+"Created on <b>${creation}</b> by <b>${creator}</b>, last changed <b>"
+"${activity}</b> by <b>${actor}</b>."
+msgstr ""
+"Créé le <b>${creation}</b> par <b>${creator}</b>, "
+"modifié le <b>${activity}</b> par <b>${actor}</b>."
+
+#: ../templates/classic/html/issue.item.html:125
+#: ../templates/classic/html/msg.item.html:56
+msgid "Files"
+msgstr "Fichiers"
+
+#: ../templates/classic/html/issue.item.html:127
+#: ../templates/classic/html/msg.item.html:58
+msgid "File name"
+msgstr "Nom de fichier"
+
+#: ../templates/classic/html/issue.item.html:128
+#: ../templates/classic/html/msg.item.html:59
+msgid "Uploaded"
+msgstr "Téléchargé"
+
+#: ../templates/classic/html/issue.item.html:129
+msgid "Type"
+msgstr "Type"
+
+#: ../templates/classic/html/issue.item.html:130
+#: ../templates/classic/html/query.edit.html:30
+msgid "Edit"
+msgstr "Modifier"
+
+#: ../templates/classic/html/issue.item.html:131
+msgid "Remove"
+msgstr "Effacer"
+
+#: ../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"
+
+#: ../templates/classic/html/issue.item.html:158
+#: ../templates/classic/html/msg.index.html:9
+msgid "Messages"
+msgstr "Messages"
+
+#: ../templates/classic/html/issue.item.html:162
+msgid "msg${id} (view)"
+msgstr "msg${id} (voir)"
+
+#: ../templates/classic/html/issue.item.html:163
+msgid "Author: ${author}"
+msgstr "Auteur: ${author}"
+
+#: ../templates/classic/html/issue.item.html:165
+msgid "Date: ${date}"
+msgstr "Date: ${date}"
+
+#: ../templates/classic/html/issue.search.html:2
+msgid "Issue searching - ${tracker}"
+msgstr "Recherche de demande - ${tracker}"
+
+#: ../templates/classic/html/issue.search.html:4
+msgid "Issue searching"
+msgstr "Recherche de demande"
+
+#: ../templates/classic/html/issue.search.html:25
+msgid "Filter on"
+msgstr "Filter sur"
+
+#: ../templates/classic/html/issue.search.html:26
+msgid "Display"
+msgstr "Afficher"
+
+#: ../templates/classic/html/issue.search.html:27
+msgid "Sort on"
+msgstr "Trier par"
+
+#: ../templates/classic/html/issue.search.html:28
+msgid "Group on"
+msgstr "Grouper par"
+
+#: ../templates/classic/html/issue.search.html:32
+msgid "All text*:"
+msgstr "Tout le texte*:"
+
+#: ../templates/classic/html/issue.search.html:40
+msgid "Title:"
+msgstr "Titre:"
+
+#: ../templates/classic/html/issue.search.html:50
+msgid "Topic:"
+msgstr "Sujet:"
+
+#: ../templates/classic/html/issue.search.html:58
+msgid "ID:"
+msgstr "ID:"
+
+#: ../templates/classic/html/issue.search.html:66
+msgid "Creation Date:"
+msgstr "Date de création:"
+
+#: ../templates/classic/html/issue.search.html:77
+msgid "Creator:"
+msgstr "Créateur:"
+
+#: ../templates/classic/html/issue.search.html:79
+msgid "created by me"
+msgstr "créé par moi"
+
+#: ../templates/classic/html/issue.search.html:88
+msgid "Activity:"
+msgstr "Activité:"
+
+#: ../templates/classic/html/issue.search.html:99
+msgid "Actor:"
+msgstr "Acteur:"
+
+#: ../templates/classic/html/issue.search.html:101
+msgid "done by me"
+msgstr "fait par moi"
+
+#: ../templates/classic/html/issue.search.html:112
+msgid "Priority:"
+msgstr "Priorité:"
+
+#: ../templates/classic/html/issue.search.html:114
+#: ../templates/classic/html/issue.search.html:130
+msgid "not selected"
+msgstr "non sélectionné"
+
+#: ../templates/classic/html/issue.search.html:125
+msgid "Status:"
+msgstr "État:"
+
+#: ../templates/classic/html/issue.search.html:128
+msgid "not resolved"
+msgstr "non résolu"
+
+#: ../templates/classic/html/issue.search.html:143
+msgid "Assigned to:"
+msgstr "Assigné à:"
+
+#: ../templates/classic/html/issue.search.html:146
+msgid "assigned to me"
+msgstr "assigné à moi"
+
+#: ../templates/classic/html/issue.search.html:148
+msgid "unassigned"
+msgstr "non assigné"
+
+#: ../templates/classic/html/issue.search.html:158
+msgid "No Sort or group:"
+msgstr "Aucun tri ou groupe:"
+
+#: ../templates/classic/html/issue.search.html:166
+msgid "Pagesize:"
+msgstr "Taille de la page:"
+
+#: ../templates/classic/html/issue.search.html:172
+msgid "Start With:"
+msgstr "Commence par"
+
+#: ../templates/classic/html/issue.search.html:178
+msgid "Sort Descending:"
+msgstr "Tri descendant:"
+
+#: ../templates/classic/html/issue.search.html:185
+msgid "Group Descending:"
+msgstr "Groupage descendant:"
+
+#: ../templates/classic/html/issue.search.html:192
+msgid "Query name**:"
+msgstr "Nom de requête**:"
+
+#: ../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
+msgid "Search"
+msgstr "Rechercher"
+
+#: ../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"
+
+#: ../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ête sera "
+"sauvegardée et disponible comme lien dans la barre latérale"
+
+#: ../templates/classic/html/keyword.item.html:3
+msgid "Keyword editing - ${tracker}"
+msgstr "Édition de mots-clé - ${tracker}"
+
+#: ../templates/classic/html/keyword.item.html:5
+msgid "Keyword editing"
+msgstr "Édition de mots-clé"
+
+#: ../templates/classic/html/keyword.item.html:11
+msgid "Existing Keywords"
+msgstr "Mots-clé existants"
+
+#: ../templates/classic/html/keyword.item.html:20
+msgid ""
+"To edit an existing keyword (for spelling or typing errors), click on its "
+"entry above."
+msgstr ""
+"Pour modifier un mot-clé existant (pour les erreurs d'orthographe et "
+"de frappe), cliquez sur son entrée ci-dessus."
+
+#: ../templates/classic/html/keyword.item.html:27
+msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
+msgstr ""
+"Pour créer un nouveau mot-clé, entrez-le ci-dessous et cliquer "
+"\"Soumettre une nouvelle entrée\"."
+
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
+msgstr "Mot-clé"
+
+#: ../templates/classic/html/msg.index.html:3
+msgid "List of messages - ${tracker}"
+msgstr "Liste de messages - ${tracker}"
+
+#: ../templates/classic/html/msg.index.html:5
+msgid "Message listing"
+msgstr "Liste de messages"
+
+#: ../templates/classic/html/msg.item.html:6
+msgid "Message ${id} - ${tracker}"
+msgstr "Message ${id} - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:9
+msgid "New Message - ${tracker}"
+msgstr "Nouveau message - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:13
+msgid "New Message"
+msgstr "Nouveau message"
+
+#: ../templates/classic/html/msg.item.html:15
+msgid "New Message Editing"
+msgstr "Édition d'un nouveau message"
+
+#: ../templates/classic/html/msg.item.html:18
+msgid "Message${id}"
+msgstr "Message${id}"
+
+#: ../templates/classic/html/msg.item.html:21
+msgid "Message${id} Editing"
+msgstr "Édition de Message${id}"
+
+#: ../templates/classic/html/msg.item.html:33
+msgid "Author"
+msgstr "Auteur"
+
+#: ../templates/classic/html/msg.item.html:38
+msgid "Recipients"
+msgstr "Destinataires"
+
+#: ../templates/classic/html/msg.item.html:49
+msgid "Content"
+msgstr "Contenu"
+
+#: ../templates/classic/html/page.html:41
+msgid "<b>Your Queries</b> (<a href=\"query?@template=edit\">edit</a>)"
+msgstr ""
+"<b>Vos requêtes</b> (<a href=\"query?@template=edit\">modifier</a>)"
+
+#: ../templates/classic/html/page.html:52
+msgid "Issues"
+msgstr "Demandes"
+
+#: ../templates/classic/html/page.html:54
+#: ../templates/classic/html/page.html:74
+msgid "Create New"
+msgstr "Créer"
+
+#: ../templates/classic/html/page.html:56
+msgid "Show Unassigned"
+msgstr "Montrer les non-assignés"
+
+#: ../templates/classic/html/page.html:58
+msgid "Show All"
+msgstr "Montrer tout"
+
+#: ../templates/classic/html/page.html:61
+msgid "Show issue:"
+msgstr "Montrer la demande:"
+
+#: ../templates/classic/html/page.html:72
+msgid "Keywords"
+msgstr "Mots-clé"
+
+#: ../templates/classic/html/page.html:78
+msgid "Edit Existing"
+msgstr "Modifier"
+
+#: ../templates/classic/html/page.html:84
+#: ../templates/minimal/html/page.html:65
+msgid "Administration"
+msgstr "Administration"
+
+#: ../templates/classic/html/page.html:86
+#: ../templates/minimal/html/page.html:66
+msgid "Class List"
+msgstr "Liste des classes"
+
+#: ../templates/classic/html/page.html:90
+#: ../templates/minimal/html/page.html:68
+msgid "User List"
+msgstr "Liste des utilisateurs"
+
+#: ../templates/classic/html/page.html:92
+#: ../templates/minimal/html/page.html:71
+msgid "Add User"
+msgstr "Ajouter un utilisateur"
+
+#: ../templates/classic/html/page.html:99
+#: ../templates/classic/html/page.html:105
+#: ../templates/minimal/html/page.html:46
+msgid "Login"
+msgstr "Se connecter"
+
+#: ../templates/classic/html/page.html:104
+#: ../templates/minimal/html/page.html:45
+msgid "Remember me?"
+msgstr "Se souvenir de moi ?"
+
+#: ../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"
+
+#: ../templates/classic/html/page.html:111
+msgid "Lost your login?"
+msgstr "Perdu votre login ?"
+
+#: ../templates/classic/html/page.html:116
+msgid "Hello, ${user}"
+msgstr "Bienvenue, ${user}"
+
+#: ../templates/classic/html/page.html:118
+msgid "Your Issues"
+msgstr "Vos demandes"
+
+#: ../templates/classic/html/page.html:119
+#: ../templates/minimal/html/page.html:57
+msgid "Your Details"
+msgstr "Vos détails"
+
+#: ../templates/classic/html/page.html:121
+#: ../templates/minimal/html/page.html:59
+msgid "Logout"
+msgstr "Se déconnecter"
+
+#: ../templates/classic/html/page.html:125
+msgid "Help"
+msgstr "Aide"
+
+#: ../templates/classic/html/page.html:126
+msgid "Roundup docs"
+msgstr "Documentation de Roundup"
+
+#: ../templates/classic/html/page.html:136
+#: ../templates/minimal/html/page.html:81
+msgid "clear this message"
+msgstr "Supprimer ce message"
+
+#: ../templates/classic/html/page.html:181
+msgid "don't care"
+msgstr "aucune importance"
+
+#: ../templates/classic/html/page.html:183
+msgid "------------"
+msgstr ""
+
+#: ../templates/classic/html/page.html:210
+msgid "no value"
+msgstr "pas de valeur"
+
+#: ../templates/classic/html/query.edit.html:4
+msgid "\"Your Queries\" Editing - ${tracker}"
+msgstr "Édition de \"Vos requêtes\" - ${tracker}"
+
+#: ../templates/classic/html/query.edit.html:6
+msgid "\"Your Queries\" Editing"
+msgstr "Édition de \"Vos requêtes\""
+
+#: ../templates/classic/html/query.edit.html:11
+msgid "You are not allowed to edit queries."
+msgstr "Vous n'avez pas l'autorisation d'éditer des requêtes."
+
+#: ../templates/classic/html/query.edit.html:28
+msgid "Query"
+msgstr "Requête"
+
+#: ../templates/classic/html/query.edit.html:29
+msgid "Include in \"Your Queries\""
+msgstr "Inclus dans \"Vos requêtes\""
+
+#: ../templates/classic/html/query.edit.html:31
+msgid "Private to you?"
+msgstr "Privé ?"
+
+#: ../templates/classic/html/query.edit.html:44
+msgid "leave out"
+msgstr "sortir"
+
+#: ../templates/classic/html/query.edit.html:45
+msgid "include"
+msgstr "inclure"
+
+#: ../templates/classic/html/query.edit.html:49
+msgid "leave in"
+msgstr "entrer"
+
+#: ../templates/classic/html/query.edit.html:54
+msgid "[query is retired]"
+msgstr "[requête abandonnée]"
+
+#: ../templates/classic/html/query.edit.html:67
+#: ../templates/classic/html/query.edit.html:92
+msgid "edit"
+msgstr "éditer"
+
+#: ../templates/classic/html/query.edit.html:71
+msgid "yes"
+msgstr "oui"
+
+#: ../templates/classic/html/query.edit.html:73
+msgid "no"
+msgstr "non"
+
+#: ../templates/classic/html/query.edit.html:79
+msgid "Delete"
+msgstr "Effacer"
+
+#: ../templates/classic/html/query.edit.html:94
+msgid "[not yours to edit]"
+msgstr "[ne vous appartient pas]"
+
+#: ../templates/classic/html/query.edit.html:102
+msgid "Save Selection"
+msgstr "Sauvegarder la sélection"
+
+#: ../templates/classic/html/user.forgotten.html:3
+msgid "Password reset request - ${tracker}"
+msgstr "Demande de réinitialisation de mot de passe - ${tracker}"
+
+#: ../templates/classic/html/user.forgotten.html:5
+msgid "Password reset request"
+msgstr "Demande de réinitialisation de mot de passe"
+
+#: ../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 ""
+"Vous avez deux solutions si vous avez oublié votre mot de passe. Si "
+"vous connaissez l'adresse de messagerie avec laquelle vous vous êtes "
+"enregistré, introduisez-là ci-dessous."
+
+#: ../templates/classic/html/user.forgotten.html:16
+msgid "Email Address:"
+msgstr "Adresse de messagerie:"
+
+#: ../templates/classic/html/user.forgotten.html:24
+#: ../templates/classic/html/user.forgotten.html:34
+msgid "Request password reset"
+msgstr "Demander une réinitialisation du mot de passe"
+
+#: ../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."
+
+#: ../templates/classic/html/user.forgotten.html:33
+msgid "Username:"
+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 être envoyé - veuillez "
+"suivre les instructions qui y sont données pour terminer le processus "
+"de réinitialisation de votre mot de passe."
+
+#: ../templates/classic/html/user.index.html:3
+#: ../templates/minimal/html/user.index.html:3
+msgid "User listing - ${tracker}"
+msgstr "Liste des utilisateurs - ${tracker}"
+
+#: ../templates/classic/html/user.index.html:5
+#: ../templates/minimal/html/user.index.html:5
+msgid "User listing"
+msgstr "Liste des utilisateurs"
+
+#: ../templates/classic/html/user.index.html:14
+#: ../templates/minimal/html/user.index.html:14
+msgid "Username"
+msgstr "Nom d'utilisateur"
+
+#: ../templates/classic/html/user.index.html:15
+msgid "Real name"
+msgstr "Nom réel"
+
+#: ../templates/classic/html/user.index.html:16
+#: ../templates/classic/html/user.item.html:70
+#: ../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
+msgid "Email address"
+msgstr "Adresse de messagerie"
+
+#: ../templates/classic/html/user.index.html:18
+msgid "Phone number"
+msgstr "Numéro de téléphone"
+
+#: ../templates/classic/html/user.index.html:19
+msgid "Retire"
+msgstr "Abandonner"
+
+#: ../templates/classic/html/user.index.html:32
+msgid "retire"
+msgstr "abandonner"
+
+#: ../templates/classic/html/user.item.html:7
+#: ../templates/minimal/html/user.item.html:7
+msgid "User ${id}: ${title} - ${tracker}"
+msgstr "Utilisateur ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:10
+#: ../templates/minimal/html/user.item.html:10
+msgid "New User - ${tracker}"
+msgstr "Nouvel utilisateur - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:14
+#: ../templates/minimal/html/user.item.html:14
+msgid "New User"
+msgstr "Nouvel utilisateur"
+
+#: ../templates/classic/html/user.item.html:16
+#: ../templates/minimal/html/user.item.html:16
+msgid "New User Editing"
+msgstr "Édition d'un nouvel utilisateur"
+
+#: ../templates/classic/html/user.item.html:19
+#: ../templates/minimal/html/user.item.html:19
+msgid "User${id}"
+msgstr "User${id}"
+
+#: ../templates/classic/html/user.item.html:22
+#: ../templates/minimal/html/user.item.html:22
+msgid "User${id} Editing"
+msgstr "Édition de User${id}"
+
+#: ../templates/classic/html/user.item.html:43
+#: ../templates/classic/html/user.register.html:21
+#: ../templates/minimal/html/user.item.html:40
+#: ../templates/minimal/html/user.register.html:26
+msgid "Login Name"
+msgstr "Nom d'utilisateur"
+
+#: ../templates/classic/html/user.item.html:47
+#: ../templates/classic/html/user.register.html:25
+#: ../templates/minimal/html/user.item.html:44
+#: ../templates/minimal/html/user.register.html:30
+msgid "Login Password"
+msgstr "Mot de passe"
+
+#: ../templates/classic/html/user.item.html:51
+#: ../templates/classic/html/user.register.html:29
+#: ../templates/minimal/html/user.item.html:48
+#: ../templates/minimal/html/user.register.html:34
+msgid "Confirm Password"
+msgstr "Confirmation du mot de passe"
+
+#: ../templates/classic/html/user.item.html:55
+#: ../templates/classic/html/user.register.html:33
+#: ../templates/minimal/html/user.item.html:52
+#: ../templates/minimal/html/user.register.html:38
+msgid "Roles"
+msgstr "Rôles"
+
+#: ../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ôle, introduisez une liste,"
+"séparée,par,des,virgules)"
+
+#: ../templates/classic/html/user.item.html:66
+#: ../templates/classic/html/user.register.html:41
+msgid "Phone"
+msgstr "Téléphone"
+
+#: ../templates/classic/html/user.item.html:74
+msgid "Timezone"
+msgstr "Fuseau horaire"
+
+#: ../templates/classic/html/user.item.html:78
+msgid "(this is a numeric hour offset, the default is ${zone})"
+msgstr ""
+"(il s'agit d'un décalage horaire numérique, par défaut: "
+"${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 "Adresse de messagerie"
+
+#: ../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"
+
+#: ../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ès de ${tracker}"
+
+#: ../templates/classic/html/user.rego_progress.html:4
+#: ../templates/minimal/html/user.rego_progress.html:4
+msgid "Registration in progress - ${tracker}"
+msgstr "Inscription en cours - ${tracker}"
+
+#: ../templates/classic/html/user.rego_progress.html:6
+#: ../templates/minimal/html/user.rego_progress.html:6
+msgid "Registration in progress..."
+msgstr "Inscription en cours..."
+
+#: ../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 ""
+"Vous recevrez sous peu un courriel confirmant votre inscription. Pour "
+"clôturer le processus d'inscription, veuillez suivre le lien "
+"indiqué dans le courriel."
+
+#: ../templates/minimal/html/home.html:2
+msgid "Tracker home - ${tracker}"
+msgstr "Base du pisteur - ${tracker}"
+
+#: ../templates/minimal/html/home.html:4
+msgid "Tracker home"
+msgstr "Base du pisteur"
+
+#: ../templates/minimal/html/home.html:16
+msgid "Please select from one of the menu options on the left."
+msgstr ""
+"Veuillez sélectionner l'une des options de menu à la gauche."
+
+#: ../templates/minimal/html/home.html:19
+msgid "Please log in or register."
+msgstr "Veuillez vous connecter ou vous inscrire."
+
+#: ../templates/minimal/html/page.html:55
+msgid "Hello,<br>${user}"
+msgstr "Bienvenue,<br>${user}"
Added: tracker/vendor/roundup/current/locale/lt.po
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/locale/lt.po Sun Nov 5 21:30:25 2006
@@ -0,0 +1,3129 @@
+# Lithuanian message file for Roundup Issue Tracker
+# Aiste Kesminaite <aiste at pov.lt>, 2005
+#
+# $Id: lt.po,v 1.5 2006/03/04 08:29:18 a1s Exp $
+#
+# roundup.pot revision 1.17
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: roundup-1.1.0\n"
+"Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
+"POT-Creation-Date: 2006-02-28 07:44+0200\n"
+"PO-Revision-Date: 2006-03-04 10:10+0200\n"
+"Last-Translator: Nerijus Baliunas <nerijus at users.sourceforge.net>\n"
+"Language-Team:\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:85 :962 :1011 :1033
+#: ../roundup/admin.py:85 ../roundup/admin.py:979 ../roundup/admin.py:1028
+#: ../roundup/admin.py:1050
+#, python-format
+msgid "no such class \"%(classname)s\""
+msgstr "nÄra klasÄs \"%(classname)s\""
+
+# ../roundup/admin.py:95 :99
+#: ../roundup/admin.py:95 ../roundup/admin.py:99
+#, python-format
+msgid "argument \"%(arg)s\" not propname=value"
+msgstr "argumentas \"%(arg)s\" nÄra parinktis=reikÅ¡mÄ formato"
+
+#: ../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"
+"\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 ""
+"%(message)sNaudojimas: roundup-admin [parinktys] [<komanda> <argumentai>]\n"
+"\n"
+"Variantai:\n"
+" -i instance home -- roundup'o \"namų direktorija\" administravimui\n"
+" -u -- vartotojas[:slaptažodis] komandoms autentifikuoti\n"
+" -d -- iÅ¡spausdinti pilnus dezignatorius, ne tik klasÄs id\n"
+" numerius\n"
+" -c -- iÅ¡vedant duomenų sÄ
rašus, atskirti juos kableliais.\n"
+" Taip pat kaip '-S \",\"'.\n"
+" -S <string> -- iÅ¡vedant duomenų sÄ
rašus, atskirti stringu.\n"
+" -s -- iÅ¡vedant duomenų sÄ
rašus, atskirti juos tarpais.\n"
+" Taip pat kaip '-S \" \"'.\n"
+"\n"
+" Tik vienas iš -s, -c ar -S gali būti nurodyta.\n"
+"\n"
+"Pagalba:\n"
+" roundup-admin -h\n"
+" roundup-admin help -- Å¡is pagalbos puslapis\n"
+" roundup-admin help <komanda> -- specifinÄ pagalba komandoms\n"
+" roundup-admin help all -- visa įmanoma pagalba\n"
+
+#: ../roundup/admin.py:138
+msgid "Commands:"
+msgstr "Komandos:"
+
+#: ../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 ""
+"Komandos gali bÅ«ti sutrumpintos, taÄiau sutrumpinimas turi atitikti tik\n"
+"vienÄ
komandÄ
, pvz. l == li == lis == list."
+
+#: ../roundup/admin.py:175
+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 ""
+"\n"
+"Visos komandos (išskyrus help) reikalauja tracker'io specifikatoriaus.\n"
+"Tai yra tiesiog kelias iki roundup tracker'io, su kuriuo dirbate. Roundup\n"
+"laiko duomenų bazÄ ir konfigÅ«racijos failÄ
, kuris aprašo kreipinių valdymo\n"
+"sistemÄ
būtent ten. Apie jį galima galvoti kaip apie kreipinių valdymo\n"
+"sistemos \"namų direktorijÄ
\". Jis gali būti nurodytas aplinkos "
+"kintamajame \n"
+"TRACKER_HOME arba komandinÄje eilutÄje kaip \"-i tracker\".\n"
+"\n"
+"Dezignatorius - tai klasÄs vardas sujungtas su elemento id, pvz.\n"
+"vartotojas10, kreipinys1. \n"
+"\n"
+"Atributo reikÅ¡mÄs yra rodomos kaip simbolių eilutÄs komandų argumentuose ir\n"
+"atvaizduojamuose rezultatuose:\n"
+" . Simbolių eilutÄs yra, hm... simbolių eilutÄs.\n"
+" . Datos reikÅ¡mÄs yra atvaizduojamos pilnu datos formatu lokalioje laiko\n"
+" zonoje, ir priimamos pilnu formatu arba bet kuriuo iš dalinių formatų,\n"
+" paaiškintų žemiau.\n"
+" . Saito reikÅ¡mÄs yra atvaizduojamos kaip elemento dezignatoriai. Kai jos\n"
+" pateikiamos kaip argumentai, priimami ir elemento dezignatoriai, ir \n"
+" raktų simbolių eilutÄs. \n"
+" . DaugiasaitÄs reikÅ¡mÄs yra atvaizduojamos kaip elemento dezignatorių\n"
+" sÄ
rašai, sujungti kableliais. Kai jos pateikiamos kaip argumentai, yra \n"
+" priimami ir elementų dezignatoriai, ir raktų simbolių eilutÄs; tuÅ¡Äia\n"
+" eilutÄ, vienas elementas ar elementų sÄ
rašas yra taip pat priimami.\n"
+"\n"
+"Kai atributo reikÅ¡mÄse yra bÅ«tini tarpai, tiesiog apskliauskite reikÅ¡mÄ\n"
+"kabutÄmis, ' arba \". Vienas tarpas taipogi gali bÅ«ti paslepiamas \n"
+"atvirkÅ¡tiniu pasviru brÅ«kÅ¡neliu. Jei vertÄje bÅ«tinas kabuÄių simbolis, jis\n"
+"turi būti paslepiamas atvirkštiniu pasviru brūkšneliu arba apskliaudžiamas \n"
+"kabutÄmis.\n"
+"Pavyzdžiai:\n"
+" hello world (2 leksemos: hello, world)\n"
+" \"hello world\" (1 leksema: hello world)\n"
+" \"Roch'e\" Compaan (2 leksemos: Roch'e Compaan)\n"
+" Roch\\'e Compaan (2 leksemos: Roch'e Compaan)\n"
+" address=\"1 2 3\" (1 leksema: address=1 2 3)\n"
+" \\\\ (1 leksema: \\)\n"
+" \\n\\r\\t (1 leksema: LF, CR ir TAB simboliai)\n"
+"\n"
+"Kai roundup'o get ar set komandoms yra paduodami keli elementai, nurodyti\n"
+"atributai yra gaunami ar nustatomi visiems paduotiems elementams. \n"
+"\n"
+"Kai roundup get ar roundup find komandos grÄ
žina kelis rezultatus, jie yra \n"
+"paprastai atvaizduojami po vienÄ
eilutÄje arba sujungiami kableliais \n"
+"(nurodžius -c parinktis).\n"
+"\n"
+"Kur komanda pakeiÄia duomenis, reikalaujamas vartotojo vardas/slaptažodis.\n"
+"Vartotojo vardas gali būti nurodomas kaip \"vartotojas\" arba \n"
+"\"vartotojas:slaptažodis\".\n"
+" . ROUNDUP_LOGIN aplinkos kintamasis\n"
+" . -u komandinÄs eilutÄs parinktis\n"
+"Jei arba vartotojo vardas, arba slaptažodis nÄra pateikiamas, jie gaunami \n"
+"iÅ¡ komandinÄs eilutÄs. \n"
+"\n"
+"Datos formato pavyzdžiai:\n"
+" \"2000-04-17.03:45\" reiškia <Data 2000-04-17.08:45:00>\n"
+" \"2000-04-17\" reiškia <Data 2000-04-17.00:00:00>\n"
+" \"01-25\" reiškia <Data yyyy-01-25.00:00:00>\n"
+" \"08-13.22:13\" reiškia <Data yyyy-08-14.03:13:00>\n"
+" \"11-07.09:32:43\" reiškia <Data yyyy-11-07.14:32:43>\n"
+" \"14:25\" reiškia <Data yyyy-mm-dd.19:25:00>\n"
+" \"8:47:11\" reiškia <Data yyyy-mm-dd.13:47:11>\n"
+" \".\" reiškia \"dabar\"\n"
+"\n"
+"Komandų pagalba:\n"
+
+#: ../roundup/admin.py:238
+#, python-format
+msgid "%s:"
+msgstr "%s:"
+
+#: ../roundup/admin.py:243
+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 ""
+"Naudojimas: help tema\n"
+" Pagalba atitinkama tema.\n"
+"\n"
+" commands -- išvardinti komandas\n"
+" <komanda> -- pagalba specifinei komandai\n"
+" initopts -- init komandų parinktys\n"
+" all -- visa įmanoma pagalba\n"
+" "
+
+#: ../roundup/admin.py:266
+#, python-format
+msgid "Sorry, no help for \"%(topic)s\""
+msgstr "AtsipraÅ¡ome, pagalbos temai \"%(topic)s\" nÄra"
+
+# ../roundup/admin.py:338 :387
+#: ../roundup/admin.py:338 ../roundup/admin.py:394
+msgid "Templates:"
+msgstr "Å ablonai:"
+
+# ../roundup/admin.py:341 :398
+#: ../roundup/admin.py:341 ../roundup/admin.py:405
+msgid "Back ends:"
+msgstr "Duomenų saugyklos:"
+
+#: ../roundup/admin.py:344
+msgid ""
+"Usage: install [template [backend [admin password [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"
+"\n"
+" The last command line argument allows to pass initial values\n"
+" 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 ""
+"Naudojimas: install [šablonas [saugykla [administratoriaus slaptažodis "
+"[raktas=reikÅ¡mÄ[,raktas=reikÅ¡mÄ]]]]]\n"
+" Ä®diegti naujÄ
Roundup kreipinių valdymo sistemÄ
.\n"
+"\n"
+" Ši komanda paprašys tracker'io namų direktorijos (jei nepaduota\n"
+" per TRACKER_HOME ar -i parinktį). Šablonas, duomenų saugykla ir\n"
+" administratoriaus slaptažodis gali būti nurodyti kaip\n"
+" argumentai komandinÄje eilutÄje bÅ«tent Å¡ia tvarka.\n"
+"\n"
+" Paskutinis komandinÄs eilutÄs argumentas priskiria pradines\n"
+" reikšmes konfigūracijos parinktims. Pavyzdžiui, nurodydami\n"
+" \"web_http_auth=no,rdbms_user=dinsdale\" pakeisite http_auth\n"
+" sekcijoje [web] ir user sekcijoje [rdbms] parinkÄių reikÅ¡mes\n"
+" pagal nutylÄjimÄ
. Nenaudokite tarpų Å¡iame argumente (visÄ
\n"
+" argumentÄ
raÅ¡ykite kabutÄse, jei parinkties reikÅ¡mÄje yra "
+"tarpų).\n"
+"\n"
+" Inicializavimo komanda turi būti pateikta po šios komandos tam,\n"
+" kad inicializuotųsi tracker'io duomenų bazÄ. JÅ«s galite "
+"pakeisti\n"
+" pradinį tracker'io duomenų bazÄs turinį prieÅ¡ paleisdami Å¡iÄ
\n"
+" komandÄ
, pakeisdami tracker'io dbinit.py modulio init() "
+"funkcijÄ
.\n"
+"\n"
+" Taip pat pažiÅ«rÄkite initopts pagalbÄ
.\n"
+" "
+
+# ../roundup/admin.py:360 :447 :508 :587 :637 :695 :716 :744 :815 :882 :953
+# :1001 :1023 :1050 :1117 :1184
+#: ../roundup/admin.py:367 ../roundup/admin.py:464 ../roundup/admin.py:525
+#: ../roundup/admin.py:604 ../roundup/admin.py:654 ../roundup/admin.py:712
+#: ../roundup/admin.py:733 ../roundup/admin.py:761 ../roundup/admin.py:832
+#: ../roundup/admin.py:899 ../roundup/admin.py:970 ../roundup/admin.py:1018
+#: ../roundup/admin.py:1040 ../roundup/admin.py:1067 ../roundup/admin.py:1134
+#: ../roundup/admin.py:1204
+msgid "Not enough arguments supplied"
+msgstr "Paduota nepakankamai argumentų"
+
+#: ../roundup/admin.py:373
+#, python-format
+msgid "Instance home parent directory \"%(parent)s\" does not exist"
+msgstr "Namų direktorijos tÄvinÄ direktorija \"%(parent)s\" neegzistuoja"
+
+#: ../roundup/admin.py:381
+#, 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 ""
+"PERSPÄJIMAS: \"%(tracker_home)s\" jau yra tracker'is!\n"
+"Jei jūs jį perdiegsite, prarasite visus duomenis!\n"
+"Ištrinti jį? Y/N: "
+
+#: ../roundup/admin.py:396
+msgid "Select template [classic]: "
+msgstr "Pasirinkite Å¡ablonÄ
[klasikinis]: "
+
+#: ../roundup/admin.py:407
+msgid "Select backend [anydbm]: "
+msgstr "Pasirinkite duomenų saugyklÄ
[anydbm]: "
+
+#: ../roundup/admin.py:417
+#, python-format
+msgid "Error in configuration settings: \"%s\""
+msgstr "Klaida konfigūracijos nustatymuose: \"%s\""
+
+#: ../roundup/admin.py:426
+#, python-format
+msgid ""
+"\n"
+"---------------------------------------------------------------------------\n"
+" You should now edit the tracker configuration file:\n"
+" %(config_file)s"
+msgstr ""
+"\n"
+"---------------------------------------------------------------------------\n"
+" Dabar jÅ«s turÄtumÄte pakeisti tracker'io konfigÅ«racijos failÄ
:\n"
+" %(config_file)s"
+
+#: ../roundup/admin.py:436
+msgid " ... at a minimum, you must set following options:"
+msgstr " ... mažiausiai turÄtumÄte nustalyti Å¡ias parinktis:"
+
+#: ../roundup/admin.py:441
+#, 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 ""
+"\n"
+" Jei jÅ«s norite keisti duomenų bazÄs schema,\n"
+" jÅ«s taip pat turÄtumÄte pakeisti schema failÄ
:\n"
+" %(database_config_file)s\n"
+" JÅ«s taip pat galite pakeisti duomenų bazÄs inicializacijos failÄ
:\n"
+" %(database_init_file)s\n"
+" ... jei reikia daugiau informacijos, žiÅ«rÄkite dokumentacijÄ
apie \n"
+" pakeitimus.\n"
+"\n"
+" JÅ«s PRIVALOTE paleisti \"roundup-admin initialise\" komandÄ
, kai atliksite\n"
+" aukÅ¡Äiau minÄtus žingsnius.\n"
+"---------------------------------------------------------------------------\n"
+
+#: ../roundup/admin.py:459
+msgid ""
+"Usage: genconfig <filename>\n"
+" Generate a new tracker config file (ini style) with default values\n"
+" in <filename>.\n"
+" "
+msgstr ""
+"Naudojimas: genconfig <failovardas>\n"
+" Generuoti naujÄ
tracker'io konfigÅ«racijos failÄ
(ini tipo) su\n"
+" įprastomis reikÅ¡mÄmis faile <failovardas>.\n"
+" "
+
+#. password
+#: ../roundup/admin.py:469
+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 ""
+"Naudojimas: initialise [adminslaptažodis]\n"
+" Inicializuoti naujÄ
Roundup kreipinių valdymo sistemÄ
.\n"
+"\n"
+" Administratoriaus pasirinkimai bus nustatomi šiuo žingsniu.\n"
+"\n"
+" Vykdyti tracker'io inicializacijos funkcijÄ
dbinit.init()\n"
+" "
+
+#: ../roundup/admin.py:483
+msgid "Admin Password: "
+msgstr "Administratoriaus slaptažodis: "
+
+#: ../roundup/admin.py:484
+msgid " Confirm: "
+msgstr " Patvirtinkite: "
+
+#: ../roundup/admin.py:488
+msgid "Instance home does not exist"
+msgstr "Namų direktorija neegzistuoja"
+
+#: ../roundup/admin.py:492
+msgid "Instance has not been installed"
+msgstr "Egzempliorius nebuvo įdiegtas"
+
+#: ../roundup/admin.py:497
+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 ""
+"PERSPÄJIMAS: Duomenų bazÄ jau inicializuota!\n"
+"Jei jÅ«s jÄ
inicializuosite dar kartÄ
, prarasite visus duomenis!\n"
+"IÅ¡trinti duomenų bazÄ? Y/N: "
+
+#: ../roundup/admin.py:518
+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 ""
+"Naudojimas: get parinktis dezignatorius[,dezignatorius]*\n"
+" Gauti pateikto vieno ar kelių dezignatorių parinktį.\n"
+"\n"
+" Gauna elementų, nurodytų dezignatoriais parinkties reikÅ¡mÄ.\n"
+" "
+
+# ../roundup/admin.py:541 :556
+#: ../roundup/admin.py:558 ../roundup/admin.py:573
+#, python-format
+msgid "property %s is not of type Multilink or Link so -d flag does not apply."
+msgstr ""
+"parinktis %s nÄra Multilink ar Link tipo, komandų eilutÄs parametras\n"
+"-d netinkamas."
+
+# ../roundup/admin.py:564 :964 :1013 :1035
+#: ../roundup/admin.py:581 ../roundup/admin.py:981 ../roundup/admin.py:1030
+#: ../roundup/admin.py:1052
+#, python-format
+msgid "no such %(classname)s node \"%(nodeid)s\""
+msgstr "nÄra tokio %(classname)s elemento \"%(nodeid)s\""
+
+#: ../roundup/admin.py:583
+#, python-format
+msgid "no such %(classname)s property \"%(propname)s\""
+msgstr "nÄra tokio %(classname)s parinkties \"%(propname)s\""
+
+#: ../roundup/admin.py:592
+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 ""
+"Naudojimas: set elementas parinktis=reikÅ¡mÄ parinktis=reikÅ¡mÄ ...\n"
+" Nustatyti pateiktas parinktis vienam ar keliems elementams.\n"
+"\n"
+" Elementai nnurodomi kaip klasÄ arba kaip kableliais atskirtas\n"
+" sÄ
rašas elementų dezignatorių \n"
+" (t.y \"dezignatorius[, dezignatorius,...]\").\n"
+"\n"
+" Ši komanda priskiria reikšmes visų duotų dezignatorių\n"
+" parinktims. Jei reikÅ¡mÄs nÄra (t.y. \"parinktis=\"), tada \n"
+" parinkties reikÅ¡mÄ yra grÄ
žinama į standartinÄ. Jei parinktis\n"
+" yra daugiasaitÄ, nurodomi susieti id kaip kableliais atskirtos\n"
+" reikÅ¡mÄs (t.y. \"1,2,3\").\n"
+" "
+
+#: ../roundup/admin.py:646
+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 ""
+"Naudojimas: find klasÄsvardas parinktis=reikÅ¡mÄ ...\n"
+" Rasti duotos klasÄs elementus, atitinkanÄius pateiktos sÄ
sajos\n"
+" parinkties reikÅ¡mÄ.\n"
+"\n"
+" Rasti duotos klasÄs elementus, atitinkanÄius pateiktos sqsajos\n"
+" parinkties reikÅ¡mÄ. ReikÅ¡mÄ gali bÅ«ti arba susieto elemento id\n"
+" arba jo raktinÄ reikÅ¡mÄ.\n"
+" "
+
+# ../roundup/admin.py:682 :835 :847 :901
+#: ../roundup/admin.py:699 ../roundup/admin.py:852 ../roundup/admin.py:864
+#: ../roundup/admin.py:918
+#, python-format
+msgid "%(classname)s has no property \"%(propname)s\""
+msgstr "%(classname)s neturi parinkties \"%(propname)s\""
+
+#: ../roundup/admin.py:706
+msgid ""
+"Usage: specification classname\n"
+" Show the properties for a classname.\n"
+"\n"
+" This lists the properties for a given class.\n"
+" "
+msgstr ""
+"Naudojimas: specification klasÄsvardas\n"
+" Rodyti klasÄsvardas parinktis.\n"
+"\n"
+" Å i komanda iÅ¡vardina duotos klasÄs parinktis.\n"
+" "
+
+#: ../roundup/admin.py:721
+#, python-format
+msgid "%(key)s: %(value)s (key property)"
+msgstr "%(key)s: %(value)s (key property)"
+
+#: ../roundup/admin.py:723
+#, python-format
+msgid "%(key)s: %(value)s"
+msgstr "%(key)s: %(value)s"
+
+#: ../roundup/admin.py:726
+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 ""
+"Naudojimas: display dezignatorius[, dezignatorius]*\n"
+" Rodyti duoto elemento(ų) parinkties reikšmes.\n"
+"\n"
+" Ši komanda išvardina parinktis ir jų reikšmes duotam elementui.\n"
+" "
+
+#: ../roundup/admin.py:750
+#, python-format
+msgid "%(key)s: %(value)r"
+msgstr "%(key)s: %(value)r"
+
+#: ../roundup/admin.py:753
+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 ""
+"Naudojimas: create klasÄsvardas parinktis=reikÅ¡mÄ ...\n"
+" Sukurti naujÄ
įraÅ¡Ä
duotai klasei.\n"
+"\n"
+" Å i komanda sukuria naujÄ
įraÅ¡Ä
duotai klasei naudodama\n"
+" parinkties vardas=reikÅ¡mÄ argumentus, pateikiamus komandinÄje\n"
+" eilutÄje po \"create\" komandos.\n"
+" "
+
+#: ../roundup/admin.py:780
+#, python-format
+msgid "%(propname)s (Password): "
+msgstr "%(propname)s (Slaptažodis): "
+
+#: ../roundup/admin.py:782
+#, python-format
+msgid " %(propname)s (Again): "
+msgstr " %(propname)s (Pakartoti): "
+
+#: ../roundup/admin.py:784
+msgid "Sorry, try again..."
+msgstr "Bandykite dar kartÄ
..."
+
+#: ../roundup/admin.py:788
+#, python-format
+msgid "%(propname)s (%(proptype)s): "
+msgstr "%(propname)s (%(proptype)s): "
+
+#: ../roundup/admin.py:806
+#, python-format
+msgid "you must provide the \"%(propname)s\" property."
+msgstr "turite pateikti parinktį \"%(propname)s\"."
+
+#: ../roundup/admin.py:817
+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 ""
+"Naudojimas: list klasÄsvardas [parinktis]\n"
+" IÅ¡vardina klasÄs egzempliorius.\n"
+"\n"
+" IÅ¡vardina visus duotos klasÄs egzempliorius. Jei parinktis \n"
+" nenurodyta, naudojama \"label\" parinktis. Ji yra bandoma\n"
+" iÅ¡ eilÄs: raktas, \"vardas\", \"pavadinimas\", o tada pirma\n"
+" iÅ¡ eilÄs parinktis abÄcÄlÄs tvarka.\n"
+"\n"
+" Pasirinkus -c, -S ar -s atvaizduoja sÄ
raÅ¡Ä
elementų id jei nÄra\n"
+" nurodyta parinktis. Jei parinktis nurodyta, atvaizduojamas tos\n"
+" parinkties sÄ
raÅ¡as kiekvienam klasÄs egzemplioriui.\n"
+" "
+
+#: ../roundup/admin.py:830
+msgid "Too many arguments supplied"
+msgstr "Pateikta per daug argumentų"
+
+#: ../roundup/admin.py:866
+#, python-format
+msgid "%(nodeid)4s: %(value)s"
+msgstr "%(nodeid)4s: %(value)s"
+
+#: ../roundup/admin.py:870
+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 ""
+"Naudojimas: table klasÄsvardas [parinktis[,parinktis]*]\n"
+" IÅ¡vardina klasÄs egzempliorius lentelÄs pavidale.\n"
+"\n"
+" IÅ¡vardina visus duotos klasÄs egzempliorius. Jei parinktys\n"
+" nenurodytos, parodomos visos parinktys. Standartiškai, "
+"stulpelių\n"
+" plotis yra ilgiausios reikÅ¡mÄs ploÄio. Plotis gali bÅ«ti "
+"nurodomas\n"
+" pateikiant parinktį \"vardas:plotis\".\n"
+" Pavyzdžiui::\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"
+" jei norite, kad stulpelio protis atitiktų etiketÄs plotį, \n"
+" palikite : nenurodydami parinkties ploÄio. Pavyzdžiui::\n"
+"\n"
+" roundup> table priority id,name:\n"
+" Id Name\n"
+" 1 fata\n"
+" 2 bug\n"
+" 3 usab\n"
+" 4 feat\n"
+"\n"
+" pateiks 4 simbolių ilgio \"Name\" stulpelį.\n"
+" "
+
+#: ../roundup/admin.py:914
+#, python-format
+msgid "\"%(spec)s\" not name:width"
+msgstr "\"%(spec)s\" ne vardas:plotis"
+
+#: ../roundup/admin.py:964
+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 ""
+"Naudojimas: history dezignatorius\n"
+" Parodo dezignatoriaus įrašų istorijÄ
.\n"
+"\n"
+" Parodo žurnalinius įrašus elementui identifikuotam \n"
+" dezignatoriaus. \n"
+" "
+
+#: ../roundup/admin.py:985
+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 ""
+"Naudojimas: commit\n"
+" IÅ¡saugoti pakeitimus atliktus duomenų bazÄje interaktyvios\n"
+" sesijos metu.\n"
+"\n"
+" Pakeitimai, atlikti interaktyvios sesijos metu, nÄra \n"
+" automatiÅ¡kai įraÅ¡omi į duomenų bazÄ - jie turi bÅ«ti iÅ¡saugomi \n"
+" Å¡ios komandos pagalba.\n"
+"\n"
+" VienetinÄs komandos komandinÄje eilutÄje yra atutomatiÅ¡kai \n"
+" iÅ¡saugomos, jei jos įvykdomos sÄkmingai.\n"
+" "
+
+#: ../roundup/admin.py:999
+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 ""
+"Naudojimas: rollback\n"
+" Anuliuoti visus pakeitimus duomenų bazÄje, kurie turi bÅ«ti \n"
+" iÅ¡saugoti naudojant commit komandÄ
.\n"
+"\n"
+" Pakeitimai, atlikti interaktyvios sesijos metu, nÄra\n"
+" automatiÅ¡kai įraÅ¡omi į duomenų bazÄ - jie turi bÅ«ti\n"
+" išsaugomi rankinių būdu. Ši komanda anuliuoja visus\n"
+" pakeitimus, taigi commit komanda iškarto po šios komandos\n"
+" nepadarys jokių pakeitimų duomenų bazÄje.\n"
+" "
+
+#: ../roundup/admin.py:1011
+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 ""
+"Naudojimas: retire dezignatorius[,dezignatorius]*\n"
+" Deaktyvuoti elementÄ
nurodyta dezignatoriaus.\n"
+"\n"
+" Å i komanda nurodo, jog konkretus elementas nerodomas\n"
+" list ar find komandų ir jo raktas gali būti panaudotas dar \n"
+" kartÄ
.\n"
+" "
+
+#: ../roundup/admin.py:1034
+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 ""
+"Naudojimas: restore dezignatorius[,dezignatorius]*\n"
+" Aktyvuoti deaktyvuotÄ
elementÄ
, nurodomÄ
dezignatoriaus.\n"
+"\n"
+" Duotas elementas vÄl taps prieinamas vartotojams.\n"
+" "
+
+#. grab the directory to export to
+#: ../roundup/admin.py:1056
+msgid ""
+"Usage: export [class[,class]] export_dir\n"
+" Export the database to colon-separated-value files.\n"
+"\n"
+" Optionally limit the export to just the names classes.\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 ""
+"Naudojimas: export [klasÄ[,klasÄ]] eksporto_direktorija\n"
+" Eksportuoti duomenų bazÄ kaip kableliais atskirtų reikÅ¡mių \n"
+" failÄ
.\n"
+"\n"
+" Galima apriboti eksportÄ
tik vardų klasÄmis.\n"
+"\n"
+" Å i komanda eksportuoja dabartinius duomenis iÅ¡ duomenų bazÄs\n"
+" į kableliais atskirtų reikšmių failus, kurie išsaugomi \n"
+" nurodytoje direktorijoje.\n"
+" "
+
+#: ../roundup/admin.py:1114
+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 ""
+"Naudojimas: import importo_direktorija\n"
+" Importuoti duomenų bazÄ iÅ¡ direktorijos su CSV failais,\n"
+" vienai klasei -- du failai.\n"
+"\n"
+" Failai, kurių reikia importui yra:\n"
+"\n"
+" <klasÄ>.csv\n"
+" Å is failas turi nurodyti tokias paÄias parinktis kaip ir\n"
+" klasÄ (taip pat turi turÄti antraÅ¡tÄ su parinkÄių vardais.)\n"
+" <klasÄ>-journals.csv\n"
+" Å is failas apibrÄžia importuojamų vienetų žurnalus.\n"
+"\n"
+" Importuoti elementai turÄs tuos paÄius elementų id kaip\n"
+" nurodyta importo failuose, tokiu bÅ«du pakeisdami bet kokį esamÄ
\n"
+" turinį.\n"
+"\n"
+" Nauji elementai pridedami prie esamos duomenų bazÄs -- jei \n"
+" norite sukurti naujÄ
duomenų bazÄ naudodami importuojamus \n"
+" duomenis, tada reikia sukurti naujÄ
duomenų bazÄ (arba \n"
+" deaktyvuoti visus esamus duomenis -- daug darbo reikalaujantis\n"
+" veiksmas).\n"
+" "
+
+#: ../roundup/admin.py:1186
+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 ""
+"Naudojimas: pack periodas | data\n"
+"\n"
+" Pašalinti žurnalo įrašus senesnius nei nurodytas laiko tarpas\n"
+" ar atsiradusius anksÄiau nei nurodyta data.\n"
+"\n"
+" Periodas norodomas naudojant priesagas \"y\", \"m\" ir \"d\".\n"
+" Priesaga \"w\" (savaitÄ (week)) reiÅ¡kia 7 dienas.\n"
+"\n"
+" \"3y\" reiškia tris metus\n"
+" \"2y 1m\" reiÅ¡kia du mentus ir vienÄ
mÄnesį\n"
+" \"1m 25d\" reiÅ¡kia vienÄ
mÄnesį ir 25 dienas\n"
+" \"2w 3d\" reiškia dvi saivaites ir tris dienas\n"
+" \n"
+" Datos formatas yra \"YYYY-MM-DD\" pvz:\n"
+" 2001-12-31 \n"
+"\n"
+" "
+
+#: ../roundup/admin.py:1214
+msgid "Invalid format"
+msgstr "Netinkamas formatas"
+
+#: ../roundup/admin.py:1224
+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 ""
+"Naudojimas: reindex [klasÄs_vardas|dezignatorius]*\n"
+" Regeneruoti tracker'io paieškos indeksus.\n"
+"\n"
+" Ši komanda regeneruoja paieškos indeksus tracker'iui.\n"
+" Paprastai tai įvyksta automatiškai.\n"
+" "
+
+#: ../roundup/admin.py:1238
+#, python-format
+msgid "no such item \"%(designator)s\""
+msgstr "nÄra elemento \"%(designator)s\""
+
+#: ../roundup/admin.py:1248
+msgid ""
+"Usage: security [Role name]\n"
+" Display the Permissions available to one or all Roles.\n"
+" "
+msgstr ""
+"Naudojimas: security [RolÄs pavadinimas]\n"
+" Parodo vienos ar kelių rolių permisijas.\n"
+" "
+
+#: ../roundup/admin.py:1256
+#, python-format
+msgid "No such Role \"%(role)s\""
+msgstr "NÄra tokios rolÄs \"%(role)s\""
+
+#: ../roundup/admin.py:1262
+#, python-format
+msgid "New Web users get the Roles \"%(role)s\""
+msgstr "Naujiems web vartotojams suteikiamos rolÄs \"%(role)s\""
+
+#: ../roundup/admin.py:1264
+#, python-format
+msgid "New Web users get the Role \"%(role)s\""
+msgstr "Naujiems web vartotojams suteikiama rolÄ \"%(role)s\""
+
+#: ../roundup/admin.py:1267
+#, python-format
+msgid "New Email users get the Roles \"%(role)s\""
+msgstr "Naujiems vartotojams per el. paÅ¡tÄ
suteikiamos rolÄs \"%(role)s\""
+
+#: ../roundup/admin.py:1269
+#, python-format
+msgid "New Email users get the Role \"%(role)s\""
+msgstr "Naujiems vartotojams per el. paÅ¡tÄ
suteikiama rolÄ \"%(role)s\""
+
+#: ../roundup/admin.py:1272
+#, python-format
+msgid "Role \"%(name)s\":"
+msgstr "RolÄ \"%(name)s\":"
+
+#: ../roundup/admin.py:1277
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
+msgstr " %(description)s (%(name)s skirta tik \"%(klass)s\": %(properties)s)"
+
+#: ../roundup/admin.py:1280
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
+msgstr " %(description)s (%(name)s skirta tik \"%(klass)s\")"
+
+#: ../roundup/admin.py:1283
+#, python-format
+msgid " %(description)s (%(name)s)"
+msgstr " %(description)s (%(name)s)"
+
+#: ../roundup/admin.py:1312
+#, python-format
+msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
+msgstr ""
+"Nežinoma komanda \"%(command)s\" (įveskite \"help commands\" komandų\n"
+"sÄ
rašui gauti)"
+
+#: ../roundup/admin.py:1318
+#, python-format
+msgid "Multiple commands match \"%(command)s\": %(list)s"
+msgstr "Kelios komandos atitinka \"%(command)s\": %(list)s"
+
+#: ../roundup/admin.py:1325
+msgid "Enter tracker home: "
+msgstr "Ä®veskite tracker'io namų direktorijÄ
: "
+
+# ../roundup/admin.py:1312 :1318 :1338
+#: ../roundup/admin.py:1332 ../roundup/admin.py:1338 ../roundup/admin.py:1358
+#, python-format
+msgid "Error: %(message)s"
+msgstr "Klaida: %(message)s"
+
+#: ../roundup/admin.py:1346
+#, python-format
+msgid "Error: Couldn't open tracker: %(message)s"
+msgstr "Klaida: Negaliu atidaryti tracker'io: %(message)s"
+
+#: ../roundup/admin.py:1371
+#, python-format
+msgid ""
+"Roundup %s ready for input.\n"
+"Type \"help\" for help."
+msgstr ""
+"Roundup %s pasiruoÅ¡Äs priimti duomenis.\n"
+"NorÄdami iÅ¡kviesti pagalbÄ
įveskite \"help\"."
+
+#: ../roundup/admin.py:1376
+msgid "Note: command history and editing not available"
+msgstr "Pastaba: komandų archyvas ir redagavimas neprieinami"
+
+#: ../roundup/admin.py:1380
+msgid "roundup> "
+msgstr "roundup> "
+
+#: ../roundup/admin.py:1382
+msgid "exit..."
+msgstr "išeiti..."
+
+#: ../roundup/admin.py:1392
+msgid "There are unsaved changes. Commit them (y/N)? "
+msgstr "Yra neišsaugotų pakeitimų. Išsaugoti juos (y/N)? "
+
+#: ../roundup/backends/back_anydbm.py:1997
+#, python-format
+msgid "WARNING: invalid date tuple %r"
+msgstr "PERSPÄJIMAS: netinkamas datos tuple'as %r"
+
+#: ../roundup/backends/rdbms_common.py:1434
+msgid "create"
+msgstr "sukurti"
+
+#: ../roundup/backends/rdbms_common.py:1600
+msgid "unlink"
+msgstr "atsieti"
+
+#: ../roundup/backends/rdbms_common.py:1604
+msgid "link"
+msgstr "susieti"
+
+#: ../roundup/backends/rdbms_common.py:1724
+msgid "set"
+msgstr "nustatyti"
+
+#: ../roundup/backends/rdbms_common.py:1748
+msgid "retired"
+msgstr "deaktyvuotas"
+
+#: ../roundup/backends/rdbms_common.py:1778
+msgid "restored"
+msgstr "aktyvuotas"
+
+#: ../roundup/cgi/actions.py:58
+#, python-format
+msgid "You do not have permission to %(action)s the %(classname)s class."
+msgstr "JÅ«s neturite leidimo %(action)s %(classname)s klasÄ."
+
+#: ../roundup/cgi/actions.py:89
+msgid "No type specified"
+msgstr "Nenurodytas tipas"
+
+#: ../roundup/cgi/actions.py:91
+msgid "No ID entered"
+msgstr "Neįvestas ID"
+
+#: ../roundup/cgi/actions.py:97
+#, python-format
+msgid "\"%(input)s\" is not an ID (%(classname)s ID required)"
+msgstr "\"%(input)s\" nÄra ID (reikia %(classname)s ID)"
+
+#: ../roundup/cgi/actions.py:117
+msgid "You may not retire the admin or anonymous user"
+msgstr "Negalite deaktyvuoti administratoriaus ar anoniminio vartotojo"
+
+#: ../roundup/cgi/actions.py:124
+#, python-format
+msgid "%(classname)s %(itemid)s has been retired"
+msgstr "%(classname)s %(itemid)s buvo deaktyvuotas"
+
+# ../roundup/cgi/actions.py:163 :191
+#: ../roundup/cgi/actions.py:174 ../roundup/cgi/actions.py:202
+msgid "You do not have permission to edit queries"
+msgstr "Neturite leidimo redaguoti užklausas"
+
+# ../roundup/cgi/actions.py:169 :197
+#: ../roundup/cgi/actions.py:180 ../roundup/cgi/actions.py:209
+msgid "You do not have permission to store queries"
+msgstr "Neturite leidimo išsaugoti užklausas"
+
+#: ../roundup/cgi/actions.py:297
+#, python-format
+msgid "Not enough values on line %(line)s"
+msgstr "Nepakanka reikÅ¡mių eilutÄje %(line)s"
+
+#: ../roundup/cgi/actions.py:344
+msgid "Items edited OK"
+msgstr "Elementų pakeitimai išsaugoti"
+
+#: ../roundup/cgi/actions.py:404
+#, python-format
+msgid "%(class)s %(id)s %(properties)s edited ok"
+msgstr "%(class)s %(id)s %(properties)s pakeitimai išsaugoti"
+
+#: ../roundup/cgi/actions.py:407
+#, python-format
+msgid "%(class)s %(id)s - nothing changed"
+msgstr "%(class)s %(id)s - niekas nepakeista"
+
+#: ../roundup/cgi/actions.py:419
+#, python-format
+msgid "%(class)s %(id)s created"
+msgstr "sukurta %(class)s %(id)s"
+
+#: ../roundup/cgi/actions.py:451
+#, python-format
+msgid "You do not have permission to edit %(class)s"
+msgstr "Neturite leidimo redaguoti %(class)s"
+
+#: ../roundup/cgi/actions.py:463
+#, python-format
+msgid "You do not have permission to create %(class)s"
+msgstr "Neturite leidimo sukurti %(class)s"
+
+#: ../roundup/cgi/actions.py:487
+msgid "You do not have permission to edit user roles"
+msgstr "Neturite leidimo redaguoti vartotojų roles"
+
+#: ../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 ""
+"Redagavimo klaida: kitas vartotojas redagavo %s (%s). PeržiÅ«rÄkite <a target="
+"\"new\" href=\"%s%s\">jų pakeitimus</a> naujame lange."
+
+#: ../roundup/cgi/actions.py:565
+#, python-format
+msgid "Edit Error: %s"
+msgstr "Redagavimo klaida: %s"
+
+# ../roundup/cgi/actions.py:579 :590 :761 :780
+#: ../roundup/cgi/actions.py:596 ../roundup/cgi/actions.py:607
+#: ../roundup/cgi/actions.py:778 ../roundup/cgi/actions.py:797
+#, python-format
+msgid "Error: %s"
+msgstr "Klaida: %s"
+
+#: ../roundup/cgi/actions.py:633
+msgid ""
+"Invalid One Time Key!\n"
+"(a Mozilla bug may cause this message to show up erroneously, please check "
+"your email)"
+msgstr ""
+"Netinkamas One Time Key!\n"
+"(šį praneÅ¡imÄ
gali neteisingai sukelti Mozilla klaida, patikrinkite savo "
+"paÅ¡tÄ
.)"
+
+#: ../roundup/cgi/actions.py:675
+#, python-format
+msgid "Password reset and email sent to %s"
+msgstr "Slaptažodis atstatytas ir el. laiškas išsiųstas %s"
+
+#: ../roundup/cgi/actions.py:684
+msgid "Unknown username"
+msgstr "Nežinomas vartotojo vardas"
+
+#: ../roundup/cgi/actions.py:692
+msgid "Unknown email address"
+msgstr "Nežinomas el. pašto adresas"
+
+#: ../roundup/cgi/actions.py:697
+msgid "You need to specify a username or address"
+msgstr "Privalote nurodyti vartotojo vardÄ
ar el. paÅ¡to adresÄ
"
+
+#: ../roundup/cgi/actions.py:722
+#, python-format
+msgid "Email sent to %s"
+msgstr "El. laiškas išsiųstas %s"
+
+#: ../roundup/cgi/actions.py:741
+msgid "You are now registered, welcome!"
+msgstr "JÅ«s esate užregistruotas, sveiki prisijungÄ!"
+
+#: ../roundup/cgi/actions.py:786
+msgid "It is not permitted to supply roles at registration."
+msgstr "Negalima pateikti rolių registracijos metu."
+
+#: ../roundup/cgi/actions.py:878
+msgid "You are logged out"
+msgstr "JÅ«s atsijungÄte"
+
+#: ../roundup/cgi/actions.py:895
+msgid "Username required"
+msgstr "Reikalingas vartotojo vardas"
+
+# ../roundup/cgi/actions.py:897 :901
+#: ../roundup/cgi/actions.py:930 ../roundup/cgi/actions.py:934
+msgid "Invalid login"
+msgstr "Neteisingas vartotojo vardas"
+
+#: ../roundup/cgi/actions.py:940
+msgid "You do not have permission to login"
+msgstr "Neturite prisijungimo teisių"
+
+#: ../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>Å ablono klaida</h1>\n"
+"<p><b>%(exc_type)s</b>: %(exc_value)s</p>\n"
+"<p class=\"help\">Informacija klaidų taisymui</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>Ieškau \"%(name)s\", dabartinis kelias :<ol>%(path)s</ol></li>"
+
+#: ../roundup/cgi/cgitb.py:71
+#, python-format
+msgid "<li>In %s</li>"
+msgstr "<li>%s viduje</li>"
+
+#: ../roundup/cgi/cgitb.py:76
+#, python-format
+msgid "A problem occurred in your template \"%s\"."
+msgstr "Rasta klaida jūsų šablone \"%s\"."
+
+#: ../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>Vertinant %(info)r reiÅ¡kinį eilutÄje %(line)d\n"
+"<table class=\"otherinfo\" style=\"font-size: 90%%\">\n"
+" <tr><th colspan=\"2\" class=\"header\">Esami kintamieji:</th></tr>\n"
+" %(globals)s\n"
+" %(locals)s\n"
+"</table></li>\n"
+
+#: ../roundup/cgi/cgitb.py:103
+msgid "Full traceback:"
+msgstr "Pilnas traceback:"
+
+#: ../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>IÅ¡kilo problema leidžiant Python skriptÄ
. Äia pateikta seka funkcijų "
+"iškvietimų iki klaidos, kur naujausias (giliausias) iškvietimas yra pirmas. "
+"Klaidos atributai yra:"
+
+#: ../roundup/cgi/cgitb.py:129
+msgid "<file is None - probably inside <tt>eval</tt> or <tt>exec</tt>>"
+msgstr ""
+"<failas yra None - greiÄiausiai viduje <tt>eval</tt> ar <tt>exec</tt>>"
+
+#: ../roundup/cgi/cgitb.py:138
+#, python-format
+msgid "in <strong>%s</strong>"
+msgstr "<strong>%s</strong> viduje"
+
+# ../roundup/cgi/cgitb.py:172 :178
+#: ../roundup/cgi/cgitb.py:172 ../roundup/cgi/cgitb.py:178
+msgid "<em>undefined</em>"
+msgstr "<em>neapibrÄžta</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>Klaida</title></head>\n"
+"<body><h1>Klaida</h1>\n"
+"<p>Ä®vyko klaida vykdant jÅ«sų užklausÄ
.\n"
+"Apie klaidÄ
praneÅ¡Äme tracker'io administratoriui.</p>\n"
+"</body></html>"
+
+#: ../roundup/cgi/client.py:308
+msgid "Form Error: "
+msgstr "Formos klaida: "
+
+#: ../roundup/cgi/client.py:363
+#, python-format
+msgid "Unrecognized charset: %r"
+msgstr "Neatpažinta koduotÄ: %r"
+
+#: ../roundup/cgi/client.py:490
+msgid "Anonymous users are not allowed to use the web interface"
+msgstr "Anoniminiai vartotojai neturi teisių naudoti web interfeisÄ
"
+
+#: ../roundup/cgi/client.py:645
+msgid "You are not allowed to view this file."
+msgstr "JÅ«s neturite teisių žiÅ«rÄti šį failÄ
."
+
+#: ../roundup/cgi/client.py:737
+#, python-format
+msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
+msgstr "%(starttag)sPraÄjÄs laikas: %(seconds)fs%(endtag)s\n"
+
+#: ../roundup/cgi/client.py:741
+#, 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)sAtmintinÄs atitikimai: %(cache_hits)d, neatitikimai %"
+"(cache_misses)d. Įkeliami elementai: %(get_items)f sek. Filtruojama: %"
+"(filtering)f sek.%(endtag)s\n"
+
+#: ../roundup/cgi/form_parser.py:283
+#, python-format
+msgid "link \"%(key)s\" value \"%(value)s\" not a designator"
+msgstr "sÄ
sajos \"%(key)s\" reikÅ¡mÄ \"%(value)s\" nÄra dezignatorius"
+
+#: ../roundup/cgi/form_parser.py:290
+#, python-format
+msgid "%(class)s %(property)s is not a link or multilink property"
+msgstr "%(class)s %(property)s nÄra sÄ
sajos ar multisÄ
sajos parinktis"
+
+#: ../roundup/cgi/form_parser.py:312
+#, python-format
+msgid ""
+"You have submitted a %(action)s action for the property \"%(property)s\" "
+"which doesn't exist"
+msgstr ""
+"JÅ«s pateikÄte %(action)s komandÄ
parinkÄiai \"%(property)s\", kuri "
+"neegzistuoja"
+
+# ../roundup/cgi/form_parser.py:331 :357
+#: ../roundup/cgi/form_parser.py:331 ../roundup/cgi/form_parser.py:357
+#, python-format
+msgid "You have submitted more than one value for the %s property"
+msgstr "JÅ«s pateikÄte daugiau nei vienÄ
reikÅ¡mÄ parinkÄiai %s"
+
+# ../roundup/cgi/form_parser.py:354 :360
+#: ../roundup/cgi/form_parser.py:354 ../roundup/cgi/form_parser.py:360
+msgid "Password and confirmation text do not match"
+msgstr "Slaptažodis ir patvirtinimo tekstas neatitinka"
+
+#: ../roundup/cgi/form_parser.py:395
+#, python-format
+msgid "property \"%(propname)s\": \"%(value)s\" not currently in list"
+msgstr "parinkties \"%(propname)s\": \"%(value)s\" nÄra sÄ
raše"
+
+#: ../roundup/cgi/form_parser.py:512
+#, python-format
+msgid "Required %(class)s property %(property)s not supplied"
+msgid_plural "Required %(class)s properties %(property)s not supplied"
+msgstr[0] "Reikalinga %(class)s parinktis %(property)s nepateikta"
+msgstr[1] "Reikalingos %(class)s parinktys %(property)s nepateiktos"
+msgstr[2] "Reikalingos %(class)s parinktys %(property)s nepateiktos"
+
+#: ../roundup/cgi/form_parser.py:535
+msgid "File is empty"
+msgstr "Failas tuÅ¡Äias"
+
+#: ../roundup/cgi/templating.py:72
+#, python-format
+msgid "You are not allowed to %(action)s items of class %(class)s"
+msgstr ""
+"JÅ«s negalite atlikti komandos %(action)s su klasÄs %(class)s elementais"
+
+#: ../roundup/cgi/templating.py:627
+msgid "(list)"
+msgstr "(list)"
+
+#: ../roundup/cgi/templating.py:696
+msgid "Submit New Entry"
+msgstr "Ä®vesti naujÄ
įraÅ¡Ä
"
+
+# ../roundup/cgi/templating.py:700 :819 :1193 :1214 :1258 :1280 :1314 :1353
+# :1404 :1421 :1497 :1517 :1530 :1547 :1557 :1607 :1794
+#: ../roundup/cgi/templating.py:710 ../roundup/cgi/templating.py:829
+#: ../roundup/cgi/templating.py:1236 ../roundup/cgi/templating.py:1257
+#: ../roundup/cgi/templating.py:1304 ../roundup/cgi/templating.py:1327
+#: ../roundup/cgi/templating.py:1361 ../roundup/cgi/templating.py:1400
+#: ../roundup/cgi/templating.py:1453 ../roundup/cgi/templating.py:1470
+#: ../roundup/cgi/templating.py:1549 ../roundup/cgi/templating.py:1569
+#: ../roundup/cgi/templating.py:1587 ../roundup/cgi/templating.py:1619
+#: ../roundup/cgi/templating.py:1629 ../roundup/cgi/templating.py:1683
+#: ../roundup/cgi/templating.py:1875
+msgid "[hidden]"
+msgstr "[paslÄpta]"
+
+#: ../roundup/cgi/templating.py:711
+msgid "New node - no history"
+msgstr "Naujas elementas -- nÄra istorijos"
+
+#: ../roundup/cgi/templating.py:811
+msgid "Submit Changes"
+msgstr "IÅ¡saugoti pakeitimus"
+
+#: ../roundup/cgi/templating.py:893
+msgid "<em>The indicated property no longer exists</em>"
+msgstr "<em>Nurodytos parinkties nÄra</em>"
+
+#: ../roundup/cgi/templating.py:894
+#, python-format
+msgid "<em>%s: %s</em>\n"
+msgstr "<em>%s: %s</em>\n"
+
+#: ../roundup/cgi/templating.py:907
+#, python-format
+msgid "The linked class %(classname)s no longer exists"
+msgstr "Susietos klasÄs %(classname)s nebÄra"
+
+# ../roundup/cgi/templating.py:930 :951
+#: ../roundup/cgi/templating.py:940 ../roundup/cgi/templating.py:964
+msgid "<strike>The linked node no longer exists</strike>"
+msgstr "<strike>Susieto elemento nebÄra</strike>"
+
+# ../roundup/cgi/templating.py:993 :1357 :1378 :1384
+#: ../roundup/cgi/templating.py:1006 ../roundup/cgi/templating.py:1404
+#: ../roundup/cgi/templating.py:1425 ../roundup/cgi/templating.py:1431
+msgid "No"
+msgstr "Ne"
+
+# ../roundup/cgi/templating.py:993 :1357 :1376 :1381
+#: ../roundup/cgi/templating.py:1006 ../roundup/cgi/templating.py:1404
+#: ../roundup/cgi/templating.py:1423 ../roundup/cgi/templating.py:1428
+msgid "Yes"
+msgstr "Taip"
+
+#: ../roundup/cgi/templating.py:1017
+#, python-format
+msgid "%s: (no value)"
+msgstr "%s: (no value)"
+
+#: ../roundup/cgi/templating.py:1029
+msgid ""
+"<strong><em>This event is not handled by the history display!</em></strong>"
+msgstr "<strong><em>Å is įvykis nÄra rodomas archyve!</em></strong>"
+
+#: ../roundup/cgi/templating.py:1041
+msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
+msgstr "<tr><td colspan=4><strong>Pastaba:</strong></td></tr>"
+
+#: ../roundup/cgi/templating.py:1050
+msgid "History"
+msgstr "Archyvas"
+
+#: ../roundup/cgi/templating.py:1052
+msgid "<th>Date</th>"
+msgstr "<th>Data</th>"
+
+#: ../roundup/cgi/templating.py:1053
+msgid "<th>User</th>"
+msgstr "<th>Vartotojas</th>"
+
+#: ../roundup/cgi/templating.py:1054
+msgid "<th>Action</th>"
+msgstr "<th>Veiksmas</th>"
+
+#: ../roundup/cgi/templating.py:1055
+msgid "<th>Args</th>"
+msgstr "<th>Argumentai</th>"
+
+#: ../roundup/cgi/templating.py:1097
+#, python-format
+msgid "Copy of %(class)s %(id)s"
+msgstr "%(class)s %(id)s kopija"
+
+#: ../roundup/cgi/templating.py:1331
+msgid "*encrypted*"
+msgstr "*užkoduota*"
+
+#: ../roundup/cgi/templating.py:1514
+msgid ""
+"default value for DateHTMLProperty must be either DateHTMLProperty or string "
+"date representation."
+msgstr ""
+"standartinÄ DateHTMLProperty reikÅ¡mÄ turi bÅ«ti arba DateHTMLProperty arba "
+"datos reprezentacija kaip simbolių eilutÄs."
+
+#: ../roundup/cgi/templating.py:1674
+#, python-format
+msgid "Attempt to look up %(attr)s on a missing value"
+msgstr "BandÄte pažiÅ«rÄti %(attr)s neegzistuojanÄiai reikÅ¡mei"
+
+#: ../roundup/cgi/templating.py:1750
+#, python-format
+msgid "<option %svalue=\"-1\">- no selection -</option>"
+msgstr "<option %svalue=\"-1\">- nepasirinkta -</option>"
+
+#: ../roundup/date.py:186
+msgid ""
+"Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or "
+"\"yyyy-mm-dd.HH:MM:SS.SSS\""
+msgstr ""
+"Ne data, nurodykite: âyyyy-mm-ddâ, âmm-ddâ, âHH:MMâ, âHH:MM:SSâ ar âyyyy-mm-"
+"dd.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 ne data / datos formatas âyyyy-mm-ddâ, âmm-ddâ, âHH:MMâ, âHH:MM:SSâ ar "
+"âyyyy-mm-dd.HH:MM:SS.SSSâ"
+
+#: ../roundup/date.py:538
+msgid ""
+"Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
+msgstr ""
+"Ne intervalas, nurodyti: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [datos "
+"formatas]"
+
+#: ../roundup/date.py:557
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+msgstr "Ne intervalas, formatas: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+
+#: ../roundup/date.py:694
+#, python-format
+msgid "%(number)s year"
+msgid_plural "%(number)s years"
+msgstr[0] "%(number)s metai"
+msgstr[1] "%(number)s metai"
+msgstr[2] "%(number)s metų"
+
+#: ../roundup/date.py:698
+#, python-format
+msgid "%(number)s month"
+msgid_plural "%(number)s months"
+msgstr[0] "%(number)s mÄnuo"
+msgstr[1] "%(number)s mÄnesiai"
+msgstr[2] "%(number)s mÄnesių"
+
+#: ../roundup/date.py:702
+#, python-format
+msgid "%(number)s week"
+msgid_plural "%(number)s weeks"
+msgstr[0] "%(number)s savaitÄ"
+msgstr[1] "%(number)s savaitÄs"
+msgstr[2] "%(number)s savaiÄių"
+
+#: ../roundup/date.py:706
+#, python-format
+msgid "%(number)s day"
+msgid_plural "%(number)s days"
+msgstr[0] "%(number)s diena"
+msgstr[1] "%(number)s dienos"
+msgstr[2] "%(number)s dienų"
+
+#: ../roundup/date.py:710
+msgid "tomorrow"
+msgstr "rytoj"
+
+#: ../roundup/date.py:712
+msgid "yesterday"
+msgstr "vakar"
+
+#: ../roundup/date.py:715
+#, python-format
+msgid "%(number)s hour"
+msgid_plural "%(number)s hours"
+msgstr[0] "%(number)s valanda"
+msgstr[1] "%(number)s valandos"
+msgstr[2] "%(number)s valandų"
+
+#: ../roundup/date.py:719
+msgid "an hour"
+msgstr "valanda"
+
+#: ../roundup/date.py:721
+msgid "1 1/2 hours"
+msgstr "1 1/2 valandos"
+
+#: ../roundup/date.py:723
+#, python-format
+msgid "1 %(number)s/4 hours"
+msgid_plural "1 %(number)s/4 hours"
+msgstr[0] "1 %(number)s/4 valandos"
+msgstr[1] "1 %(number)s/4 valandos"
+msgstr[2] "1 %(number)s/4 valandos"
+
+#: ../roundup/date.py:727
+msgid "in a moment"
+msgstr "už minutÄs"
+
+#: ../roundup/date.py:729
+msgid "just now"
+msgstr "kÄ
tik"
+
+#: ../roundup/date.py:732
+msgid "1 minute"
+msgstr "1 minutÄ"
+
+#: ../roundup/date.py:735
+#, python-format
+msgid "%(number)s minute"
+msgid_plural "%(number)s minutes"
+msgstr[0] "%(number)s minutÄ"
+msgstr[1] "%(number)s minutÄs"
+msgstr[2] "%(number)s minuÄių"
+
+#: ../roundup/date.py:738
+msgid "1/2 an hour"
+msgstr "1/2 valandos"
+
+#: ../roundup/date.py:740
+#, python-format
+msgid "%(number)s/4 hour"
+msgid_plural "%(number)s/4 hours"
+msgstr[0] "%(number)s/4 valandos"
+msgstr[1] "%(number)s/4 valandos"
+msgstr[2] "%(number)s/4 valandos"
+
+#: ../roundup/date.py:744
+#, python-format
+msgid "%s ago"
+msgstr "prieš %s"
+
+#: ../roundup/date.py:746
+#, python-format
+msgid "in %s"
+msgstr "po %s"
+
+#: ../roundup/init.py:134
+#, python-format
+msgid ""
+"WARNING: directory '%s'\n"
+"\tcontains old-style template - ignored"
+msgstr ""
+"PERSPÄJIMAS: direktorijoje '%s'\n"
+"\tseno tipo Å¡ablonas, praleistas"
+
+#: ../roundup/roundupdb.py:141
+msgid "files"
+msgstr "failai"
+
+#: ../roundup/roundupdb.py:141
+msgid "messages"
+msgstr "pranešimai"
+
+#: ../roundup/roundupdb.py:141
+msgid "nosy"
+msgstr "informuoti"
+
+#: ../roundup/roundupdb.py:141
+msgid "superseder"
+msgstr "pirmtakas"
+
+#: ../roundup/roundupdb.py:141
+msgid "title"
+msgstr "antraÅ¡tÄ"
+
+#: ../roundup/roundupdb.py:142
+msgid "assignedto"
+msgstr "priskirta"
+
+#: ../roundup/roundupdb.py:142
+msgid "priority"
+msgstr "prioritetas"
+
+#: ../roundup/roundupdb.py:142
+msgid "status"
+msgstr "statusas"
+
+#: ../roundup/roundupdb.py:142
+msgid "topic"
+msgstr "tema"
+
+#: ../roundup/roundupdb.py:145
+msgid "activity"
+msgstr "veiksmas"
+
+#. following properties are common for all hyperdb classes
+#. they are listed here to keep things in one place
+#: ../roundup/roundupdb.py:145
+msgid "actor"
+msgstr "veikÄjas"
+
+#: ../roundup/roundupdb.py:145
+msgid "creation"
+msgstr "sukūrimas"
+
+#: ../roundup/roundupdb.py:145
+msgid "creator"
+msgstr "kÅ«rÄjas"
+
+#: ../roundup/scripts/roundup_demo.py:32
+#, python-format
+msgid "Enter directory path to create demo tracker [%s]: "
+msgstr "Ä®veskite keliÄ
į direktorijÄ
demo track'erio sukūrimui [%s]: "
+
+#: ../roundup/scripts/roundup_gettext.py:22
+#, python-format
+msgid "Usage: %(program)s <tracker home>"
+msgstr "Naudojimas: %(program)s <tracker'io namų direktorija>"
+
+#: ../roundup/scripts/roundup_gettext.py:37
+#, python-format
+msgid "No tracker templates found in directory %s"
+msgstr "Direktorijoje %s nÄra tracker'io Å¡ablonų"
+
+#: ../roundup/scripts/roundup_mailgw.py:36
+#, python-format
+msgid ""
+"Usage: %(program)s [-v] [-c] [[-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"
+"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 ""
+"Naudojimas: %(program)s [-v] [-c] [[-C klasÄ] -S laukas=reikÅ¡mÄ]* <namų "
+"direktorija> [metodas]\n"
+"\n"
+"Parinktys:\n"
+" -v: iÅ¡spausdinti vesijÄ
ir baigti\n"
+" -c: stadartinÄ kuriamo elemento klasÄ (arba tracker'io MAIL_DEFAULT_CLASS)\n"
+" -C / -S: žiÅ«rÄti žemiau\n"
+"\n"
+"Roundup pašto vartai gali būti iškviesti vienu iš keturių būdų:\n"
+" . su namų direktorija kaip vieninteliu argumentu,\n"
+" . su namų direktorija ir pašto spool failu,\n"
+" . su namų direktorija ir POP/APOP serverio abonentu, ar\n"
+" . su namų direktorija ir IMAP/IMAPS serverio abonentu.\n"
+"\n"
+"Ši komanda taipogi palaiko neprivalomus -C ir -S argumentus, kurie leidžia\n"
+"nustatyti laukus klasei, sukurtai roundup-mailgw. StandartinÄ klasÄ, jei\n"
+"kitaip nenurodyta yra msg, taÄiau kitos klasÄs: issue, file, user taip pat\n"
+"gali bÅ«ti naudojamos. -S ar --set parinktys naudoja tÄ
paÄiÄ
\n"
+"parinktis=reikÅ¡mÄ[;pariktis=reikÅ¡mÄ] notacijÄ
, priimamÄ
roundup komandinÄs\n"
+"eilutÄs komandos ar komandų, kurios gali bÅ«ti duodamos el. paÅ¡to Temos\n"
+"eilutÄje.\n"
+"\n"
+"Å i komanda leidžia nustatyti praneÅ¡imo tipÄ
kiekvienam skirtingam\n"
+"el. pašto adresui.\n"
+"\n"
+"PIPE:\n"
+" Pirmu atveju, paÅ¡to vartai nuskaito vienÄ
praneÅ¡imÄ
iÅ¡ standartinÄs\n"
+" įvesties ir pateikia tÄ
praneÅ¡imÄ
roundup.mailgw moduliui.\n"
+"\n"
+"UNIX paÅ¡to dÄžutÄ:\n"
+" Antru atveju, vartai nuskaito visus pranešimus iš pašto spool failo\n"
+" ir pateikia kiekvienÄ
iÅ¡ eilÄs roundup.mailgw moduliui. Failas yra\n"
+" iÅ¡valomas kai visi praneÅ¡imai sÄkmingai perduodami. Failas nurodomas\n"
+" kaip:\n"
+" dÄžutÄ /kelias/iki/dÄžutÄs\n"
+"\n"
+"POP:\n"
+" TreÄiu atveju, vartai nuskaito visus praneÅ¡imus iÅ¡ nurodyto POP\n"
+" serverio ir pateikia kiekvienÄ
iÅ¡ eilÄs roundup.mailgw moduliui.\n"
+" Serveris yra nurodomas kaip:\n"
+" pop vartotojas:slaptažodis at serveris\n"
+" Vartotojo vardas ir slaptažodis gali būti praleisti:\n"
+" pop vartotojas at serveris\n"
+" pop serveris\n"
+" yra tinkami formatai. Vartotojo vardo ar/ir slaptažodžio sistema paprašys,\n"
+" jei jų nepateikÄte komandinÄje eilutÄje.\n"
+"\n"
+"APOP:\n"
+" Taip pat kaip POP, taÄiau naudojant Authenticated POP:\n"
+" apop vartotojas:slaptažodis at serveris\n"
+"\n"
+"IMAP:\n"
+" Prisijungimas prie IMAP serverio. Tai palaiko tÄ
paÄiÄ
notacijÄ
\n"
+" kaip POP pašto.\n"
+" imap vartotojas:slaptažodis at serveris\n"
+" Tai taip pat leidžia nurodyti kitÄ
paÅ¡to dÄžutÄ nei INBOX naudojant šį\n"
+" formatÄ
:\n"
+" imap vartotojas:slaptažodis at serveris dÄžutÄ\n"
+"\n"
+"IMAPS:\n"
+" Prisijungimas prie IMAP serverio per ssl.\n"
+" Palaiko tÄ
paÄia notacijÄ
kaip IMAP.\n"
+" imaps vartotojas:slaptažodis at serveris [dÄžutÄ]\n"
+"\n"
+
+#: ../roundup/scripts/roundup_mailgw.py:147
+msgid "Error: not enough source specification information"
+msgstr "Klaida: nepakankamai Å¡altinio specifikacijos informacijos"
+
+#: ../roundup/scripts/roundup_mailgw.py:163
+msgid "Error: pop specification not valid"
+msgstr "Klaida: pop specifikacija netinkama"
+
+#: ../roundup/scripts/roundup_mailgw.py:170
+msgid "Error: apop specification not valid"
+msgstr "Klaida: apop specifikacija netinkama"
+
+#: ../roundup/scripts/roundup_mailgw.py:184
+msgid ""
+"Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or "
+"\"imaps\""
+msgstr "Klaida: Å altinis turi bÅ«ti âmailboxâ, âpopâ, âapopâ, âimapâ ar â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>Roundup tracker'io indeksas</title></head>\n"
+"<body><h1>Roundup tracker'io indeksas</h1><ol>\n"
+
+#: ../roundup/scripts/roundup_server.py:287
+#, python-format
+msgid "Error: %s: %s"
+msgstr "Klaida: %s: %s"
+
+#: ../roundup/scripts/roundup_server.py:297
+msgid "WARNING: ignoring \"-g\" argument, not root"
+msgstr "PERSPÄJIMAS: \"-g\" argumentas ignoruojamas, nÄra root teisių"
+
+#: ../roundup/scripts/roundup_server.py:303
+msgid "Can't change groups - no grp module"
+msgstr "Negaliu pakeisti grupių -- nÄra grp modulio"
+
+#: ../roundup/scripts/roundup_server.py:312
+#, python-format
+msgid "Group %(group)s doesn't exist"
+msgstr "GrupÄs %(group)s nÄra"
+
+#: ../roundup/scripts/roundup_server.py:323
+msgid "Can't run as root!"
+msgstr "Negaliu paleisti root teisÄmis!"
+
+#: ../roundup/scripts/roundup_server.py:326
+msgid "WARNING: ignoring \"-u\" argument, not root"
+msgstr "PERSPÄJIMAS: \"-u\" argumentas ignoruojamas, nÄra root teisių"
+
+#: ../roundup/scripts/roundup_server.py:331
+msgid "Can't change users - no pwd module"
+msgstr "Negaliu pakesiti vartotojų - nÄra pwd modulio"
+
+#: ../roundup/scripts/roundup_server.py:340
+#, python-format
+msgid "User %(user)s doesn't exist"
+msgstr "Vartotojo %(user)s nÄra"
+
+#: ../roundup/scripts/roundup_server.py:471
+#, python-format
+msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
+msgstr "MultiprocesinÄ aplinka \"%s\" neprieinama, perjungiu į vienprocesinÄ"
+
+#: ../roundup/scripts/roundup_server.py:494
+#, python-format
+msgid "Unable to bind to port %s, port already in use."
+msgstr "Negaliu prijungti prie jungties %s, jungtis jau naudojama."
+
+#: ../roundup/scripts/roundup_server.py:562
+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 ""
+" -c <Komanda> Windows Service parinktys.\n"
+" Jei norite paleisti serverį kaip Windows Service, turite\n"
+" naudoti konfigÅ«racijos failÄ
tracker'io namų direktorijoms\n"
+" nurodyti. Žurnalo failo parinktis būtina, jei norite \n"
+" paleisti Roundup Tracker servisÄ
.\n"
+" Ä®vedÄ \"roundup-server -c help\" pamatysite Windows Services\n"
+" specifikÄ
."
+
+#: ../roundup/scripts/roundup_server.py:569
+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 ""
+" -u <UID> paleidžia Roundup žiniatinklio serverį kaip šis UID\n"
+" -g <GID> paleidžia Roundup žiniatinklio serverį kaip šis GID\n"
+" -d <PIDfile> paleidžia serverį fone ir įraÅ¡o serverio PID į failÄ
,\n"
+" nurodytÄ
PIDfaile. Parinktis -l *privalo* būti nurodyta\n"
+" jei naudojama -d."
+
+#: ../roundup/scripts/roundup_server.py:576
+#, 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 ""
+"%(message)sNaudojimas: roundup-server [parinktys] [vardas=tracker'io namu "
+"direktorija]*\n"
+"\n"
+"Parinktys:\n"
+" -v išspausdinti Roundup versijos numerį ir baigti\n"
+" -h atspausdinti šį tekstÄ
ir baigti\n"
+" -S sukurti arba atnaujinti konfigÅ«racijos failÄ
ir baigti\n"
+" -C <fvardas> naudoti konfigÅ«racijos failÄ
<fvardas>\n"
+" -n <vardas> nustatyti Roundup žiniatinklio serverio kompiuterio vardÄ
\n"
+" -p <jungtis> nustatyti jungtį stebÄjimui (standartinÄ: %(port)s)\n"
+" -l <fvardas> registruoti į failÄ
fvardas vietoj stderr/stdout\n"
+" -N registruoti klientų kompiuterių vardus vietoj IP adresų\n"
+" (daug lÄÄiau)\n"
+" -t <veiksena> multiprocesinÄ veiksena (standartas: %(mp_def)s).\n"
+" Leidžiamos reikÅ¡mÄs: %(mp_types)s.\n"
+"%(os_part)s\n"
+"\n"
+"Ilgos parinktys:\n"
+" --version išspausdinti Roundup versijos numerį ir baigti\n"
+" --help atspausdinti šį tekstÄ
ir baigti\n"
+" --save-config sukurti arba atnaujinti konfigÅ«racijos failÄ
ir baigti\n"
+" --config <fvardas> naudoti konfigÅ«racijos failÄ
<fvardas>\n"
+" Visi konfigūracijos failo [main] sekcijos nustatymai taip pat gali būti\n"
+" nurodyti kaip --<name>=<value>\n"
+"\n"
+"Pavyzdžiai:\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"
+"Konfigūracijos failo formatas:\n"
+" Roundup Serverio konfigÅ«racijos failas turi bendrÄ
.ini failų formatÄ
.\n"
+" Konfigūracijos failas, kuris sukuriamas naudojant 'roundup-server -S' \n"
+" komandÄ
, turi detalų paaiÅ¡kinimÄ
kiekvienai parinkÄiai. NaudokitÄs tuo \n"
+" failu norÄdami peržiÅ«rÄti parinkÄių apraÅ¡ymus.\n"
+"\n"
+"Kaip naudoti âvardas=tracker'io_namų_direktorijaâ:\n"
+" Å ie argumentai nustato tracker'io namų direktorijÄ
/as naudojimui.\n"
+" Vardas tai tracker'io identifikacija URL (pirma dalis URL). \n"
+" Tracker'io namų direktorija tai direktorija, kuri buvo nurodyta,\n"
+" kai paleidote âroundup-admin initâ. Galite nurodyti bet kokį skaiÄių\n"
+" 'vardas:namų_direktorija' porų komandinÄje eilutÄje. Ä®sitikinkite, kad\n"
+" varde nÄra jokių url-nesaugių simbolių, kaip tarpai, nes IE jų \n"
+" nesupras.\n"
+"\n"
+
+#: ../roundup/scripts/roundup_server.py:724
+msgid "Instances must be name=home"
+msgstr "Egzempliorius turi būti nurodomas taip: vardas=namų_direktorija"
+
+#: ../roundup/scripts/roundup_server.py:738
+#, python-format
+msgid "Configuration saved to %s"
+msgstr "Konfigūracija išsaugota %s"
+
+#: ../roundup/scripts/roundup_server.py:756
+msgid "Sorry, you can't run the server as a daemon on this Operating System"
+msgstr ""
+"JÅ«s negalite paleisti serverio kaip daemon'o Å¡ioje operacinÄje sistemoje"
+
+#: ../roundup/scripts/roundup_server.py:768
+#, python-format
+msgid "Roundup server started on %(HOST)s:%(PORT)s"
+msgstr "Roundup serveris paleistas ant %(HOST)s:%(PORT)s"
+
+#: ../templates/classic/html/_generic.collision.html:4
+#: ../templates/minimal/html/_generic.collision.html:4
+msgid "${class} Edit Collision - ${tracker}"
+msgstr "${class} Redagavimo kolizija - ${tracker}"
+
+#: ../templates/classic/html/_generic.collision.html:7
+#: ../templates/minimal/html/_generic.collision.html:7
+msgid "${class} Edit Collision"
+msgstr "${class} Redagavimo kolizija"
+
+#: ../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"
+" Ä®vyko kolizija. Kitas vartotojas atnaujino šį elementÄ
\n"
+" kol jūs redagavote. <a href='${context}'>Perkraukite</a>\n"
+" elementÄ
ir peržiÅ«rÄkite savo pakeitimus.\n"
+
+#: ../templates/classic/html/_generic.help.html:9
+#: ../templates/minimal/html/_generic.help.html:9
+msgid "${property} help - ${tracker}"
+msgstr "${property} pagalba - ${tracker}"
+
+#: ../templates/classic/html/_generic.help.html:31
+#: ../templates/minimal/html/_generic.help.html:31
+msgid " Cancel "
+msgstr " Atšaukti "
+
+#: ../templates/classic/html/_generic.help.html:34
+#: ../templates/minimal/html/_generic.help.html:34
+msgid " Apply "
+msgstr " Vykdyti "
+
+#: ../templates/classic/html/_generic.help.html:41
+#: ../templates/classic/html/issue.index.html:73
+#: ../templates/minimal/html/_generic.help.html:41
+msgid "<< previous"
+msgstr "<< pirmesnis"
+
+#: ../templates/classic/html/_generic.help.html:52
+#: ../templates/classic/html/issue.index.html:81
+#: ../templates/minimal/html/_generic.help.html:52
+msgid "${start}..${end} out of ${total}"
+msgstr "${start}..${end} iš ${total}"
+
+#: ../templates/classic/html/_generic.help.html:56
+#: ../templates/classic/html/issue.index.html:84
+#: ../templates/minimal/html/_generic.help.html:56
+msgid "next >>"
+msgstr "kitas >>"
+
+#: ../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 "${class} redagavimas - ${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 "${class} redagavimas"
+
+#: ../templates/classic/html/_generic.index.html:14
+#: ../templates/classic/html/_generic.item.html:12
+#: ../templates/classic/html/file.item.html:9
+#: ../templates/classic/html/issue.index.html:16
+#: ../templates/classic/html/issue.item.html:28
+#: ../templates/classic/html/msg.item.html:26
+#: ../templates/classic/html/user.index.html:9
+#: ../templates/classic/html/user.item.html:28
+#: ../templates/minimal/html/_generic.index.html:14
+#: ../templates/minimal/html/_generic.item.html:12
+#: ../templates/minimal/html/user.index.html:9
+#: ../templates/minimal/html/user.item.html:28
+#: ../templates/minimal/html/user.register.html:14
+msgid "You are not allowed to view this page."
+msgstr "JÅ«s neturite teisių peržiÅ«rÄti šį puslapį."
+
+#: ../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\"> JÅ«s galite redaguoti ${classname} klasÄs turinį "
+"naudodami Å¡iÄ
formÄ
. Su kableliais, naujomis eilutÄmis ir dvigubomis "
+"kabutÄmis (\") turi bÅ«ti elgiamasi atsargiai. JÅ«s galite naudoti kablelius "
+"ir naujas eilutes užkomentuodami reikÅ¡mes dvigubomis kabutÄmis (\"). "
+"Dvigubos kabutÄs turi bÅ«ti užkomentuotos dvigubinant (\"\"). </p> <p class="
+"\"form-help\"> DaugiasÄ
sajÄse parinktyse reikÅ¡mÄs atskirtos dvitaÅ¡kiu (\":"
+"\") (... ,\"one:two:three\", ...) </p> <p class=\"form-help\"> Įrašai "
+"iÅ¡trinami iÅ¡trinant jų eilutÄ. Nauji įraÅ¡ai pridedami prijungiant juos prie "
+"lentelÄs - įraÅ¡ykite X id stulpelyje. </p>"
+
+#: ../templates/classic/html/_generic.index.html:44
+#: ../templates/minimal/html/_generic.index.html:44
+msgid "Edit Items"
+msgstr "Redaguoti elementus"
+
+#: ../templates/classic/html/file.index.html:4
+msgid "List of files - ${tracker}"
+msgstr "Failų sÄ
rašas - ${tracker}"
+
+#: ../templates/classic/html/file.index.html:5
+msgid "List of files"
+msgstr "Failų sÄ
rašas"
+
+#: ../templates/classic/html/file.index.html:10
+msgid "Download"
+msgstr "Parsisiųsti"
+
+#: ../templates/classic/html/file.index.html:11
+#: ../templates/classic/html/file.item.html:22
+msgid "Content Type"
+msgstr "Turinio tipas"
+
+#: ../templates/classic/html/file.index.html:12
+msgid "Uploaded By"
+msgstr "Nusiųsta iki"
+
+#: ../templates/classic/html/file.index.html:13
+#: ../templates/classic/html/msg.item.html:43
+msgid "Date"
+msgstr "Data"
+
+#: ../templates/classic/html/file.item.html:2
+msgid "File display - ${tracker}"
+msgstr "Failų pateikimas - ${tracker}"
+
+#: ../templates/classic/html/file.item.html:4
+msgid "File display"
+msgstr "Failų pateikimas"
+
+#: ../templates/classic/html/file.item.html:18
+#: ../templates/classic/html/user.item.html:39
+#: ../templates/classic/html/user.register.html:17
+msgid "Name"
+msgstr "Vardas"
+
+#: ../templates/classic/html/file.item.html:40
+msgid "download"
+msgstr "parsisiųsti"
+
+#: ../templates/classic/html/home.classlist.html:2
+#: ../templates/minimal/html/home.classlist.html:2
+msgid "List of classes - ${tracker}"
+msgstr "Klasių sÄ
rašas - ${tracker}"
+
+#: ../templates/classic/html/home.classlist.html:4
+#: ../templates/minimal/html/home.classlist.html:4
+msgid "List of classes"
+msgstr "Klasių sÄ
rašas"
+
+#: ../templates/classic/html/issue.index.html:7
+msgid "List of issues - ${tracker}"
+msgstr "Kreipinių sÄ
rašas - ${tracker}"
+
+#: ../templates/classic/html/issue.index.html:11
+msgid "List of issues"
+msgstr "Kreipinių sÄ
rašas"
+
+#: ../templates/classic/html/issue.index.html:22
+#: ../templates/classic/html/issue.item.html:44
+msgid "Priority"
+msgstr "Prioritetas"
+
+#: ../templates/classic/html/issue.index.html:23
+msgid "ID"
+msgstr "ID"
+
+#: ../templates/classic/html/issue.index.html:24
+msgid "Creation"
+msgstr "Sukūrimas"
+
+#: ../templates/classic/html/issue.index.html:25
+msgid "Activity"
+msgstr "Veikla"
+
+#: ../templates/classic/html/issue.index.html:26
+msgid "Actor"
+msgstr "VeikÄjas"
+
+#: ../templates/classic/html/issue.index.html:27
+msgid "Topic"
+msgstr "Tema"
+
+#: ../templates/classic/html/issue.index.html:28
+#: ../templates/classic/html/issue.item.html:39
+msgid "Title"
+msgstr "Pavadinimas"
+
+#: ../templates/classic/html/issue.index.html:29
+#: ../templates/classic/html/issue.item.html:46
+msgid "Status"
+msgstr "Statusas"
+
+#: ../templates/classic/html/issue.index.html:30
+msgid "Creator"
+msgstr "SukÅ«rÄjas"
+
+#: ../templates/classic/html/issue.index.html:31
+msgid "Assigned To"
+msgstr "Priskirta"
+
+#: ../templates/classic/html/issue.index.html:97
+msgid "Download as CSV"
+msgstr "Parsisiųsti kaip CSV"
+
+#: ../templates/classic/html/issue.index.html:105
+msgid "Sort on:"
+msgstr "Išrūšiuoti pagal:"
+
+#: ../templates/classic/html/issue.index.html:108
+#: ../templates/classic/html/issue.index.html:125
+msgid "- nothing -"
+msgstr "- nieko -"
+
+#: ../templates/classic/html/issue.index.html:116
+#: ../templates/classic/html/issue.index.html:133
+msgid "Descending:"
+msgstr "MažÄjanÄia tvarka:"
+
+#: ../templates/classic/html/issue.index.html:122
+msgid "Group on:"
+msgstr "Grupuoti pagal:"
+
+#: ../templates/classic/html/issue.index.html:139
+msgid "Redisplay"
+msgstr "PerpaiÅ¡yti vaizdÄ
"
+
+#: ../templates/classic/html/issue.item.html:7
+msgid "Issue ${id}: ${title} - ${tracker}"
+msgstr "Kreipinys ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:10
+msgid "New Issue - ${tracker}"
+msgstr "Naujas kreipinys - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:14
+msgid "New Issue"
+msgstr "Naujas kreipinys"
+
+#: ../templates/classic/html/issue.item.html:16
+msgid "New Issue Editing"
+msgstr "Naujo kreipinio redagavimas"
+
+#: ../templates/classic/html/issue.item.html:19
+msgid "Issue${id}"
+msgstr "Kreipinys${id}"
+
+#: ../templates/classic/html/issue.item.html:22
+msgid "Issue${id} Editing"
+msgstr "Kreipinio${id} redagavimas"
+
+#: ../templates/classic/html/issue.item.html:51
+msgid "Superseder"
+msgstr "Pirmtakas"
+
+#: ../templates/classic/html/issue.item.html:56
+msgid "View: ${link}"
+msgstr "ŽiÅ«rÄti: ${link}"
+
+#: ../templates/classic/html/issue.item.html:60
+msgid "Nosy List"
+msgstr "SÄ
rašas informuoti"
+
+#: ../templates/classic/html/issue.item.html:69
+msgid "Assigned To"
+msgstr "Priskirta"
+
+#: ../templates/classic/html/issue.item.html:71
+msgid "Topics"
+msgstr "Temos"
+
+#: ../templates/classic/html/issue.item.html:79
+msgid "Change Note"
+msgstr "Pakeitimų pastabos"
+
+#: ../templates/classic/html/issue.item.html:87
+msgid "File"
+msgstr "Failas"
+
+#: ../templates/classic/html/issue.item.html:99
+msgid "Make a copy"
+msgstr "Kopijuoti"
+
+#: ../templates/classic/html/issue.item.html:107
+#: ../templates/classic/html/user.item.html:106
+#: ../templates/classic/html/user.register.html:69
+#: ../templates/minimal/html/user.item.html:86
+msgid ""
+"<table class=\"form\"> <tr> <td>Note: </td> <th class=\"required"
+"\">highlighted</th> <td> fields are required.</td> </tr> </table>"
+msgstr ""
+"<table class=\"form\"> <tr> <td>Pastaba: </td> <th class=\"required"
+"\">pažymÄti</th> <td> laukai yra privalomi.</td> </tr> </table>"
+
+#: ../templates/classic/html/issue.item.html:121
+msgid ""
+"Created on <b>${creation}</b> by <b>${creator}</b>, last changed <b>"
+"${activity}</b> by <b>${actor}</b>."
+msgstr ""
+"Sukurta <b>${creation}</b> <b>${creator}</b>, paskutinį kartÄ
keista <b>"
+"${activity}</b> <b>${actor}</b>."
+
+#: ../templates/classic/html/issue.item.html:125
+#: ../templates/classic/html/msg.item.html:56
+msgid "Files"
+msgstr "Failai"
+
+#: ../templates/classic/html/issue.item.html:127
+#: ../templates/classic/html/msg.item.html:58
+msgid "File name"
+msgstr "Failo vardas"
+
+#: ../templates/classic/html/issue.item.html:128
+#: ../templates/classic/html/msg.item.html:59
+msgid "Uploaded"
+msgstr "Nusiųsta"
+
+#: ../templates/classic/html/issue.item.html:129
+msgid "Type"
+msgstr "Tipas"
+
+#: ../templates/classic/html/issue.item.html:130
+#: ../templates/classic/html/query.edit.html:30
+msgid "Edit"
+msgstr "Redaguoti"
+
+#: ../templates/classic/html/issue.item.html:131
+msgid "Remove"
+msgstr "Pašalinti"
+
+#: ../templates/classic/html/issue.item.html:151
+#: ../templates/classic/html/issue.item.html:172
+#: ../templates/classic/html/query.edit.html:50
+msgid "remove"
+msgstr "pašalinti"
+
+#: ../templates/classic/html/issue.item.html:158
+#: ../templates/classic/html/msg.index.html:9
+msgid "Messages"
+msgstr "Pranešimai"
+
+#: ../templates/classic/html/issue.item.html:162
+msgid "msg${id} (view)"
+msgstr "msg${id} (view)"
+
+#: ../templates/classic/html/issue.item.html:163
+msgid "Author: ${author}"
+msgstr "Autorius: ${author}"
+
+#: ../templates/classic/html/issue.item.html:165
+msgid "Date: ${date}"
+msgstr "Data: ${date}"
+
+#: ../templates/classic/html/issue.search.html:2
+msgid "Issue searching - ${tracker}"
+msgstr "Kreipinių paieška - ${tracker}"
+
+#: ../templates/classic/html/issue.search.html:4
+msgid "Issue searching"
+msgstr "Kreipinių paieška"
+
+#: ../templates/classic/html/issue.search.html:25
+msgid "Filter on"
+msgstr "Filtruoti pagal"
+
+#: ../templates/classic/html/issue.search.html:26
+msgid "Display"
+msgstr "Parodyti"
+
+#: ../templates/classic/html/issue.search.html:27
+msgid "Sort on"
+msgstr "RÅ«Å¡iuoti pagal"
+
+#: ../templates/classic/html/issue.search.html:28
+msgid "Group on"
+msgstr "Grupuoti pagal"
+
+#: ../templates/classic/html/issue.search.html:32
+msgid "All text*:"
+msgstr "Visas tekstas*:"
+
+#: ../templates/classic/html/issue.search.html:40
+msgid "Title:"
+msgstr "Pavadinimas:"
+
+#: ../templates/classic/html/issue.search.html:50
+msgid "Topic:"
+msgstr "Tema:"
+
+#: ../templates/classic/html/issue.search.html:58
+msgid "ID:"
+msgstr "ID:"
+
+#: ../templates/classic/html/issue.search.html:66
+msgid "Creation Date:"
+msgstr "Sukūrimo data:"
+
+#: ../templates/classic/html/issue.search.html:77
+msgid "Creator:"
+msgstr "KÅ«rÄjas:"
+
+#: ../templates/classic/html/issue.search.html:79
+msgid "created by me"
+msgstr "mano sukurta"
+
+#: ../templates/classic/html/issue.search.html:88
+msgid "Activity:"
+msgstr "Veikla:"
+
+#: ../templates/classic/html/issue.search.html:99
+msgid "Actor:"
+msgstr "VeikÄjas:"
+
+#: ../templates/classic/html/issue.search.html:101
+msgid "done by me"
+msgstr "mano atlikta"
+
+#: ../templates/classic/html/issue.search.html:112
+msgid "Priority:"
+msgstr "Prioritetas:"
+
+#: ../templates/classic/html/issue.search.html:114
+#: ../templates/classic/html/issue.search.html:130
+msgid "not selected"
+msgstr "nepasirinkta"
+
+#: ../templates/classic/html/issue.search.html:125
+msgid "Status:"
+msgstr "Statusas:"
+
+#: ../templates/classic/html/issue.search.html:128
+msgid "not resolved"
+msgstr "neiÅ¡sprÄsta"
+
+#: ../templates/classic/html/issue.search.html:143
+msgid "Assigned to:"
+msgstr "Priskirta:"
+
+#: ../templates/classic/html/issue.search.html:146
+msgid "assigned to me"
+msgstr "priskirta man"
+
+#: ../templates/classic/html/issue.search.html:148
+msgid "unassigned"
+msgstr "nepriskirta"
+
+#: ../templates/classic/html/issue.search.html:158
+msgid "No Sort or group:"
+msgstr "NÄra rÅ«Å¡iavimo ar grupavimo:"
+
+#: ../templates/classic/html/issue.search.html:166
+msgid "Pagesize:"
+msgstr "Puslapio dydis:"
+
+#: ../templates/classic/html/issue.search.html:172
+msgid "Start With:"
+msgstr "PradÄti nuo:"
+
+#: ../templates/classic/html/issue.search.html:178
+msgid "Sort Descending:"
+msgstr "RÅ«Å¡iuoti mažÄjanÄia tvarka:"
+
+#: ../templates/classic/html/issue.search.html:185
+msgid "Group Descending:"
+msgstr "Grupuoti mažÄjanÄia tvarka:"
+
+#: ../templates/classic/html/issue.search.html:192
+msgid "Query name**:"
+msgstr "Užklausos vardas**:"
+
+#: ../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
+msgid "Search"
+msgstr "Paieška"
+
+#: ../templates/classic/html/issue.search.html:209
+msgid "*: The \"all text\" field will look in message bodies and issue titles"
+msgstr ""
+"*: Laukas âvisas tekstasâ iÅ¡Å¡oks praneÅ¡imų tekste ir kreipinių "
+"pavadinimuose"
+
+#: ../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 ""
+"**: Jei jÅ«s pateiksite vardÄ
, užklausa bus išsaugota "
+"ir prieinama kaip nuoroda Å¡oninÄje juostoje"
+
+#: ../templates/classic/html/keyword.item.html:3
+msgid "Keyword editing - ${tracker}"
+msgstr "Raktinių žodžių redagavimas - ${tracker}"
+
+#: ../templates/classic/html/keyword.item.html:5
+msgid "Keyword editing"
+msgstr "Raktinių žodžių redagavimas"
+
+#: ../templates/classic/html/keyword.item.html:11
+msgid "Existing Keywords"
+msgstr "Esami raktiniai žodžiai"
+
+#: ../templates/classic/html/keyword.item.html:20
+msgid ""
+"To edit an existing keyword (for spelling or typing errors), click on its "
+"entry above."
+msgstr ""
+"Esamų raktinių žodžių redagavimui (rašybos klaidos ir netikslumai), "
+"spustelkite ant atitinkamo įrašo viršuje."
+
+#: ../templates/classic/html/keyword.item.html:27
+msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
+msgstr ""
+"Tam, kad sukurtumÄte raktinį žodį, įveskite jį apaÄioje ir paspauskite "
+"âÄ®vesti naujÄ
įraÅ¡Ä
â."
+
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
+msgstr "Raktinis žodis"
+
+#: ../templates/classic/html/msg.index.html:3
+msgid "List of messages - ${tracker}"
+msgstr "PraneÅ¡imų sÄ
rašas - ${tracker}"
+
+#: ../templates/classic/html/msg.index.html:5
+msgid "Message listing"
+msgstr "PraneÅ¡imų sÄ
rašas"
+
+#: ../templates/classic/html/msg.item.html:6
+msgid "Message ${id} - ${tracker}"
+msgstr "Pranešimas ${id} - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:9
+msgid "New Message - ${tracker}"
+msgstr "Naujas pranešimas - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:13
+msgid "New Message"
+msgstr "Naujas pranešimas"
+
+#: ../templates/classic/html/msg.item.html:15
+msgid "New Message Editing"
+msgstr "Naujo pranešimo redagavimas"
+
+#: ../templates/classic/html/msg.item.html:18
+msgid "Message${id}"
+msgstr "Pranešimas${id}"
+
+#: ../templates/classic/html/msg.item.html:21
+msgid "Message${id} Editing"
+msgstr "Pranešimo${id} redagavimas"
+
+#: ../templates/classic/html/msg.item.html:33
+msgid "Author"
+msgstr "Autorius"
+
+#: ../templates/classic/html/msg.item.html:38
+msgid "Recipients"
+msgstr "GavÄjai"
+
+#: ../templates/classic/html/msg.item.html:49
+msgid "Content"
+msgstr "Turinys"
+
+#: ../templates/classic/html/page.html:41
+msgid "<b>Your Queries</b> (<a href=\"query?@template=edit\">edit</a>)"
+msgstr "<b>Jūsų užklausos</b> (<a href=\"query?@template=edit\">redaguoti</a>)"
+
+#: ../templates/classic/html/page.html:52
+msgid "Issues"
+msgstr "Kreipiniai"
+
+#: ../templates/classic/html/page.html:54
+#: ../templates/classic/html/page.html:74
+msgid "Create New"
+msgstr "Sukurti naujÄ
"
+
+#: ../templates/classic/html/page.html:56
+msgid "Show Unassigned"
+msgstr "Rodyti nepriskirtus"
+
+#: ../templates/classic/html/page.html:58
+msgid "Show All"
+msgstr "Rodyti visus"
+
+#: ../templates/classic/html/page.html:61
+msgid "Show issue:"
+msgstr "Rodyti kreipinį:"
+
+#: ../templates/classic/html/page.html:72
+msgid "Keywords"
+msgstr "Raktiniai žodžiai"
+
+#: ../templates/classic/html/page.html:78
+msgid "Edit Existing"
+msgstr "Redaguoti esamus"
+
+#: ../templates/classic/html/page.html:84
+#: ../templates/minimal/html/page.html:65
+msgid "Administration"
+msgstr "Administravimas"
+
+#: ../templates/classic/html/page.html:86
+#: ../templates/minimal/html/page.html:66
+msgid "Class List"
+msgstr "Klasių sÄ
rašas"
+
+#: ../templates/classic/html/page.html:90
+#: ../templates/minimal/html/page.html:68
+msgid "User List"
+msgstr "Vartotojų sÄ
rašas"
+
+#: ../templates/classic/html/page.html:92
+#: ../templates/minimal/html/page.html:71
+msgid "Add User"
+msgstr "PridÄti vartotojÄ
"
+
+#: ../templates/classic/html/page.html:99
+#: ../templates/classic/html/page.html:105
+#: ../templates/minimal/html/page.html:46
+msgid "Login"
+msgstr "Prisijungti"
+
+#: ../templates/classic/html/page.html:104
+#: ../templates/minimal/html/page.html:45
+msgid "Remember me?"
+msgstr "Prisiminti mane?"
+
+#: ../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 "Užsiregistruoti"
+
+#: ../templates/classic/html/page.html:111
+msgid "Lost your login?"
+msgstr "PamirÅ¡ote savo vartotojo vardÄ
?"
+
+#: ../templates/classic/html/page.html:116
+msgid "Hello, ${user}"
+msgstr "Sveiki, ${user}"
+
+#: ../templates/classic/html/page.html:118
+msgid "Your Issues"
+msgstr "Jūsų kreipiniai"
+
+#: ../templates/classic/html/page.html:119
+#: ../templates/minimal/html/page.html:57
+msgid "Your Details"
+msgstr "SmulkesnÄ informacija apie jus"
+
+#: ../templates/classic/html/page.html:121
+#: ../templates/minimal/html/page.html:59
+msgid "Logout"
+msgstr "Atsijungti"
+
+#: ../templates/classic/html/page.html:125
+msgid "Help"
+msgstr "Pagalba"
+
+#: ../templates/classic/html/page.html:126
+msgid "Roundup docs"
+msgstr "Roundup dokumentacija"
+
+#: ../templates/classic/html/page.html:136
+#: ../templates/minimal/html/page.html:81
+msgid "clear this message"
+msgstr "iÅ¡valyti šį praneÅ¡imÄ
"
+
+#: ../templates/classic/html/page.html:181
+msgid "don't care"
+msgstr "nesvarbu"
+
+#: ../templates/classic/html/page.html:183
+msgid "------------"
+msgstr "------------"
+
+#: ../templates/classic/html/page.html:210
+msgid "no value"
+msgstr "nÄra reikÅ¡mÄs"
+
+#: ../templates/classic/html/query.edit.html:4
+msgid "\"Your Queries\" Editing - ${tracker}"
+msgstr "âJÅ«sų užklausosâ redagavimas - ${tracker}"
+
+#: ../templates/classic/html/query.edit.html:6
+msgid "\"Your Queries\" Editing"
+msgstr "âJÅ«sų užklausosâ redagavimas"
+
+#: ../templates/classic/html/query.edit.html:11
+msgid "You are not allowed to edit queries."
+msgstr "Jūs neturite teisių redaguoti užklausas."
+
+#: ../templates/classic/html/query.edit.html:28
+msgid "Query"
+msgstr "Užklausa"
+
+#: ../templates/classic/html/query.edit.html:29
+msgid "Include in \"Your Queries\""
+msgstr "Ä®terpkite į âJÅ«sų užklausosâ"
+
+#: ../templates/classic/html/query.edit.html:31
+msgid "Private to you?"
+msgstr "Privatus?"
+
+#: ../templates/classic/html/query.edit.html:44
+msgid "leave out"
+msgstr "neįtraukti"
+
+#: ../templates/classic/html/query.edit.html:45
+msgid "include"
+msgstr "įtraukti"
+
+#: ../templates/classic/html/query.edit.html:49
+msgid "leave in"
+msgstr "palikti įtrauktÄ
"
+
+#: ../templates/classic/html/query.edit.html:54
+msgid "[query is retired]"
+msgstr "[užklausa deaktyvuota]"
+
+#: ../templates/classic/html/query.edit.html:67
+#: ../templates/classic/html/query.edit.html:92
+msgid "edit"
+msgstr "redaguoti"
+
+#: ../templates/classic/html/query.edit.html:71
+msgid "yes"
+msgstr "taip"
+
+#: ../templates/classic/html/query.edit.html:73
+msgid "no"
+msgstr "ne"
+
+#: ../templates/classic/html/query.edit.html:79
+msgid "Delete"
+msgstr "IÅ¡trinti"
+
+#: ../templates/classic/html/query.edit.html:94
+msgid "[not yours to edit]"
+msgstr "[negalite redaguoti]"
+
+#: ../templates/classic/html/query.edit.html:102
+msgid "Save Selection"
+msgstr "IÅ¡saugoti atrankÄ
"
+
+#: ../templates/classic/html/user.forgotten.html:3
+msgid "Password reset request - ${tracker}"
+msgstr "Slaptažodžio atstatymo užklausa - ${tracker}"
+
+#: ../templates/classic/html/user.forgotten.html:5
+msgid "Password reset request"
+msgstr "Slaptažodžio atstatymo užklausa"
+
+#: ../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 ""
+"Jūs turite du pasirinkimus jei pamiršote savo slaptažodį. Jei žinote el. "
+"paÅ¡to adresÄ
, kuriuo prisiregistravote, įveskite jį žemiau."
+
+#: ../templates/classic/html/user.forgotten.html:16
+msgid "Email Address:"
+msgstr "El. pašto adresas"
+
+#: ../templates/classic/html/user.forgotten.html:24
+#: ../templates/classic/html/user.forgotten.html:34
+msgid "Request password reset"
+msgstr "Prašyti slaptažodžio atstatymo"
+
+#: ../templates/classic/html/user.forgotten.html:30
+msgid "Or, if you know your username, then enter it below."
+msgstr "Arba, jei žinote vartotojo vardÄ
, įveskite jį žemiau."
+
+#: ../templates/classic/html/user.forgotten.html:33
+msgid "Username:"
+msgstr "Vartotojo vardas:"
+
+#: ../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 ""
+"Jums bus išsiųstas patvirtinimo el. laiškas - sekite jame duotas "
+"instrukcijas, kad pabaigtumÄte atstatymo procesÄ
"
+
+#: ../templates/classic/html/user.index.html:3
+#: ../templates/minimal/html/user.index.html:3
+msgid "User listing - ${tracker}"
+msgstr "Vartotojų sÄ
rašas - ${tracker}"
+
+#: ../templates/classic/html/user.index.html:5
+#: ../templates/minimal/html/user.index.html:5
+msgid "User listing"
+msgstr "Vartotojų sÄ
rašas"
+
+#: ../templates/classic/html/user.index.html:14
+#: ../templates/minimal/html/user.index.html:14
+msgid "Username"
+msgstr "Vartotojo vardas"
+
+#: ../templates/classic/html/user.index.html:15
+msgid "Real name"
+msgstr "Tikras vardas"
+
+#: ../templates/classic/html/user.index.html:16
+#: ../templates/classic/html/user.item.html:70
+#: ../templates/classic/html/user.register.html:45
+msgid "Organisation"
+msgstr "Organizacija"
+
+#: ../templates/classic/html/user.index.html:17
+#: ../templates/minimal/html/user.index.html:15
+msgid "Email address"
+msgstr "El. pašto adresas"
+
+#: ../templates/classic/html/user.index.html:18
+msgid "Phone number"
+msgstr "Telefono numeris"
+
+#: ../templates/classic/html/user.index.html:19
+msgid "Retire"
+msgstr "Deaktyvuoti"
+
+#: ../templates/classic/html/user.index.html:32
+msgid "retire"
+msgstr "deaktyvuoti"
+
+#: ../templates/classic/html/user.item.html:7
+#: ../templates/minimal/html/user.item.html:7
+msgid "User ${id}: ${title} - ${tracker}"
+msgstr "Vartotojas ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:10
+#: ../templates/minimal/html/user.item.html:10
+msgid "New User - ${tracker}"
+msgstr "Naujas vartotojas - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:14
+#: ../templates/minimal/html/user.item.html:14
+msgid "New User"
+msgstr "Naujas vartotojas"
+
+#: ../templates/classic/html/user.item.html:16
+#: ../templates/minimal/html/user.item.html:16
+msgid "New User Editing"
+msgstr "Naujo vartotojo redagavimas"
+
+#: ../templates/classic/html/user.item.html:19
+#: ../templates/minimal/html/user.item.html:19
+msgid "User${id}"
+msgstr "Vartotojas${id}"
+
+#: ../templates/classic/html/user.item.html:22
+#: ../templates/minimal/html/user.item.html:22
+msgid "User${id} Editing"
+msgstr "Vartotojo${id} redagavimas"
+
+#: ../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 "Prisijungimo vardas"
+
+#: ../templates/classic/html/user.item.html:47
+#: ../templates/classic/html/user.register.html:25
+#: ../templates/minimal/html/user.item.html:44
+#: ../templates/minimal/html/user.register.html:30
+msgid "Login Password"
+msgstr "Prisijungimo slaptažodis"
+
+#: ../templates/classic/html/user.item.html:51
+#: ../templates/classic/html/user.register.html:29
+#: ../templates/minimal/html/user.item.html:48
+#: ../templates/minimal/html/user.register.html:34
+msgid "Confirm Password"
+msgstr "Patvirtinti slaptažodį"
+
+#: ../templates/classic/html/user.item.html:55
+#: ../templates/classic/html/user.register.html:33
+#: ../templates/minimal/html/user.item.html:52
+#: ../templates/minimal/html/user.register.html:38
+msgid "Roles"
+msgstr "RolÄs"
+
+#: ../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 ""
+"(jei norite vartotojui priskirti daugiau nei vienÄ
rolÄ, įveskite kableliais,"
+"atskirtÄ
,sÄ
raÅ¡Ä
)"
+
+#: ../templates/classic/html/user.item.html:66
+#: ../templates/classic/html/user.register.html:41
+msgid "Phone"
+msgstr "Telefonas"
+
+#: ../templates/classic/html/user.item.html:74
+msgid "Timezone"
+msgstr "Laiko zona"
+
+#: ../templates/classic/html/user.item.html:78
+msgid "(this is a numeric hour offset, the default is ${zone})"
+msgstr "(tai yra skaitinis valandų poslinkis, standartas yra ${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 "El. pašto adresas"
+
+#: ../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 "AlternatyvÅ«s el. paÅ¡to adresai<br>Vienas adresas eilutÄje"
+
+#: ../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 "Registruotis į ${tracker}"
+
+#: ../templates/classic/html/user.rego_progress.html:4
+#: ../templates/minimal/html/user.rego_progress.html:4
+msgid "Registration in progress - ${tracker}"
+msgstr "Vyksta registracija - ${tracker}"
+
+#: ../templates/classic/html/user.rego_progress.html:6
+#: ../templates/minimal/html/user.rego_progress.html:6
+msgid "Registration in progress..."
+msgstr "Vyksta registracija..."
+
+#: ../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 ""
+"Netrukus jÅ«s gausite el. laiÅ¡kÄ
su jūsų registracijos patvirtinimu. kad "
+"pabaigtumÄte registracijoc procesÄ
, sekite nuorodÄ
atsiųstame laiške."
+
+#: ../templates/minimal/html/home.html:2
+msgid "Tracker home - ${tracker}"
+msgstr "Tracker'io namų direktorija - ${tracker}"
+
+#: ../templates/minimal/html/home.html:4
+msgid "Tracker home"
+msgstr "Tracker'io namų direktorija"
+
+#: ../templates/minimal/html/home.html:16
+msgid "Please select from one of the menu options on the left."
+msgstr "Pasirinkite vienÄ
iÅ¡ parinkÄių iÅ¡ meniu kairÄje."
+
+#: ../templates/minimal/html/home.html:19
+msgid "Please log in or register."
+msgstr "Prisijunkite ar užsiregistruokite."
+
+#: ../templates/minimal/html/page.html:55
+msgid "Hello,<br>${user}"
+msgstr "Sveiki,<br>${user}"
Added: tracker/vendor/roundup/current/locale/roundup.pot
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/locale/roundup.pot Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2562 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR See Roundup README.txt
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
+"POT-Creation-Date: 2006-02-28 07:44+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"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+
+# ../roundup/admin.py:85 :979 :1028 :1050
+#: ../roundup/admin.py:85 ../roundup/admin.py:979 ../roundup/admin.py:1028
+#: ../roundup/admin.py:1050
+#, python-format
+msgid "no such class \"%(classname)s\""
+msgstr ""
+
+# ../roundup/admin.py:95 :99
+#: ../roundup/admin.py:95 ../roundup/admin.py:99
+#, python-format
+msgid "argument \"%(arg)s\" not propname=value"
+msgstr ""
+
+#: ../roundup/admin.py:112
+#, python-format
+msgid ""
+"Problem: %(message)s\n"
+"\n"
+msgstr ""
+
+#: ../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"
+"\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:138
+msgid "Commands:"
+msgstr ""
+
+#: ../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:175
+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:238
+#, python-format
+msgid "%s:"
+msgstr ""
+
+#: ../roundup/admin.py:243
+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:266
+#, python-format
+msgid "Sorry, no help for \"%(topic)s\""
+msgstr ""
+
+# ../roundup/admin.py:338 :394
+#: ../roundup/admin.py:338 ../roundup/admin.py:394
+msgid "Templates:"
+msgstr ""
+
+# ../roundup/admin.py:341 :405
+#: ../roundup/admin.py:341 ../roundup/admin.py:405
+msgid "Back ends:"
+msgstr ""
+
+#: ../roundup/admin.py:344
+msgid ""
+"Usage: install [template [backend [admin password [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"
+"\n"
+" The last command line argument allows to pass initial values\n"
+" 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:367 :464 :525 :604 :654 :712 :733 :761 :832 :899 :970
+# :1018 :1040 :1067 :1134 :1204
+#: ../roundup/admin.py:367 ../roundup/admin.py:464 ../roundup/admin.py:525
+#: ../roundup/admin.py:604 ../roundup/admin.py:654 ../roundup/admin.py:712
+#: ../roundup/admin.py:733 ../roundup/admin.py:761 ../roundup/admin.py:832
+#: ../roundup/admin.py:899 ../roundup/admin.py:970 ../roundup/admin.py:1018
+#: ../roundup/admin.py:1040 ../roundup/admin.py:1067 ../roundup/admin.py:1134
+#: ../roundup/admin.py:1204
+msgid "Not enough arguments supplied"
+msgstr ""
+
+#: ../roundup/admin.py:373
+#, python-format
+msgid "Instance home parent directory \"%(parent)s\" does not exist"
+msgstr ""
+
+#: ../roundup/admin.py:381
+#, 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 ""
+
+#: ../roundup/admin.py:396
+msgid "Select template [classic]: "
+msgstr ""
+
+#: ../roundup/admin.py:407
+msgid "Select backend [anydbm]: "
+msgstr ""
+
+#: ../roundup/admin.py:417
+#, python-format
+msgid "Error in configuration settings: \"%s\""
+msgstr ""
+
+#: ../roundup/admin.py:426
+#, python-format
+msgid ""
+"\n"
+"---------------------------------------------------------------------------\n"
+" You should now edit the tracker configuration file:\n"
+" %(config_file)s"
+msgstr ""
+
+#: ../roundup/admin.py:436
+msgid " ... at a minimum, you must set following options:"
+msgstr ""
+
+#: ../roundup/admin.py:441
+#, 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:459
+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:469
+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:483
+msgid "Admin Password: "
+msgstr ""
+
+#: ../roundup/admin.py:484
+msgid " Confirm: "
+msgstr ""
+
+#: ../roundup/admin.py:488
+msgid "Instance home does not exist"
+msgstr ""
+
+#: ../roundup/admin.py:492
+msgid "Instance has not been installed"
+msgstr ""
+
+#: ../roundup/admin.py:497
+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:518
+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:558 :573
+#: ../roundup/admin.py:558 ../roundup/admin.py:573
+#, python-format
+msgid "property %s is not of type Multilink or Link so -d flag does not apply."
+msgstr ""
+
+# ../roundup/admin.py:581 :981 :1030 :1052
+#: ../roundup/admin.py:581 ../roundup/admin.py:981 ../roundup/admin.py:1030
+#: ../roundup/admin.py:1052
+#, python-format
+msgid "no such %(classname)s node \"%(nodeid)s\""
+msgstr ""
+
+#: ../roundup/admin.py:583
+#, python-format
+msgid "no such %(classname)s property \"%(propname)s\""
+msgstr ""
+
+#: ../roundup/admin.py:592
+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:646
+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:699 :852 :864 :918
+#: ../roundup/admin.py:699 ../roundup/admin.py:852 ../roundup/admin.py:864
+#: ../roundup/admin.py:918
+#, python-format
+msgid "%(classname)s has no property \"%(propname)s\""
+msgstr ""
+
+#: ../roundup/admin.py:706
+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:721
+#, python-format
+msgid "%(key)s: %(value)s (key property)"
+msgstr ""
+
+#: ../roundup/admin.py:723
+#, python-format
+msgid "%(key)s: %(value)s"
+msgstr ""
+
+#: ../roundup/admin.py:726
+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:750
+#, python-format
+msgid "%(key)s: %(value)r"
+msgstr ""
+
+#: ../roundup/admin.py:753
+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:780
+#, python-format
+msgid "%(propname)s (Password): "
+msgstr ""
+
+#: ../roundup/admin.py:782
+#, python-format
+msgid " %(propname)s (Again): "
+msgstr ""
+
+#: ../roundup/admin.py:784
+msgid "Sorry, try again..."
+msgstr ""
+
+#: ../roundup/admin.py:788
+#, python-format
+msgid "%(propname)s (%(proptype)s): "
+msgstr ""
+
+#: ../roundup/admin.py:806
+#, python-format
+msgid "you must provide the \"%(propname)s\" property."
+msgstr ""
+
+#: ../roundup/admin.py:817
+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:830
+msgid "Too many arguments supplied"
+msgstr ""
+
+#: ../roundup/admin.py:866
+#, python-format
+msgid "%(nodeid)4s: %(value)s"
+msgstr ""
+
+#: ../roundup/admin.py:870
+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:914
+#, python-format
+msgid "\"%(spec)s\" not name:width"
+msgstr ""
+
+#: ../roundup/admin.py:964
+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:985
+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:999
+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:1011
+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:1034
+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:1056
+msgid ""
+"Usage: export [class[,class]] export_dir\n"
+" Export the database to colon-separated-value files.\n"
+"\n"
+" Optionally limit the export to just the names classes.\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:1114
+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:1186
+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:1214
+msgid "Invalid format"
+msgstr ""
+
+#: ../roundup/admin.py:1224
+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:1238
+#, python-format
+msgid "no such item \"%(designator)s\""
+msgstr ""
+
+#: ../roundup/admin.py:1248
+msgid ""
+"Usage: security [Role name]\n"
+" Display the Permissions available to one or all Roles.\n"
+" "
+msgstr ""
+
+#: ../roundup/admin.py:1256
+#, python-format
+msgid "No such Role \"%(role)s\""
+msgstr ""
+
+#: ../roundup/admin.py:1262
+#, python-format
+msgid "New Web users get the Roles \"%(role)s\""
+msgstr ""
+
+#: ../roundup/admin.py:1264
+#, python-format
+msgid "New Web users get the Role \"%(role)s\""
+msgstr ""
+
+#: ../roundup/admin.py:1267
+#, python-format
+msgid "New Email users get the Roles \"%(role)s\""
+msgstr ""
+
+#: ../roundup/admin.py:1269
+#, python-format
+msgid "New Email users get the Role \"%(role)s\""
+msgstr ""
+
+#: ../roundup/admin.py:1272
+#, python-format
+msgid "Role \"%(name)s\":"
+msgstr ""
+
+#: ../roundup/admin.py:1277
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
+msgstr ""
+
+#: ../roundup/admin.py:1280
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
+msgstr ""
+
+#: ../roundup/admin.py:1283
+#, python-format
+msgid " %(description)s (%(name)s)"
+msgstr ""
+
+#: ../roundup/admin.py:1312
+#, python-format
+msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
+msgstr ""
+
+#: ../roundup/admin.py:1318
+#, python-format
+msgid "Multiple commands match \"%(command)s\": %(list)s"
+msgstr ""
+
+#: ../roundup/admin.py:1325
+msgid "Enter tracker home: "
+msgstr ""
+
+# ../roundup/admin.py:1332 :1338 :1358
+#: ../roundup/admin.py:1332 ../roundup/admin.py:1338 ../roundup/admin.py:1358
+#, python-format
+msgid "Error: %(message)s"
+msgstr ""
+
+#: ../roundup/admin.py:1346
+#, python-format
+msgid "Error: Couldn't open tracker: %(message)s"
+msgstr ""
+
+#: ../roundup/admin.py:1371
+#, python-format
+msgid ""
+"Roundup %s ready for input.\n"
+"Type \"help\" for help."
+msgstr ""
+
+#: ../roundup/admin.py:1376
+msgid "Note: command history and editing not available"
+msgstr ""
+
+#: ../roundup/admin.py:1380
+msgid "roundup> "
+msgstr ""
+
+#: ../roundup/admin.py:1382
+msgid "exit..."
+msgstr ""
+
+#: ../roundup/admin.py:1392
+msgid "There are unsaved changes. Commit them (y/N)? "
+msgstr ""
+
+#: ../roundup/backends/back_anydbm.py:1997
+#, python-format
+msgid "WARNING: invalid date tuple %r"
+msgstr ""
+
+#: ../roundup/backends/rdbms_common.py:1434
+msgid "create"
+msgstr ""
+
+#: ../roundup/backends/rdbms_common.py:1600
+msgid "unlink"
+msgstr ""
+
+#: ../roundup/backends/rdbms_common.py:1604
+msgid "link"
+msgstr ""
+
+#: ../roundup/backends/rdbms_common.py:1724
+msgid "set"
+msgstr ""
+
+#: ../roundup/backends/rdbms_common.py:1748
+msgid "retired"
+msgstr ""
+
+#: ../roundup/backends/rdbms_common.py:1778
+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
+msgid "No type specified"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:91
+msgid "No ID entered"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:97
+#, 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"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:124
+#, python-format
+msgid "%(classname)s %(itemid)s has been retired"
+msgstr ""
+
+# ../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 ""
+
+# ../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 ""
+
+#: ../roundup/cgi/actions.py:297
+#, python-format
+msgid "Not enough values on line %(line)s"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:344
+msgid "Items edited OK"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:404
+#, python-format
+msgid "%(class)s %(id)s %(properties)s edited ok"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:407
+#, python-format
+msgid "%(class)s %(id)s - nothing changed"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:419
+#, python-format
+msgid "%(class)s %(id)s created"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:451
+#, python-format
+msgid "You do not have permission to edit %(class)s"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:463
+#, python-format
+msgid "You do not have permission to create %(class)s"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:487
+msgid "You do not have permission to edit user roles"
+msgstr ""
+
+#: ../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 ""
+
+#: ../roundup/cgi/actions.py:565
+#, python-format
+msgid "Edit Error: %s"
+msgstr ""
+
+# ../roundup/cgi/actions.py:596 :607 :778 :797
+#: ../roundup/cgi/actions.py:596 ../roundup/cgi/actions.py:607
+#: ../roundup/cgi/actions.py:778 ../roundup/cgi/actions.py:797
+#, python-format
+msgid "Error: %s"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:633
+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:675
+#, python-format
+msgid "Password reset and email sent to %s"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:684
+msgid "Unknown username"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:692
+msgid "Unknown email address"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:697
+msgid "You need to specify a username or address"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:722
+#, python-format
+msgid "Email sent to %s"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:741
+msgid "You are now registered, welcome!"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:786
+msgid "It is not permitted to supply roles at registration."
+msgstr ""
+
+#: ../roundup/cgi/actions.py:878
+msgid "You are logged out"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:895
+msgid "Username required"
+msgstr ""
+
+# ../roundup/cgi/actions.py:930 :934
+#: ../roundup/cgi/actions.py:930 ../roundup/cgi/actions.py:934
+msgid "Invalid login"
+msgstr ""
+
+#: ../roundup/cgi/actions.py:940
+msgid "You do not have permission to login"
+msgstr ""
+
+#: ../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 ""
+
+#: ../roundup/cgi/cgitb.py:64
+#, python-format
+msgid "<li>\"%(name)s\" (%(info)s)</li>"
+msgstr ""
+
+#: ../roundup/cgi/cgitb.py:67
+#, python-format
+msgid "<li>Looking for \"%(name)s\", current path:<ol>%(path)s</ol></li>"
+msgstr ""
+
+#: ../roundup/cgi/cgitb.py:71
+#, python-format
+msgid "<li>In %s</li>"
+msgstr ""
+
+#: ../roundup/cgi/cgitb.py:76
+#, python-format
+msgid "A problem occurred in your template \"%s\"."
+msgstr ""
+
+#: ../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 ""
+
+#: ../roundup/cgi/cgitb.py:103
+msgid "Full traceback:"
+msgstr ""
+
+#: ../roundup/cgi/cgitb.py:116
+#, python-format
+msgid "<font size=+1><strong>%(exc_type)s</strong>: %(exc_value)s</font>"
+msgstr ""
+
+#: ../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 ""
+
+#: ../roundup/cgi/cgitb.py:129
+msgid "<file is None - probably inside <tt>eval</tt> or <tt>exec</tt>>"
+msgstr ""
+
+#: ../roundup/cgi/cgitb.py:138
+#, python-format
+msgid "in <strong>%s</strong>"
+msgstr ""
+
+# ../roundup/cgi/cgitb.py:172 :178
+#: ../roundup/cgi/cgitb.py:172 ../roundup/cgi/cgitb.py:178
+msgid "<em>undefined</em>"
+msgstr ""
+
+#: ../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 ""
+
+#: ../roundup/cgi/client.py:308
+msgid "Form Error: "
+msgstr ""
+
+#: ../roundup/cgi/client.py:363
+#, python-format
+msgid "Unrecognized charset: %r"
+msgstr ""
+
+#: ../roundup/cgi/client.py:490
+msgid "Anonymous users are not allowed to use the web interface"
+msgstr ""
+
+#: ../roundup/cgi/client.py:645
+msgid "You are not allowed to view this file."
+msgstr ""
+
+#: ../roundup/cgi/client.py:737
+#, python-format
+msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
+msgstr ""
+
+#: ../roundup/cgi/client.py:741
+#, 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 \"%(value)s\" not a designator"
+msgstr ""
+
+#: ../roundup/cgi/form_parser.py:290
+#, python-format
+msgid "%(class)s %(property)s is not a link or multilink property"
+msgstr ""
+
+#: ../roundup/cgi/form_parser.py:312
+#, python-format
+msgid ""
+"You have submitted a %(action)s action for the property \"%(property)s\" "
+"which doesn't exist"
+msgstr ""
+
+# ../roundup/cgi/form_parser.py:331 :357
+#: ../roundup/cgi/form_parser.py:331 ../roundup/cgi/form_parser.py:357
+#, python-format
+msgid "You have submitted more than one value for the %s property"
+msgstr ""
+
+# ../roundup/cgi/form_parser.py:354 :360
+#: ../roundup/cgi/form_parser.py:354 ../roundup/cgi/form_parser.py:360
+msgid "Password and confirmation text do not match"
+msgstr ""
+
+#: ../roundup/cgi/form_parser.py:395
+#, python-format
+msgid "property \"%(propname)s\": \"%(value)s\" not currently in list"
+msgstr ""
+
+#: ../roundup/cgi/form_parser.py:512
+#, 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:535
+msgid "File is empty"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:72
+#, python-format
+msgid "You are not allowed to %(action)s items of class %(class)s"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:627
+msgid "(list)"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:696
+msgid "Submit New Entry"
+msgstr ""
+
+# ../roundup/cgi/templating.py:710 :829 :1236 :1257 :1304 :1327 :1361 :1400
+# :1453 :1470 :1549 :1569 :1587 :1619 :1629 :1683 :1875
+#: ../roundup/cgi/templating.py:710 ../roundup/cgi/templating.py:829
+#: ../roundup/cgi/templating.py:1236 ../roundup/cgi/templating.py:1257
+#: ../roundup/cgi/templating.py:1304 ../roundup/cgi/templating.py:1327
+#: ../roundup/cgi/templating.py:1361 ../roundup/cgi/templating.py:1400
+#: ../roundup/cgi/templating.py:1453 ../roundup/cgi/templating.py:1470
+#: ../roundup/cgi/templating.py:1549 ../roundup/cgi/templating.py:1569
+#: ../roundup/cgi/templating.py:1587 ../roundup/cgi/templating.py:1619
+#: ../roundup/cgi/templating.py:1629 ../roundup/cgi/templating.py:1683
+#: ../roundup/cgi/templating.py:1875
+msgid "[hidden]"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:711
+msgid "New node - no history"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:811
+msgid "Submit Changes"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:893
+msgid "<em>The indicated property no longer exists</em>"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:894
+#, python-format
+msgid "<em>%s: %s</em>\n"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:907
+#, python-format
+msgid "The linked class %(classname)s no longer exists"
+msgstr ""
+
+# ../roundup/cgi/templating.py:940 :964
+#: ../roundup/cgi/templating.py:940 ../roundup/cgi/templating.py:964
+msgid "<strike>The linked node no longer exists</strike>"
+msgstr ""
+
+# ../roundup/cgi/templating.py:1006 :1404 :1425 :1431
+#: ../roundup/cgi/templating.py:1006 ../roundup/cgi/templating.py:1404
+#: ../roundup/cgi/templating.py:1425 ../roundup/cgi/templating.py:1431
+msgid "No"
+msgstr ""
+
+# ../roundup/cgi/templating.py:1006 :1404 :1423 :1428
+#: ../roundup/cgi/templating.py:1006 ../roundup/cgi/templating.py:1404
+#: ../roundup/cgi/templating.py:1423 ../roundup/cgi/templating.py:1428
+msgid "Yes"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1017
+#, python-format
+msgid "%s: (no value)"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1029
+msgid ""
+"<strong><em>This event is not handled by the history display!</em></strong>"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1041
+msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1050
+msgid "History"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1052
+msgid "<th>Date</th>"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1053
+msgid "<th>User</th>"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1054
+msgid "<th>Action</th>"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1055
+msgid "<th>Args</th>"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1097
+#, python-format
+msgid "Copy of %(class)s %(id)s"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1331
+msgid "*encrypted*"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1514
+msgid ""
+"default value for DateHTMLProperty must be either DateHTMLProperty or string "
+"date representation."
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1674
+#, python-format
+msgid "Attempt to look up %(attr)s on a missing value"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:1750
+#, python-format
+msgid "<option %svalue=\"-1\">- no selection -</option>"
+msgstr ""
+
+#: ../roundup/date.py:186
+msgid ""
+"Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or \"yyyy-"
+"mm-dd.HH:MM:SS.SSS\""
+msgstr ""
+
+#: ../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 ""
+
+#: ../roundup/date.py:538
+msgid ""
+"Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
+msgstr ""
+
+#: ../roundup/date.py:557
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+msgstr ""
+
+#: ../roundup/date.py:694
+#, python-format
+msgid "%(number)s year"
+msgid_plural "%(number)s years"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../roundup/date.py:698
+#, python-format
+msgid "%(number)s month"
+msgid_plural "%(number)s months"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../roundup/date.py:702
+#, python-format
+msgid "%(number)s week"
+msgid_plural "%(number)s weeks"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../roundup/date.py:706
+#, python-format
+msgid "%(number)s day"
+msgid_plural "%(number)s days"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../roundup/date.py:710
+msgid "tomorrow"
+msgstr ""
+
+#: ../roundup/date.py:712
+msgid "yesterday"
+msgstr ""
+
+#: ../roundup/date.py:715
+#, python-format
+msgid "%(number)s hour"
+msgid_plural "%(number)s hours"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../roundup/date.py:719
+msgid "an hour"
+msgstr ""
+
+#: ../roundup/date.py:721
+msgid "1 1/2 hours"
+msgstr ""
+
+#: ../roundup/date.py:723
+#, python-format
+msgid "1 %(number)s/4 hours"
+msgid_plural "1 %(number)s/4 hours"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../roundup/date.py:727
+msgid "in a moment"
+msgstr ""
+
+#: ../roundup/date.py:729
+msgid "just now"
+msgstr ""
+
+#: ../roundup/date.py:732
+msgid "1 minute"
+msgstr ""
+
+#: ../roundup/date.py:735
+#, python-format
+msgid "%(number)s minute"
+msgid_plural "%(number)s minutes"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../roundup/date.py:738
+msgid "1/2 an hour"
+msgstr ""
+
+#: ../roundup/date.py:740
+#, python-format
+msgid "%(number)s/4 hour"
+msgid_plural "%(number)s/4 hours"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../roundup/date.py:744
+#, python-format
+msgid "%s ago"
+msgstr ""
+
+#: ../roundup/date.py:746
+#, python-format
+msgid "in %s"
+msgstr ""
+
+#: ../roundup/init.py:134
+#, python-format
+msgid ""
+"WARNING: directory '%s'\n"
+"\tcontains old-style template - ignored"
+msgstr ""
+
+#: ../roundup/roundupdb.py:141
+msgid "files"
+msgstr ""
+
+#: ../roundup/roundupdb.py:141
+msgid "messages"
+msgstr ""
+
+#: ../roundup/roundupdb.py:141
+msgid "nosy"
+msgstr ""
+
+#: ../roundup/roundupdb.py:141
+msgid "superseder"
+msgstr ""
+
+#: ../roundup/roundupdb.py:141
+msgid "title"
+msgstr ""
+
+#: ../roundup/roundupdb.py:142
+msgid "assignedto"
+msgstr ""
+
+#: ../roundup/roundupdb.py:142
+msgid "priority"
+msgstr ""
+
+#: ../roundup/roundupdb.py:142
+msgid "status"
+msgstr ""
+
+#: ../roundup/roundupdb.py:142
+msgid "topic"
+msgstr ""
+
+#: ../roundup/roundupdb.py:145
+msgid "activity"
+msgstr ""
+
+#. following properties are common for all hyperdb classes
+#. they are listed here to keep things in one place
+#: ../roundup/roundupdb.py:145
+msgid "actor"
+msgstr ""
+
+#: ../roundup/roundupdb.py:145
+msgid "creation"
+msgstr ""
+
+#: ../roundup/roundupdb.py:145
+msgid "creator"
+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] [[-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"
+"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:147
+msgid "Error: not enough source specification information"
+msgstr ""
+
+#: ../roundup/scripts/roundup_mailgw.py:163
+msgid "Error: pop specification not valid"
+msgstr ""
+
+#: ../roundup/scripts/roundup_mailgw.py:170
+msgid "Error: apop specification not valid"
+msgstr ""
+
+#: ../roundup/scripts/roundup_mailgw.py:184
+msgid ""
+"Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or "
+"\"imaps\""
+msgstr ""
+
+#: ../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 ""
+
+#: ../roundup/scripts/roundup_server.py:287
+#, python-format
+msgid "Error: %s: %s"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:297
+msgid "WARNING: ignoring \"-g\" argument, not root"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:303
+msgid "Can't change groups - no grp module"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:312
+#, python-format
+msgid "Group %(group)s doesn't exist"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:323
+msgid "Can't run as root!"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:326
+msgid "WARNING: ignoring \"-u\" argument, not root"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:331
+msgid "Can't change users - no pwd module"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:340
+#, python-format
+msgid "User %(user)s doesn't exist"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:471
+#, python-format
+msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:494
+#, python-format
+msgid "Unable to bind to port %s, port already in use."
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:562
+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:569
+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:576
+#, 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:724
+msgid "Instances must be name=home"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:738
+#, python-format
+msgid "Configuration saved to %s"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:756
+msgid "Sorry, you can't run the server as a daemon on this Operating System"
+msgstr ""
+
+#: ../roundup/scripts/roundup_server.py:768
+#, python-format
+msgid "Roundup server started on %(HOST)s:%(PORT)s"
+msgstr ""
+
+#: ../templates/classic/html/_generic.collision.html:4
+#: ../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
+msgid "${class} Edit Collision"
+msgstr ""
+
+#: ../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 ""
+
+#: ../templates/classic/html/_generic.help.html:9
+#: ../templates/minimal/html/_generic.help.html:9
+msgid "${property} help - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/_generic.help.html:31
+#: ../templates/minimal/html/_generic.help.html:31
+msgid " Cancel "
+msgstr ""
+
+#: ../templates/classic/html/_generic.help.html:34
+#: ../templates/minimal/html/_generic.help.html:34
+msgid " Apply "
+msgstr ""
+
+#: ../templates/classic/html/_generic.help.html:41
+#: ../templates/classic/html/issue.index.html:73
+#: ../templates/minimal/html/_generic.help.html:41
+msgid "<< previous"
+msgstr ""
+
+#: ../templates/classic/html/_generic.help.html:52
+#: ../templates/classic/html/issue.index.html:81
+#: ../templates/minimal/html/_generic.help.html:52
+msgid "${start}..${end} out of ${total}"
+msgstr ""
+
+#: ../templates/classic/html/_generic.help.html:56
+#: ../templates/classic/html/issue.index.html:84
+#: ../templates/minimal/html/_generic.help.html:56
+msgid "next >>"
+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
+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
+msgid "${class} editing"
+msgstr ""
+
+#: ../templates/classic/html/_generic.index.html:14
+#: ../templates/classic/html/_generic.item.html:12
+#: ../templates/classic/html/file.item.html:9
+#: ../templates/classic/html/issue.index.html:16
+#: ../templates/classic/html/issue.item.html:28
+#: ../templates/classic/html/msg.item.html:26
+#: ../templates/classic/html/user.index.html:9
+#: ../templates/classic/html/user.item.html:28
+#: ../templates/minimal/html/_generic.index.html:14
+#: ../templates/minimal/html/_generic.item.html:12
+#: ../templates/minimal/html/user.index.html:9
+#: ../templates/minimal/html/user.item.html:28
+#: ../templates/minimal/html/user.register.html:14
+msgid "You are not allowed to view this page."
+msgstr ""
+
+#: ../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 ""
+
+#: ../templates/classic/html/_generic.index.html:44
+#: ../templates/minimal/html/_generic.index.html:44
+msgid "Edit Items"
+msgstr ""
+
+#: ../templates/classic/html/file.index.html:4
+msgid "List of files - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/file.index.html:5
+msgid "List of files"
+msgstr ""
+
+#: ../templates/classic/html/file.index.html:10
+msgid "Download"
+msgstr ""
+
+#: ../templates/classic/html/file.index.html:11
+#: ../templates/classic/html/file.item.html:22
+msgid "Content Type"
+msgstr ""
+
+#: ../templates/classic/html/file.index.html:12
+msgid "Uploaded By"
+msgstr ""
+
+#: ../templates/classic/html/file.index.html:13
+#: ../templates/classic/html/msg.item.html:43
+msgid "Date"
+msgstr ""
+
+#: ../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:18
+#: ../templates/classic/html/user.item.html:39
+#: ../templates/classic/html/user.register.html:17
+msgid "Name"
+msgstr ""
+
+#: ../templates/classic/html/file.item.html:40
+msgid "download"
+msgstr ""
+
+#: ../templates/classic/html/home.classlist.html:2
+#: ../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
+msgid "List of classes"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:7
+msgid "List of issues - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:11
+msgid "List of issues"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:22
+#: ../templates/classic/html/issue.item.html:44
+msgid "Priority"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:23
+msgid "ID"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:24
+msgid "Creation"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:25
+msgid "Activity"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:26
+msgid "Actor"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:27
+msgid "Topic"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:28
+#: ../templates/classic/html/issue.item.html:39
+msgid "Title"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:29
+#: ../templates/classic/html/issue.item.html:46
+msgid "Status"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:30
+msgid "Creator"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:31
+msgid "Assigned To"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:97
+msgid "Download as CSV"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:105
+msgid "Sort on:"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:108
+#: ../templates/classic/html/issue.index.html:125
+msgid "- nothing -"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:116
+#: ../templates/classic/html/issue.index.html:133
+msgid "Descending:"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:122
+msgid "Group on:"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:139
+msgid "Redisplay"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:7
+msgid "Issue ${id}: ${title} - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:10
+msgid "New Issue - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:14
+msgid "New Issue"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:16
+msgid "New Issue Editing"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:19
+msgid "Issue${id}"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:22
+msgid "Issue${id} Editing"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:51
+msgid "Superseder"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:56
+msgid "View: ${link}"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:60
+msgid "Nosy List"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:69
+msgid "Assigned To"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:71
+msgid "Topics"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:79
+msgid "Change Note"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:87
+msgid "File"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:99
+msgid "Make a copy"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:107
+#: ../templates/classic/html/user.item.html:106
+#: ../templates/classic/html/user.register.html:69
+#: ../templates/minimal/html/user.item.html:86
+msgid ""
+"<table class=\"form\"> <tr> <td>Note: </td> <th class=\"required"
+"\">highlighted</th> <td> fields are required.</td> </tr> </table>"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:121
+msgid ""
+"Created on <b>${creation}</b> by <b>${creator}</b>, last changed <b>"
+"${activity}</b> by <b>${actor}</b>."
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:125
+#: ../templates/classic/html/msg.item.html:56
+msgid "Files"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:127
+#: ../templates/classic/html/msg.item.html:58
+msgid "File name"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:128
+#: ../templates/classic/html/msg.item.html:59
+msgid "Uploaded"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:129
+msgid "Type"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:130
+#: ../templates/classic/html/query.edit.html:30
+msgid "Edit"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:131
+msgid "Remove"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:151
+#: ../templates/classic/html/issue.item.html:172
+#: ../templates/classic/html/query.edit.html:50
+msgid "remove"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:158
+#: ../templates/classic/html/msg.index.html:9
+msgid "Messages"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:162
+msgid "msg${id} (view)"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:163
+msgid "Author: ${author}"
+msgstr ""
+
+#: ../templates/classic/html/issue.item.html:165
+msgid "Date: ${date}"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:2
+msgid "Issue searching - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:4
+msgid "Issue searching"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:25
+msgid "Filter on"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:26
+msgid "Display"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:27
+msgid "Sort on"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:28
+msgid "Group on"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:32
+msgid "All text*:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:40
+msgid "Title:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:50
+msgid "Topic:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:58
+msgid "ID:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:66
+msgid "Creation Date:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:77
+msgid "Creator:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:79
+msgid "created by me"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:88
+msgid "Activity:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:99
+msgid "Actor:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:101
+msgid "done by me"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:112
+msgid "Priority:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:114
+#: ../templates/classic/html/issue.search.html:130
+msgid "not selected"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:125
+msgid "Status:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:128
+msgid "not resolved"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:143
+msgid "Assigned to:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:146
+msgid "assigned to me"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:148
+msgid "unassigned"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:158
+msgid "No Sort or group:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:166
+msgid "Pagesize:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:172
+msgid "Start With:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:178
+msgid "Sort Descending:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:185
+msgid "Group Descending:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:192
+msgid "Query name**:"
+msgstr ""
+
+#: ../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
+msgid "Search"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:209
+msgid "*: The \"all text\" field will look in message bodies and issue titles"
+msgstr ""
+
+#: ../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 ""
+
+#: ../templates/classic/html/keyword.item.html:3
+msgid "Keyword editing - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/keyword.item.html:5
+msgid "Keyword editing"
+msgstr ""
+
+#: ../templates/classic/html/keyword.item.html:11
+msgid "Existing Keywords"
+msgstr ""
+
+#: ../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
+msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
+msgstr ""
+
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
+msgstr ""
+
+#: ../templates/classic/html/msg.index.html:3
+msgid "List of messages - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/msg.index.html:5
+msgid "Message listing"
+msgstr ""
+
+#: ../templates/classic/html/msg.item.html:6
+msgid "Message ${id} - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/msg.item.html:9
+msgid "New Message - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/msg.item.html:13
+msgid "New Message"
+msgstr ""
+
+#: ../templates/classic/html/msg.item.html:15
+msgid "New Message Editing"
+msgstr ""
+
+#: ../templates/classic/html/msg.item.html:18
+msgid "Message${id}"
+msgstr ""
+
+#: ../templates/classic/html/msg.item.html:21
+msgid "Message${id} Editing"
+msgstr ""
+
+#: ../templates/classic/html/msg.item.html:33
+msgid "Author"
+msgstr ""
+
+#: ../templates/classic/html/msg.item.html:38
+msgid "Recipients"
+msgstr ""
+
+#: ../templates/classic/html/msg.item.html:49
+msgid "Content"
+msgstr ""
+
+#: ../templates/classic/html/page.html:41
+msgid "<b>Your Queries</b> (<a href=\"query?@template=edit\">edit</a>)"
+msgstr ""
+
+#: ../templates/classic/html/page.html:52
+msgid "Issues"
+msgstr ""
+
+#: ../templates/classic/html/page.html:54
+#: ../templates/classic/html/page.html:74
+msgid "Create New"
+msgstr ""
+
+#: ../templates/classic/html/page.html:56
+msgid "Show Unassigned"
+msgstr ""
+
+#: ../templates/classic/html/page.html:58
+msgid "Show All"
+msgstr ""
+
+#: ../templates/classic/html/page.html:61
+msgid "Show issue:"
+msgstr ""
+
+#: ../templates/classic/html/page.html:72
+msgid "Keywords"
+msgstr ""
+
+#: ../templates/classic/html/page.html:78
+msgid "Edit Existing"
+msgstr ""
+
+#: ../templates/classic/html/page.html:84
+#: ../templates/minimal/html/page.html:65
+msgid "Administration"
+msgstr ""
+
+#: ../templates/classic/html/page.html:86
+#: ../templates/minimal/html/page.html:66
+msgid "Class List"
+msgstr ""
+
+#: ../templates/classic/html/page.html:90
+#: ../templates/minimal/html/page.html:68
+msgid "User List"
+msgstr ""
+
+#: ../templates/classic/html/page.html:92
+#: ../templates/minimal/html/page.html:71
+msgid "Add User"
+msgstr ""
+
+#: ../templates/classic/html/page.html:99
+#: ../templates/classic/html/page.html:105
+#: ../templates/minimal/html/page.html:46
+msgid "Login"
+msgstr ""
+
+#: ../templates/classic/html/page.html:104
+#: ../templates/minimal/html/page.html:45
+msgid "Remember me?"
+msgstr ""
+
+#: ../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 ""
+
+#: ../templates/classic/html/page.html:111
+msgid "Lost your login?"
+msgstr ""
+
+#: ../templates/classic/html/page.html:116
+msgid "Hello, ${user}"
+msgstr ""
+
+#: ../templates/classic/html/page.html:118
+msgid "Your Issues"
+msgstr ""
+
+#: ../templates/classic/html/page.html:119
+#: ../templates/minimal/html/page.html:57
+msgid "Your Details"
+msgstr ""
+
+#: ../templates/classic/html/page.html:121
+#: ../templates/minimal/html/page.html:59
+msgid "Logout"
+msgstr ""
+
+#: ../templates/classic/html/page.html:125
+msgid "Help"
+msgstr ""
+
+#: ../templates/classic/html/page.html:126
+msgid "Roundup docs"
+msgstr ""
+
+#: ../templates/classic/html/page.html:136
+#: ../templates/minimal/html/page.html:81
+msgid "clear this message"
+msgstr ""
+
+#: ../templates/classic/html/page.html:181
+msgid "don't care"
+msgstr ""
+
+#: ../templates/classic/html/page.html:183
+msgid "------------"
+msgstr ""
+
+#: ../templates/classic/html/page.html:210
+msgid "no value"
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:4
+msgid "\"Your Queries\" Editing - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:6
+msgid "\"Your Queries\" Editing"
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:11
+msgid "You are not allowed to edit queries."
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:28
+msgid "Query"
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:29
+msgid "Include in \"Your Queries\""
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:31
+msgid "Private to you?"
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:44
+msgid "leave out"
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:45
+msgid "include"
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:49
+msgid "leave in"
+msgstr ""
+
+#: ../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:92
+msgid "edit"
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:71
+msgid "yes"
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:73
+msgid "no"
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:79
+msgid "Delete"
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:94
+msgid "[not yours to edit]"
+msgstr ""
+
+#: ../templates/classic/html/query.edit.html:102
+msgid "Save Selection"
+msgstr ""
+
+#: ../templates/classic/html/user.forgotten.html:3
+msgid "Password reset request - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/user.forgotten.html:5
+msgid "Password reset request"
+msgstr ""
+
+#: ../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 ""
+
+#: ../templates/classic/html/user.forgotten.html:24
+#: ../templates/classic/html/user.forgotten.html:34
+msgid "Request password reset"
+msgstr ""
+
+#: ../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 ""
+
+#: ../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.index.html:3
+#: ../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
+msgid "User listing"
+msgstr ""
+
+#: ../templates/classic/html/user.index.html:14
+#: ../templates/minimal/html/user.index.html:14
+msgid "Username"
+msgstr ""
+
+#: ../templates/classic/html/user.index.html:15
+msgid "Real name"
+msgstr ""
+
+#: ../templates/classic/html/user.index.html:16
+#: ../templates/classic/html/user.item.html:70
+#: ../templates/classic/html/user.register.html:45
+msgid "Organisation"
+msgstr ""
+
+#: ../templates/classic/html/user.index.html:17
+#: ../templates/minimal/html/user.index.html:15
+msgid "Email address"
+msgstr ""
+
+#: ../templates/classic/html/user.index.html:18
+msgid "Phone number"
+msgstr ""
+
+#: ../templates/classic/html/user.index.html:19
+msgid "Retire"
+msgstr ""
+
+#: ../templates/classic/html/user.index.html:32
+msgid "retire"
+msgstr ""
+
+#: ../templates/classic/html/user.item.html:7
+#: ../templates/minimal/html/user.item.html:7
+msgid "User ${id}: ${title} - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/user.item.html:10
+#: ../templates/minimal/html/user.item.html:10
+msgid "New User - ${tracker}"
+msgstr ""
+
+#: ../templates/classic/html/user.item.html:14
+#: ../templates/minimal/html/user.item.html:14
+msgid "New User"
+msgstr ""
+
+#: ../templates/classic/html/user.item.html:16
+#: ../templates/minimal/html/user.item.html:16
+msgid "New User Editing"
+msgstr ""
+
+#: ../templates/classic/html/user.item.html:19
+#: ../templates/minimal/html/user.item.html:19
+msgid "User${id}"
+msgstr ""
+
+#: ../templates/classic/html/user.item.html:22
+#: ../templates/minimal/html/user.item.html:22
+msgid "User${id} Editing"
+msgstr ""
+
+#: ../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 ""
+
+#: ../templates/classic/html/user.item.html:47
+#: ../templates/classic/html/user.register.html:25
+#: ../templates/minimal/html/user.item.html:44
+#: ../templates/minimal/html/user.register.html:30
+msgid "Login Password"
+msgstr ""
+
+#: ../templates/classic/html/user.item.html:51
+#: ../templates/classic/html/user.register.html:29
+#: ../templates/minimal/html/user.item.html:48
+#: ../templates/minimal/html/user.register.html:34
+msgid "Confirm Password"
+msgstr ""
+
+#: ../templates/classic/html/user.item.html:55
+#: ../templates/classic/html/user.register.html:33
+#: ../templates/minimal/html/user.item.html:52
+#: ../templates/minimal/html/user.register.html:38
+msgid "Roles"
+msgstr ""
+
+#: ../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 ""
+
+#: ../templates/classic/html/user.item.html:66
+#: ../templates/classic/html/user.register.html:41
+msgid "Phone"
+msgstr ""
+
+#: ../templates/classic/html/user.item.html:74
+msgid "Timezone"
+msgstr ""
+
+#: ../templates/classic/html/user.item.html:78
+msgid "(this is a numeric hour offset, the default is ${zone})"
+msgstr ""
+
+#: ../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 ""
+
+#: ../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 ""
+
+#: ../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 ""
+
+#: ../templates/classic/html/user.rego_progress.html:4
+#: ../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
+msgid "Registration in progress..."
+msgstr ""
+
+#: ../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 ""
+
+#: ../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 ""
+
+#: ../templates/minimal/html/home.html:19
+msgid "Please log in or register."
+msgstr ""
+
+#: ../templates/minimal/html/page.html:55
+msgid "Hello,<br>${user}"
+msgstr ""
Added: tracker/vendor/roundup/current/locale/ru.po
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/locale/ru.po Sun Nov 5 21:30:25 2006
@@ -0,0 +1,3135 @@
+# Russian message file for Roundup Issue Tracker
+# alexander smishlajev <alex at tycobka.lv>, 2004
+#
+# $Id: ru.po,v 1.13 2006/03/04 08:51:32 a1s Exp $
+#
+# roundup.pot revision 1.17
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Roundup 0.9.0\n"
+"Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
+"POT-Creation-Date: 2006-02-28 07:44+0200\n"
+"PO-Revision-Date: 2006-03-04 10:42+0200\n"
+"Last-Translator: alexander smishlajev <alex at tycobka.lv>\n"
+"Language-Team: Russian\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=koi8-r\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%"
+"10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#: ../roundup/admin.py:85 ../roundup/admin.py:979 ../roundup/admin.py:1028
+#: ../roundup/admin.py:1050
+#, python-format
+msgid "no such class \"%(classname)s\""
+msgstr "ëÌÁÓÓ \"%(classname)s\" ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
+
+#: ../roundup/admin.py:95 ../roundup/admin.py:99
+#, python-format
+msgid "argument \"%(arg)s\" not propname=value"
+msgstr "ÁÒÇÕÍÅÎÔ \"%(arg)s\" ÄÏÌÖÅÎ ÉÍÅÔØ ×ÉÄ ÉÍÑ=ÚÎÁÞÅÎÉÅ"
+
+#: ../roundup/admin.py:112
+#, python-format
+msgid ""
+"Problem: %(message)s\n"
+"\n"
+msgstr ""
+"ïÛÉÂËÁ: %(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"
+"\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 ""
+"%(message)s÷ÙÚÏ×: roundup-admin [ËÌÀÞÉ] [<ËÏÍÁÎÄÁ> <ÁÒÇÕÍÅÎÔÙ>]\n"
+"\n"
+"ëÌÀÞÉ:\n"
+" -i <ËÁÔÁÌÏÇ> -- ÕËÁÚÙ×ÁÅÔ \"ÄÏÍÁÛÎÉÊ ËÁÔÁÌÏÇ\" ÔÒÅËÅÒÁ.\n"
+" -u -- ÉÍÑ ÐÏÌØÚÏ×ÁÔÅÌÑ. íÏÖÎÏ ÕËÁÚÁÔØ ÐÁÒÏÌØ ÞÅÒÅÚ Ä×ÏÅÔÏÞÉÅ.\n"
+" -d -- ×ÍÅÓÔÏ ÉÄÅÎÔÉÆÉËÁÔÏÒÏ× ×ÙÄÁ×ÁÔØ ÏÐÉÓÁÔÅÌÉ ÏÂßÅËÔÏ×.\n"
+" -c -- ÐÒÉ ×ÙÄÁÞÅ ÓÐÉÓËÏ× ÒÁÚÄÅÌÑÔØ ÚÎÁÞÅÎÉÑ ÚÁÐÑÔÙÍÉ.\n"
+" ôÏ ÖÅ, ÞÔÏ '-S \",\"'.\n"
+" -S <ÓÔÒÏËÁ> -- ÐÒÉ ×ÙÄÁÞÅ ÓÐÉÓËÏ× ÒÁÚÄÅÌÑÔØ ÚÎÁÞÅÎÉÑ ÕËÁÚÁÎÎÏÊ ÓÔÒÏËÏÊ.\n"
+" -s -- ÐÒÉ ×ÙÄÁÞÅ ÓÐÉÓËÏ× ÒÁÚÄÅÌÑÔØ ÚÎÁÞÅÎÉÑ ÐÒÏÂÅÌÁÍÉ.\n"
+" ôÏ ÖÅ, ÞÔÏ '-S \" \"'.\n"
+"\n"
+" ïÄÎÏ×ÒÅÍÅÎÎÏ ÍÏÖÎÏ ÉÓÐÏÌØÚÏ×ÁÔØ ÔÏÌØËÏ ÏÄÉÎ ÉÚ ËÌÀÞÅÊ -s, -c É -S.\n"
+"\n"
+"óÐÒÁ×ËÉ:\n"
+" roundup-admin -h\n"
+" roundup-admin help -- ÜÔÏ ÓÏÏÂÝÅÎÉÅ\n"
+" roundup-admin help <command> -- ÓÐÒÁ×ËÁ ÐÏ ËÏÍÁÎÄÅ\n"
+" roundup-admin help all -- ×ÓÅ ÓÐÒÁ×ÏÞÎÙÅ ÓÏÏÂÝÅÎÉÑ\n"
+
+#: ../roundup/admin.py:138
+msgid "Commands:"
+msgstr "ëÏÍÁÎÄÙ:"
+
+#: ../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 ""
+"íÏÖÎÏ ÉÓÐÏÌØÚÏ×ÁÔØ ÔÏÌØËÏ ÎÁÞÁÌØÎÙÅ ÂÕË×Ù ÉÍÅÎÉ ËÏÍÁÎÄÙ,\n"
+"ÅÓÌÉ ÜÔÉÈ ÂÕË× ÄÏÓÔÁÔÏÞÎÏ ÄÌÑ ÏÐÒÅÄÅÌÅÎÉÑ ËÏÍÁÎÄÙ.\n"
+"îÁÐÒÉÍÅÒ, l, li É lis ×ÙÚÙ×ÁÀÔ ËÏÍÁÎÄÕ list."
+
+# ÐÒÏÛÕ ÐÒÏÝÅÎÉÑ, ÎÅ ÍÏÇÕ ÐÒÉÄÕÍÁÔØ, ËÁË ÐÏ-ÒÕÓÓËÉ ÎÁÚÙ×ÁÅÔÓÑ
+# backslash escape. Ñ ÎÁÐÉÓÁÌ "ÚÁÜËÒÁÎÉÒÏ×ÁÔØ ÏÂÒÁÔÎÏÊ ËÏÓÏÊ ÞÅÒÔÏÊ",
+# ÎÏ ÍÎÅ ÜÔÏ ÓÏ×ÓÅÍ ÎÅ ÎÒÁ×ÉÔÓÑ.
+#
+# ÞÔÏ ÌÕÞÛÅ ÎÁÐÉÓÁÔØ ×ÍÅÓÔÏ "××ÅÓÔÉ Ó ÔÅÒÍÉÎÁÌÁ"?
+#: ../roundup/admin.py:175
+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 ""
+"\n"
+"÷ÓÅ ËÏÍÁÎÄÙ (ËÒÏÍÅ ËÏÍÁÎÄÙ 'help') ÔÒÅÂÕÀÔ ÕËÁÚÁÎÉÑ ÔÒÅËÅÒÁ.\n"
+"÷Ù ÄÏÌÖÎÙ ÓÏÏÂÝÉÔØ ÉÍÑ ËÁÔÁÌÏÇÁ, × ËÏÔÏÒÏÍ roundup ÈÒÁÎÉÔ ÂÁÚÕ ÄÁÎÎÙÈ\n"
+"É ÎÁÓÔÒÏÅÞÎÙÊ ÆÁÊÌ, ÏÐÉÓÙ×ÁÀÝÉÊ ËÏÎÆÉÇÕÒÁÃÉÀ ÔÒÅËÅÒÁ. üÔÏÔ ËÁÔÁÌÏÇ\n"
+"ÎÁÚÙ×ÁÅÔÓÑ \"ÄÏÍÁÛÎÉÍ ËÁÔÁÌÏÇÏÍ\" ÔÒÅËÅÒÁ. ðÕÔØ Ë ÜÔÏÍÕ ËÁÔÁÌÏÇÕ ÍÏÖÅÔ\n"
+"ÚÁÄÁ×ÁÔØÓÑ ÐÅÒÅÍÅÎÎÏÊ ÏËÒÕÖÅÎÉÑ TRACKER_HOME ÉÌÉ ËÌÀÞÏÍ ËÏÍÁÎÄÎÏÊ ÓÔÒÏËÉ\n"
+"\"-i <ÐÕÔØ>\".\n"
+"\n"
+"ïÐÉÓÁÔÅÌØ ÏÂßÅËÔÁ ÓÏÓÔÁ×ÌÑÅÔÓÑ ÉÚ ÉÍÅÎÉ ËÌÁÓÓÁ É ÉÄÅÎÔÉÆÉËÁÔÏÒÁ ÏÂßÅËÔÁ\n"
+"îÁÐÒÉÍÅÒ: bug1, user10 ÉÔÐ.\n"
+"\n"
+"úÎÁÞÅÎÉÑ ÁÔÒÉÂÕÔÏ× × ÁÒÇÕÍÅÎÔÁÈ ËÏÍÁÎÄÙ É ÐÒÉ ÐÅÞÁÔÉ ÒÅÚÕÌØÔÁÔÏ×\n"
+"ÐÒÅÄÓÔÁ×ÌÑÀÔÓÑ ÓÔÒÏËÁÍÉ:\n"
+" . óÔÒÏËÏ×ÙÅ ÚÎÁÞÅÎÉÑ - ÓÔÒÏËÉ É ÅÓÔØ.\n"
+" . úÎÁÞÅÎÉÑ ÄÁÔ ÐÅÞÁÔÁÀÔÓÑ × ÍÅÓÔÎÏÍ ÞÁÓÏ×ÏÍ ÐÏÑÓÅ, ÉÓÐÏÌØÚÕÑ ÐÏÌÎÏÅ\n"
+" ÐÒÅÄÓÔÁ×ÌÅÎÉÅ ÄÁÔÙ. ÷ ÁÒÇÕÍÅÎÔÁÈ ËÏÍÁÎÄ ÄÁÔÙ ÍÏÇÕÔ ÕËÁÚÙ×ÁÔØÓÑ ÐÏÌÎÙÍ\n"
+" ÐÒÅÄÓÔÁ×ÌÅÎÉÅÍ ÉÌÉ ÌÀÂÙÍ ÉÚ ÎÉÖÅÏÐÉÓÁÎÎÙÈ ÞÁÓÔÉÞÎÙÈ ÐÒÅÄÓÔÁ×ÌÅÎÉÊ.\n"
+" . úÎÁÞÅÎÉÑ ÓÓÙÌÏË (Link) ÐÅÞÁÔÁÀÔÓÑ × ×ÉÄÅ ÏÐÉÓÁÔÅÌÅÊ ÏÂßÅËÔÏ×.\n"
+" ÷ ÁÒÇÕÍÅÎÔÁÈ ËÏÍÁÎÄ ÍÏÇÕÔ ÉÓÐÏÌØÚÏ×ÁÔØÓÑ ÏÐÉÓÁÔÅÌÉ ÏÂßÅËÔÏ×\n"
+" ÉÌÉ ÚÎÁÞÅÎÉÑ ËÌÀÞÅ×ÙÈ ÁÔÒÉÂÕÔÏ×.\n"
+" . íÎÏÖÅÓÔ×ÅÎÎÙÅ ÓÓÙÌËÉ (Multilink) ÐÅÞÁÔÁÀÔÓÑ × ×ÉÄÅ ÓÐÉÓËÁ ÏÐÉÓÁÔÅÌÅÊ\n"
+" ÏÂßÅËÔÏ×, ÞÅÒÅÚ ÚÁÐÑÔÕÀ. ÷ ÁÒÇÕÍÅÎÔÁÈ ËÏÍÁÎÄ ÐÒÉÎÉÍÁÀÔÓÑ ËÁË ÏÐÉÓÁÔÅÌÉ\n"
+" ÏÂßÅËÔÏ×, ÔÁË É ÚÎÁÞÅÎÉÑ ËÌÀÞÅ×ÙÈ ÁÔÒÉÂÕÔÏ×; ÓÐÉÓÏË ÓÓÙÌÏË ÍÏÖÅÔ ÂÙÔØ\n"
+" ÐÕÓÔÏÊ ÓÔÒÏËÏÊ, ÏÂÏÚÎÁÞÅÎÉÅÍ (ÏÐÉÓÁÔÅÌÅÍ ÉÌÉ ËÌÀÞÅ×ÙÍ ÚÎÁÞÅÎÉÅÍ) ÏÂßÅËÔÁ\n"
+" ÉÌÉ ÓÐÉÓËÏÍ ÏÂÏÚÎÁÞÅÎÉÊ, ÒÁÚÄÅÌÅÎÎÙÈ ÚÁÐÑÔÙÍÉ.\n"
+"\n"
+"åÓÌÉ × ÚÎÁÞÅÎÉÑÈ ÁÔÒÉÂÕÔÏ× ×ÓÔÒÅÞÁÀÔÓÑ ÐÒÏÂÅÌÙ, ÔÁËÉÅ ÚÎÁÞÅÎÉÑ ÄÏÌÖÎÙ ÂÙÔØ\n"
+"ÚÁËÌÀÞÅÎÙ × ËÁ×ÙÞËÉ (ÏÄÉÎÁÒÎÙÅ ÉÌÉ Ä×ÏÊÎÙÅ - ×ÓÅ ÒÁ×ÎÏ). ïÄÉÎÏÞÎÙÊ ÐÒÏÂÅÌ\n"
+"ÍÏÖÎÏ \"ÚÁÜËÒÁÎÉÒÏ×ÁÔØ\" ÏÂÒÁÔÎÏÊ ËÏÓÏÊ ÞÅÒÔÏÊ. åÓÌÉ × ÚÎÁÞÅÎÉÉ "
+"×ÓÔÒÅÞÁÅÔÓÑ\n"
+"ËÁ×ÙÞËÁ, ÏÎÁ ÄÏÌÖÎÁ ÂÙÔØ ÚÁÜËÒÁÎÉÒÏ×ÁÎÁ ÏÂÒÁÔÎÏÊ ËÏÓÏÊ ÞÅÒÔÏÊ. ðÒÉÍÅÒÙ:\n"
+" hello world (2 ÓÌÏ×Á: hello, world)\n"
+" \"hello world\" (1 ÓÌÏ×Ï: hello world)\n"
+" \"Roch'e\" Compaan (2 ÓÌÏ×Á: Roch'e Compaan)\n"
+" Roch'e Compaan (2 ÓÌÏ×Á: Roch'e Compaan)\n"
+" address=\"1 2 3\" (1 ÓÌÏ×Ï: address=1 2 3)\n"
+" \\\\ (1 ÓÌÏ×Ï: \\)\n"
+" \\n\\r\\t (1 ÓÌÏ×Ï: ÐÅÒÅ×ÏÄ ÓÔÒÏËÉ, ×ÏÚ×ÒÁÔ ËÁÒÅÔËÉ É "
+"ÔÁÂÕÌÑÃÉÑ)\n"
+"\n"
+"åÓÌÉ × ËÏÍÁÎÄÅ get ÉÌÉ set ÕËÁÚÁÎÙ ÎÅÓËÏÌØËÏ ÏÂßÅËÔÏ×, ÚÁÐÒÏÛÅÎÎÙÅ ÁÔÒÉÂÕÔÙ\n"
+"ÂÕÄÕÔ ×ÙÄÁÎÙ ÉÌÉ ÕÓÔÁ×ÎÏ×ÌÅÎÙ ÄÌÑ ËÁÖÄÏÇÏ ÏÂßÅËÔÁ × ÓÐÉÓËÅ.\n"
+"\n"
+"åÓÌÉ ËÏÍÁÎÄÁ get ÉÌÉ find ×ÏÚ×ÒÁÞÁÅÔ ÎÅÓËÏÌØËÏ ÒÅÚÕÌØÔÁÔÏ×, ÏÎÉ ÏÂÙÞÎÏ\n"
+"ÐÅÞÁÔÁÀÔÓÑ ÐÏ ÏÄÎÏÍÕ × ËÁÖÄÏÊ ÓÔÒÏËÅ. åÓÌÉ ÕËÁÚÁÎ ËÌÀÞ \"-c\", ÒÅÚÕÌØÔÁÔÙ\n"
+"ÐÅÞÁÔÁÀÔÓÑ ÞÅÒÅÚ ÚÁÐÑÔÕÀ.\n"
+"\n"
+"ëÏÍÁÎÄÙ, ÉÚÍÅÎÑÀÝÉÅ ÂÁÚÕ ÄÁÎÎÙÈ, ÔÒÅÂÕÀÔ ÕËÁÚÁÎÉÑ ÉÍÅÎÉ ÐÏÌØÚÏ×ÁÔÅÌÑ\n"
+"É ÐÁÒÏÌÑ. ïÎÉ ÍÏÇÕÔ ÂÙÔØ ÕËÁÚÁÎÙ × ×ÉÄÅ \"ÉÍÑ\" ÉÌÉ \"ÉÍÑ:ÐÁÒÏÌØ\":\n"
+" . × ÐÅÒÅÍÅÎÎÏÊ ÏËÒÕÖÅÎÉÑ ROUNDUP_LOGIN\n"
+" . × ÐÁÒÁÍÅÔÒÅ ËÌÀÞÁ ËÏÍÁÎÄÎÏÊ ÓÔÒÏËÉ \"-u\"\n"
+"åÓÌÉ ÉÍÑ ÉÌÉ ÐÁÒÏÌØ ÎÅ ÕËÁÚÁÎÙ, ÐÒÏÇÒÁÍÍÁ ÐÏÐÒÏÓÉÔ ××ÅÓÔÉ ÉÈ Ó ÔÅÒÍÉÎÁÌÁ.\n"
+"\n"
+"ðÒÉÍÅÒÙ ÚÁÐÉÓÉ ÄÁÔ:\n"
+" \"2000-04-17.03:45\" ÏÚÎÁÞÁÅÔ <Date 2000-04-17.08:45:00>\n"
+" \"2000-04-17\" ÏÚÎÁÞÁÅÔ <Date 2000-04-17.00:00:00>\n"
+" \"01-25\" ÏÚÎÁÞÁÅÔ <Date yyyy-01-25.00:00:00>\n"
+" \"08-13.22:13\" ÏÚÎÁÞÁÅÔ <Date yyyy-08-14.03:13:00>\n"
+" \"11-07.09:32:43\" ÏÚÎÁÞÁÅÔ <Date yyyy-11-07.14:32:43>\n"
+" \"14:25\" ÏÚÎÁÞÁÅÔ <Date yyyy-mm-dd.19:25:00>\n"
+" \"8:47:11\" ÏÚÎÁÞÁÅÔ <Date yyyy-mm-dd.13:47:11>\n"
+" \".\" ÏÚÎÁÞÁÅÔ \"ÓÅÊÞÁÓ\"\n"
+"\n"
+"óÐÒÁ×ËÁ ÐÏ ËÏÍÁÎÄÁÍ:\n"
+
+#: ../roundup/admin.py:238
+#, python-format
+msgid "%s:"
+msgstr ""
+
+#: ../roundup/admin.py:243
+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 ""
+"÷ÙÚÏ×: help <ÔÅÍÁ>\n"
+" ÷ÙÄÁÔØ ÓÐÒÁ×ËÕ ÐÏ ÕËÁÚÁÎÎÏÊ ÔÅÍÅ.\n"
+"\n"
+" commands -- ÓÐÉÓÏË ËÏÍÁÎÄ\n"
+" <ËÏÍÁÎÄÁ> -- ÓÐÒÁ×ËÁ ÐÏ ÕËÁÚÁÎÎÏÊ ËÏÍÁÎÄÅ\n"
+" initopts -- ËÌÀÞÉ ËÏÍÁÎÄÙ 'init'\n"
+" all -- ×ÓÅ ÓÐÒÁ×ËÉ\n"
+" "
+
+#: ../roundup/admin.py:266
+#, python-format
+msgid "Sorry, no help for \"%(topic)s\""
+msgstr "é×ÉÎÉÔÅ, ÓÐÒÁ×ËÁ \"%(topic)s\" ÎÅ ÓÕÝÅÓÔ×ÕÅÔ."
+
+#: ../roundup/admin.py:338 ../roundup/admin.py:394
+msgid "Templates:"
+msgstr "ûÁÂÌÏÎÙ:"
+
+#: ../roundup/admin.py:341 ../roundup/admin.py:405
+msgid "Back ends:"
+msgstr "óÅÒ×ÅÒÙ:"
+
+#: ../roundup/admin.py:344
+msgid ""
+"Usage: install [template [backend [admin password [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"
+"\n"
+" The last command line argument allows to pass initial values\n"
+" 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 ""
+"÷ÙÚÏ×: install [ÛÁÂÌÏÎ [ÓÅÒ×ÅÒ [ÐÁÒÏÌØ [ËÌÀÞ=ÚÎÁÞÅÎÉÅ[,ËÌÀÞ=ÚÎÁÞÅÎÉÅ]]]]]\n"
+" õÓÔÁÎÏ×ÉÔØ ÎÏ×ÙÊ ÔÒÅËÅÒ Roundup.\n"
+"\n"
+" ÷ÁÍ ÎÁÄÏ ÂÕÄÅÔ ÕËÁÚÁÔØ \"ÄÏÍÁÛÎÉÊ ËÁÔÁÌÏÇ\" ÔÒÅËÅÒÁ (ÅÓÌÉ ÏÎ\n"
+" ÎÅ ÚÁÄÁÎ ÐÅÒÅÍÅÎÎÏÊ ÏËÒÕÖÅÎÉÑ TRACKER_HOME ÉÌÉ ËÌÀÞÏÍ ËÏÍÁÎÄÎÏÊ\n"
+" ÓÔÒÏËÉ '-i'). ûÁÂÌÏÎ ÔÒÅËÅÒÁ, ÔÉÐ ÂÁÚÙ ÄÁÎÎÙÈ É ÐÁÒÏÌØ\n"
+" ÁÄÍÉÎÉÓÔÒÁÔÏÒÁ ÍÏÖÎÏ ÕËÁÚÁÔØ × ÐÁÒÁÍÅÔÒÁÈ ËÏÍÁÎÄÎÏÊ ÓÔÒÏËÉ\n"
+" ÉÌÉ ××ÅÓÔÉ × ÏÔ×ÅÔ ÎÁ ÓÏÏÔ×ÅÔÓÔ×ÕÀÝÉÅ ÐÏÄÓËÁÚËÉ ÐÒÏÇÒÁÍÍÙ.\n"
+"\n"
+" ðÏÓÌÅÄÎÉÊ ÐÁÒÁÍÅÔÒ ËÏÍÁÎÄÎÏÊ ÓÔÒÏËÉ ÐÏÚ×ÏÌÑÅÔ ÚÁÄÁÔØ ÎÁÞÁÌØÎÙÅ\n"
+" ÚÎÁÞÅÎÉÑ ÄÌÑ ÆÁÊÌÁ ËÏÎÆÉÇÕÒÁÃÉÉ Roundup. îÁÐÒÉÍÅÒ, ÓÔÒÏËÁ\n"
+" \"web_http_auth=no,rdbms_user=dinsdale\" ÚÁÍÅÎÉÔ ÚÎÁÞÅÎÉÅ\n"
+" ÐÁÒÁÍÅÔÒÁ http_auth × ÓÅËÃÉÉ [web] É ÐÁÒÁÍÅÔÒÁ user × ÓÅËÃÉÉ\n"
+" [rdbms]. âÕÄØÔÅ ×ÎÉÍÁÔÅÌØÎÙ: ÎÁÓÔÒÏÊËÉ ÎÕÖÎÏ ÕËÁÚÙ×ÁÔØ ÐÏÄÒÑÄ,\n"
+" ÂÅÚ ÐÒÏÂÅÌÏ×. åÓÌÉ ÚÎÁÞÅÎÉÅ ÐÁÒÁÍÅÔÒÁ ÎÁÓÔÒÏÊËÉ Roundup ÄÏÌÖÎÏ\n"
+" ÓÏÄÅÒÖÁÔØ ÐÒÏÂÅÌ, ÚÁËÌÀÞÉÔÅ ×ÅÓØ ÐÁÒÁÍÅÔÒ ËÏÍÁÎÄÎÏÊ ÓÔÒÏËÉ\n"
+" × ËÁ×ÙÞËÉ.\n"
+"\n"
+" ðÏÓÌÅ ÜÔÏÊ ËÏÍÁÎÄÙ ÎÕÖÎÏ ×ÙÚ×ÁÔØ ËÏÍÁÎÄÕ 'initialise', ÞÔÏÂÙ\n"
+" ÓÏÚÄÁÔØ ÂÁÚÕ ÄÁÎÎÙÈ ÔÒÅËÅÒÁ. ÷Ù ÍÏÖÅÔÅ ÐÒÅÄ×ÁÒÉÔÅÌØÎÏ ÉÚÍÅÎÉÔØ\n"
+" ÓÈÅÍÕ ÂÁÚÙ ÄÁÎÎÙÈ, ËÏÔÏÒÁÑ ÏÐÉÓÁÎÁ × ÆÕÎËÃÉÉ init() ÍÏÄÕÌÑ\n"
+" dbinit.py.\n"
+"\n"
+" óÍ.ÔÁËÖÅ \"help initopts\".\n"
+" "
+
+#: ../roundup/admin.py:367 ../roundup/admin.py:464 ../roundup/admin.py:525
+#: ../roundup/admin.py:604 ../roundup/admin.py:654 ../roundup/admin.py:712
+#: ../roundup/admin.py:733 ../roundup/admin.py:761 ../roundup/admin.py:832
+#: ../roundup/admin.py:899 ../roundup/admin.py:970 ../roundup/admin.py:1018
+#: ../roundup/admin.py:1040 ../roundup/admin.py:1067 ../roundup/admin.py:1134
+#: ../roundup/admin.py:1204
+msgid "Not enough arguments supplied"
+msgstr "îÅÄÏÓÔÁÔÏÞÎÏ ÁÒÇÕÍÅÎÔÏ×"
+
+#: ../roundup/admin.py:373
+#, python-format
+msgid "Instance home parent directory \"%(parent)s\" does not exist"
+msgstr "ëÁÔÁÌÏÇ \"%(parent)s\" ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
+
+#: ../roundup/admin.py:381
+#, 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 ""
+"÷îéíáîéå: × ËÁÔÁÌÏÇÅ \"%(tracker_home)s\" ÏÂÎÁÒÕÖÅÎ ÓÕÝÅÓÔ×ÕÀÝÉÊ ÔÒÅËÅÒ!\n"
+"ðÏ×ÔÏÒÎÁÑ ÕÓÔÁÎÏ×ËÁ ÕÎÉÞÔÏÖÉÔ ×ÓÅ ×ÁÛÉ ÄÁÎÎÙÅ!\n"
+"õÄÁÌÉÔØ ÓÕÝÅÓÔ×ÕÀÝÉÊ ÔÒÅËÅÒ? Y/N: "
+
+#: ../roundup/admin.py:396
+msgid "Select template [classic]: "
+msgstr "÷ÙÂÅÒÉÔÅ ÛÁÂÌÏÎ [classic]: "
+
+#: ../roundup/admin.py:407
+msgid "Select backend [anydbm]: "
+msgstr "÷ÙÂÅÒÉÔÅ ÓÅÒ×ÅÒ [anydbm]: "
+
+#: ../roundup/admin.py:417
+#, python-format
+msgid "Error in configuration settings: \"%s\""
+msgstr "ïÛÉÂËÁ × ÐÁÒÁÍÅÔÒÁÈ ËÏÎÆÉÇÕÒÁÃÉÉ: \"%s\""
+
+#: ../roundup/admin.py:426
+#, python-format
+msgid ""
+"\n"
+"---------------------------------------------------------------------------\n"
+" You should now edit the tracker configuration file:\n"
+" %(config_file)s"
+msgstr ""
+"\n"
+"---------------------------------------------------------------------------\n"
+" ôÅÐÅÒØ ×ÁÍ ÎÕÖÎÏ ÉÓÐÒÁ×ÉÔØ ËÏÎÆÉÇÕÒÁÃÉÏÎÎÙÊ ÆÁÊÌ ÔÒÅËÅÒÁ:\n"
+" %(config_file)s"
+
+#: ../roundup/admin.py:436
+msgid " ... at a minimum, you must set following options:"
+msgstr " ... ËÁË ÍÉÎÉÍÕÍ, ×Ù ÄÏÌÖÎÙ ÕÓÔÁÎÏ×ÉÔØ ÎÁÓÔÒÏÊËÉ:"
+
+# õËÁÚÁÎÏ ÁÎÇÌÉÊÓËÏÅ ÎÁÚ×ÁÎÉÅ ÄÏËÕÍÅÎÔÁ
+#: ../roundup/admin.py:441
+#, 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 ""
+"\n"
+" åÓÌÉ ×Ù ÈÏÔÉÔÅ ÉÚÍÅÎÉÔØ ÓÔÁÎÄÁÒÔÎÕÀ ÓÈÅÍÕ ÂÁÚÙ ÄÁÎÎÙÈ,\n"
+" ×ÎÅÓÉÔÅ ÉÚÍÅÎÅÎÉÑ × ÆÁÊÌ ËÏÎÆÉÇÕÒÁÃÉÉ ÂÁÚÙ:\n"
+" %(database_config_file)s\n"
+" ÷Ù ÔÁËÖÅ ÍÏÖÅÔÅ ÉÚÍÅÎÉÔØ ÆÁÊÌ ÐÅÒ×ÏÎÁÞÁÌØÎÏÊ ÚÁÇÒÕÚËÉ ÄÁÎÎÙÈ:\n"
+" %(database_init_file)s\n"
+" ï ÔÏÍ, ËÁË ÜÔÏ ÄÅÌÁÔØ, ÒÁÓÓËÁÚÁÎÏ × ÄÏËÕÍÅÎÔÅ \"Customising Roundup\".\n"
+"\n"
+" ðÏÓÌÅ ÜÔÏÇÏ ×Ù ÄÏÌÖÎÙ ×ÙÐÏÌÎÉÔØ ËÏÍÁÎÄÕ \"roundup-admin initialise\".\n"
+"---------------------------------------------------------------------------\n"
+
+#: ../roundup/admin.py:459
+msgid ""
+"Usage: genconfig <filename>\n"
+" Generate a new tracker config file (ini style) with default values\n"
+" in <filename>.\n"
+" "
+msgstr ""
+"÷ÙÚÏ×: genconfig <ÉÍÑ ÆÁÊÌÁ>\n"
+" óÏÚÄÁÔØ ÎÏ×ÙÊ ËÏÎÆÉÇÕÒÁÃÉÏÎÎÙÊ ÆÁÊÌ ÔÒÅËÅÒÁ,\n"
+" ÉÓÐÏÌØÚÕÑ ÎÁÓÔÒÏÊËÉ ÐÏ ÕÍÏÌÞÁÎÉÀ.\n"
+" "
+
+# password
+#. password
+#: ../roundup/admin.py:469
+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 ""
+"÷ÙÚÏ×: initialise [ÐÁÒÏÌØ]\n"
+" ðÒÏÉÎÉÃÉÁÌÉÚÉÒÏ×ÁÔØ ÎÏ×ÙÊ ÔÒÅËÅÒ Roundup.\n"
+"\n"
+" îÁ ÜÔÏÍ ÛÁÇÅ ÚÁÐÏÌÎÑÅÔÓÑ ÕÞÅÔÎÁÑ ËÁÒÔÏÞËÁ ÁÄÍÉÎÉÓÔÒÁÔÏÒÁ.\n"
+"\n"
+" éÎÉÃÉÁÌÉÚÁÃÉÑ ÔÒÅËÅÒÁ ÄÅÌÁÅÔÓÑ ÆÕÎËÃÉÅÊ dbinit.init()\n"
+" "
+
+#: ../roundup/admin.py:483
+msgid "Admin Password: "
+msgstr "ðÁÒÏÌØ ÁÄÍÉÎÉÓÔÒÁÔÏÒÁ: "
+
+#: ../roundup/admin.py:484
+msgid " Confirm: "
+msgstr " åÝÅ ÒÁÚ: "
+
+#: ../roundup/admin.py:488
+msgid "Instance home does not exist"
+msgstr "äÏÍÁÛÎÉÊ ËÁÔÁÌÏÇ ÔÒÅËÅÒÁ ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
+
+#: ../roundup/admin.py:492
+msgid "Instance has not been installed"
+msgstr "ôÒÅËÅÒ ÎÅ ÕÓÔÁÎÏ×ÌÅÎ"
+
+#: ../roundup/admin.py:497
+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 ""
+"÷îéíáîéå: ÂÁÚÁ ÄÁÎÎÙÈ ÕÖÅ ÂÙÌÁ ÐÒÏÉÎÉÃÉÁÌÉÚÉÒÏ×ÁÎÁ!\n"
+"ðÏ×ÔÏÒÎÁÑ ÉÎÉÃÉÁÌÉÚÁÃÉÑ ÕÎÉÞÔÏÖÉÔ ×ÓÅ ×ÁÛÉ ÄÁÎÎÙÅ!\n"
+"õÄÁÌÉÔØ ÓÕÝÅÓÔ×ÕÀÝÕÀ ÂÁÚÕ? Y/N: "
+
+#: ../roundup/admin.py:518
+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 ""
+"÷ÙÚÏ×: get ÁÔÒÉÂÕÔ ÏÐÉÓÁÔÅÌØ[,ÏÐÉÓÁÔÅÌØ]*\n"
+" ðÏÌÕÞÉÔØ ÚÎÁÞÅÎÉÅ ÕËÁÚÁÎÎÏÇÏ ÁÔÒÉÂÕÔÁ ÏÄÎÏÇÏ ÉÌÉ ÎÅÓËÏÌØËÉÈ\n"
+" ÏÂßÅËÔÏ×.\n"
+"\n"
+" ÷ÙÄÁÅÔ ÚÎÁÞÅÎÉÅ ÕËÁÚÁÎÎÏÇÏ ÁÔÒÉÂÕÔÁ ÄÌÑ ×ÓÅÈ ÏÂßÅËÔÏ×,\n"
+" ÐÅÒÅÞÉÓÌÅÎÎÙÈ × ÓÐÉÓËÅ ÏÐÉÓÁÔÅÌÅÊ.\n"
+" "
+
+#: ../roundup/admin.py:558 ../roundup/admin.py:573
+#, python-format
+msgid "property %s is not of type Multilink or Link so -d flag does not apply."
+msgstr ""
+"ëÌÀÞ '-d' ÎÅÐÒÉÍÅÎÉÍ, ÐÏÔÏÍÕ ÞÔÏ ÔÉÐ ÁÔÒÉÂÕÔÁ %s - ÎÅ Link É ÎÅ Multilink"
+
+#: ../roundup/admin.py:581 ../roundup/admin.py:981 ../roundup/admin.py:1030
+#: ../roundup/admin.py:1052
+#, python-format
+msgid "no such %(classname)s node \"%(nodeid)s\""
+msgstr "÷ ËÌÁÓÓÅ %(classname)s ÎÅÔ ÏÂßÅËÔÁ \"%(nodeid)s\""
+
+#: ../roundup/admin.py:583
+#, python-format
+msgid "no such %(classname)s property \"%(propname)s\""
+msgstr "õ ËÌÁÓÓÁ %(classname)s ÎÅÔ ÁÔÒÉÂÕÔÁ \"%(propname)s\""
+
+#: ../roundup/admin.py:592
+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 ""
+"÷ÙÚÏ×: set items ÁÔÒÉÂÕÔ=ÚÎÁÞÅÎÉÅ ÁÔÒÉÂÕÔ=ÚÎÁÞÅÎÉÅ ...\n"
+" õÓÔÁÎÏ×ÉÔØ ÕËÁÚÁÎÎÙÅ ÁÔÒÉÂÕÔÙ ÏÄÎÏÇÏ ÉÌÉ ÎÅÓËÏÌØËÉÈ ÏÂßÅËÔÏ×.\n"
+"\n"
+" ïÂßÅËÔÙ ÚÁÄÁÀÔÓÑ ÉÍÅÎÅÍ ËÌÁÓÓÁ ÉÌÉ ÓÐÉÓËÏÍ ÏÐÉÓÁÔÅÌÅÊ, ÒÁÚÄÅÌÅÎÎÙÈ\n"
+" ÚÁÐÑÔÙÍÉ. (îÁÐÒÉÍÅÒ, \"user1,user5\".)\n"
+"\n"
+" üÔÁ ËÏÍÁÎÄÁ ÐÒÉÓ×ÁÉ×ÁÅÔ ÕËÁÚÁÎÎÙÅ ÚÎÁÞÅÎÉÑ ÁÔÒÉÂÕÔÁÍ ×ÓÅÈ ÕËÁÚÁÎÎÙÈ\n"
+" ÏÂßÅËÔÏ×. åÓÌÉ ÚÎÁÞÅÎÉÅ ÎÅ ÕËÁÚÁÎÏ (Ô.Å. ÕËÁÚÁÎÏ \"ÁÔÒÉÂÕÔ=\"),\n"
+" ÁÔÒÉÂÕÔ ÏÞÉÝÁÅÔÓÑ. åÓÌÉ ÁÔÒÉÂÕÔ Ñ×ÌÑÅÔÓÑ ÓÐÉÓËÏÍ ÓÓÙÌÏË ÎÁ ÏÂßÅËÔÙ\n"
+" ÄÒÕÇÏÇÏ ËÌÁÓÓÁ (Multilink), × ÚÎÁÞÅÎÉÉ ÄÏÌÖÎÙ ÂÙÔØ ÞÅÒÅÚ ÚÁÐÑÔÕÀ\n"
+" ÐÅÒÅÞÉÓÌÅÎÙ ÉÄÅÎÔÉÆÉËÁÔÏÒÙ ÏÂßÅËÔÏ×, ÎÁ ËÏÔÏÒÙÅ ÓÓÙÌÁÅÔÓÑ ÜÔÏÔ\n"
+" ÁÔÒÉÂÕÔ. (îÁÐÒÉÍÅÒ, \"1,2,3\".)\n"
+" "
+
+#: ../roundup/admin.py:646
+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 ""
+"÷ÙÚÏ×: find ËÌÁÓÓ ÁÔÒÉÂÕÔ=ÚÎÁÞÅÎÉÅ ...\n"
+" îÁÊÔÉ ÏÂßÅËÔÙ ËÌÁÓÓÁ Ó ÄÁÎÎÙÍ ÚÎÁÞÅÎÉÅÍ ÓÓÙÌÏÞÎÏÇÏ ÁÔÒÉÂÕÔÁ.\n"
+"\n"
+" îÁÊÔÉ ÏÂßÅËÔÙ ÕËÁÚÁÎÎÏÇÏ ËÌÁÓÓÁ Ó ÄÁÎÎÙÍ ÚÎÁÞÅÎÉÅÍ ÓÓÙÌÏÞÎÏÇÏ\n"
+" ÁÔÒÉÂÕÔÁ. úÎÁÞÅÎÉÅ ÍÏÖÅÔ ÂÙÔØ ÉÄÅÎÔÉÆÉËÁÔÏÒÏÍ ÏÂßÅËÔÁ, ÎÁ\n"
+" ËÏÔÏÒÙÊ ÓÓÙÌÁÅÔÓÑ ÁÔÒÉÂÕÔ, ÉÌÉ ËÌÀÞÏÍ ÜÔÏÇÏ ÏÂßÅËÔÁ.\n"
+" "
+
+#: ../roundup/admin.py:699 ../roundup/admin.py:852 ../roundup/admin.py:864
+#: ../roundup/admin.py:918
+#, python-format
+msgid "%(classname)s has no property \"%(propname)s\""
+msgstr "ëÌÁÓÓ %(classname)s ÎÅ ÉÍÅÅÔ ÁÔÒÉÂÕÔÁ \"%(propname)s\""
+
+#: ../roundup/admin.py:706
+msgid ""
+"Usage: specification classname\n"
+" Show the properties for a classname.\n"
+"\n"
+" This lists the properties for a given class.\n"
+" "
+msgstr ""
+"÷ÙÚÏ×: specification ËÌÁÓÓ\n"
+" ðÏËÁÚÁÔØ ÁÔÒÉÂÕÔÙ ËÌÁÓÓÁ.\n"
+"\n"
+" ÷ÙÄÁÅÔ ÓÐÉÓÏË ÁÔÒÉÂÕÔÏ× ÕËÁÚÁÎÎÏÇÏ ËÌÁÓÓÁ.\n"
+" "
+
+#: ../roundup/admin.py:721
+#, python-format
+msgid "%(key)s: %(value)s (key property)"
+msgstr "%(key)s: %(value)s (ËÌÀÞÅ×ÏÊ ÁÔÒÉÂÕÔ)"
+
+#: ../roundup/admin.py:723
+#, python-format
+msgid "%(key)s: %(value)s"
+msgstr ""
+
+#: ../roundup/admin.py:726
+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 ""
+"÷ÙÚÏ×: display ÏÐÉÓÁÔÅÌØ[,ÏÐÉÓÁÔÅÌØ]*\n"
+" ðÏËÁÚÁÔØ ÚÎÁÞÅÎÉÑ ÁÔÒÉÂÕÔÏ× ÕËÁÚÁÎÎÙÈ ÏÂßÅËÔÏ×.\n"
+"\n"
+" ÷ÙÄÁÅÔ ÓÐÉÓÏË ÁÔÒÉÂÕÔÏ× É ÉÈ ÚÎÁÞÅÎÉÊ ÄÌÑ ÏÂßÅËÔÏ×,\n"
+" ÚÁÄÁÎÎÙÈ ÏÐÉÓÁÔÅÌÑÍÉ.\n"
+" "
+
+#: ../roundup/admin.py:750
+#, python-format
+msgid "%(key)s: %(value)r"
+msgstr ""
+
+#: ../roundup/admin.py:753
+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 ""
+"÷ÙÚÏ×: create ËÌÁÓÓ ÁÔÒÉÂÕÔ=ÚÎÁÞÅÎÉÅ ...\n"
+" óÏÚÄÁÔØ ÎÏ×ÙÊ ÏÂßÅËÔ ÕËÁÚÁÎÎÏÇÏ ËÌÁÓÓÁ.\n"
+"\n"
+" óÏÚÄÁÅÔ ÎÏ×ÙÊ ÏÂßÅËÔ ÕËÁÚÁÎÎÏÇÏ ËÌÁÓÓÁ É ÚÁÐÏÌÎÑÅÔ ÁÔÒÉÂÕÔÙ\n"
+" ÜÔÏÇÏ ÏÂßÅËÔÁ ÕËÁÚÁÎÎÙÍÉ ÚÎÁÞÅÎÉÑÍÉ.\n"
+" "
+
+#: ../roundup/admin.py:780
+#, python-format
+msgid "%(propname)s (Password): "
+msgstr " %(propname)s (ÐÁÒÏÌØ): "
+
+#: ../roundup/admin.py:782
+#, python-format
+msgid " %(propname)s (Again): "
+msgstr "%(propname)s (ÅÝÅ ÒÁÚ): "
+
+#: ../roundup/admin.py:784
+msgid "Sorry, try again..."
+msgstr "ðÁÒÏÌÉ ÎÅ ÓÏ×ÐÁÌÉ. ðÏÐÒÏÂÕÊÔÅ ÅÝÅ ÒÁÚ."
+
+#: ../roundup/admin.py:788
+#, python-format
+msgid "%(propname)s (%(proptype)s): "
+msgstr ""
+
+#: ../roundup/admin.py:806
+#, python-format
+msgid "you must provide the \"%(propname)s\" property."
+msgstr "áÔÒÉÂÕÔ \"%(propname)s\" ÄÏÌÖÅÎ ÂÙÔØ ÚÁÐÏÌÎÅÎ."
+
+#: ../roundup/admin.py:817
+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 ""
+"÷ÙÚÏ×: list ËÌÁÓÓ [ÁÔÒÉÂÕÔ]\n"
+" ÷ÙÄÁÔØ ÓÐÉÓÏË ÏÂßÅËÔÏ× ËÌÁÓÓÁ.\n"
+"\n"
+" ðÅÞÁÔÁÅÔ ÓÐÉÓÏË ×ÓÅÈ ÏÂßÅËÔÏ× ÕËÁÚÁÎÎÏÇÏ ËÌÁÓÓÁ.\n"
+" åÓÌÉ ÁÔÒÉÂÕÔ ÎÅ ÕËÁÚÁÎ, ÉÓÐÏÌØÚÕÅÔÓÑ \"ÔÉÔÕÌØÎÙÊ\" ÁÔÒÉÂÕÔ.\n"
+" ÷ ËÁÞÅÓÔ×Å \"ÔÉÔÕÌØÎÏÇÏ\" ÁÔÒÉÂÕÔÁ ÉÓÐÏÌØÚÕÅÔÓÑ ÐÅÒ×ÙÊ ÎÁÊÄÅÎÎÙÊ\n"
+" ÉÚ ÓÌÅÄÕÀÝÉÈ ÁÔÒÉÂÕÔÏ×: ËÌÀÞ, \"name\", \"title\" ÉÌÉ ÐÅÒ×ÙÊ\n"
+" ÉÚ ÁÔÒÉÂÕÔÏ× ËÌÁÓÓÁ × ÁÌÆÁ×ÉÔÎÏÍ ÐÏÒÑÄËÅ.\n"
+"\n"
+" ó ËÌÀÞÁÍÉ -c, -S ÉÌÉ -s, ÅÓÌÉ ÎÅ ÕËÁÚÁÎÏ ÉÍÑ ÁÔÒÉÂÕÔÁ, ÐÅÞÁÔÁÅÔ\n"
+" ÓÐÉÓÏË ÉÄÅÎÔÉÆÉËÁÔÏÒÏ× ÏÂßÅËÔÏ×. åÓÌÉ ÉÍÑ ÁÔÒÉÂÕÔÁ ÕËÁÚÁÎÏ,\n"
+" ×ÙÄÁÅÔ ÓÐÉÓÏË ÚÎÁÞÅÎÉÊ ÜÔÏÇÏ ÁÔÒÉÂÕÔÁ.\n"
+" "
+
+#: ../roundup/admin.py:830
+msgid "Too many arguments supplied"
+msgstr "ðÏÄÁÎÏ ÓÌÉÛËÏÍ ÍÎÏÇÏ ÐÁÒÁÍÅÔÒÏ×"
+
+#: ../roundup/admin.py:866
+#, python-format
+msgid "%(nodeid)4s: %(value)s"
+msgstr ""
+
+#: ../roundup/admin.py:870
+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 ""
+"÷ÙÚÏ×: table ËÌÁÓÓ [ÁÔÒÉÂÕÔ[,ÁÔÒÉÂÕÔ]*]\n"
+" ðÏÌÕÞÉÔØ ÓÐÉÓÏË ÏÂßÅËÔÏ× ËÌÁÓÓÁ × ×ÉÄÅ ÔÁÂÌÉÃÙ.\n"
+"\n"
+" ÷ÙÄÁÅÔ ÓÐÉÓÏË ×ÓÅÈ ÏÂßÅËÔÏ× ÕËÁÚÁÎÎÏÇÏ ËÌÁÓÓÁ. åÓÌÉ ÓÐÉÓÏË\n"
+" ÁÔÒÉÂÕÔÏ× ÎÅ ÕËÁÚÁÎ, ÐÅÞÁÔÁÅÔ ×ÓÅ ÁÔÒÉÂÕÔÙ. ðÏ ÕÍÏÌÞÁÎÉÀ,\n"
+" ÓÔÏÌÂÃÙ ÉÍÅÀÔ ÛÉÒÉÎÕ ÓÁÍÏÇÏ ÄÌÉÎÎÏÇÏ ÚÎÁÞÅÎÉÑ × ÜÔÏÍ ÓÔÏÌÂÃÅ.\n"
+" íÏÖÎÏ Ñ×ÎÏ ÕËÁÚÙ×ÁÔØ ÛÉÒÉÎÙ ÓÔÏÌÂÃÏ× × ×ÉÄÅ \"ÁÔÒÉÂÕÔ:ÛÉÒÉÎÁ\".\n"
+" îÁÐÒÉÍÅÒ::\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"
+" äÌÑ ÔÏÇÏ, ÞÔÏÂÙ ÓÔÏÌÂÅà ÉÍÅÌ ÛÉÒÉÎÕ ÚÁÇÏÌÏ×ËÁ, ÎÕÖÎÏ Ë ÉÍÅÎÉ\n"
+" ÁÔÒÉÂÕÔÁ ÄÏÂÁ×ÉÔØ Ä×ÏÅÔÏÞÉÅ, ÎÏ ÎÅ ÕËÁÚÙ×ÁÔØ ÛÉÒÉÎÕ. îÁÐÒÉÍÅÒ::\n"
+"\n"
+" roundup> table priority id,name:\n"
+" Id Name\n"
+" 1 fata\n"
+" 2 bug\n"
+" 3 usab\n"
+" 4 feat\n"
+"\n"
+" ÏÂÒÅÚÁÅÔ ÚÎÁÞÅÎÉÑ ÓÔÏÌÂÃÁ \"Name\" ÄÏ ÞÅÔÙÒÅÈ ÓÉÍ×ÏÌÏ×.\n"
+" "
+
+#: ../roundup/admin.py:914
+#, python-format
+msgid "\"%(spec)s\" not name:width"
+msgstr "úÎÁÞÅÎÉÅ \"%(spec)s\" ÄÏÌÖÎÏ ÂÙÔØ ÚÁÄÁÎÏ ËÁË ÉÍÑ:ÛÉÒÉÎÁ"
+
+#: ../roundup/admin.py:964
+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 ""
+"÷ÙÚÏ×: history ÏÐÉÓÁÔÅÌØ\n"
+" ðÏËÁÚÁÔØ ÉÓÔÏÒÉÀ ÏÂßÅËÔÁ.\n"
+"\n"
+" ÷ÙÄÁÅÔ ÓÐÉÓÏË ÐÒÏÔÏËÏÌØÎÙÈ ÓÏÏÂÝÅÎÉÊ ÄÌÑ ÏÂßÅËÔÁ,\n"
+" ÚÁÄÁÎÎÏÇÏ ÏÐÉÓÁÔÅÌÅÍ.\n"
+" "
+
+#: ../roundup/admin.py:985
+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 ""
+"÷ÙÚÏ×: commit\n"
+" óÏÈÒÁÎÉÔØ ÉÚÍÅÎÅÎÉÑ ÂÁÚÙ ÄÁÎÎÙÈ, ÓÄÅÌÁÎÎÙÅ × ÉÎÔÅÒÁËÔÉ×ÎÏÍ ÒÅÖÉÍÅ.\n"
+"\n"
+" éÚÍÅÎÅÎÉÑ, ×ÎÅÓÅÎÎÙÅ × ÉÎÔÅÒÁËÔÉ×ÎÏÍ ÒÅÖÉÍÅ, ÎÅ ÚÁÐÉÓÙ×ÁÀÔÓÑ × ÂÁÚÕ\n"
+" ÄÁÎÎÙÈ Á×ÔÏÍÁÔÉÞÅÓËÉ. ïÎÉ ÄÏÌÖÎÙ ÂÙÔØ ÐÒÉÎÕÄÉÔÅÌØÎÏ ÓÏÈÒÁÎÅÎÙ\n"
+" ÐÒÉ ÐÏÍÏÝÉ ÜÔÏÊ ËÏÍÁÎÄÙ.\n"
+"\n"
+" òÅÚÕÌØÔÁÔÙ ×ÙÐÏÌÎÅÎÉÑ ËÏÍÁÎÄÎÏÊ ÓÔÒÏËÉ ÚÁÐÉÓÙ×ÁÀÔÓÑ × ÂÁÚÕ ÄÁÎÎÙÈ\n"
+" Á×ÔÏÍÁÔÉÞÅÓËÉ, ÅÓÌÉ ÐÒÉ ×ÙÐÏÌÎÅÎÉÉ ËÏÍÁÎÄÙ ÎÅ ÐÒÏÉÚÏÛÌÏ ÏÛÉÂËÉ.\n"
+" "
+
+#: ../roundup/admin.py:999
+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 ""
+"÷ÙÚÏ×: rollback\n"
+" ïÔÍÅÎÉÔØ ÉÚÍÅÎÅÎÉÑ ÂÁÚÙ ÄÁÎÎÙÈ, ÓÄÅÌÁÎÎÙÅ × ÉÎÔÅÒÁËÔÉ×ÎÏÍ ÒÅÖÉÍÅ.\n"
+"\n"
+" éÚÍÅÎÅÎÉÑ, ×ÎÅÓÅÎÎÙÅ × ÉÎÔÅÒÁËÔÉ×ÎÏÍ ÒÅÖÉÍÅ, ÎÅ ÚÁÐÉÓÙ×ÁÀÔÓÑ × ÂÁÚÕ\n"
+" ÄÁÎÎÙÈ Á×ÔÏÍÁÔÉÞÅÓËÉ. ïÎÉ ÄÏÌÖÎÙ ÂÙÔØ ÐÒÉÎÕÄÉÔÅÌØÎÏ ÓÏÈÒÁÎÅÎÙ\n"
+" ÐÒÉ ÐÏÍÏÝÉ ËÏÍÁÎÄÙ commit. ëÏÍÁÎÄÁ rollback ÏÔÍÅÎÑÅÔ ×ÓÅ ÜÔÉ\n"
+" ÉÚÍÅÎÅÎÉÑ, ÔÁË ÞÔÏ ÂÁÚÁ ÄÁÎÎÙÈ ×ÏÚ×ÒÁÝÁÅÔÓÑ × ÓÏÓÔÏÑÎÉÅ, ËÏÔÏÒÏÅ\n"
+" ÂÙÌÏ × ÍÏÍÅÎÔ ÐÏÓÌÅÄÎÅÊ ÚÁÐÉÓÉ.\n"
+" "
+
+#: ../roundup/admin.py:1011
+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 ""
+"÷ÙÚÏ×: retire ÏÐÉÓÁÔÅÌØ[,ÏÐÉÓÁÔÅÌØ]*\n"
+" õÄÁÌÉÔØ ÕËÁÚÁÎÎÙÅ ÏÂßÅËÔÙ.\n"
+"\n"
+" üÔÁ ËÏÍÁÎÄÁ ÐÏÍÅÞÁÅÔ ÕËÁÚÁÎÎÙÅ ÏÂßÅËÔÙ ËÁË ÕÄÁÌÅÎÎÙÅ.\n"
+" õÄÁÌÅÎÎÙÅ ÏÂßÅËÔÙ ÎÅ ÐÏËÁÚÙ×ÁÀÔÓÑ × ÓÐÉÓËÁÈ, ×ÙÄÁ×ÁÅÍÙÈ\n"
+" ËÏÍÁÎÄÁÍÉ list É find, É ÉÈ ËÌÀÞÅ×ÙÅ ÚÎÁÞÅÎÉÑ ÍÏÇÕÔ ÂÙÔØ\n"
+" ÉÓÐÏÌØÚÏ×ÁÎÙ × ÄÒÕÇÉÈ ÏÂßÅËÔÁÈ.\n"
+" "
+
+#: ../roundup/admin.py:1034
+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 ""
+"÷ÙÚÏ×: restore ÏÐÉÓÁÔÅÌØ[,ÏÐÉÓÁÔÅÌØ]*\n"
+" ÷ÏÓÓÔÁÎÏ×ÉÔØ ÕÄÁÌÅÎÎÙÅ ÏÂßÅËÔÙ.\n"
+"\n"
+" ó ÚÁÄÁÎÎÙÈ ÏÂßÅËÔÏ× ÓÎÉÍÁÅÔÓÑ ÐÏÍÅÔËÁ ÕÄÁÌÅÎÉÑ, É ÜÔÉÍÉ ÏÂßÅËÔÁÍÉ\n"
+" ÍÏÖÎÏ ÐÏÌØÚÏ×ÁÔØÓÑ ÓÎÏ×Á.\n"
+" "
+
+#. grab the directory to export to
+#: ../roundup/admin.py:1056
+msgid ""
+"Usage: export [class[,class]] export_dir\n"
+" Export the database to colon-separated-value files.\n"
+"\n"
+" Optionally limit the export to just the names classes.\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 ""
+"÷ÙÚÏ×: export [ËÌÁÓÓ[,ËÌÁÓÓ]] ËÁÔÁÌÏÇ\n"
+" üËÓÐÏÒÔÉÒÏ×ÁÔØ ÂÁÚÕ ÄÁÎÎÙÈ × ÔÅËÓÔÏ×ÙÅ ÆÁÊÌÙ.\n"
+"\n"
+" åÓÌÉ ÓÐÉÓÏË ËÌÁÓÓÏ× ÎÅ ÚÁÄÁÎ, ÜËÓÐÏÒÔÉÒÕÀÔÓÑ ×ÓÅ ËÌÁÓÓÙ\n"
+" ÂÁÚÙ ÄÁÎÎÙÈ.\n"
+"\n"
+" üÔÁ ËÏÍÁÎÄÁ ÜËÓÐÏÒÔÉÒÕÅÔ ÄÁÎÎÙÅ ÉÚ ÂÁÚÙ ÔÒÅËÅÒÁ × ÔÅËÓÔÏ×ÙÅ ÆÁÊÌÙ\n"
+" × ÕËÁÚÁÎÎÏÍ ËÁÔÁÌÏÇÅ. äÌÑ ËÁÖÄÏÇÏ ÜËÓÐÏÒÔÉÒÕÅÍÏÇÏ ËÌÁÓÓÁ ÓÏÚÄÁÅÔÓÑ\n"
+" ÏÔÄÅÌØÎÙÊ ÜËÓÐÏÒÔÎÙÊ ÆÁÊÌ. äÌÑ ËÁÖÄÏÇÏ ÏÂßÅËÔÁ ËÌÁÓÓÁ ÓÏÚÄÁÅÔÓÑ\n"
+" ÓÔÒÏËÁ ÜËÓÐÏÒÔÎÏÇÏ ÆÁÊÌÁ. úÎÁÞÅÎÉÑ ÁÔÒÉÂÕÔÏ× ÒÁÚÄÅÌÑÀÔÓÑ\n"
+" Ä×ÏÅÔÏÞÉÑÍÉ.\n"
+" "
+
+#: ../roundup/admin.py:1114
+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 ""
+"÷ÙÚÏ×: import ËÁÔÁÌÏÇ\n"
+" éÍÐÏÒÔÉÒÏ×ÁÔØ ÂÁÚÕ ÄÁÎÎÙÈ ÉÚ ËÁÔÁÌÏÇÁ, ÓÏÄÅÒÖÁÝÅÇÏ ÆÁÊÌÙ\n"
+" × ÆÏÒÍÁÔÅ CSV (Comma Separated Values), ÐÏ Ä×Á ÆÁÊÌÁ ÎÁ ËÌÁÓÓ.\n"
+"\n"
+" ÷ ÉÍÐÏÒÔÅ ÕÞÁÓÔ×ÕÀÔ ÓÌÅÄÕÀÝÉÅ ÆÁÊÌÙ:\n"
+"\n"
+" <ËÌÁÓÓ>.csv\n"
+" üÔÏÔ ÆÁÊÌ ÄÏÌÖÅÎ ÓÏÄÅÒÖÁÔØ ÄÁÎÎÙÅ ËÌÁÓÓÁ (Ó ÕËÁÚÁÎÉÅÍ ÉÍÅÎ\n"
+" ÁÔÒÉÂÕÔÏ× × ÐÅÒ×ÏÊ ÓÔÒÏËÅ.)\n"
+" <ËÌÁÓÓ>-journals.csv\n"
+" óÏÄÅÒÖÉÔ ÐÒÏÔÏËÏÌÙ ÉÚÍÅÎÅÎÉÊ ÏÂßÅËÔÏ× ËÌÁÓÓÁ.\n"
+"\n"
+" éÍÐÏÒÔÉÒÕÅÍÙÅ ÏÂßÅËÔÙ ÉÍÅÀÔ ÉÄÅÎÔÉÆÉËÁÔÏÒÙ, ÕËÁÚÁÎÎÙÅ ×\n"
+" ÉÍÐÏÒÔÎÏÍ ÆÁÊÌÅ É ÚÁÍÅÝÁÀÔ ÏÂßÅËÔÙ Ó ÔÁËÉÍÉ ÖÅ ÉÄÅÎÔÉÆÉËÁÔÏÒÁÍÉ,\n"
+" ÓÕÝÅÓÔ×ÕÀÝÉÅ × ÂÁÚÅ ÄÁÎÎÙÈ.\n"
+"\n"
+" îÏ×ÙÅ ÏÂßÅËÔÙ ÄÏÂÁ×ÌÑÀÔÓÑ Ë ÓÕÝÅÓÔ×ÕÀÝÅÊ ÂÁÚÅ ÄÁÎÎÙÈ.\n"
+" åÓÌÉ ×Ù ÈÏÔÉÔÅ ÚÁÐÏÌÎÉÔØ ÂÁÚÕ ÔÏÌØËÏ ÉÍÐÏÒÔÉÒÕÅÍÙÍÉ ÏÂßÅËÔÁÍÉ,\n"
+" ×ÁÍ ÎÕÖÎÏ ÓÏÚÄÁÔØ ÎÏ×ÕÀ ÂÁÚÕ ÄÁÎÎÙÈ (ÉÌÉ, ÅÓÌÉ ÎÅ ÌÅÎØ, ÕÄÁÌÉÔØ\n"
+" ÉÚ ÓÕÝÅÓÔ×ÕÀÝÅÊ ÂÁÚÙ ×ÓÅ ÏÂßÅËÔÙ).\n"
+" "
+
+#: ../roundup/admin.py:1186
+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 ""
+"÷ÙÚÏ×: pack ÐÅÒÉÏÄ | ÄÁÔÁ\n"
+"\n"
+" õÄÁÌÉÔØ ÐÒÏÔÏËÏÌØÎÙÅ ÓÏÏÂÝÅÎÉÑ, ÈÒÁÎÑÝÉÅÓÑ ÄÏÌØÛÅ ÕËÁÚÁÎÎÏÇÏ\n"
+" ÐÅÒÉÏÄÁ ÉÌÉ ÓÏÚÄÁÎÎÙÅ ÄÏ ÕËÁÚÁÎÎÏÊ ÄÁÔÙ.\n"
+"\n"
+" ðÅÒÉÏÄ ÚÁÄÁÅÔÓÑ ÞÉÓÌÁÍÉ, Ë ËÏÔÏÒÙÍ ÄÏÂÁ×ÌÅÎÙ ÂÕË×Ù \"y\"\n"
+" (year - ÇÏÄ), \"m\" (month - ÍÅÓÑÃ), \"d\" (day - ÄÅÎØ)\n"
+" ÉÌÉ \"w\" (week - ÎÅÄÅÌÑ, ÓÅÍØ ÄÎÅÊ).\n"
+"\n"
+" \"3y\" ÏÚÎÁÞÁÅÔ ÔÒÉ ÇÏÄÁ\n"
+" \"2y 1m\" ÏÚÎÁÞÁÅÔ Ä×Á ÇÏÄÁ É ÏÄÉÎ ÍÅÓÑÃ\n"
+" \"1m 25d\" ÏÚÎÁÞÁÅÔ ÏÄÉÎ ÍÅÓÑÃ É 25 ÄÎÅÊ\n"
+" \"2w 3d\" ÏÚÎÁÞÁÅÔ Ä×Å ÎÅÄÅÌÉ É ÔÒÉ ÄÎÑ\n"
+"\n"
+" äÁÔÁ ÚÁÄÁÅÔÓÑ × ÆÏÒÍÁÔÅ \"YYYY-MM-DD\", ÎÁÐÒÉÍÅÒ:\n"
+" 2001-01-01\n"
+"\n"
+" "
+
+#: ../roundup/admin.py:1214
+msgid "Invalid format"
+msgstr "îÅÐÒÁ×ÉÌØÎÙÊ ÆÏÒÍÁÔ"
+
+#: ../roundup/admin.py:1224
+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 ""
+"÷ÙÚÏ×: reindex [ËÌÁÓÓ|ÏÐÒÅÄÅÌÉÔÅÌØ]*\n"
+" ðÅÒÅÉÎÄÅËÓÉÒÏ×ÁÔØ ÂÁÚÕ ÄÁÎÎÙÈ.\n"
+"\n"
+" üÔÁ ËÏÍÁÎÄÁ ÐÅÒÅÓÔÒÁÉ×ÁÅÔ ÉÎÄÅËÓÙ, ÉÓÐÏÌØÚÕÅÍÙÅ ÄÌÑ ÐÏÉÓËÁ × ÂÁÚÅ\n"
+" ÄÁÎÎÙÈ. ïÂÙÞÎÏ ÐÏÓÔÒÏÅÎÉÅ ÉÎÄÅËÓÏ× ÐÒÏÉÓÈÏÄÉÔ Á×ÔÏÍÁÔÉÞÅÓËÉ.\n"
+" "
+
+#: ../roundup/admin.py:1238
+#, python-format
+msgid "no such item \"%(designator)s\""
+msgstr "ÏÂßÅËÔ \"%(designator)s\" ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
+
+#: ../roundup/admin.py:1248
+msgid ""
+"Usage: security [Role name]\n"
+" Display the Permissions available to one or all Roles.\n"
+" "
+msgstr ""
+"÷ÙÚÏ×: security [ÒÏÌØ]\n"
+" ðÏËÁÚÁÔØ ÐÒÁ×Á, ×ÙÄÁÎÎÙÅ ÕËÁÚÁÎÎÏÊ ÒÏÌÉ ÉÌÉ ×ÓÅÍ ÓÕÝÅÓÔ×ÕÀÝÉÍ\n"
+" ÒÏÌÑÍ.\n"
+" "
+
+#: ../roundup/admin.py:1256
+#, python-format
+msgid "No such Role \"%(role)s\""
+msgstr "òÏÌØ \"%(role)s\" ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
+
+#: ../roundup/admin.py:1262
+#, python-format
+msgid "New Web users get the Roles \"%(role)s\""
+msgstr "îÏ×ÙÅ ÐÏÌØÚÏ×ÁÔÅÌÉ web ÐÏÌÕÞÁÀÔ ÒÏÌÉ \"%(role)s\""
+
+#: ../roundup/admin.py:1264
+#, python-format
+msgid "New Web users get the Role \"%(role)s\""
+msgstr "îÏ×ÙÅ ÐÏÌØÚÏ×ÁÔÅÌÉ web ÐÏÌÕÞÁÀÔ ÒÏÌØ \"%(role)s\""
+
+#: ../roundup/admin.py:1267
+#, python-format
+msgid "New Email users get the Roles \"%(role)s\""
+msgstr "îÏ×ÙÅ ÐÏÌØÚÏ×ÁÔÅÌÉ email ÐÏÌÕÞÁÀÔ ÒÏÌÉ \"%(role)s\""
+
+#: ../roundup/admin.py:1269
+#, python-format
+msgid "New Email users get the Role \"%(role)s\""
+msgstr "îÏ×ÙÅ ÐÏÌØÚÏ×ÁÔÅÌÉ email ÐÏÌÕÞÁÀÔ ÒÏÌØ \"%(role)s\""
+
+#: ../roundup/admin.py:1272
+#, python-format
+msgid "Role \"%(name)s\":"
+msgstr "òÏÌØ \"%(name)s\":"
+
+#: ../roundup/admin.py:1277
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\": %(properties)s only)"
+msgstr ""
+" %(description)s (%(name)s ÄÌÑ ËÌÁÓÓÁ \"%(klass)s\": ÔÏÌØËÏ Ó×ÏÊÓÔ×Á %"
+"(properties)s)"
+
+#: ../roundup/admin.py:1280
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
+msgstr " %(description)s (%(name)s ÔÏÌØËÏ ÄÌÑ ËÌÁÓÓÁ \"%(klass)s\")"
+
+#: ../roundup/admin.py:1283
+#, python-format
+msgid " %(description)s (%(name)s)"
+msgstr ""
+
+#: ../roundup/admin.py:1312
+#, python-format
+msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
+msgstr ""
+"ëÏÍÁÎÄÁ \"%(command)s\" ÎÅÉÚ×ÅÓÔÎÁ. (\"help commands\" ×ÙÄÁÅÔ ÓÐÉÓÏË ËÏÍÁÎÄ)"
+
+#: ../roundup/admin.py:1318
+#, python-format
+msgid "Multiple commands match \"%(command)s\": %(list)s"
+msgstr "\"%(command)s\" ÓÏÏÔ×ÅÔÓÔ×ÕÅÔ ÎÅÓËÏÌØËÉÍ ËÏÍÁÎÄÁÍ: %(list)s"
+
+#: ../roundup/admin.py:1325
+msgid "Enter tracker home: "
+msgstr "äÏÍÁÛÎÉÊ ËÁÔÁÌÏÇ ÔÒÅËÅÒÁ: "
+
+#: ../roundup/admin.py:1332 ../roundup/admin.py:1338 ../roundup/admin.py:1358
+#, python-format
+msgid "Error: %(message)s"
+msgstr "ïÛÉÂËÁ: %(message)s"
+
+#: ../roundup/admin.py:1346
+#, python-format
+msgid "Error: Couldn't open tracker: %(message)s"
+msgstr "ïÛÉÂËÁ: ôÒÅËÅÒ ÎÅ ÏÔËÒÙ×ÁÅÔÓÑ: %(message)s"
+
+#: ../roundup/admin.py:1371
+#, python-format
+msgid ""
+"Roundup %s ready for input.\n"
+"Type \"help\" for help."
+msgstr ""
+"Roundup %s Ë ×ÁÛÉÍ ÕÓÌÕÇÁÍ.\n"
+"÷×ÅÄÉÔÅ \"help\" ÄÌÑ ÓÐÒÁ×ËÉ."
+
+#: ../roundup/admin.py:1376
+msgid "Note: command history and editing not available"
+msgstr "ðÒÉÍÅÞÁÎÉÅ: ÒÁÂÏÔÁÅÔ ÒÅÄÁËÔÏÒ É ÉÓÔÏÒÉÑ ËÏÍÁÎÄ"
+
+#: ../roundup/admin.py:1380
+msgid "roundup> "
+msgstr ""
+
+#: ../roundup/admin.py:1382
+msgid "exit..."
+msgstr "ÐÒÉÈÏÄÉÔÅ Ë ÎÁÍ ÅÝÅ..."
+
+#: ../roundup/admin.py:1392
+msgid "There are unsaved changes. Commit them (y/N)? "
+msgstr "ïÊ, ÔÕÔ ÎÅÓÏÈÒÁÎÅÎÎÙÅ ÉÚÍÅÎÅÎÉÑ. úÁÐÉÓÁÔØ × ÂÁÚÕ ÄÁÎÎÙÈ (y/N)? "
+
+#: ../roundup/backends/back_anydbm.py:1997
+#, python-format
+msgid "WARNING: invalid date tuple %r"
+msgstr "÷îéíáîéå! îÅ×ÅÒÎÁÑ ÄÁÔÁ: %r"
+
+#: ../roundup/backends/rdbms_common.py:1434
+msgid "create"
+msgstr "ÓÏÚÄÁÎÉÅ"
+
+#: ../roundup/backends/rdbms_common.py:1600
+msgid "unlink"
+msgstr "ÏÔ×ÑÚËÁ"
+
+#: ../roundup/backends/rdbms_common.py:1604
+msgid "link"
+msgstr "ÐÒÉ×ÑÚËÁ"
+
+#: ../roundup/backends/rdbms_common.py:1724
+msgid "set"
+msgstr "ÕÓÔÁÎÏ×ËÁ"
+
+#: ../roundup/backends/rdbms_common.py:1748
+msgid "retired"
+msgstr "ÚÁÐÒÅÝÅÎÉÅ"
+
+#: ../roundup/backends/rdbms_common.py:1778
+msgid "restored"
+msgstr "×ÏÓÓÔÁÎÏ×ÌÅÎÉÅ"
+
+#: ../roundup/cgi/actions.py:58
+#, python-format
+msgid "You do not have permission to %(action)s the %(classname)s class."
+msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ %(action)s ÄÌÑ ËÌÁÓÓÁ %(classname)s."
+
+#: ../roundup/cgi/actions.py:89
+msgid "No type specified"
+msgstr "îÅ ÕËÁÚÁÎ ÔÉÐ"
+
+#: ../roundup/cgi/actions.py:91
+msgid "No ID entered"
+msgstr "îÅ ÕËÁÚÁÎ ÉÄÅÎÔÉÆÉËÁÔÏÒ"
+
+#: ../roundup/cgi/actions.py:97
+#, python-format
+msgid "\"%(input)s\" is not an ID (%(classname)s ID required)"
+msgstr ""
+"\"%(input)s\" - ÎÅ ÉÄÅÎÔÉÆÉËÁÔÏÒ (ÔÒÅÂÕÅÔÓÑ ÉÄÅÎÔÉÆÉËÁÔÏÒ ËÌÁÓÓÁ %(classname)"
+"s)"
+
+#: ../roundup/cgi/actions.py:117
+msgid "You may not retire the admin or anonymous user"
+msgstr "îÅÌØÚÑ ÕÄÁÌÑÔØ ÐÏÌØÚÏ×ÁÔÅÌÅÊ admin É anonymous."
+
+#: ../roundup/cgi/actions.py:124
+#, python-format
+msgid "%(classname)s %(itemid)s has been retired"
+msgstr "%(classname)s %(itemid)s ÕÄÁÌÅÎ"
+
+#: ../roundup/cgi/actions.py:174 ../roundup/cgi/actions.py:202
+msgid "You do not have permission to edit queries"
+msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÒÅÄÁËÔÉÒÏ×ÁÎÉÅ ÚÁÐÒÏÓÏ×"
+
+#: ../roundup/cgi/actions.py:180 ../roundup/cgi/actions.py:209
+msgid "You do not have permission to store queries"
+msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÓÏÈÒÁÎÅÎÉÅ ÚÁÐÒÏÓÏ×"
+
+#: ../roundup/cgi/actions.py:297
+#, python-format
+msgid "Not enough values on line %(line)s"
+msgstr "÷ ÓÔÒÏËÅ %(line)s ÎÅ È×ÁÔÁÅÔ ÚÎÁÞÅÎÉÊ"
+
+#: ../roundup/cgi/actions.py:344
+msgid "Items edited OK"
+msgstr "ïÂßÅËÔÙ ÉÚÍÅÎÅÎÙ ÕÓÐÅÛÎÏ"
+
+#: ../roundup/cgi/actions.py:404
+#, python-format
+msgid "%(class)s %(id)s %(properties)s edited ok"
+msgstr "éÚÍÅÎÅÎÙ ÁÔÒÉÂÕÔÙ %(properties)s ÏÂßÅËÔÁ %(class)s %(id)s"
+
+#: ../roundup/cgi/actions.py:407
+#, python-format
+msgid "%(class)s %(id)s - nothing changed"
+msgstr "%(class)s %(id)s - ÎÅÔ ÉÚÍÅÎÅÎÉÊ"
+
+#: ../roundup/cgi/actions.py:419
+#, python-format
+msgid "%(class)s %(id)s created"
+msgstr "%(class)s %(id)s ÓÏÚÄÁÎ"
+
+#: ../roundup/cgi/actions.py:451
+#, python-format
+msgid "You do not have permission to edit %(class)s"
+msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÒÅÄÁËÔÉÒÏ×ÁÔØ %(class)s"
+
+#: ../roundup/cgi/actions.py:463
+#, python-format
+msgid "You do not have permission to create %(class)s"
+msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÓÏÚÄÁ×ÁÔØ %(class)s"
+
+#: ../roundup/cgi/actions.py:487
+msgid "You do not have permission to edit user roles"
+msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÉÚÍÅÎÅÎÉÅ ÒÏÌÅÊ ÐÏÌØÚÏ×ÁÔÅÌÅÊ"
+
+#: ../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 ""
+"ïÛÉÂËÁ ÒÅÄÁËÔÉÒÏ×ÁÎÉÑ: %s (%s) ÉÚÍÅÎÉÌ ÄÒÕÇÏÊ ÐÏÌØÚÏ×ÁÔÅÌØ. <a target=\"new"
+"\" href=\"%s%s\">ðÒÏÓÍÏÔÒÅÔØ ÜÔÉ ÉÚÍÅÎÅÎÉÑ</a> × ÄÒÕÇÏÍ ÏËÎÅ."
+
+#: ../roundup/cgi/actions.py:565
+#, python-format
+msgid "Edit Error: %s"
+msgstr "ïÛÉÂËÁ ÒÅÄÁËÔÉÒÏ×ÁÎÉÑ: %s"
+
+#: ../roundup/cgi/actions.py:596 ../roundup/cgi/actions.py:607
+#: ../roundup/cgi/actions.py:778 ../roundup/cgi/actions.py:797
+#, python-format
+msgid "Error: %s"
+msgstr "ïÛÉÂËÁ: %s"
+
+#: ../roundup/cgi/actions.py:633
+msgid ""
+"Invalid One Time Key!\n"
+"(a Mozilla bug may cause this message to show up erroneously, please check "
+"your email)"
+msgstr ""
+"ëÌÀÞ ÐÏÄÔ×ÅÒÖÄÅÎÉÑ ÎÅÐÒÁ×ÉÌÅÎ!\n"
+"(éÚ-ÚÁ ÏÛÉÂËÉ × ÂÒÁÕÚÅÒÅ Mozilla ÜÔÏ ÓÏÏÂÝÅÎÉÅ ÍÏÖÅÔ ÂÙÔØ ÎÅ×ÅÒÎÙÍ. "
+"ðÒÏ×ÅÒØÔÅ ×ÁÛÕ ÐÏÞÔÕ, ÐÏÖÁÌÕÊÓÔÁ.)"
+
+#: ../roundup/cgi/actions.py:675
+#, python-format
+msgid "Password reset and email sent to %s"
+msgstr "ðÁÒÏÌØ ÓÂÒÏÛÅÎ. ðÏ ÁÄÒÅÓÕ %s ÏÔÐÒÁ×ÌÅÎÏ ÐÉÓØÍÏ."
+
+#: ../roundup/cgi/actions.py:684
+msgid "Unknown username"
+msgstr "îÅÉÚ×ÅÓÔÎÏÅ ÉÍÑ ÐÏÌØÚÏ×ÁÔÅÌÑ"
+
+#: ../roundup/cgi/actions.py:692
+msgid "Unknown email address"
+msgstr "îÅÉÚ×ÅÓÔÎÙÊ ÁÄÒÅÓ email"
+
+#: ../roundup/cgi/actions.py:697
+msgid "You need to specify a username or address"
+msgstr "÷Ù ÄÏÌÖÎÙ ÕËÁÚÁÔØ ÉÍÑ ÐÏÌØÚÏ×ÁÔÅÌÑ ÉÌÉ ÁÄÒÅÓ email"
+
+#: ../roundup/cgi/actions.py:722
+#, python-format
+msgid "Email sent to %s"
+msgstr "ðÉÓØÍÏ ÏÔÐÒÁ×ÌÅÎÏ ÎÁ %s"
+
+#: ../roundup/cgi/actions.py:741
+msgid "You are now registered, welcome!"
+msgstr "÷Ù ÚÁÒÅÇÉÓÔÒÉÒÏ×ÁÎÙ. äÏÂÒÏ ÐÏÖÁÌÏ×ÁÔØ!"
+
+#: ../roundup/cgi/actions.py:786
+msgid "It is not permitted to supply roles at registration."
+msgstr "îÅÌØÚÑ ÕËÁÚÙ×ÁÔØ ÒÏÌÉ ÐÒÉ ÒÅÇÉÓÔÒÁÃÉÉ"
+
+#: ../roundup/cgi/actions.py:878
+msgid "You are logged out"
+msgstr "óÅÁÎÓ ÒÁÂÏÔÙ ÚÁ×ÅÒÛÅÎ"
+
+#: ../roundup/cgi/actions.py:895
+msgid "Username required"
+msgstr "îÅ ÕËÁÚÁÎÏ ÉÍÑ ÐÏÌØÚÏ×ÁÔÅÌÑ"
+
+#: ../roundup/cgi/actions.py:930 ../roundup/cgi/actions.py:934
+msgid "Invalid login"
+msgstr "îÅÐÒÁ×ÉÌØÎÙÊ ÐÁÒÏÌØ ÉÌÉ ÉÍÑ ÐÏÌØÚÏ×ÁÔÅÌÑ."
+
+#: ../roundup/cgi/actions.py:940
+msgid "You do not have permission to login"
+msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÒÁÂÏÔÕ Ó ÓÉÓÔÅÍÏÊ"
+
+#: ../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>ïÛÉÂËÁ ÛÁÂÌÏÎÁ</h1>\n"
+"<p><b>%(exc_type)s</b>: %(exc_value)s</p>\n"
+"<p class=\"help\">éÎÆÏÒÍÁÃÉÑ Ï ÏÛÉÂËÅ:</p>"
+
+#: ../roundup/cgi/cgitb.py:64
+#, python-format
+msgid "<li>\"%(name)s\" (%(info)s)</li>"
+msgstr ""
+
+#: ../roundup/cgi/cgitb.py:67
+#, python-format
+msgid "<li>Looking for \"%(name)s\", current path:<ol>%(path)s</ol></li>"
+msgstr "<li>óÉÍ×ÏÌ \"%(name)s\" ÎÅ ÎÁÊÄÅÎ × ÐÕÔÉ:<ol>%(path)s</ol><li>"
+
+#: ../roundup/cgi/cgitb.py:71
+#, python-format
+msgid "<li>In %s</li>"
+msgstr "<li>÷ %s</li>"
+
+#: ../roundup/cgi/cgitb.py:76
+#, python-format
+msgid "A problem occurred in your template \"%s\"."
+msgstr "ïÛÉÂËÁ ÐÒÏÉÚÏÛÌÁ ÐÒÉ ÏÂÒÁÂÏÔËÅ ÛÁÂÌÏÎÁ \"%s\"."
+
+#: ../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>ðÒÉ ×ÙÞÉÓÌÅÎÉÉ ×ÙÒÁÖÅÎÉÑ %(info)r × ÓÔÒÏËÅ %(line)d\n"
+"<table class=\"otherinfo\" style=\"font-size: 90%%\">\n"
+" <tr><th colspan=\"2\" class=\"header\">ïÐÒÅÄÅÌÅÎÙ ÓÌÅÄÕÀÝÉÅ ÐÅÒÅÍÅÎÎÙÅ:</"
+"th></tr>\n"
+" %(globals)s\n"
+" %(locals)s\n"
+"</table></li>\n"
+
+#: ../roundup/cgi/cgitb.py:103
+msgid "Full traceback:"
+msgstr "óÔÅË ×ÙÚÏ×Ï×:"
+
+#: ../roundup/cgi/cgitb.py:116
+#, python-format
+msgid "<font size=+1><strong>%(exc_type)s</strong>: %(exc_value)s</font>"
+msgstr ""
+
+#: ../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>ðÒÉ ×ÙÐÏÌÎÅÎÉÉ ÐÒÏÇÒÁÍÍÙ ÐÒÏÉÚÏÛÌÁ ÏÛÉÂËÁ. îÉÖÅ ÐÒÉ×ÅÄÅÎÁ "
+"ÐÏÓÌÅÄÏ×ÁÔÅÌØÎÏÓÔØ ×ÙÚÏ×Ï× ÆÕÎËÃÉÊ, ËÏÔÏÒÁÑ ÐÒÉ×ÅÌÁ Ë ÏÛÉÂËÅ. æÕÎËÃÉÑ, × "
+"ËÏÔÏÒÏÊ ÐÒÏÉÚÏÛÌÁ ÏÛÉÂËÁ, - ÐÏÓÌÅÄÎÑÑ ×ÙÚ×ÁÎÎÁÑ ÆÕÎËÃÉÑ - ÐÏËÁÚÁÎÁ ÐÅÒ×ÏÊ. "
+"éÎÆÏÒÍÁÃÉÑ Ï ÏÛÉÂËÅ:"
+
+#: ../roundup/cgi/cgitb.py:129
+msgid "<file is None - probably inside <tt>eval</tt> or <tt>exec</tt>>"
+msgstr ""
+"<ÉÍÑ ÆÁÊÌÁ ÎÅ ÏÐÒÅÄÅÌÅÎÏ - ×ÅÒÏÑÔÎÏ ×ÙÚ×ÁÎÏ ÉÚ <tt>eval</tt> ÉÌÉ "
+"<tt>exec</tt>>"
+
+#: ../roundup/cgi/cgitb.py:138
+#, python-format
+msgid "in <strong>%s</strong>"
+msgstr "× <strong>%s</strong>"
+
+#: ../roundup/cgi/cgitb.py:172 ../roundup/cgi/cgitb.py:178
+msgid "<em>undefined</em>"
+msgstr "<em>ÎÅÏÐÒÅÄÅÌÅÎÏ</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>ïÛÉÂËÁ × ÔÒÅËÅÒÅ</title></head>\n"
+"<body><h1>ïÛÉÂËÁ × ÔÒÅËÅÒÅ</h1>\n"
+"<p>ðÒÉ ÏÂÒÁÂÏÔËÅ ÷ÁÛÅÇÏ ÚÁÐÒÏÓÁ ÐÒÏÉÚÏÛÌÁ ÏÛÉÂËÁ.\n"
+"áÄÍÉÎÉÓÔÒÁÔÏÒÕ ÔÒÅËÅÒÁ ÏÔÏÓÌÁÎÏ ÓÏÏÂÝÅÎÉÅ Ï ÏÛÉÂËÅ.</p>\n"
+"</body></html>"
+
+#: ../roundup/cgi/client.py:308
+msgid "Form Error: "
+msgstr "ïÛÉÂËÁ ÆÏÒÍÙ: "
+
+#: ../roundup/cgi/client.py:363
+#, python-format
+msgid "Unrecognized charset: %r"
+msgstr "ëÏÄÉÒÏ×ËÁ %r ÎÅ ÒÁÓÐÏÚÎÁÎÁ"
+
+#: ../roundup/cgi/client.py:490
+msgid "Anonymous users are not allowed to use the web interface"
+msgstr "áÎÏÎÉÍÎÙÍ ÐÏÌØÚÏ×ÁÔÅÌÑÍ ÎÅ ÒÁÚÒÅÛÅÎÏ ÐÏÌØÚÏ×ÁÔØÓÑ ×ÅÂ-ÉÎÔÅÒÆÅÊÓÏÍ."
+
+#: ../roundup/cgi/client.py:645
+msgid "You are not allowed to view this file."
+msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÐÒÏÓÍÏÔÒ ÜÔÏÇÏ ÆÁÊÌÁ."
+
+#: ../roundup/cgi/client.py:737
+#, python-format
+msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
+msgstr "%(starttag)súÁÔÒÁÞÅÎÎÏÅ ×ÒÅÍÑ: %(seconds)fs%(endtag)s\n"
+
+#: ../roundup/cgi/client.py:741
+#, python-format
+msgid ""
+"%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading "
+"items: %(get_items)f secs. Filtering: %(filtering)f secs.%(endtag)s\n"
+msgstr ""
+"%(starttag)sëÅÛÉÒÏ×ÁÎÎÙÅ ÜÌÅÍÅÎÔÙ: %(cache_hits)d, ×ÙÞÉÓÌÅÎÎÙÅ: %"
+"(cache_misses)d. úÁÇÒÕÚËÁ ÏÂßÅËÔÏ×: %(get_items)f ÓÅË. æÉÌØÔÒÁÃÉÑ: %"
+"(filtering)f ÓÅË.%(endtag)s\n"
+
+#: ../roundup/cgi/form_parser.py:283
+#, python-format
+msgid "link \"%(key)s\" value \"%(value)s\" not a designator"
+msgstr "úÎÁÞÅÎÉÅ \"%(value)s ÓÓÙÌËÉ \"%(key)s\" ÎÅ ÕËÁÚÙ×ÁÅÔ ÎÁ ÏÂßÅËÔ"
+
+#: ../roundup/cgi/form_parser.py:290
+#, python-format
+msgid "%(class)s %(property)s is not a link or multilink property"
+msgstr "áÔÒÉÂÕÔ %(property)s ËÌÁÓÓÁ %(class)s ÎÅ Ñ×ÌÑÅÔÓÑ ÓÓÙÌÏÞÎÙÍ"
+
+#: ../roundup/cgi/form_parser.py:312
+#, python-format
+msgid ""
+"You have submitted a %(action)s action for the property \"%(property)s\" "
+"which doesn't exist"
+msgstr ""
+"÷Ù ÚÁÐÒÏÓÉÌÉ ÄÅÊÓÔ×ÉÅ \"%(action)s\" ÄÌÑ ÁÔÒÉÂÕÔÁ \"%(property)s\", ËÏÔÏÒÙÊ "
+"ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
+
+#: ../roundup/cgi/form_parser.py:331 ../roundup/cgi/form_parser.py:357
+#, python-format
+msgid "You have submitted more than one value for the %s property"
+msgstr "÷Ù ××ÅÌÉ ÎÅÓËÏÌØËÏ ÚÎÁÞÅÎÉÊ ÄÌÑ ÁÔÒÉÂÕÔÁ %s"
+
+# ../roundup/cgi/form_parser.py:354 :360
+#: ../roundup/cgi/form_parser.py:354 ../roundup/cgi/form_parser.py:360
+msgid "Password and confirmation text do not match"
+msgstr "ðÁÒÏÌÉ ÎÅ ÓÏ×ÐÁÌÉ"
+
+#: ../roundup/cgi/form_parser.py:395
+#, python-format
+msgid "property \"%(propname)s\": \"%(value)s\" not currently in list"
+msgstr "ÁÔÒÉÂÕÔ \"%(propname)s\": ÚÎÁÞÅÎÉÅ \"%(value)s\" ÏÔÓÕÔÓÔ×ÕÅÔ × ÓÐÉÓËÅ"
+
+#: ../roundup/cgi/form_parser.py:512
+#, python-format
+msgid "Required %(class)s property %(property)s not supplied"
+msgid_plural "Required %(class)s properties %(property)s not supplied"
+msgstr[0] "ïÂÑÚÁÔÅÌØÎÙÊ ÁÔÒÉÂÕÔ %(property)s ËÌÁÓÓÁ %(class)s ÎÅ ÚÁÐÏÌÎÅÎ"
+msgstr[1] "ïÂÑÚÁÔÅÌØÎÙÅ ÁÔÒÉÂÕÔÙ %(property)s ËÌÁÓÓÁ %(class)s ÎÅ ÚÁÐÏÌÎÅÎÙ"
+msgstr[2] "ïÂÑÚÁÔÅÌØÎÙÅ ÁÔÒÉÂÕÔÙ %(property)s ËÌÁÓÓÁ %(class)s ÎÅ ÚÁÐÏÌÎÅÎÙ"
+
+#: ../roundup/cgi/form_parser.py:535
+msgid "File is empty"
+msgstr "æÁÊÌ ÐÕÓÔ"
+
+#: ../roundup/cgi/templating.py:72
+#, python-format
+msgid "You are not allowed to %(action)s items of class %(class)s"
+msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ %(action)s ÄÌÑ ËÌÁÓÓÁ %(class)s"
+
+#: ../roundup/cgi/templating.py:627
+msgid "(list)"
+msgstr "(ÓÐÉÓÏË)"
+
+#: ../roundup/cgi/templating.py:696
+msgid "Submit New Entry"
+msgstr "äÏÂÁ×ÉÔØ"
+
+# ../roundup/cgi/templating.py:673 :792 :1166 :1187 :1231 :1253 :1287 :1326
+# :1377 :1394 :1470 :1490 :1503 :1520 :1530 :1580 :1755
+#: ../roundup/cgi/templating.py:710 ../roundup/cgi/templating.py:829
+#: ../roundup/cgi/templating.py:1236 ../roundup/cgi/templating.py:1257
+#: ../roundup/cgi/templating.py:1304 ../roundup/cgi/templating.py:1327
+#: ../roundup/cgi/templating.py:1361 ../roundup/cgi/templating.py:1400
+#: ../roundup/cgi/templating.py:1453 ../roundup/cgi/templating.py:1470
+#: ../roundup/cgi/templating.py:1549 ../roundup/cgi/templating.py:1569
+#: ../roundup/cgi/templating.py:1587 ../roundup/cgi/templating.py:1619
+#: ../roundup/cgi/templating.py:1629 ../roundup/cgi/templating.py:1683
+#: ../roundup/cgi/templating.py:1875
+msgid "[hidden]"
+msgstr "[ÎÅÄÏÓÔÕÐÎÏ]"
+
+#: ../roundup/cgi/templating.py:711
+msgid "New node - no history"
+msgstr "îÏ×ÁÑ ËÁÒÔÏÞËÁ - ÎÅÔ ÉÓÔÏÒÉÉ"
+
+#: ../roundup/cgi/templating.py:811
+msgid "Submit Changes"
+msgstr "éÚÍÅÎÉÔØ"
+
+#: ../roundup/cgi/templating.py:893
+msgid "<em>The indicated property no longer exists</em>"
+msgstr "<em>õËÁÚÁÎÎÙÊ ÁÔÒÉÂÕÔ ÕÖÅ ÎÅ ÓÕÝÅÓÔ×ÕÅÔ.</em>"
+
+#: ../roundup/cgi/templating.py:894
+#, python-format
+msgid "<em>%s: %s</em>\n"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:907
+#, python-format
+msgid "The linked class %(classname)s no longer exists"
+msgstr "ó×ÑÚÑÎÎÙÊ ËÌÁÓÓ %(classname)s ÕÖÅ ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
+
+# :823
+#: ../roundup/cgi/templating.py:940 ../roundup/cgi/templating.py:964
+msgid "<strike>The linked node no longer exists</strike>"
+msgstr "<strike>ó×ÑÚÁÎÎÙÊ ÏÂßÅËÔ ÕÖÅ ÎÅ ÓÕÝÅÓÔ×ÕÅÔ</strike>"
+
+#: ../roundup/cgi/templating.py:1006 ../roundup/cgi/templating.py:1404
+#: ../roundup/cgi/templating.py:1425 ../roundup/cgi/templating.py:1431
+msgid "No"
+msgstr "îÅÔ"
+
+#: ../roundup/cgi/templating.py:1006 ../roundup/cgi/templating.py:1404
+#: ../roundup/cgi/templating.py:1423 ../roundup/cgi/templating.py:1428
+msgid "Yes"
+msgstr "äÁ"
+
+#: ../roundup/cgi/templating.py:1017
+#, python-format
+msgid "%s: (no value)"
+msgstr "%s: (ÎÅÔ ÚÎÁÞÅÎÉÑ)"
+
+#: ../roundup/cgi/templating.py:1029
+msgid ""
+"<strong><em>This event is not handled by the history display!</em></strong>"
+msgstr "<strong><em>îÅÉÚ×ÅÓÔÎÙÊ ÔÉÐ ÓÏÂÙÔÉÑ!</em></strong>"
+
+#: ../roundup/cgi/templating.py:1041
+msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
+msgstr "<tr><td colspan=4><strong>ðÒÉÍÅÞÁÎÉÅ:</strong></td></tr>"
+
+#: ../roundup/cgi/templating.py:1050
+msgid "History"
+msgstr "éÓÔÏÒÉÑ"
+
+#: ../roundup/cgi/templating.py:1052
+msgid "<th>Date</th>"
+msgstr "<th>äÁÔÁ</th>"
+
+#: ../roundup/cgi/templating.py:1053
+msgid "<th>User</th>"
+msgstr "<th>ðÏÌØÚÏ×ÁÔÅÌØ</th>"
+
+#: ../roundup/cgi/templating.py:1054
+msgid "<th>Action</th>"
+msgstr "<th>äÅÊÓÔ×ÉÅ</th>"
+
+#: ../roundup/cgi/templating.py:1055
+msgid "<th>Args</th>"
+msgstr "<th>ðÁÒÁÍÅÔÒÙ</th>"
+
+#: ../roundup/cgi/templating.py:1097
+#, python-format
+msgid "Copy of %(class)s %(id)s"
+msgstr "ëÏÐÉÑ: %(class)s %(id)s"
+
+#: ../roundup/cgi/templating.py:1331
+msgid "*encrypted*"
+msgstr "*ÚÁÛÉÆÒÏ×ÁÎ*"
+
+#: ../roundup/cgi/templating.py:1514
+msgid ""
+"default value for DateHTMLProperty must be either DateHTMLProperty or string "
+"date representation."
+msgstr ""
+"ÚÎÁÞÅÎÉÅ ÐÏ ÕÍÏÌÞÁÎÉÀ ÄÌÑ DateHTMLProperty ÄÏÌÖÎÏ ÂÙÔØ ÏÂßÅËÔÏÍ "
+"DateHTMLProperty ÉÌÉ ÓÔÒÏËÏ×ÙÍ ÐÒÅÄÓÔÁ×ÌÅÎÉÅÍ ÄÁÔÙ."
+
+#: ../roundup/cgi/templating.py:1674
+#, python-format
+msgid "Attempt to look up %(attr)s on a missing value"
+msgstr "ðÏÐÙÔËÁ ÐÏÌÕÞÉÔØ ÁÔÒÉÂÕÔ \"%(attr)s\" ÎÅÓÕÝÅÓÔ×ÕÀÝÅÇÏ ÏÂßÅËÔÁ"
+
+#: ../roundup/cgi/templating.py:1750
+#, python-format
+msgid "<option %svalue=\"-1\">- no selection -</option>"
+msgstr "<option %svalue=\"-1\">- ÎÅ ÕËÁÚÁÎÏ -</option>"
+
+#: ../roundup/date.py:186
+msgid ""
+"Not a date spec: \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or "
+"\"yyyy-mm-dd.HH:MM:SS.SSS\""
+msgstr ""
+"äÁÔÁ ÄÏÌÖÎÁ ÂÙÔØ × ÆÏÒÍÁÔÅ \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS"
+"\" ÉÌÉ \"yyyy-mm-dd.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. äÁÔÁ ÄÏÌÖÎÁ ÂÙÔØ × ÆÏÒÍÁÔÅ \"yyyy-mm-dd"
+"\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" ÉÌÉ \"yyyy-mm-dd.HH:MM:SS.SSS\""
+
+#: ../roundup/date.py:538
+msgid ""
+"Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
+msgstr ""
+"éÎÔÅÒ×ÁÌ ÄÏÌÖÅÎ ÂÙÔØ × ÆÏÒÍÁÔÅ [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [ÄÁÔÁ]"
+
+#: ../roundup/date.py:557
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+msgstr "éÎÔÅÒ×ÁÌ ÄÏÌÖÅÎ ÂÙÔØ × ÆÏÒÍÁÔÅ [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+
+#: ../roundup/date.py:694
+#, python-format
+msgid "%(number)s year"
+msgid_plural "%(number)s years"
+msgstr[0] "%(number)s ÇÏÄ"
+msgstr[1] "%(number)s ÇÏÄÁ"
+msgstr[2] "%(number)s ÌÅÔ"
+
+#: ../roundup/date.py:698
+#, python-format
+msgid "%(number)s month"
+msgid_plural "%(number)s months"
+msgstr[0] "%(number)s ÍÅÓÑÃ"
+msgstr[1] "%(number)s ÍÅÓÑÃÁ"
+msgstr[2] "%(number)s ÍÅÓÑÃÅ×"
+
+#: ../roundup/date.py:702
+#, python-format
+msgid "%(number)s week"
+msgid_plural "%(number)s weeks"
+msgstr[0] "%(number)s ÎÅÄÅÌÑ"
+msgstr[1] "%(number)s ÎÅÄÅÌÉ"
+msgstr[2] "%(number)s ÎÅÄÅÌØ"
+
+#: ../roundup/date.py:706
+#, python-format
+msgid "%(number)s day"
+msgid_plural "%(number)s days"
+msgstr[0] "%(number)s ÄÅÎØ"
+msgstr[1] "%(number)s ÄÎÑ"
+msgstr[2] "%(number)s ÄÎÅÊ"
+
+#: ../roundup/date.py:710
+msgid "tomorrow"
+msgstr "ÚÁ×ÔÒÁ"
+
+#: ../roundup/date.py:712
+msgid "yesterday"
+msgstr "×ÞÅÒÁ"
+
+#: ../roundup/date.py:715
+#, python-format
+msgid "%(number)s hour"
+msgid_plural "%(number)s hours"
+msgstr[0] "%(number)s ÞÁÓ"
+msgstr[1] "%(number)s ÞÁÓÁ"
+msgstr[2] "%(number)s ÞÁÓÏ×"
+
+#: ../roundup/date.py:719
+msgid "an hour"
+msgstr "ÞÁÓ"
+
+#: ../roundup/date.py:721
+msgid "1 1/2 hours"
+msgstr "ÐÏÌÔÏÒÁ ÞÁÓÁ"
+
+# third form ain't used
+#: ../roundup/date.py:723
+#, python-format
+msgid "1 %(number)s/4 hours"
+msgid_plural "1 %(number)s/4 hours"
+msgstr[0] "ÞÁÓ Ó ÞÅÔ×ÅÒÔØÀ"
+msgstr[1] "ÞÁÓ É %(number)s ÞÅÔ×ÅÒÔÉ"
+msgstr[2] "ÞÁÓ É %(number)s ÞÅÔ×ÅÒÔÅÊ"
+
+#: ../roundup/date.py:727
+msgid "in a moment"
+msgstr "ÓÅÊÞÁÓ"
+
+#: ../roundup/date.py:729
+msgid "just now"
+msgstr "ÔÏÌØËÏ ÞÔÏ"
+
+# ÉÓÐÏÌØÚÕÅÔÓÑ × ×ÙÒÁÖÅÎÉÑÈ "ÞÅÒÅÚ ÍÉÎÕÔÕ" ÉÌÉ "ÍÉÎÕÔÕ ÎÁÚÁÄ"
+#: ../roundup/date.py:732
+msgid "1 minute"
+msgstr "ÍÉÎÕÔÕ"
+
+# ÉÓÐÏÌØÚÕÅÔÓÑ × ×ÙÒÁÖÅÎÉÑÈ "ÞÅÒÅÚ 2 ÍÉÎÕÔÙ" ÉÌÉ "2 ÍÉÎÕÔÙ ÎÁÚÁÄ"
+#: ../roundup/date.py:735
+#, python-format
+msgid "%(number)s minute"
+msgid_plural "%(number)s minutes"
+msgstr[0] "%(number)s ÍÉÎÕÔÕ"
+msgstr[1] "%(number)s ÍÉÎÕÔÙ"
+msgstr[2] "%(number)s ÍÉÎÕÔ"
+
+#: ../roundup/date.py:738
+msgid "1/2 an hour"
+msgstr "ÐÏÌÞÁÓÁ"
+
+#: ../roundup/date.py:740
+#, python-format
+msgid "%(number)s/4 hour"
+msgid_plural "%(number)s/4 hours"
+msgstr[0] "ÞÅÔ×ÅÒÔØ ÞÁÓÁ"
+msgstr[1] "%(number)s ÞÅÔ×ÅÒÔÉ ÞÁÓÁ"
+msgstr[2] "%(number)s ÞÅÔ×ÅÒÔÅÊ ÞÁÓÁ"
+
+#: ../roundup/date.py:744
+#, python-format
+msgid "%s ago"
+msgstr "%s ÎÁÚÁÄ"
+
+#: ../roundup/date.py:746
+#, python-format
+msgid "in %s"
+msgstr "ÞÅÒÅÚ %s"
+
+#: ../roundup/init.py:134
+#, python-format
+msgid ""
+"WARNING: directory '%s'\n"
+"\tcontains old-style template - ignored"
+msgstr ""
+"÷îéíáîéå! ëÁÔÁÌÏÇ '%s'\n"
+"\tÓÏÄÅÒÖÉÔ ÛÁÂÌÏÎ ÓÔÁÒÏÇÏ ÏÂÒÁÚÃÁ - ÐÒÏÐÕÝÅÎ"
+
+#: ../roundup/roundupdb.py:141
+msgid "files"
+msgstr "ÆÁÊÌÙ"
+
+#: ../roundup/roundupdb.py:141
+msgid "messages"
+msgstr "ÓÏÏÂÝÅÎÉÑ"
+
+#: ../roundup/roundupdb.py:141
+msgid "nosy"
+msgstr "ÉÚ×ÅÝÅÎÉÑ"
+
+#: ../roundup/roundupdb.py:141
+msgid "superseder"
+msgstr "ÚÁÍÅÝÅÎÉÅ"
+
+#: ../roundup/roundupdb.py:141
+msgid "title"
+msgstr "ÚÁÇÌÁ×ÉÅ"
+
+#: ../roundup/roundupdb.py:142
+msgid "assignedto"
+msgstr "ÉÓÐÏÌÎÉÔÅÌØ"
+
+#: ../roundup/roundupdb.py:142
+msgid "priority"
+msgstr "ÐÒÉÏÒÉÔÅÔ"
+
+#: ../roundup/roundupdb.py:142
+msgid "status"
+msgstr "ÓÔÁÔÕÓ"
+
+#: ../roundup/roundupdb.py:142
+msgid "topic"
+msgstr "ÔÅÍÁ"
+
+#: ../roundup/roundupdb.py:145
+msgid "activity"
+msgstr "ÄÅÊÓÔ×ÉÅ"
+
+#. following properties are common for all hyperdb classes
+#. they are listed here to keep things in one place
+#: ../roundup/roundupdb.py:145
+msgid "actor"
+msgstr "×ÙÐÏÌÎÉÌ"
+
+#: ../roundup/roundupdb.py:145
+msgid "creation"
+msgstr "ÄÁÔÁ ÓÏÚÄÁÎÉÑ"
+
+#: ../roundup/roundupdb.py:145
+msgid "creator"
+msgstr "Á×ÔÏÒ"
+
+#: ../roundup/scripts/roundup_demo.py:32
+#, python-format
+msgid "Enter directory path to create demo tracker [%s]: "
+msgstr "÷×ÅÄÉÔÅ ÉÍÑ ËÁÔÁÌÏÇÁ ÄÌÑ ÄÅÍÏÎÓÔÒÁÃÉÏÎÎÏÇÏ ÔÒÅËÅÒÁ [%s]: "
+
+#: ../roundup/scripts/roundup_gettext.py:22
+#, python-format
+msgid "Usage: %(program)s <tracker home>"
+msgstr "÷ÙÚÏ×: %(program)s <ÄÏÍÁÛÎÉÊ ËÁÔÁÌÏÇ ÔÒÅËÅÒÁ>"
+
+#: ../roundup/scripts/roundup_gettext.py:37
+#, python-format
+msgid "No tracker templates found in directory %s"
+msgstr "÷ ËÁÔÁÌÏÇÅ %s ÎÅ ÎÁÊÄÅÎÏ ÎÉ ÏÄÎÏÇÏ ÛÁÂÌÏÎÁ ÔÒÅËÅÒÁ"
+
+#: ../roundup/scripts/roundup_mailgw.py:36
+#, python-format
+msgid ""
+"Usage: %(program)s [-v] [-c] [[-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"
+"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 ""
+"÷ÙÚÏ×: %(program)s [-v] [-c] [[-C ËÌÁÓÓ] -S ÐÏÌÅ=ÚÎÁÞÅÎÉÅ]* <ËÁÔÁÌÏÇ "
+"ÔÒÅËÅÒÁ> [ÐÏÞÔÏ×ÙÊ ÑÝÉË]\n"
+"\n"
+"ëÌÀÞÉ:\n"
+" -v: ÐÏËÁÚÁÔØ ÎÏÍÅÒ ×ÅÒÓÉÉ É ×ÙÊÔÉ\n"
+" -c: ËÌÁÓÓ ÓÏÚÄÁ×ÁÅÍÙÈ ÏÂßÅËÔÏ×, ÅÓÌÉ ÏÎ ÎÅ ÓÏÄÅÒÖÉÔÓÑ × ÐÉÓØÍÅ.\n"
+" åÓÌÉ ÎÅ ÕËÁÚÁÎ, ÉÓÐÏÌØÚÕÅÔÓÑ ÎÁÓÔÒÏÊËÁ ÔÒÅËÅÒÁ MAIL_DEFAULT_CLASS\n"
+" -C / -S: ÓÍ.ÎÉÖÅ\n"
+"\n"
+"ðÏÞÔÏ×ÙÊ ÛÌÀÚ roundup ÍÏÖÅÔ ÂÙÔØ ×ÙÚ×ÁÎ ÏÄÎÉÍ ÉÚ ÞÅÔÙÒÅÈ ÓÐÏÓÏÂÏ×:\n"
+" . Ó ÅÄÉÎÓÔ×ÅÎÎÙÍ ÁÒÇÕÍÅÎÔÏÍ - \"ÄÏÍÁÛÎÉÍ\" ËÁÔÁÌÏÇÏÍ ÔÒÅËÅÒÁ,\n"
+" . Ó ÕËÁÚÁÎÉÅÍ ËÁÔÁÌÏÇÁ ÔÒÅËÅÒÁ É ÐÏÞÔÏ×ÏÇÏ ÆÁÊÌÁ × ÆÏÒÍÁÔÅ mailbox,\n"
+" . Ó ÕËÁÚÁÎÉÅÍ ËÁÔÁÌÏÇÁ ÔÒÅËÅÒÁ É ÐÏÞÔÏ×ÏÇÏ ÑÝÉËÁ POP ÉÌÉ APOP,\n"
+" . Ó ÕËÁÚÁÎÉÅÍ ËÁÔÁÌÏÇÁ ÔÒÅËÅÒÁ É ÐÏÞÔÏ×ÏÇÏ ÑÝÉËÁ IMAP ÉÌÉ IMAPS.\n"
+"\n"
+"ðÏÞÔÏ×ÙÊ ÛÌÀÚ ÐÏÚ×ÏÌÑÅÔ ÕËÁÚÁÔØ ÔÁËÖÅ ÐÁÒÁÍÅÔÒÙ -C É -S, ÐÒÉ ÐÏÍÏÝÉ\n"
+"ËÏÔÏÒÙÈ ÍÏÖÎÏ ÚÁÐÏÌÎÉÔØ ÄÏÐÏÌÎÉÔÅÌØÎÙÅ ÐÏÌÑ ËÌÁÓÓÁ, ÏÂßÅËÔÙ ËÏÔÏÒÏÇÏ\n"
+"ÓÏÚÄÁÅÔ roundup-mailgw. åÓÌÉ ËÌÁÓÓ ÎÅ ÕËÁÚÁÎ, ÓÏÚÄÁÀÔÓÑ ÏÂßÅËÔÙ msg,\n"
+"ÎÏ ÍÏÖÎÏ ÉÓÐÏÌØÚÏ×ÁÔØ É ÄÒÕÇÉÅ ËÌÁÓÓÙ: issue, file ÉÌÉ user.\n"
+"ëÌÀÞ -S ÉÌÉ --set ÕÓÔÁÎÁ×ÌÉ×ÁÅÔ ÁÔÒÉÂÕÔÙ ÏÂßÅËÔÁ, ÉÓÐÏÌØÚÕÑ ÔÕ ÖÅ\n"
+"ÎÏÔÁÃÉÀ, ÞÔÏ É ÉÎÔÅÒÆÅÊÓ ËÏÍÁÎÄÎÏÊ ÓÔÒÏËÉ roundup ÉÌÉ ËÏÍÁÎÄÙ,\n"
+"ËÏÔÏÒÙÅ ÍÏÇÕÔ ÂÙÔØ ÐÏÄÁÎÙ × ÓÔÒÏËÅ Subject email-ÓÏÏÂÝÅÎÉÑ:\n"
+"ÁÔÒÉÂÕÔ=ÚÎÁÞÅÎÉÅ[;ÁÔÒÉÂÕÔ=ÚÎÁÞÅÎÉÅ].\n"
+"\n"
+"ðÒÉ ÐÏÍÏÝÉ ÜÔÉÈ ÐÁÒÁÍÅÔÒÏ× ÍÏÖÎÏ ÓÔÒÏÉÔØ ÒÁÚÎÙÅ ÏÂÒÁÂÏÔÞÉËÉ ÓÏÏÂÝÅÎÉÊ\n"
+"ÄÌÑ ÒÁÚÎÙÈ ÁÄÒÅÓÏ× ÐÏÞÔÏ×ÏÇÏ ÛÌÀÚÁ.\n"
+"\n"
+"óÐÏÓÏÂÙ ÐÏÌÕÞÅÎÉÑ email-ÓÏÏÂÝÅÎÉÊ:\n"
+"\n"
+"ëÁÎÁÌ (pipe):\n"
+" åÓÌÉ × ÐÁÒÁÍÅÔÒÁÈ ×ÙÚÏ×Á ÎÅ ÕËÁÚÁÎ ÐÏÞÔÏ×ÙÊ ÑÝÉË, roundup-mailgw\n"
+" ÞÉÔÁÅÔ ÏÄÎÏ ÓÏÏÂÝÅÎÉÅ ÉÚ ÓÔÁÎÄÁÒÔÎÏÇÏ ×ÈÏÄÎÏÇÏ ÐÏÔÏËÁ (stdin).\n"
+" üÔÏ ÓÏÏÂÝÅÎÉÅ ÐÅÒÅÄÁÅÔÓÑ ÄÌÑ ÏÂÒÁÂÏÔËÉ ÍÏÄÕÌÀ roundup.mailgw.\n"
+"\n"
+"Mailbox:\n"
+" ÷ ÜÔÏÍ ÓÌÕÞÁÅ roundup-mailgw ÞÉÔÁÅÔ ×ÓÅ ÓÏÏÂÝÅÎÉÑ ÉÚ ÐÏÞÔÏ×ÏÇÏ ÆÁÊÌÁ,\n"
+" ËÏÔÏÒÙÊ ÄÏÌÖÅÎ ÉÍÅÔØ ÆÏÒÍÁÔ mail spool file. ëÁÖÄÏÅ ÓÏÏÂÝÅÎÉÅ ÐÅÒÅÄÁÅÔÓÑ\n"
+" ÄÌÑ ÏÂÒÁÂÏÔËÉ ÍÏÄÕÌÀ roundup.mailgw. ëÏÇÄÁ ×ÓÅ ÓÏÏÂÝÅÎÉÑ ÕÓÐÅÛÎÏ\n"
+" ÏÂÒÁÂÏÔÁÎÙ, ÐÏÞÔÏ×ÙÊ ÆÁÊÌ ÏÞÉÝÁÅÔÓÑ. éÍÑ ÐÏÞÔÏ×ÏÇÏ ÆÁÊÌÁ ÕËÁÚÙ×ÁÅÔÓÑ ÔÁË:\n"
+" mailbox /path/to/mailbox\n"
+"\n"
+"POP:\n"
+" ðÒÉ ÔÁËÏÍ ×ÙÚÏ×Å roundup-mailgs ÚÁÂÉÒÁÅÔ ×ÓÅ ÓÏÏÂÝÅÎÉÑ, ÐÏÓÔÕÐÉ×ÛÉÅ\n"
+" × ÐÏÞÔÏ×ÙÊ ÑÝÉË ÎÁ ÓÅÒ×ÅÒÅ POP3. ëÁÖÄÏÅ ÐÏÌÕÞÅÎÎÏÅ ÓÏÏÂÝÅÎÉÅ ÐÅÒÅÄÁÅÔÓÑ\n"
+" ÄÌÑ ÏÂÒÁÂÏÔËÉ ÍÏÄÕÌÀ roundup.mailgw. ðÏÞÔÏ×ÙÊ ÑÝÉË ÕËÁÚÙ×ÁÅÔÓÑ × ×ÉÄÅ:\n"
+" pop username:password at server\n"
+" éÍÑ ÐÏÌØÚÏ×ÁÔÅÌÑ É ÐÁÒÏÌØ ÍÏÇÕÔ ÂÙÔØ ÏÐÕÝÅÎÙ:\n"
+" pop username at server\n"
+" pop server\n"
+" åÓÌÉ ÉÍÑ ÉÌÉ ÐÁÒÏÌØ ÎÅ ÕËÁÚÁÎÙ, ÐÒÏÇÒÁÍÍÁ ÐÏÐÒÏÓÉÔ ××ÅÓÔÉ ÉÈ Ó ÔÅÒÍÉÎÁÌÁ.\n"
+"\n"
+"APOP:\n"
+" ôÏ ÖÅ, ÞÔÏ É POP, ÎÏ Ó ÉÓÐÏÌØÚÏ×ÁÎÉÅÍ ÐÒÏÔÏËÏÌÁ Authenticated POP:\n"
+" apop username:password at server\n"
+"\n"
+"IMAP:\n"
+" ðÏÌÕÞÉÔØ ÐÏÞÔÕ Ó ÓÅÒ×ÅÒÁ IMAP. ðÏÞÔÏ×ÙÊ ÑÝÉË ÕËÁÚÙ×ÁÅÔÓÑ ÔÁË ÖÅ,\n"
+" ËÁË É ÄÌÑ POP-ÓÅÒ×ÅÒÁ:\n"
+" imap username:password at server\n"
+" ðÒÉ ÉÓÐÏÌØÚÏ×ÁÎÉÉ IMAP ÍÏÖÎÏ ÕËÁÚÁÔØ ÉÍÑ ÐÁÐËÉ ×ÈÏÄÎÏÊ ÐÏÞÔÙ,\n"
+" ÅÓÌÉ ÏÎÏ ÏÔÌÉÞÁÅÔÓÑ ÏÔ ÏÂÙÞÎÏÇÏ (\"INBOX\"):\n"
+" imap username:password at server folder\n"
+"\n"
+"IMAPS:\n"
+" ðÏÌÕÞÉÔØ ÐÏÞÔÕ Ó ÓÅÒ×ÅÒÁ IMAP, ÐÏÚ×ÏÌÑÀÝÅÇÏ ÛÉÆÒÏ×ÁÎÎÙÅ ÓÏÅÄÉÎÅÎÉÑ.\n"
+" ðÏÞÔÏ×ÙÊ ÑÝÉË ÕËÁÚÙ×ÁÅÔÓÑ ÔÁË ÖÅ, ËÁË É ÄÌÑ ÏÂÙÞÎÏÇÏ IMAP-ÓÅÒ×ÅÒÁ:\n"
+" imaps username:password at server [mailbox]\n"
+"\n"
+
+#: ../roundup/scripts/roundup_mailgw.py:147
+msgid "Error: not enough source specification information"
+msgstr "ïÛÉÂËÁ: ÎÅ ÕËÁÚÁÎ ÐÕÔØ Ë ÐÏÞÔÏ×ÏÍÕ ÑÝÉËÕ"
+
+#: ../roundup/scripts/roundup_mailgw.py:163
+msgid "Error: pop specification not valid"
+msgstr "ïÛÉÂËÁ: ÎÅÐÒÁ×ÉÌØÎÙÊ ÁÄÒÅÓ pop-ÓÅÒ×ÅÒÁ"
+
+#: ../roundup/scripts/roundup_mailgw.py:170
+msgid "Error: apop specification not valid"
+msgstr "ïÛÉÂËÁ: ÎÅÐÒÁ×ÉÌØÎÙÊ ÁÄÒÅÓ apop-ÓÅÒ×ÅÒÁ"
+
+#: ../roundup/scripts/roundup_mailgw.py:184
+msgid ""
+"Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or "
+"\"imaps\""
+msgstr ""
+"ïÛÉÂËÁ: ôÉÐ ÐÏÞÔÏ×ÏÇÏ ÑÝÉËÁ ÄÏÌÖÅÎ ÂÙÔØ \"mailbox\", \"pop\", \"apop\", "
+"\"imap\" ÉÌÉ \"imaps\""
+
+#: ../roundup/scripts/roundup_server.py:157
+msgid ""
+"<html><head><title>Roundup trackers index</title></head>\n"
+"<body><h1>Roundup trackers index</h1><ol>\n"
+msgstr ""
+"<html><head><title>óÐÉÓÏË ÔÒÅËÅÒÏ× Roundup</title></head>\n"
+"<body><h1>óÐÉÓÏË ÔÒÅËÅÒÏ× Roundup</h1><ol>\n"
+
+#: ../roundup/scripts/roundup_server.py:287
+#, python-format
+msgid "Error: %s: %s"
+msgstr "ïÛÉÂËÁ: %s: %s"
+
+#: ../roundup/scripts/roundup_server.py:297
+msgid "WARNING: ignoring \"-g\" argument, not root"
+msgstr ""
+"÷îéíáîéå: ÐÁÒÁÍÅÔÒ \"-g\" ÎÅ ÉÓÐÏÌØÚÕÅÔÓÑ, ÏÎ ÒÁÚÒÅÛÅÎ ÔÏÌØËÏ ÄÌÑ "
+"ÐÏÌØÚÏ×ÁÔÅÌÑ root"
+
+#: ../roundup/scripts/roundup_server.py:303
+msgid "Can't change groups - no grp module"
+msgstr "ðÏÄÍÅÎÁ ÇÒÕÐÐÙ ÎÅ×ÏÚÍÏÖÎÁ - ÎÕÖÅÎ ÍÏÄÕÌØ grp"
+
+#: ../roundup/scripts/roundup_server.py:312
+#, python-format
+msgid "Group %(group)s doesn't exist"
+msgstr "çÒÕÐÐÁ %(group)s ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
+
+#: ../roundup/scripts/roundup_server.py:323
+msgid "Can't run as root!"
+msgstr "úÁÐÕÓË ÓÅÒ×ÅÒÁ Ó ÐÏÌÎÏÍÏÞÉÑÍÉ ÐÏÌØÚÏ×ÁÔÅÌÑ root ÚÁÐÒÅÝÅÎ!"
+
+#: ../roundup/scripts/roundup_server.py:326
+msgid "WARNING: ignoring \"-u\" argument, not root"
+msgstr ""
+"÷îéíáîéå: ÐÁÒÁÍÅÔÒ \"-u\" ÎÅ ÉÓÐÏÌØÚÕÅÔÓÑ, ÏÎ ÒÁÚÒÅÛÅÎ ÔÏÌØËÏ ÄÌÑ "
+"ÐÏÌØÚÏ×ÁÔÅÌÑ root"
+
+#: ../roundup/scripts/roundup_server.py:331
+msgid "Can't change users - no pwd module"
+msgstr "ðÏÄÍÅÎÁ ÐÏÌØÚÏ×ÁÔÅÌÑ ÎÅ×ÏÚÍÏÖÎÁ - ÎÕÖÅÎ ÍÏÄÕÌØ pwd"
+
+#: ../roundup/scripts/roundup_server.py:340
+#, python-format
+msgid "User %(user)s doesn't exist"
+msgstr "ðÏÌØÚÏ×ÁÔÅÌØ %(user)s ÎÅ ÓÕÝÅÓÔ×ÕÅÔ"
+
+#: ../roundup/scripts/roundup_server.py:471
+#, python-format
+msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
+msgstr "òÅÖÉÍ \"%s\" ÎÅÄÏÓÔÕÐÅÎ, ÐÅÒÅËÌÀÞÁÅÍÓÑ × ÏÄÎÏÚÁÄÁÞÎÙÊ ÒÅÖÉÍ"
+
+#: ../roundup/scripts/roundup_server.py:494
+#, python-format
+msgid "Unable to bind to port %s, port already in use."
+msgstr "îÅ×ÏÚÍÏÖÎÏ ÕÓÔÁÎÏ×ÉÔØ ÓÅÒ×ÅÒ ÎÁ ÐÏÒÔÕ %s, ÐÏÒÔ ÕÖÅ ÚÁÎÑÔ."
+
+#: ../roundup/scripts/roundup_server.py:562
+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 ""
+" -c <ËÏÍÁÎÄÁ> ËÌÀÞ ÓÅÒ×ÉÓÁ Windows.\n"
+" åÓÌÉ ×Ù ÓÏÂÉÒÁÅÔÅÓØ ÚÁÐÕÓËÁÔØ ÓÅÒ×ÅÒ ËÁË ÓÅÒ×ÉÓ Windows,\n"
+" ×Ù ÄÏÌÖÎÙ ÉÓÐÏÌØÚÏ×ÁÔØ ËÏÎÆÉÇÕÒÁÃÉÏÎÎÙÊ ÆÁÊÌ ÓÅÒ×ÅÒÁ\n"
+" ÄÌÑ ÚÁÄÁÎÉÑ ÄÏÍÁÛÎÉÈ ËÁÔÁÌÏÇÏ× ÔÒÅËÅÒÏ×.\n"
+" äÌÑ ÚÁÐÕÓËÁ × ÒÅÖÉÍÅ ÓÅÒ×ÉÓÁ Windows ÎÅÏÂÈÏÄÉÍÏ ÕËÁÚÁÔØ.\n"
+" ÆÁÊÌ ÐÒÏÔÏËÏÌÁ. ëÏÍÁÎÄÁ 'roundup-server -c help'\n"
+" ×ÙÄÁÅÔ ÓÐÒÁ×ËÕ Ï ËÏÍÁÎÄÎÏÊ ÓÔÒÏËÅ ÓÅÒ×ÉÓÁ Windows."
+
+#: ../roundup/scripts/roundup_server.py:569
+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 ""
+" -u <UID> ÚÁÐÕÓÔÉÔØ cÅÒ×ÅÒ Roundup Ó ÐÒÁ×ÁÍÉ ÐÏÌØÚÏ×ÁÔÅÌÑ UID\n"
+" -g <GID> ÚÁÐÕÓÔÉÔØ ÓÅÒ×ÅÒ Roundup × ÇÒÕÐÐÅ GID\n"
+" -d <PIDfile> ÚÁÐÉÓÁÔØ ÎÏÍÅÒ ÐÒÏÃÅÓÓÁ (pid) ÓÅÒ×ÅÒÁ × ÆÁÊÌ PIDfile\n"
+" É ÚÁÐÕÓÔÉÔØ ÓÅÒ×ÅÒ × ÆÏÎÏ×ÏÍ ÒÅÖÉÍÅ. åÓÌÉ ÕËÁÚÁÎÏ \"-d\",\n"
+" ÆÁÊÌ ÐÒÏÔÏËÏÌÁ *ÏÂÑÚÁÔÅÌØÎÏ* ÄÏÌÖÅÎ ÂÙÔØ ÚÁÄÁÎ ËÌÀÞÏÍ \"-l\""
+
+#: ../roundup/scripts/roundup_server.py:576
+#, 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 ""
+"%(message)s÷ÙÚÏ×: roundup-server [ËÌÀÞÉ] [ÉÍÑ=ËÁÔÁÌÏÇ]*\n"
+"\n"
+"ëÌÀÞÉ:\n"
+" -v ÐÏËÁÚÁÔØ ÎÏÍÅÒ ×ÅÒÓÉÉ É ×ÙÊÔÉ\n"
+" -h ÐÏËÁÚÁÔØ ÜÔÏ ÓÏÏÂÝÅÎÉÅ É ×ÙÊÔÉ\n"
+" -S ÓÏÚÄÁÔØ ÉÌÉ ÏÂÎÏ×ÉÔØ ÆÁÊÌ ËÏÎÆÉÇÕÒÁÃÉÉ ÓÅÒ×ÅÒÁ É ×ÙÊÔÉ\n"
+" -C <ÆÁÊÌ> ÉÓÐÏÌØÚÏ×ÁÔØ <ÆÁÊÌ> ËÏÎÆÉÇÕÒÁÃÉÉ ÓÅÒ×ÅÒÁ\n"
+" -n <ÁÄÒÅÓ> ÕÓÔÁÎÏ×ÉÔØ ÁÄÒÅÓ ×ÅÂ-ÓÅÒ×ÅÒÁ Roundup\n"
+" -p <ÐÏÒÔ> ÉÚÍÅÎÉÔØ ÎÏÍÅÒ ÐÏÒÔÁ (ÐÏ ÕÍÏÌÞÁÎÉÀ - %(port)s)\n"
+" -l <ÆÁÊÌ> ×ÅÓÔÉ ÐÒÏÔÏËÏÌ × ÕËÁÚÁÎÎÏÍ ÆÁÊÌÅ (×ÍÅÓÔÏ stderr/stdout)\n"
+" -N ÐÒÏÔÏËÏÌÉÒÏ×ÁÔØ ÉÍÅÎÁ ÍÁÛÉÎ ËÌÉÅÎÔÏ× ×ÍÅÓÔÏ IP-ÁÄÒÅÓÏ×\n"
+" (ÓÉÌØÎÏ ÚÁÍÅÄÌÑÅÔ ÒÁÂÏÔÕ).\n"
+" -t <ÒÅÖÉÍ> ÒÅÖÉÍ ÍÎÏÇÏÚÁÄÁÞÎÏÓÔÉ (ÐÏ ÕÍÏÌÞÁÎÉÀ - %(mp_def)s).\n"
+" äÏÓÔÕÐÎÙÅ ÒÅÖÉÍÙ: %(mp_types)s.\n"
+"%(os_part)s\n"
+"\n"
+"òÁÓÛÉÒÅÎÎÙÅ ËÌÀÞÉ:\n"
+" --version ÐÏËÁÚÁÔØ ÎÏÍÅÒ ×ÅÒÓÉÉ É ×ÙÊÔÉ\n"
+" --help ÐÏËÁÚÁÔØ ÜÔÏ ÓÏÏÂÝÅÎÉÅ É ×ÙÊÔÉ\n"
+" --save-config ÓÏÚÄÁÔØ ÉÌÉ ÏÂÎÏ×ÉÔØ ÆÁÊÌ ËÏÎÆÉÇÕÒÁÃÉÉ ÓÅÒ×ÅÒÁ É ×ÙÊÔÉ\n"
+" --config <ÆÁÊÌ> ÉÓÐÏÌØÚÏ×ÁÔØ <ÆÁÊÌ> ËÏÎÆÉÇÕÒÁÃÉÉ ÓÅÒ×ÅÒÁ\n"
+" --help print this text and exit\n"
+" ëÒÏÍÅ ÜÔÏÇÏ, ×ÓÅ ÎÁÓÔÒÏÊËÉ ÓÅËÃÉÉ [main] × ËÏÎÆÉÇÕÒÁÃÉÏÎÎÏÍ ÆÁÊÌÅ\n"
+" ÍÏÇÕÔ ÚÁÄÁ×ÁÔØÓÑ ËÁË ËÌÀÞÉ ËÏÍÁÎÄÎÏÊ ÓÔÒÏËÉ × ×ÉÄÅ --<ÉÍÑ>=<ÚÎÁÞÅÎÉÅ>\n"
+"\n"
+"ðÒÉÍÅÒÙ:\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"
+"æÏÒÍÁÔ ËÏÎÆÉÇÕÒÁÃÉÏÎÎÏÇÏ ÆÁÊÌÁ:\n"
+" æÁÊÌ ËÏÎÆÉÇÕÒÁÃÉÉ ÓÅÒ×ÅÒÁ Roundup ÉÍÅÅÔ ÏÂÙÞÎÙÊ ÆÏÒÍÁÔ ini-ÆÁÊÌÏ×.\n"
+" ëÏÎÆÉÇÕÒÁÃÉÏÎÎÙÊ ÆÁÊÌ, ÓÏÚÄÁÎÎÙÊ ËÏÍÁÎÄÏÊ 'roundup-server -S',\n"
+" ÓÏÄÅÒÖÉÔ ÐÏÄÒÏÂÎÙÅ ÏÐÉÓÁÎÉÑ ×ÓÅÈ ÎÁÓÔÒÏÅË.\n"
+"\n"
+"ðÁÒÁÍÅÔÒÙ \"ÉÍÑ=ËÁÔÁÌÏÇ\"\n"
+" ÚÁÄÁÀÔ ÔÒÅËÅÒ (ÉÌÉ ÔÒÅËÅÒÙ), ÏÂÓÌÕÖÉ×ÁÅÍÙÅ ÜÔÉÍ ÓÅÒ×ÅÒÏÍ.\n"
+" éÍÑ ÔÒÅËÅÒÁ Ñ×ÌÑÅÔÓÑ ÞÁÓÔØÀ URL (ÐÅÒ×ÁÑ ÞÁÓÔØ ÐÕÔÉ × URL).\n"
+" ëÁÔÁÌÏÇ ÕËÁÚÙ×ÁÅÔ ÎÁ ÒÁÓÐÏÌÏÖÅÎÉÅ ÄÁÎÎÙÈ ÔÒÅËÅÒÁ. üÔÏ - ÔÏÔ\n"
+" ËÁÔÁÌÏÇ, ËÏÔÏÒÙÊ ×Ù ÕËÁÚÙ×ÁÌÉ ÐÒÉ ×ÙÐÏÌÎÅÎÉÉ ËÏÍÁÎÄÙ\n"
+" 'roundup-admin init'.\n"
+" ÷Ù ÍÏÖÅÔÅ ÕËÁÚÁÔØ × ËÏÍÁÎÄÎÏÊ ÓÔÒÏËÅ ÓËÏÌØËÏ ÕÇÏÄÎÏ ÐÁÒ ÉÍÑ=ËÁÔÁÌÏÇ.\n"
+" óÌÅÄÉÔÅ ÚÁ ÔÅÍ, ÞÔÏÂÙ × ÉÍÅÎÁÈ ÔÒÅËÅÒÏ× ÎÅ ÂÙÌÏ ÓÉÍ×ÏÌÏ×, ËÏÔÏÒÙÅ\n"
+" ÎÅ ÍÏÇÕÔ ÉÓÐÏÌØÚÏ×ÁÔØÓÑ × URL (ÐÒÏÂÅÌÙ, ÒÕÓÓËÉÅ ÂÕË×Ù É ÐÒÏÞ.),\n"
+" ÐÏÔÏÍÕ ÞÔÏ ÔÁËÉÅ ÉÍÅÎÁ ÓÂÉ×ÁÀÔ Ó ÔÏÌËÕ ÎÅËÏÔÏÒÙÅ ÂÒÁÕÚÅÒÙ ÔÉÐÁ IE.\n"
+
+#: ../roundup/scripts/roundup_server.py:724
+msgid "Instances must be name=home"
+msgstr "óÐÉÓÏË ÔÒÅËÅÒÏ× ÄÏÌÖÅÎ ÂÙÔØ × ÆÏÒÍÁÔÅ ÉÍÑ=ËÁÔÁÌÏÇ"
+
+#: ../roundup/scripts/roundup_server.py:738
+#, python-format
+msgid "Configuration saved to %s"
+msgstr "ëÏÎÆÉÇÕÒÁÃÉÑ ÚÁÐÉÓÁÎÁ × %s"
+
+#: ../roundup/scripts/roundup_server.py:756
+msgid "Sorry, you can't run the server as a daemon on this Operating System"
+msgstr ""
+"éÚ×ÉÎÉÔÅ, × ÜÔÏÊ ÏÐÅÒÁÃÉÏÎÎÏÊ ÓÉÓÔÅÍÅ ÒÁÂÏÔÁ × ÆÏÎÏ×ÏÍ ÒÅÖÉÍÅ ÎÅ×ÏÚÍÏÖÎÁ"
+
+#: ../roundup/scripts/roundup_server.py:768
+#, python-format
+msgid "Roundup server started on %(HOST)s:%(PORT)s"
+msgstr "óÅÒ×ÅÒ Roundup ÇÏÔÏ× Ë ÒÁÂÏÔÅ ÐÏ ÁÄÒÅÓÕ %(HOST)s:%(PORT)s"
+
+#: ../templates/classic/html/_generic.collision.html:4
+#: ../templates/minimal/html/_generic.collision.html:4
+msgid "${class} Edit Collision - ${tracker}"
+msgstr "ëÏÎÆÌÉËÔ ÒÅÄÁËÔÉÒÏ×ÁÎÉÑ ${class} - ${tracker}"
+
+#: ../templates/classic/html/_generic.collision.html:7
+#: ../templates/minimal/html/_generic.collision.html:7
+msgid "${class} Edit Collision"
+msgstr "ëÏÎÆÌÉËÔ ÒÅÄÁËÔÉÒÏ×ÁÎÉÑ ${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"
+" ïÂÎÁÒÕÖÅÎ ËÏÎÆÌÉËÔ ÒÅÄÁËÔÉÒÏ×ÁÎÉÑ. ðÏËÁ ×Ù ÚÁÐÏÌÎÑÌÉ ÜÔÕ\n"
+" ÆÏÒÍÕ, ÄÒÕÇÏÊ ÐÏÌØÚÏ×ÁÔÅÌØ ÉÚÍÅÎÉÌ ÏÂßÅËÔ ÂÁÚÙ ÄÁÎÎÙÈ.\n"
+" <a href='${context}>ðÅÒÅÞÉÔÁÊÔÅ</a> ÆÏÒÍÕ É ×ÎÅÓÉÔÅ ÉÚÍÅÎÅÎÉÑ\n"
+" ÚÁÎÏ×Ï, ÐÏÖÁÌÕÊÓÔÁ.\n"
+
+#: ../templates/classic/html/_generic.help.html:9
+#: ../templates/minimal/html/_generic.help.html:9
+msgid "${property} help - ${tracker}"
+msgstr "óÐÒÁ×ËÁ ÐÏ ÐÏÌÀ \"${property}\" - ${tracker}"
+
+#: ../templates/classic/html/_generic.help.html:31
+#: ../templates/minimal/html/_generic.help.html:31
+msgid " Cancel "
+msgstr " ïÔÍÅÎÉÔØ "
+
+#: ../templates/classic/html/_generic.help.html:34
+#: ../templates/minimal/html/_generic.help.html:34
+msgid " Apply "
+msgstr " ðÒÉÍÅÎÉÔØ "
+
+#: ../templates/classic/html/_generic.help.html:41
+#: ../templates/classic/html/issue.index.html:73
+#: ../templates/minimal/html/_generic.help.html:41
+msgid "<< previous"
+msgstr "<< ÐÒÅÄÙÄÕÝÉÅ"
+
+#: ../templates/classic/html/_generic.help.html:52
+#: ../templates/classic/html/issue.index.html:81
+#: ../templates/minimal/html/_generic.help.html:52
+msgid "${start}..${end} out of ${total}"
+msgstr "${start}..${end} ÉÚ ${total}"
+
+#: ../templates/classic/html/_generic.help.html:56
+#: ../templates/classic/html/issue.index.html:84
+#: ../templates/minimal/html/_generic.help.html:56
+msgid "next >>"
+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
+msgid "${class} editing - ${tracker}"
+msgstr "òÅÄÁËÔÉÒÏ×ÁÎÉÅ ${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 "òÅÄÁËÔÉÒÏ×ÁÎÉÅ ${class}"
+
+#: ../templates/classic/html/_generic.index.html:14
+#: ../templates/classic/html/_generic.item.html:12
+#: ../templates/classic/html/file.item.html:9
+#: ../templates/classic/html/issue.index.html:16
+#: ../templates/classic/html/issue.item.html:28
+#: ../templates/classic/html/msg.item.html:26
+#: ../templates/classic/html/user.index.html:9
+#: ../templates/classic/html/user.item.html:28
+#: ../templates/minimal/html/_generic.index.html:14
+#: ../templates/minimal/html/_generic.item.html:12
+#: ../templates/minimal/html/user.index.html:9
+#: ../templates/minimal/html/user.item.html:28
+#: ../templates/minimal/html/user.register.html:14
+msgid "You are not allowed to view this page."
+msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÐÒÏÓÍÏÔÒ ÜÔÏÊ ÓÔÒÁÎÉÃÙ."
+
+#: ../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\"> ðÒÉ ÐÏÍÏÝÉ ÜÔÏÊ ÆÏÒÍÙ ×Ù ÍÏÖÅÔÅ ÉÚÍÅÎÉÔØ ÓÏÄÅÒÖÉÍÏÅ "
+"ËÌÁÓÓÁ ${classname}. âÕÄØÔÅ ÏÓÔÏÒÏÖÎÙ Ó ÚÁÐÑÔÙÍÉ, ÐÅÒÅ×ÏÄÁÍÉ ÓÔÒÏË É "
+"Ä×ÏÊÎÙÍÉ ËÁ×ÙÞËÁÍÉ (\"). úÎÁÞÅÎÉÑ, ÓÏÄÅÒÖÁÝÉÅ ÚÁÐÑÔÙÅ É ÐÅÒÅ×ÏÄÙ ÓÔÒÏË, "
+"ÄÏÌÖÎÙ ÂÙÔØ ÚÁËÌÀÞÅÎÙ × Ä×ÏÊÎÙÅ ËÁ×ÙÞËÉ (\"). ä×ÏÊÎÙÅ ËÁ×ÙÞËÉ × ÚÎÁÞÅÎÉÑÈ "
+"ÄÏÌÖÎÙ ÂÙÔØ ÕÄ×ÏÅÎÙ (\"\"). </p> <p class=\"form-help\"> úÎÁÞÅÎÉÑ "
+"ÍÎÏÖÅÓÔ×ÅÎÎÙÈ ÓÓÙÌÏË (multilink properties) ÒÁÚÄÅÌÑÀÔÓÑ Ä×ÏÅÔÏÞÉÅÍ (... ,"
+"\"ÒÁÚ:Ä×Á:ÔÒÉ\", ...) </p> <p class=\"form-help\"> äÌÑ ÔÏÇÏ, ÞÔÏÂÙ "
+"ÕÎÉÞÔÏÖÉÔØ ÚÁÐÉÓØ, ÕÄÁÌÉÔÅ ÓÏÏÔ×ÅÔÓÔ×ÕÀÝÕÀ ÓÔÒÏËÕ. îÏ×ÙÅ ÚÁÐÉÓÉ "
+"ÄÏÐÉÓÙ×ÁÀÔÓÑ × ËÏÎÅà ÔÁÂÌÉÃÙ. ÷ÍÅÓÔÏ ÉÄÅÎÔÉÆÉËÁÔÏÒÁ (id) ÎÏ×ÙÈ ÚÁÐÉÓÅÊ ÎÕÖÎÏ "
+"×ÐÉÓÙ×ÁÔØ ÌÁÔÉÎÓËÕÀ ÂÕË×Õ \"X\". </p>"
+
+#: ../templates/classic/html/_generic.index.html:44
+#: ../templates/minimal/html/_generic.index.html:44
+msgid "Edit Items"
+msgstr "éÚÍÅÎÉÔØ"
+
+#: ../templates/classic/html/file.index.html:4
+msgid "List of files - ${tracker}"
+msgstr "óÐÉÓÏË ÆÁÊÌÏ× - ${tracker}"
+
+#: ../templates/classic/html/file.index.html:5
+msgid "List of files"
+msgstr "óÐÉÓÏË ÆÁÊÌÏ×"
+
+#: ../templates/classic/html/file.index.html:10
+msgid "Download"
+msgstr "óËÁÞÁÔØ"
+
+#: ../templates/classic/html/file.index.html:11
+#: ../templates/classic/html/file.item.html:22
+msgid "Content Type"
+msgstr "ôÉÐ ÆÁÊÌÁ"
+
+#: ../templates/classic/html/file.index.html:12
+msgid "Uploaded By"
+msgstr "úÁÇÒÕÚÉÌ"
+
+#: ../templates/classic/html/file.index.html:13
+#: ../templates/classic/html/msg.item.html:43
+msgid "Date"
+msgstr "äÁÔÁ"
+
+#: ../templates/classic/html/file.item.html:2
+msgid "File display - ${tracker}"
+msgstr "ðÒÏÓÍÏÔÒ ÆÁÊÌÁ - ${tracker}"
+
+#: ../templates/classic/html/file.item.html:4
+msgid "File display"
+msgstr "ðÒÏÓÍÏÔÒ ÆÁÊÌÁ"
+
+#: ../templates/classic/html/file.item.html:18
+#: ../templates/classic/html/user.item.html:39
+#: ../templates/classic/html/user.register.html:17
+msgid "Name"
+msgstr "éÍÑ"
+
+#: ../templates/classic/html/file.item.html:40
+msgid "download"
+msgstr "ÓËÁÞÁÔØ"
+
+#: ../templates/classic/html/home.classlist.html:2
+#: ../templates/minimal/html/home.classlist.html:2
+msgid "List of classes - ${tracker}"
+msgstr "óÐÉÓÏË ËÌÁÓÓÏ× - ${tracker}"
+
+#: ../templates/classic/html/home.classlist.html:4
+#: ../templates/minimal/html/home.classlist.html:4
+msgid "List of classes"
+msgstr "óÐÉÓÏË ËÌÁÓÓÏ×"
+
+#: ../templates/classic/html/issue.index.html:7
+msgid "List of issues - ${tracker}"
+msgstr "óÐÉÓÏË ÚÁÄÁÞ - ${tracker}"
+
+#: ../templates/classic/html/issue.index.html:11
+msgid "List of issues"
+msgstr "óÐÉÓÏË ÚÁÄÁÞ"
+
+#: ../templates/classic/html/issue.index.html:22
+#: ../templates/classic/html/issue.item.html:44
+msgid "Priority"
+msgstr "ðÒÉÏÒÉÔÅÔ"
+
+#: ../templates/classic/html/issue.index.html:23
+msgid "ID"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:24
+msgid "Creation"
+msgstr "äÁÔÁ ÓÏÚÄÁÎÉÑ"
+
+#: ../templates/classic/html/issue.index.html:25
+msgid "Activity"
+msgstr "äÅÊÓÔ×ÉÅ"
+
+#: ../templates/classic/html/issue.index.html:26
+msgid "Actor"
+msgstr "÷ÙÐÏÌÎÉÌ"
+
+#: ../templates/classic/html/issue.index.html:27
+msgid "Topic"
+msgstr "ôÅÍÁ"
+
+#: ../templates/classic/html/issue.index.html:28
+#: ../templates/classic/html/issue.item.html:39
+msgid "Title"
+msgstr "úÁÇÌÁ×ÉÅ"
+
+#: ../templates/classic/html/issue.index.html:29
+#: ../templates/classic/html/issue.item.html:46
+msgid "Status"
+msgstr "óÔÁÔÕÓ"
+
+#: ../templates/classic/html/issue.index.html:30
+msgid "Creator"
+msgstr "á×ÔÏÒ"
+
+#: ../templates/classic/html/issue.index.html:31
+msgid "Assigned To"
+msgstr "éÓÐÏÌÎÉÔÅÌØ"
+
+#: ../templates/classic/html/issue.index.html:97
+msgid "Download as CSV"
+msgstr "óËÁÞÁÔØ CSV"
+
+#: ../templates/classic/html/issue.index.html:105
+msgid "Sort on:"
+msgstr "óÏÒÔÉÒÏ×ËÁ:"
+
+#: ../templates/classic/html/issue.index.html:108
+#: ../templates/classic/html/issue.index.html:125
+msgid "- nothing -"
+msgstr "- ÎÅÔ -"
+
+#: ../templates/classic/html/issue.index.html:116
+#: ../templates/classic/html/issue.index.html:133
+msgid "Descending:"
+msgstr "ðÏ ÕÂÙ×ÁÎÉÀ:"
+
+#: ../templates/classic/html/issue.index.html:122
+msgid "Group on:"
+msgstr "çÒÕÐÐÉÒÏ×ËÁ:"
+
+#: ../templates/classic/html/issue.index.html:139
+msgid "Redisplay"
+msgstr "ïÂÎÏ×ÉÔØ"
+
+#: ../templates/classic/html/issue.item.html:7
+msgid "Issue ${id}: ${title} - ${tracker}"
+msgstr "úÁÄÁÞÁ ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:10
+msgid "New Issue - ${tracker}"
+msgstr "îÏ×ÁÑ ÚÁÄÁÞÁ - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:14
+msgid "New Issue"
+msgstr "îÏ×ÁÑ ÚÁÄÁÞÁ"
+
+#: ../templates/classic/html/issue.item.html:16
+msgid "New Issue Editing"
+msgstr "òÅÇÉÓÔÒÁÃÉÑ ÎÏ×ÏÊ ÚÁÄÁÞÉ"
+
+#: ../templates/classic/html/issue.item.html:19
+msgid "Issue${id}"
+msgstr "úÁÄÁÞÁ ${id}"
+
+#: ../templates/classic/html/issue.item.html:22
+msgid "Issue${id} Editing"
+msgstr "òÅÄÁËÔÉÒÏ×ÁÎÉÅ ÚÁÄÁÞÉ ${id}"
+
+#: ../templates/classic/html/issue.item.html:51
+msgid "Superseder"
+msgstr "úÁÍÅÝÅÎÉÅ"
+
+#: ../templates/classic/html/issue.item.html:56
+msgid "View: ${link}"
+msgstr "ðÒÏÓÍÏÔÒ: ${link}"
+
+#: ../templates/classic/html/issue.item.html:60
+msgid "Nosy List"
+msgstr "òÁÓÓÙÌËÁ ÉÚ×ÅÝÅÎÉÊ"
+
+#: ../templates/classic/html/issue.item.html:69
+msgid "Assigned To"
+msgstr "éÓÐÏÌÎÉÔÅÌØ"
+
+#: ../templates/classic/html/issue.item.html:71
+msgid "Topics"
+msgstr "ôÅÍÙ"
+
+#: ../templates/classic/html/issue.item.html:79
+msgid "Change Note"
+msgstr "úÁÍÅÔËÉ"
+
+#: ../templates/classic/html/issue.item.html:87
+msgid "File"
+msgstr "æÁÊÌ"
+
+#: ../templates/classic/html/issue.item.html:99
+msgid "Make a copy"
+msgstr "óËÏÐÉÒÏ×ÁÔØ"
+
+#: ../templates/classic/html/issue.item.html:107
+#: ../templates/classic/html/user.item.html:106
+#: ../templates/classic/html/user.register.html:69
+#: ../templates/minimal/html/user.item.html:86
+msgid ""
+"<table class=\"form\"> <tr> <td>Note: </td> <th class=\"required"
+"\">highlighted</th> <td> fields are required.</td> </tr> </table>"
+msgstr ""
+"<table class=\"form\"> <tr> <td>ðÒÉÍÅÞÁÎÉÅ: </td><th class=\"required"
+"\">×ÙÄÅÌÅÎÎÙÅ</th><td> ÐÏÌÑ ÄÏÌÖÎÙ ÂÙÔØ ÚÁÐÏÌÎÅÎÙ.</td> </tr> </table>"
+
+#: ../templates/classic/html/issue.item.html:121
+msgid ""
+"Created on <b>${creation}</b> by <b>${creator}</b>, last changed <b>"
+"${activity}</b> by <b>${actor}</b>."
+msgstr ""
+"óÏÚÄÁÎÏ <b>${creation}</b> ÐÏÌØÚÏ×ÁÔÅÌÅÍ <b>${creator}</b>, ÐÏÓÌÅÄÎÅÅ "
+"ÉÚÍÅÎÅÎÉÅ <b>${activity}</b>, ÐÏÌØÚÏ×ÁÔÅÌØ <b>${actor}</b>."
+
+#: ../templates/classic/html/issue.item.html:125
+#: ../templates/classic/html/msg.item.html:56
+msgid "Files"
+msgstr "æÁÊÌÙ"
+
+#: ../templates/classic/html/issue.item.html:127
+#: ../templates/classic/html/msg.item.html:58
+msgid "File name"
+msgstr "éÍÑ ÆÁÊÌÁ"
+
+#: ../templates/classic/html/issue.item.html:128
+#: ../templates/classic/html/msg.item.html:59
+msgid "Uploaded"
+msgstr "úÁÇÒÕÖÅÎ"
+
+#: ../templates/classic/html/issue.item.html:129
+msgid "Type"
+msgstr "ôÉÐ"
+
+#: ../templates/classic/html/issue.item.html:130
+#: ../templates/classic/html/query.edit.html:30
+msgid "Edit"
+msgstr "òÅÄÁËÔÉÒÏ×ÁÔØ"
+
+#: ../templates/classic/html/issue.item.html:131
+msgid "Remove"
+msgstr "õÄÁÌÉÔØ"
+
+#: ../templates/classic/html/issue.item.html:151
+#: ../templates/classic/html/issue.item.html:172
+#: ../templates/classic/html/query.edit.html:50
+msgid "remove"
+msgstr "ÕÄÁÌÉÔØ"
+
+#: ../templates/classic/html/issue.item.html:158
+#: ../templates/classic/html/msg.index.html:9
+msgid "Messages"
+msgstr "óÏÏÂÝÅÎÉÑ"
+
+#: ../templates/classic/html/issue.item.html:162
+msgid "msg${id} (view)"
+msgstr "msg${id} (ÐÒÏÓÍÏÔÒ)"
+
+#: ../templates/classic/html/issue.item.html:163
+msgid "Author: ${author}"
+msgstr "á×ÔÏÒ: ${author}"
+
+#: ../templates/classic/html/issue.item.html:165
+msgid "Date: ${date}"
+msgstr "äÁÔÁ: ${date}"
+
+#: ../templates/classic/html/issue.search.html:2
+msgid "Issue searching - ${tracker}"
+msgstr "ðÏÉÓË - ${tracker}"
+
+#: ../templates/classic/html/issue.search.html:4
+msgid "Issue searching"
+msgstr "ðÏÉÓË"
+
+#: ../templates/classic/html/issue.search.html:25
+msgid "Filter on"
+msgstr "þÔÏ ÉÓËÁÔØ"
+
+#: ../templates/classic/html/issue.search.html:26
+msgid "Display"
+msgstr "ðÏËÁÚÁÔØ"
+
+#: ../templates/classic/html/issue.search.html:27
+msgid "Sort on"
+msgstr "óÏÒÔÉÒÏ×ÁÔØ"
+
+#: ../templates/classic/html/issue.search.html:28
+msgid "Group on"
+msgstr "çÒÕÐÐÉÒÏ×ÁÔØ"
+
+#: ../templates/classic/html/issue.search.html:32
+msgid "All text*:"
+msgstr "×Ï ×ÓÅÍ ÔÅËÓÔÅ*:"
+
+#: ../templates/classic/html/issue.search.html:40
+msgid "Title:"
+msgstr "× ÚÁÇÏÌÏ×ËÅ:"
+
+#: ../templates/classic/html/issue.search.html:50
+msgid "Topic:"
+msgstr "ôÅÍÁ:"
+
+#: ../templates/classic/html/issue.search.html:58
+msgid "ID:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:66
+msgid "Creation Date:"
+msgstr "äÁÔÁ ÓÏÚÄÁÎÉÑ:"
+
+#: ../templates/classic/html/issue.search.html:77
+msgid "Creator:"
+msgstr "á×ÔÏÒ:"
+
+#: ../templates/classic/html/issue.search.html:79
+msgid "created by me"
+msgstr "ÓÏÚÄÁÎÏ ÍÎÏÊ"
+
+#: ../templates/classic/html/issue.search.html:88
+msgid "Activity:"
+msgstr "äÅÊÓÔ×ÉÅ:"
+
+#: ../templates/classic/html/issue.search.html:99
+msgid "Actor:"
+msgstr "÷ÙÐÏÌÎÉÌ:"
+
+#: ../templates/classic/html/issue.search.html:101
+msgid "done by me"
+msgstr "×ÙÐÏÌÎÅÎÏ ÍÎÏÊ"
+
+#: ../templates/classic/html/issue.search.html:112
+msgid "Priority:"
+msgstr "ðÒÉÏÒÉÔÅÔ:"
+
+#: ../templates/classic/html/issue.search.html:114
+#: ../templates/classic/html/issue.search.html:130
+msgid "not selected"
+msgstr "ÎÅ ÕÓÔÁÎÏ×ÌÅÎ"
+
+#: ../templates/classic/html/issue.search.html:125
+msgid "Status:"
+msgstr "óÔÁÔÕÓ:"
+
+#: ../templates/classic/html/issue.search.html:128
+msgid "not resolved"
+msgstr "ÎÅ ÚÁËÒÙÔ"
+
+#: ../templates/classic/html/issue.search.html:143
+msgid "Assigned to:"
+msgstr "éÓÐÏÌÎÉÔÅÌØ:"
+
+#: ../templates/classic/html/issue.search.html:146
+msgid "assigned to me"
+msgstr "ÐÏÒÕÞÅÎÏ ÍÎÅ"
+
+#: ../templates/classic/html/issue.search.html:148
+msgid "unassigned"
+msgstr "ÎÅÎÁÚÎÁÞÅÎÏ"
+
+#: ../templates/classic/html/issue.search.html:158
+msgid "No Sort or group:"
+msgstr "îÅ ÓÏÒÔÉÒÏ×ÁÔØ / ÎÅ ÇÒÕÐÐÉÒÏ×ÁÔØ"
+
+#: ../templates/classic/html/issue.search.html:166
+msgid "Pagesize:"
+msgstr "òÁÚÍÅÒ ÓÔÒÁÎÉÃÙ:"
+
+#: ../templates/classic/html/issue.search.html:172
+msgid "Start With:"
+msgstr "îÁÞÁÔØ Ó:"
+
+#: ../templates/classic/html/issue.search.html:178
+msgid "Sort Descending:"
+msgstr "óÏÒÔÉÒÏ×ÁÔØ ÐÏ ÕÂÙ×ÁÎÉÀ:"
+
+#: ../templates/classic/html/issue.search.html:185
+msgid "Group Descending:"
+msgstr "çÒÕÐÐÉÒÏ×ÁÔØ ÐÏ ÕÂÙ×ÁÎÉÀ"
+
+#: ../templates/classic/html/issue.search.html:192
+msgid "Query name**:"
+msgstr "éÍÑ ÚÁÐÒÏÓÁ**:"
+
+#: ../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
+msgid "Search"
+msgstr "ðÏÉÓË"
+
+#: ../templates/classic/html/issue.search.html:209
+msgid "*: The \"all text\" field will look in message bodies and issue titles"
+msgstr ""
+"*: ðÏÉÓË ÐÏ ×ÓÅÍÕ ÔÅËÓÔÕ ÉÝÅÔ ××ÅÄÅÎÎÕÀ ÓÔÒÏËÕ × ÚÁÇÏÌÏ×ËÁÈ É × ÔÅÌÅ "
+"ÓÏÏÂÝÅÎÉÊ."
+
+#: ../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 ""
+"**: åÓÌÉ ÕËÁÚÁÎÏ ÉÍÑ, ÚÁÐÒÏÓ ÂÕÄÅÔ ÓÏÈÒÁÎÅÎ ÐÏÄ ÜÔÉÍ ÉÍÅÎÅÍ "
+"É ÐÏÑ×ÉÔÓÑ × ÓÐÉÓËÅ ÚÁÐÒÏÓÏ× × ÍÅÎÀ."
+
+#: ../templates/classic/html/keyword.item.html:3
+msgid "Keyword editing - ${tracker}"
+msgstr "òÅÄÁËÔÉÒÏ×ÁÎÉÅ ËÌÀÞÅ×ÙÈ ÓÌÏ× - ${tracker}"
+
+#: ../templates/classic/html/keyword.item.html:5
+msgid "Keyword editing"
+msgstr "òÅÄÁËÔÉÒÏ×ÁÎÉÅ ËÌÀÞÅ×ÙÈ ÓÌÏ×"
+
+#: ../templates/classic/html/keyword.item.html:11
+msgid "Existing Keywords"
+msgstr "óÕÝÅÓÔ×ÕÀÝÉÅ ËÌÀÞÅ×ÙÅ ÓÌÏ×Á"
+
+#: ../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
+msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
+msgstr ""
+"þÔÏÂÙ ÓÏÚÄÁÔØ ÎÏ×ÏÅ ËÌÀÞÅ×ÏÅ ÓÌÏ×Ï, ÚÁÐÏÌÎÉÔÅ ÐÏÌÅ ××ÏÄÁ É ÎÁÖÍÉÔÅ ËÎÏÐËÕ "
+"\"äÏÂÁ×ÉÔØ\"."
+
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
+msgstr "ëÌÀÞÅ×ÏÅ ÓÌÏ×Ï"
+
+#: ../templates/classic/html/msg.index.html:3
+msgid "List of messages - ${tracker}"
+msgstr "óÐÉÓÏË ÓÏÏÂÝÅÎÉÊ - ${tracker}"
+
+#: ../templates/classic/html/msg.index.html:5
+msgid "Message listing"
+msgstr "óÐÉÓÏË ÓÏÏÂÝÅÎÉÊ"
+
+#: ../templates/classic/html/msg.item.html:6
+msgid "Message ${id} - ${tracker}"
+msgstr "óÏÏÂÝÅÎÉÅ ${id} - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:9
+msgid "New Message - ${tracker}"
+msgstr "îÏ×ÏÅ ÓÏÏÂÝÅÎÉÅ - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:13
+msgid "New Message"
+msgstr "îÏ×ÏÅ ÓÏÏÂÝÅÎÉÅ"
+
+#: ../templates/classic/html/msg.item.html:15
+msgid "New Message Editing"
+msgstr "òÅÄÁËÔÉÒÏ×ÁÎÉÅ ÎÏ×ÏÇÏ ÓÏÏÂÝÅÎÉÑ"
+
+#: ../templates/classic/html/msg.item.html:18
+msgid "Message${id}"
+msgstr "óÏÏÂÝÅÎÉÅ ${id}"
+
+#: ../templates/classic/html/msg.item.html:21
+msgid "Message${id} Editing"
+msgstr "òÅÄÁËÔÉÒÏ×ÁÎÉÅ ÓÏÏÂÝÅÎÉÑ ${id}"
+
+#: ../templates/classic/html/msg.item.html:33
+msgid "Author"
+msgstr "á×ÔÏÒ"
+
+#: ../templates/classic/html/msg.item.html:38
+msgid "Recipients"
+msgstr "áÄÒÅÓÁÔÙ"
+
+#: ../templates/classic/html/msg.item.html:49
+msgid "Content"
+msgstr "óÏÄÅÒÖÁÎÉÅ"
+
+#: ../templates/classic/html/page.html:41
+msgid "<b>Your Queries</b> (<a href=\"query?@template=edit\">edit</a>)"
+msgstr "<b>÷ÁÛÉ ÚÁÐÒÏÓÙ</b> (<a href=\"query?@template=edit\">ÒÅÄÁËÔÏÒ</a>)"
+
+#: ../templates/classic/html/page.html:52
+msgid "Issues"
+msgstr "úÁÄÁÞÉ"
+
+#: ../templates/classic/html/page.html:54
+#: ../templates/classic/html/page.html:74
+msgid "Create New"
+msgstr "äÏÂÁ×ÉÔØ"
+
+#: ../templates/classic/html/page.html:56
+msgid "Show Unassigned"
+msgstr "îÅÎÁÚÎÁÞÅÎÎÙÅ"
+
+#: ../templates/classic/html/page.html:58
+msgid "Show All"
+msgstr "ðÏËÁÚÁÔØ ×ÓÅ"
+
+#: ../templates/classic/html/page.html:61
+msgid "Show issue:"
+msgstr "ðÏËÁÚÁÔØ:"
+
+#: ../templates/classic/html/page.html:72
+msgid "Keywords"
+msgstr "ëÌÀÞÅ×ÙÅ ÓÌÏ×Á"
+
+#: ../templates/classic/html/page.html:78
+msgid "Edit Existing"
+msgstr "òÅÄÁËÔÉÒÏ×ÁÔØ"
+
+#: ../templates/classic/html/page.html:84
+#: ../templates/minimal/html/page.html:65
+msgid "Administration"
+msgstr "áÄÍÉÎÉÓÔÒÉÒÏ×ÁÎÉÅ"
+
+#: ../templates/classic/html/page.html:86
+#: ../templates/minimal/html/page.html:66
+msgid "Class List"
+msgstr "óÐÉÓÏË ËÌÁÓÓÏ×"
+
+#: ../templates/classic/html/page.html:90
+#: ../templates/minimal/html/page.html:68
+msgid "User List"
+msgstr "óÐÉÓÏË ÐÏÌØÚÏ×ÁÔÅÌÅÊ"
+
+#: ../templates/classic/html/page.html:92
+#: ../templates/minimal/html/page.html:71
+msgid "Add User"
+msgstr "äÏÂÁ×ÉÔØ ÐÏÌØÚÏ×ÁÔÅÌÑ"
+
+#: ../templates/classic/html/page.html:99
+#: ../templates/classic/html/page.html:105
+#: ../templates/minimal/html/page.html:46
+msgid "Login"
+msgstr "÷ÈÏÄ"
+
+#: ../templates/classic/html/page.html:104
+#: ../templates/minimal/html/page.html:45
+msgid "Remember me?"
+msgstr "úÁÐÏÍÎÉÔØ"
+
+#: ../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 "úÁÒÅÇÉÓÔÒÉÒÏ×ÁÔØÓÑ"
+
+#: ../templates/classic/html/page.html:111
+msgid "Lost your login?"
+msgstr "úÁÂÙÌÉ ÐÁÒÏÌØ?"
+
+#: ../templates/classic/html/page.html:116
+msgid "Hello, ${user}"
+msgstr "úÄÒÁ×ÓÔ×ÕÊÔÅ, ${user}!"
+
+#: ../templates/classic/html/page.html:118
+msgid "Your Issues"
+msgstr "úÁÄÁÞÉ"
+
+#: ../templates/classic/html/page.html:119
+#: ../templates/minimal/html/page.html:57
+msgid "Your Details"
+msgstr "õÞÅÔÎÁÑ ËÁÒÔÏÞËÁ"
+
+#: ../templates/classic/html/page.html:121
+#: ../templates/minimal/html/page.html:59
+msgid "Logout"
+msgstr "÷ÙÈÏÄ"
+
+#: ../templates/classic/html/page.html:125
+msgid "Help"
+msgstr "ðÏÍÏÝØ"
+
+#: ../templates/classic/html/page.html:126
+msgid "Roundup docs"
+msgstr "äÏËÕÍÅÎÔÁÃÉÑ Roundup"
+
+#: ../templates/classic/html/page.html:136
+#: ../templates/minimal/html/page.html:81
+msgid "clear this message"
+msgstr "ÓÂÒÏÓÉÔØ ÜÔÏ ÓÏÏÂÝÅÎÉÅ"
+
+#: ../templates/classic/html/page.html:181
+msgid "don't care"
+msgstr "ÎÅ×ÁÖÎÏ"
+
+#: ../templates/classic/html/page.html:183
+msgid "------------"
+msgstr ""
+
+#: ../templates/classic/html/page.html:210
+msgid "no value"
+msgstr "ÎÅÔ ÚÎÁÞÅÎÉÑ"
+
+#: ../templates/classic/html/query.edit.html:4
+msgid "\"Your Queries\" Editing - ${tracker}"
+msgstr "òÅÄÁËÔÉÒÏ×ÁÎÉÅ \"÷ÁÛÉÈ ÚÁÐÒÏÓÏ×\" - ${tracker}"
+
+#: ../templates/classic/html/query.edit.html:6
+msgid "\"Your Queries\" Editing"
+msgstr "òÅÄÁËÔÉÒÏ×ÁÎÉÅ \"÷ÁÛÉÈ ÚÁÐÒÏÓÏ×\""
+
+#: ../templates/classic/html/query.edit.html:11
+msgid "You are not allowed to edit queries."
+msgstr "õ ×ÁÓ ÎÅÔ ÒÁÚÒÅÛÅÎÉÑ ÎÁ ÒÅÄÁËÔÉÒÏ×ÁÎÉÅ ÚÁÐÒÏÓÏ×"
+
+#: ../templates/classic/html/query.edit.html:28
+msgid "Query"
+msgstr "úÁÐÒÏÓ"
+
+#: ../templates/classic/html/query.edit.html:29
+msgid "Include in \"Your Queries\""
+msgstr "÷ËÌÀÞÉÔØ × \"÷ÁÛÉ ÚÁÐÒÏÓÙ\""
+
+#: ../templates/classic/html/query.edit.html:31
+msgid "Private to you?"
+msgstr "ìÉÞÎÙÊ?"
+
+#: ../templates/classic/html/query.edit.html:44
+msgid "leave out"
+msgstr "ÎÅ ×ËÌÀÞÁÔØ"
+
+#: ../templates/classic/html/query.edit.html:45
+msgid "include"
+msgstr "×ËÌÀÞÉÔØ"
+
+#: ../templates/classic/html/query.edit.html:49
+msgid "leave in"
+msgstr "ÏÓÔÁ×ÉÔØ"
+
+#: ../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:92
+msgid "edit"
+msgstr "ÒÅÄÁËÔÉÒÏ×ÁÔØ"
+
+#: ../templates/classic/html/query.edit.html:71
+msgid "yes"
+msgstr "ÄÁ"
+
+#: ../templates/classic/html/query.edit.html:73
+msgid "no"
+msgstr "ÎÅÔ"
+
+#: ../templates/classic/html/query.edit.html:79
+msgid "Delete"
+msgstr "õÄÁÌÉÔØ"
+
+#: ../templates/classic/html/query.edit.html:94
+msgid "[not yours to edit]"
+msgstr "[ÞÕÖÏÊ ÚÁÐÒÏÓ - ÒÅÄÁËÔÉÒÏ×ÁÔØ ÎÅÌØÚÑ]"
+
+#: ../templates/classic/html/query.edit.html:102
+msgid "Save Selection"
+msgstr "óÏÈÒÁÎÉÔØ ÉÚÍÅÎÅÎÉÑ"
+
+#: ../templates/classic/html/user.forgotten.html:3
+msgid "Password reset request - ${tracker}"
+msgstr "óÂÒÏÓ ÐÁÒÏÌÑ - ${tracker}"
+
+#: ../templates/classic/html/user.forgotten.html:5
+msgid "Password reset request"
+msgstr "óÂÒÏÓ ÐÁÒÏÌÑ"
+
+#: ../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 "áÄÒÅÓ email:"
+
+#: ../templates/classic/html/user.forgotten.html:24
+#: ../templates/classic/html/user.forgotten.html:34
+msgid "Request password reset"
+msgstr "ïÞÉÓÔÉÔØ ÐÁÒÏÌØ"
+
+#: ../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 "éÍÑ ÐÏÌØÚÏ×ÁÔÅÌÑ:"
+
+#: ../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 ""
+"äÌÑ ÐÏÄÔ×ÅÒÖÄÅÎÉÑ ÜÔÏÊ ÏÐÅÒÁÃÉÉ ×ÁÍ ÂÕÄÅÔ ÐÏÓÌÁÎÏ ÓÏÏÂÝÅÎÉÅ ÐÏ ÜÌÅËÔÒÏÎÎÏÊ "
+"ÐÏÞÔÅ. ÷ ÜÔÏÍ ÐÉÓØÍÅ ÂÕÄÅÔ ÎÁÐÉÓÁÎÏ, ÞÔÏ ×Ù ÄÏÌÖÎÙ ÓÄÅÌÁÔØ, ÞÔÏÂÙ ÏÞÉÓÔÉÔØ "
+"ÐÁÒÏÌØ Roundup."
+
+#: ../templates/classic/html/user.index.html:3
+#: ../templates/minimal/html/user.index.html:3
+msgid "User listing - ${tracker}"
+msgstr "óÐÉÓÏË ÐÏÌØÚÏ×ÁÔÅÌÅÊ - ${tracker}"
+
+#: ../templates/classic/html/user.index.html:5
+#: ../templates/minimal/html/user.index.html:5
+msgid "User listing"
+msgstr "óÐÉÓÏË ÐÏÌØÚÏ×ÁÔÅÌÅÊ"
+
+#: ../templates/classic/html/user.index.html:14
+#: ../templates/minimal/html/user.index.html:14
+msgid "Username"
+msgstr "ðÏÌØÚÏ×ÁÔÅÌØ"
+
+#: ../templates/classic/html/user.index.html:15
+msgid "Real name"
+msgstr "éÍÑ, ÆÁÍÉÌÉÑ"
+
+#: ../templates/classic/html/user.index.html:16
+#: ../templates/classic/html/user.item.html:70
+#: ../templates/classic/html/user.register.html:45
+msgid "Organisation"
+msgstr "ïÒÇÁÎÉÚÁÃÉÑ"
+
+#: ../templates/classic/html/user.index.html:17
+#: ../templates/minimal/html/user.index.html:15
+msgid "Email address"
+msgstr "áÄÒÅÓ email"
+
+#: ../templates/classic/html/user.index.html:18
+msgid "Phone number"
+msgstr "ôÅÌÅÆÏÎ"
+
+#: ../templates/classic/html/user.index.html:19
+msgid "Retire"
+msgstr "õ×ÏÌÉÔØ"
+
+#: ../templates/classic/html/user.index.html:32
+msgid "retire"
+msgstr "Õ×ÏÌÉÔØ"
+
+#: ../templates/classic/html/user.item.html:7
+#: ../templates/minimal/html/user.item.html:7
+msgid "User ${id}: ${title} - ${tracker}"
+msgstr "ðÏÌØÚÏ×ÁÔÅÌØ ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:10
+#: ../templates/minimal/html/user.item.html:10
+msgid "New User - ${tracker}"
+msgstr "îÏ×ÙÊ ÐÏÌØÚÏ×ÁÔÅÌØ - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:14
+#: ../templates/minimal/html/user.item.html:14
+msgid "New User"
+msgstr "îÏ×ÙÊ ÐÏÌØÚÏ×ÁÔÅÌØ"
+
+#: ../templates/classic/html/user.item.html:16
+#: ../templates/minimal/html/user.item.html:16
+msgid "New User Editing"
+msgstr "òÅÇÉÓÔÒÁÃÉÑ ÎÏ×ÏÇÏ ÐÏÌØÚÏ×ÁÔÅÌÑ"
+
+#: ../templates/classic/html/user.item.html:19
+#: ../templates/minimal/html/user.item.html:19
+msgid "User${id}"
+msgstr "ðÏÌØÚÏ×ÁÔÅÌØ ${id}"
+
+#: ../templates/classic/html/user.item.html:22
+#: ../templates/minimal/html/user.item.html:22
+msgid "User${id} Editing"
+msgstr "òÅÄÁËÔÉÒÏ×ÁÎÉÅ ËÁÒÔÏÞËÉ ÐÏÌØÚÏ×ÁÔÅÌÑ ${id}"
+
+#: ../templates/classic/html/user.item.html:43
+#: ../templates/classic/html/user.register.html:21
+#: ../templates/minimal/html/user.item.html:40
+#: ../templates/minimal/html/user.register.html:26
+msgid "Login Name"
+msgstr "õÞÅÔÎÏÅ ÉÍÑ"
+
+#: ../templates/classic/html/user.item.html:47
+#: ../templates/classic/html/user.register.html:25
+#: ../templates/minimal/html/user.item.html:44
+#: ../templates/minimal/html/user.register.html:30
+msgid "Login Password"
+msgstr "ðÁÒÏÌØ"
+
+#: ../templates/classic/html/user.item.html:51
+#: ../templates/classic/html/user.register.html:29
+#: ../templates/minimal/html/user.item.html:48
+#: ../templates/minimal/html/user.register.html:34
+msgid "Confirm Password"
+msgstr "(ÅÝÅ ÒÁÚ)"
+
+#: ../templates/classic/html/user.item.html:55
+#: ../templates/classic/html/user.register.html:33
+#: ../templates/minimal/html/user.item.html:52
+#: ../templates/minimal/html/user.register.html:38
+msgid "Roles"
+msgstr "òÏÌÉ"
+
+#: ../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 "(ÅÓÌÉ ÒÏÌÅÊ ÎÅÓËÏÌØËÏ, ÐÅÒÅÞÉÓÌÉÔÅ ÉÈ ÞÅÒÅÚ ÚÁÐÑÔÕÀ)"
+
+#: ../templates/classic/html/user.item.html:66
+#: ../templates/classic/html/user.register.html:41
+msgid "Phone"
+msgstr "ôÅÌÅÆÏÎ"
+
+#: ../templates/classic/html/user.item.html:74
+msgid "Timezone"
+msgstr "þÁÓÏ×ÏÊ ÐÏÑÓ"
+
+#: ../templates/classic/html/user.item.html:78
+msgid "(this is a numeric hour offset, the default is ${zone})"
+msgstr ""
+"(ÞÉÓÌÏ - ÒÁÚÎÉÃÁ ÍÅÖÄÕ ÍÅÓÔÎÙÍ É ÇÒÉÎ×ÉÞÓËÉÍ ×ÒÅÍÅÎÅÍ, ÐÏ ÕÍÏÌÞÁÎÉÀ - "
+"${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"
+
+#: ../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 "äÏÐÏÌÎÉÔÅÌØÎÙÅ ÁÄÒÅÓÁ email<br />ðÏ ÏÄÎÏÍÕ ÁÄÒÅÓÕ × ÓÔÒÏËÅ"
+
+#: ../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 "òÅÇÉÓÔÒÁÃÉÑ × ${tracker}"
+
+#: ../templates/classic/html/user.rego_progress.html:4
+#: ../templates/minimal/html/user.rego_progress.html:4
+msgid "Registration in progress - ${tracker}"
+msgstr "÷ÙÐÏÌÎÑÅÔÓÑ ÒÅÇÉÓÔÒÁÃÉÑ - ${tracker}"
+
+#: ../templates/classic/html/user.rego_progress.html:6
+#: ../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
+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/minimal/html/home.html:2
+msgid "Tracker home - ${tracker}"
+msgstr "ãÅÎÔÒ ÕÐÒÁ×ÌÅÎÉÑ ÚÁÄÁÎÉÑÍÉ - ${tracker}"
+
+#: ../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 "÷ÙÂÅÒÉÔÅ ÄÅÊÓÔ×ÉÅ ÉÚ ÍÅÎÀ ÓÌÅ×Á."
+
+#: ../templates/minimal/html/home.html:19
+msgid "Please log in or register."
+msgstr "ðÏÖÁÌÕÊÓÔÁ, ×ÈÏÄÉÔÅ ÉÌÉ ÚÁÒÅÇÉÓÔÒÉÒÕÊÔÅÓØ"
+
+#: ../templates/minimal/html/page.html:55
+msgid "Hello,<br>${user}"
+msgstr "ðÒÉ×ÅÔ,<br>${user}"
Added: tracker/vendor/roundup/current/locale/zh_CN.po
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/locale/zh_CN.po Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2743 @@
+# 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 $
+#
+# roundup.pot revision 1.10
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.8.3\n"
+"Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
+"POT-Creation-Date: 2004-10-19 12:33+0300\n"
+"PO-Revision-Date: 2005-05-16 13:56+0800\n"
+"Last-Translator: limodou <limodou at gmail.com>\n"
+"Language-Team: Chinese Simplified <limodou at gmail.com>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Poedit-Language: Chinese\n"
+"X-Poedit-Country: CHINA\n"
+
+# ../roundup/admin.py:84 :943 :992 :1014
+#: ../roundup/admin.py:84
+#: ../roundup/admin.py:943
+#: ../roundup/admin.py:992
+#: ../roundup/admin.py:1014
+#, python-format
+msgid "no such class \"%(classname)s\""
+msgstr "æ æ¤ç±»å« \"%(classname)s\""
+
+# ../roundup/admin.py:94 :98
+#: ../roundup/admin.py:94
+#: ../roundup/admin.py:98
+#, python-format
+msgid "argument \"%(arg)s\" not propname=value"
+msgstr "åæ° \"%(arg)s\" ä¸æ¯ propname=value çå½¢å¼"
+
+#: ../roundup/admin.py:111
+#, python-format
+msgid ""
+"Problem: %(message)s\n"
+"\n"
+msgstr ""
+"é®é¢: %(message)s\n"
+"\n"
+
+#: ../roundup/admin.py:112
+#, 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"
+"\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 ""
+"%(message)sç¨æ³: roundup-admin [options] [<command> <arguments>]\n"
+"\n"
+"é项ï¼\n"
+" -i å®ä¾è·¯å¾ -- æå®é®é¢è·è¸ªç³»ç» \"æ ¹ç®å½\" 为 管çå\n"
+" -u -- user[:password] ç¨äºå½ä»¤ä¸\n"
+" -d -- æå°ææçæ示信æ¯èä¸åªæ¯ç±»çIDå·\n"
+" -c -- å¨è¾åºæ°æ®å表æ¶ï¼ä½¿ç¨å¥å·('.')åéã\n"
+" å¦åæ§è¡ '-S \",\"'ã\n"
+" -S <string> -- å½è¾åºæ°æ®å表æ¶ï¼ä½¿ç¨ string åé\n"
+" -s -- å½è¾åºæ°æ®å表æ¶ï¼ä½¿ç¨ç©ºæ ¼åéã\n"
+" å¦åæ§è¡ '-S \" \"'ã\n"
+"\n"
+" -s, -c æè
-S åªè½æä¸ä¸ªè¢«æå®ã\n"
+"\n"
+"帮å©ï¼\n"
+" roundup-admin -h\n"
+" roundup-admin help -- æ¬å¸®å©\n"
+" roundup-admin help <command> -- å½ä»¤è¯¦è§£å¸®å©\n"
+" roundup-admin help all -- ææå¯ç¨ç帮å©\n"
+
+#: ../roundup/admin.py:137
+msgid "Commands:"
+msgstr "å½ä»¤ï¼"
+
+#: ../roundup/admin.py:144
+msgid ""
+"Commands may be abbreviated as long as the abbreviation\n"
+"matches only one command, e.g. l == li == lis == list."
+msgstr ""
+"å½ä»¤å¯ä»¥è¢«ç¼©åï¼åªè¦ç¼©ååªæä¸ä¸ªå½ä»¤å¯ä»¥å¹é
ä¸ï¼\n"
+"å¦ï¼l == li == lis == list."
+
+#: ../roundup/admin.py:174
+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 ""
+"\n"
+"ææçå½ä»¤(é¤äº help)è¦æ±æå®ä¸ä¸ªtrackerãè¿å°±æ¯ä½ æ£å¨å·¥ä½çtrackerçè·¯å¾ã\n"
+"ä¸ä¸ªtrackerå°±æ¯roundupç»´æ¤çæ°æ®åºåå®ä¹äºtrackeré
ç½®æ件çå°æ¹ãå¯ä»¥æå®æ³\n"
+"象为é®é¢è·è¸ªç³»ç»ç\"èµ·å§\"ç®å½ãå®å¯ä»¥å¨ç¯å¢åé TRACKER_HOME æå¨å½ä»¤è¡ä»¥ \n"
+"\"-i tracker\" æ¥æå®ã\n"
+"\n"
+"ä¸ä¸ªæ示å¨(designator)æ¯ä¸ä¸ªç±»ååä¸ä¸ªç»ç¹idçç»åä½ï¼å¦ï¼bug1, user10, ...\n"
+"\n"
+"å±æ§å¼å¨å½ä»¤åæ°ä¸åæå°ç»æä¸è¢«æ述为å符串ï¼\n"
+" . Strings 表示å符串ã\n"
+" . Date çå¼å¨æ¬å°æ¶åºä¸æå
¨æ¥ææ ¼å¼æå°ï¼å¹¶ä¸å¯ä»¥æå
¨æ¥ææ ¼å¼æä¸é¢è§£éçä»»\n"
+" ä½é¨åæ¥ææ ¼å¼æ¥æ¥æ¶ã\n"
+" . Link çå¼æç»ç¹æ示å¨(designator)æ¥æå°ãå½ä½ä¸ºåæ°ç»åºæ¶ï¼ç»ç¹æ示å¨\n"
+" (designator)åé®å符串é½å¯ä»¥æ¥æ¶ã\n"
+" . Multilink çå¼æç»ç¹æ示å¨(designator)å表(以éå·åé)æ¥æå°ãå½ä½ä¸ºä¸ä¸ªå\n"
+" æ°ç»åºæ¶ï¼ç»ç¹æ示å¨(designator)æ以éå·èæ¥çç»ç¹å表é½æ¯å¯ä»¥æ¥åçã\n"
+"\n"
+"å½å±æ§å¼å¿
é¡»å
å«ç©ºæ ¼æ¶ï¼åªéä½¿ç¨ ' æè
\" æ¥å
å«å¼ãåä¸ªç©ºæ ¼ä¹å¯ä»¥ç¨åæ线æ¥\n"
+"转ä¹ãå¦æä¸ä¸ªå¼å¿
é¡»å
å«å¼å·å符ï¼å®å¿
须使ç¨åæ线æ¥è½¬ä¹æå
é¨å
å«ãä¾å¦ï¼\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"
+"å½å¤ä¸ªç»ç¹è¢«æå®ç¨å¨ Roundup ç get æ set å½ä»¤æ¶ï¼æå®çå±æ§å¨ææååº\n"
+"çç»ç¹ä¸ä¼è¢«è·åæ设置ã\n"
+"\n"
+"å½ Roundup ç get æ find å½ä»¤è¿åå¤ä¸ªç»ææ¶ï¼æ¯è¡å°æå°ä¸ä¸ªå±æ§(缺ç)æ\n"
+"ç¨éå·èæ¥èµ·æ¥(ç¨ -c åæ°)ã\n"
+"\n"
+"å¨åå¨ä¿®æ¹æ°æ®çå½ä»¤ä¸ï¼éè¦ç»å½å/å£ä»¤ãç»å½åæè
ç¨ \"name\" æ \"name:password\"\n"
+"æ¥æå®ã\n"
+" . ROUNDUP_LOGIN ç¯å¢åé\n"
+" . -u å½ä»¤è¡é项\n"
+"å¦æååæå£ä»¤é½æ²¡ææä¾ï¼å®ä»¬å°ä»å½ä»¤è¡è·å¾ã\n"
+"\n"
+"Date format examples:\n"
+" \"2000-04-17.03:45\" 表示 <Date 2000-04-17.08:45:00>\n"
+" \"2000-04-17\" 表示 <Date 2000-04-17.00:00:00>\n"
+" \"01-25\" 表示 <Date yyyy-01-25.00:00:00>\n"
+" \"08-13.22:13\" 表示 <Date yyyy-08-14.03:13:00>\n"
+" \"11-07.09:32:43\" 表示 <Date yyyy-11-07.14:32:43>\n"
+" \"14:25\" 表示 <Date yyyy-mm-dd.19:25:00>\n"
+" \"8:47:11\" 表示 <Date yyyy-mm-dd.13:47:11>\n"
+" \".\" 表示 \"ç°å¨\"\n"
+"\n"
+"使ç¨å¸®å©ï¼\n"
+
+#: ../roundup/admin.py:237
+#, python-format
+msgid "%s:"
+msgstr ""
+
+#: ../roundup/admin.py:242
+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 ""
+"ç¨æ³ï¼help topic\n"
+" ç»åºå
³äºä¸»é¢ç帮å©ã\n"
+"\n"
+" commands -- ååºå½ä»¤\n"
+" <command> -- æå®å½ä»¤ç帮å©è§è\n"
+" initopts -- åå§åå½ä»¤é项\n"
+" all -- ææå¯ç¨ç帮å©\n"
+" "
+
+#: ../roundup/admin.py:265
+#, python-format
+msgid "Sorry, no help for \"%(topic)s\""
+msgstr "æ±æï¼æ²¡æ对 \"%(topic)s\" ç帮å©ä¿¡æ¯"
+
+# ../roundup/admin.py:337 :387
+#: ../roundup/admin.py:337
+#: ../roundup/admin.py:387
+msgid "Templates:"
+msgstr "模æ¿ï¼"
+
+# ../roundup/admin.py:340 :398
+#: ../roundup/admin.py:340
+#: ../roundup/admin.py:398
+msgid "Back ends:"
+msgstr "å端ï¼"
+
+#: ../roundup/admin.py:343
+msgid ""
+"Usage: install [template [backend [admin password]]]\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"
+"\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 ""
+"ç¨æ³ï¼install [template [backend [admin password]]]\n"
+" å®è£
ä¸ä¸ªæ°çtrackerå®ä¾ã\n"
+"\n"
+" è¿ä¸ªå½ä»¤å°æ示è¾å
¥ tracker èµ·å§ç®å½\n"
+" (å¦æ没æéè¿ TRACKER_HOME æ -i é项æä¾)ã\n"
+" 模æ¿ãå端å管çåå£ä»¤åºè¯¥å¨å½ä»¤è¡æ顺åºä»¥åæ°çå½¢å¼è¢«æå®ã\n"
+"\n"
+" åå§å(initialise)å½ä»¤å¿
é¡»å¨è¿ä¸ªå½ä»¤ä¹å被è°ç¨ï¼ä»¥ä¾¿åå§åtrackeræ°\n"
+" æ®åºãä½ å¯ä»¥å¨è¿è¡åå§åå½ä»¤ä¹åç¼è¾ tracker ç dbinit.py 模åç\n"
+" init() æ¹æ³æ¥ä¿®æ¹ tracker çåå§æ°æ®åºå
容ã\n"
+"\n"
+" 请æ¥çåå§ååæ°å¸®å©ã\n"
+" "
+
+# ../roundup/admin.py:359 :494 :573 :623 :676 :697 :725 :796 :863 :934 :982
+# :1004 :1031 :1093 :1159
+#: ../roundup/admin.py:359
+#: ../roundup/admin.py:494
+#: ../roundup/admin.py:573
+#: ../roundup/admin.py:623
+#: ../roundup/admin.py:676
+#: ../roundup/admin.py:697
+#: ../roundup/admin.py:725
+#: ../roundup/admin.py:796
+#: ../roundup/admin.py:863
+#: ../roundup/admin.py:934
+#: ../roundup/admin.py:982
+#: ../roundup/admin.py:1004
+#: ../roundup/admin.py:1031
+#: ../roundup/admin.py:1093
+#: ../roundup/admin.py:1159
+msgid "Not enough arguments supplied"
+msgstr "æªæä¾è¶³å¤çåæ°"
+
+#: ../roundup/admin.py:365
+#, python-format
+msgid "Instance home parent directory \"%(parent)s\" does not exist"
+msgstr "å®ä¾ç®å½çç¶ç®å½ \"%(parent)s\" ä¸åå¨"
+
+#: ../roundup/admin.py:374
+#, 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 ""
+"è¦åï¼å¨ \"%(tracker_home)s\" å·²ç»åå¨ä¸ä¸ªtrackeräºï¼\n"
+"å¦æä½ æç®éæ°å®è£
å®ï¼ææçæ°æ®å°ä¼ä¸¢å¤±ï¼\n"
+"å é¤å®åï¼Y/N: "
+
+#: ../roundup/admin.py:389
+msgid "Select template [classic]: "
+msgstr "éæ©æ¨¡æ¿ [classic]ï¼"
+
+#: ../roundup/admin.py:400
+msgid "Select backend [anydbm]: "
+msgstr "éæ©å端 [anydbm]ï¼"
+
+#: ../roundup/admin.py:409
+#, python-format
+msgid ""
+"\n"
+" You should now edit the tracker configuration file:\n"
+" %(config_file)s"
+msgstr ""
+"\n"
+" ç°å¨ä½ åºè¯¥ä¿®æ¹trackerçé
ç½®æ件ï¼\n"
+" %(config_file)s"
+
+#: ../roundup/admin.py:418
+msgid " ... at a minimum, you must set following options:"
+msgstr " ... è³å°ï¼ä½ å¿
须设置以ä¸é项ï¼"
+
+#: ../roundup/admin.py:423
+#, 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"
+msgstr ""
+"\n"
+" å¦æä½ æ³è¦ä¿®æ¹æ°æ®åºç»æï¼\n"
+" ä½ ä¹éè¦ç¼è¾è¡¨ç»ææ件ï¼\n"
+" %(database_config_file)s\n"
+" ä½ å¯è½ä¹éè¦ä¿®æ¹æ°æ®åºåå§åæ件ï¼\n"
+" %(database_init_file)s\n"
+" ... æ¥çå
³äºå®¢æ·åçææ¡£æ¥äºè§£æ´å¤çä¿¡æ¯ã\n"
+
+#. password
+#: ../roundup/admin.py:438
+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 ""
+"ç¨æ³ï¼initialise [adminpw]\n"
+" åå§åä¸ä¸ªæ°çtrackerã\n"
+"\n"
+" 管çåçä¿¡æ¯éè¦å¨è¿ä¸æ¥è¿è¡è®¾ç½®ã\n"
+"\n"
+" æ§è¡trackerçåå§åå½æ° dbinit.init()\n"
+" "
+
+#: ../roundup/admin.py:452
+msgid "Admin Password: "
+msgstr "管çåå£ä»¤ï¼"
+
+#: ../roundup/admin.py:453
+msgid " Confirm: "
+msgstr " 确认ï¼"
+
+#: ../roundup/admin.py:457
+msgid "Instance home does not exist"
+msgstr "å®ä¾ç®å½ä¸åå¨"
+
+#: ../roundup/admin.py:461
+msgid "Instance has not been installed"
+msgstr "å®ä¾è¿æ²¡æå®è£
"
+
+#: ../roundup/admin.py:466
+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 ""
+"è¦åï¼æ°æ®åºå·²ç»è¢«åå§åï¼\n"
+"å¦æä½ éæ°åå§åå®ï¼ææçæ°æ®å°ä¼ä¸¢å¤±ï¼\n"
+"å é¤å®åï¼Y/N: "
+
+#: ../roundup/admin.py:487
+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 ""
+"ç¨æ³ï¼get property designator[,designator]*\n"
+" å¾å°æå®å±æ§ä¸ä¸ªæå¤ä¸ªæ示å¨(designator)ã\n"
+"\n"
+" éè¿æ示å¨(designator)æ¥å¾å°æå®ç»ç¹çå±æ§å¼ã\n"
+" "
+
+# ../roundup/admin.py:527 :542
+#: ../roundup/admin.py:527
+#: ../roundup/admin.py:542
+#, python-format
+msgid "property %s is not of type Multilink or Link so -d flag does not apply."
+msgstr "å±æ§ %s ä¸æ¯ Multilink æ Link ç±»åï¼æ以 -d æ å¿ä¸è½åºç¨ã"
+
+# ../roundup/admin.py:550 :945 :994 :1016
+#: ../roundup/admin.py:550
+#: ../roundup/admin.py:945
+#: ../roundup/admin.py:994
+#: ../roundup/admin.py:1016
+#, python-format
+msgid "no such %(classname)s node \"%(nodeid)s\""
+msgstr "没æè¿æ ·ç %(classname)s ç»ç¹ \"%(nodeid)s\""
+
+#: ../roundup/admin.py:552
+#, python-format
+msgid "no such %(classname)s property \"%(propname)s\""
+msgstr "没æè¿æ ·ç %(classname)s å±æ§ \"%(propname)s\""
+
+#: ../roundup/admin.py:561
+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 ""
+"ç¨æ³ï¼set items property=value property=value ...\n"
+" 设置ä¸ä¸ªæå¤ä¸ªæ¡ç®çå±æ§ã\n"
+"\n"
+" æ¡ç®æçæ¯ä¸ä¸ªç±»å«ï¼æ以éå·åéç项ç®æ示å¨(designator)å表(ä¾å¦ï¼\"designator[,designator,...]\")ã\n"
+"\n"
+" è¿ä¸ªå½ä»¤ä¸ºææç»åºçæ示å¨(designator)设置å±æ§å¼ãå¦æå±æ§å¼è¢«çç¥\n"
+" (ä¾å¦ï¼\"property=\")é£ä¹å±æ§æ¯æªè®¾ç½®çãå¦æå±æ§æ¯ä¸ä¸ªå¤é¾æ¥(multilink)ï¼\n"
+" ä½ éè¦ä¸ºå¤é¾æ¥æä¾ç¨éå·åéçæ°å(ä¾å¦ \"1,2,3\")ã\n"
+" "
+
+#: ../roundup/admin.py:615
+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 ""
+"ç¨æ³ï¼find classname propname=value ...\n"
+" æ ¹æ®ç»å®ç link å±æ§å¼æ¥æ¥æ¾ç»å®ç±»åçç»ç¹ã\n"
+"\n"
+" æ ¹æ®ç»å®ç link å±æ§å¼æ¥æ¥æ¾ç»å®ç±»åçç»ç¹ãè¿ä¸ªå¼æè
æ¯é¾æ¥ç»ç¹çç»ç¹IDï¼\n"
+" æè
æ¯ç»ç¹çé®å¼ã\n"
+" "
+
+# ../roundup/admin.py:663 :816 :828 :882
+#: ../roundup/admin.py:663
+#: ../roundup/admin.py:816
+#: ../roundup/admin.py:828
+#: ../roundup/admin.py:882
+#, python-format
+msgid "%(classname)s has no property \"%(propname)s\""
+msgstr "%(classname)s 没æ \"%(propname)s\" å±æ§"
+
+#: ../roundup/admin.py:670
+msgid ""
+"Usage: specification classname\n"
+" Show the properties for a classname.\n"
+"\n"
+" This lists the properties for a given class.\n"
+" "
+msgstr ""
+"ç¨æ³: specification classname\n"
+" æ¾ç¤ºä¸ä¸ªç±»ååçå±æ§ã\n"
+"\n"
+" ä¼ååºç»å®ç±»åçå±æ§ã\n"
+" "
+
+#: ../roundup/admin.py:685
+#, python-format
+msgid "%(key)s: %(value)s (key property)"
+msgstr "%(key)s: %(value)s (å
³é®å±æ§)"
+
+#: ../roundup/admin.py:687
+#, python-format
+msgid "%(key)s: %(value)s"
+msgstr ""
+
+#: ../roundup/admin.py:690
+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 ""
+"ç¨æ³ï¼display designator[,designator]*\n"
+" æ¾ç¤ºç»åºç»ç¹çå±æ§å¼ã\n"
+"\n"
+" å°æ¾ç¤ºç»åºç»ç¹çå±æ§åç¸åºçå¼ã\n"
+" "
+
+#: ../roundup/admin.py:714
+#, python-format
+msgid "%(key)s: %(value)r"
+msgstr ""
+
+#: ../roundup/admin.py:717
+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 ""
+"ç¨æ³ï¼create classname property=value ...\n"
+" å建ä¸ä¸ªç»å®ç±»çæ°è®°å½ã\n"
+"\n"
+" å建ä¸ä¸ªç»å®ç±»çæ°è®°å½ï¼å°ä½¿ç¨ \"create\" å½ä»¤è¡åé¢çå±æ§ name=value åæ°ã\n"
+" "
+
+#: ../roundup/admin.py:744
+#, python-format
+msgid "%(propname)s (Password): "
+msgstr "%(propname)s (å£ä»¤)ï¼"
+
+#: ../roundup/admin.py:746
+#, python-format
+msgid " %(propname)s (Again): "
+msgstr " %(propname)s (å次)ï¼"
+
+#: ../roundup/admin.py:748
+msgid "Sorry, try again..."
+msgstr "æ±æï¼åè¯ä¸æ¬¡..."
+
+#: ../roundup/admin.py:752
+#, python-format
+msgid "%(propname)s (%(proptype)s): "
+msgstr ""
+
+#: ../roundup/admin.py:770
+#, python-format
+msgid "you must provide the \"%(propname)s\" property."
+msgstr "ä½ å¿
é¡»æä¾ \"%(propname)s\" å±æ§ã"
+
+#: ../roundup/admin.py:781
+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 ""
+"ç¨æ³ï¼list classname [property]\n"
+" ååºç±»åçå®ä¾ã\n"
+"\n"
+" ååºææç»å®ç±»åçå®ä¾ãå¦æå±æ§æªè¢«æå®ï¼åä½¿ç¨ \"label\" å±æ§ã\n"
+" label å±æ§ä»¥ä¸å顺åºè¿è¡å°è¯ï¼é®ã\"name\"ã\"title\" åæåå
¸é¡ºåº\n"
+" ç第ä¸ä¸ªå±æ§ã\n"
+"\n"
+" å¦æ没ææå®å±æ§ï¼ä½¿ç¨ -c, -S æ -s ä¼æå°åºæ¡ç® id çå表ãå¦ææ\n"
+" å®äºå±æ§ï¼å¯¹æ¯ä¸ªç±»åå®ä¾ä¼æå°åºè¿ä¸ªå±æ§ã\n"
+" "
+
+#: ../roundup/admin.py:794
+msgid "Too many arguments supplied"
+msgstr "æä¾äºå¤ªå¤çåæ°äº"
+
+#: ../roundup/admin.py:830
+#, python-format
+msgid "%(nodeid)4s: %(value)s"
+msgstr ""
+
+#: ../roundup/admin.py:834
+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 ""
+"ç¨æ³ï¼table classname [property[,property]*]\n"
+" ä»¥è¡¨æ ¼ç表å¼ååºç±»åçå®ä¾ã\n"
+"\n"
+" ååºç»å®ç±»åçææå®ä¾ãå¦æ没ææå®å±æ§ï¼ææå±æ§é½ä¼æ¾ç¤ºåºæ¥ã\n"
+" 缺çæ
åµä¸ï¼åç宽度æ¯æ大å¼ç宽度ãè¿ä¸ªå®½åº¦éè¿å®ä¹å±æ§ä¸º \"name:width\"\n"
+" 被æ¾ç¤ºå°å®ä¹ãä¾å¦ï¼\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"
+" ä¹å¯ä»¥è®©åç宽度为æ ç¾ç宽度ï¼å¨å±æ§ä¸æ²¡æ宽度å¼ãä¾å¦ï¼\n"
+"\n"
+" roundup> table priority id,name:\n"
+" Id Name\n"
+" 1 fata\n"
+" 2 bug\n"
+" 3 usab\n"
+" 4 feat\n"
+"\n"
+" å°çæ4个å符宽ç \"Name\" åã\n"
+" "
+
+#: ../roundup/admin.py:878
+#, python-format
+msgid "\"%(spec)s\" not name:width"
+msgstr "\"%(spec)s\" ä¸æ¯ åå:宽度"
+
+#: ../roundup/admin.py:928
+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 ""
+"ç¨æ³ï¼history designator\n"
+" æ¾ç¤ºæ示å¨(designator)çåå²è®°å½ã\n"
+"\n"
+" æ¾ç¤ºç±æ示å¨(designator)ææçç»ç¹çæ¥å¿è®°å½ã\n"
+" "
+
+#: ../roundup/admin.py:949
+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 ""
+"ç¨æ³ï¼commit\n"
+" æ交å¨ä¸ä¸ªäº¤äºä¼è¯ä¸æ产ççæ¹å¨ã\n"
+"\n"
+" å¨ä¸ä¸ªäº¤äºä¼è¯ä¸æ产ççæ¹å¨ä¸ä¼èªå¨åå
¥æ°æ®åº - å®ä»¬å¿
须使ç¨æ¤å½ä»¤\n"
+" æ¥æ交ã\n"
+" å¨å½ä»¤è¡ä¸ç One-off å½ä»¤å¦ææåä¼è¢«èªå¨æ交ã\n"
+" "
+
+#: ../roundup/admin.py:963
+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 ""
+"ç¨æ³ï¼rollback\n"
+" æ¤éæææªæ交å°æ°æ®åºçæ¹å¨ã\n"
+"\n"
+" å¨äº¤äºå¯¹è¯ä¸äº§ççæ¹å¨å¹¶ä¸èªå¨åå°æ°æ®åºä¸ - å®ä»¬å¿
须被æå·¥æ交ã\n"
+" è¿ä¸ªå½ä»¤ç¨æ¥æ¤éææè¿äºæ¹å¨ï¼æ以å¨åé¢è·ä¸æ交çè¯ä¸ä¼å¯¹æ°æ®åº\n"
+" 产çååã\n"
+" "
+
+#: ../roundup/admin.py:975
+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 ""
+"ç¨æ³ï¼retire designator[,designator]*\n"
+" åæ¶ç±æ示å¨(designator)æææçç»ç¹ã\n"
+"\n"
+" è¿ä¸ªå¨ä½ææä¸ä¸ªç¹å«çç»ç¹å°ä¸è½è¢« list æ find å½ä»¤å¾å°ï¼å¹¶ä¸\n"
+" å®çé®å¼å¯ä»¥è¢«éç¨ã\n"
+" "
+
+#: ../roundup/admin.py:998
+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 ""
+"Usage: restore designator[,designator]*\n"
+" æ¢å¤ç±æ示å¨(designator)表æçå·²ç»åæ¶çç»ç¹ã\n"
+"\n"
+" ç»å®çç»ç¹å°å¯¹ç¨æ·æ¥è¯´å次çæã\n"
+" "
+
+#. grab the directory to export to
+#: ../roundup/admin.py:1020
+msgid ""
+"Usage: export [class[,class]] export_dir\n"
+" Export the database to colon-separated-value files.\n"
+"\n"
+" Optionally limit the export to just the names classes.\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 ""
+"ç¨æ³ï¼export [class[,class]] export_dir\n"
+" 导åºæ°æ®åºä¸ºåå·åéå¼çæ件ã\n"
+"\n"
+" 对äºå¯¼åºçå¯ééå¶åªæ¯ç±»åã\n"
+"\n"
+" è¿ä¸ªå¨ä½ä»æ°æ®åºä¸å¯¼åºå½åçæ°æ®å°ä»¥åå·åéå¼çæ件ä¸å»ï¼å®ä»¬å°å\n"
+" æ¾å¨æå®çç®æ ç®å½ä¸ã\n"
+" "
+
+#: ../roundup/admin.py:1073
+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 ""
+"ç¨æ³ï¼import import_dir\n"
+" ä»å
å« CSV æ件çç®å½ä¸å¯¼å
¥æ°æ®åºï¼ä¸ä¸ªç±»æ两个æ件ç¨äºå¯¼å
¥ã\n"
+"\n"
+" ç¨äºå¯¼å
¥çæ件为ï¼\n"
+"\n"
+" <class>.csv\n"
+" å®å¿
é¡»å®ä¹ä¸ç±»åä¸æ ·çå±æ§(å
æ¬ä¸ä¸ª \"header\" è¡å
å«é£äº\n"
+" å±æ§çååã)\n"
+" <class>-journals.csv\n"
+" å®ç¨æ¥å®ä¹è¢«å¯¼å
¥çæ¡ç®çæ¥å¿ã\n"
+"\n"
+" 被导å
¥çç»ç¹å°å
·ä¸å¨å¯¼å
¥æ件ä¸ä¸æ ·çç»ç¹idï¼ä»¥ä¾¿å¯ä»¥æ¿æ¢ä»»ä½\n"
+" ä»»ä½å·²ç»åå¨çå
容ã\n"
+" æ°ç»ç¹è¢«å å
¥å°å·²ç»åå¨çæ°æ®åºä¸ - å¦æä½ æ³è¦ä½¿ç¨å¯¼å
¥æ°æ®æ¥å\n"
+" 建ä¸ä¸ªæ°çæ°æ®åºï¼é£ä¹å建ä¸ä¸ªæ°æ°æ®åº(æè
ï¼éº»ç¦ç¹ï¼åæ¶ææ\n"
+" æ§æ°æ®ã)\n"
+" "
+
+#: ../roundup/admin.py:1141
+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 ""
+"ç¨æ³ï¼pack period | date\n"
+"\n"
+" å é¤æ©äºæå®çæ¶æææ¥æçæ§çæµæ°´è®°å½ã\n"
+"\n"
+" ä¸ä¸ªæ¶æ使ç¨åç¼ \"y\", \"m\", å \"d\"ãåç¼ \"w\"(表示 \"week\")\n"
+" 表示 7 天ã\n"
+"\n"
+" \"3y\" 表示3年\n"
+" \"2y 1m\" 表示2å¹´1个æ\n"
+" \"1m 25d\" 表示1æ25天\n"
+" \"2w 3d\" 表示2å¨3天\n"
+"\n"
+" æ¥ææ ¼å¼æ¯ \"YYYY-MM-DD\" ä¾å¦ï¼\n"
+" 2001-01-01\n"
+"\n"
+" "
+
+#: ../roundup/admin.py:1169
+msgid "Invalid format"
+msgstr "æ æçæ ¼å¼"
+
+#: ../roundup/admin.py:1179
+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 ""
+"ç¨æ³ï¼reindex [classname|designator]*\n"
+" éæ°çæ tracker çæ索索å¼ã\n"
+"\n"
+" éæ°çæ tracker çæ索索å¼ï¼å®å°èªå¨è¿è¡ã\n"
+" "
+
+#: ../roundup/admin.py:1193
+#, python-format
+msgid "no such item \"%(designator)s\""
+msgstr "没æè¿æ ·çæ¡ç® \"%(designator)s\""
+
+#: ../roundup/admin.py:1203
+msgid ""
+"Usage: security [Role name]\n"
+" Display the Permissions available to one or all Roles.\n"
+" "
+msgstr ""
+"ç¨æ³ï¼security [è§è²å]\n"
+" æ¾ç¤ºä¸ä¸ªæå¤ä¸ªè§è²çæéã\n"
+" "
+
+#: ../roundup/admin.py:1211
+#, python-format
+msgid "No such Role \"%(role)s\""
+msgstr "没æè¿æ ·çè§è² \"%(role)s\""
+
+#: ../roundup/admin.py:1217
+#, python-format
+msgid "New Web users get the Roles \"%(role)s\""
+msgstr "æ°Webç¨æ·å¾å°è§è² \"%(role)s\""
+
+#: ../roundup/admin.py:1219
+#, python-format
+msgid "New Web users get the Role \"%(role)s\""
+msgstr "æ°Webç¨æ·å¾å°è§è² \"%(role)s\""
+
+#: ../roundup/admin.py:1222
+#, python-format
+msgid "New Email users get the Roles \"%(role)s\""
+msgstr "æ°é®ä»¶ç¨æ·å¾å°è§è² \"%(role)s\""
+
+#: ../roundup/admin.py:1224
+#, python-format
+msgid "New Email users get the Role \"%(role)s\""
+msgstr "æ°é®ä»¶ç¨æ·å¾å°è§è² \"%(role)s\""
+
+#: ../roundup/admin.py:1227
+#, python-format
+msgid "Role \"%(name)s\":"
+msgstr "è§è² \"%(name)s\"ï¼"
+
+#: ../roundup/admin.py:1230
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
+msgstr " %(description)s (%(name)s ä»
ç¨äº \"%(klass)s\")"
+
+#: ../roundup/admin.py:1233
+#, python-format
+msgid " %(description)s (%(name)s)"
+msgstr ""
+
+#: ../roundup/admin.py:1259
+#, python-format
+msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
+msgstr "æªç¥å½ä»¤ \"%(command)s\" (\"help commands\" æ¥çå½ä»¤å表)"
+
+#: ../roundup/admin.py:1265
+#, python-format
+msgid "Multiple commands match \"%(command)s\": %(list)s"
+msgstr "å¤å½ä»¤å¹é
\"%(command)s\": %(list)s"
+
+#: ../roundup/admin.py:1272
+msgid "Enter tracker home: "
+msgstr "è¾å
¥trackerèµ·å§ç®å½ï¼"
+
+# ../roundup/admin.py:1279 :1285 :1305
+#: ../roundup/admin.py:1279
+#: ../roundup/admin.py:1285
+#: ../roundup/admin.py:1305
+#, python-format
+msgid "Error: %(message)s"
+msgstr "é误ï¼%(message)s"
+
+#: ../roundup/admin.py:1293
+#, python-format
+msgid "Error: Couldn't open tracker: %(message)s"
+msgstr "é误ï¼ä¸è½æå¼trackerï¼%(message)s"
+
+#: ../roundup/admin.py:1318
+#, python-format
+msgid ""
+"Roundup %s ready for input.\n"
+"Type \"help\" for help."
+msgstr ""
+"Roundup %s è¾å
¥å°±ç»ªã\n"
+"æ²å
¥ \"help\" è·å¾å¸®å©ã"
+
+#: ../roundup/admin.py:1323
+msgid "Note: command history and editing not available"
+msgstr "注æï¼å½ä»¤åå²åç¼è¾æ æ"
+
+#: ../roundup/admin.py:1327
+msgid "roundup> "
+msgstr ""
+
+#: ../roundup/admin.py:1329
+msgid "exit..."
+msgstr "éåº..."
+
+#: ../roundup/admin.py:1339
+msgid "There are unsaved changes. Commit them (y/N)? "
+msgstr "åå¨æªè¢«ä¿åçæ¹å¨ãæ交å(y/N)ï¼"
+
+#: ../roundup/backends/rdbms_common.py:1420
+msgid "create"
+msgstr "å建"
+
+#: ../roundup/backends/rdbms_common.py:1583
+msgid "unlink"
+msgstr "解é¤"
+
+#: ../roundup/backends/rdbms_common.py:1587
+msgid "link"
+msgstr "é¾æ¥"
+
+#: ../roundup/backends/rdbms_common.py:1696
+msgid "set"
+msgstr "设置"
+
+#: ../roundup/backends/rdbms_common.py:1720
+msgid "retired"
+msgstr "æ¶å"
+
+#: ../roundup/backends/rdbms_common.py:1750
+msgid "restored"
+msgstr "æ¢å¤"
+
+#: ../roundup/cgi/actions.py:53
+#, python-format
+msgid "You do not have permission to %(action)s the %(classname)s class."
+msgstr "ä½ æ²¡ææéæ¥ %(action)s %(classname)s ç±»åã"
+
+#: ../roundup/cgi/actions.py:81
+msgid "No type specified"
+msgstr "没ææå®ç±»å"
+
+#: ../roundup/cgi/actions.py:83
+msgid "No ID entered"
+msgstr "没æè¾å
¥ID"
+
+#: ../roundup/cgi/actions.py:89
+#, python-format
+msgid "\"%(input)s\" is not an ID (%(classname)s ID required)"
+msgstr "\"%(input)s\" ä¸æ¯ä¸ä¸ª ID (è¦æ± %(classname)s ID)"
+
+#: ../roundup/cgi/actions.py:109
+msgid "You may not retire the admin or anonymous user"
+msgstr "ä½ ä¸è½å é¤ç®¡çåæå¿åç¨æ·"
+
+#: ../roundup/cgi/actions.py:116
+#, python-format
+msgid "%(classname)s %(itemid)s has been retired"
+msgstr "%(classname)s %(itemid)s å·²ç»è¢«åæ¶äº"
+
+#: ../roundup/cgi/actions.py:271
+#, python-format
+msgid "Not enough values on line %(line)s"
+msgstr "å¨ %(line)s è¡æ²¡æ足å¤çå¼"
+
+#: ../roundup/cgi/actions.py:318
+msgid "Items edited OK"
+msgstr "项ç®ç¼è¾æå"
+
+#: ../roundup/cgi/actions.py:377
+#, python-format
+msgid "%(class)s %(id)s %(properties)s edited ok"
+msgstr "%(class)s %(id)s %(properties)s ç¼è¾æå"
+
+#: ../roundup/cgi/actions.py:380
+#, python-format
+msgid "%(class)s %(id)s - nothing changed"
+msgstr "%(class)s %(id)s - 没ææ¹å¨"
+
+#: ../roundup/cgi/actions.py:392
+#, python-format
+msgid "%(class)s %(id)s created"
+msgstr "%(class)s %(id)s 被å建"
+
+#: ../roundup/cgi/actions.py:424
+#, python-format
+msgid "You do not have permission to edit %(class)s"
+msgstr "ä½ æ²¡ææéæ¥ç¼è¾ %(class)s"
+
+#: ../roundup/cgi/actions.py:436
+#, python-format
+msgid "You do not have permission to create %(class)s"
+msgstr "ä½ æ²¡ææéæ¥å建 %(class)s"
+
+#: ../roundup/cgi/actions.py:459
+msgid "You do not have permission to edit user roles"
+msgstr "ä½ æ²¡æç¼è¾ç¨æ·æè§è²çæé"
+
+#: ../roundup/cgi/actions.py:518
+#, python-format
+msgid "Edit Error: %s"
+msgstr "ç¼è¾é误ï¼%s"
+
+# ../roundup/cgi/actions.py:549 :559 :730 :749
+#: ../roundup/cgi/actions.py:549
+#: ../roundup/cgi/actions.py:559
+#: ../roundup/cgi/actions.py:730
+#: ../roundup/cgi/actions.py:749
+#, python-format
+msgid "Error: %s"
+msgstr "é误ï¼%s"
+
+#: ../roundup/cgi/actions.py:585
+msgid ""
+"Invalid One Time Key!\n"
+"(a Mozilla bug may cause this message to show up erroneously, please check your email)"
+msgstr ""
+"Invalid One Time Key!\n"
+"(ä¸ä¸ª Mozilla çé误å¯è½ä¼é误å°å¼åè¿ä¸ªæ¶æ¯ï¼ä½ æ£æ¥ä½ çé®ä»¶)"
+
+#: ../roundup/cgi/actions.py:627
+#, python-format
+msgid "Password reset and email sent to %s"
+msgstr "å£ä»¤è¢«é设ï¼é®ä»¶è¢«åç» %s"
+
+#: ../roundup/cgi/actions.py:636
+msgid "Unknown username"
+msgstr "æªç¥ç¨æ·å"
+
+#: ../roundup/cgi/actions.py:644
+msgid "Unknown email address"
+msgstr "æªç¥é®ä»¶å°å"
+
+#: ../roundup/cgi/actions.py:649
+msgid "You need to specify a username or address"
+msgstr "ä½ éè¦æå®ç¨æ·åæå°å"
+
+#: ../roundup/cgi/actions.py:674
+#, python-format
+msgid "Email sent to %s"
+msgstr "é®ä»¶åç» %s"
+
+#: ../roundup/cgi/actions.py:693
+msgid "You are now registered, welcome!"
+msgstr "ä½ å·²ç»æ³¨åï¼æ¬¢è¿ï¼"
+
+#: ../roundup/cgi/actions.py:738
+msgid "It is not permitted to supply roles at registration."
+msgstr "ä¸å
许å¨æ³¨åæ¶æä¾è§è²ã"
+
+#: ../roundup/cgi/actions.py:820
+msgid "You are logged out"
+msgstr "ä½ å·²ç»æ³¨é"
+
+#: ../roundup/cgi/actions.py:831
+msgid "Username required"
+msgstr "éè¦ç¨æ·å"
+
+#: ../roundup/cgi/actions.py:846
+msgid "Ivalid login"
+msgstr "æ æç»å½"
+
+#: ../roundup/cgi/actions.py:853
+msgid "Invalid login"
+msgstr "æ æç»å½"
+
+#: ../roundup/cgi/actions.py:861
+msgid "You do not have permission to login"
+msgstr "ä½ æ²¡æç»å½çæé"
+
+#: ../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>模æ¿é误</h1>\n"
+"<p><b>%(exc_type)s</b>: %(exc_value)s</p>\n"
+"<p class=\"help\">è°è¯ä¿¡æ¯ä¸º</p>"
+
+#: ../roundup/cgi/cgitb.py:64
+#, python-format
+msgid "<li>\"%(name)s\" (%(info)s)</li>"
+msgstr ""
+
+#: ../roundup/cgi/cgitb.py:67
+#, python-format
+msgid "<li>Looking for \"%(name)s\", current path:<ol>%(path)s</ol></li>"
+msgstr "<li>æ¥æ¾ \"%(name)s\", å½åè·¯å¾ï¼<ol>%(path)s</ol></li>"
+
+#: ../roundup/cgi/cgitb.py:71
+#, python-format
+msgid "<li>In %s</li>"
+msgstr "<li>å¨ %s</li>"
+
+#: ../roundup/cgi/cgitb.py:76
+#, python-format
+msgid "A problem occurred in your template \"%s\"."
+msgstr "å¨ä½ çæ¨¡æ¿ \"%s\" ä¸åçä¸ä¸ªé®é¢ã"
+
+#: ../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>å¨ %(line)d è¡è®¡ç® %(info)r 表达å¼\n"
+"<table class=\"otherinfo\" style=\"font-size: 90%%\">\n"
+" <tr><th colspan=\"2\" class=\"header\">å½ååéï¼</th></tr>\n"
+" %(globals)s\n"
+" %(locals)s\n"
+"</table></li>\n"
+
+#: ../roundup/cgi/cgitb.py:103
+msgid "Full traceback:"
+msgstr "å®æ´è·è¸ªä¿¡æ¯ï¼"
+
+#: ../roundup/cgi/cgitb.py:116
+#, python-format
+msgid "<font size=+1><strong>%(exc_type)s</strong>: %(exc_value)s</font>"
+msgstr ""
+
+#: ../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>å¨è¿è¡ Python èæ¬æ¶åçäºä¸ä¸ªé误ãè¿æ¯å¯¼è´åºéçä¸ç³»åçå½æ°è°ç¨ï¼æè¿ç(æéå±ç)è°ç¨å¨åãå¼å¸¸å±æ§æ¯ï¼"
+
+#: ../roundup/cgi/cgitb.py:129
+msgid "<file is None - probably inside <tt>eval</tt> or <tt>exec</tt>>"
+msgstr "<æ件为 None - å¯è½å¨ <tt>eval</tt> æè
<tt>exec</tt>>"
+
+#: ../roundup/cgi/cgitb.py:138
+#, python-format
+msgid "in <strong>%s</strong>"
+msgstr "å¨ <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>æªå®ä¹</em>"
+
+#: ../roundup/cgi/client.py:273
+msgid "Form Error: "
+msgstr "è¡¨æ ¼é误ï¼"
+
+#: ../roundup/cgi/client.py:323
+#, python-format
+msgid "Unrecognized charset: %r"
+msgstr "æ æ³è¯å«çå符éï¼%r"
+
+#: ../roundup/cgi/client.py:398
+msgid "Anonymous users are not allowed to use the web interface"
+msgstr "å¿åç¨æ·ä¸å
许使ç¨webçé¢"
+
+#: ../roundup/cgi/form_parser.py:283
+#, python-format
+msgid "link \"%(key)s\" value \"%(value)s\" not a designator"
+msgstr "é¾æ¥ \"%(key)s\" çå¼ \"%(value)s\" ä¸æ¯ä¸ä¸ª æ示å¨(designator)"
+
+#: ../roundup/cgi/form_parser.py:290
+#, python-format
+msgid "%(class)s %(property)s is not a link or multilink property"
+msgstr "%(class)s %(property)s ä¸æ¯ä¸ä¸ª Link æ MultiLink å±æ§"
+
+#: ../roundup/cgi/form_parser.py:312
+#, python-format
+msgid "You have submitted a %(action)s action for the property \"%(property)s\" which doesn't exist"
+msgstr "ä½ æ交äºä¸ä¸ªå¯¹äºä¸åå¨å±æ§ \"%(property)s\" çä¸ä¸ªæä½ %(action)s"
+
+# ../roundup/cgi/form_parser.py:331 :357
+#: ../roundup/cgi/form_parser.py:331
+#: ../roundup/cgi/form_parser.py:357
+#, python-format
+msgid "You have submitted more than one value for the %s property"
+msgstr "ä½ éè¦æ交é对 %s å±æ§çä¸ä¸ªä»¥ä¸çå¼"
+
+# ../roundup/cgi/form_parser.py:354 :360
+#: ../roundup/cgi/form_parser.py:354
+#: ../roundup/cgi/form_parser.py:360
+msgid "Password and confirmation text do not match"
+msgstr "å£ä»¤å确认ææ¬ä¸å¹é
"
+
+#: ../roundup/cgi/form_parser.py:395
+#, python-format
+msgid "property \"%(propname)s\": \"%(value)s\" not currently in list"
+msgstr "å±æ§ \"%(propname)s\": \"%(value)s\" å½åä¸å¨å表ä¸"
+
+#: ../roundup/cgi/form_parser.py:509
+#, python-format
+msgid "Required %(class)s property %(property)s not supplied"
+msgstr "è¦æ±ç %(class)s å±æ§ %(property)s 没æ被æä¾"
+
+#: ../roundup/cgi/form_parser.py:529
+msgid "File is empty"
+msgstr "æ件为空"
+
+#: ../roundup/cgi/templating.py:68
+#, python-format
+msgid "You are not allowed to %(action)s items of class %(class)s"
+msgstr "ä½ ä¸å
许 %(action)s ç±»å« %(class)s ç项ç®"
+
+#: ../roundup/cgi/templating.py:598
+msgid "(list)"
+msgstr "(å表)"
+
+#: ../roundup/cgi/templating.py:632
+msgid "Submit New Entry"
+msgstr "æ交æ°ç项"
+
+#: ../roundup/cgi/templating.py:644
+msgid "New node - no history"
+msgstr "æ°è®°å½ - æ åå²"
+
+#: ../roundup/cgi/templating.py:744
+msgid "Submit Changes"
+msgstr "æ交åå¨"
+
+#: ../roundup/cgi/templating.py:825
+msgid "<em>The indicated property no longer exists</em>"
+msgstr "<em>æ示çå±æ§ä¸ååå¨</em>"
+
+#: ../roundup/cgi/templating.py:826
+#, python-format
+msgid "<em>%s: %s</em>\n"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:839
+#, python-format
+msgid "The linked class %(classname)s no longer exists"
+msgstr "é¾æ¥çç±»å« %(classname)s ä¸ååå¨"
+
+# ../roundup/cgi/templating.py:872 :893
+#: ../roundup/cgi/templating.py:872
+#: ../roundup/cgi/templating.py:893
+msgid "<strike>The linked node no longer exists</strike>"
+msgstr "<strike>é¾æ¥çç»ç¹ä¸ååå¨</strike>"
+
+#: ../roundup/cgi/templating.py:932
+msgid "No"
+msgstr "å¦"
+
+#: ../roundup/cgi/templating.py:932
+msgid "Yes"
+msgstr "æ¯"
+
+#: ../roundup/cgi/templating.py:943
+#, python-format
+msgid "%s: (no value)"
+msgstr "%s: (æ å¼)"
+
+#: ../roundup/cgi/templating.py:955
+msgid "<strong><em>This event is not handled by the history display!</em></strong>"
+msgstr "<strong><em>è¿ä¸ªäºä»¶ä¸è½è¢«åå²æ¾ç¤ºæå¤çï¼</em></strong>"
+
+#: ../roundup/cgi/templating.py:967
+msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
+msgstr "<tr><td colspan=4><strong>注æï¼</strong></td></tr>"
+
+#: ../roundup/cgi/templating.py:976
+msgid "History"
+msgstr "åå²"
+
+#: ../roundup/cgi/templating.py:978
+msgid "<th>Date</th>"
+msgstr "<th>æ¥æ</th>"
+
+#: ../roundup/cgi/templating.py:979
+msgid "<th>User</th>"
+msgstr "<th>ç¨æ·</th>"
+
+#: ../roundup/cgi/templating.py:980
+msgid "<th>Action</th>"
+msgstr "<th>å¨ä½</th>"
+
+#: ../roundup/cgi/templating.py:981
+msgid "<th>Args</th>"
+msgstr "<th>åæ°</th>"
+
+#: ../roundup/cgi/templating.py:1221
+msgid "*encrypted*"
+msgstr "*å å¯ç*"
+
+#: ../roundup/cgi/templating.py:1386
+msgid "default value for DateHTMLProperty must be either DateHTMLProperty or string date representation."
+msgstr "DateHTMLProperty ç缺çå¼æè
æ¯ DateHTMLProperty æå符串çæ¥æ表示ã"
+
+#: ../roundup/cgi/templating.py:1571
+#, python-format
+msgid "<option %svalue=\"-1\">- no selection -</option>"
+msgstr "<option %svalue=\"-1\">- æªéæ© -</option>"
+
+#: ../roundup/date.py:180
+#, python-format
+msgid "Not a date spec: %s"
+msgstr "ä¸æ¯æ¥ææ ¼å¼ï¼%s"
+
+#: ../roundup/date.py:231
+#, python-format
+msgid "%r not a date spec (%s)"
+msgstr "%r ä¸æ¯æ¥ææ ¼å¼ (%s)"
+
+#: ../roundup/date.py:522
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
+msgstr "ä¸æ¯æ¶é´é´éè§èï¼[+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [æ¥æè§è]"
+
+#: ../roundup/date.py:541
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+msgstr "ä¸æ¯æ¶é´é´éè§èï¼[+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+
+#: ../roundup/date.py:678
+#, python-format
+msgid "%(number)s year"
+msgstr "%(number)så¹´"
+
+#: ../roundup/date.py:682
+#, python-format
+msgid "%(number)s month"
+msgstr "%(number)sæ"
+
+#: ../roundup/date.py:686
+#, python-format
+msgid "%(number)s week"
+msgstr "%(number)så¨"
+
+#: ../roundup/date.py:690
+#, python-format
+msgid "%(number)s day"
+msgstr "%(number)s天"
+
+#: ../roundup/date.py:694
+msgid "tomorrow"
+msgstr "æ天"
+
+#: ../roundup/date.py:696
+msgid "yesterday"
+msgstr "æ¨å¤©"
+
+#: ../roundup/date.py:699
+#, python-format
+msgid "%(number)s hour"
+msgstr "%(number)så°æ¶"
+
+#: ../roundup/date.py:703
+msgid "an hour"
+msgstr "1å°æ¶"
+
+#: ../roundup/date.py:705
+msgid "1 1/2 hours"
+msgstr "1个åå°æ¶"
+
+#: ../roundup/date.py:707
+#, python-format
+msgid "1 %(number)s/4 hours"
+msgstr "1 %(number)s/4 å°æ¶"
+
+#: ../roundup/date.py:711
+msgid "in a moment"
+msgstr "ä¸ä¼å¿"
+
+#: ../roundup/date.py:713
+msgid "just now"
+msgstr "åæ"
+
+#: ../roundup/date.py:716
+msgid "1 minute"
+msgstr "1åé"
+
+#: ../roundup/date.py:719
+#, python-format
+msgid "%(number)s minute"
+msgstr "%(number)såé"
+
+#: ../roundup/date.py:722
+msgid "1/2 an hour"
+msgstr "åå°æ¶"
+
+#: ../roundup/date.py:724
+#, python-format
+msgid "%(number)s/4 hour"
+msgstr "%(number)s/4 å°æ¶"
+
+#: ../roundup/date.py:728
+#, python-format
+msgid "%s ago"
+msgstr "%s ä¹å"
+
+#: ../roundup/date.py:730
+#, python-format
+msgid "in %s"
+msgstr "å¨ %s"
+
+#: ../roundup/roundupdb.py:130
+msgid "files"
+msgstr "æ件"
+
+#: ../roundup/roundupdb.py:130
+msgid "messages"
+msgstr "ä¿¡æ¯"
+
+#: ../roundup/roundupdb.py:130
+msgid "nosy"
+msgstr "æäº"
+
+#: ../roundup/roundupdb.py:130
+msgid "superseder"
+msgstr "延æ"
+
+#: ../roundup/roundupdb.py:130
+msgid "title"
+msgstr "æ é¢"
+
+#: ../roundup/roundupdb.py:131
+msgid "assignedto"
+msgstr "åé
ç»"
+
+#: ../roundup/roundupdb.py:131
+msgid "priority"
+msgstr "ä¼å
级"
+
+#: ../roundup/roundupdb.py:131
+msgid "status"
+msgstr "ç¶æ"
+
+#: ../roundup/roundupdb.py:131
+msgid "topic"
+msgstr "主é¢"
+
+#: ../roundup/roundupdb.py:134
+msgid "activity"
+msgstr "æ´»è·åº¦"
+
+#. following properties are common for all hyperdb classes
+#. they are listed here to keep things in one place
+#: ../roundup/roundupdb.py:134
+msgid "actor"
+msgstr "æ§è¡äºº"
+
+#: ../roundup/roundupdb.py:134
+msgid "creation"
+msgstr "å建"
+
+#: ../roundup/roundupdb.py:134
+msgid "creator"
+msgstr "å建è
"
+
+#: ../roundup/scripts/roundup_demo.py:32
+#, python-format
+msgid "Enter directory path to create demo tracker [%s]: "
+msgstr "è¾å
¥ç®å½æ¥å建æ¼ç¤ºtracker [%s]ï¼"
+
+#: ../roundup/scripts/roundup_mailgw.py:36
+#, python-format
+msgid ""
+"Usage: %(program)s [-v] [-c] [[-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"
+"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 ""
+"ç¨æ³ï¼%(program)s [-v] [-c] [[-C class] -S field=value]* <instance home> [method]\n"
+"\n"
+"é项ï¼\n"
+" -v: æå°çæ¬å¹¶ä¸éåº\n"
+" -c: ç¨æ¥å建æ¡ç®ç缺çç±»å(å
¶å®æ¯trackerçMAIL_DEFAULT_CLASS)\n"
+" -C / -S: çä¸é¢\n"
+"\n"
+"Roundup é®ä»¶ç½å
³ä¼ä»¥åç§æ¹å¼è¢«è°ç¨ï¼\n"
+" . å®ä¾èµ·å§ç®å½ä½ä¸ºå¯ä¸åæ°ï¼\n"
+" . å®ä¾èµ·å§ç®å½åé®ä»¶è±æº(spool)æ件ï¼\n"
+" . å®ä¾èµ·å§ç®å½å POP/APOP æå¡å¨å¸å·ï¼æè
\n"
+" . å®ä¾èµ·å§ç®å½å IMAP/IMAPS æå¡å¨å¸å·ã\n"
+"\n"
+"ä¹æ¯æ使ç¨å¯éç -C æ -S åæ°ï¼å®ä»¬å
è®¸ä½ ä¸ºroundup-mailgwæå建çç±»\n"
+"设置åãå¦æ没ææå®ï¼å缺ççç±»æ¯ msgï¼ä½æ¯å
¶å®çç±»ï¼issue, file, user\n"
+"ä¹å¯ä»¥ä½¿ç¨ã-S æ --set éé¡¹ä½¿ç¨ property=value[;property=value] 表示æ³ï¼\n"
+"å®ä»¬å¯ä»¥è¢« Roundup å½ä»¤çå½ä»¤è¡æå¯ä»¥æå®ä¸å°é®ä»¶ä¿¡æ¯æ é¢è¡çå½ä»¤ææ¥åã\n"
+"\n"
+"å®å¯ä»¥è®©ä½ ç»æ¯å°é®ä»¶è®¾ç½®ä¿¡æ¯çç±»åã\n"
+"\n"
+"PIPE:\n"
+" å¨ç¬¬ä¸ç§æ¹å¼ä¸ï¼é®ä»¶ç½å
³ä»æ åè¾å
¥è¯»ååæ¡ä¿¡æ¯ï¼å¹¶å°ä¿¡æ¯æäº¤ç» roundup.mailgw\n"
+" 模åã\n"
+"\n"
+"UNIX mailbox:\n"
+" å¨ç¬¬äºç§æ¹å¼ä¸ï¼ç½å
³ä»é®ä»¶è±æºæ件ä¸è¯»åææçä¿¡æ¯ï¼å¹¶æ顺åºæ交ç»\n"
+" roundup.mailgw 模åãä¸æ¦ææä¿¡æ¯è¢«æåå¤çï¼æ件被æ¸
空ãè¿ä¸ªæ件被\n"
+" æå®ä¸ºï¼\n"
+" mailbox /path/to/mailbox\n"
+"\n"
+"POP:\n"
+" å¨ç¬¬ä¸ç§æ¹å¼ä¸ï¼ç½å
³ä»æå®ç POP æå¡å¨è¯»åºææä¿¡æ¯ï¼å¹¶æ顺åºæ交å°\n"
+" roundup.mailgw 模åãæå¡å¨è¢«æå®ä¸ºï¼\n"
+" pop username:password at server\n"
+" ç¨æ·ååå£ä»¤å¯ä»¥è¢«çç¥ï¼\n"
+" pop username at server\n"
+" pop server\n"
+" é½æ¯ææçãå¦æ没ææä¾ç¨æ·åæå£ä»¤é½å°å¨å½ä»¤è¡è¢«æ示ã\n"
+"\n"
+"APOP:\n"
+" å POPï¼ä½ä½¿ç¨è®¤è¯ç POPï¼\n"
+" apop username:password at server\n"
+"\n"
+"IMAP:\n"
+" èæ¥å° IMAP æå¡å¨ãå®æ¯æå POP é®ä»¶ç¸åçåæ³ã\n"
+" imap username:password at server\n"
+" é¤äº INBOX å¤è¿å
è®¸ä½ æå®ä¸ä¸ªç¹å«çé®ç®±ï¼\n"
+" 使ç¨è¿ç§æ ¼å¼ï¼ imap username:password at server mailbox\n"
+"\n"
+"IMAPS:\n"
+" éè¿sslèæ¥å° IMAP æå¡å¨ã\n"
+" å®æ¯æå IMAP ä¸æ ·çåæ³ã\n"
+" imaps username:password at server [mailbox]\n"
+"\n"
+
+#: ../roundup/scripts/roundup_mailgw.py:147
+msgid "Error: not enough source specification information"
+msgstr "é误ï¼æ²¡æ足å¤çæºå议信æ¯"
+
+#: ../roundup/scripts/roundup_mailgw.py:157
+msgid "Error: pop specification not valid"
+msgstr "é误ï¼popåè®®æ æ"
+
+#: ../roundup/scripts/roundup_mailgw.py:164
+msgid "Error: apop specification not valid"
+msgstr "é误ï¼apopåè®®æ æ"
+
+#: ../roundup/scripts/roundup_mailgw.py:178
+msgid "Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or \"imaps\""
+msgstr "é误ï¼æºå¿
é¡»æ¯ \"mailbox\", \"pop\", \"apop\", \"imap\" æè
\"imaps\" ä¹ä¸"
+
+#: ../roundup/scripts/roundup_server.py:106
+msgid ""
+"<html><head><title>Roundup trackers index</title></head>\n"
+"<body><h1>Roundup trackers index</h1><ol>\n"
+msgstr ""
+"<html><head><title>Roundup tracker ç´¢å¼</title></head>\n"
+"<body><h1>Roundup tracker ç´¢å¼</h1><ol>\n"
+
+#: ../roundup/scripts/roundup_server.py:217
+#, python-format
+msgid "Error: %s: %s"
+msgstr "é误ï¼%s: %s"
+
+#: ../roundup/scripts/roundup_server.py:325
+msgid ""
+" -c <Command> Windows Service options.\n"
+" If you want to run the server as a Windows Service, you\n"
+" must configure the rest of the options by changing the\n"
+" constants of this program. You will at least configure\n"
+" one tracker in the TRACKER_HOMES variable. This option\n"
+" is mutually exclusive from the rest. Typing\n"
+" \"roundup-server -c help\" shows Windows Services\n"
+" specifics."
+msgstr ""
+" -c <Command> Windows æå¡é项ã\n"
+" å¦æä½ æ³æserverä½ä¸ºä¸ä¸ªWindowsæå¡æ¥è¿è¡ï¼ä½ å¿
é¡»éè¿ä¿®æ¹\n"
+" è¿ä¸ªç¨åºç常éæ¥é
ç½®æ¤é项çå
¶å®å
容ãä½ è³å°éè¦å¨ TRACKER_HOMES\n"
+" åéä¸é
ç½®ä¸ä¸ªtrackerãè¿ä¸ªé项ä¸å
¶ç»é项æ¯äºæ¥çãæå
¥\n"
+" \"roundup-server -c help\" æ¥äºè§£Windowsæå¡çè§èã"
+
+#: ../roundup/scripts/roundup_server.py:334
+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 ""
+" -u <UID> 以è¿ä¸ª UID æ¥è¿è¡ Roundup web æå¡å¨\n"
+" -g <GID> 以è¿ä¸ª GID æ¥è¿è¡ Roundup web æå¡å¨\n"
+" -d <PIDfile> å¨åå°è¿è¡æå¡å¨ï¼å¹¶ä¸å°æå¡å¨ç PID åå
¥æå®ç PIDFile ä¸å»ã\n"
+" å¦æ使ç¨äº -d é项ï¼å -l é项 *å¿
é¡»* è¦æå®ã"
+
+#: ../roundup/scripts/roundup_server.py:342
+#, python-format
+msgid ""
+"%(message)sUsage: roundup-server [options] [name=tracker home]*\n"
+"\n"
+"Options:\n"
+" -v prints the Roundup version number and exits\n"
+" -C <fname> use configuration file\n"
+" -n <name> sets the host name of the Roundup web server instance\n"
+" -p <port> sets 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"
+"%(os_part)s\n"
+"\n"
+"Examples:\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"
+" See the \"admin_guide\" in the Roundup \"doc\" directory.\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 ""
+"%(message)sç¨æ³ï¼roundup-server [options] [name=tracker home]*\n"
+"\n"
+"é项ï¼\n"
+" -v æå° Roundup ççæ¬å·å¹¶ä¸éåº\n"
+" -C <fname> 使ç¨é
ç½®æ件\n"
+" -n <name> 设置 Roundup web æå¡å¨å®ä¾ç主æºå\n"
+" -p <port> 设置çå¬ç«¯å£(缺çï¼%(port)s)\n"
+" -l <fname> å°æ¥å¿è¾åºå°ç± fname æå®çæ件ä¸å»ï¼èä¸æ¯ æ åé误/æ åè¾åº\n"
+" -N å°å®¢æ·ç«¯æºå¨çååèä¸æ¯IPå°åè®°å½å°æ¥å¿ä¸å»(å¯è½ä¼æ
¢ç¹)\n"
+"%(os_part)s\n"
+"\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"
+"é
ç½®æä»¶æ ¼å¼ï¼\n"
+" æ¥é
å¨Roundup \"doc\" ç®å½ä¸ç \"admin_guide\" ã\n"
+"\n"
+"å¦ä½ä½¿ç¨ \"name=tracker home\"ï¼\n"
+" è¿äºåæ°ç¨æ¥è®¾ç½®è¦ä½¿ç¨çtrackerçèµ·å§ç®å½ãname ä¼å¨URLä¸ç¨æ¥\n"
+" å®ä½tracker(å®æ¯ URL è·¯å¾ç第ä¸é¨å)ãtracker home æ¯å¨ä½ æ§è¡\n"
+" \"roundup-admin init\" æ¶ææå®çç®å½ãä½ å¯ä»¥å¨å½ä»¤è¡ä¸æå®ä»»\n"
+" ææ°éç name=home 对ãè¦ç¡®ä¿ name é¨åä¸è½å
æ¬ä»»ä½éurlå®å
¨ç\n"
+" å符ï¼è±¡ç©ºæ ¼ï¼å 为å®ä»¬ä¼æIEæä¹±ã\n"
+
+#: ../roundup/scripts/roundup_server.py:418
+msgid "WARNING: ignoring \"-g\" argument, not root"
+msgstr "è¦åï¼å¿½ç¥ \"-g\" åæ°ï¼ä¸æ¯ root"
+
+#: ../roundup/scripts/roundup_server.py:424
+msgid "Can't change groups - no grp module"
+msgstr "ä¸è½ä¿®æ¹ç» - æ grp 模å"
+
+#: ../roundup/scripts/roundup_server.py:433
+#, python-format
+msgid "Group %(group)s doesn't exist"
+msgstr "ç» %(group)s ä¸åå¨"
+
+#: ../roundup/scripts/roundup_server.py:444
+msgid "Can't run as root!"
+msgstr "ä¸è½ä»¥ root è¿è¡ï¼"
+
+#: ../roundup/scripts/roundup_server.py:447
+msgid "WARNING: ignoring \"-u\" argument, not root"
+msgstr "è¦åï¼å¿½ç¥ \"-u\" åæ°ï¼ä¸æ¯ root"
+
+#: ../roundup/scripts/roundup_server.py:452
+msgid "Can't change users - no pwd module"
+msgstr "ä¸è½ä¿®æ¹ç¨æ· - æ pwd 模å"
+
+#: ../roundup/scripts/roundup_server.py:461
+#, python-format
+msgid "User %(user)s doesn't exist"
+msgstr "ç¨æ· %(user)s ä¸åå¨"
+
+#: ../roundup/scripts/roundup_server.py:575
+msgid "Instances must be name=home"
+msgstr "å®ä¾å¿
é¡»æ¯ å®ä¾å=å®ä¾è·¯å¾"
+
+#: ../roundup/scripts/roundup_server.py:589
+#, python-format
+msgid "Configuration saved to %s"
+msgstr "é
ç½®ä¿åå° %s"
+
+#: ../roundup/scripts/roundup_server.py:606
+#, python-format
+msgid "Unable to bind to port %s, port already in use."
+msgstr "æ æ³ç»å®å°ç«¯å£ %s, 端å£å·²ç»è¢«å ç¨ã"
+
+#: ../roundup/scripts/roundup_server.py:625
+msgid "Sorry, you can't run the server as a daemon on this Operating System"
+msgstr "æ±æï¼å¨è¿ä¸ªæä½ç³»ç»ä¸ä¸è½ä»¥å®æ¤è¿ç¨çæ¹å¼æ¥è¿è¡æå¡"
+
+#: ../roundup/scripts/roundup_server.py:639
+#, python-format
+msgid "Roundup server started on %(HOST)s:%(PORT)s"
+msgstr "Roundup server å¯å¨äº %(HOST)s:%(PORT)s"
+
+#: ../templates/classic/html/_generic.collision.html:4
+#: ../templates/minimal/html/_generic.collision.html:4
+msgid "${class} Edit Collision - ${tracker}"
+msgstr "${class} ç¼è¾å²çª - ${tracker}"
+
+#: ../templates/classic/html/_generic.collision.html:7
+#: ../templates/minimal/html/_generic.collision.html:7
+msgid "${class} Edit Collision"
+msgstr "${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"
+" åå¨å²çªãå¦ä¸ä¸ªç¨æ·å¨ä½ ç¼è¾æ¶æ´æ°äºæ¤æ¡è®°å½ã\n"
+" 请 <a href='${context}'>éæ°è½½å
¥</a> è®°å½æ¥çä½ çç¼è¾ã\n"
+
+#: ../templates/classic/html/_generic.help.html:9
+#: ../templates/minimal/html/_generic.help.html:9
+msgid "${property} help - ${tracker}"
+msgstr "${property} å¸®å© - ${tracker}"
+
+#: ../templates/classic/html/_generic.help.html:30
+#: ../templates/minimal/html/_generic.help.html:30
+msgid " Cancel "
+msgstr "åæ¶"
+
+#: ../templates/classic/html/_generic.help.html:33
+#: ../templates/minimal/html/_generic.help.html:33
+msgid " Apply "
+msgstr "åºç¨"
+
+#: ../templates/classic/html/_generic.help.html:40
+#: ../templates/classic/html/issue.index.html:67
+#: ../templates/minimal/html/_generic.help.html:40
+msgid "<< previous"
+msgstr "<< åä¸"
+
+#: ../templates/classic/html/_generic.help.html:50
+#: ../templates/classic/html/issue.index.html:75
+#: ../templates/minimal/html/_generic.help.html:50
+msgid "${start}..${end} out of ${total}"
+msgstr "${start}..${end} å
¨é¨ ${total}"
+
+#: ../templates/classic/html/_generic.help.html:54
+#: ../templates/classic/html/issue.index.html:78
+#: ../templates/minimal/html/_generic.help.html:54
+msgid "next >>"
+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
+msgid "${class} editing - ${tracker}"
+msgstr "${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 "${class} ç¼è¾"
+
+#: ../templates/classic/html/_generic.index.html:14
+#: ../templates/classic/html/_generic.item.html:12
+#: ../templates/classic/html/file.item.html:9
+#: ../templates/classic/html/issue.index.html:10
+#: ../templates/classic/html/user.index.html:9
+#: ../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 "ä½ ä¸å
许æ¥çæ¤é¡µ"
+
+#: ../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\"> ä½ å¯ä»¥ä½¿ç¨è¿ä¸ªè¡¨æ ¼æ¥ç¼è¾ ${classname} ç±»å«ã éå·ï¼æ¢è¡ååå¼å·(\")å¿
须被å°å¿å¤çãä½ å¯ä»¥å¨åå¼å·(\")ä¸å
å«éå·åæ¢è¡ãåå¼å·æ¬èº«å¿
须被两个(\"\")æå
æ¬ã</p> <p class=\"form-help\"> Multilink å±æ§æå¤ä¸ªå¼ï¼è¿äºå¼ç¨åå·(\":\")åé(...ï¼\"ä¸:äº:ä¸\"ï¼...) </p> <p class=\"form-help\"> éè¿å é¤å®ä»¬æå¨çè¡æ¥å é¤é¡¹ã追å ä¸æ¡æ°è®°å½å°è¡¨ä¸ - å¨ id åç½®ä¸ä¸ä¸ª X ã</p>"
+
+#: ../templates/classic/html/_generic.index.html:44
+#: ../templates/minimal/html/_generic.index.html:44
+msgid "Edit Items"
+msgstr "ç¼è¾é¡¹ç®"
+
+#: ../templates/classic/html/file.index.html:4
+msgid "List of files - ${tracker}"
+msgstr "æ件å表 - ${tracker}"
+
+#: ../templates/classic/html/file.index.html:5
+msgid "List of files"
+msgstr "æ件å表"
+
+#: ../templates/classic/html/file.index.html:10
+msgid "Download"
+msgstr "ä¸è½½"
+
+#: ../templates/classic/html/file.index.html:11
+#: ../templates/classic/html/file.item.html:23
+#: ../templates/classic/html/file.item.html:51
+msgid "Content Type"
+msgstr "å
容类å"
+
+#: ../templates/classic/html/file.index.html:12
+msgid "Uploaded By"
+msgstr "ä¸ä¼ ç±"
+
+#: ../templates/classic/html/file.index.html:13
+#: ../templates/classic/html/msg.item.html:38
+msgid "Date"
+msgstr "æ¥æ"
+
+#: ../templates/classic/html/file.item.html:2
+msgid "File display - ${tracker}"
+msgstr "æ件æ¾ç¤º - ${tracker}"
+
+#: ../templates/classic/html/file.item.html:4
+msgid "File display"
+msgstr "æ件æ¾ç¤º"
+
+#: ../templates/classic/html/file.item.html:19
+#: ../templates/classic/html/file.item.html:47
+#: ../templates/classic/html/user.item.html:34
+#: ../templates/classic/html/user.register.html:17
+msgid "Name"
+msgstr "å§å"
+
+#: ../templates/classic/html/file.item.html:41
+msgid "download"
+msgstr "ä¸è½½"
+
+#: ../templates/classic/html/home.classlist.html:2
+#: ../templates/minimal/html/home.classlist.html:2
+msgid "List of classes - ${tracker}"
+msgstr "ç±»å«å表 - ${tracker}"
+
+#: ../templates/classic/html/home.classlist.html:4
+#: ../templates/minimal/html/home.classlist.html:4
+msgid "List of classes"
+msgstr "ç±»å«å表"
+
+#: ../templates/classic/html/issue.index.html:4
+msgid "List of issues - ${tracker}"
+msgstr "é®é¢å表 - ${tracker}"
+
+#: ../templates/classic/html/issue.index.html:6
+msgid "List of issues"
+msgstr "é®é¢å表"
+
+#: ../templates/classic/html/issue.index.html:17
+#: ../templates/classic/html/issue.item.html:38
+msgid "Priority"
+msgstr "ä¼å
级"
+
+#: ../templates/classic/html/issue.index.html:18
+msgid "ID"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:19
+msgid "Creation"
+msgstr "å建æ¶é´"
+
+#: ../templates/classic/html/issue.index.html:20
+msgid "Activity"
+msgstr "æ´»è·åº¦"
+
+#: ../templates/classic/html/issue.index.html:21
+msgid "Actor"
+msgstr "æ§è¡è
"
+
+#: ../templates/classic/html/issue.index.html:22
+msgid "Topic"
+msgstr "主é¢"
+
+#: ../templates/classic/html/issue.index.html:23
+#: ../templates/classic/html/issue.item.html:33
+msgid "Title"
+msgstr "æ é¢"
+
+#: ../templates/classic/html/issue.index.html:24
+#: ../templates/classic/html/issue.item.html:40
+msgid "Status"
+msgstr "ç¶æ"
+
+#: ../templates/classic/html/issue.index.html:25
+msgid "Creator"
+msgstr "å建人"
+
+#: ../templates/classic/html/issue.index.html:26
+msgid "Assigned To"
+msgstr "åé
ç»"
+
+#: ../templates/classic/html/issue.index.html:90
+msgid "Download as CSV"
+msgstr "以CSVæ ¼å¼ä¸è½½"
+
+#: ../templates/classic/html/issue.index.html:98
+msgid "Sort on:"
+msgstr "æåºæï¼"
+
+#: ../templates/classic/html/issue.index.html:101
+#: ../templates/classic/html/issue.index.html:118
+msgid "- nothing -"
+msgstr "- æ -"
+
+#: ../templates/classic/html/issue.index.html:109
+#: ../templates/classic/html/issue.index.html:126
+msgid "Descending:"
+msgstr "éåºï¼"
+
+#: ../templates/classic/html/issue.index.html:115
+msgid "Group on:"
+msgstr "åç»ï¼"
+
+#: ../templates/classic/html/issue.index.html:132
+msgid "Redisplay"
+msgstr "å·æ°"
+
+#: ../templates/classic/html/issue.item.html:7
+msgid "Issue ${id}: ${title} - ${tracker}"
+msgstr "é®é¢ ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:10
+msgid "New Issue - ${tracker}"
+msgstr "æ°é®é¢ - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:14
+msgid "New Issue"
+msgstr "æ°é®é¢"
+
+#: ../templates/classic/html/issue.item.html:16
+msgid "New Issue Editing"
+msgstr "æ°é®é¢ç¼è¾"
+
+#: ../templates/classic/html/issue.item.html:19
+msgid "Issue${id}"
+msgstr "é®é¢ [${id}]"
+
+#: ../templates/classic/html/issue.item.html:22
+msgid "Issue${id} Editing"
+msgstr "é®é¢ [${id}] ç¼è¾"
+
+#: ../templates/classic/html/issue.item.html:45
+msgid "Superseder"
+msgstr "æ¨è¿"
+
+#: ../templates/classic/html/issue.item.html:50
+msgid "View: ${link}"
+msgstr "æ¥çï¼${link}"
+
+#: ../templates/classic/html/issue.item.html:54
+msgid "Nosy List"
+msgstr "æäºå表"
+
+#: ../templates/classic/html/issue.item.html:63
+msgid "Assigned To"
+msgstr "åé
ç»"
+
+#: ../templates/classic/html/issue.item.html:65
+msgid "Topics"
+msgstr "主é¢"
+
+#: ../templates/classic/html/issue.item.html:73
+msgid "Change Note"
+msgstr "ä¿®æ¹è®°å½"
+
+#: ../templates/classic/html/issue.item.html:81
+msgid "File"
+msgstr "æ件"
+
+#: ../templates/classic/html/issue.item.html:100
+msgid "<table class=\"form\"> <tr> <td>Note: </td> <th class=\"required\">highlighted</th> <td> fields are required.</td> </tr> </table>"
+msgstr "<table class=\"form\"> <tr> <td>注æï¼ </td> <th class=\"required\">é«äº®</th> <td> å段æ¯å¿
é¡»çã</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 "å¨ <b>${creation}</b> ç± <b>${creator}</b> å建ï¼æåç± <b>${actor}</b> ä¿®æ¹ä¸º <b>${activity}</b>ã"
+
+#: ../templates/classic/html/issue.item.html:118
+#: ../templates/classic/html/msg.item.html:51
+msgid "Files"
+msgstr "æ件"
+
+#: ../templates/classic/html/issue.item.html:120
+#: ../templates/classic/html/msg.item.html:53
+msgid "File name"
+msgstr "æ件å"
+
+#: ../templates/classic/html/issue.item.html:121
+#: ../templates/classic/html/msg.item.html:54
+msgid "Uploaded"
+msgstr "å·²ä¸ä¼ "
+
+#: ../templates/classic/html/issue.item.html:122
+msgid "Type"
+msgstr "ç±»å"
+
+#: ../templates/classic/html/issue.item.html:123
+#: ../templates/classic/html/query.edit.html:30
+msgid "Edit"
+msgstr "ç¼è¾"
+
+#: ../templates/classic/html/issue.item.html:124
+msgid "Remove"
+msgstr "å é¤"
+
+#: ../templates/classic/html/issue.item.html:144
+#: ../templates/classic/html/issue.item.html:165
+#: ../templates/classic/html/query.edit.html:50
+msgid "remove"
+msgstr "å é¤"
+
+#: ../templates/classic/html/issue.item.html:151
+#: ../templates/classic/html/msg.index.html:9
+msgid "Messages"
+msgstr "ä¿¡æ¯"
+
+#: ../templates/classic/html/issue.item.html:155
+msgid "msg${id} (view)"
+msgstr "msg${id} (æ¥ç)"
+
+#: ../templates/classic/html/issue.item.html:156
+msgid "Author: ${author}"
+msgstr "ä½è
ï¼${author}"
+
+#: ../templates/classic/html/issue.item.html:158
+msgid "Date: ${date}"
+msgstr "æ¥æï¼${date}"
+
+#: ../templates/classic/html/issue.search.html:2
+msgid "Issue searching - ${tracker}"
+msgstr "é®é¢æç´¢ - ${tracker}"
+
+#: ../templates/classic/html/issue.search.html:4
+msgid "Issue searching"
+msgstr "é®é¢æç´¢"
+
+#: ../templates/classic/html/issue.search.html:25
+msgid "Filter on"
+msgstr "è¿æ»¤æ"
+
+#: ../templates/classic/html/issue.search.html:26
+msgid "Display"
+msgstr "æ¾ç¤º"
+
+#: ../templates/classic/html/issue.search.html:27
+msgid "Sort on"
+msgstr "æåºæã"
+
+#: ../templates/classic/html/issue.search.html:28
+msgid "Group on"
+msgstr "åç»æ"
+
+#: ../templates/classic/html/issue.search.html:32
+msgid "All text*:"
+msgstr "ææææ¬*"
+
+#: ../templates/classic/html/issue.search.html:40
+msgid "Title:"
+msgstr "æ é¢ï¼"
+
+#: ../templates/classic/html/issue.search.html:50
+msgid "Topic:"
+msgstr "主é¢ï¼"
+
+#: ../templates/classic/html/issue.search.html:58
+msgid "ID:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:66
+msgid "Creation Date:"
+msgstr "å建æ¶é´ï¼"
+
+#: ../templates/classic/html/issue.search.html:77
+msgid "Creator:"
+msgstr "å建人ï¼"
+
+#: ../templates/classic/html/issue.search.html:79
+msgid "created by me"
+msgstr "ç±æå建"
+
+#: ../templates/classic/html/issue.search.html:88
+msgid "Activity:"
+msgstr "æ´»è·åº¦ï¼"
+
+#: ../templates/classic/html/issue.search.html:99
+msgid "Actor:"
+msgstr "æ§è¡äººï¼"
+
+#: ../templates/classic/html/issue.search.html:101
+msgid "done by me"
+msgstr "ç±æå®æ"
+
+#: ../templates/classic/html/issue.search.html:112
+msgid "Priority:"
+msgstr "ä¼å
级ï¼"
+
+#: ../templates/classic/html/issue.search.html:114
+#: ../templates/classic/html/issue.search.html:130
+msgid "not selected"
+msgstr "æªéæ©"
+
+#: ../templates/classic/html/issue.search.html:125
+msgid "Status:"
+msgstr "ç¶æï¼"
+
+#: ../templates/classic/html/issue.search.html:128
+msgid "not resolved"
+msgstr "æªè§£å³"
+
+#: ../templates/classic/html/issue.search.html:143
+msgid "Assigned to:"
+msgstr "åé
ç»ï¼"
+
+#: ../templates/classic/html/issue.search.html:146
+msgid "assigned to me"
+msgstr "åé
ç»æ"
+
+#: ../templates/classic/html/issue.search.html:148
+msgid "unassigned"
+msgstr "æªåé
"
+
+#: ../templates/classic/html/issue.search.html:158
+msgid "Pagesize:"
+msgstr "页大å°ï¼"
+
+#: ../templates/classic/html/issue.search.html:164
+msgid "Start With:"
+msgstr "å¼å§å¨ï¼"
+
+#: ../templates/classic/html/issue.search.html:170
+msgid "Sort Descending:"
+msgstr "éåºæåï¼"
+
+#: ../templates/classic/html/issue.search.html:177
+msgid "Group Descending:"
+msgstr "éåºåç»ï¼"
+
+#: ../templates/classic/html/issue.search.html:184
+msgid "Query name**:"
+msgstr "æ¥è¯¢ åå**"
+
+#: ../templates/classic/html/issue.search.html:194
+#: ../templates/classic/html/page.html:47
+msgid "Search"
+msgstr "æç´¢"
+
+#: ../templates/classic/html/issue.search.html:198
+msgid "*: The \"all text\" field will look in message bodies and issue titles<br> **: If you supply a name, the query will be saved off and available as a link in the sidebar"
+msgstr "*: å¨ä¿¡æ¯ä½åé®é¢æ é¢ä¸ç \"ææçææ¬\" å段é½å°è¢«æ¥æ¾<br> **: å¦æä½ æä¾äºä¸ä¸ªååï¼è¿ä¸ªæ¥è¯¢å°è¢«ä¿å并ä¸ä½ä¸ºä¸ä¸ªé¾æ¥åºç°å¨ä¾§è¾¹æ ä¸"
+
+#: ../templates/classic/html/keyword.item.html:3
+msgid "Keyword editing - ${tracker}"
+msgstr "å
³é®åç¼è¾ - ${tracker}"
+
+#: ../templates/classic/html/keyword.item.html:5
+msgid "Keyword editing"
+msgstr "å
³é®åç¼è¾"
+
+#: ../templates/classic/html/keyword.item.html:11
+msgid "Existing Keywords"
+msgstr "åå¨çå
³é®å"
+
+#: ../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
+msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
+msgstr "æ³è¦å建æ°çå
³é®åï¼è¯·ç¹å»ä¸é¢ç \"æ交æ°ç项\"ã"
+
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
+msgstr "å
³é®å"
+
+#: ../templates/classic/html/msg.index.html:3
+msgid "List of messages - ${tracker}"
+msgstr "ä¿¡æ¯å表 - ${tracker}"
+
+#: ../templates/classic/html/msg.index.html:5
+msgid "Message listing"
+msgstr "ä¿¡æ¯å表"
+
+#: ../templates/classic/html/msg.item.html:6
+msgid "Message ${id} - ${tracker}"
+msgstr "ä¿¡æ¯ [${id}] - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:9
+msgid "New Message - ${tracker}"
+msgstr "æ°ä¿¡æ¯ - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:13
+msgid "New Message"
+msgstr "æ°ä¿¡æ¯"
+
+#: ../templates/classic/html/msg.item.html:15
+msgid "New Message Editing"
+msgstr "æ°ä¿¡æ¯ç¼è¾"
+
+#: ../templates/classic/html/msg.item.html:18
+msgid "Message${id}"
+msgstr "ä¿¡æ¯ [${id}]"
+
+#: ../templates/classic/html/msg.item.html:21
+msgid "Message${id} Editing"
+msgstr "ä¿¡æ¯ [${id}] ç¼è¾"
+
+#: ../templates/classic/html/msg.item.html:28
+msgid "Author"
+msgstr "ä½è
"
+
+#: ../templates/classic/html/msg.item.html:33
+msgid "Recipients"
+msgstr "æ¶ä¿¡äºº"
+
+#: ../templates/classic/html/msg.item.html:44
+msgid "Content"
+msgstr "å
容"
+
+#: ../templates/classic/html/page.html:28
+msgid "<b>Your Queries</b> (<a href=\"query?@template=edit\">edit</a>)"
+msgstr "<b>æçæ¥è¯¢</b> (<a href=\"query?@template=edit\">ç¼è¾</a>)"
+
+#: ../templates/classic/html/page.html:39
+msgid "Issues"
+msgstr "é®é¢"
+
+#: ../templates/classic/html/page.html:41
+#: ../templates/classic/html/page.html:60
+msgid "Create New"
+msgstr "æ°å»º"
+
+#: ../templates/classic/html/page.html:43
+msgid "Show Unassigned"
+msgstr "æ¾ç¤ºæªåé
"
+
+#: ../templates/classic/html/page.html:45
+msgid "Show All"
+msgstr "æ¾ç¤ºææ"
+
+#: ../templates/classic/html/page.html:48
+msgid "Show issue:"
+msgstr "æ¾ç¤ºé®é¢ï¼"
+
+#: ../templates/classic/html/page.html:58
+msgid "Keywords"
+msgstr "å
³é®å"
+
+#: ../templates/classic/html/page.html:64
+msgid "Edit Existing"
+msgstr "ç¼è¾å·²ç»åå¨ç"
+
+#: ../templates/classic/html/page.html:70
+#: ../templates/minimal/html/page.html:48
+msgid "Administration"
+msgstr "管ç"
+
+#: ../templates/classic/html/page.html:72
+#: ../templates/minimal/html/page.html:49
+msgid "Class List"
+msgstr "ç±»å表"
+
+#: ../templates/classic/html/page.html:76
+#: ../templates/minimal/html/page.html:51
+msgid "User List"
+msgstr "ç¨æ·å表"
+
+#: ../templates/classic/html/page.html:78
+#: ../templates/minimal/html/page.html:54
+msgid "Add User"
+msgstr "å¢å ç¨æ·"
+
+#: ../templates/classic/html/page.html:85
+#: ../templates/classic/html/page.html:89
+#: ../templates/minimal/html/page.html:30
+msgid "Login"
+msgstr "ç»å½"
+
+#: ../templates/classic/html/page.html:91
+#: ../templates/classic/html/user.register.html:63
+#: ../templates/minimal/html/page.html:33
+#: ../templates/minimal/html/user.register.html:58
+msgid "Register"
+msgstr "注å"
+
+#: ../templates/classic/html/page.html:94
+msgid "Lost your login?"
+msgstr "å¿è®°ä½ çç»å
¥å£ä»¤äºï¼"
+
+#: ../templates/classic/html/page.html:99
+msgid "Hello, ${user}"
+msgstr "ä½ å¥½ï¼${user}"
+
+#: ../templates/classic/html/page.html:101
+msgid "Your Issues"
+msgstr "æçé®é¢"
+
+#: ../templates/classic/html/page.html:102
+#: ../templates/minimal/html/page.html:40
+msgid "Your Details"
+msgstr "æçä¿¡æ¯"
+
+#: ../templates/classic/html/page.html:104
+#: ../templates/minimal/html/page.html:42
+msgid "Logout"
+msgstr "注é"
+
+#: ../templates/classic/html/page.html:108
+msgid "Help"
+msgstr "帮å©"
+
+#: ../templates/classic/html/page.html:109
+msgid "Roundup docs"
+msgstr "Roundupææ¡£"
+
+#: ../templates/classic/html/page.html:160
+msgid "don't care"
+msgstr "ä¸ç¨å
³å¿"
+
+#: ../templates/classic/html/page.html:162
+msgid "------------"
+msgstr ""
+
+#: ../templates/classic/html/page.html:187
+msgid "no value"
+msgstr "æ å¼"
+
+#: ../templates/classic/html/query.edit.html:4
+msgid "\"Your Queries\" Editing - ${tracker}"
+msgstr "\"æçæ¥è¯¢\" ä¿®æ¹ - ${tracker}"
+
+#: ../templates/classic/html/query.edit.html:6
+msgid "\"Your Queries\" Editing"
+msgstr "\"æçæ¥è¯¢\"ä¿®æ¹"
+
+#: ../templates/classic/html/query.edit.html:11
+msgid "You are not allowed to edit queries."
+msgstr "ä¸å
许ç¼è¾æ¥è¯¢"
+
+#: ../templates/classic/html/query.edit.html:28
+msgid "Query"
+msgstr "æ¥è¯¢"
+
+#: ../templates/classic/html/query.edit.html:29
+msgid "Include in \"Your Queries\""
+msgstr "å
æ¬å¨\"æçæ¥è¯¢\"ä¸"
+
+#: ../templates/classic/html/query.edit.html:31
+msgid "Private to you?"
+msgstr "æ¯ç§äººä¿¡æ¯åï¼"
+
+#: ../templates/classic/html/query.edit.html:44
+msgid "leave out"
+msgstr "çç¥"
+
+#: ../templates/classic/html/query.edit.html:45
+msgid "include"
+msgstr "å
å«"
+
+#: ../templates/classic/html/query.edit.html:49
+msgid "leave in"
+msgstr "çä¸"
+
+#: ../templates/classic/html/query.edit.html:54
+msgid "[query is retired]"
+msgstr "[æ¥è¯¢è¿æäº]"
+
+#: ../templates/classic/html/query.edit.html:67
+msgid "edit"
+msgstr "ç¼è¾"
+
+#: ../templates/classic/html/query.edit.html:71
+msgid "yes"
+msgstr "æ¯"
+
+#: ../templates/classic/html/query.edit.html:73
+msgid "no"
+msgstr "å¦"
+
+#: ../templates/classic/html/query.edit.html:79
+msgid "Delete"
+msgstr "å é¤"
+
+#: ../templates/classic/html/query.edit.html:90
+msgid "[not yours to edit]"
+msgstr "[ä¸ç±ä½ ä¿®æ¹]"
+
+#: ../templates/classic/html/query.edit.html:96
+msgid "Save Selection"
+msgstr "ä¿åéæ©"
+
+#: ../templates/classic/html/user.forgotten.html:3
+msgid "Password reset request - ${tracker}"
+msgstr "å£ä»¤éè®¾è¯·æ± - ${tracker}"
+
+#: ../templates/classic/html/user.forgotten.html:5
+msgid "Password reset request"
+msgstr "å£ä»¤é设请æ±"
+
+#: ../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 "é®ä»¶å°åï¼"
+
+#: ../templates/classic/html/user.forgotten.html:24
+#: ../templates/classic/html/user.forgotten.html:34
+msgid "Request password reset"
+msgstr "请æ±å£ä»¤é设"
+
+#: ../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 "ç¨æ·åï¼"
+
+#: ../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.index.html:3
+#: ../templates/minimal/html/user.index.html:3
+msgid "User listing - ${tracker}"
+msgstr "ç¨æ·å表 - ${tracker}"
+
+#: ../templates/classic/html/user.index.html:5
+#: ../templates/minimal/html/user.index.html:5
+msgid "User listing"
+msgstr "ç¨æ·å表"
+
+#: ../templates/classic/html/user.index.html:14
+#: ../templates/minimal/html/user.index.html:14
+msgid "Username"
+msgstr "ç¨æ·å"
+
+#: ../templates/classic/html/user.index.html:15
+msgid "Real name"
+msgstr "çå®å§å"
+
+#: ../templates/classic/html/user.index.html:16
+#: ../templates/classic/html/user.item.html:65
+#: ../templates/classic/html/user.register.html:45
+msgid "Organisation"
+msgstr "ç»ç»"
+
+#: ../templates/classic/html/user.index.html:17
+#: ../templates/minimal/html/user.index.html:15
+msgid "Email address"
+msgstr "é®ä»¶å°å"
+
+#: ../templates/classic/html/user.index.html:18
+msgid "Phone number"
+msgstr "çµè¯å·ç "
+
+#: ../templates/classic/html/user.index.html:19
+msgid "Retire"
+msgstr "æ¶å"
+
+#: ../templates/classic/html/user.index.html:32
+msgid "retire"
+msgstr "æ¶å"
+
+#: ../templates/classic/html/user.item.html:7
+msgid "User ${id}: ${title} - ${tracker}"
+msgstr "ç¨æ· [${id}]: ${title} - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:10
+msgid "New User - ${tracker}"
+msgstr "æ°ç¨æ· - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:14
+#: ../templates/minimal/html/user.item.html:6
+msgid "New User"
+msgstr "æ°ç¨æ·"
+
+#: ../templates/classic/html/user.item.html:16
+#: ../templates/minimal/html/user.item.html:8
+msgid "New User Editing"
+msgstr "æ°ç¨æ·ç¼è¾"
+
+#: ../templates/classic/html/user.item.html:19
+#: ../templates/minimal/html/user.item.html:11
+msgid "User${id}"
+msgstr "ç¨æ· [${id}]"
+
+#: ../templates/classic/html/user.item.html:22
+#: ../templates/minimal/html/user.item.html:14
+msgid "User${id} Editing"
+msgstr "ç¨æ· [${id}] ç¼è¾"
+
+#: ../templates/classic/html/user.item.html:38
+#: ../templates/classic/html/user.register.html:21
+#: ../templates/minimal/html/user.item.html:27
+#: ../templates/minimal/html/user.item.html:67
+#: ../templates/minimal/html/user.register.html:26
+msgid "Login Name"
+msgstr "ç»å½å"
+
+#: ../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 "ç»å½å£ä»¤"
+
+#: ../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 "å£ä»¤ç¡®è®¤"
+
+#: ../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
+msgid "Roles"
+msgstr "è§è²"
+
+#: ../templates/classic/html/user.item.html:56
+msgid "(to give the user more than one role, enter a comma,separated,list)"
+msgstr "(为ç»ç¨æ·æå®å¤ä¸ªè§è²ï¼ç¨éå·åéå®ä»¬)"
+
+#: ../templates/classic/html/user.item.html:61
+#: ../templates/classic/html/user.register.html:41
+msgid "Phone"
+msgstr "çµè¯"
+
+#: ../templates/classic/html/user.item.html:69
+msgid "Timezone"
+msgstr "æ¶åº"
+
+#: ../templates/classic/html/user.item.html:73
+msgid "(this is a numeric hour offset, the default is ${zone})"
+msgstr "(è¿æ¯æ°åçå°æ¶å移éï¼ç¼ºçå¼æ¯ ${zone})"
+
+#: ../templates/classic/html/user.item.html:78
+#: ../templates/classic/html/user.register.html:49
+#: ../templates/minimal/html/user.item.html:47
+#: ../templates/minimal/html/user.item.html:71
+#: ../templates/minimal/html/user.register.html:46
+msgid "E-mail address"
+msgstr "é®ä»¶å°å"
+
+#: ../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
+msgid "Alternate E-mail addresses<br>One address per line"
+msgstr "ä¿®æ¹é®ä»¶å°å<br>æ¯è¡ä¸ä¸ªå°å"
+
+#: ../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 "ç¨ ${tracker} 注å"
+
+#: ../templates/classic/html/user.rego_progress.html:4
+#: ../templates/minimal/html/user.rego_progress.html:4
+msgid "Registration in progress - ${tracker}"
+msgstr "注åæ£å¨å¤ç - ${tracker}"
+
+#: ../templates/classic/html/user.rego_progress.html:6
+#: ../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
+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/minimal/html/home.html:2
+msgid "Tracker home - ${tracker}"
+msgstr "Trackeræ ¹ç®å½ - ${tracker}"
+
+#: ../templates/minimal/html/home.html:4
+msgid "Tracker home"
+msgstr "Trackeræ ¹ç®å½"
+
+#: ../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
+msgid "Please log in or register."
+msgstr "请ç»å½æ注åã"
+
+#: ../templates/minimal/html/page.html:38
+msgid "Hello,<br>${user}"
+msgstr "ä½ å¥½,<br>${user}"
+
+#: ../templates/minimal/html/user.item.html:3
+msgid "User editing - ${tracker}"
+msgstr "ç¨æ·ç¼è¾ - ${tracker}"
+
Added: tracker/vendor/roundup/current/locale/zh_TW.po
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/locale/zh_TW.po Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2743 @@
+# 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 $
+#
+# roundup.pot revision 1.10
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.8.3\n"
+"Report-Msgid-Bugs-To: roundup-devel at lists.sourceforge.net\n"
+"POT-Creation-Date: 2004-10-19 12:33+0300\n"
+"PO-Revision-Date: 2005-05-15 17:40+0800\n"
+"Last-Translator: Fred Lin <gasolin at gmail>\n"
+"Language-Team: Chinese Traditional <gasolin at gmail.com>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Poedit-Language: Chinese\n"
+"X-Poedit-Country: TAIWAN\n"
+
+# ../roundup/admin.py:84 :943 :992 :1014
+#: ../roundup/admin.py:84
+#: ../roundup/admin.py:943
+#: ../roundup/admin.py:992
+#: ../roundup/admin.py:1014
+#, python-format
+msgid "no such class \"%(classname)s\""
+msgstr "ç¡æ¤é¡å¥ \"%(classname)s\""
+
+# ../roundup/admin.py:94 :98
+#: ../roundup/admin.py:94
+#: ../roundup/admin.py:98
+#, python-format
+msgid "argument \"%(arg)s\" not propname=value"
+msgstr "åæ¸ \"%(arg)s\" ä¸æ¯ propname=value çå½¢å¼"
+
+#: ../roundup/admin.py:111
+#, python-format
+msgid ""
+"Problem: %(message)s\n"
+"\n"
+msgstr ""
+"åé¡: %(message)s\n"
+"\n"
+
+#: ../roundup/admin.py:112
+#, 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"
+"\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 ""
+"%(message)sç¨æ³: roundup-admin [options] [<command> <arguments>]\n"
+"\n"
+"é¸é
ï¼\n"
+" -i 實ä¾è·¯å¾ -- æå®åé¡è·è¹¤ç³»çµ± \"æ ¹ç®é\" çº ç®¡çå¡\n"
+" -u -- user[:password] ç¨æ¼å½ä»¤ä¸\n"
+" -d --åå°ææçæ示信æ¯èä¸åªæ¯é¡çIDè\n"
+" -c -- å¨è¼¸åºæ¸æå表æï¼ä½¿ç¨å¥è('.')åéã\n"
+" å¦åå·è¡ '-S \",\"'ã\n"
+" -S <string> -- ç¶è¼¸åºæ¸æå表æï¼ä½¿ç¨ string åé\n"
+" -s -- ç¶è¼¸åºæ¸æå表æï¼ä½¿ç¨ç©ºæ ¼åéã\n"
+" å¦åå·è¡ '-S \" \"'ã\n"
+"\n"
+" -s, -c æè
-S åªè½æä¸å被æå®ã\n"
+"\n"
+"說æï¼\n"
+" roundup-admin -h\n"
+" roundup-admin help -- æ¬èªªæ\n"
+" roundup-admin help <command> -- å½ä»¤è©³è§£èªªæ\n"
+" roundup-admin help all -- ææå¯ç¨ç說æ\n"
+
+#: ../roundup/admin.py:137
+msgid "Commands:"
+msgstr "å½ä»¤ï¼"
+
+#: ../roundup/admin.py:144
+msgid ""
+"Commands may be abbreviated as long as the abbreviation\n"
+"matches only one command, e.g. l == li == lis == list."
+msgstr ""
+"å½ä»¤å¯ä»¥è¢«ç¸®å¯«ï¼åªè¦ç¸®å¯«åªæä¸åå½ä»¤å¯ä»¥å¹é
ä¸ï¼\n"
+"å¦ï¼l == li == lis == list."
+
+#: ../roundup/admin.py:174
+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 ""
+"\n"
+"ææçå½ä»¤(é¤äº help)è¦æ±æå®ä¸åtrackerãéå°±æ¯ä½ æ£å¨å·¥ä½çtrackerçè·¯å¾ã\n"
+"ä¸åtrackerå°±æ¯roundupç¶è·çæ¸æ庫åå®ç¾©äºtrackeré
ç½®æ件çå°æ¹ãå¯ä»¥æå®æ³\n"
+"象çºåé¡è·è¹¤ç³»çµ±ç\"èµ·å§\"ç®éãå®å¯ä»¥å¨ç°å¢è®é TRACKER_HOME æå¨å½ä»¤è¡ä»¥ \n"
+"\"-i tracker\" ä¾æå®ã\n"
+"\n"
+"ä¸åæ示å¨(designator)æ¯ä¸åé¡ååä¸åçµé»idççµåé«ï¼å¦ï¼bug1, user10, ...\n"
+"\n"
+"屬æ§å¼å¨å½ä»¤åæ¸ä¸ååå°çµæä¸è¢«æè¿°çºå符串ï¼\n"
+" . Strings 表示å符串ã\n"
+" . Date çå¼å¨æ¬å°æåä¸æå
¨æ¥ææ ¼å¼åå°ï¼ä¸¦ä¸å¯ä»¥æå
¨æ¥ææ ¼å¼æä¸é¢è§£éçä»»\n"
+" ä½é¨åæ¥ææ ¼å¼ä¾æ¥æ¶ã\n"
+" . Link çå¼æçµé»æ示å¨(designator)ä¾åå°ãç¶ä½çºåæ¸çµ¦åºæï¼çµé»æ示å¨\n"
+" (designator)åéµå符串é½å¯ä»¥æ¥æ¶ã\n"
+" . Multilink çå¼æçµé»æ示å¨(designator)å表(以éèåé)ä¾åå°ãç¶ä½çºä¸åå\n"
+" æ¸çµ¦åºæï¼çµé»æ示å¨(designator)æ以éèè¯æ¥ççµé»å表é½æ¯å¯ä»¥æ¥åçã\n"
+"\n"
+"ç¶å±¬æ§å¼å¿
é å
å«ç©ºæ ¼æï¼åªéä½¿ç¨ ' æè
\" ä¾å
å«å¼ãå®åç©ºæ ¼ä¹å¯ä»¥ç¨åæç·ä¾\n"
+"è½ç¾©ãå¦æä¸åå¼å¿
é å
å«å¼èå符ï¼å®å¿
é 使ç¨åæç·ä¾è½ç¾©æå
§é¨å
å«ãä¾å¦ï¼\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"
+"ç¶å¤åçµé»è¢«æå®ç¨å¨ Roundup ç get æ set å½ä»¤æï¼æå®ç屬æ§å¨ææååº\n"
+"ççµé»ä¸æ被ç²åæè¨ç½®ã\n"
+"\n"
+"ç¶ Roundup ç get æ find å½ä»¤è¿åå¤åçµææï¼æ¯è¡å°åå°ä¸å屬æ§(é è¨)æ\n"
+"ç¨éèè¯æ¥èµ·ä¾(ç¨ -c åæ¸)ã\n"
+"\n"
+"å¨åå¨ä¿®æ¹æ¸æçå½ä»¤ä¸ï¼éè¦ç»éå/å£ä»¤ãç»éåæè
ç¨ \"name\" æ \"name:password\"\n"
+"ä¾æå®ã\n"
+" . ROUNDUP_LOGIN ç°å¢è®é\n"
+" . -u å½ä»¤è¡é¸é
\n"
+"å¦æååæå£ä»¤é½æ²ææä¾ï¼å®åå°å¾å½ä»¤è¡ç²å¾ã\n"
+"\n"
+"Date format examples:\n"
+" \"2000-04-17.03:45\" 表示 <Date 2000-04-17.08:45:00>\n"
+" \"2000-04-17\" 表示 <Date 2000-04-17.00:00:00>\n"
+" \"01-25\" 表示 <Date yyyy-01-25.00:00:00>\n"
+" \"08-13.22:13\" 表示 <Date yyyy-08-14.03:13:00>\n"
+" \"11-07.09:32:43\" 表示 <Date yyyy-11-07.14:32:43>\n"
+" \"14:25\" 表示 <Date yyyy-mm-dd.19:25:00>\n"
+" \"8:47:11\" 表示 <Date yyyy-mm-dd.13:47:11>\n"
+" \".\" 表示 \"ç¾å¨\"\n"
+"\n"
+"使ç¨èªªæï¼\n"
+
+#: ../roundup/admin.py:237
+#, python-format
+msgid "%s:"
+msgstr ""
+
+#: ../roundup/admin.py:242
+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 ""
+"ç¨æ³ï¼help topic\n"
+" 給åºéæ¼ä¸»é¡ç說æã\n"
+"\n"
+" commands -- ååºå½ä»¤\n"
+" <command> -- æå®å½ä»¤ç說æè¦ç¯\n"
+" initopts -- åå§åå½ä»¤é¸é
\n"
+" all -- ææå¯ç¨ç說æ\n"
+" "
+
+#: ../roundup/admin.py:265
+#, python-format
+msgid "Sorry, no help for \"%(topic)s\""
+msgstr "æ±æï¼æ²æå° \"%(topic)s\" ç說æä¿¡æ¯"
+
+# ../roundup/admin.py:337 :387
+#: ../roundup/admin.py:337
+#: ../roundup/admin.py:387
+msgid "Templates:"
+msgstr "模æ¿ï¼"
+
+# ../roundup/admin.py:340 :398
+#: ../roundup/admin.py:340
+#: ../roundup/admin.py:398
+msgid "Back ends:"
+msgstr "å¾ç«¯ï¼"
+
+#: ../roundup/admin.py:343
+msgid ""
+"Usage: install [template [backend [admin password]]]\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"
+"\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 ""
+"ç¨æ³ï¼install [template [backend [admin password]]]\n"
+" å®è£ä¸åæ°çtracker實ä¾ã\n"
+"\n"
+" éåå½ä»¤å°æ示輸å
¥ tracker èµ·å§ç®é\n"
+" (å¦ææ²æéé TRACKER_HOME æ -i é¸é
æä¾)ã\n"
+" 模æ¿ãå¾ç«¯å管çå¡å£ä»¤æ該å¨å½ä»¤è¡æé åºä»¥åæ¸çå½¢å¼è¢«æå®ã\n"
+"\n"
+" åå§å(initialise)å½ä»¤å¿
é å¨éåå½ä»¤ä¹å¾è¢«èª¿ç¨ï¼ä»¥ä¾¿åå§åtrackeræ¸\n"
+" æ庫ãä½ å¯ä»¥å¨éè¡åå§åå½ä»¤ä¹å編輯 tracker ç dbinit.py 模å¡ç\n"
+" init() æ¹æ³ä¾ä¿®æ¹ tracker çåå§æ¸æ庫å
§å®¹ã\n"
+"\n"
+" è«æ¥çåå§ååæ¸èªªæã\n"
+" "
+
+# ../roundup/admin.py:359 :494 :573 :623 :676 :697 :725 :796 :863 :934 :982
+# :1004 :1031 :1093 :1159
+#: ../roundup/admin.py:359
+#: ../roundup/admin.py:494
+#: ../roundup/admin.py:573
+#: ../roundup/admin.py:623
+#: ../roundup/admin.py:676
+#: ../roundup/admin.py:697
+#: ../roundup/admin.py:725
+#: ../roundup/admin.py:796
+#: ../roundup/admin.py:863
+#: ../roundup/admin.py:934
+#: ../roundup/admin.py:982
+#: ../roundup/admin.py:1004
+#: ../roundup/admin.py:1031
+#: ../roundup/admin.py:1093
+#: ../roundup/admin.py:1159
+msgid "Not enough arguments supplied"
+msgstr "æªæä¾è¶³å¤ çåæ¸"
+
+#: ../roundup/admin.py:365
+#, python-format
+msgid "Instance home parent directory \"%(parent)s\" does not exist"
+msgstr "實ä¾ç®éçç¶ç®é \"%(parent)s\" ä¸åå¨"
+
+#: ../roundup/admin.py:374
+#, 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 ""
+"è¦åï¼å¨ \"%(tracker_home)s\" å·²ç¶åå¨ä¸åtrackeräºï¼\n"
+"å¦æä½ æç®éæ°å®è£å®ï¼ææçæ¸æå°æä¸å¤±ï¼\n"
+"åªé¤å®åï¼Y/N: "
+
+#: ../roundup/admin.py:389
+msgid "Select template [classic]: "
+msgstr "é¸ææ¨¡æ¿ [classic]ï¼"
+
+#: ../roundup/admin.py:400
+msgid "Select backend [anydbm]: "
+msgstr "é¸æå¾ç«¯ [anydbm]ï¼"
+
+#: ../roundup/admin.py:409
+#, python-format
+msgid ""
+"\n"
+" You should now edit the tracker configuration file:\n"
+" %(config_file)s"
+msgstr ""
+"\n"
+" ç¾å¨ä½ æ該修æ¹trackerçé
ç½®æ件ï¼\n"
+" %(config_file)s"
+
+#: ../roundup/admin.py:418
+msgid " ... at a minimum, you must set following options:"
+msgstr " ... è³å°ï¼ä½ å¿
é è¨ç½®ä»¥ä¸é¸é
ï¼"
+
+#: ../roundup/admin.py:423
+#, 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"
+msgstr ""
+"\n"
+" å¦æä½ æ³è¦ä¿®æ¹æ¸æ庫çµæ§ï¼\n"
+" ä½ ä¹éè¦ç·¨è¼¯è¡¨çµæ§æ件ï¼\n"
+" %(database_config_file)s\n"
+" ä½ å¯è½ä¹éè¦ä¿®æ¹æ¸æ庫åå§åæ件ï¼\n"
+" %(database_init_file)s\n"
+" ... æ¥çéæ¼å®¢æ¶åçææªä¾ç解æ´å¤çä¿¡æ¯ã\n"
+
+#. password
+#: ../roundup/admin.py:438
+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 ""
+"ç¨æ³ï¼initialise [adminpw]\n"
+" åå§åä¸åæ°çtrackerã\n"
+"\n"
+" 管çå¡çä¿¡æ¯éè¦å¨éä¸æ¥é²è¡è¨ç½®ã\n"
+"\n"
+" å·è¡trackerçåå§åå½æ¸ dbinit.init()\n"
+" "
+
+#: ../roundup/admin.py:452
+msgid "Admin Password: "
+msgstr "管çå¡å£ä»¤ï¼"
+
+#: ../roundup/admin.py:453
+msgid " Confirm: "
+msgstr " 確èªï¼"
+
+#: ../roundup/admin.py:457
+msgid "Instance home does not exist"
+msgstr "實ä¾ç®éä¸åå¨"
+
+#: ../roundup/admin.py:461
+msgid "Instance has not been installed"
+msgstr "實ä¾éæ²æå®è£"
+
+#: ../roundup/admin.py:466
+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 ""
+"è¦åï¼æ¸æ庫已ç¶è¢«åå§åï¼\n"
+"å¦æä½ éæ°åå§åå®ï¼ææçæ¸æå°æä¸å¤±ï¼\n"
+"åªé¤å®åï¼Y/N: "
+
+#: ../roundup/admin.py:487
+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 ""
+"ç¨æ³ï¼get property designator[,designator]*\n"
+" å¾å°æå®å±¬æ§ä¸åæå¤åæ示å¨(designator)ã\n"
+"\n"
+" ééæ示å¨(designator)ä¾å¾å°æå®çµé»ç屬æ§å¼ã\n"
+" "
+
+# ../roundup/admin.py:527 :542
+#: ../roundup/admin.py:527
+#: ../roundup/admin.py:542
+#, python-format
+msgid "property %s is not of type Multilink or Link so -d flag does not apply."
+msgstr "å±¬æ§ %s ä¸æ¯ Multilink æ Link é¡åï¼æ以 -d æ¨èªä¸è½æç¨ã"
+
+# ../roundup/admin.py:550 :945 :994 :1016
+#: ../roundup/admin.py:550
+#: ../roundup/admin.py:945
+#: ../roundup/admin.py:994
+#: ../roundup/admin.py:1016
+#, python-format
+msgid "no such %(classname)s node \"%(nodeid)s\""
+msgstr "æ²æé樣ç %(classname)s çµé» \"%(nodeid)s\""
+
+#: ../roundup/admin.py:552
+#, python-format
+msgid "no such %(classname)s property \"%(propname)s\""
+msgstr "æ²æé樣ç %(classname)s å±¬æ§ \"%(propname)s\""
+
+#: ../roundup/admin.py:561
+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 ""
+"ç¨æ³ï¼set items property=value property=value ...\n"
+" è¨ç½®ä¸åæå¤åæ¢ç®ç屬æ§ã\n"
+"\n"
+" æ¢ç®æçæ¯ä¸åé¡å¥ï¼æ以éèåéçé
ç®æ示å¨(designator)å表(ä¾å¦ï¼\"designator[,designator,...]\")ã\n"
+"\n"
+" éåå½ä»¤çºææ給åºçæ示å¨(designator)è¨ç½®å±¬æ§å¼ãå¦æ屬æ§å¼è¢«çç¥\n"
+" (ä¾å¦ï¼\"property=\")é£éº¼å±¬æ§æ¯æªè¨ç½®çãå¦æ屬æ§æ¯ä¸åå¤éæ¥(multilink)ï¼\n"
+" ä½ éè¦çºå¤éæ¥æä¾ç¨éèåéçæ¸å(ä¾å¦ \"1,2,3\")ã\n"
+" "
+
+#: ../roundup/admin.py:615
+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 ""
+"ç¨æ³ï¼find classname propname=value ...\n"
+" æ ¹æ給å®ç link 屬æ§å¼ä¾æ¥æ¾çµ¦å®é¡åççµé»ã\n"
+"\n"
+" æ ¹æ給å®ç link 屬æ§å¼ä¾æ¥æ¾çµ¦å®é¡åççµé»ãéåå¼æè
æ¯éæ¥çµé»ççµé»IDï¼\n"
+" æè
æ¯çµé»çéµå¼ã\n"
+" "
+
+# ../roundup/admin.py:663 :816 :828 :882
+#: ../roundup/admin.py:663
+#: ../roundup/admin.py:816
+#: ../roundup/admin.py:828
+#: ../roundup/admin.py:882
+#, python-format
+msgid "%(classname)s has no property \"%(propname)s\""
+msgstr "%(classname)s æ²æ \"%(propname)s\" 屬æ§"
+
+#: ../roundup/admin.py:670
+msgid ""
+"Usage: specification classname\n"
+" Show the properties for a classname.\n"
+"\n"
+" This lists the properties for a given class.\n"
+" "
+msgstr ""
+"ç¨æ³: specification classname\n"
+" 顯示ä¸åé¡ååç屬æ§ã\n"
+"\n"
+" æååºçµ¦å®é¡åç屬æ§ã\n"
+" "
+
+#: ../roundup/admin.py:685
+#, python-format
+msgid "%(key)s: %(value)s (key property)"
+msgstr "%(key)s: %(value)s (ééµå±¬æ§)"
+
+#: ../roundup/admin.py:687
+#, python-format
+msgid "%(key)s: %(value)s"
+msgstr ""
+
+#: ../roundup/admin.py:690
+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 ""
+"ç¨æ³ï¼display designator[,designator]*\n"
+" 顯示給åºçµé»ç屬æ§å¼ã\n"
+"\n"
+" å°é¡¯ç¤ºçµ¦åºçµé»ç屬æ§åç¸æçå¼ã\n"
+" "
+
+#: ../roundup/admin.py:714
+#, python-format
+msgid "%(key)s: %(value)r"
+msgstr ""
+
+#: ../roundup/admin.py:717
+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 ""
+"ç¨æ³ï¼create classname property=value ...\n"
+" 建ç«ä¸å給å®é¡çæ°è¨éã\n"
+"\n"
+" 建ç«ä¸å給å®é¡çæ°è¨éï¼å°ä½¿ç¨ \"create\" å½ä»¤è¡å¾é¢çå±¬æ§ name=value åæ¸ã\n"
+" "
+
+#: ../roundup/admin.py:744
+#, python-format
+msgid "%(propname)s (Password): "
+msgstr "%(propname)s (å£ä»¤)ï¼"
+
+#: ../roundup/admin.py:746
+#, python-format
+msgid " %(propname)s (Again): "
+msgstr " %(propname)s (å次)ï¼"
+
+#: ../roundup/admin.py:748
+msgid "Sorry, try again..."
+msgstr "æ±æï¼å試ä¸æ¬¡..."
+
+#: ../roundup/admin.py:752
+#, python-format
+msgid "%(propname)s (%(proptype)s): "
+msgstr ""
+
+#: ../roundup/admin.py:770
+#, python-format
+msgid "you must provide the \"%(propname)s\" property."
+msgstr "ä½ å¿
é æä¾ \"%(propname)s\" 屬æ§ã"
+
+#: ../roundup/admin.py:781
+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 ""
+"ç¨æ³ï¼list classname [property]\n"
+" ååºé¡åç實ä¾ã\n"
+"\n"
+" ååºææ給å®é¡åç實ä¾ãå¦æ屬æ§æªè¢«æå®ï¼åä½¿ç¨ \"label\" 屬æ§ã\n"
+" label 屬æ§ä»¥ä¸åé åºé²è¡å試ï¼éµã\"name\"ã\"title\" åæåå
¸é åº\n"
+" ç第ä¸å屬æ§ã\n"
+"\n"
+" å¦ææ²ææå®å±¬æ§ï¼ä½¿ç¨ -c, -S æ -s æåå°åºæ¢ç® id çå表ãå¦ææ\n"
+" å®äºå±¬æ§ï¼å°æ¯åé¡å實ä¾æåå°åºéå屬æ§ã\n"
+" "
+
+#: ../roundup/admin.py:794
+msgid "Too many arguments supplied"
+msgstr "æä¾äºå¤ªå¤çåæ¸äº"
+
+#: ../roundup/admin.py:830
+#, python-format
+msgid "%(nodeid)4s: %(value)s"
+msgstr ""
+
+#: ../roundup/admin.py:834
+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 ""
+"ç¨æ³ï¼table classname [property[,property]*]\n"
+" ä»¥è¡¨æ ¼ç表å¼ååºé¡åç實ä¾ã\n"
+"\n"
+" ååºçµ¦å®é¡åçææ實ä¾ãå¦ææ²ææå®å±¬æ§ï¼ææ屬æ§é½æ顯示åºä¾ã\n"
+" é è¨æ
æ³ä¸ï¼åç寬度æ¯æ大å¼ç寬度ãéå寬度ééå®ç¾©å±¬æ§çº \"name:width\"\n"
+" 被顯示å°å®ç¾©ãä¾å¦ï¼\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"
+" ä¹å¯ä»¥è®åç寬度çºæ¨ç±¤ç寬度ï¼å¨å±¬æ§ä¸æ²æ寬度å¼ãä¾å¦ï¼\n"
+"\n"
+" roundup> table priority id,name:\n"
+" Id Name\n"
+" 1 fata\n"
+" 2 bug\n"
+" 3 usab\n"
+" 4 feat\n"
+"\n"
+" å°çæ4åå符寬ç \"Name\" åã\n"
+" "
+
+#: ../roundup/admin.py:878
+#, python-format
+msgid "\"%(spec)s\" not name:width"
+msgstr "\"%(spec)s\" ä¸æ¯ åå:寬度"
+
+#: ../roundup/admin.py:928
+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 ""
+"ç¨æ³ï¼history designator\n"
+" 顯示æ示å¨(designator)çæ·å²è¨éã\n"
+"\n"
+" 顯示ç±æ示å¨(designator)ææççµé»çæ¥èªè¨éã\n"
+" "
+
+#: ../roundup/admin.py:949
+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 ""
+"ç¨æ³ï¼commit\n"
+" æ交å¨ä¸å交äºæ話ä¸æç¢ççæ¹åã\n"
+"\n"
+" å¨ä¸å交äºæ話ä¸æç¢ççæ¹åä¸æèªå寫å
¥æ¸æ庫 - å®åå¿
é 使ç¨æ¤å½ä»¤\n"
+" ä¾æ交ã\n"
+" å¨å½ä»¤è¡ä¸ç One-off å½ä»¤å¦ææåæ被èªåæ交ã\n"
+" "
+
+#: ../roundup/admin.py:963
+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 ""
+"ç¨æ³ï¼rollback\n"
+" æ¤é·æææªæ交å°æ¸æ庫çæ¹åã\n"
+"\n"
+" å¨äº¤äºå°è©±ä¸ç¢ççæ¹å並ä¸èªå寫å°æ¸æåº«ä¸ - å®åå¿
é 被æå·¥æ交ã\n"
+" éåå½ä»¤ç¨ä¾æ¤é·ææéäºæ¹åï¼æ以å¨å¾é¢è·ä¸æ交ç話ä¸æå°æ¸æ庫\n"
+" ç¢çè®åã\n"
+" "
+
+#: ../roundup/admin.py:975
+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 ""
+"ç¨æ³ï¼retire designator[,designator]*\n"
+" åæ¶ç±æ示å¨(designator)æææççµé»ã\n"
+"\n"
+" éååä½ææä¸åç¹å¥ççµé»å°ä¸è½è¢« list æ find å½ä»¤å¾å°ï¼ä¸¦ä¸\n"
+" å®çéµå¼å¯ä»¥è¢«éç¨ã\n"
+" "
+
+#: ../roundup/admin.py:998
+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 ""
+"Usage: restore designator[,designator]*\n"
+" æ¢å¾©ç±æ示å¨(designator)表æçå·²ç¶åæ¶ççµé»ã\n"
+"\n"
+" 給å®ççµé»å°å°ç¨æ¶ä¾èªªå次çæã\n"
+" "
+
+#. grab the directory to export to
+#: ../roundup/admin.py:1020
+msgid ""
+"Usage: export [class[,class]] export_dir\n"
+" Export the database to colon-separated-value files.\n"
+"\n"
+" Optionally limit the export to just the names classes.\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 ""
+"ç¨æ³ï¼export [class[,class]] export_dir\n"
+" å°åºæ¸æ庫çºåèåéå¼çæ件ã\n"
+"\n"
+" å°æ¼å°åºçå¯é¸éå¶åªæ¯é¡åã\n"
+"\n"
+" éååä½å¾æ¸æ庫ä¸å°åºç¶åçæ¸æå°ä»¥åèåéå¼çæ件ä¸å»ï¼å®åå°å\n"
+" æ¾å¨æå®çç®æ¨ç®éä¸ã\n"
+" "
+
+#: ../roundup/admin.py:1073
+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 ""
+"ç¨æ³ï¼import import_dir\n"
+" å¾å
å« CSV æ件çç®éä¸å°å
¥æ¸æ庫ï¼ä¸åé¡æå
©åæ件ç¨æ¼å°å
¥ã\n"
+"\n"
+" ç¨æ¼å°å
¥çæ件çºï¼\n"
+"\n"
+" <class>.csv\n"
+" å®å¿
é å®ç¾©èé¡åä¸æ¨£ç屬æ§(å
æ¬ä¸å \"header\" è¡å
å«é£äº\n"
+" 屬æ§çååã)\n"
+" <class>-journals.csv\n"
+" å®ç¨ä¾å®ç¾©è¢«å°å
¥çæ¢ç®çæ¥èªã\n"
+"\n"
+" 被å°å
¥ççµé»å°å
·èå¨å°å
¥æ件ä¸ä¸æ¨£ççµé»idï¼ä»¥ä¾¿å¯ä»¥æ¿æä»»ä½\n"
+" ä»»ä½å·²ç¶åå¨çå
§å®¹ã\n"
+" æ°çµé»è¢«å å
¥å°å·²ç¶åå¨çæ¸æåº«ä¸ - å¦æä½ æ³è¦ä½¿ç¨å°å
¥æ¸æä¾å»º\n"
+" ç«ä¸åæ°çæ¸æ庫ï¼é£éº¼åµå»ºä¸åæ°æ¸æ庫(æè
ï¼éº»ç
©é»ï¼åæ¶ææ\n"
+" èæ¸æã)\n"
+" "
+
+#: ../roundup/admin.py:1141
+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 ""
+"ç¨æ³ï¼pack period | date\n"
+"\n"
+" åªé¤æ©æ¼æå®çææææ¥æçèçæµæ°´è¨éã\n"
+"\n"
+" ä¸åææ使ç¨å¾ç¶´ \"y\", \"m\", å \"d\"ãå¾ç¶´ \"w\"(表示 \"week\")\n"
+" 表示 7 天ã\n"
+"\n"
+" \"3y\" 表示3年\n"
+" \"2y 1m\" 表示2å¹´1åæ\n"
+" \"1m 25d\" 表示1æ25天\n"
+" \"2w 3d\" 表示2å¨3天\n"
+"\n"
+" æ¥ææ ¼å¼æ¯ \"YYYY-MM-DD\" ä¾å¦ï¼\n"
+" 2001-01-01\n"
+"\n"
+" "
+
+#: ../roundup/admin.py:1169
+msgid "Invalid format"
+msgstr "ç¡æçæ ¼å¼"
+
+#: ../roundup/admin.py:1179
+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 ""
+"ç¨æ³ï¼reindex [classname|designator]*\n"
+" éæ°çæ tracker çæ索索å¼ã\n"
+"\n"
+" éæ°çæ tracker çæ索索å¼ï¼å®å°èªåé²è¡ã\n"
+" "
+
+#: ../roundup/admin.py:1193
+#, python-format
+msgid "no such item \"%(designator)s\""
+msgstr "æ²æé樣çæ¢ç® \"%(designator)s\""
+
+#: ../roundup/admin.py:1203
+msgid ""
+"Usage: security [Role name]\n"
+" Display the Permissions available to one or all Roles.\n"
+" "
+msgstr ""
+"ç¨æ³ï¼security [è§è²å]\n"
+" 顯示ä¸åæå¤åè§è²çæ¬éã\n"
+" "
+
+#: ../roundup/admin.py:1211
+#, python-format
+msgid "No such Role \"%(role)s\""
+msgstr "æ²æé樣çè§è² \"%(role)s\""
+
+#: ../roundup/admin.py:1217
+#, python-format
+msgid "New Web users get the Roles \"%(role)s\""
+msgstr "æ°Webç¨æ¶å¾å°è§è² \"%(role)s\""
+
+#: ../roundup/admin.py:1219
+#, python-format
+msgid "New Web users get the Role \"%(role)s\""
+msgstr "æ°Webç¨æ¶å¾å°è§è² \"%(role)s\""
+
+#: ../roundup/admin.py:1222
+#, python-format
+msgid "New Email users get the Roles \"%(role)s\""
+msgstr "æ°éµä»¶ç¨æ¶å¾å°è§è² \"%(role)s\""
+
+#: ../roundup/admin.py:1224
+#, python-format
+msgid "New Email users get the Role \"%(role)s\""
+msgstr "æ°éµä»¶ç¨æ¶å¾å°è§è² \"%(role)s\""
+
+#: ../roundup/admin.py:1227
+#, python-format
+msgid "Role \"%(name)s\":"
+msgstr "è§è² \"%(name)s\"ï¼"
+
+#: ../roundup/admin.py:1230
+#, python-format
+msgid " %(description)s (%(name)s for \"%(klass)s\" only)"
+msgstr " %(description)s (%(name)s å
ç¨æ¼ \"%(klass)s\")"
+
+#: ../roundup/admin.py:1233
+#, python-format
+msgid " %(description)s (%(name)s)"
+msgstr ""
+
+#: ../roundup/admin.py:1259
+#, python-format
+msgid "Unknown command \"%(command)s\" (\"help commands\" for a list)"
+msgstr "æªç¥å½ä»¤ \"%(command)s\" (\"help commands\" æ¥çå½ä»¤å表)"
+
+#: ../roundup/admin.py:1265
+#, python-format
+msgid "Multiple commands match \"%(command)s\": %(list)s"
+msgstr "å¤å½ä»¤å¹é
\"%(command)s\": %(list)s"
+
+#: ../roundup/admin.py:1272
+msgid "Enter tracker home: "
+msgstr "輸å
¥trackerèµ·å§ç®éï¼"
+
+# ../roundup/admin.py:1279 :1285 :1305
+#: ../roundup/admin.py:1279
+#: ../roundup/admin.py:1285
+#: ../roundup/admin.py:1305
+#, python-format
+msgid "Error: %(message)s"
+msgstr "é¯èª¤ï¼%(message)s"
+
+#: ../roundup/admin.py:1293
+#, python-format
+msgid "Error: Couldn't open tracker: %(message)s"
+msgstr "é¯èª¤ï¼ä¸è½æétrackerï¼%(message)s"
+
+#: ../roundup/admin.py:1318
+#, python-format
+msgid ""
+"Roundup %s ready for input.\n"
+"Type \"help\" for help."
+msgstr ""
+"Roundup %s 輸å
¥å°±ç·ã\n"
+"æ²å
¥ \"help\" ç²å¾èªªæã"
+
+#: ../roundup/admin.py:1323
+msgid "Note: command history and editing not available"
+msgstr "注æï¼å½ä»¤æ·å²å編輯ç¡æ"
+
+#: ../roundup/admin.py:1327
+msgid "roundup> "
+msgstr ""
+
+#: ../roundup/admin.py:1329
+msgid "exit..."
+msgstr "éåº..."
+
+#: ../roundup/admin.py:1339
+msgid "There are unsaved changes. Commit them (y/N)? "
+msgstr "åå¨æªè¢«ä¿åçæ¹åãæ交å(y/N)ï¼"
+
+#: ../roundup/backends/rdbms_common.py:1420
+msgid "create"
+msgstr "建ç«"
+
+#: ../roundup/backends/rdbms_common.py:1583
+msgid "unlink"
+msgstr "解é¤"
+
+#: ../roundup/backends/rdbms_common.py:1587
+msgid "link"
+msgstr "éæ¥"
+
+#: ../roundup/backends/rdbms_common.py:1696
+msgid "set"
+msgstr "è¨ç½®"
+
+#: ../roundup/backends/rdbms_common.py:1720
+msgid "retired"
+msgstr "æ¶å"
+
+#: ../roundup/backends/rdbms_common.py:1750
+msgid "restored"
+msgstr "æ¢å¾©"
+
+#: ../roundup/cgi/actions.py:53
+#, python-format
+msgid "You do not have permission to %(action)s the %(classname)s class."
+msgstr "ä½ æ²ææ¬éä¾ %(action)s %(classname)s é¡åã"
+
+#: ../roundup/cgi/actions.py:81
+msgid "No type specified"
+msgstr "æ²ææå®é¡å"
+
+#: ../roundup/cgi/actions.py:83
+msgid "No ID entered"
+msgstr "æ²æ輸å
¥ID"
+
+#: ../roundup/cgi/actions.py:89
+#, python-format
+msgid "\"%(input)s\" is not an ID (%(classname)s ID required)"
+msgstr "\"%(input)s\" ä¸æ¯ä¸å ID (è¦æ± %(classname)s ID)"
+
+#: ../roundup/cgi/actions.py:109
+msgid "You may not retire the admin or anonymous user"
+msgstr "ä½ ä¸è½åªé¤ç®¡çå¡æå¿åç¨æ¶"
+
+#: ../roundup/cgi/actions.py:116
+#, python-format
+msgid "%(classname)s %(itemid)s has been retired"
+msgstr "%(classname)s %(itemid)s å·²ç¶è¢«åæ¶äº"
+
+#: ../roundup/cgi/actions.py:271
+#, python-format
+msgid "Not enough values on line %(line)s"
+msgstr "å¨ %(line)s è¡æ²æè¶³å¤ çå¼"
+
+#: ../roundup/cgi/actions.py:318
+msgid "Items edited OK"
+msgstr "é
ç®ç·¨è¼¯æå"
+
+#: ../roundup/cgi/actions.py:377
+#, python-format
+msgid "%(class)s %(id)s %(properties)s edited ok"
+msgstr "%(class)s %(id)s %(properties)s 編輯æå"
+
+#: ../roundup/cgi/actions.py:380
+#, python-format
+msgid "%(class)s %(id)s - nothing changed"
+msgstr "%(class)s %(id)s - æ²ææ¹å"
+
+#: ../roundup/cgi/actions.py:392
+#, python-format
+msgid "%(class)s %(id)s created"
+msgstr "%(class)s %(id)s 被建ç«"
+
+#: ../roundup/cgi/actions.py:424
+#, python-format
+msgid "You do not have permission to edit %(class)s"
+msgstr "ä½ æ²ææ¬éä¾ç·¨è¼¯ %(class)s"
+
+#: ../roundup/cgi/actions.py:436
+#, python-format
+msgid "You do not have permission to create %(class)s"
+msgstr "ä½ æ²ææ¬éä¾å»ºç« %(class)s"
+
+#: ../roundup/cgi/actions.py:459
+msgid "You do not have permission to edit user roles"
+msgstr "ä½ æ²æ編輯ç¨æ¶æè§è²çæ¬é"
+
+#: ../roundup/cgi/actions.py:518
+#, python-format
+msgid "Edit Error: %s"
+msgstr "編輯é¯èª¤ï¼%s"
+
+# ../roundup/cgi/actions.py:549 :559 :730 :749
+#: ../roundup/cgi/actions.py:549
+#: ../roundup/cgi/actions.py:559
+#: ../roundup/cgi/actions.py:730
+#: ../roundup/cgi/actions.py:749
+#, python-format
+msgid "Error: %s"
+msgstr "é¯èª¤ï¼%s"
+
+#: ../roundup/cgi/actions.py:585
+msgid ""
+"Invalid One Time Key!\n"
+"(a Mozilla bug may cause this message to show up erroneously, please check your email)"
+msgstr ""
+"Invalid One Time Key!\n"
+"(ä¸å Mozilla çé¯èª¤å¯è½æé¯èª¤å°å¼ç¼éåæ¶æ¯ï¼ä½ 檢æ¥ä½ çéµä»¶)"
+
+#: ../roundup/cgi/actions.py:627
+#, python-format
+msgid "Password reset and email sent to %s"
+msgstr "å£ä»¤è¢«éè¨ï¼éµä»¶è¢«ç¼çµ¦ %s"
+
+#: ../roundup/cgi/actions.py:636
+msgid "Unknown username"
+msgstr "æªç¥ç¨æ¶å"
+
+#: ../roundup/cgi/actions.py:644
+msgid "Unknown email address"
+msgstr "æªç¥éµä»¶å°å"
+
+#: ../roundup/cgi/actions.py:649
+msgid "You need to specify a username or address"
+msgstr "ä½ éè¦æå®ç¨æ¶åæå°å"
+
+#: ../roundup/cgi/actions.py:674
+#, python-format
+msgid "Email sent to %s"
+msgstr "éµä»¶ç¼çµ¦ %s"
+
+#: ../roundup/cgi/actions.py:693
+msgid "You are now registered, welcome!"
+msgstr "ä½ å·²ç¶è¨»åï¼æ¡è¿ï¼"
+
+#: ../roundup/cgi/actions.py:738
+msgid "It is not permitted to supply roles at registration."
+msgstr "ä¸å
許å¨è¨»åææä¾è§è²ã"
+
+#: ../roundup/cgi/actions.py:820
+msgid "You are logged out"
+msgstr "ä½ å·²ç¶è¨»é·"
+
+#: ../roundup/cgi/actions.py:831
+msgid "Username required"
+msgstr "éè¦ç¨æ¶å"
+
+#: ../roundup/cgi/actions.py:846
+msgid "Ivalid login"
+msgstr "ç¡æç»é"
+
+#: ../roundup/cgi/actions.py:853
+msgid "Invalid login"
+msgstr "ç¡æç»é"
+
+#: ../roundup/cgi/actions.py:861
+msgid "You do not have permission to login"
+msgstr "ä½ æ²æç»éçæ¬é"
+
+#: ../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>模æ¿é¯èª¤</h1>\n"
+"<p><b>%(exc_type)s</b>: %(exc_value)s</p>\n"
+"<p class=\"help\">調試信æ¯çº</p>"
+
+#: ../roundup/cgi/cgitb.py:64
+#, python-format
+msgid "<li>\"%(name)s\" (%(info)s)</li>"
+msgstr ""
+
+#: ../roundup/cgi/cgitb.py:67
+#, python-format
+msgid "<li>Looking for \"%(name)s\", current path:<ol>%(path)s</ol></li>"
+msgstr "<li>æ¥æ¾ \"%(name)s\", ç¶åè·¯å¾ï¼<ol>%(path)s</ol></li>"
+
+#: ../roundup/cgi/cgitb.py:71
+#, python-format
+msgid "<li>In %s</li>"
+msgstr "<li>å¨ %s</li>"
+
+#: ../roundup/cgi/cgitb.py:76
+#, python-format
+msgid "A problem occurred in your template \"%s\"."
+msgstr "å¨ä½ çæ¨¡æ¿ \"%s\" ä¸ç¼çä¸ååé¡ã"
+
+#: ../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>å¨ %(line)d è¡è¨ç® %(info)r 表éå¼\n"
+"<table class=\"otherinfo\" style=\"font-size: 90%%\">\n"
+" <tr><th colspan=\"2\" class=\"header\">ç¶åè®éï¼</th></tr>\n"
+" %(globals)s\n"
+" %(locals)s\n"
+"</table></li>\n"
+
+#: ../roundup/cgi/cgitb.py:103
+msgid "Full traceback:"
+msgstr "å®æ´è·è¹¤ä¿¡æ¯ï¼"
+
+#: ../roundup/cgi/cgitb.py:116
+#, python-format
+msgid "<font size=+1><strong>%(exc_type)s</strong>: %(exc_value)s</font>"
+msgstr ""
+
+#: ../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>å¨éè¡ Python è
³æ¬æç¼çäºä¸åé¯èª¤ãéæ¯å°è´åºé¯çä¸ç³»åçå½æ¸èª¿ç¨ï¼æè¿ç(æ裡層ç)調ç¨å¨åãç°å¸¸å±¬æ§æ¯ï¼"
+
+#: ../roundup/cgi/cgitb.py:129
+msgid "<file is None - probably inside <tt>eval</tt> or <tt>exec</tt>>"
+msgstr "<æä»¶çº None - å¯è½å¨ <tt>eval</tt> æè
<tt>exec</tt>>"
+
+#: ../roundup/cgi/cgitb.py:138
+#, python-format
+msgid "in <strong>%s</strong>"
+msgstr "å¨ <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>æªå®ç¾©</em>"
+
+#: ../roundup/cgi/client.py:273
+msgid "Form Error: "
+msgstr "è¡¨æ ¼é¯èª¤ï¼"
+
+#: ../roundup/cgi/client.py:323
+#, python-format
+msgid "Unrecognized charset: %r"
+msgstr "ç¡æ³èå¥çå符éï¼%r"
+
+#: ../roundup/cgi/client.py:398
+msgid "Anonymous users are not allowed to use the web interface"
+msgstr "å¿åç¨æ¶ä¸å
許使ç¨webçé¢"
+
+#: ../roundup/cgi/form_parser.py:283
+#, python-format
+msgid "link \"%(key)s\" value \"%(value)s\" not a designator"
+msgstr "éæ¥ \"%(key)s\" çå¼ \"%(value)s\" ä¸æ¯ä¸å æ示å¨(designator)"
+
+#: ../roundup/cgi/form_parser.py:290
+#, python-format
+msgid "%(class)s %(property)s is not a link or multilink property"
+msgstr "%(class)s %(property)s ä¸æ¯ä¸å Link æ MultiLink 屬æ§"
+
+#: ../roundup/cgi/form_parser.py:312
+#, python-format
+msgid "You have submitted a %(action)s action for the property \"%(property)s\" which doesn't exist"
+msgstr "ä½ æ交äºä¸åå°æ¼ä¸åå¨å±¬æ§ \"%(property)s\" çä¸åæä½ %(action)s"
+
+# ../roundup/cgi/form_parser.py:331 :357
+#: ../roundup/cgi/form_parser.py:331
+#: ../roundup/cgi/form_parser.py:357
+#, python-format
+msgid "You have submitted more than one value for the %s property"
+msgstr "ä½ éè¦æ交éå° %s 屬æ§çä¸å以ä¸çå¼"
+
+# ../roundup/cgi/form_parser.py:354 :360
+#: ../roundup/cgi/form_parser.py:354
+#: ../roundup/cgi/form_parser.py:360
+msgid "Password and confirmation text do not match"
+msgstr "å£ä»¤å確èªææ¬ä¸å¹é
"
+
+#: ../roundup/cgi/form_parser.py:395
+#, python-format
+msgid "property \"%(propname)s\": \"%(value)s\" not currently in list"
+msgstr "å±¬æ§ \"%(propname)s\": \"%(value)s\" ç¶åä¸å¨å表ä¸"
+
+#: ../roundup/cgi/form_parser.py:509
+#, python-format
+msgid "Required %(class)s property %(property)s not supplied"
+msgstr "è¦æ±ç %(class)s å±¬æ§ %(property)s æ²æ被æä¾"
+
+#: ../roundup/cgi/form_parser.py:529
+msgid "File is empty"
+msgstr "æ件çºç©º"
+
+#: ../roundup/cgi/templating.py:68
+#, python-format
+msgid "You are not allowed to %(action)s items of class %(class)s"
+msgstr "ä½ ä¸å
許 %(action)s é¡å¥ %(class)s çé
ç®"
+
+#: ../roundup/cgi/templating.py:598
+msgid "(list)"
+msgstr "(å表)"
+
+#: ../roundup/cgi/templating.py:632
+msgid "Submit New Entry"
+msgstr "æ交æ°çé
"
+
+#: ../roundup/cgi/templating.py:644
+msgid "New node - no history"
+msgstr "æ°è¨é - ç¡æ·å²"
+
+#: ../roundup/cgi/templating.py:744
+msgid "Submit Changes"
+msgstr "æ交è®å"
+
+#: ../roundup/cgi/templating.py:825
+msgid "<em>The indicated property no longer exists</em>"
+msgstr "<em>æ示ç屬æ§ä¸ååå¨</em>"
+
+#: ../roundup/cgi/templating.py:826
+#, python-format
+msgid "<em>%s: %s</em>\n"
+msgstr ""
+
+#: ../roundup/cgi/templating.py:839
+#, python-format
+msgid "The linked class %(classname)s no longer exists"
+msgstr "éæ¥çé¡å¥ %(classname)s ä¸ååå¨"
+
+# ../roundup/cgi/templating.py:872 :893
+#: ../roundup/cgi/templating.py:872
+#: ../roundup/cgi/templating.py:893
+msgid "<strike>The linked node no longer exists</strike>"
+msgstr "<strike>éæ¥ççµé»ä¸ååå¨</strike>"
+
+#: ../roundup/cgi/templating.py:932
+msgid "No"
+msgstr "å¦"
+
+#: ../roundup/cgi/templating.py:932
+msgid "Yes"
+msgstr "æ¯"
+
+#: ../roundup/cgi/templating.py:943
+#, python-format
+msgid "%s: (no value)"
+msgstr "%s: (ç¡å¼)"
+
+#: ../roundup/cgi/templating.py:955
+msgid "<strong><em>This event is not handled by the history display!</em></strong>"
+msgstr "<strong><em>éåäºä»¶ä¸è½è¢«æ·å²é¡¯ç¤ºæèçï¼</em></strong>"
+
+#: ../roundup/cgi/templating.py:967
+msgid "<tr><td colspan=4><strong>Note:</strong></td></tr>"
+msgstr "<tr><td colspan=4><strong>注æï¼</strong></td></tr>"
+
+#: ../roundup/cgi/templating.py:976
+msgid "History"
+msgstr "æ·å²"
+
+#: ../roundup/cgi/templating.py:978
+msgid "<th>Date</th>"
+msgstr "<th>æ¥æ</th>"
+
+#: ../roundup/cgi/templating.py:979
+msgid "<th>User</th>"
+msgstr "<th>ç¨æ¶</th>"
+
+#: ../roundup/cgi/templating.py:980
+msgid "<th>Action</th>"
+msgstr "<th>åä½</th>"
+
+#: ../roundup/cgi/templating.py:981
+msgid "<th>Args</th>"
+msgstr "<th>åæ¸</th>"
+
+#: ../roundup/cgi/templating.py:1221
+msgid "*encrypted*"
+msgstr "*å å¯ç*"
+
+#: ../roundup/cgi/templating.py:1386
+msgid "default value for DateHTMLProperty must be either DateHTMLProperty or string date representation."
+msgstr "DateHTMLProperty çé è¨å¼æè
æ¯ DateHTMLProperty æå符串çæ¥æ表示ã"
+
+#: ../roundup/cgi/templating.py:1571
+#, python-format
+msgid "<option %svalue=\"-1\">- no selection -</option>"
+msgstr "<option %svalue=\"-1\">- æªé¸æ -</option>"
+
+#: ../roundup/date.py:180
+#, python-format
+msgid "Not a date spec: %s"
+msgstr "ä¸æ¯æ¥ææ ¼å¼ï¼%s"
+
+#: ../roundup/date.py:231
+#, python-format
+msgid "%r not a date spec (%s)"
+msgstr "%r ä¸æ¯æ¥ææ ¼å¼ (%s)"
+
+#: ../roundup/date.py:522
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]"
+msgstr "ä¸æ¯æéééè¦ç¯ï¼[+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [æ¥æè¦ç¯]"
+
+#: ../roundup/date.py:541
+msgid "Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+msgstr "ä¸æ¯æéééè¦ç¯ï¼[+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]"
+
+#: ../roundup/date.py:678
+#, python-format
+msgid "%(number)s year"
+msgstr "%(number)så¹´"
+
+#: ../roundup/date.py:682
+#, python-format
+msgid "%(number)s month"
+msgstr "%(number)sæ"
+
+#: ../roundup/date.py:686
+#, python-format
+msgid "%(number)s week"
+msgstr "%(number)så¨"
+
+#: ../roundup/date.py:690
+#, python-format
+msgid "%(number)s day"
+msgstr "%(number)s天"
+
+#: ../roundup/date.py:694
+msgid "tomorrow"
+msgstr "æ天"
+
+#: ../roundup/date.py:696
+msgid "yesterday"
+msgstr "æ¨å¤©"
+
+#: ../roundup/date.py:699
+#, python-format
+msgid "%(number)s hour"
+msgstr "%(number)så°æ"
+
+#: ../roundup/date.py:703
+msgid "an hour"
+msgstr "1å°æ"
+
+#: ../roundup/date.py:705
+msgid "1 1/2 hours"
+msgstr "1ååå°æ"
+
+#: ../roundup/date.py:707
+#, python-format
+msgid "1 %(number)s/4 hours"
+msgstr "1 %(number)s/4 å°æ"
+
+#: ../roundup/date.py:711
+msgid "in a moment"
+msgstr "ä¸æå
"
+
+#: ../roundup/date.py:713
+msgid "just now"
+msgstr "åæ"
+
+#: ../roundup/date.py:716
+msgid "1 minute"
+msgstr "1åé"
+
+#: ../roundup/date.py:719
+#, python-format
+msgid "%(number)s minute"
+msgstr "%(number)såé"
+
+#: ../roundup/date.py:722
+msgid "1/2 an hour"
+msgstr "åå°æ"
+
+#: ../roundup/date.py:724
+#, python-format
+msgid "%(number)s/4 hour"
+msgstr "%(number)s/4 å°æ"
+
+#: ../roundup/date.py:728
+#, python-format
+msgid "%s ago"
+msgstr "%s ä¹å"
+
+#: ../roundup/date.py:730
+#, python-format
+msgid "in %s"
+msgstr "å¨ %s"
+
+#: ../roundup/roundupdb.py:130
+msgid "files"
+msgstr "æ件"
+
+#: ../roundup/roundupdb.py:130
+msgid "messages"
+msgstr "ä¿¡æ¯"
+
+#: ../roundup/roundupdb.py:130
+msgid "nosy"
+msgstr "éäº"
+
+#: ../roundup/roundupdb.py:130
+msgid "superseder"
+msgstr "延æ"
+
+#: ../roundup/roundupdb.py:130
+msgid "title"
+msgstr "æ¨é¡"
+
+#: ../roundup/roundupdb.py:131
+msgid "assignedto"
+msgstr "åé
給"
+
+#: ../roundup/roundupdb.py:131
+msgid "priority"
+msgstr "åªå
ç´"
+
+#: ../roundup/roundupdb.py:131
+msgid "status"
+msgstr "çæ
"
+
+#: ../roundup/roundupdb.py:131
+msgid "topic"
+msgstr "主é¡"
+
+#: ../roundup/roundupdb.py:134
+msgid "activity"
+msgstr "æ´»èºåº¦"
+
+#. following properties are common for all hyperdb classes
+#. they are listed here to keep things in one place
+#: ../roundup/roundupdb.py:134
+msgid "actor"
+msgstr "å·è¡äºº"
+
+#: ../roundup/roundupdb.py:134
+msgid "creation"
+msgstr "建ç«"
+
+#: ../roundup/roundupdb.py:134
+msgid "creator"
+msgstr "建ç«è
"
+
+#: ../roundup/scripts/roundup_demo.py:32
+#, python-format
+msgid "Enter directory path to create demo tracker [%s]: "
+msgstr "輸å
¥ç®éä¾å»ºç«æ¼ç¤ºtracker [%s]ï¼"
+
+#: ../roundup/scripts/roundup_mailgw.py:36
+#, python-format
+msgid ""
+"Usage: %(program)s [-v] [-c] [[-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"
+"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 ""
+"ç¨æ³ï¼%(program)s [-v] [-c] [[-C class] -S field=value]* <instance home> [method]\n"
+"\n"
+"é¸é
ï¼\n"
+" -v: åå°çæ¬ä¸¦ä¸éåº\n"
+" -c: ç¨ä¾å»ºç«æ¢ç®çé è¨é¡å(å
¶å®æ¯trackerçMAIL_DEFAULT_CLASS)\n"
+" -C / -S: çä¸é¢\n"
+"\n"
+"Roundup éµä»¶ç¶²éæ以å種æ¹å¼è¢«èª¿ç¨ï¼\n"
+" . 實ä¾èµ·å§ç®éä½çºå¯ä¸åæ¸ï¼\n"
+" . 實ä¾èµ·å§ç®éåéµä»¶è«æ©(spool)æ件ï¼\n"
+" . 實ä¾èµ·å§ç®éå POP/APOP æåå¨å¸³èï¼æè
\n"
+" . 實ä¾èµ·å§ç®éå IMAP/IMAPS æåå¨å¸³èã\n"
+"\n"
+"ä¹æ¯æ使ç¨å¯é¸ç -C æ -S åæ¸ï¼å®åå
è¨±ä½ çºroundup-mailgwæ建ç«çé¡\n"
+"è¨ç½®åãå¦ææ²ææå®ï¼åé è¨çé¡æ¯ msgï¼ä½æ¯å
¶å®çé¡ï¼issue, file, user\n"
+"ä¹å¯ä»¥ä½¿ç¨ã-S æ --set é¸é
ä½¿ç¨ property=value[;property=value] 表示æ³ï¼\n"
+"å®åå¯ä»¥è¢« Roundup å½ä»¤çå½ä»¤è¡æå¯ä»¥æå®ä¸å°éµä»¶ä¿¡æ¯æ¨é¡è¡çå½ä»¤ææ¥åã\n"
+"\n"
+"å®å¯ä»¥è®ä½ 給æ¯å°éµä»¶è¨ç½®ä¿¡æ¯çé¡åã\n"
+"\n"
+"PIPE:\n"
+" å¨ç¬¬ä¸ç¨®æ¹å¼ä¸ï¼éµä»¶ç¶²éå¾æ¨æºè¼¸å
¥è®åå®æ¢ä¿¡æ¯ï¼ä¸¦å°ä¿¡æ¯æ交給 roundup.mailgw\n"
+" 模å¡ã\n"
+"\n"
+"UNIX mailbox:\n"
+" å¨ç¬¬äºç¨®æ¹å¼ä¸ï¼ç¶²éå¾éµä»¶è«æ©æ件ä¸è®åææçä¿¡æ¯ï¼ä¸¦æé åºæ交給\n"
+" roundup.mailgw 模å¡ãä¸æ¦ææä¿¡æ¯è¢«æåèçï¼æ件被æ¸
空ãéåæ件被\n"
+" æå®çºï¼\n"
+" mailbox /path/to/mailbox\n"
+"\n"
+"POP:\n"
+" å¨ç¬¬ä¸ç¨®æ¹å¼ä¸ï¼ç¶²éå¾æå®ç POP æåå¨è®åºææä¿¡æ¯ï¼ä¸¦æé åºæ交å°\n"
+" roundup.mailgw 模å¡ãæåå¨è¢«æå®çºï¼\n"
+" pop username:password at server\n"
+" ç¨æ¶ååå£ä»¤å¯ä»¥è¢«çç¥ï¼\n"
+" pop username at server\n"
+" pop server\n"
+" é½æ¯ææçãå¦ææ²ææä¾ç¨æ¶åæå£ä»¤é½å°å¨å½ä»¤è¡è¢«æ示ã\n"
+"\n"
+"APOP:\n"
+" å POPï¼ä½ä½¿ç¨èªèç POPï¼\n"
+" apop username:password at server\n"
+"\n"
+"IMAP:\n"
+" è¯æ¥å° IMAP æåå¨ãå®æ¯æå POP éµä»¶ç¸åç寫æ³ã\n"
+" imap username:password at server\n"
+" é¤äº INBOX å¤éå
è¨±ä½ æå®ä¸åç¹å¥çéµç®±ï¼\n"
+" 使ç¨éç¨®æ ¼å¼ï¼ imap username:password at server mailbox\n"
+"\n"
+"IMAPS:\n"
+" éésslè¯æ¥å° IMAP æåå¨ã\n"
+" å®æ¯æå IMAP ä¸æ¨£ç寫æ³ã\n"
+" imaps username:password at server [mailbox]\n"
+"\n"
+
+#: ../roundup/scripts/roundup_mailgw.py:147
+msgid "Error: not enough source specification information"
+msgstr "é¯èª¤ï¼æ²æè¶³å¤ çæºåè°è³è¨"
+
+#: ../roundup/scripts/roundup_mailgw.py:157
+msgid "Error: pop specification not valid"
+msgstr "é¯èª¤ï¼popåè°ç¡æ"
+
+#: ../roundup/scripts/roundup_mailgw.py:164
+msgid "Error: apop specification not valid"
+msgstr "é¯èª¤ï¼apopåè°ç¡æ"
+
+#: ../roundup/scripts/roundup_mailgw.py:178
+msgid "Error: The source must be either \"mailbox\", \"pop\", \"apop\", \"imap\" or \"imaps\""
+msgstr "é¯èª¤ï¼æºå¿
é æ¯ \"mailbox\", \"pop\", \"apop\", \"imap\" æè
\"imaps\" ä¹ä¸"
+
+#: ../roundup/scripts/roundup_server.py:106
+msgid ""
+"<html><head><title>Roundup trackers index</title></head>\n"
+"<body><h1>Roundup trackers index</h1><ol>\n"
+msgstr ""
+"<html><head><title>Roundup tracker ç´¢å¼</title></head>\n"
+"<body><h1>Roundup tracker ç´¢å¼</h1><ol>\n"
+
+#: ../roundup/scripts/roundup_server.py:217
+#, python-format
+msgid "Error: %s: %s"
+msgstr "é¯èª¤ï¼%s: %s"
+
+#: ../roundup/scripts/roundup_server.py:325
+msgid ""
+" -c <Command> Windows Service options.\n"
+" If you want to run the server as a Windows Service, you\n"
+" must configure the rest of the options by changing the\n"
+" constants of this program. You will at least configure\n"
+" one tracker in the TRACKER_HOMES variable. This option\n"
+" is mutually exclusive from the rest. Typing\n"
+" \"roundup-server -c help\" shows Windows Services\n"
+" specifics."
+msgstr ""
+" -c <Command> Windows æåé¸é
ã\n"
+" å¦æä½ æ³æserverä½çºä¸åWindowsæåä¾éè¡ï¼ä½ å¿
é ééä¿®æ¹\n"
+" éåç¨åºç常éä¾é
ç½®æ¤é¸é
çå
¶å®å
§å®¹ãä½ è³å°éè¦å¨ TRACKER_HOMES\n"
+" è®éä¸é
ç½®ä¸åtrackerãéåé¸é
èå
¶ç¶é¸é
æ¯äºæ¥çãæå
¥\n"
+" \"roundup-server -c help\" ä¾ç解Windowsæåçè¦ç¯ã"
+
+#: ../roundup/scripts/roundup_server.py:334
+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 ""
+" -u <UID> 以éå UID ä¾éè¡ Roundup web æåå¨\n"
+" -g <GID> 以éå GID ä¾éè¡ Roundup web æåå¨\n"
+" -d <PIDfile> å¨å¾å°éè¡æåå¨ï¼ä¸¦ä¸å°æåå¨ç PID 寫å
¥æå®ç PIDFile ä¸å»ã\n"
+" å¦æ使ç¨äº -d é¸é
ï¼å -l é¸é
*å¿
é * è¦æå®ã"
+
+#: ../roundup/scripts/roundup_server.py:342
+#, python-format
+msgid ""
+"%(message)sUsage: roundup-server [options] [name=tracker home]*\n"
+"\n"
+"Options:\n"
+" -v prints the Roundup version number and exits\n"
+" -C <fname> use configuration file\n"
+" -n <name> sets the host name of the Roundup web server instance\n"
+" -p <port> sets 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"
+"%(os_part)s\n"
+"\n"
+"Examples:\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"
+" See the \"admin_guide\" in the Roundup \"doc\" directory.\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 ""
+"%(message)sç¨æ³ï¼roundup-server [options] [name=tracker home]*\n"
+"\n"
+"é¸é
ï¼\n"
+" -v åå° Roundup ççæ¬è並ä¸éåº\n"
+" -C <fname> 使ç¨é
ç½®æ件\n"
+" -n <name> è¨ç½® Roundup web æåå¨å¯¦ä¾ç主æ©å\n"
+" -p <port> è¨ç½®ç£è½ç«¯å£(é è¨ï¼%(port)s)\n"
+" -l <fname> å°æ¥èªè¼¸åºå°ç± fname æå®çæ件ä¸å»ï¼èä¸æ¯ æ¨æºé¯èª¤/æ¨æºè¼¸åº\n"
+" -N å°å®¢æ¶ç«¯æ©å¨çååèä¸æ¯IPå°åè¨éå°æ¥èªä¸å»(å¯è½ææ
¢é»)\n"
+"%(os_part)s\n"
+"\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"
+"é
ç½®æä»¶æ ¼å¼ï¼\n"
+" æ¥é±å¨Roundup \"doc\" ç®éä¸ç \"admin_guide\" ã\n"
+"\n"
+"å¦ä½ä½¿ç¨ \"name=tracker home\"ï¼\n"
+" éäºåæ¸ç¨ä¾è¨ç½®è¦ä½¿ç¨çtrackerçèµ·å§ç®éãname æå¨URLä¸ç¨ä¾\n"
+" å®ä½tracker(å®æ¯ URL è·¯å¾ç第ä¸é¨å)ãtracker home æ¯å¨ä½ å·è¡\n"
+" \"roundup-admin init\" æææå®çç®éãä½ å¯ä»¥å¨å½ä»¤è¡ä¸æå®ä»»\n"
+" ææ¸éç name=home å°ãè¦ç¢ºä¿ name é¨åä¸è½å
æ¬ä»»ä½éurlå®å
¨ç\n"
+" å符ï¼åç©ºæ ¼ï¼å çºå®åææIEæäºã\n"
+
+#: ../roundup/scripts/roundup_server.py:418
+msgid "WARNING: ignoring \"-g\" argument, not root"
+msgstr "è¦åï¼å¿½ç¥ \"-g\" åæ¸ï¼ä¸æ¯ root"
+
+#: ../roundup/scripts/roundup_server.py:424
+msgid "Can't change groups - no grp module"
+msgstr "ä¸è½ä¿®æ¹çµ - ç¡ grp 模å¡"
+
+#: ../roundup/scripts/roundup_server.py:433
+#, python-format
+msgid "Group %(group)s doesn't exist"
+msgstr "çµ %(group)s ä¸åå¨"
+
+#: ../roundup/scripts/roundup_server.py:444
+msgid "Can't run as root!"
+msgstr "ä¸è½ä»¥ root éè¡ï¼"
+
+#: ../roundup/scripts/roundup_server.py:447
+msgid "WARNING: ignoring \"-u\" argument, not root"
+msgstr "è¦åï¼å¿½ç¥ \"-u\" åæ¸ï¼ä¸æ¯ root"
+
+#: ../roundup/scripts/roundup_server.py:452
+msgid "Can't change users - no pwd module"
+msgstr "ä¸è½ä¿®æ¹ç¨æ¶ - ç¡ pwd 模å¡"
+
+#: ../roundup/scripts/roundup_server.py:461
+#, python-format
+msgid "User %(user)s doesn't exist"
+msgstr "ç¨æ¶ %(user)s ä¸åå¨"
+
+#: ../roundup/scripts/roundup_server.py:575
+msgid "Instances must be name=home"
+msgstr "實ä¾å¿
é æ¯ å¯¦ä¾å=實ä¾è·¯å¾"
+
+#: ../roundup/scripts/roundup_server.py:589
+#, python-format
+msgid "Configuration saved to %s"
+msgstr "é
ç½®ä¿åå° %s"
+
+#: ../roundup/scripts/roundup_server.py:606
+#, python-format
+msgid "Unable to bind to port %s, port already in use."
+msgstr "ç¡æ³ç¶å®å°ç«¯å£ %s, 端å£å·²ç¶è¢«ä½ç¨ã"
+
+#: ../roundup/scripts/roundup_server.py:625
+msgid "Sorry, you can't run the server as a daemon on this Operating System"
+msgstr "æ±æï¼å¨éåæä½ç³»çµ±ä¸ä¸è½ä»¥å®è·é²ç¨çæ¹å¼ä¾éè¡æå"
+
+#: ../roundup/scripts/roundup_server.py:639
+#, python-format
+msgid "Roundup server started on %(HOST)s:%(PORT)s"
+msgstr "Roundup server ååæ¼ %(HOST)s:%(PORT)s"
+
+#: ../templates/classic/html/_generic.collision.html:4
+#: ../templates/minimal/html/_generic.collision.html:4
+msgid "${class} Edit Collision - ${tracker}"
+msgstr "${class} 編輯è¡çª - ${tracker}"
+
+#: ../templates/classic/html/_generic.collision.html:7
+#: ../templates/minimal/html/_generic.collision.html:7
+msgid "${class} Edit Collision"
+msgstr "${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"
+" åå¨è¡çªãå¦ä¸åç¨æ¶å¨ä½ 編輯ææ´æ°äºæ¤æ¢è¨éã\n"
+" è« <a href='${context}'>éæ°è¼å
¥</a> è¨éæ¥çä½ ç編輯ã\n"
+
+#: ../templates/classic/html/_generic.help.html:9
+#: ../templates/minimal/html/_generic.help.html:9
+msgid "${property} help - ${tracker}"
+msgstr "${property} 說æ - ${tracker}"
+
+#: ../templates/classic/html/_generic.help.html:30
+#: ../templates/minimal/html/_generic.help.html:30
+msgid " Cancel "
+msgstr "åæ¶"
+
+#: ../templates/classic/html/_generic.help.html:33
+#: ../templates/minimal/html/_generic.help.html:33
+msgid " Apply "
+msgstr "æç¨"
+
+#: ../templates/classic/html/_generic.help.html:40
+#: ../templates/classic/html/issue.index.html:67
+#: ../templates/minimal/html/_generic.help.html:40
+msgid "<< previous"
+msgstr "<< åä¸"
+
+#: ../templates/classic/html/_generic.help.html:50
+#: ../templates/classic/html/issue.index.html:75
+#: ../templates/minimal/html/_generic.help.html:50
+msgid "${start}..${end} out of ${total}"
+msgstr "${start}..${end} å
¨é¨ ${total}"
+
+#: ../templates/classic/html/_generic.help.html:54
+#: ../templates/classic/html/issue.index.html:78
+#: ../templates/minimal/html/_generic.help.html:54
+msgid "next >>"
+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
+msgid "${class} editing - ${tracker}"
+msgstr "${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 "${class} 編輯"
+
+#: ../templates/classic/html/_generic.index.html:14
+#: ../templates/classic/html/_generic.item.html:12
+#: ../templates/classic/html/file.item.html:9
+#: ../templates/classic/html/issue.index.html:10
+#: ../templates/classic/html/user.index.html:9
+#: ../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 "ä½ ä¸å
許æ¥çæ¤é "
+
+#: ../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\"> ä½ å¯ä»¥ä½¿ç¨éåè¡¨æ ¼ä¾ç·¨è¼¯ ${classname} é¡å¥ã éèï¼æè¡åéå¼è(\")å¿
é 被å°å¿èçãä½ å¯ä»¥å¨éå¼è(\")ä¸å
å«éèåæè¡ãéå¼èæ¬èº«å¿
é 被å
©å(\"\")æå
æ¬ã</p> <p class=\"form-help\"> Multilink 屬æ§æå¤åå¼ï¼éäºå¼ç¨åè(\":\")åé(...ï¼\"ä¸:äº:ä¸\"ï¼...) </p> <p class=\"form-help\"> ééåªé¤å®åæå¨çè¡ä¾åªé¤é
ã追å ä¸æ¢æ°è¨éå°è¡¨ä¸ - å¨ id åç½®ä¸ä¸å X ã</p>"
+
+#: ../templates/classic/html/_generic.index.html:44
+#: ../templates/minimal/html/_generic.index.html:44
+msgid "Edit Items"
+msgstr "編輯é
ç®"
+
+#: ../templates/classic/html/file.index.html:4
+msgid "List of files - ${tracker}"
+msgstr "æ件å表 - ${tracker}"
+
+#: ../templates/classic/html/file.index.html:5
+msgid "List of files"
+msgstr "æ件å表"
+
+#: ../templates/classic/html/file.index.html:10
+msgid "Download"
+msgstr "ä¸è¼"
+
+#: ../templates/classic/html/file.index.html:11
+#: ../templates/classic/html/file.item.html:23
+#: ../templates/classic/html/file.item.html:51
+msgid "Content Type"
+msgstr "å
§å®¹é¡å"
+
+#: ../templates/classic/html/file.index.html:12
+msgid "Uploaded By"
+msgstr "ä¸å³ç±"
+
+#: ../templates/classic/html/file.index.html:13
+#: ../templates/classic/html/msg.item.html:38
+msgid "Date"
+msgstr "æ¥æ"
+
+#: ../templates/classic/html/file.item.html:2
+msgid "File display - ${tracker}"
+msgstr "æ件顯示 - ${tracker}"
+
+#: ../templates/classic/html/file.item.html:4
+msgid "File display"
+msgstr "æ件顯示"
+
+#: ../templates/classic/html/file.item.html:19
+#: ../templates/classic/html/file.item.html:47
+#: ../templates/classic/html/user.item.html:34
+#: ../templates/classic/html/user.register.html:17
+msgid "Name"
+msgstr "å§å"
+
+#: ../templates/classic/html/file.item.html:41
+msgid "download"
+msgstr "ä¸è¼"
+
+#: ../templates/classic/html/home.classlist.html:2
+#: ../templates/minimal/html/home.classlist.html:2
+msgid "List of classes - ${tracker}"
+msgstr "é¡å¥å表 - ${tracker}"
+
+#: ../templates/classic/html/home.classlist.html:4
+#: ../templates/minimal/html/home.classlist.html:4
+msgid "List of classes"
+msgstr "é¡å¥å表"
+
+#: ../templates/classic/html/issue.index.html:4
+msgid "List of issues - ${tracker}"
+msgstr "åé¡å表 - ${tracker}"
+
+#: ../templates/classic/html/issue.index.html:6
+msgid "List of issues"
+msgstr "åé¡å表"
+
+#: ../templates/classic/html/issue.index.html:17
+#: ../templates/classic/html/issue.item.html:38
+msgid "Priority"
+msgstr "åªå
ç´"
+
+#: ../templates/classic/html/issue.index.html:18
+msgid "ID"
+msgstr ""
+
+#: ../templates/classic/html/issue.index.html:19
+msgid "Creation"
+msgstr "建ç«æé"
+
+#: ../templates/classic/html/issue.index.html:20
+msgid "Activity"
+msgstr "æ´»èºåº¦"
+
+#: ../templates/classic/html/issue.index.html:21
+msgid "Actor"
+msgstr "å·è¡è
"
+
+#: ../templates/classic/html/issue.index.html:22
+msgid "Topic"
+msgstr "主é¡"
+
+#: ../templates/classic/html/issue.index.html:23
+#: ../templates/classic/html/issue.item.html:33
+msgid "Title"
+msgstr "æ¨é¡"
+
+#: ../templates/classic/html/issue.index.html:24
+#: ../templates/classic/html/issue.item.html:40
+msgid "Status"
+msgstr "çæ
"
+
+#: ../templates/classic/html/issue.index.html:25
+msgid "Creator"
+msgstr "建ç«è
"
+
+#: ../templates/classic/html/issue.index.html:26
+msgid "Assigned To"
+msgstr "åé
給"
+
+#: ../templates/classic/html/issue.index.html:90
+msgid "Download as CSV"
+msgstr "以CSVæ ¼å¼ä¸è¼"
+
+#: ../templates/classic/html/issue.index.html:98
+msgid "Sort on:"
+msgstr "æåºæï¼"
+
+#: ../templates/classic/html/issue.index.html:101
+#: ../templates/classic/html/issue.index.html:118
+msgid "- nothing -"
+msgstr "- ç¡ -"
+
+#: ../templates/classic/html/issue.index.html:109
+#: ../templates/classic/html/issue.index.html:126
+msgid "Descending:"
+msgstr "éåºï¼"
+
+#: ../templates/classic/html/issue.index.html:115
+msgid "Group on:"
+msgstr "åçµï¼"
+
+#: ../templates/classic/html/issue.index.html:132
+msgid "Redisplay"
+msgstr "å·æ°"
+
+#: ../templates/classic/html/issue.item.html:7
+msgid "Issue ${id}: ${title} - ${tracker}"
+msgstr "åé¡ ${id}: ${title} - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:10
+msgid "New Issue - ${tracker}"
+msgstr "æ°åé¡ - ${tracker}"
+
+#: ../templates/classic/html/issue.item.html:14
+msgid "New Issue"
+msgstr "æ°åé¡"
+
+#: ../templates/classic/html/issue.item.html:16
+msgid "New Issue Editing"
+msgstr "æ°åé¡ç·¨è¼¯"
+
+#: ../templates/classic/html/issue.item.html:19
+msgid "Issue${id}"
+msgstr "åé¡ [${id}]"
+
+#: ../templates/classic/html/issue.item.html:22
+msgid "Issue${id} Editing"
+msgstr "åé¡ [${id}] 編輯"
+
+#: ../templates/classic/html/issue.item.html:45
+msgid "Superseder"
+msgstr "æ¨é²"
+
+#: ../templates/classic/html/issue.item.html:50
+msgid "View: ${link}"
+msgstr "æ¥çï¼${link}"
+
+#: ../templates/classic/html/issue.item.html:54
+msgid "Nosy List"
+msgstr "éäºå表"
+
+#: ../templates/classic/html/issue.item.html:63
+msgid "Assigned To"
+msgstr "åé
給"
+
+#: ../templates/classic/html/issue.item.html:65
+msgid "Topics"
+msgstr "主é¡"
+
+#: ../templates/classic/html/issue.item.html:73
+msgid "Change Note"
+msgstr "ä¿®æ¹è¨é"
+
+#: ../templates/classic/html/issue.item.html:81
+msgid "File"
+msgstr "æ件"
+
+#: ../templates/classic/html/issue.item.html:100
+msgid "<table class=\"form\"> <tr> <td>Note: </td> <th class=\"required\">highlighted</th> <td> fields are required.</td> </tr> </table>"
+msgstr "<table class=\"form\"> <tr> <td>注æï¼ </td> <th class=\"required\">é«äº®</th> <td> å段æ¯å¿
é çã</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 "å¨ <b>${creation}</b> ç± <b>${creator}</b> 建ç«ï¼æå¾ç± <b>${actor}</b> ä¿®æ¹çº <b>${activity}</b>ã"
+
+#: ../templates/classic/html/issue.item.html:118
+#: ../templates/classic/html/msg.item.html:51
+msgid "Files"
+msgstr "æ件"
+
+#: ../templates/classic/html/issue.item.html:120
+#: ../templates/classic/html/msg.item.html:53
+msgid "File name"
+msgstr "æ件å"
+
+#: ../templates/classic/html/issue.item.html:121
+#: ../templates/classic/html/msg.item.html:54
+msgid "Uploaded"
+msgstr "å·²ä¸å³"
+
+#: ../templates/classic/html/issue.item.html:122
+msgid "Type"
+msgstr "é¡å"
+
+#: ../templates/classic/html/issue.item.html:123
+#: ../templates/classic/html/query.edit.html:30
+msgid "Edit"
+msgstr "編輯"
+
+#: ../templates/classic/html/issue.item.html:124
+msgid "Remove"
+msgstr "åªé¤"
+
+#: ../templates/classic/html/issue.item.html:144
+#: ../templates/classic/html/issue.item.html:165
+#: ../templates/classic/html/query.edit.html:50
+msgid "remove"
+msgstr "åªé¤"
+
+#: ../templates/classic/html/issue.item.html:151
+#: ../templates/classic/html/msg.index.html:9
+msgid "Messages"
+msgstr "ä¿¡æ¯"
+
+#: ../templates/classic/html/issue.item.html:155
+msgid "msg${id} (view)"
+msgstr "msg${id} (æ¥ç)"
+
+#: ../templates/classic/html/issue.item.html:156
+msgid "Author: ${author}"
+msgstr "ä½è
ï¼${author}"
+
+#: ../templates/classic/html/issue.item.html:158
+msgid "Date: ${date}"
+msgstr "æ¥æï¼${date}"
+
+#: ../templates/classic/html/issue.search.html:2
+msgid "Issue searching - ${tracker}"
+msgstr "åé¡æç´¢ - ${tracker}"
+
+#: ../templates/classic/html/issue.search.html:4
+msgid "Issue searching"
+msgstr "åé¡æç´¢"
+
+#: ../templates/classic/html/issue.search.html:25
+msgid "Filter on"
+msgstr "é濾æ"
+
+#: ../templates/classic/html/issue.search.html:26
+msgid "Display"
+msgstr "顯示"
+
+#: ../templates/classic/html/issue.search.html:27
+msgid "Sort on"
+msgstr "æåºæã"
+
+#: ../templates/classic/html/issue.search.html:28
+msgid "Group on"
+msgstr "åçµæ"
+
+#: ../templates/classic/html/issue.search.html:32
+msgid "All text*:"
+msgstr "ææææ¬*"
+
+#: ../templates/classic/html/issue.search.html:40
+msgid "Title:"
+msgstr "æ¨é¡ï¼"
+
+#: ../templates/classic/html/issue.search.html:50
+msgid "Topic:"
+msgstr "主é¡ï¼"
+
+#: ../templates/classic/html/issue.search.html:58
+msgid "ID:"
+msgstr ""
+
+#: ../templates/classic/html/issue.search.html:66
+msgid "Creation Date:"
+msgstr "建ç«æéï¼"
+
+#: ../templates/classic/html/issue.search.html:77
+msgid "Creator:"
+msgstr "建ç«è
ï¼"
+
+#: ../templates/classic/html/issue.search.html:79
+msgid "created by me"
+msgstr "ç±æ建ç«"
+
+#: ../templates/classic/html/issue.search.html:88
+msgid "Activity:"
+msgstr "æ´»èºåº¦ï¼"
+
+#: ../templates/classic/html/issue.search.html:99
+msgid "Actor:"
+msgstr "å·è¡äººï¼"
+
+#: ../templates/classic/html/issue.search.html:101
+msgid "done by me"
+msgstr "ç±æå®æ"
+
+#: ../templates/classic/html/issue.search.html:112
+msgid "Priority:"
+msgstr "åªå
ç´ï¼"
+
+#: ../templates/classic/html/issue.search.html:114
+#: ../templates/classic/html/issue.search.html:130
+msgid "not selected"
+msgstr "æªé¸æ"
+
+#: ../templates/classic/html/issue.search.html:125
+msgid "Status:"
+msgstr "çæ
ï¼"
+
+#: ../templates/classic/html/issue.search.html:128
+msgid "not resolved"
+msgstr "æªè§£æ±º"
+
+#: ../templates/classic/html/issue.search.html:143
+msgid "Assigned to:"
+msgstr "åé
給ï¼"
+
+#: ../templates/classic/html/issue.search.html:146
+msgid "assigned to me"
+msgstr "åé
給æ"
+
+#: ../templates/classic/html/issue.search.html:148
+msgid "unassigned"
+msgstr "æªåé
"
+
+#: ../templates/classic/html/issue.search.html:158
+msgid "Pagesize:"
+msgstr "é 大å°ï¼"
+
+#: ../templates/classic/html/issue.search.html:164
+msgid "Start With:"
+msgstr "éå§å¨ï¼"
+
+#: ../templates/classic/html/issue.search.html:170
+msgid "Sort Descending:"
+msgstr "éåºæåï¼"
+
+#: ../templates/classic/html/issue.search.html:177
+msgid "Group Descending:"
+msgstr "éåºåçµï¼"
+
+#: ../templates/classic/html/issue.search.html:184
+msgid "Query name**:"
+msgstr "æ¥è©¢ åå**"
+
+#: ../templates/classic/html/issue.search.html:194
+#: ../templates/classic/html/page.html:47
+msgid "Search"
+msgstr "æç´¢"
+
+#: ../templates/classic/html/issue.search.html:198
+msgid "*: The \"all text\" field will look in message bodies and issue titles<br> **: If you supply a name, the query will be saved off and available as a link in the sidebar"
+msgstr "*: å¨ä¿¡æ¯é«ååé¡æ¨é¡ä¸ç \"ææçææ¬\" å段é½å°è¢«æ¥æ¾<br> **: å¦æä½ æä¾äºä¸åååï¼éåæ¥è©¢å°è¢«ä¿å並ä¸ä½çºä¸åéæ¥åºç¾å¨å´éæ¬ä¸"
+
+#: ../templates/classic/html/keyword.item.html:3
+msgid "Keyword editing - ${tracker}"
+msgstr "ééµå編輯 - ${tracker}"
+
+#: ../templates/classic/html/keyword.item.html:5
+msgid "Keyword editing"
+msgstr "ééµå編輯"
+
+#: ../templates/classic/html/keyword.item.html:11
+msgid "Existing Keywords"
+msgstr "åå¨çééµå"
+
+#: ../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
+msgid "To create a new keyword, enter it below and click \"Submit New Entry\"."
+msgstr "æ³è¦å»ºç«æ°çééµåï¼è«é»æä¸é¢ç \"æ交æ°çé
\"ã"
+
+#: ../templates/classic/html/keyword.item.html:37
+msgid "Keyword"
+msgstr "ééµå"
+
+#: ../templates/classic/html/msg.index.html:3
+msgid "List of messages - ${tracker}"
+msgstr "ä¿¡æ¯å表 - ${tracker}"
+
+#: ../templates/classic/html/msg.index.html:5
+msgid "Message listing"
+msgstr "ä¿¡æ¯å表"
+
+#: ../templates/classic/html/msg.item.html:6
+msgid "Message ${id} - ${tracker}"
+msgstr "ä¿¡æ¯ [${id}] - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:9
+msgid "New Message - ${tracker}"
+msgstr "æ°ä¿¡æ¯ - ${tracker}"
+
+#: ../templates/classic/html/msg.item.html:13
+msgid "New Message"
+msgstr "æ°ä¿¡æ¯"
+
+#: ../templates/classic/html/msg.item.html:15
+msgid "New Message Editing"
+msgstr "æ°ä¿¡æ¯ç·¨è¼¯"
+
+#: ../templates/classic/html/msg.item.html:18
+msgid "Message${id}"
+msgstr "ä¿¡æ¯ [${id}]"
+
+#: ../templates/classic/html/msg.item.html:21
+msgid "Message${id} Editing"
+msgstr "ä¿¡æ¯ [${id}] 編輯"
+
+#: ../templates/classic/html/msg.item.html:28
+msgid "Author"
+msgstr "ä½è
"
+
+#: ../templates/classic/html/msg.item.html:33
+msgid "Recipients"
+msgstr "æ¶ä¿¡äºº"
+
+#: ../templates/classic/html/msg.item.html:44
+msgid "Content"
+msgstr "å
§å®¹"
+
+#: ../templates/classic/html/page.html:28
+msgid "<b>Your Queries</b> (<a href=\"query?@template=edit\">edit</a>)"
+msgstr "<b>æçæ¥è©¢</b> (<a href=\"query?@template=edit\">編輯</a>)"
+
+#: ../templates/classic/html/page.html:39
+msgid "Issues"
+msgstr "åé¡"
+
+#: ../templates/classic/html/page.html:41
+#: ../templates/classic/html/page.html:60
+msgid "Create New"
+msgstr "æ°å»º"
+
+#: ../templates/classic/html/page.html:43
+msgid "Show Unassigned"
+msgstr "顯示æªåé
"
+
+#: ../templates/classic/html/page.html:45
+msgid "Show All"
+msgstr "顯示ææ"
+
+#: ../templates/classic/html/page.html:48
+msgid "Show issue:"
+msgstr "顯示åé¡ï¼"
+
+#: ../templates/classic/html/page.html:58
+msgid "Keywords"
+msgstr "ééµå"
+
+#: ../templates/classic/html/page.html:64
+msgid "Edit Existing"
+msgstr "編輯已ç¶åå¨ç"
+
+#: ../templates/classic/html/page.html:70
+#: ../templates/minimal/html/page.html:48
+msgid "Administration"
+msgstr "管ç"
+
+#: ../templates/classic/html/page.html:72
+#: ../templates/minimal/html/page.html:49
+msgid "Class List"
+msgstr "é¡å¥å表"
+
+#: ../templates/classic/html/page.html:76
+#: ../templates/minimal/html/page.html:51
+msgid "User List"
+msgstr "ç¨æ¶å表"
+
+#: ../templates/classic/html/page.html:78
+#: ../templates/minimal/html/page.html:54
+msgid "Add User"
+msgstr "å¢å ç¨æ¶"
+
+#: ../templates/classic/html/page.html:85
+#: ../templates/classic/html/page.html:89
+#: ../templates/minimal/html/page.html:30
+msgid "Login"
+msgstr "ç»é"
+
+#: ../templates/classic/html/page.html:91
+#: ../templates/classic/html/user.register.html:63
+#: ../templates/minimal/html/page.html:33
+#: ../templates/minimal/html/user.register.html:58
+msgid "Register"
+msgstr "註å"
+
+#: ../templates/classic/html/page.html:94
+msgid "Lost your login?"
+msgstr "å¿è¨ä½ çç»å
¥å£ä»¤äºï¼"
+
+#: ../templates/classic/html/page.html:99
+msgid "Hello, ${user}"
+msgstr "追蹤ï¼${user}"
+
+#: ../templates/classic/html/page.html:101
+msgid "Your Issues"
+msgstr "æçåé¡å表"
+
+#: ../templates/classic/html/page.html:102
+#: ../templates/minimal/html/page.html:40
+msgid "Your Details"
+msgstr "æçè³è¨"
+
+#: ../templates/classic/html/page.html:104
+#: ../templates/minimal/html/page.html:42
+msgid "Logout"
+msgstr "註é·"
+
+#: ../templates/classic/html/page.html:108
+msgid "Help"
+msgstr "說æ"
+
+#: ../templates/classic/html/page.html:109
+msgid "Roundup docs"
+msgstr "Roundupææª"
+
+#: ../templates/classic/html/page.html:160
+msgid "don't care"
+msgstr "ä¸ç¨éå¿"
+
+#: ../templates/classic/html/page.html:162
+msgid "------------"
+msgstr ""
+
+#: ../templates/classic/html/page.html:187
+msgid "no value"
+msgstr "ç¡å¼"
+
+#: ../templates/classic/html/query.edit.html:4
+msgid "\"Your Queries\" Editing - ${tracker}"
+msgstr "\"æçæ¥è©¢\" ä¿®æ¹ - ${tracker}"
+
+#: ../templates/classic/html/query.edit.html:6
+msgid "\"Your Queries\" Editing"
+msgstr "\"æçæ¥è©¢\"ä¿®æ¹"
+
+#: ../templates/classic/html/query.edit.html:11
+msgid "You are not allowed to edit queries."
+msgstr "ä¸å
許編輯æ¥è©¢"
+
+#: ../templates/classic/html/query.edit.html:28
+msgid "Query"
+msgstr "æ¥è©¢"
+
+#: ../templates/classic/html/query.edit.html:29
+msgid "Include in \"Your Queries\""
+msgstr "å
æ¬å¨\"æçæ¥è©¢\"ä¸"
+
+#: ../templates/classic/html/query.edit.html:31
+msgid "Private to you?"
+msgstr "æ¯ç§äººä¿¡æ¯åï¼"
+
+#: ../templates/classic/html/query.edit.html:44
+msgid "leave out"
+msgstr "çç¥"
+
+#: ../templates/classic/html/query.edit.html:45
+msgid "include"
+msgstr "å
å«"
+
+#: ../templates/classic/html/query.edit.html:49
+msgid "leave in"
+msgstr "çä¸"
+
+#: ../templates/classic/html/query.edit.html:54
+msgid "[query is retired]"
+msgstr "[æ¥è©¢éæäº]"
+
+#: ../templates/classic/html/query.edit.html:67
+msgid "edit"
+msgstr "編輯"
+
+#: ../templates/classic/html/query.edit.html:71
+msgid "yes"
+msgstr "æ¯"
+
+#: ../templates/classic/html/query.edit.html:73
+msgid "no"
+msgstr "å¦"
+
+#: ../templates/classic/html/query.edit.html:79
+msgid "Delete"
+msgstr "åªé¤"
+
+#: ../templates/classic/html/query.edit.html:90
+msgid "[not yours to edit]"
+msgstr "[ä¸ç±ä½ ä¿®æ¹]"
+
+#: ../templates/classic/html/query.edit.html:96
+msgid "Save Selection"
+msgstr "ä¿åé¸æ"
+
+#: ../templates/classic/html/user.forgotten.html:3
+msgid "Password reset request - ${tracker}"
+msgstr "å£ä»¤éè¨è«æ± - ${tracker}"
+
+#: ../templates/classic/html/user.forgotten.html:5
+msgid "Password reset request"
+msgstr "å£ä»¤éè¨è«æ±"
+
+#: ../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 "éµä»¶å°åï¼"
+
+#: ../templates/classic/html/user.forgotten.html:24
+#: ../templates/classic/html/user.forgotten.html:34
+msgid "Request password reset"
+msgstr "è«æ±å£ä»¤éè¨"
+
+#: ../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 "ç¨æ¶åï¼"
+
+#: ../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.index.html:3
+#: ../templates/minimal/html/user.index.html:3
+msgid "User listing - ${tracker}"
+msgstr "ç¨æ¶å表 - ${tracker}"
+
+#: ../templates/classic/html/user.index.html:5
+#: ../templates/minimal/html/user.index.html:5
+msgid "User listing"
+msgstr "ç¨æ¶å表"
+
+#: ../templates/classic/html/user.index.html:14
+#: ../templates/minimal/html/user.index.html:14
+msgid "Username"
+msgstr "ç¨æ¶å"
+
+#: ../templates/classic/html/user.index.html:15
+msgid "Real name"
+msgstr "ç實å§å"
+
+#: ../templates/classic/html/user.index.html:16
+#: ../templates/classic/html/user.item.html:65
+#: ../templates/classic/html/user.register.html:45
+msgid "Organisation"
+msgstr "çµç¹"
+
+#: ../templates/classic/html/user.index.html:17
+#: ../templates/minimal/html/user.index.html:15
+msgid "Email address"
+msgstr "éµä»¶å°å"
+
+#: ../templates/classic/html/user.index.html:18
+msgid "Phone number"
+msgstr "é»è©±è碼"
+
+#: ../templates/classic/html/user.index.html:19
+msgid "Retire"
+msgstr "æ¶å"
+
+#: ../templates/classic/html/user.index.html:32
+msgid "retire"
+msgstr "æ¶å"
+
+#: ../templates/classic/html/user.item.html:7
+msgid "User ${id}: ${title} - ${tracker}"
+msgstr "ç¨æ¶ [${id}]: ${title} - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:10
+msgid "New User - ${tracker}"
+msgstr "æ°ç¨æ¶ - ${tracker}"
+
+#: ../templates/classic/html/user.item.html:14
+#: ../templates/minimal/html/user.item.html:6
+msgid "New User"
+msgstr "æ°ç¨æ¶"
+
+#: ../templates/classic/html/user.item.html:16
+#: ../templates/minimal/html/user.item.html:8
+msgid "New User Editing"
+msgstr "æ°ç¨æ¶ç·¨è¼¯"
+
+#: ../templates/classic/html/user.item.html:19
+#: ../templates/minimal/html/user.item.html:11
+msgid "User${id}"
+msgstr "ç¨æ¶ [${id}]"
+
+#: ../templates/classic/html/user.item.html:22
+#: ../templates/minimal/html/user.item.html:14
+msgid "User${id} Editing"
+msgstr "ç¨æ¶ [${id}] 編輯"
+
+#: ../templates/classic/html/user.item.html:38
+#: ../templates/classic/html/user.register.html:21
+#: ../templates/minimal/html/user.item.html:27
+#: ../templates/minimal/html/user.item.html:67
+#: ../templates/minimal/html/user.register.html:26
+msgid "Login Name"
+msgstr "ç»éå"
+
+#: ../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 "ç»éå£ä»¤"
+
+#: ../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 "å£ä»¤ç¢ºèª"
+
+#: ../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
+msgid "Roles"
+msgstr "è§è²"
+
+#: ../templates/classic/html/user.item.html:56
+msgid "(to give the user more than one role, enter a comma,separated,list)"
+msgstr "(çºçµ¦ç¨æ¶æå®å¤åè§è²ï¼ç¨éèåéå®å)"
+
+#: ../templates/classic/html/user.item.html:61
+#: ../templates/classic/html/user.register.html:41
+msgid "Phone"
+msgstr "é»è©±"
+
+#: ../templates/classic/html/user.item.html:69
+msgid "Timezone"
+msgstr "æå"
+
+#: ../templates/classic/html/user.item.html:73
+msgid "(this is a numeric hour offset, the default is ${zone})"
+msgstr "(éæ¯æ¸åçå°æå移éï¼é è¨å¼æ¯ ${zone})"
+
+#: ../templates/classic/html/user.item.html:78
+#: ../templates/classic/html/user.register.html:49
+#: ../templates/minimal/html/user.item.html:47
+#: ../templates/minimal/html/user.item.html:71
+#: ../templates/minimal/html/user.register.html:46
+msgid "E-mail address"
+msgstr "éµä»¶å°å"
+
+#: ../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
+msgid "Alternate E-mail addresses<br>One address per line"
+msgstr "ä¿®æ¹éµä»¶å°å<br>æ¯è¡ä¸åå°å"
+
+#: ../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 "ç¨ ${tracker} 註å"
+
+#: ../templates/classic/html/user.rego_progress.html:4
+#: ../templates/minimal/html/user.rego_progress.html:4
+msgid "Registration in progress - ${tracker}"
+msgstr "註åæ£å¨èç - ${tracker}"
+
+#: ../templates/classic/html/user.rego_progress.html:6
+#: ../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
+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/minimal/html/home.html:2
+msgid "Tracker home - ${tracker}"
+msgstr "Trackeræ ¹ç®é - ${tracker}"
+
+#: ../templates/minimal/html/home.html:4
+msgid "Tracker home"
+msgstr "Trackeræ ¹ç®é"
+
+#: ../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
+msgid "Please log in or register."
+msgstr "è«ç»éæ註åã"
+
+#: ../templates/minimal/html/page.html:38
+msgid "Hello,<br>${user}"
+msgstr "ä½ å¥½,<br>${user}"
+
+#: ../templates/minimal/html/user.item.html:3
+msgid "User editing - ${tracker}"
+msgstr "ç¨æ¶ç·¨è¼¯ - ${tracker}"
+
Added: tracker/vendor/roundup/current/patches/20020205.alternate_auth
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/patches/20020205.alternate_auth Sun Nov 5 21:30:25 2006
@@ -0,0 +1,254 @@
+From daniel_clark at us.ibm.com Wed Feb 6 04:27:15 2002
+X-Sieve: cmu-sieve 2.0
+Return-Path: <roundup-devel-admin at lists.sourceforge.net>
+Received: (from uucp at localhost)
+ by crown.off.ekorp.com (8.9.3/8.9.3) id RAA12435
+ for rjones at ekit-inc.com; Tue, 5 Feb 2002 17:30:24 GMT
+Received: from usw-sf-fw2.sourceforge.net(216.136.171.252), claiming to be "usw-sf-list1.sourceforge.net"
+ via SMTP by mx3.ekorp.com, id smtpdAAALJaWqy; Tue Feb 5 17:30:22 2002
+Received: from localhost ([127.0.0.1] helo=usw-sf-list1.sourceforge.net)
+ by usw-sf-list1.sourceforge.net with esmtp (Exim 3.31-VA-mm2 #1 (Debian))
+ id 16Y9Q6-0002kj-00; Tue, 05 Feb 2002 09:30:14 -0800
+Received: from lotus2.lotus.com ([129.42.241.42])
+ by usw-sf-list1.sourceforge.net with esmtp (Exim 3.31-VA-mm2 #1 (Debian))
+ id 16Y9Ps-0002ee-00
+ for <roundup-devel at lists.sourceforge.net>; Tue, 05 Feb 2002 09:30:00 -0800
+Received: from internet2.lotus.com (internet2 [172.16.131.236])
+ by lotus2.lotus.com (8.12.1/8.12.1) with ESMTP id g15HUnTQ013140
+ for <roundup-devel at lists.sourceforge.net>; Tue, 5 Feb 2002 12:30:54 -0500 (EST)
+Received: from a3mail.lotus.com (a3mail.lotus.com [9.95.5.66])
+ by internet2.lotus.com (8.12.1/8.12.1) with ESMTP id g15HTHS0005917
+ for <roundup-devel at lists.sourceforge.net>; Tue, 5 Feb 2002 12:29:17 -0500 (EST)
+To: roundup-devel at lists.sourceforge.net
+X-Mailer: Lotus Notes Release 5.0.8 June 18, 2001
+Message-ID: <OF2C7B87C4.DF1574A8-ON85256B56.0060B9A2 at lotus.com>
+From: "Daniel Clark/CAM/Lotus" <daniel_clark at us.ibm.com>
+X-MIMETrack: Serialize by Router on A3MAIL/CAM/H/Lotus(Build V5010_01222002 |January 22, 2002) at
+ 02/05/2002 12:25:48 PM
+MIME-Version: 1.0
+Content-type: text/plain;
+ charset=iso-8859-1
+Content-transfer-encoding: quoted-printable
+Subject: [Roundup-devel] Alternative authentication for roundup
+Sender: roundup-devel-admin at lists.sourceforge.net
+Errors-To: roundup-devel-admin at lists.sourceforge.net
+X-BeenThere: roundup-devel at lists.sourceforge.net
+X-Mailman-Version: 2.0.5
+Precedence: bulk
+List-Help: <mailto:roundup-devel-request at lists.sourceforge.net?subject=help>
+List-Post: <mailto:roundup-devel at lists.sourceforge.net>
+List-Subscribe: <https://lists.sourceforge.net/lists/listinfo/roundup-devel>,
+ <mailto:roundup-devel-request at lists.sourceforge.net?subject=subscribe>
+List-Id: <roundup-devel.lists.sourceforge.net>
+List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/roundup-devel>,
+ <mailto:roundup-devel-request at lists.sourceforge.net?subject=unsubscribe>
+List-Archive: <http://www.geocrawler.com/redir-sf.php3?list=roundup-devel>
+X-Original-Date: Tue, 5 Feb 2002 12:27:15 -0500
+Date: Tue, 5 Feb 2002 12:27:15 -0500
+Status: R
+X-Status: N
+
+I'm trying to get roundup to work with an alternative method of
+authentication (due to a corporate requirement of using a common intran=
+et
+password). I've created an "altauth" module to abstract the details of =
+the
+authentication. Since the hyperdb usernames and passwords seem to be
+referenced in a lot of places in the code, I am just creating hyperdb
+entries for the users if they exist and enter their correct passwords
+against the alternate authentication source. For the most part this eff=
+ects
+the login_action function in cgi_client.py. I've completed some changes=
+
+that make this work for the web interface, but as I am new to roundup a=
+nd
+relatively new to python I thought I'd post the changes for review. If
+others would find this functionality useful I would be happy if these
+changes (probably reworked) could make it into future releases.
+
+The main things I think I still need to do are add equivalent changes t=
+o
+mailgw.py and handle messages from the alternative authentication sourc=
+e
+better.
+
+--- cgi_client.py Tue Feb 5 21:56:30 2002
++++ cgi_client.py-altauth Tue Feb 5 21:56:30 2002
+@@ -27,6 +27,13 @@
+ import roundupdb, htmltemplate, date, hyperdb, password
+ from roundup.i18n import _
+
++try:
++ from altauth import altauth
++ import password as password_module
++ altauth_exists =3D 1
++except:
++ altauth_exists =3D 0
++
+ class Unauthorised(ValueError):
+ pass
+
+@@ -807,7 +814,24 @@
+ password =3D self.form['__login_password'].value
+ else:
+ password =3D ''
++ # if using alternate authentication, perform it.
++ if altauth_exists:
++ auth =3D altauth(self.user, password)
+ # make sure the user exists
++ if altauth_exists:
++ if auth.exists:
++ try:
++ uid =3D self.db.user.lookup(self.user)
++ except KeyError:
++ username =3D str(self.user)
++ self.db =3D self.instance.open('admin')
++ cl =3D self.db.user
++ props =3D {'username':username, 'realname':auth.re=
+alname,
++ 'organisation':auth.org, 'address':auth.e=
+mail,
++ 'phone':auth.phone}
++ uid =3D cl.create(**props)
++ self.user =3D cl.get(uid, 'username')
++ self.db.commit()
+ try:
+ uid =3D self.db.user.lookup(self.user)
+ except KeyError:
+@@ -819,6 +843,20 @@
+ return 0
+
+ # and that the password is correct
++ if altauth_exists:
++ if auth.success:
++ name =3D str(self.user)
++ self.db =3D self.instance.open(name)
++ value =3D password_module.Password(password.strip())
++ password_dict =3D {'password':value}
++ user =3D self.db.user
++ user.set(uid, **password_dict)
++ self.db.commit()
++ else:
++ self.make_user_anonymous()o
++ action =3D self.form['__destination_url'].value
++ self.login(message=3D_(auth.message), action=3Daction)=
+
++ return 0
+ pw =3D self.db.user.get(uid, 'password')
+ if password !=3D pw:
+ self.make_user_anonymous()
+
+
+example altauth.py:
+
+__doc__ =3D """
+Alternative authentication for roundup
+"""
+
+import pipes, os, string
+
+class altauth:
+ """
+ Arguments:
+ username : username
+ password : password in plaintext
+
+ Instance variables:
+ realname : username's real name
+ org : username's organization
+ email : username's email address
+ phone : username's phone number
+
+ code : return code from alternate authentication
+ message : message from alternate authentication
+ exists : does user exist in alternate autentication source?
+ success : did user enter a valid user / password combo?
+ """
+ def __init__(self, username=3DNone, password=3DNone):
+ # Make sure user and password have values - else java cwauthcmd=
+ hangs.
+ if username is None:
+ username =3D "test"
+ if password is None:
+ password =3D "test"
+
+ # In Bluepages, your username is your email address, but this m=
+ight not
+ # be true for other authentication sources.
+ self.email =3D username
+
+ # Get realname, phone and org from Bluepages
+ cmd =3D "phone ldap emailaddress=3D%s format givenname sn telep=
+honenumber dept" % self.email
+ s =3D os.popen(cmd).readlines()[0].strip().split()
+ self.realname =3D string.join(s[:-2])
+ self.phone =3D s[-2]
+ self.org =3D s[-1]
+
+ # Open a pipeline to java cwauth stuff. The most secure option =
+I could think of
+ # besides JPE (Java Python Extension), which I couldn't get to =
+work.
+ os.umask(077)
+ t=3Dpipes.Template()
+ t.append('java cwauthcmd', '--')
+ tmpfile =3D os.tmpnam()
+ f=3Dt.open(tmpfile, 'w')
+ f.write(username + " " + password)
+ f.close()
+ self.code =3D int(open(tmpfile).read().strip())
+ os.remove(tmpfile)
+
+ if self.code =3D=3D 0:
+ self.message =3D "Success. The authentication was successfu=
+l."
+ self.exists =3D 1
+ self.success =3D 1
+ elif self.code =3D=3D 2:
+ self.message =3D "Not registered. Visit http://w3.ibm.com/p=
+assword/"
+ self.exists =3D 0
+ self.success =3D 0
+ elif self.code =3D=3D 3:
+ self.message =3D "LDAP Error. There was an error communicat=
+ing with Bluepages."
+ self.exists =3D 0
+ self.success =3D 0
+ elif self.code =3D=3D 4:
+ self.message =3D "No Record Found. No user was found havin=
+g that e-mail address."
+ self.exists =3D 0
+ self.success =3D 0
+ elif self.code =3D=3D 5:
+ self.message =3D "Multiple Records Found. More than one ent=
+ry exists for that e-mail address."
+ self.exists =3D 1
+ self.success =3D 0
+ elif self.code =3D=3D 6:
+ self.message =3D "Incorrect password. Try again or visit ht=
+tp://w3.ibm.com/password"
+ self.exists =3D 1
+ self.success =3D 0
+ else:
+ self.message =3D "Unknown result code. Contact daniel_clark=
+ at us.ibm.com"
+ self.exists =3D 0
+ self.success =3D 0
+
+
+--
+Daniel Clark =A7 Sys Admin & Assistant Release Engineer
+IBM =BB Lotus =BB Messaging Technology Group =A7 http://w3.mtg.lotus.co=
+m
+Tieline 693-7353 =A7 External 617-693-7353 =A7 Mobile 617-877-0702
+AIM as djbclark =A7 Sametime as Daniel Clark/CAM/Lotus
+=
+
+
+
+_______________________________________________
+Roundup-devel mailing list
+Roundup-devel at lists.sourceforge.net
+https://lists.sourceforge.net/lists/listinfo/roundup-devel
+
+
Added: tracker/vendor/roundup/current/roundup/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,3 @@
+*.pyc
+*.pyo
+*.cover
Added: tracker/vendor/roundup/current/roundup/__init__.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/__init__.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,73 @@
+#
+# 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: __init__.py,v 1.38 2006/04/27 05:55:18 richard Exp $
+
+'''Roundup - issue tracking for knowledge workers.
+
+This is a simple-to-use and -install issue-tracking system with
+command-line, web and e-mail interfaces.
+
+Roundup manages a number of issues (with properties such as
+"description", "priority", and so on) and provides the ability to (a) submit
+new issues, (b) find and edit existing issues, and (c) discuss issues with
+other participants. The system will facilitate communication among the
+participants by managing discussions and notifying interested parties when
+issues are edited.
+
+Roundup's structure is that of a cake::
+
+ _________________________________________________________________________
+ | E-mail Client | Web Browser | Detector Scripts | Shell |
+ |------------------+-----------------+----------------------+-------------|
+ | E-mail User | Web User | Detector | Command |
+ |-------------------------------------------------------------------------|
+ | Roundup Database Layer |
+ |-------------------------------------------------------------------------|
+ | Hyperdatabase Layer |
+ |-------------------------------------------------------------------------|
+ | Storage Layer |
+ -------------------------------------------------------------------------
+
+1. The first layer represents the users (chocolate).
+2. The second layer is the Roundup interface to the users (vanilla).
+3. The third and fourth layers are the internal Roundup database storage
+ mechanisms (strawberry).
+4. The final, lowest layer is the underlying database storage (rum).
+
+These are implemented in the code in the following manner::
+
+ E-mail User: roundup-mailgw and roundup.mailgw
+ Web User: cgi-bin/roundup.cgi or roundup-server over
+ roundup.cgi.client and roundup.cgi.template
+ Detector: roundup.roundupdb and templates/<template>/detectors
+ Command: roundup-admin
+ Roundup DB: roundup.roundupdb
+ Hyper DB: roundup.hyperdb, roundup.date
+ Storage: roundup.backends.*
+
+Additionally, there is a directory of unit tests in "test".
+
+For more information, see the original overview and specification documents
+written by Ka-Ping Yee in the "doc" directory. If nothing else, it has a
+much prettier cake :)
+'''
+__docformat__ = 'restructuredtext'
+
+__version__ = '1.1.2'
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/roundup/admin.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/admin.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,1466 @@
+#! /usr/bin/env python
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: admin.py,v 1.99 2006/04/27 03:38:53 richard Exp $
+
+'''Administration commands for maintaining Roundup trackers.
+'''
+__docformat__ = 'restructuredtext'
+
+import csv, getopt, getpass, os, re, shutil, sys, UserDict
+
+from roundup import date, hyperdb, roundupdb, init, password, token
+from roundup import __version__ as roundup_version
+import roundup.instance
+from roundup.configuration import CoreConfig
+from roundup.i18n import _
+
+class CommandDict(UserDict.UserDict):
+ '''Simple dictionary that lets us do lookups using partial keys.
+
+ Original code submitted by Engelbert Gruber.
+ '''
+ _marker = []
+ def get(self, key, default=_marker):
+ if self.data.has_key(key):
+ return [(key, self.data[key])]
+ keylist = self.data.keys()
+ keylist.sort()
+ l = []
+ for ki in keylist:
+ if ki.startswith(key):
+ l.append((ki, self.data[ki]))
+ if not l and default is self._marker:
+ raise KeyError, key
+ return l
+
+class UsageError(ValueError):
+ pass
+
+class AdminTool:
+ ''' 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
+ loop for the roundup-admin script.
+
+ Actions are defined by do_*() methods, with help for the action
+ 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():
+ if k[:3] == 'do_':
+ self.commands[k[3:]] = getattr(self, k)
+ self.help = {}
+ for k in AdminTool.__dict__.keys():
+ if k[:5] == 'help_':
+ self.help[k[5:]] = getattr(self, k)
+ self.tracker_home = ''
+ self.db = None
+
+ def get_class(self, classname):
+ '''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.
+
+ The args list is specified as ``prop=value prop=value ...``.
+ '''
+ props = {}
+ for arg in args:
+ if arg.find('=') == -1:
+ raise UsageError, _('argument "%(arg)s" not propname=value'
+ )%locals()
+ l = arg.split('=')
+ if len(l) < 2:
+ raise UsageError, _('argument "%(arg)s" not propname=value'
+ )%locals()
+ key, value = l[0], '='.join(l[1:])
+ if value:
+ props[key] = value
+ else:
+ props[key] = None
+ return props
+
+ def usage(self, message=''):
+ ''' Display a simple usage message.
+ '''
+ if message:
+ message = _('Problem: %(message)s\n\n')%locals()
+ print _('''%(message)sUsage: roundup-admin [options] [<command> <arguments>]
+
+Options:
+ -i instance home -- specify the issue tracker "home directory" to administer
+ -u -- the user[:password] to use for commands
+ -d -- print full designators not just class id numbers
+ -c -- when outputting lists of data, comma-separate them.
+ Same as '-S ","'.
+ -S <string> -- when outputting lists of data, string-separate them
+ -s -- when outputting lists of data, space-separate them.
+ Same as '-S " "'.
+ -V -- be verbose when importing
+ -v -- report Roundup and Python versions (and quit)
+
+ Only one of -s, -c or -S can be specified.
+
+Help:
+ roundup-admin -h
+ roundup-admin help -- this help
+ roundup-admin help <command> -- command-specific help
+ roundup-admin help all -- all available help
+''')%locals()
+ self.help_commands()
+
+ def help_commands(self):
+ ''' List the commands available with their help summary.
+ '''
+ print _('Commands:'),
+ commands = ['']
+ for command in self.commands.values():
+ h = _(command.__doc__).split('\n')[0]
+ commands.append(' '+h[7:])
+ commands.sort()
+ commands.append(_(
+"""Commands may be abbreviated as long as the abbreviation
+matches only one command, e.g. l == li == lis == list."""))
+ print '\n'.join(commands)
+ print
+
+ def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
+ ''' Produce an HTML command list.
+ '''
+ commands = self.commands.values()
+ def sortfun(a, b):
+ return cmp(a.__name__, b.__name__)
+ commands.sort(sortfun)
+ for command in commands:
+ h = _(command.__doc__).split('\n')
+ name = command.__name__[3:]
+ usage = h[0]
+ print '''
+<tr><td valign=top><strong>%(name)s</strong></td>
+ <td><tt>%(usage)s</tt><p>
+<pre>''' % locals()
+ indent = indent_re.match(h[3])
+ if indent: indent = len(indent.group(1))
+ for line in h[3:]:
+ if indent:
+ print line[indent:]
+ else:
+ print line
+ print '</pre></td></tr>\n'
+
+ def help_all(self):
+ print _('''
+All commands (except help) require 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
+an issue tracker. It may be thought of as the issue tracker's "home
+directory". It may be specified in the environment variable TRACKER_HOME
+or on the command line as "-i tracker".
+
+A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
+
+Property values are represented as strings in command arguments and in the
+printed results:
+ . Strings are, well, strings.
+ . Date values are printed in the full date format in the local time zone,
+ and accepted in the full format or any of the partial formats explained
+ below.
+ . Link values are printed as node designators. When given as an argument,
+ node designators and key strings are both accepted.
+ . Multilink values are printed as lists of node designators joined
+ by commas. When given as an argument, node designators and key
+ strings are both accepted; an empty string, a single node, or a list
+ of nodes joined by commas is accepted.
+
+When property values must contain spaces, just surround the value with
+quotes, either ' or ". A single space may also be backslash-quoted. If a
+value must contain a quote character, it must be backslash-quoted or inside
+quotes. Examples:
+ hello world (2 tokens: hello, world)
+ "hello world" (1 token: hello world)
+ "Roch'e" Compaan (2 tokens: Roch'e Compaan)
+ Roch\\'e Compaan (2 tokens: Roch'e Compaan)
+ address="1 2 3" (1 token: address=1 2 3)
+ \\\\ (1 token: \\)
+ \\n\\r\\t (1 token: a newline, carriage-return and tab)
+
+When multiple nodes are specified to the roundup get or roundup set
+commands, the specified properties are retrieved or set on all the listed
+nodes.
+
+When multiple results are returned by the roundup get or roundup find
+commands, they are printed one per line (default) or joined by commas (with
+the -c) option.
+
+Where the command changes data, a login name/password is required. The
+login may be specified as either "name" or "name:password".
+ . ROUNDUP_LOGIN environment variable
+ . the -u command-line option
+If either the name or password is not supplied, they are obtained from the
+command-line.
+
+Date format examples:
+ "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
+ "2000-04-17" means <Date 2000-04-17.00:00:00>
+ "01-25" means <Date yyyy-01-25.00:00:00>
+ "08-13.22:13" means <Date yyyy-08-14.03:13:00>
+ "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
+ "14:25" means <Date yyyy-mm-dd.19:25:00>
+ "8:47:11" means <Date yyyy-mm-dd.13:47:11>
+ "." means "right now"
+
+Command help:
+''')
+ for name, command in self.commands.items():
+ print _('%s:')%name
+ print ' ', _(command.__doc__)
+
+ def do_help(self, args, nl_re=re.compile('[\r\n]'),
+ indent_re=re.compile(r'^(\s+)\S+')):
+ ""'''Usage: help topic
+ Give help about topic.
+
+ commands -- list commands
+ <command> -- help specific to a command
+ initopts -- init command options
+ all -- all available help
+ '''
+ if len(args)>0:
+ topic = args[0]
+ else:
+ topic = 'help'
+
+
+ # try help_ methods
+ if self.help.has_key(topic):
+ self.help[topic]()
+ return 0
+
+ # try command docstrings
+ try:
+ l = self.commands.get(topic)
+ except KeyError:
+ print _('Sorry, no help for "%(topic)s"')%locals()
+ return 1
+
+ # display the help for each match, removing the docsring indent
+ for name, help in l:
+ lines = nl_re.split(_(help.__doc__))
+ print lines[0]
+ indent = indent_re.match(lines[1])
+ if indent: indent = len(indent.group(1))
+ for line in lines[1:]:
+ if indent:
+ print line[indent:]
+ else:
+ print line
+ return 0
+
+ def listTemplates(self):
+ ''' List all the available templates.
+
+ Look in the following places, where the later rules take precedence:
+
+ 1. <prefix>/share/roundup/templates/*
+ this should be the standard place to find them when Roundup is
+ installed
+ 2. <roundup.admin.__file__>/../templates/*
+ this will be used if Roundup's run in the distro (aka. source)
+ directory
+ 3. <current working dir>/*
+ this is for when someone unpacks a 3rd-party template
+ 4. <current working dir>
+ this is for someone who "cd"s to the 3rd-party template dir
+ '''
+ # OK, try <prefix>/share/roundup/templates
+ # -- this module (roundup.admin) will be installed in something
+ # like:
+ # /usr/lib/python2.2/site-packages/roundup/admin.py (5 dirs up)
+ # c:\python22\lib\site-packages\roundup\admin.py (4 dirs up)
+ # we're interested in where the "lib" directory is - ie. the /usr/
+ # part
+ templates = {}
+ for N in 4, 5:
+ path = __file__
+ # move up N elements in the path
+ for i in range(N):
+ path = os.path.dirname(path)
+ tdir = os.path.join(path, 'share', 'roundup', 'templates')
+ if os.path.isdir(tdir):
+ templates = init.listTemplates(tdir)
+ break
+
+ # OK, now try as if we're in the roundup source distribution
+ # directory, so this module will be in .../roundup-*/roundup/admin.py
+ # and we're interested in the .../roundup-*/ part.
+ path = __file__
+ for i in range(2):
+ path = os.path.dirname(path)
+ tdir = os.path.join(path, 'templates')
+ if os.path.isdir(tdir):
+ templates.update(init.listTemplates(tdir))
+
+ # Try subdirs of the current dir
+ templates.update(init.listTemplates(os.getcwd()))
+
+ # Finally, try the current directory as a template
+ template = init.loadTemplateInfo(os.getcwd())
+ if template:
+ templates[template['name']] = template
+
+ return templates
+
+ def help_initopts(self):
+ templates = self.listTemplates()
+ print _('Templates:'), ', '.join(templates.keys())
+ import roundup.backends
+ backends = roundup.backends.list_backends()
+ print _('Back ends:'), ', '.join(backends)
+
+ def do_install(self, tracker_home, args):
+ ""'''Usage: install [template [backend [admin password [key=val[,key=val]]]]]
+ Install a new Roundup tracker.
+
+ The command will prompt for the tracker home directory
+ (if not supplied through TRACKER_HOME or the -i option).
+ The template, backend and admin password may be specified
+ on the command-line as arguments, in that order.
+
+ The last command line argument allows to pass initial values
+ for config options. For example, passing
+ "web_http_auth=no,rdbms_user=dinsdale" will override defaults
+ for options http_auth in section [web] and user in section [rdbms].
+ Please be careful to not use spaces in this argument! (Enclose
+ whole argument in quotes if you need spaces in option value).
+
+ The initialise command must be called after this command in order
+ to initialise the tracker's database. You may edit the tracker's
+ initial database contents before running that command by editing
+ the tracker's dbinit.py module init() function.
+
+ See also initopts help.
+ '''
+ if len(args) < 1:
+ raise UsageError, _('Not enough arguments supplied')
+
+ # make sure the tracker home can be created
+ tracker_home = os.path.abspath(tracker_home)
+ parent = os.path.split(tracker_home)[0]
+ if not os.path.exists(parent):
+ raise UsageError, _('Instance home parent directory "%(parent)s"'
+ ' does not exist')%locals()
+
+ config_ini_file = os.path.join(tracker_home, CoreConfig.INI_FILE)
+ # check for both old- and new-style configs
+ if filter(os.path.exists, [config_ini_file,
+ os.path.join(tracker_home, 'config.py')]):
+ ok = raw_input(_(
+"""WARNING: There appears to be a tracker in "%(tracker_home)s"!
+If you re-install it, you will lose all the data!
+Erase it? Y/N: """) % locals())
+ if ok.strip().lower() != 'y':
+ return 0
+
+ # clear it out so the install isn't confused
+ shutil.rmtree(tracker_home)
+
+ # select template
+ templates = self.listTemplates()
+ template = len(args) > 1 and args[1] or ''
+ if not templates.has_key(template):
+ print _('Templates:'), ', '.join(templates.keys())
+ while not templates.has_key(template):
+ template = raw_input(_('Select template [classic]: ')).strip()
+ if not template:
+ template = 'classic'
+
+ # select hyperdb backend
+ import roundup.backends
+ backends = roundup.backends.list_backends()
+ backend = len(args) > 2 and args[2] or ''
+ if backend not in backends:
+ print _('Back ends:'), ', '.join(backends)
+ while backend not in backends:
+ backend = raw_input(_('Select backend [anydbm]: ')).strip()
+ if not backend:
+ backend = 'anydbm'
+ # XXX perform a unit test based on the user's selections
+
+ # Process configuration file definitions
+ if len(args) > 3:
+ try:
+ defns = dict([item.split("=") for item in args[3].split(",")])
+ except:
+ print _('Error in configuration settings: "%s"') % args[3]
+ raise
+ else:
+ defns = {}
+
+ # install!
+ init.install(tracker_home, templates[template]['path'], settings=defns)
+ init.write_select_db(tracker_home, backend)
+
+ print _("""
+---------------------------------------------------------------------------
+ You should now edit the tracker configuration file:
+ %(config_file)s""") % {"config_file": config_ini_file}
+
+ # find list of options that need manual adjustments
+ # XXX config._get_unset_options() is marked as private
+ # (leading underscore). make it public or don't care?
+ need_set = CoreConfig(tracker_home)._get_unset_options()
+ if need_set:
+ print _(" ... at a minimum, you must set following options:")
+ for section, options in need_set.items():
+ print " [%s]: %s" % (section, ", ".join(options))
+
+ # note about schema modifications
+ print _("""
+ If you wish to modify the database schema,
+ you should also edit the schema file:
+ %(database_config_file)s
+ You may also change the database initialisation file:
+ %(database_init_file)s
+ ... see the documentation on customizing for more information.
+
+ You MUST run the "roundup-admin initialise" command once you've performed
+ the above steps.
+---------------------------------------------------------------------------
+""") % {
+ 'database_config_file': os.path.join(tracker_home, 'schema.py'),
+ 'database_init_file': os.path.join(tracker_home, 'initial_data.py'),
+}
+ return 0
+
+ def do_genconfig(self, args):
+ ""'''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]
+ 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]
+ else:
+ adminpw = ''
+ confirm = 'x'
+ while adminpw != confirm:
+ adminpw = getpass.getpass(_('Admin Password: '))
+ confirm = getpass.getpass(_(' Confirm: '))
+
+ # make sure the tracker home is installed
+ if not os.path.exists(tracker_home):
+ raise UsageError, _('Instance home does not exist')%locals()
+ try:
+ tracker = roundup.instance.open(tracker_home)
+ except roundup.instance.TrackerError:
+ raise UsageError, _('Instance has not been installed')%locals()
+
+ # is there already a database?
+ if tracker.exists():
+ ok = raw_input(_(
+"""WARNING: The database is already initialised!
+If you re-initialise it, you will lose all the data!
+Erase it? Y/N: """))
+ if ok.strip().lower() != 'y':
+ return 0
+
+ backend = tracker.get_backend_name()
+
+ # nuke it
+ tracker.nuke()
+
+ # re-write the backend select file
+ init.write_select_db(tracker_home, backend)
+
+ # GO
+ tracker.init(password.Password(adminpw))
+
+ return 0
+
+
+ def do_get(self, args):
+ ""'''Usage: get property designator[,designator]*
+ Get the given property of one or more designator(s).
+
+ Retrieves the property value of the nodes specified
+ by the designators.
+ '''
+ if len(args) < 2:
+ raise UsageError, _('Not enough arguments supplied')
+ propname = args[0]
+ designators = args[1].split(',')
+ l = []
+ for designator in designators:
+ # decode the node designator
+ try:
+ classname, nodeid = hyperdb.splitDesignator(designator)
+ except hyperdb.DesignatorError, message:
+ raise UsageError, message
+
+ # get the class
+ cl = self.get_class(classname)
+ try:
+ id=[]
+ if self.separator:
+ if self.print_designator:
+ # see if property is a link or multilink for
+ # which getting a desginator make sense.
+ # Algorithm: Get the properties of the
+ # current designator's class. (cl.getprops)
+ # get the property object for the property the
+ # user requested (properties[propname])
+ # verify its type (isinstance...)
+ # raise error if not link/multilink
+ # get class name for link/multilink property
+ # do the get on the designators
+ # append the new designators
+ # print
+ properties = cl.getprops()
+ property = properties[propname]
+ if not (isinstance(property, hyperdb.Multilink) or
+ isinstance(property, hyperdb.Link)):
+ raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname
+ propclassname = self.db.getclass(property.classname).classname
+ id = cl.get(nodeid, propname)
+ for i in id:
+ l.append(propclassname + i)
+ else:
+ id = cl.get(nodeid, propname)
+ for i in id:
+ l.append(i)
+ else:
+ if self.print_designator:
+ properties = cl.getprops()
+ property = properties[propname]
+ if not (isinstance(property, hyperdb.Multilink) or
+ isinstance(property, hyperdb.Link)):
+ raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname
+ propclassname = self.db.getclass(property.classname).classname
+ id = cl.get(nodeid, propname)
+ for i in id:
+ print propclassname + i
+ else:
+ print cl.get(nodeid, propname)
+ except IndexError:
+ raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+ except KeyError:
+ raise UsageError, _('no such %(classname)s property '
+ '"%(propname)s"')%locals()
+ if self.separator:
+ print self.separator.join(l)
+
+ return 0
+
+
+ def do_set(self, args):
+ ""'''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
+ list of item designators (ie "designator[,designator,...]").
+
+ This command sets the properties to the values for all designators
+ 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
+
+ designators = args[0].split(',')
+ if len(designators) == 1:
+ designator = designators[0]
+ try:
+ designator = hyperdb.splitDesignator(designator)
+ designators = [designator]
+ except hyperdb.DesignatorError:
+ cl = self.get_class(designator)
+ designators = [(designator, x) for x in cl.list()]
+ else:
+ try:
+ designators = [hyperdb.splitDesignator(x) for x in designators]
+ except hyperdb.DesignatorError, message:
+ raise UsageError, message
+
+ # get the props from the args
+ props = self.props_from_args(args[1:])
+
+ # now do the set for all the nodes
+ for classname, itemid in designators:
+ cl = self.get_class(classname)
+
+ properties = cl.getprops()
+ for key, value in props.items():
+ try:
+ props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid,
+ key, value)
+ except hyperdb.HyperdbValueError, message:
+ raise UsageError, message
+
+ # try the set
+ try:
+ apply(cl.set, (itemid, ), props)
+ except (TypeError, IndexError, ValueError), message:
+ import traceback; traceback.print_exc()
+ raise UsageError, message
+ return 0
+
+ def do_find(self, args):
+ ""'''Usage: find classname propname=value ...
+ Find the nodes of the given class with a given link property value.
+
+ Find the nodes of the given class with a given link property value.
+ The value may be either the nodeid of the linked node, or its key
+ value.
+ '''
+ if len(args) < 1:
+ raise UsageError, _('Not enough arguments supplied')
+ classname = args[0]
+ # get the class
+ cl = self.get_class(classname)
+
+ # handle the propname=value argument
+ props = self.props_from_args(args[1:])
+
+ # convert the user-input value to a value used for find()
+ for propname, value in props.items():
+ if ',' in value:
+ values = value.split(',')
+ else:
+ values = [value]
+ d = props[propname] = {}
+ for value in values:
+ value = hyperdb.rawToHyperdb(self.db, cl, None, propname, value)
+ if isinstance(value, list):
+ for entry in value:
+ d[entry] = 1
+ else:
+ d[value] = 1
+
+ # now do the find
+ try:
+ id = []
+ designator = []
+ if self.separator:
+ if self.print_designator:
+ id=apply(cl.find, (), props)
+ for i in id:
+ designator.append(classname + i)
+ print self.separator.join(designator)
+ else:
+ print self.separator.join(apply(cl.find, (), props))
+
+ else:
+ if self.print_designator:
+ id=apply(cl.find, (), props)
+ for i in id:
+ designator.append(classname + i)
+ print designator
+ else:
+ print apply(cl.find, (), props)
+ except KeyError:
+ raise UsageError, _('%(classname)s has no property '
+ '"%(propname)s"')%locals()
+ except (ValueError, TypeError), message:
+ raise UsageError, message
+ return 0
+
+ def do_specification(self, args):
+ ""'''Usage: specification classname
+ Show the properties for a classname.
+
+ This lists the properties for a given class.
+ '''
+ if len(args) < 1:
+ raise UsageError, _('Not enough arguments supplied')
+ classname = args[0]
+ # get the class
+ cl = self.get_class(classname)
+
+ # get the key property
+ keyprop = cl.getkey()
+ for key, value in cl.properties.items():
+ if keyprop == key:
+ print _('%(key)s: %(value)s (key property)')%locals()
+ else:
+ print _('%(key)s: %(value)s')%locals()
+
+ def do_display(self, args):
+ ""'''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')
+
+ # decode the node designator
+ for designator in args[0].split(','):
+ try:
+ classname, nodeid = hyperdb.splitDesignator(designator)
+ except hyperdb.DesignatorError, message:
+ raise UsageError, message
+
+ # get the class
+ cl = self.get_class(classname)
+
+ # display the values
+ keys = cl.properties.keys()
+ keys.sort()
+ for key in keys:
+ value = cl.get(nodeid, key)
+ print _('%(key)s: %(value)r')%locals()
+
+ def do_create(self, args):
+ ""'''Usage: create classname property=value ...
+ Create a new entry of a given class.
+
+ This creates a new entry of the given class using the property
+ name=value arguments provided on the command line after the "create"
+ command.
+ '''
+ if len(args) < 1:
+ raise UsageError, _('Not enough arguments supplied')
+ from roundup import hyperdb
+
+ classname = args[0]
+
+ # get the class
+ cl = self.get_class(classname)
+
+ # now do a create
+ props = {}
+ properties = cl.getprops(protected = 0)
+ if len(args) == 1:
+ # ask for the properties
+ for key, value in properties.items():
+ if key == 'id': continue
+ name = value.__class__.__name__
+ if isinstance(value , hyperdb.Password):
+ again = None
+ while value != again:
+ value = getpass.getpass(_('%(propname)s (Password): ')%{
+ 'propname': key.capitalize()})
+ again = getpass.getpass(_(' %(propname)s (Again): ')%{
+ 'propname': key.capitalize()})
+ if value != again: print _('Sorry, try again...')
+ if value:
+ props[key] = value
+ else:
+ value = raw_input(_('%(propname)s (%(proptype)s): ')%{
+ 'propname': key.capitalize(), 'proptype': name})
+ if value:
+ props[key] = value
+ else:
+ props = self.props_from_args(args[1:])
+
+ # convert types
+ for propname, value in props.items():
+ try:
+ props[propname] = hyperdb.rawToHyperdb(self.db, cl, None,
+ propname, value)
+ except hyperdb.HyperdbValueError, message:
+ raise UsageError, message
+
+ # check for the key property
+ propname = cl.getkey()
+ if propname and not props.has_key(propname):
+ raise UsageError, _('you must provide the "%(propname)s" '
+ 'property.')%locals()
+
+ # do the actual create
+ try:
+ print apply(cl.create, (), props)
+ except (TypeError, IndexError, ValueError), message:
+ raise UsageError, message
+ return 0
+
+ def do_list(self, args):
+ ""'''Usage: list classname [property]
+ List the instances of a class.
+
+ Lists all instances of the given class. If the property is not
+ specified, the "label" property is used. The label property is
+ tried in order: the key, "name", "title" and then the first
+ property, alphabetically.
+
+ 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)
+
+ # figure the property
+ if len(args) > 1:
+ propname = args[1]
+ else:
+ propname = cl.labelprop()
+
+ if self.separator:
+ if len(args) == 2:
+ # create a list of propnames since user specified propname
+ proplist=[]
+ for nodeid in cl.list():
+ try:
+ proplist.append(cl.get(nodeid, propname))
+ except KeyError:
+ raise UsageError, _('%(classname)s has no property '
+ '"%(propname)s"')%locals()
+ print self.separator.join(proplist)
+ else:
+ # create a list of index id's since user didn't specify
+ # otherwise
+ print self.separator.join(cl.list())
+ else:
+ for nodeid in cl.list():
+ try:
+ value = cl.get(nodeid, propname)
+ except KeyError:
+ raise UsageError, _('%(classname)s has no property '
+ '"%(propname)s"')%locals()
+ print _('%(nodeid)4s: %(value)s')%locals()
+ return 0
+
+ def do_table(self, args):
+ ""'''Usage: table classname [property[,property]*]
+ List the instances of a class in tabular form.
+
+ Lists all instances of the given class. If the properties are not
+ specified, all properties are displayed. By default, the column
+ widths are the width of the largest value. The width may be
+ explicitly defined by defining the property as "name:width".
+ For example::
+
+ roundup> table priority id,name:10
+ Id Name
+ 1 fatal-bug
+ 2 bug
+ 3 usability
+ 4 feature
+
+ Also to make the width of the column the width of the label,
+ leave a trailing : without a width on the property. For example::
+
+ roundup> table priority id,name:
+ Id Name
+ 1 fata
+ 2 bug
+ 3 usab
+ 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]
+
+ # get the class
+ cl = self.get_class(classname)
+
+ # figure the property names to display
+ if len(args) > 1:
+ prop_names = args[1].split(',')
+ all_props = cl.getprops()
+ for spec in prop_names:
+ if ':' in spec:
+ try:
+ propname, width = spec.split(':')
+ except (ValueError, TypeError):
+ raise UsageError, _('"%(spec)s" not name:width')%locals()
+ else:
+ propname = spec
+ if not all_props.has_key(propname):
+ raise UsageError, _('%(classname)s has no property '
+ '"%(propname)s"')%locals()
+ else:
+ prop_names = cl.getprops().keys()
+
+ # now figure column widths
+ props = []
+ for spec in prop_names:
+ if ':' in spec:
+ name, width = spec.split(':')
+ if width == '':
+ props.append((name, len(spec)))
+ else:
+ props.append((name, int(width)))
+ else:
+ # this is going to be slow
+ maxlen = len(spec)
+ for nodeid in cl.list():
+ curlen = len(str(cl.get(nodeid, spec)))
+ if curlen > maxlen:
+ maxlen = curlen
+ props.append((spec, maxlen))
+
+ # now display the heading
+ print ' '.join([name.capitalize().ljust(width) for name,width in props])
+
+ # and the table data
+ for nodeid in cl.list():
+ l = []
+ for name, width in props:
+ if name != 'id':
+ try:
+ value = str(cl.get(nodeid, name))
+ except KeyError:
+ # we already checked if the property is valid - a
+ # KeyError here means the node just doesn't have a
+ # value for it
+ value = ''
+ else:
+ value = str(nodeid)
+ f = '%%-%ds'%width
+ l.append(f%value[:width])
+ print ' '.join(l)
+ return 0
+
+ def do_history(self, args):
+ ""'''Usage: history designator
+ Show the history entries of a designator.
+
+ Lists the journal entries for the node identified by the designator.
+ '''
+ if len(args) < 1:
+ raise UsageError, _('Not enough arguments supplied')
+ try:
+ classname, nodeid = hyperdb.splitDesignator(args[0])
+ except hyperdb.DesignatorError, message:
+ raise UsageError, message
+
+ try:
+ print self.db.getclass(classname).history(nodeid)
+ except KeyError:
+ raise UsageError, _('no such class "%(classname)s"')%locals()
+ except IndexError:
+ raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+ return 0
+
+ def do_commit(self, args):
+ ""'''Usage: commit
+ Commit changes made to the database during an interactive session.
+
+ The changes made during an interactive session are not
+ automatically written to the database - they must be committed
+ using this command.
+
+ One-off commands on the command-line are automatically committed if
+ they are successful.
+ '''
+ self.db.commit()
+ return 0
+
+ def do_rollback(self, args):
+ ""'''Usage: rollback
+ Undo all changes that are pending commit to the database.
+
+ The changes made during an interactive session are not
+ automatically written to the database - they must be committed
+ manually. This command undoes all those changes, so a commit
+ immediately after would make no changes to the database.
+ '''
+ self.db.rollback()
+ return 0
+
+ def do_retire(self, args):
+ ""'''Usage: retire designator[,designator]*
+ Retire the node specified by designator.
+
+ This action indicates that a particular node is not to be retrieved
+ by the list or find commands, and its key value may be re-used.
+ '''
+ if len(args) < 1:
+ raise UsageError, _('Not enough arguments supplied')
+ designators = args[0].split(',')
+ for designator in designators:
+ try:
+ classname, nodeid = hyperdb.splitDesignator(designator)
+ except hyperdb.DesignatorError, message:
+ raise UsageError, message
+ try:
+ self.db.getclass(classname).retire(nodeid)
+ except KeyError:
+ raise UsageError, _('no such class "%(classname)s"')%locals()
+ except IndexError:
+ raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+ return 0
+
+ def do_restore(self, args):
+ ""'''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(',')
+ for designator in designators:
+ try:
+ classname, nodeid = hyperdb.splitDesignator(designator)
+ except hyperdb.DesignatorError, message:
+ raise UsageError, message
+ try:
+ self.db.getclass(classname).restore(nodeid)
+ except KeyError:
+ raise UsageError, _('no such class "%(classname)s"')%locals()
+ except IndexError:
+ raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+ return 0
+
+ def do_export(self, args):
+ ""'''Usage: export [class[,class]] export_dir
+ Export the database to colon-separated-value files.
+
+ Optionally limit the export to just the names classes.
+
+ 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')
+
+ dir = args[-1]
+
+ # get the list of classes to export
+ if len(args) == 2:
+ classes = args[0].split(',')
+ else:
+ classes = self.db.classes.keys()
+
+ class colon_separated(csv.excel):
+ delimiter = ':'
+
+ # make sure target dir exists
+ if not os.path.exists(dir):
+ os.makedirs(dir)
+
+ # do all the classes specified
+ for classname in classes:
+ cl = self.get_class(classname)
+
+ f = open(os.path.join(dir, classname+'.csv'), 'wb')
+ writer = csv.writer(f, colon_separated)
+
+ properties = cl.getprops()
+ propnames = cl.export_propnames()
+ fields = propnames[:]
+ fields.append('is retired')
+ writer.writerow(fields)
+
+ # all nodes for this class
+ for nodeid in cl.getnodeids():
+ writer.writerow(cl.export_list(propnames, nodeid))
+ if hasattr(cl, 'export_files'):
+ cl.export_files(dir, nodeid)
+
+ # close this file
+ f.close()
+
+ # export the journals
+ jf = open(os.path.join(dir, classname+'-journals.csv'), 'wb')
+ journals = csv.writer(jf, colon_separated)
+ map(journals.writerow, cl.export_journals())
+ jf.close()
+ return 0
+
+ def do_import(self, args):
+ ""'''Usage: import import_dir
+ Import a database from the directory containing CSV files,
+ two per class to import.
+
+ The files used in the import are:
+
+ <class>.csv
+ This must define the same properties as the class (including
+ having a "header" line with those property names.)
+ <class>-journals.csv
+ This defines the journals for the items being imported.
+
+ The imported nodes will have the same nodeid as defined in the
+ import file, thus replacing any existing content.
+
+ 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
+
+ # directory to import from
+ dir = args[0]
+
+ class colon_separated(csv.excel):
+ delimiter = ':'
+
+ # import all the files
+ for file in os.listdir(dir):
+ classname, ext = os.path.splitext(file)
+ # we only care about CSV files
+ if ext != '.csv' or classname.endswith('-journals'):
+ continue
+
+ cl = self.get_class(classname)
+
+ # ensure that the properties and the CSV file headings match
+ f = open(os.path.join(dir, file), 'r')
+ reader = csv.reader(f, colon_separated)
+ file_props = None
+ maxid = 1
+ # loop through the file and create a node for each entry
+ for n, r in enumerate(reader):
+ if file_props is None:
+ file_props = r
+ continue
+
+ sys.stdout.write('Importing %s - %d\r'%(classname, n))
+ sys.stdout.flush()
+
+ # do the import and figure the current highest nodeid
+ nodeid = int(cl.import_list(file_props, r))
+ if hasattr(cl, 'import_files'):
+ cl.import_files(dir, nodeid)
+ maxid = max(maxid, nodeid)
+ print
+ f.close()
+
+ # import the journals
+ f = open(os.path.join(args[0], classname + '-journals.csv'), 'r')
+ reader = csv.reader(f, colon_separated)
+ cl.import_journals(reader)
+ f.close()
+
+ # set the id counter
+ print 'setting', classname, maxid+1
+ self.db.setid(classname, str(maxid+1))
+
+ return 0
+
+ def do_pack(self, args):
+ ""'''Usage: pack period | date
+
+ Remove journal entries older than a period of time specified or
+ before a certain date.
+
+ A period is specified using the suffixes "y", "m", and "d". The
+ suffix "w" (for "week") means 7 days.
+
+ "3y" means three years
+ "2y 1m" means two years and one month
+ "1m 25d" means one month and 25 days
+ "2w 3d" means two weeks and three days
+
+ 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'''
+ (?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)
+ m = date_re.match(value)
+ if not m:
+ raise ValueError, _('Invalid format')
+ m = m.groupdict()
+ if m['period']:
+ pack_before = date.Date(". - %s"%value)
+ elif m['date']:
+ pack_before = date.Date(value)
+ self.db.pack(pack_before)
+ return 0
+
+ def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')):
+ ""'''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)
+ if m:
+ cl = self.get_class(m.group(1))
+ try:
+ cl.index(m.group(2))
+ except IndexError:
+ raise UsageError, _('no such item "%(designator)s"')%{
+ 'designator': arg}
+ else:
+ cl = self.get_class(arg)
+ self.db.reindex(arg)
+ else:
+ self.db.reindex(show_progress=True)
+ return 0
+
+ def do_security(self, args):
+ ""'''Usage: security [Role name]
+ Display the Permissions available to one or all Roles.
+ '''
+ if len(args) == 1:
+ role = args[0]
+ try:
+ roles = [(args[0], self.db.security.role[args[0]])]
+ except KeyError:
+ print _('No such Role "%(role)s"')%locals()
+ return 1
+ else:
+ roles = self.db.security.role.items()
+ role = self.db.config.NEW_WEB_USER_ROLES
+ if ',' in role:
+ print _('New Web users get the Roles "%(role)s"')%locals()
+ else:
+ print _('New Web users get the Role "%(role)s"')%locals()
+ role = self.db.config.NEW_EMAIL_USER_ROLES
+ if ',' in role:
+ print _('New Email users get the Roles "%(role)s"')%locals()
+ else:
+ print _('New Email users get the Role "%(role)s"')%locals()
+ roles.sort()
+ for rolename, role in roles:
+ print _('Role "%(name)s":')%role.__dict__
+ for permission in role.permissions:
+ d = permission.__dict__
+ if permission.klass:
+ if permission.properties:
+ print _(' %(description)s (%(name)s for "%(klass)s"'
+ ': %(properties)s only)')%d
+ else:
+ print _(' %(description)s (%(name)s for "%(klass)s" '
+ 'only)')%d
+ else:
+ print _(' %(description)s (%(name)s)')%d
+ return 0
+
+ def run_command(self, args):
+ '''Run a single command
+ '''
+ command = args[0]
+
+ # handle help now
+ if command == 'help':
+ if len(args)>1:
+ self.do_help(args[1:])
+ return 0
+ self.do_help(['help'])
+ return 0
+ if command == 'morehelp':
+ self.do_help(['help'])
+ self.help_commands()
+ self.help_all()
+ return 0
+ if command == 'config':
+ self.do_config(args[1:])
+ return 0
+
+ # figure what the command is
+ try:
+ functions = self.commands.get(command)
+ except KeyError:
+ # not a valid command
+ print _('Unknown command "%(command)s" ("help commands" for a '
+ 'list)')%locals()
+ return 1
+
+ # check for multiple matches
+ if len(functions) > 1:
+ print _('Multiple commands match "%(command)s": %(list)s')%{'command':
+ command, 'list': ', '.join([i[0] for i in functions])}
+ return 1
+ command, function = functions[0]
+
+ # make sure we have a tracker_home
+ while not self.tracker_home:
+ self.tracker_home = raw_input(_('Enter tracker home: ')).strip()
+
+ # before we open the db, we may be doing an install or init
+ if command == 'initialise':
+ try:
+ return self.do_initialise(self.tracker_home, args)
+ except UsageError, message:
+ print _('Error: %(message)s')%locals()
+ return 1
+ elif command == 'install':
+ try:
+ return self.do_install(self.tracker_home, args)
+ except UsageError, message:
+ print _('Error: %(message)s')%locals()
+ return 1
+
+ # get the tracker
+ try:
+ tracker = roundup.instance.open(self.tracker_home)
+ except ValueError, message:
+ self.tracker_home = ''
+ print _("Error: Couldn't open tracker: %(message)s")%locals()
+ return 1
+
+ # only open the database once!
+ if not self.db:
+ self.db = tracker.open('admin')
+
+ # do the command
+ ret = 0
+ try:
+ ret = function(args[1:])
+ except UsageError, message:
+ print _('Error: %(message)s')%locals()
+ print
+ print function.__doc__
+ ret = 1
+ except:
+ import traceback
+ traceback.print_exc()
+ ret = 1
+ return ret
+
+ def interactive(self):
+ '''Run in an interactive mode
+ '''
+ print _('Roundup %s ready for input.\nType "help" for help.'
+ % roundup_version)
+ try:
+ import readline
+ except ImportError:
+ print _('Note: command history and editing not available')
+
+ while 1:
+ try:
+ command = raw_input(_('roundup> '))
+ except EOFError:
+ print _('exit...')
+ break
+ if not command: continue
+ args = token.token_split(command)
+ if not args: continue
+ if args[0] in ('quit', 'exit'): break
+ self.run_command(args)
+
+ # exit.. check for transactions
+ if self.db and self.db.transactions:
+ commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
+ if commit and commit[0].lower() == 'y':
+ self.db.commit()
+ return 0
+
+ def main(self):
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'i:u:hcdsS:v')
+ except getopt.GetoptError, e:
+ self.usage(str(e))
+ return 1
+
+ # handle command-line args
+ self.tracker_home = os.environ.get('TRACKER_HOME', '')
+ # TODO: reinstate the user/password stuff (-u arg too)
+ name = password = ''
+ if os.environ.has_key('ROUNDUP_LOGIN'):
+ l = os.environ['ROUNDUP_LOGIN'].split(':')
+ name = l[0]
+ if len(l) > 1:
+ password = l[1]
+ self.separator = None
+ self.print_designator = 0
+ self.verbose = 0
+ for opt, arg in opts:
+ if opt == '-h':
+ self.usage()
+ return 0
+ elif opt == '-v':
+ print '%s (python %s)'%(roundup_version, sys.version.split()[0])
+ return 0
+ elif opt == '-V':
+ self.verbose = 1
+ elif opt == '-i':
+ self.tracker_home = arg
+ elif opt == '-c':
+ if self.separator != None:
+ self.usage('Only one of -c, -S and -s may be specified')
+ return 1
+ self.separator = ','
+ elif opt == '-S':
+ if self.separator != None:
+ self.usage('Only one of -c, -S and -s may be specified')
+ return 1
+ self.separator = arg
+ elif opt == '-s':
+ if self.separator != None:
+ self.usage('Only one of -c, -S and -s may be specified')
+ return 1
+ self.separator = ' '
+ elif opt == '-d':
+ self.print_designator = 1
+
+ # if no command - go interactive
+ # wrap in a try/finally so we always close off the db
+ ret = 0
+ try:
+ if not args:
+ self.interactive()
+ else:
+ ret = self.run_command(args)
+ if self.db: self.db.commit()
+ return ret
+ finally:
+ if self.db:
+ self.db.close()
+
+if __name__ == '__main__':
+ tool = AdminTool()
+ sys.exit(tool.main())
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/backends/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,3 @@
+*.pyc
+*.pyo
+*.cover
Added: tracker/vendor/roundup/current/roundup/backends/__init__.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/__init__.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,87 @@
+#
+# 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: __init__.py,v 1.36 2006/01/25 03:24:09 richard Exp $
+
+'''Container for the hyperdb storage backend implementations.
+'''
+__docformat__ = 'restructuredtext'
+
+import sys
+
+# These names are used to suppress import errors.
+# If get_backend raises an ImportError with appropriate
+# module name, have_backend quietly returns False.
+# Otherwise the error is reraised.
+_modules = {
+ 'mysql': 'MySQLdb',
+ 'postgresql': 'psycopg',
+ 'tsearch2': 'psycopg',
+}
+
+def get_backend(name):
+ '''Get a specific backend by name.'''
+ vars = globals()
+ # if requested backend has been imported yet, return current instance
+ if vars.has_key(name):
+ return vars[name]
+ # import the backend module
+ module_name = 'back_%s' % name
+ try:
+ module = __import__(module_name, vars)
+ except:
+ # import failed, but in versions prior to 2.4, a (broken)
+ # module is left in sys.modules and package globals;
+ # subsequent imports would succeed and get the broken module.
+ # This no longer happens in Python 2.4 and later.
+ if sys.version_info < (2, 4):
+ del sys.modules['.'.join((__name__, module_name))]
+ del vars[module_name]
+ raise
+ else:
+ vars[name] = module
+ return module
+
+def have_backend(name):
+ '''Is backend "name" available?'''
+ if name == 'tsearch2':
+ # currently not working
+ return 0
+ try:
+ get_backend(name)
+ return 1
+ except ImportError, e:
+ global _modules
+ if not str(e).startswith('No module named %s'
+ % _modules.get(name, name)):
+ raise
+ return 0
+
+def list_backends():
+ '''List all available backend names.
+
+ This function has side-effect of registering backward-compatible
+ globals for all available backends.
+
+ '''
+ l = []
+ for name in 'anydbm', 'mysql', 'sqlite', 'metakit', 'postgresql':
+ if have_backend(name):
+ l.append(name)
+ return l
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/backends/back_anydbm.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/back_anydbm.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2158 @@
+#
+# 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: back_anydbm.py,v 1.199 2006/04/27 04:59:37 richard Exp $
+'''This module defines a backend that saves the hyperdatabase in a
+database chosen by anydbm. It is guaranteed to always be available in python
+versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
+serious bugs, and is not available)
+'''
+__docformat__ = 'restructuredtext'
+
+try:
+ import anydbm, sys
+ # dumbdbm only works in python 2.1.2+
+ if sys.version_info < (2,1,2):
+ import dumbdbm
+ assert anydbm._defaultmod != dumbdbm
+ del dumbdbm
+except AssertionError:
+ print "WARNING: you should upgrade to python 2.1.3"
+
+import whichdb, os, marshal, re, weakref, string, copy, time, shutil, logging
+
+from roundup import hyperdb, date, password, roundupdb, security, support
+from roundup.backends import locking
+from roundup.i18n import _
+
+from blobfiles import FileStorage
+from sessions_dbm import Sessions, OneTimeKeys
+
+try:
+ from indexer_xapian import Indexer
+except ImportError:
+ from indexer_dbm import Indexer
+
+def db_exists(config):
+ # check for the user db
+ for db in 'nodes.user nodes.user.db'.split():
+ if os.path.exists(os.path.join(config.DATABASE, db)):
+ return 1
+ return 0
+
+def db_nuke(config):
+ shutil.rmtree(config.DATABASE)
+
+#
+# Now the database
+#
+class Database(FileStorage, hyperdb.Database, roundupdb.Database):
+ '''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.
+
+ The 'storagelocator' is obtained from config.DATABASE.
+ The meaning of 'storagelocator' depends on the particular
+ implementation of the hyperdatabase. It could be a file name,
+ a directory path, a socket descriptor for a connection to a
+ database over the network, etc.
+
+ The 'journaltag' is a token that will be attached to the journal
+ entries for any edits done on the database. If 'journaltag' is
+ None, the database is opened in read-only mode: the Class.create(),
+ Class.set(), Class.retire(), and Class.restore() methods are
+ disabled.
+ '''
+ self.config, self.journaltag = config, journaltag
+ self.dir = config.DATABASE
+ self.classes = {}
+ self.cache = {} # cache of nodes loaded or created
+ self.stats = {'cache_hits': 0, 'cache_misses': 0, 'get_items': 0,
+ 'filtering': 0}
+ self.dirtynodes = {} # keep track of the dirty nodes by class
+ self.newnodes = {} # keep track of the new nodes by class
+ self.destroyednodes = {}# keep track of the destroyed nodes by class
+ self.transactions = []
+ self.indexer = Indexer(self)
+ self.security = security.Security(self)
+ os.umask(config.UMASK)
+
+ # lock it
+ lockfilenm = os.path.join(self.dir, 'lock')
+ self.lockfile = locking.acquire_lock(lockfilenm)
+ self.lockfile.write(str(os.getpid()))
+ self.lockfile.flush()
+
+ def post_init(self):
+ '''Called once the schema initialisation has finished.
+ '''
+ # reindex the db if necessary
+ if self.indexer.should_reindex():
+ self.reindex()
+
+ def refresh_database(self):
+ """Rebuild the database
+ """
+ self.reindex()
+
+ def getSessionManager(self):
+ return Sessions(self)
+
+ def getOTKManager(self):
+ return OneTimeKeys(self)
+
+ def reindex(self, classname=None, show_progress=False):
+ if classname:
+ classes = [self.getclass(classname)]
+ else:
+ classes = self.classes.values()
+ for klass in classes:
+ if show_progress:
+ for nodeid in support.Progress('Reindex %s'%klass.classname,
+ klass.list()):
+ klass.index(nodeid)
+ else:
+ for nodeid in klass.list():
+ klass.index(nodeid)
+ self.indexer.save_index()
+
+ def __repr__(self):
+ return '<back_anydbm instance at %x>'%id(self)
+
+ #
+ # Classes
+ #
+ def __getattr__(self, classname):
+ '''A convenient way of calling self.getclass(classname).'''
+ if self.classes.has_key(classname):
+ return self.classes[classname]
+ raise AttributeError, classname
+
+ def addclass(self, cl):
+ cn = cl.classname
+ if self.classes.has_key(cn):
+ raise ValueError, cn
+ self.classes[cn] = cl
+
+ # add default Edit and View permissions
+ self.security.addPermission(name="Create", klass=cn,
+ description="User is allowed to create "+cn)
+ self.security.addPermission(name="Edit", klass=cn,
+ description="User is allowed to edit "+cn)
+ self.security.addPermission(name="View", klass=cn,
+ description="User is allowed to access "+cn)
+
+ def getclasses(self):
+ '''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.
+
+ If 'classname' is not a valid class name, a KeyError is raised.
+ '''
+ try:
+ return self.classes[classname]
+ except KeyError:
+ raise KeyError, 'There is no class called "%s"'%classname
+
+ #
+ # Class DBs
+ #
+ def clear(self):
+ '''Delete all database contents
+ '''
+ logging.getLogger('hyperdb').info('clear')
+ for cn in self.classes.keys():
+ for dummy in 'nodes', 'journals':
+ path = os.path.join(self.dir, 'journals.%s'%cn)
+ if os.path.exists(path):
+ os.remove(path)
+ elif os.path.exists(path+'.db'): # dbm appends .db
+ os.remove(path+'.db')
+ # reset id sequences
+ path = os.path.join(os.getcwd(), self.dir, '_ids')
+ if os.path.exists(path):
+ os.remove(path)
+ elif os.path.exists(path+'.db'): # dbm appends .db
+ os.remove(path+'.db')
+
+ def getclassdb(self, classname, mode='r'):
+ ''' 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
+ '''
+ db_type = ''
+ if os.path.exists(path):
+ db_type = whichdb.whichdb(path)
+ if not db_type:
+ raise hyperdb.DatabaseError, "Couldn't identify database type"
+ elif os.path.exists(path+'.db'):
+ # if the path ends in '.db', it's a dbm database, whether
+ # anydbm says it's dbhash or not!
+ db_type = 'dbm'
+ return db_type
+
+ def opendb(self, name, mode):
+ '''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)
+
+ # new database? let anydbm pick the best dbm
+ if not db_type:
+ if __debug__:
+ logging.getLogger('hyperdb').debug("opendb anydbm.open(%r, 'c')"%path)
+ return anydbm.open(path, 'c')
+
+ # open the database with the correct module
+ try:
+ dbm = __import__(db_type)
+ except ImportError:
+ raise hyperdb.DatabaseError, \
+ "Couldn't open database - the required module '%s'"\
+ " is not available"%db_type
+ if __debug__:
+ logging.getLogger('hyperdb').debug("opendb %r.open(%r, %r)"%(db_type, path,
+ mode))
+ return dbm.open(path, mode)
+
+ #
+ # Node IDs
+ #
+ def newid(self, classname):
+ ''' 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):
+ newid = db[classname] = str(int(db[classname]) + 1)
+ else:
+ # the count() bit is transitional - older dbs won't start at 1
+ newid = str(self.getclass(classname).count()+1)
+ db[classname] = newid
+ db.close()
+ return newid
+
+ def setid(self, classname, setid):
+ ''' 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)
+ db.close()
+
+ #
+ # Nodes
+ #
+ def addnode(self, classname, nodeid, node):
+ ''' 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
+ # calling code's node assumptions)
+ node = node.copy()
+ node['creator'] = self.getuid()
+ node['actor'] = self.getuid()
+ node['creation'] = node['activity'] = date.Date()
+
+ self.newnodes.setdefault(classname, {})[nodeid] = 1
+ self.cache.setdefault(classname, {})[nodeid] = node
+ self.savenode(classname, nodeid, node)
+
+ def setnode(self, classname, nodeid, node):
+ ''' change the specified node
+ '''
+ self.dirtynodes.setdefault(classname, {})[nodeid] = 1
+
+ # can't set without having already loaded the node
+ self.cache[classname][nodeid] = node
+ self.savenode(classname, nodeid, node)
+
+ def savenode(self, classname, nodeid, node):
+ ''' 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
+
+ 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):
+ if __debug__:
+ logging.getLogger('hyperdb').debug('get %s%s cached'%(classname, nodeid))
+ self.stats['cache_hits'] += 1
+ return cache_dict[nodeid]
+
+ if __debug__:
+ self.stats['cache_misses'] += 1
+ start_t = time.time()
+ logging.getLogger('hyperdb').debug('get %s%s'%(classname, nodeid))
+
+ # get from the database and save in the cache
+ if db is None:
+ db = self.getclassdb(classname)
+ if not db.has_key(nodeid):
+ raise IndexError, "no such %s %s"%(classname, nodeid)
+
+ # check the uncommitted, destroyed nodes
+ if (self.destroyednodes.has_key(classname) and
+ self.destroyednodes[classname].has_key(nodeid)):
+ raise IndexError, "no such %s %s"%(classname, nodeid)
+
+ # decode
+ res = marshal.loads(db[nodeid])
+
+ # reverse the serialisation
+ res = self.unserialise(classname, res)
+
+ # store off in the cache dict
+ if cache:
+ cache_dict[nodeid] = res
+
+ if __debug__:
+ self.stats['get_items'] += (time.time() - start_t)
+
+ return res
+
+ def destroynode(self, classname, nodeid):
+ '''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
+ if (self.cache.has_key(classname) and
+ self.cache[classname].has_key(nodeid)):
+ del self.cache[classname][nodeid]
+ if (self.newnodes.has_key(classname) and
+ self.newnodes[classname].has_key(nodeid)):
+ del self.newnodes[classname][nodeid]
+
+ # see if there's any obvious commit actions that we should get rid of
+ for entry in self.transactions[:]:
+ if entry[1][:2] == (classname, nodeid):
+ self.transactions.remove(entry)
+
+ # add to the destroyednodes map
+ self.destroyednodes.setdefault(classname, {})[nodeid] = 1
+
+ # add the destroy commit action
+ self.transactions.append((self.doDestroyNode, (classname, nodeid)))
+
+ def serialise(self, classname, node):
+ '''Copy the node contents, converting non-marshallable data into
+ marshallable data.
+ '''
+ properties = self.getclass(classname).getprops()
+ d = {}
+ for k, v in node.items():
+ if k == self.RETIRED_FLAG:
+ d[k] = v
+ continue
+
+ # if the property doesn't exist then we really don't care
+ if not properties.has_key(k):
+ continue
+
+ # get the property spec
+ prop = properties[k]
+
+ if isinstance(prop, hyperdb.Password) and v is not None:
+ d[k] = str(v)
+ elif isinstance(prop, hyperdb.Date) and v is not None:
+ d[k] = v.serialise()
+ elif isinstance(prop, hyperdb.Interval) and v is not None:
+ d[k] = v.serialise()
+ else:
+ d[k] = v
+ return d
+
+ def unserialise(self, classname, node):
+ '''Decode the marshalled node data
+ '''
+ properties = self.getclass(classname).getprops()
+ d = {}
+ for k, v in node.items():
+ # if the property doesn't exist, or is the "retired" flag then
+ # it won't be in the properties dict
+ if not properties.has_key(k):
+ d[k] = v
+ continue
+
+ # get the property spec
+ prop = properties[k]
+
+ if isinstance(prop, hyperdb.Date) and v is not None:
+ d[k] = date.Date(v)
+ elif isinstance(prop, hyperdb.Interval) and v is not None:
+ d[k] = date.Interval(v)
+ elif isinstance(prop, hyperdb.Password) and v is not None:
+ p = password.Password()
+ p.unpack(v)
+ d[k] = p
+ else:
+ d[k] = v
+ return d
+
+ def hasnode(self, classname, nodeid, db=None):
+ ''' determine if the database has a given node
+ '''
+ # try the cache
+ cache = self.cache.setdefault(classname, {})
+ if cache.has_key(nodeid):
+ return 1
+
+ # not in the cache - check the database
+ if db is None:
+ db = self.getclassdb(classname)
+ res = db.has_key(nodeid)
+ return res
+
+ def countnodes(self, classname, db=None):
+ count = 0
+
+ # include the uncommitted nodes
+ if self.newnodes.has_key(classname):
+ count += len(self.newnodes[classname])
+ if self.destroyednodes.has_key(classname):
+ count -= len(self.destroyednodes[classname])
+
+ # and count those in the DB
+ if db is None:
+ db = self.getclassdb(classname)
+ count = count + len(db.keys())
+ return count
+
+
+ #
+ # Files - special node properties
+ # inherited from FileStorage
+
+ #
+ # Journal
+ #
+ def addjournal(self, classname, nodeid, action, params, creator=None,
+ creation=None):
+ ''' Journal the Action
+ 'action' may be:
+
+ 'create' or 'set' -- 'params' is a dictionary of property values
+ 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
+ 'retire' -- 'params' is None
+
+ '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))
+ if creator is None:
+ creator = self.getuid()
+ self.transactions.append((self.doSaveJournal, (classname, nodeid,
+ action, params, creator, creation)))
+
+ def setjournal(self, classname, nodeid, journal):
+ '''Set the journal to the "journal" list.'''
+ if __debug__:
+ logging.getLogger('hyperdb').debug('setjournal %s%s %r'%(classname,
+ nodeid, journal))
+ self.transactions.append((self.doSetJournal, (classname, nodeid,
+ journal)))
+
+ def getjournal(self, classname, nodeid):
+ ''' get the journal for id
+
+ Raise IndexError if the node doesn't exist (as per history()'s
+ API)
+ '''
+ # our journal result
+ res = []
+
+ # add any journal entries for transactions not committed to the
+ # database
+ for method, args in self.transactions:
+ if method != self.doSaveJournal:
+ continue
+ (cache_classname, cache_nodeid, cache_action, cache_params,
+ cache_creator, cache_creation) = args
+ if cache_classname == classname and cache_nodeid == nodeid:
+ if not cache_creator:
+ cache_creator = self.getuid()
+ if not cache_creation:
+ cache_creation = date.Date()
+ res.append((cache_nodeid, cache_creation, cache_creator,
+ cache_action, cache_params))
+
+ # attempt to open the journal - in some rare cases, the journal may
+ # not exist
+ try:
+ db = self.opendb('journals.%s'%classname, 'r')
+ except anydbm.error, error:
+ if str(error) == "need 'c' or 'n' flag to open new db":
+ raise IndexError, 'no such %s %s'%(classname, nodeid)
+ elif error.args[0] != 2:
+ # this isn't a "not found" error, be alarmed!
+ raise
+ if res:
+ # we have unsaved journal entries, return them
+ return res
+ raise IndexError, 'no such %s %s'%(classname, nodeid)
+ try:
+ journal = marshal.loads(db[nodeid])
+ except KeyError:
+ db.close()
+ if res:
+ # we have some unsaved journal entries, be happy!
+ return res
+ raise IndexError, 'no such %s %s'%(classname, nodeid)
+ db.close()
+
+ # add all the saved journal entries for this node
+ for nodeid, date_stamp, user, action, params in journal:
+ res.append((nodeid, date.Date(date_stamp), user, action, params))
+ return res
+
+ def pack(self, pack_before):
+ ''' Delete all journal entries except "create" before 'pack_before'.
+ '''
+ pack_before = pack_before.serialise()
+ for classname in self.getclasses():
+ packed = 0
+ # get the journal db
+ db_name = 'journals.%s'%classname
+ path = os.path.join(os.getcwd(), self.dir, classname)
+ db_type = self.determine_db_type(path)
+ db = self.opendb(db_name, 'w')
+
+ for key in db.keys():
+ # get the journal for this db entry
+ journal = marshal.loads(db[key])
+ l = []
+ last_set_entry = None
+ for entry in journal:
+ # unpack the entry
+ (nodeid, date_stamp, self.journaltag, action,
+ params) = entry
+ # if the entry is after the pack date, _or_ the initial
+ # create entry, then it stays
+ if date_stamp > pack_before or action == 'create':
+ l.append(entry)
+ else:
+ packed += 1
+ db[key] = marshal.dumps(l)
+
+ logging.getLogger('hyperdb').info('packed %d %s items'%(packed,
+ classname))
+
+ if db_type == 'gdbm':
+ db.reorganize()
+ db.close()
+
+
+ #
+ # Basic transaction support
+ #
+ def commit(self):
+ ''' Commit the current transactions.
+ '''
+ logging.getLogger('hyperdb').info('commit %s transactions'%(
+ len(self.transactions)))
+
+ # keep a handle to all the database files opened
+ self.databases = {}
+
+ try:
+ # now, do all the transactions
+ reindex = {}
+ for method, args in self.transactions:
+ reindex[method(*args)] = 1
+ finally:
+ # make sure we close all the database files
+ for db in self.databases.values():
+ db.close()
+ del self.databases
+
+ # reindex the nodes that request it
+ for classname, nodeid in filter(None, reindex.keys()):
+ self.getclass(classname).index(nodeid)
+
+ # save the indexer state
+ self.indexer.save_index()
+
+ self.clearCache()
+
+ def clearCache(self):
+ # all transactions committed, back to normal
+ self.cache = {}
+ self.dirtynodes = {}
+ self.newnodes = {}
+ self.destroyednodes = {}
+ self.transactions = []
+
+ def getCachedClassDB(self, classname):
+ ''' 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):
+ self.databases[db_name] = self.getclassdb(classname, 'c')
+ return self.databases[db_name]
+
+ def doSaveNode(self, classname, nodeid, node):
+ db = self.getCachedClassDB(classname)
+
+ # now save the marshalled data
+ db[nodeid] = marshal.dumps(self.serialise(classname, node))
+
+ # return the classname, nodeid so we reindex this content
+ return (classname, nodeid)
+
+ def getCachedJournalDB(self, classname):
+ ''' 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):
+ self.databases[db_name] = self.opendb(db_name, 'c')
+ return self.databases[db_name]
+
+ def doSaveJournal(self, classname, nodeid, action, params, creator,
+ creation):
+ # serialise the parameters now if necessary
+ if isinstance(params, type({})):
+ if action in ('set', 'create'):
+ params = self.serialise(classname, params)
+
+ # handle supply of the special journalling parameters (usually
+ # supplied on importing an existing database)
+ journaltag = creator
+ if creation:
+ journaldate = creation.serialise()
+ else:
+ journaldate = date.Date().serialise()
+
+ # create the journal entry
+ entry = (nodeid, journaldate, journaltag, action, params)
+
+ db = self.getCachedJournalDB(classname)
+
+ # now insert the journal entry
+ if db.has_key(nodeid):
+ # append to existing
+ s = db[nodeid]
+ l = marshal.loads(s)
+ l.append(entry)
+ else:
+ l = [entry]
+
+ db[nodeid] = marshal.dumps(l)
+
+ def doSetJournal(self, classname, nodeid, journal):
+ l = []
+ for nodeid, journaldate, journaltag, action, params in journal:
+ # serialise the parameters now if necessary
+ if isinstance(params, type({})):
+ if action in ('set', 'create'):
+ params = self.serialise(classname, params)
+ journaldate = journaldate.serialise()
+ l.append((nodeid, journaldate, journaltag, action, params))
+ db = self.getCachedJournalDB(classname)
+ db[nodeid] = marshal.dumps(l)
+
+ def doDestroyNode(self, classname, nodeid):
+ # delete from the class database
+ db = self.getCachedClassDB(classname)
+ if db.has_key(nodeid):
+ del db[nodeid]
+
+ # delete from the database
+ db = self.getCachedJournalDB(classname)
+ if db.has_key(nodeid):
+ del db[nodeid]
+
+ # return the classname, nodeid so we reindex this content
+ return (classname, nodeid)
+
+ def rollback(self):
+ ''' Reverse all actions from the current transaction.
+ '''
+ logging.getLogger('hyperdb').info('rollback %s transactions'%(
+ len(self.transactions)))
+
+ for method, args in self.transactions:
+ # delete temporary files
+ if method == self.doStoreFile:
+ self.rollbackStoreFile(*args)
+ self.cache = {}
+ self.dirtynodes = {}
+ self.newnodes = {}
+ self.destroyednodes = {}
+ self.transactions = []
+
+ def close(self):
+ ''' Nothing to do
+ '''
+ if self.lockfile is not None:
+ locking.release_lock(self.lockfile)
+ self.lockfile.close()
+ self.lockfile = None
+
+_marker = []
+class Class(hyperdb.Class):
+ '''The handle to a particular class of nodes in a hyperdatabase.'''
+
+ def enableJournalling(self):
+ '''Turn journalling on for this class
+ '''
+ self.do_journal = 1
+
+ def disableJournalling(self):
+ '''Turn journalling off for this class
+ '''
+ self.do_journal = 0
+
+ # Editing nodes:
+
+ def create(self, **propvalues):
+ '''Create a new node of this class and return its id.
+
+ The keyword arguments in 'propvalues' map property names to values.
+
+ The values of arguments must be acceptable for the types of their
+ corresponding properties or a TypeError is raised.
+
+ If this class has a key property, it must be present and its value
+ must not collide with other key strings or a ValueError is raised.
+
+ Any other properties on this class that are missing from the
+ 'propvalues' dictionary are set to None.
+
+ If an id in a link or multilink property does not refer to a valid
+ node, an IndexError is raised.
+
+ 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.
+ '''
+ if propvalues.has_key('id'):
+ raise KeyError, '"id" is reserved'
+
+ if self.db.journaltag is None:
+ raise hyperdb.DatabaseError, 'Database open read-only'
+
+ if propvalues.has_key('creation') or propvalues.has_key('activity'):
+ raise KeyError, '"creation" and "activity" are reserved'
+ # new node's id
+ newid = self.db.newid(self.classname)
+
+ # validate propvalues
+ num_re = re.compile('^\d+$')
+ for key, value in propvalues.items():
+ if key == self.key:
+ try:
+ self.lookup(value)
+ except KeyError:
+ pass
+ else:
+ raise ValueError, 'node with key "%s" exists'%value
+
+ # try to handle this property
+ try:
+ prop = self.properties[key]
+ except KeyError:
+ raise KeyError, '"%s" has no property "%s"'%(self.classname,
+ key)
+
+ if value is not None and isinstance(prop, hyperdb.Link):
+ if type(value) != type(''):
+ raise ValueError, 'link value must be String'
+ link_class = self.properties[key].classname
+ # if it isn't a number, it's a key
+ if not num_re.match(value):
+ try:
+ value = self.db.classes[link_class].lookup(value)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ key, value, link_class)
+ elif not self.db.getclass(link_class).hasnode(value):
+ raise IndexError, '%s has no node %s'%(link_class, value)
+
+ # save off the value
+ propvalues[key] = value
+
+ # register the link with the newly linked node
+ if self.do_journal and self.properties[key].do_journal:
+ self.db.addjournal(link_class, value, 'link',
+ (self.classname, newid, key))
+
+ elif isinstance(prop, hyperdb.Multilink):
+ if type(value) != type([]):
+ raise TypeError, 'new property "%s" not a list of ids'%key
+
+ # clean up and validate the list of links
+ link_class = self.properties[key].classname
+ l = []
+ for entry in value:
+ if type(entry) != type(''):
+ raise ValueError, '"%s" multilink value (%r) '\
+ 'must contain Strings'%(key, value)
+ # if it isn't a number, it's a key
+ if not num_re.match(entry):
+ try:
+ entry = self.db.classes[link_class].lookup(entry)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ key, entry, self.properties[key].classname)
+ l.append(entry)
+ value = l
+ propvalues[key] = value
+
+ # handle additions
+ for nodeid in value:
+ if not self.db.getclass(link_class).hasnode(nodeid):
+ raise IndexError, '%s has no node %s'%(link_class,
+ nodeid)
+ # register the link with the newly linked node
+ if self.do_journal and self.properties[key].do_journal:
+ self.db.addjournal(link_class, nodeid, 'link',
+ (self.classname, newid, key))
+
+ elif isinstance(prop, hyperdb.String):
+ if type(value) != type('') and type(value) != type(u''):
+ raise TypeError, 'new property "%s" not a string'%key
+ if prop.indexme:
+ self.db.indexer.add_text((self.classname, newid, key),
+ value)
+
+ elif isinstance(prop, hyperdb.Password):
+ if not isinstance(value, password.Password):
+ raise TypeError, 'new property "%s" not a Password'%key
+
+ elif isinstance(prop, hyperdb.Date):
+ if value is not None and not isinstance(value, date.Date):
+ raise TypeError, 'new property "%s" not a Date'%key
+
+ elif isinstance(prop, hyperdb.Interval):
+ if value is not None and not isinstance(value, date.Interval):
+ raise TypeError, 'new property "%s" not an Interval'%key
+
+ elif value is not None and isinstance(prop, hyperdb.Number):
+ try:
+ float(value)
+ except ValueError:
+ raise TypeError, 'new property "%s" not numeric'%key
+
+ elif value is not None and isinstance(prop, hyperdb.Boolean):
+ try:
+ int(value)
+ except ValueError:
+ raise TypeError, 'new property "%s" not boolean'%key
+
+ # make sure there's data where there needs to be
+ for key, prop in self.properties.items():
+ if propvalues.has_key(key):
+ continue
+ if key == self.key:
+ raise ValueError, 'key property "%s" is required'%key
+ if isinstance(prop, hyperdb.Multilink):
+ propvalues[key] = []
+
+ # done
+ self.db.addnode(self.classname, newid, propvalues)
+ if self.do_journal:
+ self.db.addjournal(self.classname, newid, 'create', {})
+
+ return newid
+
+ def get(self, nodeid, propname, default=_marker, cache=1):
+ '''Get the value of a property on an existing node of this class.
+
+ 'nodeid' must be the id of an existing node of this class or an
+ IndexError is raised. 'propname' must be the name of a property
+ of this class or a KeyError is raised.
+
+ 'cache' exists for backward compatibility, and is not used.
+
+ Attempts to get the "creation" or "activity" properties should
+ do the right thing.
+ '''
+ if propname == 'id':
+ return nodeid
+
+ # get the node's dict
+ d = self.db.getnode(self.classname, nodeid)
+
+ # check for one of the special props
+ if propname == 'creation':
+ if d.has_key('creation'):
+ return d['creation']
+ if not self.do_journal:
+ raise ValueError, 'Journalling is disabled for this class'
+ journal = self.db.getjournal(self.classname, nodeid)
+ if journal:
+ return self.db.getjournal(self.classname, nodeid)[0][1]
+ else:
+ # on the strange chance that there's no journal
+ return date.Date()
+ if propname == 'activity':
+ if d.has_key('activity'):
+ return d['activity']
+ if not self.do_journal:
+ raise ValueError, 'Journalling is disabled for this class'
+ journal = self.db.getjournal(self.classname, nodeid)
+ if journal:
+ return self.db.getjournal(self.classname, nodeid)[-1][1]
+ else:
+ # on the strange chance that there's no journal
+ return date.Date()
+ if propname == 'creator':
+ if d.has_key('creator'):
+ return d['creator']
+ if not self.do_journal:
+ raise ValueError, 'Journalling is disabled for this class'
+ journal = self.db.getjournal(self.classname, nodeid)
+ if journal:
+ num_re = re.compile('^\d+$')
+ value = journal[0][2]
+ if num_re.match(value):
+ return value
+ else:
+ # old-style "username" journal tag
+ try:
+ return self.db.user.lookup(value)
+ except KeyError:
+ # user's been retired, return admin
+ return '1'
+ else:
+ return self.db.getuid()
+ if propname == 'actor':
+ if d.has_key('actor'):
+ return d['actor']
+ if not self.do_journal:
+ raise ValueError, 'Journalling is disabled for this class'
+ journal = self.db.getjournal(self.classname, nodeid)
+ if journal:
+ num_re = re.compile('^\d+$')
+ value = journal[-1][2]
+ if num_re.match(value):
+ return value
+ else:
+ # old-style "username" journal tag
+ try:
+ return self.db.user.lookup(value)
+ except KeyError:
+ # user's been retired, return admin
+ return '1'
+ else:
+ return self.db.getuid()
+
+ # get the property (raises KeyErorr if invalid)
+ prop = self.properties[propname]
+
+ if not d.has_key(propname):
+ if default is _marker:
+ if isinstance(prop, hyperdb.Multilink):
+ return []
+ else:
+ return None
+ else:
+ return default
+
+ # return a dupe of the list so code doesn't get confused
+ if isinstance(prop, hyperdb.Multilink):
+ return d[propname][:]
+
+ return d[propname]
+
+ def set(self, nodeid, **propvalues):
+ '''Modify a property on an existing node of this class.
+
+ 'nodeid' must be the id of an existing node of this class or an
+ IndexError is raised.
+
+ Each key in 'propvalues' must be the name of a property of this
+ class or a KeyError is raised.
+
+ All values in 'propvalues' must be acceptable types for their
+ corresponding properties or a TypeError is raised.
+
+ If the value of the key property is set, it must not collide with
+ other key strings or a ValueError is raised.
+
+ If the value of a Link or Multilink property contains an invalid
+ node id, a ValueError is raised.
+
+ 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():
+ if oldvalues.has_key(name):
+ continue
+ if isinstance(prop, hyperdb.Multilink):
+ oldvalues[name] = []
+ else:
+ oldvalues[name] = None
+ propvalues = self.set_inner(nodeid, **propvalues)
+ self.fireReactors('set', nodeid, oldvalues)
+ return propvalues
+
+ def set_inner(self, nodeid, **propvalues):
+ ''' Called by set, in-between the audit and react calls.
+ '''
+ if not propvalues:
+ return propvalues
+
+ if propvalues.has_key('creation') or propvalues.has_key('activity'):
+ raise KeyError, '"creation" and "activity" are reserved'
+
+ if propvalues.has_key('id'):
+ raise KeyError, '"id" is reserved'
+
+ if self.db.journaltag is None:
+ raise hyperdb.DatabaseError, 'Database open read-only'
+
+ node = self.db.getnode(self.classname, nodeid)
+ if node.has_key(self.db.RETIRED_FLAG):
+ raise IndexError
+ num_re = re.compile('^\d+$')
+
+ # if the journal value is to be different, store it in here
+ journalvalues = {}
+
+ for propname, value in propvalues.items():
+ # check to make sure we're not duplicating an existing key
+ if propname == self.key and node[propname] != value:
+ try:
+ self.lookup(value)
+ except KeyError:
+ pass
+ else:
+ raise ValueError, 'node with key "%s" exists'%value
+
+ # this will raise the KeyError if the property isn't valid
+ # ... we don't use getprops() here because we only care about
+ # the writeable properties.
+ try:
+ prop = self.properties[propname]
+ except KeyError:
+ raise KeyError, '"%s" has no property named "%s"'%(
+ self.classname, propname)
+
+ # if the value's the same as the existing value, no sense in
+ # doing anything
+ current = node.get(propname, None)
+ if value == current:
+ del propvalues[propname]
+ continue
+ journalvalues[propname] = current
+
+ # do stuff based on the prop type
+ if isinstance(prop, hyperdb.Link):
+ link_class = prop.classname
+ # if it isn't a number, it's a key
+ if value is not None and not isinstance(value, type('')):
+ raise ValueError, 'property "%s" link value be a string'%(
+ propname)
+ if isinstance(value, type('')) and not num_re.match(value):
+ try:
+ value = self.db.classes[link_class].lookup(value)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ propname, value, prop.classname)
+
+ if (value is not None and
+ not self.db.getclass(link_class).hasnode(value)):
+ raise IndexError, '%s has no node %s'%(link_class, value)
+
+ if self.do_journal and prop.do_journal:
+ # register the unlink with the old linked node
+ if node.has_key(propname) and node[propname] is not None:
+ self.db.addjournal(link_class, node[propname], 'unlink',
+ (self.classname, nodeid, propname))
+
+ # register the link with the newly linked node
+ if value is not None:
+ self.db.addjournal(link_class, value, 'link',
+ (self.classname, nodeid, propname))
+
+ elif isinstance(prop, hyperdb.Multilink):
+ if type(value) != type([]):
+ raise TypeError, 'new property "%s" not a list of'\
+ ' ids'%propname
+ link_class = self.properties[propname].classname
+ l = []
+ for entry in value:
+ # if it isn't a number, it's a key
+ if type(entry) != type(''):
+ raise ValueError, 'new property "%s" link value ' \
+ 'must be a string'%propname
+ if not num_re.match(entry):
+ try:
+ entry = self.db.classes[link_class].lookup(entry)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ propname, entry,
+ self.properties[propname].classname)
+ l.append(entry)
+ value = l
+ propvalues[propname] = value
+
+ # figure the journal entry for this property
+ add = []
+ remove = []
+
+ # handle removals
+ if node.has_key(propname):
+ l = node[propname]
+ else:
+ l = []
+ for id in l[:]:
+ if id in value:
+ continue
+ # register the unlink with the old linked node
+ if self.do_journal and self.properties[propname].do_journal:
+ self.db.addjournal(link_class, id, 'unlink',
+ (self.classname, nodeid, propname))
+ l.remove(id)
+ remove.append(id)
+
+ # 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
+ # 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',
+ (self.classname, nodeid, propname))
+ l.append(id)
+ add.append(id)
+
+ # figure the journal entry
+ l = []
+ if add:
+ l.append(('+', add))
+ if remove:
+ l.append(('-', remove))
+ if l:
+ journalvalues[propname] = tuple(l)
+
+ elif isinstance(prop, hyperdb.String):
+ if value is not None and type(value) != type('') and type(value) != type(u''):
+ raise TypeError, 'new property "%s" not a string'%propname
+ if prop.indexme:
+ self.db.indexer.add_text((self.classname, nodeid, propname),
+ value)
+
+ elif isinstance(prop, hyperdb.Password):
+ if not isinstance(value, password.Password):
+ raise TypeError, 'new property "%s" not a Password'%propname
+ propvalues[propname] = value
+
+ elif value is not None and isinstance(prop, hyperdb.Date):
+ if not isinstance(value, date.Date):
+ raise TypeError, 'new property "%s" not a Date'% propname
+ propvalues[propname] = value
+
+ elif value is not None and isinstance(prop, hyperdb.Interval):
+ if not isinstance(value, date.Interval):
+ raise TypeError, 'new property "%s" not an '\
+ 'Interval'%propname
+ propvalues[propname] = value
+
+ elif value is not None and isinstance(prop, hyperdb.Number):
+ try:
+ float(value)
+ except ValueError:
+ raise TypeError, 'new property "%s" not numeric'%propname
+
+ elif value is not None and isinstance(prop, hyperdb.Boolean):
+ try:
+ int(value)
+ except ValueError:
+ raise TypeError, 'new property "%s" not boolean'%propname
+
+ node[propname] = value
+
+ # nothing to do?
+ if not propvalues:
+ return propvalues
+
+ # update the activity time
+ node['activity'] = date.Date()
+ node['actor'] = self.db.getuid()
+
+ # do the set, and journal it
+ self.db.setnode(self.classname, nodeid, node)
+
+ if self.do_journal:
+ self.db.addjournal(self.classname, nodeid, 'set', journalvalues)
+
+ return propvalues
+
+ def retire(self, nodeid):
+ '''Retire a node.
+
+ The properties on the node remain available from the get() method,
+ and the node's id is never reused.
+
+ Retired nodes are not returned by the find(), list(), or lookup()
+ methods, and other nodes may reuse the values of their key properties.
+
+ 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'
+
+ self.fireAuditors('retire', nodeid, None)
+
+ node = self.db.getnode(self.classname, nodeid)
+ node[self.db.RETIRED_FLAG] = 1
+ self.db.setnode(self.classname, nodeid, node)
+ if self.do_journal:
+ self.db.addjournal(self.classname, nodeid, 'retired', None)
+
+ self.fireReactors('retire', nodeid, None)
+
+ def restore(self, nodeid):
+ '''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'
+
+ node = self.db.getnode(self.classname, nodeid)
+ # check if key property was overrided
+ key = self.getkey()
+ try:
+ id = self.lookup(node[key])
+ except KeyError:
+ pass
+ else:
+ raise KeyError, "Key property (%s) of retired node clashes with \
+ existing one (%s)" % (key, node[key])
+ # Now we can safely restore node
+ self.fireAuditors('restore', nodeid, None)
+ del node[self.db.RETIRED_FLAG]
+ self.db.setnode(self.classname, nodeid, node)
+ if self.do_journal:
+ self.db.addjournal(self.classname, nodeid, 'restored', None)
+
+ self.fireReactors('restore', nodeid, None)
+
+ def is_retired(self, nodeid, cldb=None):
+ '''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.
+
+ WARNING: this method should never be used except in extremely rare
+ situations where there could never be links to the node being
+ deleted
+
+ WARNING: use retire() instead
+
+ WARNING: the properties of this node will not be available ever again
+
+ WARNING: really, use retire() instead
+
+ Well, I think that's enough warnings. This method exists mostly to
+ support the session storage of the cgi interface.
+ '''
+ 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.
+
+ 'nodeid' must be the id of an existing node of this class or an
+ IndexError is raised.
+
+ The returned list contains tuples of the form
+
+ (nodeid, date, tag, action, params)
+
+ 'date' is a Timestamp object specifying the time of the change and
+ 'tag' is the journaltag specified when the database was opened.
+ '''
+ if not self.do_journal:
+ raise ValueError, 'Journalling is disabled for this class'
+ return self.db.getjournal(self.classname, nodeid)
+
+ # Locating nodes:
+ def hasnode(self, nodeid):
+ '''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.
+
+ '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 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.
+
+ 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)
+ try:
+ for nodeid in self.getnodeids(cldb):
+ node = self.db.getnode(self.classname, nodeid, cldb)
+ if node.has_key(self.db.RETIRED_FLAG):
+ continue
+ if not node.has_key(self.key):
+ continue
+ if node[self.key] == keyvalue:
+ return nodeid
+ finally:
+ cldb.close()
+ raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key,
+ keyvalue, self.classname)
+
+ # 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.
+
+ 'propspec' consists of keyword args propname=nodeid or
+ propname={nodeid:1, }
+ 'propname' must be the name of a property in this class, or a
+ KeyError is raised. That property must be a Link or
+ Multilink property, or a TypeError is raised.
+
+ Any node in this class whose 'propname' property links to any of
+ the nodeids will be returned. Examples::
+
+ db.issue.find(messages='1')
+ db.issue.find(messages={'1':1,'3':1}, files={'7':1})
+ '''
+ propspec = propspec.items()
+ for propname, itemids in propspec:
+ # check the prop is OK
+ prop = self.properties[propname]
+ if not isinstance(prop, hyperdb.Link) and not isinstance(prop, hyperdb.Multilink):
+ raise TypeError, "'%s' not a Link/Multilink property"%propname
+
+ # ok, now do the find
+ cldb = self.db.getclassdb(self.classname)
+ l = []
+ try:
+ for id in self.getnodeids(db=cldb):
+ item = self.db.getnode(self.classname, id, db=cldb)
+ if item.has_key(self.db.RETIRED_FLAG):
+ continue
+ for propname, itemids in propspec:
+ if type(itemids) is not type({}):
+ itemids = {itemids:1}
+
+ # special case if the item doesn't have this property
+ if not item.has_key(propname):
+ if itemids.has_key(None):
+ l.append(id)
+ break
+ continue
+
+ # grab the property definition and its value on this item
+ prop = self.properties[propname]
+ value = item[propname]
+ if isinstance(prop, hyperdb.Link) and itemids.has_key(value):
+ l.append(id)
+ break
+ elif isinstance(prop, hyperdb.Multilink):
+ hit = 0
+ for v in value:
+ if itemids.has_key(v):
+ l.append(id)
+ hit = 1
+ break
+ if hit:
+ break
+ finally:
+ cldb.close()
+ return l
+
+ def stringFind(self, **requirements):
+ '''Locate a particular node by matching a set of its String
+ properties in a caseless search.
+
+ If the property is not a String property, a TypeError is raised.
+
+ The return is a list of the id of all nodes that match.
+ '''
+ for propname in requirements.keys():
+ prop = self.properties[propname]
+ if not isinstance(prop, hyperdb.String):
+ raise TypeError, "'%s' not a String property"%propname
+ requirements[propname] = requirements[propname].lower()
+ l = []
+ cldb = self.db.getclassdb(self.classname)
+ try:
+ for nodeid in self.getnodeids(cldb):
+ node = self.db.getnode(self.classname, nodeid, cldb)
+ if node.has_key(self.db.RETIRED_FLAG):
+ continue
+ for key, value in requirements.items():
+ if not node.has_key(key):
+ break
+ if node[key] is None or node[key].lower() != value:
+ break
+ else:
+ l.append(nodeid)
+ finally:
+ cldb.close()
+ return l
+
+ def list(self):
+ ''' Return a list of the ids of the active nodes in this class.
+ '''
+ l = []
+ cn = self.classname
+ cldb = self.db.getclassdb(cn)
+ try:
+ for nodeid in self.getnodeids(cldb):
+ node = self.db.getnode(cn, nodeid, cldb)
+ if node.has_key(self.db.RETIRED_FLAG):
+ continue
+ l.append(nodeid)
+ finally:
+ cldb.close()
+ l.sort()
+ return l
+
+ def getnodeids(self, db=None, retired=None):
+ ''' 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
+ if self.db.newnodes.has_key(self.classname):
+ res += self.db.newnodes[self.classname].keys()
+
+ must_close = False
+ if db is None:
+ db = self.db.getclassdb(self.classname)
+ must_close = True
+ try:
+ res = res + db.keys()
+
+ # remove the uncommitted, destroyed nodes
+ if self.db.destroyednodes.has_key(self.classname):
+ for nodeid in self.db.destroyednodes[self.classname].keys():
+ if db.has_key(nodeid):
+ res.remove(nodeid)
+
+ # check retired flag
+ if retired is False or retired is True:
+ l = []
+ for nodeid in res:
+ node = self.db.getnode(self.classname, nodeid, db)
+ is_ret = node.has_key(self.db.RETIRED_FLAG)
+ if retired == is_ret:
+ l.append(nodeid)
+ res = l
+ finally:
+ if must_close:
+ db.close()
+ return res
+
+ def filter(self, search_matches, filterspec, sort=(None,None),
+ group=(None,None), num_re = re.compile('^\d+$')):
+ """Return a list of the ids of the active nodes in this class that
+ match the 'filter' spec, sorted by the group spec and then the
+ sort spec.
+
+ "filterspec" is {propname: value(s)}
+
+ "sort" and "group" are (dir, prop) where dir is '+', '-' or None
+ and prop is a prop name or None
+
+ "search_matches" is {nodeid: marker} or None
+
+ The filter must match all properties specificed. If the property
+ value to match is a list:
+
+ 1. String properties must match all elements in the list, and
+ 2. Other properties must match any of the elements in the list.
+ """
+ if __debug__:
+ start_t = time.time()
+
+ cn = self.classname
+
+ # optimise filterspec
+ l = []
+ props = self.getprops()
+ LINK = 'spec:link'
+ MULTILINK = 'spec:multilink'
+ STRING = 'spec:string'
+ DATE = 'spec:date'
+ INTERVAL = 'spec:interval'
+ OTHER = 'spec:other'
+
+ for k, v in filterspec.items():
+ propclass = props[k]
+ if isinstance(propclass, hyperdb.Link):
+ if type(v) is not type([]):
+ v = [v]
+ u = []
+ for entry in v:
+ # the value -1 is a special "not set" sentinel
+ if entry == '-1':
+ entry = None
+ u.append(entry)
+ l.append((LINK, k, u))
+ elif isinstance(propclass, hyperdb.Multilink):
+ # the value -1 is a special "not set" sentinel
+ if v in ('-1', ['-1']):
+ v = []
+ elif type(v) is not type([]):
+ v = [v]
+ l.append((MULTILINK, k, v))
+ elif isinstance(propclass, hyperdb.String) and k != 'id':
+ if type(v) is not type([]):
+ v = [v]
+ for v in v:
+ # simple glob searching
+ v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
+ v = v.replace('?', '.')
+ v = v.replace('*', '.*?')
+ l.append((STRING, k, re.compile(v, re.I)))
+ elif isinstance(propclass, hyperdb.Date):
+ try:
+ date_rng = propclass.range_from_raw(v, self.db)
+ l.append((DATE, k, date_rng))
+ except ValueError:
+ # If range creation fails - ignore that search parameter
+ pass
+ elif isinstance(propclass, hyperdb.Interval):
+ try:
+ intv_rng = date.Range(v, date.Interval)
+ l.append((INTERVAL, k, intv_rng))
+ except ValueError:
+ # If range creation fails - ignore that search parameter
+ pass
+
+ elif isinstance(propclass, hyperdb.Boolean):
+ if type(v) != type([]):
+ v = v.split(',')
+ bv = []
+ for val in v:
+ if type(val) is type(''):
+ bv.append(val.lower() in ('yes', 'true', 'on', '1'))
+ else:
+ bv.append(val)
+ l.append((OTHER, k, bv))
+
+ elif k == 'id':
+ if type(v) != type([]):
+ v = v.split(',')
+ l.append((OTHER, k, [str(int(val)) for val in v]))
+
+ elif isinstance(propclass, hyperdb.Number):
+ if type(v) != type([]):
+ v = v.split(',')
+ l.append((OTHER, k, [float(val) for val in v]))
+
+ filterspec = l
+
+ # now, find all the nodes that are active and pass filtering
+ matches = []
+ cldb = self.db.getclassdb(cn)
+ t = 0
+ try:
+ # TODO: only full-scan once (use items())
+ for nodeid in self.getnodeids(cldb):
+ node = self.db.getnode(cn, nodeid, cldb)
+ if node.has_key(self.db.RETIRED_FLAG):
+ continue
+ # apply filter
+ for t, k, v in filterspec:
+ # handle the id prop
+ if k == 'id':
+ if nodeid not in v:
+ break
+ continue
+
+ # get the node value
+ nv = node.get(k, None)
+
+ match = 0
+
+ # now apply the property filter
+ if t == LINK:
+ # link - if this node's property doesn't appear in the
+ # filterspec's nodeid list, skip it
+ match = nv in v
+ elif t == MULTILINK:
+ # multilink - if any of the nodeids required by the
+ # filterspec aren't in this node's property, then skip
+ # it
+ nv = node.get(k, [])
+
+ # check for matching the absence of multilink values
+ if not v:
+ match = not nv
+ else:
+ # othewise, make sure this node has each of the
+ # required values
+ for want in v:
+ if want in nv:
+ match = 1
+ break
+ elif t == STRING:
+ if nv is None:
+ nv = ''
+ # RE search
+ match = v.search(nv)
+ elif t == DATE or t == INTERVAL:
+ if nv is None:
+ match = v is None
+ else:
+ if v.to_value:
+ if v.from_value <= nv and v.to_value >= nv:
+ match = 1
+ else:
+ if v.from_value <= nv:
+ match = 1
+ elif t == OTHER:
+ # straight value comparison for the other types
+ match = nv in v
+ if not match:
+ break
+ else:
+ matches.append([nodeid, node])
+
+ # filter based on full text search
+ if search_matches is not None:
+ k = []
+ for v in matches:
+ if search_matches.has_key(v[0]):
+ k.append(v)
+ matches = k
+
+ # always sort by id if no other sort is specified
+ if sort == (None, None):
+ sort = ('+', 'id')
+
+ # add sorting information to the match entries
+ directions = []
+ JPROPS = {'actor':1, 'activity':1, 'creator':1, 'creation':1}
+ for dir, prop in sort, group:
+ if dir is None or prop is None:
+ continue
+ directions.append(dir)
+ propclass = props[prop]
+ try:
+ # cache the opened link class db, if needed.
+ lcldb = None
+ # cache the linked class items too
+ lcache = {}
+
+ for entry in matches:
+ itemid = entry[-2]
+ item = entry[-1]
+ # handle the properties that might be "faked"
+ # also, handle possible missing properties
+ try:
+ v = item[prop]
+ except KeyError:
+ if JPROPS.has_key(prop):
+ # force lookup of the special journal prop
+ v = self.get(itemid, prop)
+ else:
+ # the node doesn't have a value for this
+ # property
+ if isinstance(propclass, hyperdb.Multilink): v = []
+ else: v = None
+ entry.insert(0, v)
+ continue
+
+ # missing (None) values are always sorted first
+ if v is None:
+ entry.insert(0, v)
+ continue
+
+ if isinstance(propclass, hyperdb.String):
+ # it might be a string that's really an integer
+ try: tv = int(v)
+ except: v = v.lower()
+ else: v = tv
+ elif isinstance(propclass, hyperdb.Link):
+ lcn = propclass.classname
+ link = self.db.classes[lcn]
+ key = link.orderprop()
+ if key!='id':
+ if not lcache.has_key(v):
+ # open the link class db if it's not already
+ if lcldb is None:
+ lcldb = self.db.getclassdb(lcn)
+ lcache[v] = self.db.getnode(lcn, v, lcldb)
+ v = lcache[v][key]
+ entry.insert(0, v)
+ finally:
+ # if we opened the link class db, close it now
+ if lcldb is not None:
+ lcldb.close()
+ del lcache
+ finally:
+ cldb.close()
+
+ # sort vals are inserted, but directions are appended, so reverse
+ directions.reverse()
+
+ if '-' in directions:
+ # one or more of the sort specs is in reverse order, so we have
+ # to use this icky function to sort
+ def sortfun(a, b, directions=directions, n=range(len(directions))):
+ for i in n:
+ if not cmp(a[i], b[i]):
+ continue
+ if directions[i] == '+':
+ # compare in the usual, ascending direction
+ return cmp(a[i],b[i])
+ else:
+ # compare in the reverse, descending direction
+ return cmp(b[i],a[i])
+ # for consistency, sort by the id if the items are equal
+ return cmp(a[-2], b[-2])
+ matches.sort(sortfun)
+ else:
+ # sorting is in the normal, ascending direction
+ matches.sort()
+
+ # pull the id out of the individual entries
+ matches = [entry[-2] for entry in matches]
+ if __debug__:
+ self.db.stats['filtering'] += (time.time() - start_t)
+ return matches
+
+ def count(self):
+ '''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.
+ If the "protected" flag is true, we include protected properties -
+ those which may not be modified.
+
+ In addition to the actual properties on the node, these
+ 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()
+ d['creation'] = hyperdb.Date()
+ d['activity'] = hyperdb.Date()
+ d['creator'] = hyperdb.Link('user')
+ d['actor'] = hyperdb.Link('user')
+ return d
+
+ def addprop(self, **properties):
+ '''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 '''
+ # find all the String properties that have indexme
+ for prop, propclass in self.getprops().items():
+ if isinstance(propclass, hyperdb.String) and propclass.indexme:
+ # index them under (classname, nodeid, property)
+ try:
+ value = str(self.get(nodeid, prop))
+ except IndexError:
+ # node has been destroyed
+ continue
+ self.db.indexer.add_text((self.classname, nodeid, prop), value)
+
+ #
+ # import / export support
+ #
+ def export_list(self, propnames, nodeid):
+ ''' Export a node - generate a list of CSV-able data in the order
+ specified by propnames for the given node.
+ '''
+ properties = self.getprops()
+ l = []
+ for prop in propnames:
+ proptype = properties[prop]
+ value = self.get(nodeid, prop)
+ # "marshal" data where needed
+ if value is None:
+ pass
+ elif isinstance(proptype, hyperdb.Date):
+ value = value.get_tuple()
+ elif isinstance(proptype, hyperdb.Interval):
+ value = value.get_tuple()
+ elif isinstance(proptype, hyperdb.Password):
+ value = str(value)
+ l.append(repr(value))
+
+ # append retired flag
+ l.append(repr(self.is_retired(nodeid)))
+
+ return l
+
+ def import_list(self, propnames, proplist):
+ ''' Import a node - all information including "id" is present and
+ should not be sanity checked. Triggers are not triggered. The
+ journal should be initialised using the "creator" and "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()
+
+ # make the new node's property map
+ d = {}
+ newid = None
+ for i in range(len(propnames)):
+ # Figure the property for this column
+ propname = propnames[i]
+
+ # Use eval to reverse the repr() used to output the CSV
+ value = eval(proplist[i])
+
+ # "unmarshal" where necessary
+ if propname == 'id':
+ newid = value
+ continue
+ elif propname == 'is retired':
+ # is the item retired?
+ if int(value):
+ d[self.db.RETIRED_FLAG] = 1
+ continue
+ elif value is None:
+ d[propname] = None
+ continue
+
+ prop = properties[propname]
+ if isinstance(prop, hyperdb.Date):
+ value = date.Date(value)
+ elif isinstance(prop, hyperdb.Interval):
+ value = date.Interval(value)
+ elif isinstance(prop, hyperdb.Password):
+ pwd = password.Password()
+ pwd.unpack(value)
+ value = pwd
+ d[propname] = value
+
+ # get a new id if necessary
+ if newid is None:
+ newid = self.db.newid(self.classname)
+
+ # add the node and journal
+ self.db.addnode(self.classname, newid, d)
+ return newid
+
+ def export_journals(self):
+ '''Export a class's journal - generate a list of lists of
+ CSV-able data:
+
+ nodeid, date, user, action, params
+
+ No heading here - the columns are fixed.
+ '''
+ properties = self.getprops()
+ r = []
+ for nodeid in self.getnodeids():
+ for nodeid, date, user, action, params in self.history(nodeid):
+ date = date.get_tuple()
+ if action == 'set':
+ export_data = {}
+ for propname, value in params.items():
+ if not properties.has_key(propname):
+ # property no longer in the schema
+ continue
+
+ prop = properties[propname]
+ # make sure the params are eval()'able
+ if value is None:
+ # don't export empties
+ continue
+ elif isinstance(prop, hyperdb.Date):
+ # this is a hack - some dates are stored as strings
+ if not isinstance(value, type('')):
+ value = value.get_tuple()
+ elif isinstance(prop, hyperdb.Interval):
+ # hack too - some intervals are stored as strings
+ if not isinstance(value, type('')):
+ value = value.get_tuple()
+ elif isinstance(prop, hyperdb.Password):
+ value = str(value)
+ export_data[propname] = value
+ params = export_data
+ l = [nodeid, date, user, action, params]
+ r.append(map(repr, l))
+ return r
+
+ def import_journals(self, entries):
+ '''Import a class's journal.
+
+ Uses setjournal() to set the journal for each item.'''
+ properties = self.getprops()
+ d = {}
+ for l in entries:
+ l = map(eval, l)
+ nodeid, jdate, user, action, params = l
+ r = d.setdefault(nodeid, [])
+ if action == 'set':
+ for propname, value in params.items():
+ prop = properties[propname]
+ if value is None:
+ pass
+ elif isinstance(prop, hyperdb.Date):
+ if type(value) == type(()):
+ print _('WARNING: invalid date tuple %r')%(value,)
+ value = date.Date( "2000-1-1" )
+ value = date.Date(value)
+ elif isinstance(prop, hyperdb.Interval):
+ value = date.Interval(value)
+ elif isinstance(prop, hyperdb.Password):
+ pwd = password.Password()
+ pwd.unpack(value)
+ value = pwd
+ params[propname] = value
+ r.append((nodeid, date.Date(jdate), user, action, params))
+
+ for nodeid, l in d.items():
+ self.db.setjournal(self.classname, nodeid, l)
+
+class FileClass(hyperdb.FileClass, Class):
+ '''This class defines a large chunk of data. To support this, it has a
+ 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"
+ and "type" properties.
+ '''
+ if not properties.has_key('content'):
+ properties['content'] = hyperdb.String(indexme='yes')
+ if not properties.has_key('type'):
+ properties['type'] = hyperdb.String()
+ Class.__init__(self, db, classname, **properties)
+
+ def create(self, **propvalues):
+ ''' 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)
+
+ # now remove the content property so it's not stored in the db
+ content = propvalues['content']
+ del propvalues['content']
+
+ # make sure we have a MIME type
+ mime_type = propvalues.get('type', self.default_mime_type)
+
+ # do the database create
+ newid = self.create_inner(**propvalues)
+
+ # 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
+
+ 'cache' exists for backwards compatibility, and is not used.
+ '''
+ poss_msg = 'Possibly an access right configuration problem.'
+ if propname == 'content':
+ try:
+ return self.db.getfile(self.classname, nodeid, None)
+ except IOError, (strerror):
+ # XXX by catching this we don't see an error in the log.
+ return 'ERROR reading file: %s%s\n%s\n%s'%(
+ self.classname, nodeid, poss_msg, strerror)
+ if default is not _marker:
+ return Class.get(self, nodeid, propname, default)
+ else:
+ return Class.get(self, nodeid, propname)
+
+ def set(self, itemid, **propvalues):
+ ''' Snarf the "content" propvalue and update it in a file
+ '''
+ self.fireAuditors('set', itemid, propvalues)
+
+ # create the oldvalues dict - fill in any missing values
+ oldvalues = copy.deepcopy(self.db.getnode(self.classname, itemid))
+ for name,prop in self.getprops(protected=0).items():
+ if oldvalues.has_key(name):
+ continue
+ if isinstance(prop, hyperdb.Multilink):
+ oldvalues[name] = []
+ else:
+ oldvalues[name] = None
+
+ # now remove the content property so it's not stored in the db
+ content = None
+ if propvalues.has_key('content'):
+ content = propvalues['content']
+ del propvalues['content']
+
+ # do the database update
+ propvalues = self.set_inner(itemid, **propvalues)
+
+ # do content?
+ if content:
+ # store and possibly index
+ self.db.storefile(self.classname, itemid, None, content)
+ if self.properties['content'].indexme:
+ mime_type = self.get(itemid, 'type', self.default_mime_type)
+ self.db.indexer.add_text((self.classname, itemid, 'content'),
+ content, mime_type)
+ propvalues['content'] = content
+
+ # fire reactors
+ self.fireReactors('set', itemid, oldvalues)
+ return propvalues
+
+ def index(self, nodeid):
+ ''' Add (or refresh) the node to search indexes.
+
+ Use the content-type property for the content property.
+ '''
+ # find all the String properties that have indexme
+ for prop, propclass in self.getprops().items():
+ if prop == 'content' and propclass.indexme:
+ mime_type = self.get(nodeid, 'type', self.default_mime_type)
+ self.db.indexer.add_text((self.classname, nodeid, 'content'),
+ str(self.get(nodeid, 'content')), mime_type)
+ elif isinstance(propclass, hyperdb.String) and propclass.indexme:
+ # index them under (classname, nodeid, property)
+ try:
+ value = str(self.get(nodeid, prop))
+ except IndexError:
+ # node has been destroyed
+ continue
+ self.db.indexer.add_text((self.classname, nodeid, prop), value)
+
+# deviation from spec - was called ItemClass
+class IssueClass(Class, roundupdb.IssueClass):
+ # Overridden methods:
+ def __init__(self, db, classname, **properties):
+ '''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'):
+ properties['messages'] = hyperdb.Multilink("msg")
+ if not properties.has_key('files'):
+ properties['files'] = hyperdb.Multilink("file")
+ if not properties.has_key('nosy'):
+ # note: journalling is turned off as it really just wastes
+ # space. this behaviour may be overridden in an instance
+ properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
+ if not properties.has_key('superseder'):
+ properties['superseder'] = hyperdb.Multilink(classname)
+ Class.__init__(self, db, classname, **properties)
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/backends/back_metakit.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/back_metakit.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2104 @@
+# $Id: back_metakit.py,v 1.108 2006/04/27 04:59:37 richard Exp $
+'''Metakit backend for Roundup, originally by Gordon McMillan.
+
+Known Current Bugs:
+
+- You can't change a class' key properly. This shouldn't be too hard to fix.
+- Some unit tests are overridden.
+
+Notes by Richard:
+
+This backend has some behaviour specific to metakit:
+
+- there's no concept of an explicit "unset" in metakit, so all types
+ have some "unset" value:
+
+ ========= ===== ======================================================
+ Type Value Action when fetching from mk
+ ========= ===== ======================================================
+ Strings '' convert to None
+ Date 0 (seconds since 1970-01-01.00:00:00) convert to None
+ Interval '' convert to None
+ Number 0 ambiguious :( - do nothing (see BACKWARDS_COMPATIBLE)
+ Boolean 0 ambiguious :( - do nothing (see BACKWARDS_COMPATABILE)
+ Link 0 convert to None
+ Multilink [] actually, mk can handle this one ;)
+ Password '' convert to None
+ ========= ===== ======================================================
+
+ The get/set routines handle these values accordingly by converting
+ to/from None where they can. The Number/Boolean types are not able
+ to handle an "unset" at all, so they default the "unset" to 0.
+- Metakit relies in reference counting to close the database, there is
+ no explicit close call. This can cause issues if a metakit
+ database is referenced multiple times, one might not actually be
+ closing the db.
+- probably a bunch of stuff that I'm not aware of yet because I haven't
+ fully read through the source. One of these days....
+'''
+__docformat__ = 'restructuredtext'
+# Enable this flag to break backwards compatibility (i.e. can't read old
+# databases) but comply with more roundup features, like adding NULL support.
+BACKWARDS_COMPATIBLE = 1
+
+from roundup import hyperdb, date, password, roundupdb, security
+import logging
+import metakit
+from sessions_dbm import Sessions, OneTimeKeys
+import re, marshal, os, sys, time, calendar, shutil
+from indexer_common import Indexer
+import locking
+from roundup.date import Range
+from blobfiles import files_in_dir
+
+# view modes for opening
+# XXX FIXME BPK -> these don't do anything, they are ignored
+# should we just get rid of them for simplicities sake?
+READ = 0
+READWRITE = 1
+
+def db_exists(config):
+ return os.path.exists(os.path.join(config.TRACKER_HOME, 'db',
+ 'tracker.mk4'))
+
+def db_nuke(config):
+ shutil.rmtree(os.path.join(config.TRACKER_HOME, 'db'))
+
+# general metakit error
+class MKBackendError(Exception):
+ pass
+
+_dbs = {}
+
+def Database(config, journaltag=None):
+ ''' Only have a single instance of the Database class for each instance
+ '''
+ db = _dbs.get(config.DATABASE, None)
+ if db is None or db._db is None:
+ db = _Database(config, journaltag)
+ _dbs[config.DATABASE] = db
+ else:
+ db.journaltag = journaltag
+ return db
+
+class _Database(hyperdb.Database, roundupdb.Database):
+ # Metakit has no concept of an explicit NULL
+ BACKEND_MISSING_STRING = ''
+ BACKEND_MISSING_NUMBER = 0
+ BACKEND_MISSING_BOOLEAN = 0
+
+ def __init__(self, config, journaltag=None):
+ self.config = config
+ self.journaltag = journaltag
+ self.classes = {}
+ self.dirty = 0
+ self.lockfile = None
+ self._db = self.__open()
+ self.indexer = Indexer(self)
+ self.security = security.Security(self)
+
+ self.stats = {'cache_hits': 0, 'cache_misses': 0, 'get_items': 0,
+ 'filtering': 0}
+
+ os.umask(config.UMASK)
+
+ def post_init(self):
+ if self.indexer.should_reindex():
+ self.reindex()
+
+ def refresh_database(self):
+ # XXX handle refresh
+ self.reindex()
+
+ def reindex(self, classname=None):
+ if classname:
+ classes = [self.getclass(classname)]
+ else:
+ classes = self.classes.values()
+ for klass in classes:
+ for nodeid in klass.list():
+ klass.index(nodeid)
+ self.indexer.save_index()
+
+ def getSessionManager(self):
+ return Sessions(self)
+
+ def getOTKManager(self):
+ return OneTimeKeys(self)
+
+ # --- defined in ping's spec
+ def __getattr__(self, classname):
+ if classname == 'transactions':
+ return self.dirty
+ # fall back on the classes
+ try:
+ return self.getclass(classname)
+ except KeyError, msg:
+ # KeyError's not appropriate here
+ raise AttributeError, str(msg)
+ def getclass(self, classname):
+ try:
+ return self.classes[classname]
+ except KeyError:
+ raise KeyError, 'There is no class called "%s"'%classname
+ def getclasses(self):
+ return self.classes.keys()
+ # --- end of ping's spec
+
+ # --- exposed methods
+ def commit(self):
+ '''commit all changes to the database'''
+ if self.dirty:
+ self._db.commit()
+ for cl in self.classes.values():
+ cl._commit()
+ self.indexer.save_index()
+ self.dirty = 0
+ def rollback(self):
+ '''roll back all changes since the last commit'''
+ if self.dirty:
+ for cl in self.classes.values():
+ cl._rollback()
+ self._db.rollback()
+ self._db = None
+ self._db = metakit.storage(self.dbnm, 1)
+ self.hist = self._db.view('history')
+ self.tables = self._db.view('tables')
+ self.indexer.rollback()
+ self.indexer.datadb = self._db
+ self.dirty = 0
+ def clearCache(self):
+ '''clear the internal cache by committing all pending database changes'''
+ for cl in self.classes.values():
+ cl._commit()
+ def clear(self):
+ '''clear the internal cache but don't commit any changes'''
+ for cl in self.classes.values():
+ cl._clear()
+ def hasnode(self, classname, nodeid):
+ '''does a particular class contain a nodeid?'''
+ return self.getclass(classname).hasnode(nodeid)
+ def pack(self, pack_before):
+ ''' Delete all journal entries except "create" before 'pack_before'.
+ '''
+ mindate = int(calendar.timegm(pack_before.get_tuple()))
+ i = 0
+ while i < len(self.hist):
+ if self.hist[i].date < mindate and self.hist[i].action != _CREATE:
+ self.hist.delete(i)
+ else:
+ i = i + 1
+ def addclass(self, cl):
+ ''' Add a Class to the hyperdatabase.
+ '''
+ cn = cl.classname
+ self.classes[cn] = cl
+ if self.tables.find(name=cn) < 0:
+ self.tables.append(name=cn)
+
+ # add default Edit and View permissions
+ self.security.addPermission(name="Create", klass=cn,
+ description="User is allowed to create "+cn)
+ self.security.addPermission(name="Edit", klass=cn,
+ description="User is allowed to edit "+cn)
+ self.security.addPermission(name="View", klass=cn,
+ description="User is allowed to access "+cn)
+
+ def addjournal(self, tablenm, nodeid, action, params, creator=None,
+ creation=None):
+ ''' Journal the Action
+ 'action' may be:
+
+ 'create' or 'set' -- 'params' is a dictionary of property values
+ 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
+ 'retire' -- 'params' is None
+ '''
+ tblid = self.tables.find(name=tablenm)
+ if tblid == -1:
+ tblid = self.tables.append(name=tablenm)
+ if creator is None:
+ creator = int(self.getuid())
+ else:
+ try:
+ creator = int(creator)
+ except TypeError:
+ creator = int(self.getclass('user').lookup(creator))
+ if creation is None:
+ creation = int(time.time())
+ elif isinstance(creation, date.Date):
+ creation = int(calendar.timegm(creation.get_tuple()))
+ # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
+ self.hist.append(tableid=tblid,
+ nodeid=int(nodeid),
+ date=creation,
+ action=action,
+ user=creator,
+ params=marshal.dumps(params))
+
+ def setjournal(self, tablenm, nodeid, journal):
+ '''Set the journal to the "journal" list.'''
+ tblid = self.tables.find(name=tablenm)
+ if tblid == -1:
+ tblid = self.tables.append(name=tablenm)
+ for nodeid, date, user, action, params in journal:
+ # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
+ self.hist.append(tableid=tblid,
+ nodeid=int(nodeid),
+ date=date,
+ action=action,
+ user=int(user),
+ params=marshal.dumps(params))
+
+ def getjournal(self, tablenm, nodeid):
+ ''' get the journal for id
+ '''
+ rslt = []
+ tblid = self.tables.find(name=tablenm)
+ if tblid == -1:
+ return rslt
+ q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
+ if len(q) == 0:
+ raise IndexError, "no history for id %s in %s" % (nodeid, tablenm)
+ i = 0
+ #userclass = self.getclass('user')
+ for row in q:
+ try:
+ params = marshal.loads(row.params)
+ except ValueError:
+ logging.getLogger("hyperdb").error(
+ "history couldn't unmarshal %r" % row.params)
+ params = {}
+ #usernm = userclass.get(str(row.user), 'username')
+ dt = date.Date(time.gmtime(row.date))
+ #rslt.append((nodeid, dt, usernm, _actionnames[row.action], params))
+ rslt.append((nodeid, dt, str(row.user), _actionnames[row.action],
+ params))
+ return rslt
+
+ def destroyjournal(self, tablenm, nodeid):
+ nodeid = int(nodeid)
+ tblid = self.tables.find(name=tablenm)
+ if tblid == -1:
+ return
+ i = 0
+ hist = self.hist
+ while i < len(hist):
+ if hist[i].tableid == tblid and hist[i].nodeid == nodeid:
+ hist.delete(i)
+ else:
+ i = i + 1
+ self.dirty = 1
+
+ def close(self):
+ ''' Close off the connection.
+ '''
+ # de-reference count the metakit databases,
+ # as this is the only way they will be closed
+ for cl in self.classes.values():
+ cl.db = None
+ self._db = None
+ if self.lockfile is not None:
+ locking.release_lock(self.lockfile)
+ if _dbs.has_key(self.config.DATABASE):
+ del _dbs[self.config.DATABASE]
+ if self.lockfile is not None:
+ self.lockfile.close()
+ self.lockfile = None
+ self.classes = {}
+
+ # force the indexer to close
+ self.indexer.close()
+ self.indexer = None
+
+ # --- internal
+ def __open(self):
+ ''' Open the metakit database
+ '''
+ # make the database dir if it doesn't exist
+ if not os.path.exists(self.config.DATABASE):
+ os.makedirs(self.config.DATABASE)
+
+ # figure the file names
+ self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
+ lockfilenm = db[:-3]+'lck'
+
+ # get the database lock
+ self.lockfile = locking.acquire_lock(lockfilenm)
+ self.lockfile.write(str(os.getpid()))
+ self.lockfile.flush()
+
+ # see if the schema has changed since last db access
+ self.fastopen = 0
+ if os.path.exists(db):
+ dbtm = os.path.getmtime(db)
+ schemafile = os.path.join(self.config['HOME'], 'schema.py')
+ if not os.path.isfile(schemafile):
+ # try old-style schema
+ schemafile = os.path.join(self.config['HOME'], 'dbinit.py')
+ if os.path.isfile(schemafile) \
+ and (os.path.getmtime(schemafile) < dbtm):
+ # found schema file - it's older than the db
+ self.fastopen = 1
+
+ # open the db
+ db = metakit.storage(db, 1)
+ hist = db.view('history')
+ tables = db.view('tables')
+ if not self.fastopen:
+ # create the database if it's brand new
+ if not hist.structure():
+ hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
+ if not tables.structure():
+ tables = db.getas('tables[name:S]')
+ db.commit()
+
+ # we now have an open, initialised database
+ self.tables = tables
+ self.hist = hist
+ return db
+
+ def setid(self, classname, maxid):
+ ''' No-op in metakit
+ '''
+ cls = self.getclass(classname)
+ cls.setid(int(maxid))
+
+ def numfiles(self):
+ '''Get number of files in storage, even across subdirectories.
+ '''
+ files_dir = os.path.join(self.config.DATABASE, 'files')
+ return files_in_dir(files_dir)
+
+_STRINGTYPE = type('')
+_LISTTYPE = type([])
+_CREATE, _SET, _RETIRE, _LINK, _UNLINK, _RESTORE = range(6)
+
+_actionnames = {
+ _CREATE : 'create',
+ _SET : 'set',
+ _RETIRE : 'retire',
+ _RESTORE : 'restore',
+ _LINK : 'link',
+ _UNLINK : 'unlink',
+}
+
+_names_to_actionnames = {
+ 'create': _CREATE,
+ 'set': _SET,
+ 'retire': _RETIRE,
+ 'restore': _RESTORE,
+ 'link': _LINK,
+ 'unlink': _UNLINK,
+}
+
+_marker = []
+
+_ALLOWSETTINGPRIVATEPROPS = 0
+
+class Class(hyperdb.Class):
+ ''' The handle to a particular class of nodes in a hyperdatabase.
+
+ All methods except __repr__ and getnode must be implemented by a
+ concrete backend Class of which this is one.
+ '''
+
+ privateprops = None
+ def __init__(self, db, classname, **properties):
+ if hasattr(db, classname):
+ raise ValueError, "Class %s already exists"%classname
+
+ hyperdb.Class.__init__ (self, db, classname, **properties)
+ self.db = db # why isn't this a weakref as for other backends??
+ self.key = None
+ self.ruprops = self.properties
+ self.privateprops = { 'id' : hyperdb.String(),
+ 'activity' : hyperdb.Date(),
+ 'actor' : hyperdb.Link('user'),
+ 'creation' : hyperdb.Date(),
+ 'creator' : hyperdb.Link('user') }
+
+ self.idcache = {}
+ self.uncommitted = {}
+ self.comactions = []
+ self.rbactions = []
+
+ view = self.__getview()
+ self.maxid = 1
+ if view:
+ self.maxid = view[-1].id + 1
+
+ def setid(self, maxid):
+ self.maxid = maxid + 1
+
+ def enableJournalling(self):
+ '''Turn journalling on for this class
+ '''
+ self.do_journal = 1
+
+ def disableJournalling(self):
+ '''Turn journalling off for this class
+ '''
+ self.do_journal = 0
+
+ # --- the hyperdb.Class methods
+ def create(self, **propvalues):
+ ''' Create a new node of this class and return its id.
+
+ The keyword arguments in 'propvalues' map property names to values.
+
+ The values of arguments must be acceptable for the types of their
+ corresponding properties or a TypeError is raised.
+
+ If this class has a key property, it must be present and its value
+ must not collide with other key strings or a ValueError is raised.
+
+ Any other properties on this class that are missing from the
+ 'propvalues' dictionary are set to None.
+
+ If an id in a link or multilink property does not refer to a valid
+ node, an IndexError is raised.
+ '''
+ if not propvalues:
+ raise ValueError, "Need something to create!"
+ self.fireAuditors('create', None, propvalues)
+ newid = self.create_inner(**propvalues)
+ self.fireReactors('create', newid, None)
+ return newid
+
+ def create_inner(self, **propvalues):
+ ''' Called by create, in-between the audit and react calls.
+ '''
+ rowdict = {}
+ rowdict['id'] = newid = self.maxid
+ self.maxid += 1
+ ndx = self.getview(READWRITE).append(rowdict)
+ propvalues['#ISNEW'] = 1
+ try:
+ self.set_inner(str(newid), **propvalues)
+ except Exception:
+ self.maxid -= 1
+ raise
+ return str(newid)
+
+ def get(self, nodeid, propname, default=_marker, cache=1):
+ '''Get the value of a property on an existing node of this class.
+
+ 'nodeid' must be the id of an existing node of this class or an
+ IndexError is raised. 'propname' must be the name of a property
+ of this class or a KeyError is raised.
+
+ 'cache' exists for backwards compatibility, and is not used.
+ '''
+ view = self.getview()
+ id = int(nodeid)
+ if cache == 0:
+ oldnode = self.uncommitted.get(id, None)
+ if oldnode and oldnode.has_key(propname):
+ raw = oldnode[propname]
+ converter = _converters.get(raw.__class__, None)
+ if converter:
+ return converter(raw)
+ return raw
+ ndx = self.idcache.get(id, None)
+
+ if ndx is None:
+ ndx = view.find(id=id)
+ if ndx < 0:
+ raise IndexError, "%s has no node %s" % (self.classname, nodeid)
+ self.idcache[id] = ndx
+ try:
+ raw = getattr(view[ndx], propname)
+ except AttributeError:
+ raise KeyError, propname
+ rutyp = self.ruprops.get(propname, None)
+
+ if rutyp is None:
+ rutyp = self.privateprops[propname]
+
+ converter = _converters.get(rutyp.__class__, None)
+ if converter:
+ raw = converter(raw)
+ return raw
+
+ def set(self, nodeid, **propvalues):
+ '''Modify a property on an existing node of this class.
+
+ 'nodeid' must be the id of an existing node of this class or an
+ IndexError is raised.
+
+ Each key in 'propvalues' must be the name of a property of this
+ class or a KeyError is raised.
+
+ All values in 'propvalues' must be acceptable types for their
+ corresponding properties or a TypeError is raised.
+
+ If the value of the key property is set, it must not collide with
+ other key strings or a ValueError is raised.
+
+ If the value of a Link or Multilink property contains an invalid
+ node id, a ValueError is raised.
+ '''
+ self.fireAuditors('set', nodeid, propvalues)
+ propvalues, oldnode = self.set_inner(nodeid, **propvalues)
+ self.fireReactors('set', nodeid, oldnode)
+
+ def set_inner(self, nodeid, **propvalues):
+ '''Called outside of auditors'''
+ isnew = 0
+ if propvalues.has_key('#ISNEW'):
+ isnew = 1
+ del propvalues['#ISNEW']
+
+ if propvalues.has_key('id'):
+ raise KeyError, '"id" is reserved'
+ if self.db.journaltag is None:
+ raise hyperdb.DatabaseError, 'Database open read-only'
+ view = self.getview(READWRITE)
+
+ # node must exist & not be retired
+ id = int(nodeid)
+ ndx = view.find(id=id)
+ if ndx < 0:
+ raise IndexError, "%s has no node %s" % (self.classname, nodeid)
+ row = view[ndx]
+ if row._isdel:
+ raise IndexError, "%s has no node %s" % (self.classname, nodeid)
+ oldnode = self.uncommitted.setdefault(id, {})
+ changes = {}
+
+ for key, value in propvalues.items():
+ # this will raise the KeyError if the property isn't valid
+ # ... we don't use getprops() here because we only care about
+ # the writeable properties.
+ if _ALLOWSETTINGPRIVATEPROPS:
+ prop = self.ruprops.get(key, None)
+ if not prop:
+ prop = self.privateprops[key]
+ else:
+ prop = self.ruprops[key]
+ converter = _converters.get(prop.__class__, lambda v: v)
+ # if the value's the same as the existing value, no sense in
+ # doing anything
+ oldvalue = converter(getattr(row, key))
+ if value == oldvalue:
+ del propvalues[key]
+ continue
+
+ # check to make sure we're not duplicating an existing key
+ if key == self.key:
+ iv = self.getindexview(READWRITE)
+ ndx = iv.find(k=value)
+ if ndx == -1:
+ iv.append(k=value, i=row.id)
+ if not isnew:
+ ndx = iv.find(k=oldvalue)
+ if ndx > -1:
+ iv.delete(ndx)
+ else:
+ raise ValueError, 'node with key "%s" exists'%value
+
+ # do stuff based on the prop type
+ if isinstance(prop, hyperdb.Link):
+ link_class = prop.classname
+ # must be a string or None
+ if value is not None and not isinstance(value, type('')):
+ raise ValueError, 'property "%s" link value be a string'%(
+ key)
+ # Roundup sets to "unselected" by passing None
+ if value is None:
+ value = 0
+ # if it isn't a number, it's a key
+ try:
+ int(value)
+ except ValueError:
+ try:
+ value = self.db.getclass(link_class).lookup(value)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ key, value, prop.classname)
+
+ if (value is not None and
+ not self.db.getclass(link_class).hasnode(value)):
+ raise IndexError, '%s has no node %s'%(link_class, value)
+
+ setattr(row, key, int(value))
+ changes[key] = oldvalue
+
+ if self.do_journal and prop.do_journal:
+ # register the unlink with the old linked node
+ if oldvalue:
+ self.db.addjournal(link_class, oldvalue, _UNLINK,
+ (self.classname, str(row.id), key))
+
+ # register the link with the newly linked node
+ if value:
+ self.db.addjournal(link_class, value, _LINK,
+ (self.classname, str(row.id), key))
+
+ elif isinstance(prop, hyperdb.Multilink):
+ if value is not None and type(value) != _LISTTYPE:
+ raise TypeError, 'new property "%s" not a list of ids'%key
+ link_class = prop.classname
+ l = []
+ if value is None:
+ value = []
+ for entry in value:
+ if type(entry) != _STRINGTYPE:
+ raise ValueError, 'new property "%s" link value ' \
+ 'must be a string'%key
+ # if it isn't a number, it's a key
+ try:
+ int(entry)
+ except ValueError:
+ try:
+ entry = self.db.getclass(link_class).lookup(entry)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ key, entry, prop.classname)
+ l.append(entry)
+ propvalues[key] = value = l
+
+ # handle removals
+ rmvd = []
+ for id in oldvalue:
+ if id not in value:
+ rmvd.append(id)
+ # register the unlink with the old linked node
+ if self.do_journal and prop.do_journal:
+ self.db.addjournal(link_class, id, _UNLINK,
+ (self.classname, str(row.id), key))
+
+ # handle additions
+ adds = []
+ for id in value:
+ if id not in oldvalue:
+ if not self.db.getclass(link_class).hasnode(id):
+ raise IndexError, '%s has no node %s'%(
+ link_class, id)
+ adds.append(id)
+ # register the link with the newly linked node
+ if self.do_journal and prop.do_journal:
+ self.db.addjournal(link_class, id, _LINK,
+ (self.classname, str(row.id), key))
+
+ # perform the modifications on the actual property value
+ sv = getattr(row, key)
+ i = 0
+ while i < len(sv):
+ if str(sv[i].fid) in rmvd:
+ sv.delete(i)
+ else:
+ i += 1
+ for id in adds:
+ sv.append(fid=int(id))
+
+ # figure the journal entry
+ l = []
+ if adds:
+ l.append(('+', adds))
+ if rmvd:
+ l.append(('-', rmvd))
+ if l:
+ changes[key] = tuple(l)
+ #changes[key] = oldvalue
+
+ if not rmvd and not adds:
+ del propvalues[key]
+
+ elif isinstance(prop, hyperdb.String):
+ if value is not None and type(value) != _STRINGTYPE:
+ raise TypeError, 'new property "%s" not a string'%key
+ if value is None:
+ value = ''
+ setattr(row, key, value)
+ changes[key] = oldvalue
+ if hasattr(prop, 'isfilename') and prop.isfilename:
+ propvalues[key] = os.path.basename(value)
+ if prop.indexme:
+ self.db.indexer.add_text((self.classname, nodeid, key),
+ value, 'text/plain')
+
+ elif isinstance(prop, hyperdb.Password):
+ if value is not None and not isinstance(value, password.Password):
+ raise TypeError, 'new property "%s" not a Password'% key
+ if value is None:
+ value = ''
+ setattr(row, key, str(value))
+ changes[key] = str(oldvalue)
+ propvalues[key] = str(value)
+
+ elif isinstance(prop, hyperdb.Date):
+ if value is not None and not isinstance(value, date.Date):
+ raise TypeError, 'new property "%s" not a Date'% key
+ if value is None:
+ setattr(row, key, 0)
+ else:
+ setattr(row, key, int(calendar.timegm(value.get_tuple())))
+ if oldvalue is None:
+ changes[key] = oldvalue
+ else:
+ changes[key] = str(oldvalue)
+ propvalues[key] = str(value)
+
+ elif isinstance(prop, hyperdb.Interval):
+ if value is not None and not isinstance(value, date.Interval):
+ raise TypeError, 'new property "%s" not an Interval'% key
+ if value is None:
+ setattr(row, key, '')
+ else:
+ # kedder: we should store interval values serialized
+ setattr(row, key, value.serialise())
+ changes[key] = str(oldvalue)
+ propvalues[key] = str(value)
+
+ elif isinstance(prop, hyperdb.Number):
+ if value is None:
+ v = 0
+ else:
+ try:
+ v = float(value)
+ except ValueError:
+ raise TypeError, "%s (%s) is not numeric"%(key, repr(value))
+ if not BACKWARDS_COMPATIBLE:
+ if v >=0:
+ v = v + 1
+ setattr(row, key, v)
+ changes[key] = oldvalue
+ propvalues[key] = value
+
+ elif isinstance(prop, hyperdb.Boolean):
+ if value is None:
+ bv = 0
+ elif value not in (0,1):
+ raise TypeError, "%s (%s) is not boolean"%(key, repr(value))
+ else:
+ bv = value
+ if not BACKWARDS_COMPATIBLE:
+ bv += 1
+ setattr(row, key, bv)
+ changes[key] = oldvalue
+ propvalues[key] = value
+
+ oldnode[key] = oldvalue
+
+ # nothing to do?
+ if not isnew and not propvalues:
+ return propvalues, oldnode
+ if not propvalues.has_key('activity'):
+ row.activity = int(time.time())
+ if not propvalues.has_key('actor'):
+ row.actor = int(self.db.getuid())
+ if isnew:
+ if not row.creation:
+ row.creation = int(time.time())
+ if not row.creator:
+ row.creator = int(self.db.getuid())
+
+ self.db.dirty = 1
+
+ if self.do_journal:
+ if isnew:
+ self.db.addjournal(self.classname, nodeid, _CREATE, {})
+ else:
+ self.db.addjournal(self.classname, nodeid, _SET, changes)
+
+ return propvalues, oldnode
+
+ def retire(self, nodeid):
+ '''Retire a node.
+
+ The properties on the node remain available from the get() method,
+ and the node's id is never reused.
+
+ Retired nodes are not returned by the find(), list(), or lookup()
+ methods, and other nodes may reuse the values of their key properties.
+ '''
+ if self.db.journaltag is None:
+ raise hyperdb.DatabaseError, 'Database open read-only'
+ self.fireAuditors('retire', nodeid, None)
+ view = self.getview(READWRITE)
+ ndx = view.find(id=int(nodeid))
+ if ndx < 0:
+ raise KeyError, "nodeid %s not found" % nodeid
+
+ row = view[ndx]
+ oldvalues = self.uncommitted.setdefault(row.id, {})
+ oldval = oldvalues['_isdel'] = row._isdel
+ row._isdel = 1
+
+ if self.do_journal:
+ self.db.addjournal(self.classname, nodeid, _RETIRE, {})
+ if self.key:
+ iv = self.getindexview(READWRITE)
+ ndx = iv.find(k=getattr(row, self.key))
+ # find is broken with multiple attribute lookups
+ # on ordered views
+ #ndx = iv.find(k=getattr(row, self.key),i=row.id)
+ if ndx > -1 and iv[ndx].i == row.id:
+ iv.delete(ndx)
+
+ self.db.dirty = 1
+ self.fireReactors('retire', nodeid, None)
+
+ def restore(self, nodeid):
+ '''Restore a retired node.
+
+ Make node available for all operations like it was before retirement.
+ '''
+ if self.db.journaltag is None:
+ raise hyperdb.DatabaseError, 'Database open read-only'
+
+ # check if key property was overrided
+ key = self.getkey()
+ keyvalue = self.get(nodeid, key)
+
+ try:
+ id = self.lookup(keyvalue)
+ except KeyError:
+ pass
+ else:
+ raise KeyError, "Key property (%s) of retired node clashes with \
+ existing one (%s)" % (key, keyvalue)
+ # Now we can safely restore node
+ self.fireAuditors('restore', nodeid, None)
+ view = self.getview(READWRITE)
+ ndx = view.find(id=int(nodeid))
+ if ndx < 0:
+ raise KeyError, "nodeid %s not found" % nodeid
+
+ row = view[ndx]
+ oldvalues = self.uncommitted.setdefault(row.id, {})
+ oldval = oldvalues['_isdel'] = row._isdel
+ row._isdel = 0
+
+ if self.do_journal:
+ self.db.addjournal(self.classname, nodeid, _RESTORE, {})
+ if self.key:
+ iv = self.getindexview(READWRITE)
+ ndx = iv.find(k=getattr(row, self.key),i=row.id)
+ if ndx > -1:
+ iv.delete(ndx)
+ self.db.dirty = 1
+ self.fireReactors('restore', nodeid, None)
+
+ def is_retired(self, nodeid):
+ '''Return true if the node is retired
+ '''
+ view = self.getview(READWRITE)
+ # node must exist & not be retired
+ id = int(nodeid)
+ ndx = view.find(id=id)
+ if ndx < 0:
+ raise IndexError, "%s has no node %s" % (self.classname, nodeid)
+ row = view[ndx]
+ return row._isdel
+
+ def history(self, nodeid):
+ '''Retrieve the journal of edits on a particular node.
+
+ 'nodeid' must be the id of an existing node of this class or an
+ IndexError is raised.
+
+ The returned list contains tuples of the form
+
+ (nodeid, date, tag, action, params)
+
+ 'date' is a Timestamp object specifying the time of the change and
+ 'tag' is the journaltag specified when the database was opened.
+ '''
+ if not self.do_journal:
+ raise ValueError, 'Journalling is disabled for this class'
+ return self.db.getjournal(self.classname, nodeid)
+
+ def setkey(self, propname):
+ '''Select a String property of this class to be the key property.
+
+ 'propname' must be the name of a String property of this class or
+ None, or a TypeError is raised. The values of the key property on
+ all existing nodes must be unique or a ValueError is raised.
+ '''
+ if self.key:
+ if propname == self.key:
+ return
+ else:
+ # drop the old key table
+ tablename = "_%s.%s"%(self.classname, self.key)
+ self.db._db.getas(tablename)
+
+ #raise ValueError, "%s already indexed on %s"%(self.classname,
+ # self.key)
+
+ prop = self.properties.get(propname, None)
+ if prop is None:
+ prop = self.privateprops.get(propname, None)
+ if prop is None:
+ raise KeyError, "no property %s" % propname
+ if not isinstance(prop, hyperdb.String):
+ raise TypeError, "%s is not a String" % propname
+
+ # the way he index on properties is by creating a
+ # table named _%(classname)s.%(key)s, if this table
+ # exists then everything is okay. If this table
+ # doesn't exist, then generate a new table on the
+ # key value.
+
+ # first setkey for this run or key has been changed
+ self.key = propname
+ tablename = "_%s.%s"%(self.classname, self.key)
+
+ iv = self.db._db.view(tablename)
+ if self.db.fastopen and iv.structure():
+ return
+
+ # very first setkey ever or the key has changed
+ self.db.dirty = 1
+ iv = self.db._db.getas('_%s[k:S,i:I]' % tablename)
+ iv = iv.ordered(1)
+ for row in self.getview():
+ iv.append(k=getattr(row, propname), i=row.id)
+ self.db.commit()
+
+ def getkey(self):
+ '''Return the name of the key property for this class or None.'''
+ return self.key
+
+ def lookup(self, keyvalue):
+ '''Locate a particular node by its key property and return its id.
+
+ If this class has no key property, a TypeError is raised. If the
+ keyvalue matches one of the values for the key property among
+ the nodes in this class, the matching node's id is returned;
+ otherwise a KeyError is raised.
+ '''
+ if not self.key:
+ raise TypeError, 'No key property set for class %s'%self.classname
+
+ if type(keyvalue) is not _STRINGTYPE:
+ raise TypeError, '%r is not a string'%keyvalue
+
+ # XXX FIX ME -> this is a bit convoluted
+ # First we search the index view to get the id
+ # which is a quicker look up.
+ # Then we lookup the row with id=id
+ # if the _isdel property of the row is 0, return the
+ # string version of the id. (Why string version???)
+ #
+ # Otherwise, just lookup the non-indexed key
+ # in the non-index table and check the _isdel property
+ iv = self.getindexview()
+ if iv:
+ # look up the index view for the id,
+ # then instead of looking up the keyvalue, lookup the
+ # quicker id
+ ndx = iv.find(k=keyvalue)
+ if ndx > -1:
+ view = self.getview()
+ ndx = view.find(id=iv[ndx].i)
+ if ndx > -1:
+ row = view[ndx]
+ if not row._isdel:
+ return str(row.id)
+ else:
+ # perform the slower query
+ view = self.getview()
+ ndx = view.find({self.key:keyvalue})
+ if ndx > -1:
+ row = view[ndx]
+ if not row._isdel:
+ return str(row.id)
+
+ raise KeyError, keyvalue
+
+ def destroy(self, id):
+ '''Destroy a node.
+
+ WARNING: this method should never be used except in extremely rare
+ situations where there could never be links to the node being
+ deleted
+
+ WARNING: use retire() instead
+
+ WARNING: the properties of this node will not be available ever again
+
+ WARNING: really, use retire() instead
+
+ Well, I think that's enough warnings. This method exists mostly to
+ support the session storage of the cgi interface.
+
+ The node is completely removed from the hyperdb, including all journal
+ entries. It will no longer be available, and will generally break code
+ if there are any references to the node.
+ '''
+ view = self.getview(READWRITE)
+ ndx = view.find(id=int(id))
+ if ndx > -1:
+ if self.key:
+ keyvalue = getattr(view[ndx], self.key)
+ iv = self.getindexview(READWRITE)
+ if iv:
+ ivndx = iv.find(k=keyvalue)
+ if ivndx > -1:
+ iv.delete(ivndx)
+ view.delete(ndx)
+ self.db.destroyjournal(self.classname, id)
+ self.db.dirty = 1
+
+ def find(self, **propspec):
+ '''Get the ids of nodes in this class which link to the given nodes.
+
+ 'propspec' consists of keyword args propname=nodeid or
+ propname={nodeid:1, }
+ 'propname' must be the name of a property in this class, or a
+ KeyError is raised. That property must be a Link or
+ Multilink property, or a TypeError is raised.
+
+ Any node in this class whose 'propname' property links to any of
+ the nodeids will be returned. Examples::
+
+ db.issue.find(messages='1')
+ db.issue.find(messages={'1':1,'3':1}, files={'7':1})
+ '''
+ propspec = propspec.items()
+ for propname, nodeid in propspec:
+ # check the prop is OK
+ prop = self.ruprops[propname]
+ if (not isinstance(prop, hyperdb.Link) and
+ not isinstance(prop, hyperdb.Multilink)):
+ raise TypeError, "'%s' not a Link/Multilink property"%propname
+
+ vws = []
+ for propname, ids in propspec:
+ if type(ids) is _STRINGTYPE:
+ ids = {int(ids):1}
+ elif ids is None:
+ ids = {0:1}
+ else:
+ d = {}
+ for id in ids.keys():
+ if id is None:
+ d[0] = 1
+ else:
+ d[int(id)] = 1
+ ids = d
+ prop = self.ruprops[propname]
+ view = self.getview()
+ if isinstance(prop, hyperdb.Multilink):
+ def ff(row, nm=propname, ids=ids):
+ if not row._isdel:
+ sv = getattr(row, nm)
+ for sr in sv:
+ if ids.has_key(sr.fid):
+ return 1
+ return 0
+ else:
+ def ff(row, nm=propname, ids=ids):
+ return not row._isdel and ids.has_key(getattr(row, nm))
+ ndxview = view.filter(ff)
+ vws.append(ndxview.unique())
+
+ # handle the empty match case
+ if not vws:
+ return []
+
+ ndxview = vws[0]
+ for v in vws[1:]:
+ ndxview = ndxview.union(v)
+ view = self.getview().remapwith(ndxview)
+ rslt = []
+ for row in view:
+ rslt.append(str(row.id))
+ return rslt
+
+
+ def list(self):
+ ''' Return a list of the ids of the active nodes in this class.
+ '''
+ l = []
+ for row in self.getview().select(_isdel=0):
+ l.append(str(row.id))
+ return l
+
+ def getnodeids(self, retired=None):
+ ''' Retrieve all the ids of the nodes for a particular Class.
+
+ Set retired=None to get all nodes. Otherwise it'll get all the
+ retired or non-retired nodes, depending on the flag.
+ '''
+ l = []
+ if retired is False or retired is True:
+ result = self.getview().select(_isdel=retired)
+ else:
+ result = self.getview()
+ for row in result:
+ l.append(str(row.id))
+ return l
+
+ def count(self):
+ return len(self.getview())
+
+ def getprops(self, protected=1):
+ # protected is not in ping's spec
+ allprops = self.ruprops.copy()
+ if protected and self.privateprops is not None:
+ allprops.update(self.privateprops)
+ return allprops
+
+ def addprop(self, **properties):
+ for key in properties.keys():
+ if self.ruprops.has_key(key):
+ raise ValueError, "%s is already a property of %s"%(key,
+ self.classname)
+ self.ruprops.update(properties)
+ # Class structure has changed
+ self.db.fastopen = 0
+ view = self.__getview()
+ self.db.commit()
+ # ---- end of ping's spec
+
+ def filter(self, search_matches, filterspec, sort=(None,None),
+ group=(None,None)):
+ '''Return a list of the ids of the active nodes in this class that
+ match the 'filter' spec, sorted by the group spec and then the
+ sort spec
+
+ "filterspec" is {propname: value(s)}
+
+ "sort" and "group" are (dir, prop) where dir is '+', '-' or None
+ and prop is a prop name or None
+
+ "search_matches" is {nodeid: marker} or None
+
+ The filter must match all properties specificed - but if the
+ property value to match is a list, any one of the values in the
+ list may match for that property to match.
+ '''
+ if __debug__:
+ start_t = time.time()
+
+ where = {'_isdel':0}
+ wherehigh = {}
+ mlcriteria = {}
+ regexes = []
+ orcriteria = {}
+ for propname, value in filterspec.items():
+ prop = self.ruprops.get(propname, None)
+ if prop is None:
+ prop = self.privateprops[propname]
+ if isinstance(prop, hyperdb.Multilink):
+ if value in ('-1', ['-1']):
+ value = []
+ elif type(value) is not _LISTTYPE:
+ value = [value]
+ # transform keys to ids
+ u = []
+ for item in value:
+ try:
+ item = int(item)
+ except (TypeError, ValueError):
+ item = int(self.db.getclass(prop.classname).lookup(item))
+ if item == -1:
+ item = 0
+ u.append(item)
+ mlcriteria[propname] = u
+ elif isinstance(prop, hyperdb.Link):
+ if type(value) is not _LISTTYPE:
+ value = [value]
+ # transform keys to ids
+ u = []
+ for item in value:
+ if item is None:
+ item = -1
+ else:
+ try:
+ item = int(item)
+ except (TypeError, ValueError):
+ linkcl = self.db.getclass(prop.classname)
+ item = int(linkcl.lookup(item))
+ if item == -1:
+ item = 0
+ u.append(item)
+ if len(u) == 1:
+ where[propname] = u[0]
+ else:
+ orcriteria[propname] = u
+ elif isinstance(prop, hyperdb.String):
+ if type(value) is not type([]):
+ value = [value]
+ for v in value:
+ # simple glob searching
+ v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
+ v = v.replace('?', '.')
+ v = v.replace('*', '.*?')
+ regexes.append((propname, re.compile(v, re.I)))
+ elif propname == 'id':
+ where[propname] = int(value)
+ elif isinstance(prop, hyperdb.Boolean):
+ if type(value) is _STRINGTYPE:
+ bv = value.lower() in ('yes', 'true', 'on', '1')
+ else:
+ bv = value
+ where[propname] = bv
+ elif isinstance(prop, hyperdb.Date):
+ try:
+ # Try to filter on range of dates
+ date_rng = prop.range_from_raw (value, self.db)
+ if date_rng.from_value:
+ t = date_rng.from_value.get_tuple()
+ where[propname] = int(calendar.timegm(t))
+ else:
+ # use minimum possible value to exclude items without
+ # 'prop' property
+ where[propname] = 0
+ if date_rng.to_value:
+ t = date_rng.to_value.get_tuple()
+ wherehigh[propname] = int(calendar.timegm(t))
+ else:
+ wherehigh[propname] = None
+ except ValueError:
+ # If range creation fails - ignore that search parameter
+ pass
+ elif isinstance(prop, hyperdb.Interval):
+ try:
+ # Try to filter on range of intervals
+ date_rng = Range(value, date.Interval)
+ if date_rng.from_value:
+ #t = date_rng.from_value.get_tuple()
+ where[propname] = date_rng.from_value.serialise()
+ else:
+ # use minimum possible value to exclude items without
+ # 'prop' property
+ where[propname] = '-99999999999999'
+ if date_rng.to_value:
+ #t = date_rng.to_value.get_tuple()
+ wherehigh[propname] = date_rng.to_value.serialise()
+ else:
+ wherehigh[propname] = None
+ except ValueError:
+ # If range creation fails - ignore that search parameter
+ pass
+ elif isinstance(prop, hyperdb.Number):
+ if type(value) is _LISTTYPE:
+ orcriteria[propname] = [float(v) for v in value]
+ else:
+ where[propname] = float(value)
+ else:
+ where[propname] = str(value)
+ v = self.getview()
+ if where:
+ where_higherbound = where.copy()
+ where_higherbound.update(wherehigh)
+ v = v.select(where, where_higherbound)
+
+ if mlcriteria:
+ # multilink - if any of the nodeids required by the
+ # filterspec aren't in this node's property, then skip it
+ def ff(row, ml=mlcriteria):
+ for propname, values in ml.items():
+ sv = getattr(row, propname)
+ if not values and not sv:
+ return 1
+ for id in values:
+ if sv.find(fid=id) != -1:
+ return 1
+ return 0
+ iv = v.filter(ff)
+ v = v.remapwith(iv)
+
+ if orcriteria:
+ def ff(row, crit=orcriteria):
+ for propname, allowed in crit.items():
+ val = getattr(row, propname)
+ if val not in allowed:
+ return 0
+ return 1
+
+ iv = v.filter(ff)
+ v = v.remapwith(iv)
+
+ if regexes:
+ def ff(row, r=regexes):
+ for propname, regex in r:
+ val = str(getattr(row, propname))
+ if not regex.search(val):
+ return 0
+ return 1
+
+ iv = v.filter(ff)
+ v = v.remapwith(iv)
+
+ if sort or group:
+ sortspec = []
+ rev = []
+ for dir, propname in group, sort:
+ if propname is None: continue
+ isreversed = 0
+ if dir == '-':
+ isreversed = 1
+ try:
+ prop = getattr(v, propname)
+ except AttributeError:
+ logging.getLogger("hyperdb").error(
+ "MK has no property %s" % propname)
+ continue
+ propclass = self.ruprops.get(propname, None)
+ if propclass is None:
+ propclass = self.privateprops.get(propname, None)
+ if propclass is None:
+ logging.getLogger("hyperdb").error(
+ "Schema has no property %s" % propname)
+ continue
+ if isinstance(propclass, hyperdb.Link):
+ linkclass = self.db.getclass(propclass.classname)
+ lv = linkclass.getview()
+ lv = lv.rename('id', propname)
+ v = v.join(lv, prop, 1)
+ prop = getattr(v, linkclass.orderprop())
+ if isreversed:
+ rev.append(prop)
+ sortspec.append(prop)
+ v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
+
+ rslt = []
+ for row in v:
+ id = str(row.id)
+ if search_matches is not None:
+ if search_matches.has_key(id):
+ rslt.append(id)
+ else:
+ rslt.append(id)
+
+ if __debug__:
+ self.db.stats['filtering'] += (time.time() - start_t)
+
+ return rslt
+
+ def hasnode(self, nodeid):
+ '''Determine if the given nodeid actually exists
+ '''
+ return int(nodeid) < self.maxid
+
+ def stringFind(self, **requirements):
+ '''Locate a particular node by matching a set of its String
+ properties in a caseless search.
+
+ If the property is not a String property, a TypeError is raised.
+
+ The return is a list of the id of all nodes that match.
+ '''
+ for propname in requirements.keys():
+ prop = self.properties[propname]
+ if isinstance(not prop, hyperdb.String):
+ raise TypeError, "'%s' not a String property"%propname
+ requirements[propname] = requirements[propname].lower()
+ requirements['_isdel'] = 0
+
+ l = []
+ for row in self.getview().select(requirements):
+ l.append(str(row.id))
+ return l
+
+ def addjournal(self, nodeid, action, params):
+ '''Add a journal to the given nodeid,
+ 'action' may be:
+
+ 'create' or 'set' -- 'params' is a dictionary of property values
+ 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
+ 'retire' -- 'params' is None
+ '''
+ self.db.addjournal(self.classname, nodeid, action, params)
+
+ def index(self, nodeid):
+ ''' Add (or refresh) the node to search indexes '''
+ # find all the String properties that have indexme
+ for prop, propclass in self.getprops().items():
+ if isinstance(propclass, hyperdb.String) and propclass.indexme:
+ # index them under (classname, nodeid, property)
+ self.db.indexer.add_text((self.classname, nodeid, prop),
+ str(self.get(nodeid, prop)))
+
+ # --- used by Database
+ def _commit(self):
+ ''' called post commit of the DB.
+ interested subclasses may override '''
+ self.uncommitted = {}
+ for action in self.comactions:
+ action()
+ self.comactions = []
+ self.rbactions = []
+ self.idcache = {}
+ def _rollback(self):
+ ''' called pre rollback of the DB.
+ interested subclasses may override '''
+ self.comactions = []
+ for action in self.rbactions:
+ action()
+ self.rbactions = []
+ self.uncommitted = {}
+ self.idcache = {}
+ def _clear(self):
+ view = self.getview(READWRITE)
+ if len(view):
+ view[:] = []
+ self.db.dirty = 1
+ iv = self.getindexview(READWRITE)
+ if iv:
+ iv[:] = []
+ def commitaction(self, action):
+ ''' call this to register a callback called on commit
+ callback is removed on end of transaction '''
+ self.comactions.append(action)
+ def rollbackaction(self, action):
+ ''' call this to register a callback called on rollback
+ callback is removed on end of transaction '''
+ self.rbactions.append(action)
+ # --- internal
+ def __getview(self):
+ ''' Find the interface for a specific Class in the hyperdb.
+
+ This method checks to see whether the schema has changed and
+ re-works the underlying metakit structure if it has.
+ '''
+ db = self.db._db
+ view = db.view(self.classname)
+ mkprops = view.structure()
+
+ # if we have structure in the database, and the structure hasn't
+ # changed
+ # note on view.ordered ->
+ # return a metakit view ordered on the id column
+ # id is always the first column. This speeds up
+ # look-ups on the id column.
+
+ if mkprops and self.db.fastopen:
+ return view.ordered(1)
+
+ # is the definition the same?
+ for nm, rutyp in self.ruprops.items():
+ for mkprop in mkprops:
+ if mkprop.name == nm:
+ break
+ else:
+ mkprop = None
+ if mkprop is None:
+ break
+ if _typmap[rutyp.__class__] != mkprop.type:
+ break
+ else:
+ # make sure we have the 'actor' property too
+ for mkprop in mkprops:
+ if mkprop.name == 'actor':
+ return view.ordered(1)
+
+ # The schema has changed. We need to create or restructure the mk view
+ # id comes first, so we can use view.ordered(1) so that
+ # MK will order it for us to allow binary-search quick lookups on
+ # the id column
+ self.db.dirty = 1
+ s = ["%s[id:I" % self.classname]
+
+ # these columns will always be added, we can't trample them :)
+ _columns = {"id":"I", "_isdel":"I", "activity":"I", "actor": "I",
+ "creation":"I", "creator":"I"}
+
+ for nm, rutyp in self.ruprops.items():
+ mktyp = _typmap[rutyp.__class__].upper()
+ if nm in _columns and _columns[nm] != mktyp:
+ # oops, two columns with the same name and different properties
+ raise MKBackendError("column %s for table %sis defined with multiple types"%(nm, self.classname))
+ _columns[nm] = mktyp
+ s.append('%s:%s' % (nm, mktyp))
+ if mktyp == 'V':
+ s[-1] += ('[fid:I]')
+
+ # XXX FIX ME -> in some tests, creation:I becomes creation:S is this
+ # okay? Does this need to be supported?
+ s.append('_isdel:I,activity:I,actor:I,creation:I,creator:I]')
+ view = self.db._db.getas(','.join(s))
+ self.db.commit()
+ return view.ordered(1)
+ def getview(self, RW=0):
+ # XXX FIX ME -> The RW flag doesn't do anything.
+ return self.db._db.view(self.classname).ordered(1)
+ def getindexview(self, RW=0):
+ # XXX FIX ME -> The RW flag doesn't do anything.
+ tablename = "_%s.%s"%(self.classname, self.key)
+ return self.db._db.view("_%s" % tablename).ordered(1)
+
+ #
+ # import / export
+ #
+ def export_list(self, propnames, nodeid):
+ ''' Export a node - generate a list of CSV-able data in the order
+ specified by propnames for the given node.
+ '''
+ properties = self.getprops()
+ l = []
+ for prop in propnames:
+ proptype = properties[prop]
+ value = self.get(nodeid, prop)
+ # "marshal" data where needed
+ if value is None:
+ pass
+ elif isinstance(proptype, hyperdb.Date):
+ value = value.get_tuple()
+ elif isinstance(proptype, hyperdb.Interval):
+ value = value.get_tuple()
+ elif isinstance(proptype, hyperdb.Password):
+ value = str(value)
+ l.append(repr(value))
+
+ # append retired flag
+ l.append(repr(self.is_retired(nodeid)))
+
+ return l
+
+ def import_list(self, propnames, proplist):
+ ''' Import a node - all information including "id" is present and
+ should not be sanity checked. Triggers are not triggered. The
+ journal should be initialised using the "creator" and "creation"
+ information.
+
+ Return the nodeid of the node imported.
+ '''
+ if self.db.journaltag is None:
+ raise hyperdb.DatabaseError, 'Database open read-only'
+ properties = self.getprops()
+
+ d = {}
+ view = self.getview(READWRITE)
+ for i in range(len(propnames)):
+ value = eval(proplist[i])
+ if not value:
+ continue
+
+ propname = propnames[i]
+ if propname == 'id':
+ newid = value = int(value)
+ elif propname == 'is retired':
+ # is the item retired?
+ if int(value):
+ d['_isdel'] = 1
+ continue
+ elif value is None:
+ d[propname] = None
+ continue
+
+ prop = properties[propname]
+ if isinstance(prop, hyperdb.Date):
+ value = int(calendar.timegm(value))
+ elif isinstance(prop, hyperdb.Interval):
+ value = date.Interval(value).serialise()
+ elif isinstance(prop, hyperdb.Number):
+ value = float(value)
+ elif isinstance(prop, hyperdb.Boolean):
+ value = int(value)
+ elif isinstance(prop, hyperdb.Link) and value:
+ value = int(value)
+ elif isinstance(prop, hyperdb.Multilink):
+ # we handle multilinks separately
+ continue
+ d[propname] = value
+
+ # possibly make a new node
+ if not d.has_key('id'):
+ d['id'] = newid = self.maxid
+ self.maxid += 1
+
+ # save off the node
+ view.append(d)
+
+ # fix up multilinks
+ ndx = view.find(id=newid)
+ row = view[ndx]
+ for i in range(len(propnames)):
+ value = eval(proplist[i])
+ propname = propnames[i]
+ if propname == 'is retired':
+ continue
+ prop = properties[propname]
+ if not isinstance(prop, hyperdb.Multilink):
+ continue
+ sv = getattr(row, propname)
+ for entry in value:
+ sv.append((int(entry),))
+
+ self.db.dirty = 1
+ return newid
+
+ def export_journals(self):
+ '''Export a class's journal - generate a list of lists of
+ CSV-able data:
+
+ nodeid, date, user, action, params
+
+ No heading here - the columns are fixed.
+ '''
+ from roundup.hyperdb import Interval, Date, Password
+ properties = self.getprops()
+ r = []
+ for nodeid in self.getnodeids():
+ for nodeid, date, user, action, params in self.history(nodeid):
+ date = date.get_tuple()
+ if action == 'set':
+ export_data = {}
+ for propname, value in params.items():
+ if not properties.has_key(propname):
+ # property no longer in the schema
+ continue
+
+ prop = properties[propname]
+ # make sure the params are eval()'able
+ if value is None:
+ pass
+ elif isinstance(prop, Date):
+ value = value.get_tuple()
+ elif isinstance(prop, Interval):
+ value = value.get_tuple()
+ elif isinstance(prop, Password):
+ value = str(value)
+ export_data[propname] = value
+ params = export_data
+ l = [nodeid, date, user, action, params]
+ r.append(map(repr, l))
+ return r
+
+ def import_journals(self, entries):
+ '''Import a class's journal.
+
+ Uses setjournal() to set the journal for each item.'''
+ properties = self.getprops()
+ d = {}
+ for l in entries:
+ l = map(eval, l)
+ nodeid, jdate, user, action, params = l
+ jdate = int(calendar.timegm(date.Date(jdate).get_tuple()))
+ r = d.setdefault(nodeid, [])
+ if action == 'set':
+ for propname, value in params.items():
+ prop = properties[propname]
+ if value is None:
+ pass
+ elif isinstance(prop, hyperdb.Date):
+ value = date.Date(value)
+ elif isinstance(prop, hyperdb.Interval):
+ value = date.Interval(value)
+ elif isinstance(prop, hyperdb.Password):
+ pwd = password.Password()
+ pwd.unpack(value)
+ value = pwd
+ params[propname] = value
+ action = _names_to_actionnames[action]
+ r.append((nodeid, jdate, user, action, params))
+
+ for nodeid, l in d.items():
+ self.db.setjournal(self.classname, nodeid, l)
+
+def _fetchML(sv):
+ l = []
+ for row in sv:
+ if row.fid:
+ l.append(str(row.fid))
+ return l
+
+def _fetchPW(s):
+ ''' Convert to a password.Password unless the password is '' which is
+ our sentinel for "unset".
+ '''
+ if s == '':
+ return None
+ p = password.Password()
+ p.unpack(s)
+ return p
+
+def _fetchLink(n):
+ ''' Return None if the link is 0 - otherwise strify it.
+ '''
+ return n and str(n) or None
+
+def _fetchDate(n):
+ ''' Convert the timestamp to a date.Date instance - unless it's 0 which
+ is our sentinel for "unset".
+ '''
+ if n == 0:
+ return None
+ return date.Date(time.gmtime(n))
+
+def _fetchInterval(n):
+ ''' Convert to a date.Interval unless the interval is '' which is our
+ sentinel for "unset".
+ '''
+ if n == '':
+ return None
+ return date.Interval(n)
+
+# Converters for boolean and numbers to properly
+# return None values.
+# These are in conjunction with the setters above
+# look for hyperdb.Boolean and hyperdb.Number
+if BACKWARDS_COMPATIBLE:
+ def getBoolean(bool): return bool
+ def getNumber(number): return number
+else:
+ def getBoolean(bool):
+ if not bool: res = None
+ else: res = bool - 1
+ return res
+
+ def getNumber(number):
+ if number == 0: res = None
+ elif number < 0: res = number
+ else: res = number - 1
+ return res
+
+_converters = {
+ hyperdb.Date : _fetchDate,
+ hyperdb.Link : _fetchLink,
+ hyperdb.Multilink : _fetchML,
+ hyperdb.Interval : _fetchInterval,
+ hyperdb.Password : _fetchPW,
+ hyperdb.Boolean : getBoolean,
+ hyperdb.Number : getNumber,
+ hyperdb.String : lambda s: s and str(s) or None,
+}
+
+class FileName(hyperdb.String):
+ isfilename = 1
+
+_typmap = {
+ FileName : 'S',
+ hyperdb.String : 'S',
+ hyperdb.Date : 'I',
+ hyperdb.Link : 'I',
+ hyperdb.Multilink : 'V',
+ hyperdb.Interval : 'S',
+ hyperdb.Password : 'S',
+ hyperdb.Boolean : 'I',
+ hyperdb.Number : 'D',
+}
+class FileClass(hyperdb.FileClass, Class):
+ ''' like Class but with a content property
+ '''
+ def __init__(self, db, classname, **properties):
+ '''The newly-created class automatically includes the "content"
+ and "type" properties.
+ '''
+ if not properties.has_key('content'):
+ properties['content'] = hyperdb.String(indexme='yes')
+ if not properties.has_key('type'):
+ properties['type'] = hyperdb.String()
+ Class.__init__(self, db, classname, **properties)
+
+ def gen_filename(self, nodeid):
+ nm = '%s%s' % (self.classname, nodeid)
+ sd = str(int(int(nodeid) / 1000))
+ d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
+ if not os.path.exists(d):
+ os.makedirs(d)
+ return os.path.join(d, nm)
+
+ def export_files(self, dirname, nodeid):
+ ''' Export the "content" property as a file, not csv column
+ '''
+ source = self.gen_filename(nodeid)
+ x, filename = os.path.split(source)
+ x, subdir = os.path.split(x)
+ dest = os.path.join(dirname, self.classname+'-files', subdir, filename)
+ if not os.path.exists(os.path.dirname(dest)):
+ os.makedirs(os.path.dirname(dest))
+ shutil.copyfile(source, dest)
+
+ def import_files(self, dirname, nodeid):
+ ''' Import the "content" property as a file
+ '''
+ dest = self.gen_filename(nodeid)
+ x, filename = os.path.split(dest)
+ x, subdir = os.path.split(x)
+ source = os.path.join(dirname, self.classname+'-files', subdir,
+ filename)
+ if not os.path.exists(os.path.dirname(dest)):
+ os.makedirs(os.path.dirname(dest))
+ shutil.copyfile(source, dest)
+
+ mime_type = None
+ if self.getprops().has_key('type'):
+ mime_type = self.get(nodeid, 'type')
+ if not mime_type:
+ mime_type = self.default_mime_type
+ self.db.indexer.add_text((self.classname, nodeid, 'content'),
+ self.get(nodeid, 'content'), mime_type)
+
+ def get(self, nodeid, propname, default=_marker, cache=1):
+ if propname == 'content':
+ poss_msg = 'Possibly an access right configuration problem.'
+ fnm = self.gen_filename(nodeid)
+ if not os.path.exists(fnm):
+ fnm = fnm + '.tmp'
+ try:
+ f = open(fnm, 'rb')
+ except IOError, (strerror):
+ # XXX by catching this we donot see an error in the log.
+ return 'ERROR reading file: %s%s\n%s\n%s'%(
+ self.classname, nodeid, poss_msg, strerror)
+ x = f.read()
+ f.close()
+ else:
+ x = Class.get(self, nodeid, propname, default)
+ return x
+
+ def create(self, **propvalues):
+ if not propvalues:
+ raise ValueError, "Need something to create!"
+ self.fireAuditors('create', None, propvalues)
+
+ content = propvalues['content']
+ del propvalues['content']
+
+ newid = Class.create_inner(self, **propvalues)
+ if not content:
+ return newid
+
+ # figure a filename
+ nm = self.gen_filename(newid)
+
+ # make sure we don't register the rename action more than once
+ if not os.path.exists(nm + '.tmp'):
+ # register commit and rollback actions
+ def commit(fnm=nm):
+ os.rename(fnm + '.tmp', fnm)
+ self.commitaction(commit)
+ def undo(fnm=nm):
+ os.remove(fnm + '.tmp')
+ self.rollbackaction(undo)
+
+ # save the tempfile
+ f = open(nm + '.tmp', 'wb')
+ f.write(content)
+ f.close()
+
+ mimetype = propvalues.get('type', self.default_mime_type)
+ self.db.indexer.add_text((self.classname, newid, 'content'), content,
+ mimetype)
+ return newid
+
+ def set(self, itemid, **propvalues):
+ if not propvalues:
+ return
+ self.fireAuditors('set', None, propvalues)
+
+ content = propvalues.get('content', None)
+ if content is not None:
+ del propvalues['content']
+
+ propvalues, oldnode = Class.set_inner(self, itemid, **propvalues)
+
+ # figure a filename
+ if content is not None:
+ nm = self.gen_filename(itemid)
+
+ # make sure we don't register the rename action more than once
+ if not os.path.exists(nm + '.tmp'):
+ # register commit and rollback actions
+ def commit(fnm=nm):
+ if os.path.exists(fnm):
+ os.remove(fnm)
+ os.rename(fnm + '.tmp', fnm)
+ self.commitaction(commit)
+ def undo(fnm=nm):
+ os.remove(fnm + '.tmp')
+ self.rollbackaction(undo)
+
+ f = open(nm + '.tmp', 'wb')
+ f.write(content)
+ f.close()
+
+ if self.properties['content'].indexme:
+ mimetype = self.get('type', self.default_mime_type)
+ self.db.indexer.add_text((self.classname, itemid, 'content'),
+ content, mimetype)
+
+ self.fireReactors('set', oldnode, propvalues)
+
+ def index(self, nodeid):
+ ''' Add (or refresh) the node to search indexes.
+
+ Use the content-type property for the content property.
+ '''
+ # find all the String properties that have indexme
+ for prop, propclass in self.getprops().items():
+ if prop == 'content' and propclass.indexme:
+ mime_type = self.get(nodeid, 'type', self.default_mime_type)
+ self.db.indexer.add_text((self.classname, nodeid, 'content'),
+ str(self.get(nodeid, 'content')), mime_type)
+ elif isinstance(propclass, hyperdb.String) and propclass.indexme:
+ # index them under (classname, nodeid, property)
+ try:
+ value = str(self.get(nodeid, prop))
+ except IndexError:
+ # node has been destroyed
+ continue
+ self.db.indexer.add_text((self.classname, nodeid, prop), value)
+
+class IssueClass(Class, roundupdb.IssueClass):
+ ''' The newly-created class automatically includes the "messages",
+ "files", "nosy", and "superseder" properties. If the 'properties'
+ dictionary attempts to specify any of these properties or a
+ "creation" or "activity" property, a ValueError is raised.
+ '''
+ def __init__(self, db, classname, **properties):
+ if not properties.has_key('title'):
+ properties['title'] = hyperdb.String(indexme='yes')
+ if not properties.has_key('messages'):
+ properties['messages'] = hyperdb.Multilink("msg")
+ if not properties.has_key('files'):
+ properties['files'] = hyperdb.Multilink("file")
+ if not properties.has_key('nosy'):
+ # note: journalling is turned off as it really just wastes
+ # space. this behaviour may be overridden in an instance
+ properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
+ if not properties.has_key('superseder'):
+ properties['superseder'] = hyperdb.Multilink(classname)
+ Class.__init__(self, db, classname, **properties)
+
+CURVERSION = 2
+
+class MetakitIndexer(Indexer):
+ def __init__(self, db):
+ self.path = os.path.join(db.config.DATABASE, 'index.mk4')
+ self.db = metakit.storage(self.path, 1)
+ self.datadb = db._db
+ self.reindex = 0
+ v = self.db.view('version')
+ if not v.structure():
+ v = self.db.getas('version[vers:I]')
+ self.db.commit()
+ v.append(vers=CURVERSION)
+ self.reindex = 1
+ elif v[0].vers != CURVERSION:
+ v[0].vers = CURVERSION
+ self.reindex = 1
+ if self.reindex:
+ self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
+ self.db.getas('index[word:S,hits[pos:I]]')
+ self.db.commit()
+ self.reindex = 1
+ self.changed = 0
+ self.propcache = {}
+
+ def close(self):
+ '''close the indexing database'''
+ del self.db
+ self.db = None
+
+ def force_reindex(self):
+ '''Force a reindexing of the database. This essentially
+ empties the tables ids and index and sets a flag so
+ that the databases are reindexed'''
+ v = self.db.view('ids')
+ v[:] = []
+ v = self.db.view('index')
+ v[:] = []
+ self.db.commit()
+ self.reindex = 1
+
+ def should_reindex(self):
+ '''returns True if the indexes need to be rebuilt'''
+ return self.reindex
+
+ def _getprops(self, classname):
+ props = self.propcache.get(classname, None)
+ if props is None:
+ props = self.datadb.view(classname).structure()
+ props = [prop.name for prop in props]
+ self.propcache[classname] = props
+ return props
+
+ def _getpropid(self, classname, propname):
+ return self._getprops(classname).index(propname)
+
+ def _getpropname(self, classname, propid):
+ return self._getprops(classname)[propid]
+
+ def add_text(self, identifier, text, mime_type='text/plain'):
+ if mime_type != 'text/plain':
+ return
+ classname, nodeid, property = identifier
+ tbls = self.datadb.view('tables')
+ tblid = tbls.find(name=classname)
+ if tblid < 0:
+ raise KeyError, "unknown class %r"%classname
+ nodeid = int(nodeid)
+ propid = self._getpropid(classname, property)
+ ids = self.db.view('ids')
+ oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
+ if oldpos > -1:
+ ids[oldpos].ignore = 1
+ self.changed = 1
+ pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
+
+ wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
+ words = {}
+ for word in wordlist:
+ if not self.is_stopword(word):
+ words[word] = 1
+ words = words.keys()
+
+ index = self.db.view('index').ordered(1)
+ for word in words:
+ ndx = index.find(word=word)
+ if ndx < 0:
+ index.append(word=word)
+ ndx = index.find(word=word)
+ index[ndx].hits.append(pos=pos)
+ self.changed = 1
+
+ def find(self, wordlist):
+ '''look up all the words in the wordlist.
+ If none are found return an empty dictionary
+ * more rules here
+ '''
+ hits = None
+ index = self.db.view('index').ordered(1)
+ for word in wordlist:
+ word = word.upper()
+ if not 2 < len(word) < 26:
+ continue
+ ndx = index.find(word=word)
+ if ndx < 0:
+ return {}
+ if hits is None:
+ hits = index[ndx].hits
+ else:
+ hits = hits.intersect(index[ndx].hits)
+ if len(hits) == 0:
+ return {}
+ if hits is None:
+ return []
+ rslt = []
+ ids = self.db.view('ids').remapwith(hits)
+ tbls = self.datadb.view('tables')
+ for i in range(len(ids)):
+ hit = ids[i]
+ if not hit.ignore:
+ classname = tbls[hit.tblid].name
+ nodeid = str(hit.nodeid)
+ property = self._getpropname(classname, hit.propid)
+ rslt.append((classname, nodeid, property))
+ return rslt
+
+ def save_index(self):
+ if self.changed:
+ self.db.commit()
+ self.changed = 0
+
+ def rollback(self):
+ if self.changed:
+ self.db.rollback()
+ self.db = metakit.storage(self.path, 1)
+ self.changed = 0
+
+try:
+ from indexer_xapian import Indexer
+except ImportError:
+ Indexer = MetakitIndexer
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/backends/back_mysql.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/back_mysql.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,830 @@
+#$Id: back_mysql.py,v 1.68 2006/04/27 03:40:42 richard Exp $
+#
+# Copyright (c) 2003 Martynas Sklyzmantas, Andrey Lebedev <andrey at micro.lt>
+#
+# 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.
+#
+
+'''This module defines a backend implementation for MySQL.
+
+
+How to implement AUTO_INCREMENT:
+
+mysql> create table foo (num integer auto_increment primary key, name
+varchar(255)) AUTO_INCREMENT=1 type=InnoDB;
+
+ql> insert into foo (name) values ('foo5');
+Query OK, 1 row affected (0.00 sec)
+
+mysql> SELECT num FROM foo WHERE num IS NULL;
++-----+
+| num |
++-----+
+| 4 |
++-----+
+1 row in set (0.00 sec)
+
+mysql> SELECT num FROM foo WHERE num IS NULL;
+Empty set (0.00 sec)
+
+NOTE: we don't need an index on the id column if it's PRIMARY KEY
+
+'''
+__docformat__ = 'restructuredtext'
+
+from roundup.backends.rdbms_common import *
+from roundup.backends import rdbms_common
+import MySQLdb
+import os, shutil
+from MySQLdb.constants import ER
+import logging
+
+def connection_dict(config, dbnamestr=None):
+ d = rdbms_common.connection_dict(config, dbnamestr)
+ if d.has_key('password'):
+ d['passwd'] = d['password']
+ del d['password']
+ if d.has_key('port'):
+ d['port'] = int(d['port'])
+ return d
+
+def db_nuke(config):
+ """Clear all database contents and drop database itself"""
+ if db_exists(config):
+ kwargs = connection_dict(config)
+ conn = MySQLdb.connect(**kwargs)
+ try:
+ conn.select_db(config.RDBMS_NAME)
+ except:
+ # no, it doesn't exist
+ pass
+ else:
+ cursor = conn.cursor()
+ cursor.execute("SHOW TABLES")
+ tables = cursor.fetchall()
+ # 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)
+ cursor.execute(command)
+ command = "DROP DATABASE %s"%config.RDBMS_NAME
+ logging.getLogger('hyperdb').info(command)
+ cursor.execute(command)
+ conn.commit()
+ conn.close()
+
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+
+def db_create(config):
+ """Create the database."""
+ kwargs = connection_dict(config)
+ conn = MySQLdb.connect(**kwargs)
+ cursor = conn.cursor()
+ command = "CREATE DATABASE %s"%config.RDBMS_NAME
+ logging.getLogger('hyperdb').info(command)
+ cursor.execute(command)
+ conn.commit()
+ conn.close()
+
+def db_exists(config):
+ """Check if database already exists."""
+ kwargs = connection_dict(config)
+ conn = MySQLdb.connect(**kwargs)
+ try:
+ try:
+ conn.select_db(config.RDBMS_NAME)
+ except MySQLdb.OperationalError:
+ return 0
+ finally:
+ conn.close()
+ return 1
+
+
+class Database(Database):
+ arg = '%s'
+
+ # used by some code to switch styles of query
+ implements_intersect = 0
+
+ # Backend for MySQL to use.
+ # InnoDB is faster, but if you're running <4.0.16 then you'll need to
+ # use BDB to pass all unit tests.
+ mysql_backend = 'InnoDB'
+ #mysql_backend = 'BDB'
+
+ hyperdb_to_sql_datatypes = {
+ hyperdb.String : 'TEXT',
+ hyperdb.Date : 'DATETIME',
+ hyperdb.Link : 'INTEGER',
+ hyperdb.Interval : 'VARCHAR(255)',
+ hyperdb.Password : 'VARCHAR(255)',
+ hyperdb.Boolean : 'BOOL',
+ hyperdb.Number : 'REAL',
+ }
+
+ hyperdb_to_sql_value = {
+ hyperdb.String : str,
+ # no fractional seconds for MySQL
+ hyperdb.Date : lambda x: x.formal(sep=' '),
+ hyperdb.Link : int,
+ hyperdb.Interval : str,
+ hyperdb.Password : str,
+ hyperdb.Boolean : int,
+ hyperdb.Number : lambda x: x,
+ hyperdb.Multilink : lambda x: x, # used in journal marshalling
+ }
+
+ def sql_open_connection(self):
+ kwargs = connection_dict(self.config, 'db')
+ logging.getLogger('hyperdb').info('open database %r'%(kwargs['db'],))
+ try:
+ conn = MySQLdb.connect(**kwargs)
+ except MySQLdb.OperationalError, message:
+ raise DatabaseError, message
+ cursor = conn.cursor()
+ cursor.execute("SET AUTOCOMMIT=0")
+ cursor.execute("START TRANSACTION")
+ return (conn, cursor)
+
+ def open_connection(self):
+ # make sure the database actually exists
+ if not db_exists(self.config):
+ db_create(self.config)
+
+ self.conn, self.cursor = self.sql_open_connection()
+
+ try:
+ self.load_dbschema()
+ except MySQLdb.OperationalError, message:
+ if message[0] != ER.NO_DB_ERROR:
+ raise
+ except MySQLdb.ProgrammingError, message:
+ if message[0] != ER.NO_SUCH_TABLE:
+ raise DatabaseError, message
+ self.init_dbschema()
+ self.sql("CREATE TABLE `schema` (`schema` TEXT) TYPE=%s"%
+ self.mysql_backend)
+ self.sql('''CREATE TABLE ids (name VARCHAR(255),
+ num INTEGER) TYPE=%s'''%self.mysql_backend)
+ self.sql('create index ids_name_idx on ids(name)')
+ self.create_version_2_tables()
+
+ def load_dbschema(self):
+ ''' Load the schema definition that the database currently implements
+ '''
+ self.cursor.execute('select `schema` from `schema`')
+ schema = self.cursor.fetchone()
+ if schema:
+ self.database_schema = eval(schema[0])
+ else:
+ self.database_schema = {}
+
+ def save_dbschema(self):
+ ''' Save the schema definition that the database currently implements
+ '''
+ s = repr(self.database_schema)
+ self.sql('delete from `schema`')
+ self.sql('insert into `schema` values (%s)', (s,))
+
+ def create_version_2_tables(self):
+ # OTK store
+ self.sql('''CREATE TABLE otks (otk_key VARCHAR(255),
+ otk_value TEXT, otk_time FLOAT(20))
+ TYPE=%s'''%self.mysql_backend)
+ self.sql('CREATE INDEX otks_key_idx ON otks(otk_key)')
+
+ # Sessions store
+ self.sql('''CREATE TABLE sessions (session_key VARCHAR(255),
+ session_time FLOAT(20), session_value TEXT)
+ TYPE=%s'''%self.mysql_backend)
+ self.sql('''CREATE INDEX sessions_key_idx ON
+ sessions(session_key)''')
+
+ # full-text indexing store
+ self.sql('''CREATE TABLE __textids (_class VARCHAR(255),
+ _itemid VARCHAR(255), _prop VARCHAR(255), _textid INT)
+ TYPE=%s'''%self.mysql_backend)
+ self.sql('''CREATE TABLE __words (_word VARCHAR(30),
+ _textid INT) TYPE=%s'''%self.mysql_backend)
+ self.sql('CREATE INDEX words_word_ids ON __words(_word)')
+ sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
+ self.sql(sql, ('__textids', 1))
+
+ def add_new_columns_v2(self):
+ '''While we're adding the actor column, we need to update the
+ tables to have the correct datatypes.'''
+ for klass in self.classes.values():
+ cn = klass.classname
+ properties = klass.getprops()
+ old_spec = self.database_schema['tables'][cn]
+
+ # figure the non-Multilink properties to copy over
+ propnames = ['activity', 'creation', 'creator']
+
+ # figure actions based on data type
+ for name, s_prop in old_spec[1]:
+ # s_prop is a repr() string of a hyperdb type object
+ if s_prop.find('Multilink') == -1:
+ if properties.has_key(name):
+ propnames.append(name)
+ continue
+ tn = '%s_%s'%(cn, name)
+
+ if properties.has_key(name):
+ # grabe the current values
+ sql = 'select linkid, nodeid from %s'%tn
+ self.sql(sql)
+ rows = self.cursor.fetchall()
+
+ # drop the old table
+ self.drop_multilink_table_indexes(cn, name)
+ sql = 'drop table %s'%tn
+ self.sql(sql)
+
+ if properties.has_key(name):
+ # re-create and populate the new table
+ self.create_multilink_table(klass, name)
+ 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)))
+
+ # figure the column names to fetch
+ fetch = ['_%s'%name for name in propnames]
+
+ # select the data out of the old table
+ fetch.append('id')
+ fetch.append('__retired__')
+ fetchcols = ','.join(fetch)
+ sql = 'select %s from _%s'%(fetchcols, cn)
+ self.sql(sql)
+
+ # unserialise the old data
+ olddata = []
+ propnames = propnames + ['id', '__retired__']
+ cols = []
+ first = 1
+ for entry in self.cursor.fetchall():
+ l = []
+ olddata.append(l)
+ for i in range(len(propnames)):
+ name = propnames[i]
+ v = entry[i]
+
+ if name in ('id', '__retired__'):
+ if first:
+ cols.append(name)
+ l.append(int(v))
+ continue
+ if first:
+ cols.append('_' + name)
+ prop = properties[name]
+ if isinstance(prop, Date) and v is not None:
+ v = date.Date(v)
+ elif isinstance(prop, Interval) and v is not None:
+ v = date.Interval(v)
+ elif isinstance(prop, Password) and v is not None:
+ v = password.Password(encrypted=v)
+ elif (isinstance(prop, Boolean) or
+ isinstance(prop, Number)) and v is not None:
+ v = float(v)
+
+ # convert to new MySQL data type
+ prop = properties[name]
+ if v is not None:
+ e = self.hyperdb_to_sql_value[prop.__class__](v)
+ else:
+ e = None
+ l.append(e)
+
+ # Intervals store the seconds value too
+ if isinstance(prop, Interval):
+ if first:
+ cols.append('__' + name + '_int__')
+ if v is not None:
+ l.append(v.as_seconds())
+ else:
+ l.append(e)
+ first = 0
+
+ self.drop_class_table_indexes(cn, old_spec[0])
+
+ # drop the old table
+ self.sql('drop table _%s'%cn)
+
+ # create the new table
+ self.create_class_table(klass)
+
+ # do the insert of the old data
+ args = ','.join([self.arg for x in cols])
+ cols = ','.join(cols)
+ sql = 'insert into _%s (%s) values (%s)'%(cn, cols, args)
+ for entry in olddata:
+ self.sql(sql, tuple(entry))
+
+ # now load up the old journal data to migrate it
+ cols = ','.join('nodeid date tag action params'.split())
+ sql = 'select %s from %s__journal'%(cols, cn)
+ self.sql(sql)
+
+ # data conversions
+ olddata = []
+ for nodeid, journaldate, journaltag, action, params in \
+ self.cursor.fetchall():
+ #nodeid = int(nodeid)
+ journaldate = date.Date(journaldate)
+ #params = eval(params)
+ olddata.append((nodeid, journaldate, journaltag, action,
+ params))
+
+ # drop journal table and indexes
+ self.drop_journal_table_indexes(cn)
+ sql = 'drop table %s__journal'%cn
+ self.sql(sql)
+
+ # re-create journal table
+ self.create_journal_table(klass)
+ dc = self.hyperdb_to_sql_value[hyperdb.Date]
+ for nodeid, journaldate, journaltag, action, params in olddata:
+ self.save_journal(cn, cols, nodeid, dc(journaldate),
+ journaltag, action, params)
+
+ # make sure the normal schema update code doesn't try to
+ # change things
+ self.database_schema['tables'][cn] = klass.schema()
+
+ def fix_version_2_tables(self):
+ # Convert journal date column to TIMESTAMP, params column to TEXT
+ self._convert_journal_tables()
+
+ # Convert all String properties to TEXT
+ self._convert_string_properties()
+
+ def __repr__(self):
+ return '<myroundsql 0x%x>'%id(self)
+
+ def sql_fetchone(self):
+ return self.cursor.fetchone()
+
+ def sql_fetchall(self):
+ return self.cursor.fetchall()
+
+ def sql_index_exists(self, table_name, index_name):
+ self.sql('show index from %s'%table_name)
+ for index in self.cursor.fetchall():
+ if index[2] == index_name:
+ return 1
+ return 0
+
+ def create_class_table(self, spec, create_sequence=1):
+ cols, mls = self.determine_columns(spec.properties.items())
+
+ # add on our special columns
+ cols.append(('id', 'INTEGER PRIMARY KEY'))
+ cols.append(('__retired__', 'INTEGER DEFAULT 0'))
+
+ # create the base table
+ scols = ','.join(['%s %s'%x for x in cols])
+ sql = 'create table _%s (%s) type=%s'%(spec.classname, scols,
+ self.mysql_backend)
+ self.sql(sql)
+
+ self.create_class_table_indexes(spec)
+ return cols, mls
+
+ def create_class_table_indexes(self, spec):
+ ''' create the class table for the given spec
+ '''
+ # create __retired__ index
+ index_sql2 = 'create index _%s_retired_idx on _%s(__retired__)'%(
+ spec.classname, spec.classname)
+ self.sql(index_sql2)
+
+ # create index for key property
+ if spec.key:
+ if isinstance(spec.properties[spec.key], String):
+ idx = spec.key + '(255)'
+ else:
+ idx = spec.key
+ index_sql3 = 'create index _%s_%s_idx on _%s(_%s)'%(
+ spec.classname, spec.key,
+ spec.classname, idx)
+ self.sql(index_sql3)
+
+ # TODO: create indexes on (selected?) Link property columns, as
+ # they're more likely to be used for lookup
+
+ def create_class_table_key_index(self, cn, key):
+ ''' create the class table for the given spec
+ '''
+ prop = self.classes[cn].getprops()[key]
+ if isinstance(prop, String):
+ sql = 'create index _%s_%s_idx on _%s(_%s(255))'%(cn, key, cn, key)
+ else:
+ sql = 'create index _%s_%s_idx on _%s(_%s)'%(cn, key, cn, key)
+ self.sql(sql)
+
+ def drop_class_table_indexes(self, cn, key):
+ # drop the old table indexes first
+ l = ['_%s_id_idx'%cn, '_%s_retired_idx'%cn]
+ if key:
+ l.append('_%s_%s_idx'%(cn, key))
+
+ table_name = '_%s'%cn
+ for index_name in l:
+ if not self.sql_index_exists(table_name, index_name):
+ continue
+ index_sql = 'drop index %s on %s'%(index_name, table_name)
+ self.sql(index_sql)
+
+ def create_journal_table(self, spec):
+ ''' create the journal table for a class given the spec and
+ already-determined cols
+ '''
+ # journal table
+ cols = ','.join(['%s varchar'%x
+ for x in 'nodeid date tag action params'.split()])
+ sql = '''create table %s__journal (
+ nodeid integer, date datetime, tag varchar(255),
+ action varchar(255), params text) type=%s'''%(
+ spec.classname, self.mysql_backend)
+ self.sql(sql)
+ self.create_journal_table_indexes(spec)
+
+ def drop_journal_table_indexes(self, classname):
+ index_name = '%s_journ_idx'%classname
+ if not self.sql_index_exists('%s__journal'%classname, index_name):
+ return
+ index_sql = 'drop index %s on %s__journal'%(index_name, classname)
+ self.sql(index_sql)
+
+ def create_multilink_table(self, spec, ml):
+ sql = '''CREATE TABLE `%s_%s` (linkid VARCHAR(255),
+ nodeid VARCHAR(255)) TYPE=%s'''%(spec.classname, ml,
+ self.mysql_backend)
+ self.sql(sql)
+ self.create_multilink_table_indexes(spec, ml)
+
+ def drop_multilink_table_indexes(self, classname, ml):
+ l = [
+ '%s_%s_l_idx'%(classname, ml),
+ '%s_%s_n_idx'%(classname, ml)
+ ]
+ table_name = '%s_%s'%(classname, ml)
+ for index_name in l:
+ if not self.sql_index_exists(table_name, index_name):
+ continue
+ sql = 'drop index %s on %s'%(index_name, table_name)
+ self.sql(sql)
+
+ def drop_class_table_key_index(self, cn, key):
+ table_name = '_%s'%cn
+ index_name = '_%s_%s_idx'%(cn, key)
+ if not self.sql_index_exists(table_name, index_name):
+ return
+ sql = 'drop index %s on %s'%(index_name, table_name)
+ self.sql(sql)
+
+ # old-skool id generation
+ def newid(self, classname):
+ ''' Generate a new id for the given class
+ '''
+ # get the next ID - "FOR UPDATE" will lock the row for us
+ sql = 'select num from ids where name=%s FOR UPDATE'%self.arg
+ self.sql(sql, (classname, ))
+ newid = int(self.cursor.fetchone()[0])
+
+ # update the counter
+ sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
+ vals = (int(newid)+1, classname)
+ self.sql(sql, vals)
+
+ # return as string
+ return str(newid)
+
+ def setid(self, classname, setid):
+ ''' Set the id counter: used during import of database
+
+ We add one to make it behave like the seqeunces in postgres.
+ '''
+ sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
+ vals = (int(setid)+1, classname)
+ self.sql(sql, vals)
+
+ def clear(self):
+ rdbms_common.Database.clear(self)
+
+ # set the id counters to 0 (setid adds one) so we start at 1
+ for cn in self.classes.keys():
+ self.setid(cn, 0)
+
+ def create_class(self, spec):
+ rdbms_common.Database.create_class(self, spec)
+ sql = 'insert into ids (name, num) values (%s, %s)'
+ vals = (spec.classname, 1)
+ self.sql(sql, vals)
+
+ def sql_commit(self):
+ ''' Actually commit to the database.
+ '''
+ logging.getLogger('hyperdb').info('commit')
+ self.conn.commit()
+
+ # open a new cursor for subsequent work
+ self.cursor = self.conn.cursor()
+
+ # make sure we're in a new transaction and not autocommitting
+ self.sql("SET AUTOCOMMIT=0")
+ self.sql("START TRANSACTION")
+
+ def sql_close(self):
+ logging.getLogger('hyperdb').info('close')
+ try:
+ self.conn.close()
+ except MySQLdb.ProgrammingError, message:
+ if str(message) != 'closing a closed connection':
+ raise
+
+class MysqlClass:
+ # we're overriding this method for ONE missing bit of functionality.
+ # look for "I can't believe it's not a toy RDBMS" below
+ def filter(self, search_matches, filterspec, sort=(None,None),
+ group=(None,None)):
+ '''Return a list of the ids of the active nodes in this class that
+ match the 'filter' spec, sorted by the group spec and then the
+ sort spec
+
+ "filterspec" is {propname: value(s)}
+
+ "sort" and "group" are (dir, prop) where dir is '+', '-' or None
+ and prop is a prop name or None
+
+ "search_matches" is {nodeid: marker} or None
+
+ The filter must match all properties specificed. If the property
+ value to match is a list:
+
+ 1. String properties must match all elements in the list, and
+ 2. Other properties must match any of the elements in the list.
+ '''
+ # we can't match anything if search_matches is empty
+ if search_matches == {}:
+ return []
+
+ if __debug__:
+ start_t = time.time()
+
+ cn = self.classname
+
+ # vars to hold the components of the SQL statement
+ frum = ['_'+cn] # FROM clauses
+ loj = [] # LEFT OUTER JOIN clauses
+ where = [] # WHERE clauses
+ args = [] # *any* positional arguments
+ a = self.db.arg
+
+ # figure the WHERE clause from the filterspec
+ props = self.getprops()
+ mlfilt = 0 # are we joining with Multilink tables?
+ for k, v in filterspec.items():
+ propclass = props[k]
+ # now do other where clause stuff
+ if isinstance(propclass, Multilink):
+ mlfilt = 1
+ tn = '%s_%s'%(cn, k)
+ if v in ('-1', ['-1']):
+ # only match rows that have count(linkid)=0 in the
+ # corresponding multilink table)
+
+ # "I can't believe it's not a toy RDBMS"
+ # see, even toy RDBMSes like gadfly and sqlite can do
+ # sub-selects...
+ self.db.sql('select nodeid from %s'%tn)
+ s = ','.join([x[0] for x in self.db.sql_fetchall()])
+
+ where.append('_%s.id not in (%s)'%(cn, s))
+ elif isinstance(v, type([])):
+ frum.append(tn)
+ s = ','.join([a for x in v])
+ where.append('_%s.id=%s.nodeid and %s.linkid in (%s)'%(cn,
+ tn, tn, s))
+ args = args + v
+ else:
+ frum.append(tn)
+ where.append('_%s.id=%s.nodeid and %s.linkid=%s'%(cn, tn,
+ tn, a))
+ args.append(v)
+ elif k == 'id':
+ if isinstance(v, type([])):
+ s = ','.join([a for x in v])
+ where.append('_%s.%s in (%s)'%(cn, k, s))
+ args = args + v
+ else:
+ where.append('_%s.%s=%s'%(cn, k, a))
+ args.append(v)
+ elif isinstance(propclass, String):
+ if not isinstance(v, type([])):
+ v = [v]
+
+ # Quote the bits in the string that need it and then embed
+ # in a "substring" search. Note - need to quote the '%' so
+ # they make it through the python layer happily
+ v = ['%%'+self.db.sql_stringquote(s)+'%%' for s in v]
+
+ # now add to the where clause
+ where.append('('
+ +' and '.join(["_%s._%s LIKE '%s'"%(cn, k, s) for s in v])
+ +')')
+ # note: args are embedded in the query string now
+ elif isinstance(propclass, Link):
+ if isinstance(v, type([])):
+ d = {}
+ for entry in v:
+ if entry == '-1':
+ entry = None
+ d[entry] = entry
+ l = []
+ if d.has_key(None) or not d:
+ del d[None]
+ l.append('_%s._%s is NULL'%(cn, k))
+ if d:
+ v = d.keys()
+ s = ','.join([a for x in v])
+ l.append('(_%s._%s in (%s))'%(cn, k, s))
+ args = args + v
+ if l:
+ where.append('(' + ' or '.join(l) +')')
+ else:
+ if v in ('-1', None):
+ v = None
+ where.append('_%s._%s is NULL'%(cn, k))
+ else:
+ where.append('_%s._%s=%s'%(cn, k, a))
+ args.append(v)
+ elif isinstance(propclass, Date):
+ dc = self.db.hyperdb_to_sql_value[hyperdb.Date]
+ if isinstance(v, type([])):
+ s = ','.join([a for x in v])
+ where.append('_%s._%s in (%s)'%(cn, k, s))
+ args = args + [dc(date.Date(x)) for x in v]
+ else:
+ try:
+ # Try to filter on range of dates
+ date_rng = propclass.range_from_raw (v, self.db)
+ if date_rng.from_value:
+ where.append('_%s._%s >= %s'%(cn, k, a))
+ args.append(dc(date_rng.from_value))
+ if date_rng.to_value:
+ where.append('_%s._%s <= %s'%(cn, k, a))
+ args.append(dc(date_rng.to_value))
+ except ValueError:
+ # If range creation fails - ignore that search parameter
+ pass
+ elif isinstance(propclass, Interval):
+ # filter using the __<prop>_int__ column
+ if isinstance(v, type([])):
+ s = ','.join([a for x in v])
+ where.append('_%s.__%s_int__ in (%s)'%(cn, k, s))
+ args = args + [date.Interval(x).as_seconds() for x in v]
+ else:
+ try:
+ # Try to filter on range of intervals
+ date_rng = Range(v, date.Interval)
+ if date_rng.from_value:
+ where.append('_%s.__%s_int__ >= %s'%(cn, k, a))
+ args.append(date_rng.from_value.as_seconds())
+ if date_rng.to_value:
+ where.append('_%s.__%s_int__ <= %s'%(cn, k, a))
+ args.append(date_rng.to_value.as_seconds())
+ except ValueError:
+ # If range creation fails - ignore that search parameter
+ pass
+ else:
+ if isinstance(v, type([])):
+ s = ','.join([a for x in v])
+ where.append('_%s._%s in (%s)'%(cn, k, s))
+ args = args + v
+ else:
+ where.append('_%s._%s=%s'%(cn, k, a))
+ args.append(v)
+
+ # don't match retired nodes
+ where.append('_%s.__retired__ <> 1'%cn)
+
+ # add results of full text search
+ if search_matches is not None:
+ v = search_matches.keys()
+ s = ','.join([a for x in v])
+ where.append('_%s.id in (%s)'%(cn, s))
+ args = args + v
+
+ # sanity check: sorting *and* grouping on the same property?
+ if group[1] == sort[1]:
+ sort = (None, None)
+
+ # "grouping" is just the first-order sorting in the SQL fetch
+ orderby = []
+ ordercols = []
+ mlsort = []
+ rhsnum = 0
+ for sortby in group, sort:
+ sdir, prop = sortby
+ if sdir and prop:
+ if isinstance(props[prop], Multilink):
+ mlsort.append(sortby)
+ continue
+ elif isinstance(props[prop], Interval):
+ # use the int column for sorting
+ o = '__'+prop+'_int__'
+ ordercols.append(o)
+ elif isinstance(props[prop], Link):
+ # determine whether the linked Class has an order property
+ lcn = props[prop].classname
+ link = self.db.classes[lcn]
+ o = '_%s._%s'%(cn, prop)
+ op = link.orderprop ()
+ if op != 'id':
+ tn = '_' + lcn
+ rhs = 'rhs%s_'%rhsnum
+ rhsnum += 1
+ loj.append('LEFT OUTER JOIN %s as %s on %s=%s.id'%(
+ tn, rhs, o, rhs))
+ o = '%s._%s'%(rhs, op)
+ ordercols.append(o)
+ elif prop == 'id':
+ o = '_%s.id'%cn
+ else:
+ o = '_%s._%s'%(cn, prop)
+ ordercols.append(o)
+ if sdir == '-':
+ o += ' desc'
+ orderby.append(o)
+
+ # construct the SQL
+ frum = ','.join(frum)
+ if where:
+ where = ' where ' + (' and '.join(where))
+ else:
+ where = ''
+ if mlfilt:
+ # we're joining tables on the id, so we will get dupes if we
+ # don't distinct()
+ cols = ['distinct(_%s.id)'%cn]
+ else:
+ cols = ['_%s.id'%cn]
+ if orderby:
+ cols = cols + ordercols
+ order = ' order by %s'%(','.join(orderby))
+ else:
+ order = ''
+ cols = ','.join(cols)
+ loj = ' '.join(loj)
+ sql = 'select %s from %s %s %s%s'%(cols, frum, loj, where, order)
+ args = tuple(args)
+ self.db.sql(sql, args)
+ l = self.db.cursor.fetchall()
+
+ # return the IDs (the first column)
+ # XXX numeric ids
+ l = [str(row[0]) for row in l]
+
+ if not mlsort:
+ if __debug__:
+ self.db.stats['filtering'] += (time.time() - start_t)
+ return l
+
+ # ergh. someone wants to sort by a multilink.
+ r = []
+ for id in l:
+ m = []
+ for ml in mlsort:
+ m.append(self.get(id, ml[1]))
+ r.append((id, m))
+ i = 0
+ for sortby in mlsort:
+ def sortfun(a, b, dir=sortby[i], i=i):
+ if dir == '-':
+ return cmp(b[1][i], a[1][i])
+ else:
+ return cmp(a[1][i], b[1][i])
+ r.sort(sortfun)
+ i += 1
+ r = [i[0] for i in r]
+
+ if __debug__:
+ self.db.stats['filtering'] += (time.time() - start_t)
+
+ return r
+
+class Class(MysqlClass, rdbms_common.Class):
+ pass
+class IssueClass(MysqlClass, rdbms_common.IssueClass):
+ pass
+class FileClass(MysqlClass, rdbms_common.FileClass):
+ pass
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/backends/back_postgresql.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/back_postgresql.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,230 @@
+#$Id: back_postgresql.py,v 1.31 2006/01/20 02:27:12 richard Exp $
+#
+# Copyright (c) 2003 Martynas Sklyzmantas, Andrey Lebedev <andrey at micro.lt>
+#
+# 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.
+#
+'''Postgresql backend via psycopg for Roundup.'''
+__docformat__ = 'restructuredtext'
+
+import os, shutil, popen2, time
+import psycopg
+import logging
+
+from roundup import hyperdb, date
+from roundup.backends import rdbms_common
+
+def connection_dict(config, dbnamestr=None):
+ ''' read_default_group is MySQL-specific, ignore it '''
+ d = rdbms_common.connection_dict(config, dbnamestr)
+ if d.has_key('read_default_group'):
+ del d['read_default_group']
+ if d.has_key('read_default_file'):
+ del d['read_default_file']
+ return d
+
+def db_create(config):
+ """Clear all database contents and drop database itself"""
+ command = "CREATE DATABASE %s WITH ENCODING='UNICODE'"%config.RDBMS_NAME
+ logging.getLogger('hyperdb').info(command)
+ db_command(config, command)
+
+def db_nuke(config, fail_ok=0):
+ """Clear all database contents and drop database itself"""
+ command = 'DROP DATABASE %s'% config.RDBMS_NAME
+ logging.getLogger('hyperdb').info(command)
+ db_command(config, command)
+
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+
+def db_command(config, command):
+ '''Perform some sort of database-level command. Retry 10 times if we
+ fail by conflicting with another user.
+ '''
+ template1 = connection_dict(config)
+ template1['database'] = 'template1'
+
+ try:
+ conn = psycopg.connect(**template1)
+ except psycopg.OperationalError, message:
+ raise hyperdb.DatabaseError, message
+
+ conn.set_isolation_level(0)
+ cursor = conn.cursor()
+ try:
+ for n in range(10):
+ if pg_command(cursor, command):
+ return
+ finally:
+ conn.close()
+ raise RuntimeError, '10 attempts to create database failed'
+
+def pg_command(cursor, command):
+ '''Execute the postgresql command, which may be blocked by some other
+ user connecting to the database, and return a true value if it succeeds.
+ '''
+ try:
+ cursor.execute(command)
+ except psycopg.ProgrammingError, err:
+ response = str(err).split('\n')[0]
+ if response.find('FATAL') != -1:
+ raise RuntimeError, response
+ elif response.find('ERROR') != -1:
+ if response.find('is being accessed by other users') == -1:
+ raise RuntimeError, response
+ time.sleep(1)
+ return 0
+ return 1
+
+def db_exists(config):
+ """Check if database already exists"""
+ db = connection_dict(config, 'database')
+ try:
+ conn = psycopg.connect(**db)
+ conn.close()
+ return 1
+ except:
+ return 0
+
+class Database(rdbms_common.Database):
+ arg = '%s'
+
+ # used by some code to switch styles of query
+ implements_intersect = 1
+
+ def sql_open_connection(self):
+ db = connection_dict(self.config, 'database')
+ logging.getLogger('hyperdb').info('open database %r'%db['database'])
+ try:
+ conn = psycopg.connect(**db)
+ except psycopg.OperationalError, message:
+ raise hyperdb.DatabaseError, message
+
+ cursor = conn.cursor()
+
+ return (conn, cursor)
+
+ def open_connection(self):
+ if not db_exists(self.config):
+ db_create(self.config)
+
+ self.conn, self.cursor = self.sql_open_connection()
+
+ try:
+ self.load_dbschema()
+ except psycopg.ProgrammingError, message:
+ if str(message).find('"schema" does not exist') == -1:
+ raise
+ self.rollback()
+ self.init_dbschema()
+ self.sql("CREATE TABLE schema (schema TEXT)")
+ self.sql("CREATE TABLE dual (dummy integer)")
+ self.sql("insert into dual values (1)")
+ self.create_version_2_tables()
+
+ def create_version_2_tables(self):
+ # OTK store
+ self.sql('''CREATE TABLE otks (otk_key VARCHAR(255),
+ otk_value TEXT, otk_time REAL)''')
+ self.sql('CREATE INDEX otks_key_idx ON otks(otk_key)')
+
+ # Sessions store
+ self.sql('''CREATE TABLE sessions (
+ session_key VARCHAR(255), session_time REAL,
+ session_value TEXT)''')
+ self.sql('''CREATE INDEX sessions_key_idx ON
+ sessions(session_key)''')
+
+ # full-text indexing store
+ self.sql('CREATE SEQUENCE ___textids_ids')
+ 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),
+ _textid integer)''')
+ self.sql('CREATE INDEX words_word_idx ON __words(_word)')
+
+ def fix_version_2_tables(self):
+ # Convert journal date column to TIMESTAMP, params column to TEXT
+ self._convert_journal_tables()
+
+ # Convert all String properties to TEXT
+ self._convert_string_properties()
+
+ # convert session / OTK *_time columns to REAL
+ for name in ('otk', 'session'):
+ self.sql('drop index %ss_key_idx'%name)
+ self.sql('drop table %ss'%name)
+ self.sql('''CREATE TABLE %ss (%s_key VARCHAR(255),
+ %s_value VARCHAR(255), %s_time REAL)'''%(name, name, name,
+ name))
+ self.sql('CREATE INDEX %ss_key_idx ON %ss(%s_key)'%(name, name,
+ name))
+
+ def fix_version_3_tables(self):
+ rdbms_common.Database.fix_version_3_tables(self)
+ self.sql('''CREATE INDEX words_both_idx ON public.__words
+ USING btree (_word, _textid)''')
+
+ def add_actor_column(self):
+ # update existing tables to have the new actor column
+ tables = self.database_schema['tables']
+ for name in tables.keys():
+ self.sql('ALTER TABLE _%s add __actor VARCHAR(255)'%name)
+
+ def __repr__(self):
+ return '<roundpsycopgsql 0x%x>' % id(self)
+
+ def sql_stringquote(self, value):
+ ''' psycopg.QuotedString returns a "buffer" object with the
+ single-quotes around it... '''
+ return str(psycopg.QuotedString(str(value)))[1:-1]
+
+ def sql_index_exists(self, table_name, index_name):
+ sql = 'select count(*) from pg_indexes where ' \
+ 'tablename=%s and indexname=%s'%(self.arg, self.arg)
+ self.sql(sql, (table_name, index_name))
+ return self.cursor.fetchone()[0]
+
+ def create_class_table(self, spec, create_sequence=1):
+ if create_sequence:
+ sql = 'CREATE SEQUENCE _%s_ids'%spec.classname
+ self.sql(sql)
+
+ return rdbms_common.Database.create_class_table(self, spec)
+
+ def drop_class_table(self, cn):
+ sql = 'drop table _%s'%cn
+ self.sql(sql)
+
+ sql = 'drop sequence _%s_ids'%cn
+ self.sql(sql)
+
+ def newid(self, classname):
+ sql = "select nextval('_%s_ids') from dual"%classname
+ self.sql(sql)
+ return self.cursor.fetchone()[0]
+
+ def setid(self, classname, setid):
+ sql = "select setval('_%s_ids', %s) from dual"%(classname, int(setid))
+ self.sql(sql)
+
+ def clear(self):
+ rdbms_common.Database.clear(self)
+
+ # reset the sequences
+ for cn in self.classes.keys():
+ self.cursor.execute('DROP SEQUENCE _%s_ids'%cn)
+ self.cursor.execute('CREATE SEQUENCE _%s_ids'%cn)
+
+
+class Class(rdbms_common.Class):
+ pass
+class IssueClass(rdbms_common.IssueClass):
+ pass
+class FileClass(rdbms_common.FileClass):
+ pass
+
Added: tracker/vendor/roundup/current/roundup/backends/back_sqlite.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/back_sqlite.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,364 @@
+# $Id: back_sqlite.py,v 1.44 2006/04/27 04:59:37 richard Exp $
+'''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
+
+from roundup import hyperdb, date, password
+from roundup.backends import rdbms_common
+import sqlite
+
+def db_exists(config):
+ return os.path.exists(os.path.join(config.DATABASE, 'db'))
+
+def db_nuke(config):
+ shutil.rmtree(config.DATABASE)
+
+class Database(rdbms_common.Database):
+ # char to use for positional arguments
+ arg = '%s'
+
+ # used by some code to switch styles of query
+ implements_intersect = 1
+
+ hyperdb_to_sql_datatypes = {
+ hyperdb.String : 'VARCHAR(255)',
+ hyperdb.Date : 'VARCHAR(30)',
+ hyperdb.Link : 'INTEGER',
+ hyperdb.Interval : 'VARCHAR(255)',
+ hyperdb.Password : 'VARCHAR(255)',
+ hyperdb.Boolean : 'BOOLEAN',
+ hyperdb.Number : 'REAL',
+ }
+ hyperdb_to_sql_value = {
+ hyperdb.String : str,
+ hyperdb.Date : lambda x: x.serialise(),
+ hyperdb.Link : int,
+ hyperdb.Interval : str,
+ hyperdb.Password : str,
+ hyperdb.Boolean : int,
+ hyperdb.Number : lambda x: x,
+ hyperdb.Multilink : lambda x: x, # used in journal marshalling
+ }
+ sql_to_hyperdb_value = {
+ hyperdb.String : str,
+ hyperdb.Date : lambda x: date.Date(str(x)),
+ hyperdb.Link : str, # XXX numeric ids
+ hyperdb.Interval : date.Interval,
+ hyperdb.Password : lambda x: password.Password(encrypted=x),
+ hyperdb.Boolean : int,
+ hyperdb.Number : rdbms_common._num_cvt,
+ hyperdb.Multilink : lambda x: x, # used in journal marshalling
+ }
+
+ def sqlite_busy_handler(self, data, table, count):
+ """invoked whenever SQLite tries to access a database that is locked"""
+ if count == 1:
+ # use a 30 second timeout (extraordinarily generous)
+ # for handling locked database
+ self._busy_handler_endtime = time.time() + 30
+ elif time.time() > self._busy_handler_endtime:
+ # timeout expired - no more retries
+ return 0
+ # sleep adaptively as retry count grows,
+ # starting from about half a second
+ time_to_sleep = 0.01 * (2 << min(5, count))
+ time.sleep(time_to_sleep)
+ return 1
+
+ def sql_open_connection(self):
+ '''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):
+ os.makedirs(self.config.DATABASE)
+
+ db = os.path.join(self.config.DATABASE, 'db')
+ logging.getLogger('hyperdb').info('open database %r'%db)
+ conn = sqlite.connect(db=db)
+ # set a 30 second timeout (extraordinarily generous) for handling
+ # locked database
+ conn.db.sqlite_busy_handler(self.sqlite_busy_handler)
+ cursor = conn.cursor()
+ return (conn, cursor)
+
+ def open_connection(self):
+ # ensure files are group readable and writable
+ os.umask(self.config.UMASK)
+
+ (self.conn, self.cursor) = self.sql_open_connection()
+
+ try:
+ self.load_dbschema()
+ except sqlite.DatabaseError, error:
+ if str(error) != 'no such table: schema':
+ raise
+ self.init_dbschema()
+ self.sql('create table schema (schema varchar)')
+ self.sql('create table ids (name varchar, num integer)')
+ self.sql('create index ids_name_idx on ids(name)')
+ self.create_version_2_tables()
+
+ def create_version_2_tables(self):
+ self.sql('create table otks (otk_key varchar, '
+ 'otk_value varchar, otk_time integer)')
+ self.sql('create index otks_key_idx on otks(otk_key)')
+ self.sql('create table sessions (session_key varchar, '
+ 'session_time integer, session_value varchar)')
+ self.sql('create index sessions_key_idx on '
+ 'sessions(session_key)')
+
+ # full-text indexing store
+ self.sql('CREATE TABLE __textids (_class varchar, '
+ '_itemid varchar, _prop varchar, _textid integer primary key) ')
+ self.sql('CREATE TABLE __words (_word varchar, '
+ '_textid integer)')
+ self.sql('CREATE INDEX words_word_ids ON __words(_word)')
+ sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
+ self.sql(sql, ('__textids', 1))
+
+ def add_new_columns_v2(self):
+ # update existing tables to have the new actor column
+ tables = self.database_schema['tables']
+ for classname, spec in self.classes.items():
+ if tables.has_key(classname):
+ dbspec = tables[classname]
+ self.update_class(spec, dbspec, force=1, adding_v2=1)
+ # we've updated - don't try again
+ tables[classname] = spec.schema()
+
+ def fix_version_3_tables(self):
+ # NOOP - no restriction on column length here
+ pass
+
+ def update_class(self, spec, old_spec, force=0, adding_v2=0):
+ ''' 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()
+ old_spec[1].sort()
+ if not force and new_spec == old_spec:
+ # no changes
+ return 0
+
+ logging.getLogger('hyperdb').info('update_class %s'%spec.classname)
+
+ # detect multilinks that have been removed, and drop their table
+ old_has = {}
+ for name, prop in old_spec[1]:
+ old_has[name] = 1
+ if new_has(name) or not isinstance(prop, hyperdb.Multilink):
+ continue
+ # it's a multilink, and it's been removed - drop the old
+ # table. First drop indexes.
+ self.drop_multilink_table_indexes(spec.classname, name)
+ sql = 'drop table %s_%s'%(spec.classname, prop)
+ self.sql(sql)
+ old_has = old_has.has_key
+
+ # now figure how we populate the new table
+ if adding_v2:
+ fetch = ['_activity', '_creation', '_creator']
+ else:
+ fetch = ['_actor', '_activity', '_creation', '_creator']
+ properties = spec.getprops()
+ for propname,x in new_spec[1]:
+ prop = properties[propname]
+ if isinstance(prop, hyperdb.Multilink):
+ if not old_has(propname):
+ # we need to create the new table
+ self.create_multilink_table(spec, propname)
+ elif force:
+ tn = '%s_%s'%(spec.classname, propname)
+ # grabe the current values
+ sql = 'select linkid, nodeid from %s'%tn
+ self.sql(sql)
+ rows = self.cursor.fetchall()
+
+ # drop the old table
+ self.drop_multilink_table_indexes(spec.classname, propname)
+ sql = 'drop table %s'%tn
+ self.sql(sql)
+
+ # 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)
+ for linkid, nodeid in rows:
+ self.sql(sql, (int(linkid), int(nodeid)))
+ elif old_has(propname):
+ # we copy this col over from the old table
+ fetch.append('_'+propname)
+
+ # select the data out of the old table
+ fetch.append('id')
+ fetch.append('__retired__')
+ fetchcols = ','.join(fetch)
+ cn = spec.classname
+ sql = 'select %s from _%s'%(fetchcols, cn)
+ self.sql(sql)
+ olddata = self.cursor.fetchall()
+
+ # TODO: update all the other index dropping code
+ self.drop_class_table_indexes(cn, old_spec[0])
+
+ # drop the old table
+ self.sql('drop table _%s'%cn)
+
+ # create the new table
+ self.create_class_table(spec)
+
+ if olddata:
+ inscols = ['id', '_actor', '_activity', '_creation', '_creator']
+ for propname,x in new_spec[1]:
+ prop = properties[propname]
+ if isinstance(prop, hyperdb.Multilink):
+ continue
+ elif isinstance(prop, hyperdb.Interval):
+ inscols.append('_'+propname)
+ inscols.append('__'+propname+'_int__')
+ elif old_has(propname):
+ # we copy this col over from the old table
+ inscols.append('_'+propname)
+
+ # do the insert of the old data - the new columns will have
+ # NULL values
+ args = ','.join([self.arg for x in inscols])
+ cols = ','.join(inscols)
+ sql = 'insert into _%s (%s) values (%s)'%(cn, cols, args)
+ for entry in olddata:
+ d = []
+ for name in inscols:
+ # generate the new value for the Interval int column
+ if name.endswith('_int__'):
+ name = name[2:-6]
+ if entry.has_key(name):
+ v = hyperdb.Interval(entry[name]).as_seconds()
+ else:
+ v = None
+ elif entry.has_key(name):
+ v = entry[name]
+ else:
+ v = None
+ d.append(v)
+ self.sql(sql, tuple(d))
+
+ return 1
+
+ def sql_close(self):
+ ''' Squash any error caused by us already having closed the
+ connection.
+ '''
+ try:
+ self.conn.close()
+ except sqlite.ProgrammingError, value:
+ if str(value) != 'close failed - Connection is closed.':
+ raise
+
+ def sql_rollback(self):
+ ''' 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:
+ if str(value) != 'rollback failed - Connection is closed.':
+ raise
+
+ def __repr__(self):
+ return '<roundlite 0x%x>'%id(self)
+
+ def sql_commit(self):
+ ''' Actually commit to the database.
+
+ Ignore errors if there's nothing to commit.
+ '''
+ try:
+ self.conn.commit()
+ except sqlite.DatabaseError, error:
+ if str(error) != 'cannot commit - no transaction is active':
+ raise
+ # open a new cursor for subsequent work
+ self.cursor = self.conn.cursor()
+
+ def sql_index_exists(self, table_name, index_name):
+ self.sql('pragma index_list(%s)'%table_name)
+ for entry in self.cursor.fetchall():
+ if entry[1] == index_name:
+ return 1
+ return 0
+
+ # old-skool id generation
+ def newid(self, classname):
+ ''' 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, ))
+ newid = int(self.cursor.fetchone()[0])
+
+ # update the counter
+ sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
+ vals = (int(newid)+1, classname)
+ self.sql(sql, vals)
+
+ # return as string
+ return str(newid)
+
+ def setid(self, classname, setid):
+ ''' 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)
+
+ def clear(self):
+ rdbms_common.Database.clear(self)
+ # set the id counters to 0 (setid adds one) so we start at 1
+ for cn in self.classes.keys():
+ self.setid(cn, 0)
+
+ def create_class(self, spec):
+ rdbms_common.Database.create_class(self, spec)
+ sql = 'insert into ids (name, num) values (%s, %s)'
+ vals = (spec.classname, 1)
+ self.sql(sql, vals)
+
+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
+ instead of nothing
+ '''
+ return filter(None, rdbms_common.Class.filter(self, search_matches,
+ filterspec, sort=sort, group=group))
+
+class Class(sqliteClass, rdbms_common.Class):
+ pass
+
+class IssueClass(sqliteClass, rdbms_common.IssueClass):
+ pass
+
+class FileClass(sqliteClass, rdbms_common.FileClass):
+ pass
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/backends/back_tsearch2.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/back_tsearch2.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,178 @@
+#$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
+
+import psycopg
+
+from roundup import hyperdb
+from roundup.support import ensureParentsExist
+from roundup.backends import back_postgresql, tsearch2_setup, indexer_rdbms
+from roundup.backends.back_postgresql import db_create, db_nuke, db_command
+from roundup.backends.back_postgresql import pg_command, db_exists, Class, IssueClass, FileClass
+from roundup.backends.indexer_common import _isLink, Indexer
+
+# XXX: Should probably be on the Class class.
+def _indexedProps(spec):
+ """Get a list of properties to be indexed on 'spec'."""
+ return [prop for prop, propclass in spec.getprops().items()
+ if isinstance(propclass, hyperdb.String) and propclass.indexme]
+
+def _getQueryDict(spec):
+ """Get a convenience dictionary for creating tsearch2 indexes."""
+ query_dict = {'classname': spec.classname,
+ 'indexedColumns': ['_' + prop for prop in _indexedProps(spec)]}
+ query_dict['tablename'] = "_%(classname)s" % query_dict
+ query_dict['triggername'] = "%(tablename)s_tsvectorupdate" % query_dict
+ return query_dict
+
+class Database(back_postgresql.Database):
+ def __init__(self, config, journaltag=None):
+ back_postgresql.Database.__init__(self, config, journaltag)
+ self.indexer = Indexer(self)
+
+ def create_version_2_tables(self):
+ back_postgresql.Database.create_version_2_tables(self)
+ tsearch2_setup.setup(self.cursor)
+
+ def create_class_table_indexes(self, spec):
+ back_postgresql.Database.create_class_table_indexes(self, spec)
+ self.cursor.execute("""CREATE INDEX _%(classname)s_idxFTI_idx
+ ON %(tablename)s USING gist(idxFTI);""" %
+ _getQueryDict(spec))
+
+ self.create_tsearch2_trigger(spec)
+
+ def create_tsearch2_trigger(self, spec):
+ d = _getQueryDict(spec)
+ if d['indexedColumns']:
+
+ d['joined'] = " || ' ' ||".join(d['indexedColumns'])
+ query = """UPDATE %(tablename)s
+ SET idxFTI = to_tsvector('default', %(joined)s)""" % d
+ self.cursor.execute(query)
+
+ d['joined'] = ", ".join(d['indexedColumns'])
+ query = """CREATE TRIGGER %(triggername)s
+ BEFORE UPDATE OR INSERT ON %(tablename)s
+ FOR EACH ROW EXECUTE PROCEDURE
+ tsearch2(idxFTI, %(joined)s);""" % d
+ self.cursor.execute(query)
+
+ def drop_tsearch2_trigger(self, spec):
+ # Check whether the trigger exists before trying to drop it.
+ query_dict = _getQueryDict(spec)
+ self.sql("""SELECT tgname FROM pg_catalog.pg_trigger
+ WHERE tgname = '%(triggername)s'""" % query_dict)
+ if self.cursor.fetchall():
+ self.sql("""DROP TRIGGER %(triggername)s ON %(tablename)s""" %
+ query_dict)
+
+ def update_class(self, spec, old_spec, force=0):
+ result = back_postgresql.Database.update_class(self, spec, old_spec, force)
+
+ # Drop trigger...
+ self.drop_tsearch2_trigger(spec)
+
+ # and recreate if necessary.
+ self.create_tsearch2_trigger(spec)
+
+ return result
+
+ def determine_all_columns(self, spec):
+ cols, mls = back_postgresql.Database.determine_all_columns(self, spec)
+ cols.append(('idxFTI', 'tsvector'))
+ return cols, mls
+
+class Indexer(Indexer):
+ def __init__(self, db):
+ self.db = db
+
+ # This indexer never needs to reindex.
+ def should_reindex(self):
+ return 0
+
+ def getHits(self, search_terms, klass):
+ return self.find(search_terms, klass)
+
+ def find(self, search_terms, klass):
+ if not search_terms:
+ return None
+
+ hits = self.tsearchQuery(klass.classname, search_terms)
+ designator_propname = {}
+
+ for nm, propclass in klass.getprops().items():
+ if _isLink(propclass):
+ hits.extend(self.tsearchQuery(propclass.classname, search_terms))
+
+ return hits
+
+ def tsearchQuery(self, classname, search_terms):
+ query = """SELECT id FROM _%(classname)s
+ WHERE idxFTI @@ to_tsquery('default', '%(terms)s')"""
+
+ query = query % {'classname': classname,
+ 'terms': ' & '.join(search_terms)}
+ self.db.cursor.execute(query)
+ klass = self.db.getclass(classname)
+ nodeids = [str(row[0]) for row in self.db.cursor.fetchall()]
+
+ # filter out files without text/plain mime type
+ # XXX: files without text/plain shouldn't be indexed at all, we
+ # should take care of this in the trigger
+ if klass.getprops().has_key('type'):
+ nodeids = [nodeid for nodeid in nodeids
+ if klass.get(nodeid, 'type') == 'text/plain']
+
+ # XXX: We haven't implemented property-level search, so I'm just faking
+ # it here with a property named 'XXX'. We still need to fix the other
+ # backends and indexer_common.Indexer.search to only want to unpack two
+ # values.
+ return [(classname, nodeid, 'XXX') for nodeid in nodeids]
+
+ # These only exist to satisfy the interface that's expected from indexers.
+ def force_reindex(self):
+ pass
+
+ def add_text(self, identifier, text, mime_type=None):
+ pass
+
+ def close(self):
+ pass
+
+class FileClass(hyperdb.FileClass, Class):
+ '''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.
+
+ However, this implementation just stores it in the hyperdb.
+ '''
+ def __init__(self, db, classname, **properties):
+ '''The newly-created class automatically includes the "content" property.,
+ '''
+ properties['content'] = hyperdb.String(indexme='yes')
+ Class.__init__(self, db, classname, **properties)
+
+ default_mime_type = 'text/plain'
+ def create(self, **propvalues):
+ # figure the mime type
+ if self.getprops().has_key('type') and not propvalues.get('type'):
+ propvalues['type'] = self.default_mime_type
+ return Class.create(self, **propvalues)
+
+ def export_files(self, dirname, nodeid):
+ dest = self.exportFilename(dirname, nodeid)
+ ensureParentsExist(dest)
+ fp = open(dest, "w")
+ fp.write(self.get(nodeid, "content", default=''))
+ fp.close()
+
+ def import_files(self, dirname, nodeid):
+ source = self.exportFilename(dirname, nodeid)
+
+ fp = open(source, "r")
+ # Use Database.setnode instead of self.set or self.set_inner here, as
+ # Database.setnode doesn't update the "activity" or "actor" properties.
+ self.db.setnode(self.classname, nodeid, values={'content': fp.read()})
+ fp.close()
Added: tracker/vendor/roundup/current/roundup/backends/blobfiles.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/blobfiles.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,158 @@
+#
+# 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: blobfiles.py,v 1.19 2005/06/08 03:35:18 anthonybaxter Exp $
+'''This module exports file storage for roundup backends.
+Files are stored into a directory hierarchy.
+'''
+__docformat__ = 'restructuredtext'
+
+import os
+
+def files_in_dir(dir):
+ if not os.path.exists(dir):
+ return 0
+ num_files = 0
+ for dir_entry in os.listdir(dir):
+ full_filename = os.path.join(dir,dir_entry)
+ if os.path.isfile(full_filename):
+ num_files = num_files + 1
+ elif os.path.isdir(full_filename):
+ num_files = num_files + files_in_dir(full_filename)
+ return num_files
+
+class FileStorage:
+ """Store files in some directory structure"""
+ def subdirFilename(self, classname, nodeid, property=None):
+ """Determine what the filename and subdir for nodeid + classname is."""
+ if property:
+ name = '%s%s.%s'%(classname, nodeid, property)
+ else:
+ # roundupdb.FileClass never specified the property name, so don't
+ # include it
+ name = '%s%s'%(classname, nodeid)
+
+ # have a separate subdir for every thousand messages
+ subdir = str(int(nodeid) / 1000)
+ return os.path.join(subdir, name)
+
+ def filename(self, classname, nodeid, property=None, create=0):
+ '''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 create or os.path.exists(filename):
+ return filename
+
+ # try .tmp
+ filename = filename + '.tmp'
+ if os.path.exists(filename):
+ return filename
+
+ # ok, try flat (very old-style)
+ if property:
+ filename = os.path.join(self.dir, 'files', '%s%s.%s'%(classname,
+ nodeid, property))
+ else:
+ filename = os.path.join(self.dir, 'files', '%s%s'%(classname,
+ nodeid))
+ if os.path.exists(filename):
+ return filename
+
+ # file just ain't there
+ 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
+ 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)
+
+ # make sure the file storage dir exists
+ if not os.path.exists(os.path.dirname(name)):
+ os.makedirs(os.path.dirname(name))
+
+ # save to a temp file
+ name = name + '.tmp'
+
+ # make sure we don't register the rename action more than once
+ if not os.path.exists(name):
+ # save off the rename action
+ self.transactions.append((self.doStoreFile, (classname, nodeid,
+ property)))
+ open(name, 'wb').write(content)
+
+ def getfile(self, classname, nodeid, property):
+ '''Get the content of the file in the database.
+ '''
+ filename = self.filename(classname, nodeid, property)
+
+ f = open(filename, 'rb')
+ try:
+ # snarf the contents and make sure we close the file
+ return f.read()
+ finally:
+ f.close()
+
+ def numfiles(self):
+ '''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.
+ '''
+ # determine the name of the file to write to
+ name = self.filename(classname, nodeid, property)
+
+ # the file is currently ".tmp" - move it to its real name to commit
+ if name.endswith('.tmp'):
+ # creation
+ dstname = os.path.splitext(name)[0]
+ else:
+ # edit operation
+ dstname = name
+ name = name + '.tmp'
+
+ # content is being updated (and some platforms, eg. win32, won't
+ # let us rename over the top of the old file)
+ if os.path.exists(dstname):
+ os.remove(dstname)
+
+ os.rename(name, dstname)
+
+ # return the classname, nodeid so we reindex this content
+ return (classname, nodeid)
+
+ def rollbackStoreFile(self, classname, nodeid, property, **databases):
+ '''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('.tmp'):
+ name += '.tmp'
+ os.remove(name)
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/roundup/backends/indexer_common.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/indexer_common.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,92 @@
+#$Id: indexer_common.py,v 1.6 2006/04/27 05:48:26 richard Exp $
+import re, sets
+
+from roundup import hyperdb
+
+STOPWORDS = [
+ "A", "AND", "ARE", "AS", "AT", "BE", "BUT", "BY",
+ "FOR", "IF", "IN", "INTO", "IS", "IT",
+ "NO", "NOT", "OF", "ON", "OR", "SUCH",
+ "THAT", "THE", "THEIR", "THEN", "THERE", "THESE",
+ "THEY", "THIS", "TO", "WAS", "WILL", "WITH"
+]
+
+def _isLink(propclass):
+ return (isinstance(propclass, hyperdb.Link) or
+ isinstance(propclass, hyperdb.Multilink))
+
+class Indexer:
+ def __init__(self, db):
+ self.stopwords = sets.Set(STOPWORDS)
+ for word in db.config[('main', 'indexer_stopwords')]:
+ self.stopwords.add(word)
+
+ def is_stopword(self, word):
+ return word in self.stopwords
+
+ 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
+ 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:
+ return {}
+
+ designator_propname = {}
+ for nm, propclass in klass.getprops().items():
+ if _isLink(propclass):
+ designator_propname[propclass.classname] = nm
+
+ # build a dictionary of nodes and their associated messages
+ # and files
+ nodeids = {} # this is the answer
+ propspec = {} # used to do the klass.find
+ for propname in designator_propname.values():
+ propspec[propname] = {} # used as a set (value doesn't matter)
+ for classname, nodeid, property in hits:
+ # skip this result if we don't care about this class/property
+ if ignore.has_key((classname, property)):
+ continue
+
+ # if it's a property on klass, it's easy
+ if classname == klass.classname:
+ if not nodeids.has_key(nodeid):
+ nodeids[nodeid] = {}
+ continue
+
+ # make sure the class is a linked one, otherwise ignore
+ if not designator_propname.has_key(classname):
+ continue
+
+ # it's a linked class - set up to do the klass.find
+ linkprop = designator_propname[classname] # eg, msg -> messages
+ propspec[linkprop][nodeid] = 1
+
+ # retain only the meaningful entries
+ for propname, idset in propspec.items():
+ if not idset:
+ del propspec[propname]
+
+ # klass.find tells me the klass nodeids the linked nodes relate to
+ for resid in klass.find(**propspec):
+ resid = str(resid)
+ if not nodeids.has_key(id):
+ 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):
+ if propspec[linkprop].has_key(nodeid):
+ # OK, this node[propname] has a winner
+ if not node_dict.has_key(linkprop):
+ node_dict[linkprop] = [nodeid]
+ else:
+ node_dict[linkprop].append(nodeid)
+ return nodeids
+
Added: tracker/vendor/roundup/current/roundup/backends/indexer_dbm.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/indexer_dbm.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,292 @@
+#
+# This module is derived from the module described at:
+# http://gnosis.cx/publish/programming/charming_python_15.txt
+#
+# Author: David Mertz (mertz at gnosis.cx)
+# Thanks to: Pat Knight (p.knight at ktgroup.co.uk)
+# Gregory Popovitch (greg at gpy.com)
+#
+# The original module was released under this license, and remains under
+# it:
+#
+# This file is released to the public domain. I (dqm) would
+# appreciate it if you choose to keep derived works under terms
+# 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 $
+'''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.
+'''
+__docformat__ = 'restructuredtext'
+
+import os, shutil, re, mimetypes, marshal, zlib, errno
+from roundup.hyperdb import Link, Multilink
+from roundup.backends.indexer_common import Indexer as IndexerBase
+
+class Indexer(IndexerBase):
+ '''Indexes information from roundup's hyperdb to allow efficient
+ searching.
+
+ Three structures are created by the indexer::
+
+ files {identifier: (fileid, wordcount)}
+ words {word: {fileid: count}}
+ fileids {fileid: identifier}
+
+ where identifier is (classname, nodeid, propertyname)
+ '''
+ def __init__(self, db):
+ IndexerBase.__init__(self, db)
+ self.indexdb_path = os.path.join(db.config.DATABASE, 'indexes')
+ self.indexdb = os.path.join(self.indexdb_path, 'index.db')
+ self.reindex = 0
+ self.quiet = 9
+ self.changed = 0
+
+ # see if we need to reindex because of a change in code
+ version = os.path.join(self.indexdb_path, 'version')
+ if (not os.path.exists(self.indexdb_path) or
+ not os.path.exists(version)):
+ # for now the file itself is a flag
+ self.force_reindex()
+ elif os.path.exists(version):
+ version = open(version).read()
+ # check the value and reindex if it's not the latest
+ if version.strip() != '1':
+ self.force_reindex()
+
+ def force_reindex(self):
+ '''Force a reindex condition
+ '''
+ if os.path.exists(self.indexdb_path):
+ shutil.rmtree(self.indexdb_path)
+ os.makedirs(self.indexdb_path)
+ os.chmod(self.indexdb_path, 0775)
+ open(os.path.join(self.indexdb_path, 'version'), 'w').write('1\n')
+ self.reindex = 1
+ self.changed = 1
+
+ def should_reindex(self):
+ '''Should we reindex?
+ '''
+ return self.reindex
+
+ def add_text(self, identifier, text, mime_type='text/plain'):
+ '''Add some text associated with the (classname, nodeid, property)
+ identifier.
+ '''
+ # make sure the index is loaded
+ self.load_index()
+
+ # remove old entries for this identifier
+ if self.files.has_key(identifier):
+ self.purge_entry(identifier)
+
+ # split into words
+ words = self.splitter(text, mime_type)
+
+ # Find new file index, and assign it to identifier
+ # (_TOP uses trick of negative to avoid conflict with file index)
+ self.files['_TOP'] = (self.files['_TOP'][0]-1, None)
+ file_index = abs(self.files['_TOP'][0])
+ self.files[identifier] = (file_index, len(words))
+ self.fileids[file_index] = identifier
+
+ # find the unique words
+ filedict = {}
+ for word in words:
+ if self.is_stopword(word):
+ continue
+ if filedict.has_key(word):
+ filedict[word] = filedict[word]+1
+ else:
+ filedict[word] = 1
+
+ # now add to the totals
+ for word in filedict.keys():
+ # each word has a dict of {identifier: count}
+ if self.words.has_key(word):
+ entry = self.words[word]
+ else:
+ # new word
+ entry = {}
+ self.words[word] = entry
+
+ # make a reference to the file for this word
+ entry[file_index] = filedict[word]
+
+ # save needed
+ self.changed = 1
+
+ def splitter(self, text, ftype):
+ '''Split the contents of a text string into a list of 'words'
+ '''
+ if ftype == 'text/plain':
+ words = self.text_splitter(text)
+ else:
+ return []
+ return words
+
+ def text_splitter(self, text):
+ """Split text/plain string into a list of words
+ """
+ # case insensitive
+ text = str(text).upper()
+
+ # Split the raw text, losing anything longer than 25 characters
+ # since that'll be gibberish (encoded text or somesuch) or shorter
+ # than 3 characters since those short words appear all over the
+ # place
+ return re.findall(r'\b\w{2,25}\b', text)
+
+ # we override this to ignore not 2 < word < 25 and also to fix a bug -
+ # the (fail) case.
+ def find(self, wordlist):
+ '''Locate files that match ALL the words in wordlist
+ '''
+ if not hasattr(self, 'words'):
+ self.load_index()
+ self.load_index(wordlist=wordlist)
+ entries = {}
+ hits = None
+ for word in wordlist:
+ if not 2 < len(word) < 25:
+ # word outside the bounds of what we index - ignore
+ continue
+ word = word.upper()
+ entry = self.words.get(word) # For each word, get index
+ entries[word] = entry # of matching files
+ if not entry: # Nothing for this one word (fail)
+ return {}
+ if hits is None:
+ hits = {}
+ for k in entry.keys():
+ if not self.fileids.has_key(k):
+ raise ValueError, 'Index is corrupted: re-generate it'
+ hits[k] = self.fileids[k]
+ else:
+ # Eliminate hits for every non-match
+ for fileid in hits.keys():
+ if not entry.has_key(fileid):
+ del hits[fileid]
+ if hits is None:
+ return {}
+ return hits.values()
+
+ segments = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ#_-!"
+ def load_index(self, reload=0, wordlist=None):
+ # Unless reload is indicated, do not load twice
+ if self.index_loaded() and not reload:
+ return 0
+
+ # Ok, now let's actually load it
+ db = {'WORDS': {}, 'FILES': {'_TOP':(0,None)}, 'FILEIDS': {}}
+
+ # Identify the relevant word-dictionary segments
+ if not wordlist:
+ segments = self.segments
+ else:
+ segments = ['-','#']
+ for word in wordlist:
+ segments.append(word[0].upper())
+
+ # Load the segments
+ for segment in segments:
+ try:
+ f = open(self.indexdb + segment, 'rb')
+ except IOError, error:
+ # probably just nonexistent segment index file
+ if error.errno != errno.ENOENT: raise
+ else:
+ pickle_str = zlib.decompress(f.read())
+ f.close()
+ dbslice = marshal.loads(pickle_str)
+ if dbslice.get('WORDS'):
+ # if it has some words, add them
+ for word, entry in dbslice['WORDS'].items():
+ db['WORDS'][word] = entry
+ if dbslice.get('FILES'):
+ # if it has some files, add them
+ db['FILES'] = dbslice['FILES']
+ if dbslice.get('FILEIDS'):
+ # if it has fileids, add them
+ db['FILEIDS'] = dbslice['FILEIDS']
+
+ self.words = db['WORDS']
+ self.files = db['FILES']
+ self.fileids = db['FILEIDS']
+ self.changed = 0
+
+ def save_index(self):
+ # only save if the index is loaded and changed
+ if not self.index_loaded() or not self.changed:
+ return
+
+ # brutal space saver... delete all the small segments
+ for segment in self.segments:
+ try:
+ os.remove(self.indexdb + segment)
+ except OSError, error:
+ # probably just nonexistent segment index file
+ if error.errno != errno.ENOENT: raise
+
+ # First write the much simpler filename/fileid dictionaries
+ dbfil = {'WORDS':None, 'FILES':self.files, 'FILEIDS':self.fileids}
+ open(self.indexdb+'-','wb').write(zlib.compress(marshal.dumps(dbfil)))
+
+ # The hard part is splitting the word dictionary up, of course
+ letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ#_"
+ segdicts = {} # Need batch of empty dicts
+ for segment in letters:
+ segdicts[segment] = {}
+ for word, entry in self.words.items(): # Split into segment dicts
+ initchar = word[0].upper()
+ segdicts[initchar][word] = entry
+
+ # save
+ for initchar in letters:
+ db = {'WORDS':segdicts[initchar], 'FILES':None, 'FILEIDS':None}
+ pickle_str = marshal.dumps(db)
+ filename = self.indexdb + initchar
+ pickle_fh = open(filename, 'wb')
+ pickle_fh.write(zlib.compress(pickle_str))
+ os.chmod(filename, 0664)
+
+ # save done
+ self.changed = 0
+
+ def purge_entry(self, identifier):
+ '''Remove a file from file index and word index
+ '''
+ self.load_index()
+
+ if not self.files.has_key(identifier):
+ return
+
+ file_index = self.files[identifier][0]
+ del self.files[identifier]
+ del self.fileids[file_index]
+
+ # The much harder part, cleanup the word index
+ for key, occurs in self.words.items():
+ if occurs.has_key(file_index):
+ del occurs[file_index]
+
+ # save needed
+ self.changed = 1
+
+ def index_loaded(self):
+ return (hasattr(self,'fileids') and hasattr(self,'files') and
+ hasattr(self,'words'))
+
+ def rollback(self):
+ ''' load last saved index info. '''
+ self.load_index(reload=1)
+
+ def close(self):
+ pass
+
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/roundup/backends/indexer_rdbms.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/indexer_rdbms.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,139 @@
+#$Id: indexer_rdbms.py,v 1.12 2006/02/06 21:00:47 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
+
+from roundup.backends.indexer_common import Indexer as IndexerBase
+
+class Indexer(IndexerBase):
+ def __init__(self, db):
+ IndexerBase.__init__(self, db)
+ self.db = db
+ self.reindex = 0
+
+ def close(self):
+ '''close the indexing database'''
+ # just nuke the circular reference
+ self.db = None
+
+ def save_index(self):
+ '''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
+ empties the tables ids and index and sets a flag so
+ that the databases are reindexed'''
+ self.reindex = 1
+
+ def should_reindex(self):
+ '''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) '''
+ if mime_type != 'text/plain':
+ return
+
+ # first, find the id of the (classname, itemid, property)
+ a = self.db.arg
+ sql = 'select _textid from __textids where _class=%s and '\
+ '_itemid=%s and _prop=%s'%(a, a, a)
+ self.db.cursor.execute(sql, identifier)
+ r = self.db.cursor.fetchone()
+ if not r:
+ id = self.db.newid('__textids')
+ sql = 'insert into __textids (_textid, _class, _itemid, _prop)'\
+ ' values (%s, %s, %s, %s)'%(a, a, a, a)
+ self.db.cursor.execute(sql, (id, ) + identifier)
+ self.db.cursor.execute('select max(_textid) from __textids')
+ id = self.db.cursor.fetchone()[0]
+ else:
+ id = int(r[0])
+ # clear out any existing indexed values
+ sql = 'delete from __words where _textid=%s'%a
+ self.db.cursor.execute(sql, (id, ))
+
+ # ok, find all the 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 = {}
+ for word in wordlist:
+ if self.is_stopword(word): continue
+ if len(word) > 25: continue
+ words[word] = 1
+ words = words.keys()
+
+ # for each word, add an entry in the db
+ for word in words:
+ # don't dupe
+ sql = 'select * from __words where _word=%s and _textid=%s'%(a, a)
+ self.db.cursor.execute(sql, (word, id))
+ if self.db.cursor.fetchall():
+ continue
+ sql = 'insert into __words (_word, _textid) values (%s, %s)'%(a, a)
+ self.db.cursor.execute(sql, (word, id))
+
+ def find(self, wordlist):
+ '''look up all the words in the wordlist.
+ If none are found return an empty dictionary
+ * more rules here
+ '''
+ if not wordlist:
+ return {}
+
+ l = [word.upper() for word in wordlist if 26 > len(word) > 2]
+
+ if not l:
+ return {}
+
+ if self.db.implements_intersect:
+ # simple AND search
+ sql = 'select distinct(_textid) from __words where _word=%s'%self.db.arg
+ sql = '\nINTERSECT\n'.join([sql]*len(l))
+ self.db.cursor.execute(sql, tuple(l))
+ r = self.db.cursor.fetchall()
+ if not r:
+ return {}
+ a = ','.join([self.db.arg] * len(r))
+ sql = 'select _class, _itemid, _prop from __textids '\
+ 'where _textid in (%s)'%a
+ self.db.cursor.execute(sql, tuple([int(id) for (id,) in r]))
+
+ else:
+ # A more complex version for MySQL since it doesn't implement INTERSECT
+
+ # Construct SQL statement to join __words table to itself
+ # multiple times.
+ sql = """select distinct(__words1._textid)
+ from __words as __words1 %s
+ where __words1._word=%s %s"""
+
+ join_tmpl = ' left join __words as __words%d using (_textid) \n'
+ match_tmpl = ' and __words%d._word=%s \n'
+
+ join_list = []
+ match_list = []
+ for n in xrange(len(l) - 1):
+ join_list.append(join_tmpl % (n + 2))
+ match_list.append(match_tmpl % (n + 2, self.db.arg))
+
+ sql = sql%(' '.join(join_list), self.db.arg, ' '.join(match_list))
+ self.db.cursor.execute(sql, l)
+
+ r = map(lambda x: x[0], self.db.cursor.fetchall())
+ if not r:
+ return {}
+
+ a = ','.join([self.db.arg] * len(r))
+ sql = 'select _class, _itemid, _prop from __textids '\
+ 'where _textid in (%s)'%a
+
+ self.db.cursor.execute(sql, tuple(map(int, r)))
+
+ return self.db.cursor.fetchall()
+
Added: tracker/vendor/roundup/current/roundup/backends/indexer_xapian.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/indexer_xapian.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,124 @@
+#$Id: indexer_xapian.py,v 1.4 2006/02/10 00:16:13 richard Exp $
+''' This implements the full-text indexer using the Xapian indexer.
+'''
+import re, os
+
+import xapian
+
+from roundup.backends.indexer_common import Indexer as IndexerBase
+
+# TODO: we need to delete documents when a property is *reindexed*
+
+class Indexer(IndexerBase):
+ def __init__(self, db):
+ IndexerBase.__init__(self, db)
+ self.db_path = db.config.DATABASE
+ self.reindex = 0
+ self.transaction_active = False
+
+ def _get_database(self):
+ index = os.path.join(self.db_path, 'text-index')
+ return xapian.WritableDatabase(index, xapian.DB_CREATE_OR_OPEN)
+
+ def save_index(self):
+ '''Save the changes to the index.'''
+ if not self.transaction_active:
+ return
+ # XXX: Xapian databases don't actually implement transactions yet
+ database = self._get_database()
+ database.commit_transaction()
+ self.transaction_active = False
+
+ def close(self):
+ '''close the indexing database'''
+ pass
+
+ def rollback(self):
+ if not self.transaction_active:
+ return
+ # XXX: Xapian databases don't actually implement transactions yet
+ database = self._get_database()
+ database.cancel_transaction()
+ self.transaction_active = False
+
+ def force_reindex(self):
+ '''Force a reindexing of the database. This essentially
+ empties the tables ids and index and sets a flag so
+ that the databases are reindexed'''
+ self.reindex = 1
+
+ def should_reindex(self):
+ '''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) '''
+ if mime_type != 'text/plain':
+ return
+ if not text: text = ''
+
+ # open the database and start a transaction if needed
+ database = self._get_database()
+ # XXX: Xapian databases don't actually implement transactions yet
+ #if not self.transaction_active:
+ #database.begin_transaction()
+ #self.transaction_active = True
+
+ # TODO: allow configuration of other languages
+ stemmer = xapian.Stem("english")
+
+ # We use the identifier twice: once in the actual "text" being
+ # indexed so we can search on it, and again as the "data" being
+ # indexed so we know what we're matching when we get results
+ identifier = '%s:%s:%s'%identifier
+
+ # see if the id is in the database
+ enquire = xapian.Enquire(database)
+ query = xapian.Query(xapian.Query.OP_AND, [identifier])
+ enquire.set_query(query)
+ matches = enquire.get_mset(0, 10)
+ if matches.size(): # would it killya to implement __len__()??
+ b = matches.begin()
+ docid = b.get_docid()
+ else:
+ docid = None
+
+ # create the new document
+ doc = xapian.Document()
+ doc.set_data(identifier)
+ doc.add_posting(identifier, 0)
+
+ for match in re.finditer(r'\b\w{2,25}\b', text.upper()):
+ word = match.group(0)
+ if self.is_stopword(word):
+ continue
+ term = stemmer.stem_word(word)
+ doc.add_posting(term, match.start(0))
+ if docid:
+ database.replace_document(docid, doc)
+ else:
+ database.add_document(doc)
+
+ def find(self, wordlist):
+ '''look up all the words in the wordlist.
+ If none are found return an empty dictionary
+ * more rules here
+ '''
+ if not wordlist:
+ return {}
+
+ database = self._get_database()
+
+ enquire = xapian.Enquire(database)
+ stemmer = xapian.Stem("english")
+ terms = []
+ for term in [word.upper() for word in wordlist if 26 > len(word) > 2]:
+ terms.append(stemmer.stem_word(term.upper()))
+ query = xapian.Query(xapian.Query.OP_AND, terms)
+
+ enquire.set_query(query)
+ matches = enquire.get_mset(0, 10)
+
+ return [tuple(m[xapian.MSET_DOCUMENT].get_data().split(':'))
+ for m in matches]
+
Added: tracker/vendor/roundup/current/roundup/backends/locking.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/locking.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,47 @@
+#! /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
+# 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: 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.
+
+It should work on Unix and Windows.
+'''
+__docformat__ = 'restructuredtext'
+
+import portalocker
+
+def acquire_lock(path, block=1):
+ '''Acquire a lock for the given path
+ '''
+ import portalocker
+ file = open(path, 'w')
+ if block:
+ portalocker.lock(file, portalocker.LOCK_EX)
+ else:
+ portalocker.lock(file, portalocker.LOCK_EX|portalocker.LOCK_NB)
+ return file
+
+def release_lock(file):
+ '''Release our lock on the given path
+ '''
+ portalocker.unlock(file)
Added: tracker/vendor/roundup/current/roundup/backends/portalocker.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/portalocker.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,145 @@
+# portalocker.py - Cross-platform (posix/nt) API for flock-style file locking.
+# Requires python 1.5.2 or better.
+
+# ID line added by richard for Roundup file tracking
+# $Id: portalocker.py,v 1.8 2004/02/11 23:55:09 richard Exp $
+
+"""Cross-platform (posix/nt) API for flock-style file locking.
+
+Synopsis::
+
+ import portalocker
+ file = open("somefile", "r+")
+ portalocker.lock(file, portalocker.LOCK_EX)
+ file.seek(12)
+ file.write("foo")
+ file.close()
+
+If you know what you're doing, you may choose to::
+
+ portalocker.unlock(file)
+
+before closing the file, but why?
+
+Methods::
+
+ lock( file, flags )
+ unlock( file )
+
+Constants::
+
+ LOCK_EX
+ LOCK_SH
+ LOCK_NB
+
+I learned the win32 technique for locking files from sample code
+provided by John Nielsen <nielsenjf at my-deja.com> in the documentation
+that accompanies the win32 modules.
+
+:Author: Jonathan Feinberg <jdf at pobox.com>
+:Version: Id: portalocker.py,v 1.3 2001/05/29 18:47:55 Administrator Exp
+ **un-cvsified by richard so the version doesn't change**
+"""
+__docformat__ = 'restructuredtext'
+
+import os
+
+if os.name == 'nt':
+ import win32con
+ import win32file
+ import pywintypes
+ LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK
+ LOCK_SH = 0 # the default
+ LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY
+ # is there any reason not to reuse the following structure?
+ __overlapped = pywintypes.OVERLAPPED()
+elif os.name == 'posix':
+ import fcntl
+ LOCK_EX = fcntl.LOCK_EX
+ LOCK_SH = fcntl.LOCK_SH
+ LOCK_NB = fcntl.LOCK_NB
+else:
+ raise RuntimeError("PortaLocker only defined for nt and posix platforms")
+
+if os.name == 'nt':
+ # eugh, we want 0xffff0000 here, but python 2.3 won't let us :(
+ FFFF0000 = -65536
+ def lock(file, flags):
+ hfile = win32file._get_osfhandle(file.fileno())
+ # LockFileEx is not supported on all Win32 platforms (Win95, Win98, WinME).
+ # If it's not supported, win32file will raise an exception.
+ # Try LockFileEx first, as it has more functionality and handles
+ # blocking locks more efficiently.
+ try:
+ win32file.LockFileEx(hfile, flags, 0, FFFF0000, __overlapped)
+ except win32file.error, e:
+ import winerror
+ # Propagate upwards all exceptions other than not-implemented.
+ if e[0] != winerror.ERROR_CALL_NOT_IMPLEMENTED:
+ raise e
+
+ # LockFileEx is not supported. Use LockFile.
+ # LockFile does not support shared locking -- always exclusive.
+ # Care: the low/high length params are reversed compared to LockFileEx.
+ if not flags & LOCK_EX:
+ import warnings
+ warnings.warn("PortaLocker does not support shared locking on Win9x", RuntimeWarning)
+ # LockFile only supports immediate-fail locking.
+ if flags & LOCK_NB:
+ win32file.LockFile(hfile, 0, 0, FFFF0000, 0)
+ else:
+ # Emulate a blocking lock with a polling loop.
+ import time
+ while 1:
+ # Attempt a lock.
+ try:
+ win32file.LockFile(hfile, 0, 0, FFFF0000, 0)
+ break
+ except win32file.error, e:
+ # Propagate upwards all exceptions other than lock violation.
+ if e[0] != winerror.ERROR_LOCK_VIOLATION:
+ raise e
+ # Sleep and poll again.
+ time.sleep(0.1)
+ # TODO: should this return the result of the lock?
+
+ def unlock(file):
+ hfile = win32file._get_osfhandle(file.fileno())
+ # UnlockFileEx is not supported on all Win32 platforms (Win95, Win98, WinME).
+ # If it's not supported, win32file will raise an api_error exception.
+ try:
+ win32file.UnlockFileEx(hfile, 0, FFFF0000, __overlapped)
+ except win32file.error, e:
+ import winerror
+ # Propagate upwards all exceptions other than not-implemented.
+ if e[0] != winerror.ERROR_CALL_NOT_IMPLEMENTED:
+ raise e
+
+ # UnlockFileEx is not supported. Use UnlockFile.
+ # Care: the low/high length params are reversed compared to UnLockFileEx.
+ win32file.UnlockFile(hfile, 0, 0, FFFF0000, 0)
+
+elif os.name =='posix':
+ def lock(file, flags):
+ fcntl.flock(file.fileno(), flags)
+ # TODO: should this return the result of the lock?
+
+ def unlock(file):
+ fcntl.flock(file.fileno(), fcntl.LOCK_UN)
+
+if __name__ == '__main__':
+ from time import time, strftime, localtime
+ import sys
+ import portalocker
+
+ log = open('log.txt', "a+")
+ portalocker.lock(log, portalocker.LOCK_EX)
+
+ timestamp = strftime("%m/%d/%Y %H:%M:%S\n", localtime(time()))
+ log.write( timestamp )
+
+ print "Wrote lines. Hit enter to release lock."
+ dummy = sys.stdin.readline()
+
+ log.close()
+
Added: tracker/vendor/roundup/current/roundup/backends/rdbms_common.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/rdbms_common.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2649 @@
+#
+# 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: rdbms_common.py,v 1.170 2006/04/27 05:15:17 richard Exp $
+''' Relational database (SQL) backend common code.
+
+Basics:
+
+- map roundup classes to relational tables
+- automatically detect schema changes and modify the table schemas
+ appropriately (we store the "database version" of the schema in the
+ database itself as the only row of the "schema" table)
+- multilinks (which represent a many-to-many relationship) are handled through
+ intermediate tables
+- journals are stored adjunct to the per-class tables
+- table names and columns have "_" prepended so the names can't clash with
+ restricted names (like "order")
+- retirement is determined by the __retired__ column being true
+
+Database-specific changes may generally be pushed out to the overridable
+sql_* methods, since everything else should be fairly generic. There's
+probably a bit of work to be done if a database is used that actually
+honors column typing, since the initial databases don't (sqlite stores
+everything as a string.)
+
+The schema of the hyperdb being mapped to the database is stored in the
+database itself as a repr()'ed dictionary of information about each Class
+that maps to a table. If that information differs from the hyperdb schema,
+then we update it. We also store in the schema dict a version which
+allows us to upgrade the database schema when necessary. See upgrade_db().
+'''
+__docformat__ = 'restructuredtext'
+
+# standard python modules
+import sys, os, time, re, errno, weakref, copy, logging
+
+# roundup modules
+from roundup import hyperdb, date, password, roundupdb, security, support
+from roundup.hyperdb import String, Password, Date, Interval, Link, \
+ Multilink, DatabaseError, Boolean, Number, Node
+from roundup.backends import locking
+
+# support
+from blobfiles import FileStorage
+try:
+ from indexer_xapian import Indexer
+except ImportError:
+ from indexer_rdbms import Indexer
+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 = []
+
+def _num_cvt(num):
+ num = str(num)
+ try:
+ return int(num)
+ except:
+ return float(num)
+
+def _bool_cvt(value):
+ if value in ('TRUE', 'FALSE'):
+ return {'TRUE': 1, 'FALSE': 0}[value]
+ # assume it's a number returned from the db API
+ return int(value)
+
+def connection_dict(config, dbnamestr=None):
+ ''' Used by Postgresql and MySQL to detemine the keyword args for
+ opening the database connection.'''
+ d = { }
+ if dbnamestr:
+ d[dbnamestr] = config.RDBMS_NAME
+ for name in ('host', 'port', 'password', 'user', 'read_default_group',
+ 'read_default_file'):
+ cvar = 'RDBMS_'+name.upper()
+ if config[cvar] is not None:
+ d[name] = config[cvar]
+ return d
+
+class Database(FileStorage, hyperdb.Database, roundupdb.Database):
+ ''' Wrapper around an SQL database that presents a hyperdb interface.
+
+ - some functionality is specific to the actual SQL database, hence
+ the sql_* methods that are NotImplemented
+ - we keep a cache of the latest ROW_CACHE_SIZE row fetches.
+ '''
+ def __init__(self, config, journaltag=None):
+ ''' Open the database and load the schema from it.
+ '''
+ self.config, self.journaltag = config, journaltag
+ self.dir = config.DATABASE
+ self.classes = {}
+ self.indexer = Indexer(self)
+ self.security = security.Security(self)
+
+ # additional transaction support for external files and the like
+ self.transactions = []
+
+ # keep a cache of the N most recently retrieved rows of any kind
+ # (classname, nodeid) = row
+ self.cache = {}
+ self.cache_lru = []
+ self.stats = {'cache_hits': 0, 'cache_misses': 0, 'get_items': 0,
+ 'filtering': 0}
+
+ # database lock
+ self.lockfile = None
+
+ # open a connection to the database, creating the "conn" attribute
+ self.open_connection()
+
+ def clearCache(self):
+ self.cache = {}
+ self.cache_lru = []
+
+ def getSessionManager(self):
+ return Sessions(self)
+
+ def getOTKManager(self):
+ return OneTimeKeys(self)
+
+ def open_connection(self):
+ ''' Open a connection to the database, creating it if necessary.
+
+ Must call self.load_dbschema()
+ '''
+ raise NotImplemented
+
+ def sql(self, sql, args=None):
+ ''' Execute the sql with the optional args.
+ '''
+ if __debug__:
+ logging.getLogger('hyperdb').debug('SQL %r %r'%(sql, args))
+ if args:
+ self.cursor.execute(sql, args)
+ else:
+ self.cursor.execute(sql)
+
+ def sql_fetchone(self):
+ ''' Fetch a single row. If there's nothing to fetch, return None.
+ '''
+ return self.cursor.fetchone()
+
+ def sql_fetchall(self):
+ ''' Fetch all rows. If there's nothing to fetch, return [].
+ '''
+ return self.cursor.fetchall()
+
+ def sql_stringquote(self, value):
+ ''' Quote the string so it's safe to put in the 'sql quotes'
+ '''
+ return re.sub("'", "''", str(value))
+
+ def init_dbschema(self):
+ self.database_schema = {
+ 'version': self.current_db_version,
+ 'tables': {}
+ }
+
+ def load_dbschema(self):
+ ''' Load the schema definition that the database currently implements
+ '''
+ self.cursor.execute('select schema from schema')
+ schema = self.cursor.fetchone()
+ if schema:
+ self.database_schema = eval(schema[0])
+ else:
+ self.database_schema = {}
+
+ def save_dbschema(self):
+ ''' Save the schema definition that the database currently implements
+ '''
+ s = repr(self.database_schema)
+ self.sql('delete from schema')
+ self.sql('insert into schema values (%s)', (s,))
+
+ def post_init(self):
+ ''' Called once the schema initialisation has finished.
+
+ We should now confirm that the schema defined by our "classes"
+ attribute actually matches the schema in the database.
+ '''
+ save = 0
+
+ # handle changes in the schema
+ tables = self.database_schema['tables']
+ for classname, spec in self.classes.items():
+ if tables.has_key(classname):
+ dbspec = tables[classname]
+ if self.update_class(spec, dbspec):
+ tables[classname] = spec.schema()
+ save = 1
+ else:
+ self.create_class(spec)
+ tables[classname] = spec.schema()
+ save = 1
+
+ for classname, spec in tables.items():
+ if not self.classes.has_key(classname):
+ self.drop_class(classname, tables[classname])
+ del tables[classname]
+ save = 1
+
+ # now upgrade the database for column type changes, new internal
+ # tables, etc.
+ save = save | self.upgrade_db()
+
+ # update the database version of the schema
+ if save:
+ self.save_dbschema()
+
+ # reindex the db if necessary
+ if self.indexer.should_reindex():
+ self.reindex()
+
+ # commit
+ self.sql_commit()
+
+ # update this number when we need to make changes to the SQL structure
+ # of the backen database
+ current_db_version = 4
+ def upgrade_db(self):
+ ''' Update the SQL database to reflect changes in the backend code.
+
+ Return boolean whether we need to save the schema.
+ '''
+ version = self.database_schema.get('version', 1)
+ if version == self.current_db_version:
+ # nothing to do
+ return 0
+
+ if version < 2:
+ if __debug__:
+ logging.getLogger('hyperdb').info('upgrade to version 2')
+ # change the schema structure
+ self.database_schema = {'tables': self.database_schema}
+
+ # version 1 didn't have the actor column (note that in
+ # MySQL this will also transition the tables to typed columns)
+ self.add_new_columns_v2()
+
+ # version 1 doesn't have the OTK, session and indexing in the
+ # database
+ self.create_version_2_tables()
+
+ if version < 3:
+ if __debug__:
+ logging.getLogger('hyperdb').info('upgrade to version 3')
+ self.fix_version_2_tables()
+
+ if version < 4:
+ self.fix_version_3_tables()
+
+ self.database_schema['version'] = self.current_db_version
+ return 1
+
+ def fix_version_3_tables(self):
+ # drop the shorter VARCHAR OTK column and add a new TEXT one
+ for name in ('otk', 'session'):
+ self.sql('DELETE FROM %ss'%name)
+ self.sql('ALTER TABLE %ss DROP %s_value'%(name, name))
+ self.sql('ALTER TABLE %ss ADD %s_value TEXT'%(name, name))
+
+ def fix_version_2_tables(self):
+ '''Default (used by sqlite): NOOP'''
+ pass
+
+ def _convert_journal_tables(self):
+ '''Get current journal table contents, drop the table and re-create'''
+ c = self.cursor
+ cols = ','.join('nodeid date tag action params'.split())
+ for klass in self.classes.values():
+ # slurp and drop
+ sql = 'select %s from %s__journal order by date'%(cols,
+ klass.classname)
+ c.execute(sql)
+ contents = c.fetchall()
+ self.drop_journal_table_indexes(klass.classname)
+ c.execute('drop table %s__journal'%klass.classname)
+
+ # re-create and re-populate
+ self.create_journal_table(klass)
+ a = self.arg
+ sql = 'insert into %s__journal (%s) values (%s,%s,%s,%s,%s)'%(
+ klass.classname, cols, a, a, a, a, a)
+ for row in contents:
+ # no data conversion needed
+ self.cursor.execute(sql, row)
+
+ def _convert_string_properties(self):
+ '''Get current Class tables that contain String properties, and
+ convert the VARCHAR columns to TEXT'''
+ c = self.cursor
+ for klass in self.classes.values():
+ # slurp and drop
+ cols, mls = self.determine_columns(klass.properties.items())
+ scols = ','.join([i[0] for i in cols])
+ sql = 'select id,%s from _%s'%(scols, klass.classname)
+ c.execute(sql)
+ contents = c.fetchall()
+ self.drop_class_table_indexes(klass.classname, klass.getkey())
+ c.execute('drop table _%s'%klass.classname)
+
+ # re-create and re-populate
+ self.create_class_table(klass, create_sequence=0)
+ a = ','.join([self.arg for i in range(len(cols)+1)])
+ sql = 'insert into _%s (id,%s) values (%s)'%(klass.classname,
+ scols, a)
+ for row in contents:
+ l = []
+ for entry in row:
+ # mysql will already be a string - psql needs "help"
+ if entry is not None and not isinstance(entry, type('')):
+ entry = str(entry)
+ l.append(entry)
+ self.cursor.execute(sql, l)
+
+ def refresh_database(self):
+ self.post_init()
+
+
+ def reindex(self, classname=None, show_progress=False):
+ if classname:
+ classes = [self.getclass(classname)]
+ else:
+ classes = self.classes.values()
+ for klass in classes:
+ if show_progress:
+ for nodeid in support.Progress('Reindex %s'%klass.classname,
+ klass.list()):
+ klass.index(nodeid)
+ else:
+ for nodeid in klass.list():
+ klass.index(nodeid)
+ self.indexer.save_index()
+
+ hyperdb_to_sql_datatypes = {
+ hyperdb.String : 'TEXT',
+ hyperdb.Date : 'TIMESTAMP',
+ hyperdb.Link : 'INTEGER',
+ hyperdb.Interval : 'VARCHAR(255)',
+ hyperdb.Password : 'VARCHAR(255)',
+ hyperdb.Boolean : 'BOOLEAN',
+ hyperdb.Number : 'REAL',
+ }
+ def determine_columns(self, properties):
+ ''' Figure the column names and multilink properties from the spec
+
+ "properties" is a list of (name, prop) where prop may be an
+ instance of a hyperdb "type" _or_ a string repr of that type.
+ '''
+ cols = [
+ ('_actor', self.hyperdb_to_sql_datatypes[hyperdb.Link]),
+ ('_activity', self.hyperdb_to_sql_datatypes[hyperdb.Date]),
+ ('_creator', self.hyperdb_to_sql_datatypes[hyperdb.Link]),
+ ('_creation', self.hyperdb_to_sql_datatypes[hyperdb.Date]),
+ ]
+ mls = []
+ # add the multilinks separately
+ for col, prop in properties:
+ if isinstance(prop, Multilink):
+ mls.append(col)
+ continue
+
+ if isinstance(prop, type('')):
+ raise ValueError, "string property spec!"
+ #and prop.find('Multilink') != -1:
+ #mls.append(col)
+
+ datatype = self.hyperdb_to_sql_datatypes[prop.__class__]
+ cols.append(('_'+col, datatype))
+
+ # Intervals stored as two columns
+ if isinstance(prop, Interval):
+ cols.append(('__'+col+'_int__', 'BIGINT'))
+
+ cols.sort()
+ return cols, mls
+
+ def update_class(self, spec, old_spec, force=0):
+ ''' Determine the differences between the current spec and the
+ database version of the spec, and update where necessary.
+
+ If 'force' is true, update the database anyway.
+ '''
+ new_has = spec.properties.has_key
+ new_spec = spec.schema()
+ new_spec[1].sort()
+ old_spec[1].sort()
+ if not force and new_spec == old_spec:
+ # no changes
+ return 0
+
+ logger = logging.getLogger('hyperdb')
+ logger.info('update_class %s'%spec.classname)
+
+ logger.debug('old_spec %r'%(old_spec,))
+ logger.debug('new_spec %r'%(new_spec,))
+
+ # detect key prop change for potential index change
+ keyprop_changes = {}
+ if new_spec[0] != old_spec[0]:
+ if old_spec[0]:
+ keyprop_changes['remove'] = old_spec[0]
+ if new_spec[0]:
+ keyprop_changes['add'] = new_spec[0]
+
+ # detect multilinks that have been removed, and drop their table
+ old_has = {}
+ for name, prop in old_spec[1]:
+ old_has[name] = 1
+ if new_has(name):
+ continue
+
+ if prop.find('Multilink to') != -1:
+ # first drop indexes.
+ self.drop_multilink_table_indexes(spec.classname, name)
+
+ # now the multilink table itself
+ sql = 'drop table %s_%s'%(spec.classname, name)
+ else:
+ # if this is the key prop, drop the index first
+ if old_spec[0] == prop:
+ self.drop_class_table_key_index(spec.classname, name)
+ del keyprop_changes['remove']
+
+ # drop the column
+ sql = 'alter table _%s drop column _%s'%(spec.classname, name)
+
+ self.sql(sql)
+ old_has = old_has.has_key
+
+ # if we didn't remove the key prop just then, but the key prop has
+ # changed, we still need to remove the old index
+ if keyprop_changes.has_key('remove'):
+ self.drop_class_table_key_index(spec.classname,
+ keyprop_changes['remove'])
+
+ # add new columns
+ for propname, prop in new_spec[1]:
+ if old_has(propname):
+ continue
+ prop = spec.properties[propname]
+ if isinstance(prop, Multilink):
+ self.create_multilink_table(spec, propname)
+ else:
+ # add the column
+ coltype = self.hyperdb_to_sql_datatypes[prop.__class__]
+ sql = 'alter table _%s add column _%s %s'%(
+ spec.classname, propname, coltype)
+ self.sql(sql)
+
+ # extra Interval column
+ if isinstance(prop, Interval):
+ sql = 'alter table _%s add column __%s_int__ BIGINT'%(
+ spec.classname, propname)
+ self.sql(sql)
+
+ # if the new column is a key prop, we need an index!
+ if new_spec[0] == propname:
+ self.create_class_table_key_index(spec.classname, propname)
+ del keyprop_changes['add']
+
+ # if we didn't add the key prop just then, but the key prop has
+ # changed, we still need to add the new index
+ if keyprop_changes.has_key('add'):
+ self.create_class_table_key_index(spec.classname,
+ keyprop_changes['add'])
+
+ return 1
+
+ def determine_all_columns(self, spec):
+ """Figure out the columns from the spec and also add internal columns
+
+ """
+ cols, mls = self.determine_columns(spec.properties.items())
+
+ # add on our special columns
+ cols.append(('id', 'INTEGER PRIMARY KEY'))
+ cols.append(('__retired__', 'INTEGER DEFAULT 0'))
+ return cols, mls
+
+ def create_class_table(self, spec):
+ '''Create the class table for the given Class "spec". Creates the
+ indexes too.'''
+ cols, mls = self.determine_all_columns(spec)
+
+ # create the base table
+ scols = ','.join(['%s %s'%x for x in cols])
+ sql = 'create table _%s (%s)'%(spec.classname, scols)
+ self.sql(sql)
+
+ self.create_class_table_indexes(spec)
+
+ return cols, mls
+
+ def create_class_table_indexes(self, spec):
+ ''' create the class table for the given spec
+ '''
+ # create __retired__ index
+ index_sql2 = 'create index _%s_retired_idx on _%s(__retired__)'%(
+ spec.classname, spec.classname)
+ self.sql(index_sql2)
+
+ # create index for key property
+ if spec.key:
+ index_sql3 = 'create index _%s_%s_idx on _%s(_%s)'%(
+ spec.classname, spec.key,
+ spec.classname, spec.key)
+ self.sql(index_sql3)
+
+ # TODO: create indexes on (selected?) Link property columns, as
+ # they're more likely to be used for lookup
+
+ def drop_class_table_indexes(self, cn, key):
+ # drop the old table indexes first
+ l = ['_%s_id_idx'%cn, '_%s_retired_idx'%cn]
+ if key:
+ l.append('_%s_%s_idx'%(cn, key))
+
+ table_name = '_%s'%cn
+ for index_name in l:
+ if not self.sql_index_exists(table_name, index_name):
+ continue
+ index_sql = 'drop index '+index_name
+ self.sql(index_sql)
+
+ def create_class_table_key_index(self, cn, key):
+ ''' create the class table for the given spec
+ '''
+ sql = 'create index _%s_%s_idx on _%s(_%s)'%(cn, key, cn, key)
+ self.sql(sql)
+
+ def drop_class_table_key_index(self, cn, key):
+ table_name = '_%s'%cn
+ index_name = '_%s_%s_idx'%(cn, key)
+ if not self.sql_index_exists(table_name, index_name):
+ return
+ sql = 'drop index '+index_name
+ self.sql(sql)
+
+ def create_journal_table(self, spec):
+ ''' create the journal table for a class given the spec and
+ already-determined cols
+ '''
+ # journal table
+ cols = ','.join(['%s varchar'%x
+ for x in 'nodeid date tag action params'.split()])
+ sql = '''create table %s__journal (
+ nodeid integer, date %s, tag varchar(255),
+ action varchar(255), params text)''' % (spec.classname,
+ self.hyperdb_to_sql_datatypes[hyperdb.Date])
+ self.sql(sql)
+ self.create_journal_table_indexes(spec)
+
+ def create_journal_table_indexes(self, spec):
+ # index on nodeid
+ sql = 'create index %s_journ_idx on %s__journal(nodeid)'%(
+ spec.classname, spec.classname)
+ self.sql(sql)
+
+ def drop_journal_table_indexes(self, classname):
+ index_name = '%s_journ_idx'%classname
+ if not self.sql_index_exists('%s__journal'%classname, index_name):
+ return
+ index_sql = 'drop index '+index_name
+ self.sql(index_sql)
+
+ def create_multilink_table(self, spec, ml):
+ ''' Create a multilink table for the "ml" property of the class
+ given by the spec
+ '''
+ # create the table
+ sql = 'create table %s_%s (linkid INTEGER, nodeid INTEGER)'%(
+ spec.classname, ml)
+ self.sql(sql)
+ self.create_multilink_table_indexes(spec, ml)
+
+ def create_multilink_table_indexes(self, spec, ml):
+ # create index on linkid
+ index_sql = 'create index %s_%s_l_idx on %s_%s(linkid)'%(
+ spec.classname, ml, spec.classname, ml)
+ self.sql(index_sql)
+
+ # create index on nodeid
+ index_sql = 'create index %s_%s_n_idx on %s_%s(nodeid)'%(
+ spec.classname, ml, spec.classname, ml)
+ self.sql(index_sql)
+
+ def drop_multilink_table_indexes(self, classname, ml):
+ l = [
+ '%s_%s_l_idx'%(classname, ml),
+ '%s_%s_n_idx'%(classname, ml)
+ ]
+ table_name = '%s_%s'%(classname, ml)
+ for index_name in l:
+ if not self.sql_index_exists(table_name, index_name):
+ continue
+ index_sql = 'drop index %s'%index_name
+ self.sql(index_sql)
+
+ def create_class(self, spec):
+ ''' Create a database table according to the given spec.
+ '''
+ cols, mls = self.create_class_table(spec)
+ self.create_journal_table(spec)
+
+ # now create the multilink tables
+ for ml in mls:
+ self.create_multilink_table(spec, ml)
+
+ def drop_class(self, cn, spec):
+ ''' Drop the given table from the database.
+
+ Drop the journal and multilink tables too.
+ '''
+ properties = spec[1]
+ # figure the multilinks
+ mls = []
+ for propanme, prop in properties:
+ if isinstance(prop, Multilink):
+ mls.append(propname)
+
+ # drop class table and indexes
+ self.drop_class_table_indexes(cn, spec[0])
+
+ self.drop_class_table(cn)
+
+ # drop journal table and indexes
+ self.drop_journal_table_indexes(cn)
+ sql = 'drop table %s__journal'%cn
+ self.sql(sql)
+
+ for ml in mls:
+ # drop multilink table and indexes
+ self.drop_multilink_table_indexes(cn, ml)
+ sql = 'drop table %s_%s'%(spec.classname, ml)
+ self.sql(sql)
+
+ def drop_class_table(self, cn):
+ sql = 'drop table _%s'%cn
+ self.sql(sql)
+
+ #
+ # Classes
+ #
+ def __getattr__(self, classname):
+ ''' A convenient way of calling self.getclass(classname).
+ '''
+ if self.classes.has_key(classname):
+ return self.classes[classname]
+ raise AttributeError, classname
+
+ def addclass(self, cl):
+ ''' Add a Class to the hyperdatabase.
+ '''
+ cn = cl.classname
+ if self.classes.has_key(cn):
+ raise ValueError, cn
+ self.classes[cn] = cl
+
+ # add default Edit and View permissions
+ self.security.addPermission(name="Create", klass=cn,
+ description="User is allowed to create "+cn)
+ self.security.addPermission(name="Edit", klass=cn,
+ description="User is allowed to edit "+cn)
+ self.security.addPermission(name="View", klass=cn,
+ description="User is allowed to access "+cn)
+
+ def getclasses(self):
+ ''' 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.
+
+ If 'classname' is not a valid class name, a KeyError is raised.
+ '''
+ try:
+ return self.classes[classname]
+ except KeyError:
+ raise KeyError, 'There is no class called "%s"'%classname
+
+ def clear(self):
+ '''Delete all database contents.
+
+ Note: I don't commit here, which is different behaviour to the
+ "nuke from orbit" behaviour in the dbs.
+ '''
+ logging.getLogger('hyperdb').info('clear')
+ for cn in self.classes.keys():
+ sql = 'delete from _%s'%cn
+ self.sql(sql)
+
+ #
+ # Nodes
+ #
+
+ hyperdb_to_sql_value = {
+ hyperdb.String : str,
+ # fractional seconds by default
+ hyperdb.Date : lambda x: x.formal(sep=' ', sec='%06.3f'),
+ hyperdb.Link : int,
+ hyperdb.Interval : str,
+ hyperdb.Password : str,
+ hyperdb.Boolean : lambda x: x and 'TRUE' or 'FALSE',
+ hyperdb.Number : lambda x: x,
+ hyperdb.Multilink : lambda x: x, # used in journal marshalling
+ }
+ 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))
+
+ # determine the column definitions and multilink tables
+ cl = self.classes[classname]
+ cols, mls = self.determine_columns(cl.properties.items())
+
+ # we'll be supplied these props if we're doing an import
+ values = node.copy()
+ if not values.has_key('creator'):
+ # add in the "calculated" properties (dupe so we don't affect
+ # calling code's node assumptions)
+ values['creation'] = values['activity'] = date.Date()
+ values['actor'] = values['creator'] = self.getuid()
+
+ cl = self.classes[classname]
+ props = cl.getprops(protected=1)
+ del props['id']
+
+ # default the non-multilink columns
+ for col, prop in props.items():
+ if not values.has_key(col):
+ if isinstance(prop, Multilink):
+ values[col] = []
+ else:
+ values[col] = None
+
+ # clear this node out of the cache if it's in there
+ key = (classname, nodeid)
+ if self.cache.has_key(key):
+ del self.cache[key]
+ self.cache_lru.remove(key)
+
+ # figure the values to insert
+ vals = []
+ for col,dt in cols:
+ # this is somewhat dodgy....
+ if col.endswith('_int__'):
+ # XXX eugh, this test suxxors
+ value = values[col[2:-6]]
+ # this is an Interval special "int" column
+ if value is not None:
+ vals.append(value.as_seconds())
+ else:
+ vals.append(value)
+ continue
+
+ prop = props[col[1:]]
+ value = values[col[1:]]
+ if value is not None:
+ value = self.hyperdb_to_sql_value[prop.__class__](value)
+ vals.append(value)
+ vals.append(nodeid)
+ vals = tuple(vals)
+
+ # make sure the ordering is correct for column name -> column value
+ s = ','.join([self.arg for x in cols]) + ',%s'%self.arg
+ cols = ','.join([col for col,dt in cols]) + ',id'
+
+ # perform the inserts
+ sql = 'insert into _%s (%s) values (%s)'%(classname, cols, s)
+ self.sql(sql, vals)
+
+ # insert the multilink rows
+ for col in mls:
+ t = '%s_%s'%(classname, col)
+ for entry in node[col]:
+ sql = 'insert into %s (linkid, nodeid) values (%s,%s)'%(t,
+ self.arg, self.arg)
+ self.sql(sql, (entry, nodeid))
+
+ 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))
+
+ # clear this node out of the cache if it's in there
+ key = (classname, nodeid)
+ if self.cache.has_key(key):
+ del self.cache[key]
+ self.cache_lru.remove(key)
+
+ cl = self.classes[classname]
+ props = cl.getprops()
+
+ cols = []
+ mls = []
+ # add the multilinks separately
+ for col in values.keys():
+ prop = props[col]
+ if isinstance(prop, Multilink):
+ mls.append(col)
+ elif isinstance(prop, Interval):
+ # Intervals store the seconds value too
+ cols.append(col)
+ # extra leading '_' added by code below
+ cols.append('_' +col + '_int__')
+ else:
+ cols.append(col)
+ cols.sort()
+
+ # figure the values to insert
+ vals = []
+ for col in cols:
+ if col.endswith('_int__'):
+ # XXX eugh, this test suxxors
+ # Intervals store the seconds value too
+ col = col[1:-6]
+ prop = props[col]
+ value = values[col]
+ if value is None:
+ vals.append(None)
+ else:
+ vals.append(value.as_seconds())
+ else:
+ prop = props[col]
+ value = values[col]
+ if value is None:
+ e = None
+ else:
+ e = self.hyperdb_to_sql_value[prop.__class__](value)
+ vals.append(e)
+
+ vals.append(int(nodeid))
+ vals = tuple(vals)
+
+ # if there's any updates to regular columns, do them
+ if cols:
+ # make sure the ordering is correct for column name -> column value
+ s = ','.join(['_%s=%s'%(x, self.arg) for x in cols])
+ cols = ','.join(cols)
+
+ # perform the update
+ sql = 'update _%s set %s where id=%s'%(classname, s, self.arg)
+ self.sql(sql, vals)
+
+ # we're probably coming from an import, not a change
+ if not multilink_changes:
+ for name in mls:
+ prop = props[name]
+ value = values[name]
+
+ t = '%s_%s'%(classname, name)
+
+ # clear out previous values for this node
+ # XXX numeric ids
+ self.sql('delete from %s where nodeid=%s'%(t, self.arg),
+ (nodeid,))
+
+ # insert the values for this node
+ for entry in values[name]:
+ sql = 'insert into %s (linkid, nodeid) values (%s,%s)'%(t,
+ self.arg, self.arg)
+ # XXX numeric ids
+ self.sql(sql, (entry, nodeid))
+
+ # we have multilink changes to apply
+ for col, (add, remove) in multilink_changes.items():
+ tn = '%s_%s'%(classname, col)
+ if add:
+ sql = 'insert into %s (nodeid, linkid) values (%s,%s)'%(tn,
+ self.arg, self.arg)
+ for addid in add:
+ # 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)))
+
+ sql_to_hyperdb_value = {
+ hyperdb.String : str,
+ hyperdb.Date : lambda x:date.Date(str(x).replace(' ', '.')),
+# hyperdb.Link : int, # XXX numeric ids
+ hyperdb.Link : str,
+ hyperdb.Interval : date.Interval,
+ hyperdb.Password : lambda x: password.Password(encrypted=x),
+ hyperdb.Boolean : _bool_cvt,
+ hyperdb.Number : _num_cvt,
+ hyperdb.Multilink : lambda x: x, # used in journal marshalling
+ }
+ def getnode(self, classname, nodeid):
+ ''' Get a node from the database.
+ '''
+ # see if we have this node cached
+ key = (classname, nodeid)
+ if self.cache.has_key(key):
+ # push us back to the top of the LRU
+ self.cache_lru.remove(key)
+ self.cache_lru.insert(0, key)
+ if __debug__:
+ self.stats['cache_hits'] += 1
+ # return the cached information
+ return self.cache[key]
+
+ if __debug__:
+ self.stats['cache_misses'] += 1
+ start_t = time.time()
+
+ # figure the columns we're fetching
+ cl = self.classes[classname]
+ cols, mls = self.determine_columns(cl.properties.items())
+ scols = ','.join([col for col,dt in cols])
+
+ # perform the basic property fetch
+ sql = 'select %s from _%s where id=%s'%(scols, classname, self.arg)
+ self.sql(sql, (nodeid,))
+
+ values = self.sql_fetchone()
+ if values is None:
+ raise IndexError, 'no such %s node %s'%(classname, nodeid)
+
+ # make up the node
+ node = {}
+ props = cl.getprops(protected=1)
+ for col in range(len(cols)):
+ name = cols[col][0][1:]
+ if name.endswith('_int__'):
+ # XXX eugh, this test suxxors
+ # ignore the special Interval-as-seconds column
+ continue
+ value = values[col]
+ if value is not None:
+ value = self.sql_to_hyperdb_value[props[name].__class__](value)
+ node[name] = value
+
+
+ # now the multilinks
+ for col in mls:
+ # get the link ids
+ sql = 'select linkid from %s_%s where nodeid=%s'%(classname, col,
+ self.arg)
+ self.cursor.execute(sql, (nodeid,))
+ # extract the first column from the result
+ # XXX numeric ids
+ items = [int(x[0]) for x in self.cursor.fetchall()]
+ items.sort ()
+ node[col] = [str(x) for x in items]
+
+ # save off in the cache
+ key = (classname, nodeid)
+ self.cache[key] = node
+ # update the LRU
+ self.cache_lru.insert(0, key)
+ if len(self.cache_lru) > ROW_CACHE_SIZE:
+ del self.cache[self.cache_lru.pop()]
+
+ if __debug__:
+ self.stats['get_items'] += (time.time() - start_t)
+
+ return node
+
+ def destroynode(self, classname, nodeid):
+ '''Remove a node from the database. Called exclusively by the
+ destroy() method on Class.
+ '''
+ logging.getLogger('hyperdb').info('destroynode %s%s'%(classname, nodeid))
+
+ # make sure the node exists
+ if not self.hasnode(classname, nodeid):
+ raise IndexError, '%s has no node %s'%(classname, nodeid)
+
+ # see if we have this node cached
+ if self.cache.has_key((classname, nodeid)):
+ del self.cache[(classname, nodeid)]
+
+ # see if there's any obvious commit actions that we should get rid of
+ for entry in self.transactions[:]:
+ if entry[1][:2] == (classname, nodeid):
+ self.transactions.remove(entry)
+
+ # now do the SQL
+ sql = 'delete from _%s where id=%s'%(classname, self.arg)
+ self.sql(sql, (nodeid,))
+
+ # remove from multilnks
+ cl = self.getclass(classname)
+ x, mls = self.determine_columns(cl.properties.items())
+ for col in mls:
+ # get the link ids
+ sql = 'delete from %s_%s where nodeid=%s'%(classname, col, self.arg)
+ self.sql(sql, (nodeid,))
+
+ # remove journal entries
+ sql = 'delete from %s__journal where nodeid=%s'%(classname, self.arg)
+ self.sql(sql, (nodeid,))
+
+ def hasnode(self, classname, nodeid):
+ ''' Determine if the database has a given node.
+ '''
+ sql = 'select count(*) from _%s where id=%s'%(classname, self.arg)
+ self.sql(sql, (nodeid,))
+ return int(self.cursor.fetchone()[0])
+
+ def countnodes(self, classname):
+ ''' Count the number of nodes that exist for a particular Class.
+ '''
+ sql = 'select count(*) from _%s'%classname
+ self.sql(sql)
+ return self.cursor.fetchone()[0]
+
+ def addjournal(self, classname, nodeid, action, params, creator=None,
+ creation=None):
+ ''' Journal the Action
+ 'action' may be:
+
+ 'create' or 'set' -- 'params' is a dictionary of property values
+ 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
+ 'retire' -- 'params' is None
+ '''
+ # handle supply of the special journalling parameters (usually
+ # supplied on importing an existing database)
+ if creator:
+ journaltag = creator
+ else:
+ journaltag = self.getuid()
+ if creation:
+ journaldate = creation
+ else:
+ journaldate = date.Date()
+
+ # 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))
+
+ # make the journalled data marshallable
+ if isinstance(params, type({})):
+ self._journal_marshal(params, classname)
+
+ params = repr(params)
+
+ dc = self.hyperdb_to_sql_value[hyperdb.Date]
+ journaldate = dc(journaldate)
+
+ self.save_journal(classname, cols, nodeid, journaldate,
+ journaltag, action, params)
+
+ def setjournal(self, classname, nodeid, journal):
+ '''Set the journal to the "journal" list.'''
+ # clear out any existing entries
+ self.sql('delete from %s__journal where nodeid=%s'%(classname,
+ self.arg), (nodeid,))
+
+ # create the journal entry
+ cols = 'nodeid,date,tag,action,params'
+
+ dc = self.hyperdb_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))
+
+ # make the journalled data marshallable
+ if isinstance(params, type({})):
+ self._journal_marshal(params, classname)
+ params = repr(params)
+
+ self.save_journal(classname, cols, nodeid, dc(journaldate),
+ journaltag, action, params)
+
+ def _journal_marshal(self, params, classname):
+ '''Convert the journal params values into safely repr'able and
+ eval'able values.'''
+ properties = self.getclass(classname).getprops()
+ for param, value in params.items():
+ if not value:
+ continue
+ property = properties[param]
+ cvt = self.hyperdb_to_sql_value[property.__class__]
+ if isinstance(property, Password):
+ params[param] = cvt(value)
+ elif isinstance(property, Date):
+ params[param] = cvt(value)
+ elif isinstance(property, Interval):
+ params[param] = cvt(value)
+ elif isinstance(property, Boolean):
+ params[param] = cvt(value)
+
+ def getjournal(self, classname, nodeid):
+ ''' get the journal for id
+ '''
+ # make sure the node exists
+ if not self.hasnode(classname, nodeid):
+ raise IndexError, '%s has no node %s'%(classname, nodeid)
+
+ cols = ','.join('nodeid date tag action params'.split())
+ journal = self.load_journal(classname, cols, nodeid)
+
+ # now unmarshal the data
+ dc = self.sql_to_hyperdb_value[hyperdb.Date]
+ res = []
+ properties = self.getclass(classname).getprops()
+ for nodeid, date_stamp, user, action, params in journal:
+ params = eval(params)
+ if isinstance(params, type({})):
+ for param, value in params.items():
+ if not value:
+ continue
+ property = properties.get(param, None)
+ if property is None:
+ # deleted property
+ continue
+ cvt = self.sql_to_hyperdb_value[property.__class__]
+ if isinstance(property, Password):
+ params[param] = cvt(value)
+ elif isinstance(property, Date):
+ params[param] = cvt(value)
+ elif isinstance(property, Interval):
+ params[param] = cvt(value)
+ elif isinstance(property, Boolean):
+ params[param] = cvt(value)
+ # XXX numeric ids
+ res.append((str(nodeid), dc(date_stamp), user, action, params))
+ return res
+
+ def save_journal(self, classname, cols, nodeid, journaldate,
+ journaltag, action, params):
+ ''' Save the journal entry to the database
+ '''
+ entry = (nodeid, journaldate, journaltag, action, params)
+
+ # do the insert
+ a = self.arg
+ sql = 'insert into %s__journal (%s) values (%s,%s,%s,%s,%s)'%(
+ classname, cols, a, a, a, a, a)
+ self.sql(sql, entry)
+
+ def load_journal(self, classname, cols, nodeid):
+ ''' Load the journal from the database
+ '''
+ # now get the journal entries
+ sql = 'select %s from %s__journal where nodeid=%s order by date'%(
+ cols, classname, self.arg)
+ self.sql(sql, (nodeid,))
+ return self.cursor.fetchall()
+
+ def pack(self, pack_before):
+ ''' Delete all journal entries except "create" before 'pack_before'.
+ '''
+ date_stamp = self.hyperdb_to_sql_value[Date](pack_before)
+
+ # do the delete
+ for classname in self.classes.keys():
+ sql = "delete from %s__journal where date<%s and "\
+ "action<>'create'"%(classname, self.arg)
+ self.sql(sql, (date_stamp,))
+
+ def sql_commit(self):
+ ''' Actually commit to the database.
+ '''
+ logging.getLogger('hyperdb').info('commit')
+ self.conn.commit()
+
+ # open a new cursor for subsequent work
+ self.cursor = self.conn.cursor()
+
+ def commit(self):
+ ''' Commit the current transactions.
+
+ Save all data changed since the database was opened or since the
+ last commit() or rollback().
+ '''
+ # commit the database
+ self.sql_commit()
+
+ # now, do all the other transaction stuff
+ for method, args in self.transactions:
+ method(*args)
+
+ # save the indexer
+ self.indexer.save_index()
+
+ # clear out the transactions
+ self.transactions = []
+
+ def sql_rollback(self):
+ self.conn.rollback()
+
+ def rollback(self):
+ ''' Reverse all actions from the current transaction.
+
+ Undo all the changes made since the database was opened or the last
+ commit() or rollback() was performed.
+ '''
+ logging.getLogger('hyperdb').info('rollback')
+
+ self.sql_rollback()
+
+ # roll back "other" transaction stuff
+ for method, args in self.transactions:
+ # delete temporary files
+ if method == self.doStoreFile:
+ self.rollbackStoreFile(*args)
+ self.transactions = []
+
+ # clear the cache
+ self.clearCache()
+
+ def sql_close(self):
+ logging.getLogger('hyperdb').info('close')
+ self.conn.close()
+
+ def close(self):
+ ''' Close off the connection.
+ '''
+ self.indexer.close()
+ self.sql_close()
+
+#
+# The base Class class
+#
+class Class(hyperdb.Class):
+ ''' The handle to a particular class of nodes in a hyperdatabase.
+
+ All methods except __repr__ and getnode must be implemented by a
+ concrete backend Class.
+ '''
+
+ def schema(self):
+ ''' A dumpable version of the schema that we can store in the
+ database
+ '''
+ return (self.key, [(x, repr(y)) for x,y in self.properties.items()])
+
+ def enableJournalling(self):
+ '''Turn journalling on for this class
+ '''
+ self.do_journal = 1
+
+ def disableJournalling(self):
+ '''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.
+
+ The keyword arguments in 'propvalues' map property names to values.
+
+ The values of arguments must be acceptable for the types of their
+ corresponding properties or a TypeError is raised.
+
+ If this class has a key property, it must be present and its value
+ must not collide with other key strings or a ValueError is raised.
+
+ Any other properties on this class that are missing from the
+ 'propvalues' dictionary are set to None.
+
+ If an id in a link or multilink property does not refer to a valid
+ node, an IndexError is raised.
+ '''
+ 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.
+ '''
+ if propvalues.has_key('id'):
+ raise KeyError, '"id" is reserved'
+
+ if self.db.journaltag is None:
+ raise DatabaseError, 'Database open read-only'
+
+ if propvalues.has_key('creator') or propvalues.has_key('actor') or \
+ propvalues.has_key('creation') or propvalues.has_key('activity'):
+ raise KeyError, '"creator", "actor", "creation" and '\
+ '"activity" are reserved'
+
+ # new node's id
+ newid = self.db.newid(self.classname)
+
+ # validate propvalues
+ num_re = re.compile('^\d+$')
+ for key, value in propvalues.items():
+ if key == self.key:
+ try:
+ self.lookup(value)
+ except KeyError:
+ pass
+ else:
+ raise ValueError, 'node with key "%s" exists'%value
+
+ # try to handle this property
+ try:
+ prop = self.properties[key]
+ except KeyError:
+ raise KeyError, '"%s" has no property "%s"'%(self.classname,
+ key)
+
+ if value is not None and isinstance(prop, Link):
+ if type(value) != type(''):
+ raise ValueError, 'link value must be String'
+ link_class = self.properties[key].classname
+ # if it isn't a number, it's a key
+ if not num_re.match(value):
+ try:
+ value = self.db.classes[link_class].lookup(value)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ key, value, link_class)
+ elif not self.db.getclass(link_class).hasnode(value):
+ raise IndexError, '%s has no node %s'%(link_class, value)
+
+ # save off the value
+ propvalues[key] = value
+
+ # register the link with the newly linked node
+ if self.do_journal and self.properties[key].do_journal:
+ self.db.addjournal(link_class, value, 'link',
+ (self.classname, newid, key))
+
+ elif isinstance(prop, Multilink):
+ if type(value) != type([]):
+ raise TypeError, 'new property "%s" not a list of ids'%key
+
+ # clean up and validate the list of links
+ link_class = self.properties[key].classname
+ l = []
+ for entry in value:
+ if type(entry) != type(''):
+ raise ValueError, '"%s" multilink value (%r) '\
+ 'must contain Strings'%(key, value)
+ # if it isn't a number, it's a key
+ if not num_re.match(entry):
+ try:
+ entry = self.db.classes[link_class].lookup(entry)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ key, entry, self.properties[key].classname)
+ l.append(entry)
+ value = l
+ propvalues[key] = value
+
+ # handle additions
+ for nodeid in value:
+ if not self.db.getclass(link_class).hasnode(nodeid):
+ raise IndexError, '%s has no node %s'%(link_class,
+ nodeid)
+ # register the link with the newly linked node
+ if self.do_journal and self.properties[key].do_journal:
+ self.db.addjournal(link_class, nodeid, 'link',
+ (self.classname, newid, key))
+
+ elif isinstance(prop, String):
+ if type(value) != type('') and type(value) != type(u''):
+ raise TypeError, 'new property "%s" not a string'%key
+ if prop.indexme:
+ self.db.indexer.add_text((self.classname, newid, key),
+ value)
+
+ elif isinstance(prop, Password):
+ if not isinstance(value, password.Password):
+ raise TypeError, 'new property "%s" not a Password'%key
+
+ elif isinstance(prop, Date):
+ if value is not None and not isinstance(value, date.Date):
+ raise TypeError, 'new property "%s" not a Date'%key
+
+ elif isinstance(prop, Interval):
+ if value is not None and not isinstance(value, date.Interval):
+ raise TypeError, 'new property "%s" not an Interval'%key
+
+ elif value is not None and isinstance(prop, Number):
+ try:
+ float(value)
+ except ValueError:
+ raise TypeError, 'new property "%s" not numeric'%key
+
+ elif value is not None and isinstance(prop, Boolean):
+ try:
+ int(value)
+ except ValueError:
+ raise TypeError, 'new property "%s" not boolean'%key
+
+ # make sure there's data where there needs to be
+ for key, prop in self.properties.items():
+ if propvalues.has_key(key):
+ continue
+ if key == self.key:
+ raise ValueError, 'key property "%s" is required'%key
+ if isinstance(prop, Multilink):
+ propvalues[key] = []
+ else:
+ propvalues[key] = None
+
+ # done
+ self.db.addnode(self.classname, newid, propvalues)
+ if self.do_journal:
+ self.db.addjournal(self.classname, newid, ''"create", {})
+
+ # XXX numeric ids
+ return str(newid)
+
+ def get(self, nodeid, propname, default=_marker, cache=1):
+ '''Get the value of a property on an existing node of this class.
+
+ 'nodeid' must be the id of an existing node of this class or an
+ IndexError is raised. 'propname' must be the name of a property
+ of this class or a KeyError is raised.
+
+ 'cache' exists for backwards compatibility, and is not used.
+ '''
+ if propname == 'id':
+ return nodeid
+
+ # get the node's dict
+ d = self.db.getnode(self.classname, nodeid)
+
+ if propname == 'creation':
+ if d.has_key('creation'):
+ return d['creation']
+ else:
+ return date.Date()
+ if propname == 'activity':
+ if d.has_key('activity'):
+ return d['activity']
+ else:
+ return date.Date()
+ if propname == 'creator':
+ if d.has_key('creator'):
+ return d['creator']
+ else:
+ return self.db.getuid()
+ if propname == 'actor':
+ if d.has_key('actor'):
+ return d['actor']
+ else:
+ return self.db.getuid()
+
+ # get the property (raises KeyErorr if invalid)
+ prop = self.properties[propname]
+
+ # XXX may it be that propname is valid property name
+ # (above error is not raised) and not d.has_key(propname)???
+ if (not d.has_key(propname)) or (d[propname] is None):
+ if default is _marker:
+ if isinstance(prop, Multilink):
+ return []
+ else:
+ return None
+ else:
+ return default
+
+ # don't pass our list to other code
+ if isinstance(prop, Multilink):
+ return d[propname][:]
+
+ return d[propname]
+
+ def set(self, nodeid, **propvalues):
+ '''Modify a property on an existing node of this class.
+
+ 'nodeid' must be the id of an existing node of this class or an
+ IndexError is raised.
+
+ Each key in 'propvalues' must be the name of a property of this
+ class or a KeyError is raised.
+
+ All values in 'propvalues' must be acceptable types for their
+ corresponding properties or a TypeError is raised.
+
+ If the value of the key property is set, it must not collide with
+ other key strings or a ValueError is raised.
+
+ If the value of a Link or Multilink property contains an invalid
+ node id, a ValueError is raised.
+ '''
+ self.fireAuditors('set', nodeid, propvalues)
+ oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
+ propvalues = self.set_inner(nodeid, **propvalues)
+ self.fireReactors('set', nodeid, oldvalues)
+ return propvalues
+
+ def set_inner(self, nodeid, **propvalues):
+ ''' Called by set, in-between the audit and react calls.
+ '''
+ if not propvalues:
+ return propvalues
+
+ if propvalues.has_key('creation') or propvalues.has_key('creator') or \
+ propvalues.has_key('actor') or propvalues.has_key('activity'):
+ raise KeyError, '"creation", "creator", "actor" and '\
+ '"activity" are reserved'
+
+ if propvalues.has_key('id'):
+ raise KeyError, '"id" is reserved'
+
+ if self.db.journaltag is None:
+ raise DatabaseError, 'Database open read-only'
+
+ node = self.db.getnode(self.classname, nodeid)
+ if self.is_retired(nodeid):
+ raise IndexError, 'Requested item is retired'
+ num_re = re.compile('^\d+$')
+
+ # make a copy of the values dictionary - we'll modify the contents
+ propvalues = propvalues.copy()
+
+ # if the journal value is to be different, store it in here
+ journalvalues = {}
+
+ # remember the add/remove stuff for multilinks, making it easier
+ # for the Database layer to do its stuff
+ multilink_changes = {}
+
+ for propname, value in propvalues.items():
+ # check to make sure we're not duplicating an existing key
+ if propname == self.key and node[propname] != value:
+ try:
+ self.lookup(value)
+ except KeyError:
+ pass
+ else:
+ raise ValueError, 'node with key "%s" exists'%value
+
+ # this will raise the KeyError if the property isn't valid
+ # ... we don't use getprops() here because we only care about
+ # the writeable properties.
+ try:
+ prop = self.properties[propname]
+ except KeyError:
+ raise KeyError, '"%s" has no property named "%s"'%(
+ self.classname, propname)
+
+ # if the value's the same as the existing value, no sense in
+ # doing anything
+ current = node.get(propname, None)
+ if value == current:
+ del propvalues[propname]
+ continue
+ journalvalues[propname] = current
+
+ # do stuff based on the prop type
+ if isinstance(prop, Link):
+ link_class = prop.classname
+ # if it isn't a number, it's a key
+ if value is not None and not isinstance(value, type('')):
+ raise ValueError, 'property "%s" link value be a string'%(
+ propname)
+ if isinstance(value, type('')) and not num_re.match(value):
+ try:
+ value = self.db.classes[link_class].lookup(value)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ propname, value, prop.classname)
+
+ if (value is not None and
+ not self.db.getclass(link_class).hasnode(value)):
+ raise IndexError, '%s has no node %s'%(link_class, value)
+
+ if self.do_journal and prop.do_journal:
+ # register the unlink with the old linked node
+ if node[propname] is not None:
+ self.db.addjournal(link_class, node[propname],
+ ''"unlink", (self.classname, nodeid, propname))
+
+ # register the link with the newly linked node
+ if value is not None:
+ self.db.addjournal(link_class, value, ''"link",
+ (self.classname, nodeid, propname))
+
+ elif isinstance(prop, Multilink):
+ if type(value) != type([]):
+ raise TypeError, 'new property "%s" not a list of'\
+ ' ids'%propname
+ link_class = self.properties[propname].classname
+ l = []
+ for entry in value:
+ # if it isn't a number, it's a key
+ if type(entry) != type(''):
+ raise ValueError, 'new property "%s" link value ' \
+ 'must be a string'%propname
+ if not num_re.match(entry):
+ try:
+ entry = self.db.classes[link_class].lookup(entry)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ propname, entry,
+ self.properties[propname].classname)
+ l.append(entry)
+ value = l
+ propvalues[propname] = value
+
+ # figure the journal entry for this property
+ add = []
+ remove = []
+
+ # handle removals
+ if node.has_key(propname):
+ l = node[propname]
+ else:
+ l = []
+ for id in l[:]:
+ if id in value:
+ continue
+ # register the unlink with the old linked node
+ if self.do_journal and self.properties[propname].do_journal:
+ self.db.addjournal(link_class, id, 'unlink',
+ (self.classname, nodeid, propname))
+ l.remove(id)
+ remove.append(id)
+
+ # 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
+ # 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',
+ (self.classname, nodeid, propname))
+ l.append(id)
+ add.append(id)
+
+ # figure the journal entry
+ l = []
+ if add:
+ l.append(('+', add))
+ if remove:
+ l.append(('-', remove))
+ multilink_changes[propname] = (add, remove)
+ if l:
+ journalvalues[propname] = tuple(l)
+
+ elif isinstance(prop, String):
+ if value is not None and type(value) != type('') and type(value) != type(u''):
+ raise TypeError, 'new property "%s" not a string'%propname
+ if prop.indexme:
+ if value is None: value = ''
+ self.db.indexer.add_text((self.classname, nodeid, propname),
+ value)
+
+ elif isinstance(prop, Password):
+ if not isinstance(value, password.Password):
+ raise TypeError, 'new property "%s" not a Password'%propname
+ propvalues[propname] = value
+
+ elif value is not None and isinstance(prop, Date):
+ if not isinstance(value, date.Date):
+ raise TypeError, 'new property "%s" not a Date'% propname
+ propvalues[propname] = value
+
+ elif value is not None and isinstance(prop, Interval):
+ if not isinstance(value, date.Interval):
+ raise TypeError, 'new property "%s" not an '\
+ 'Interval'%propname
+ propvalues[propname] = value
+
+ elif value is not None and isinstance(prop, Number):
+ try:
+ float(value)
+ except ValueError:
+ raise TypeError, 'new property "%s" not numeric'%propname
+
+ elif value is not None and isinstance(prop, Boolean):
+ try:
+ int(value)
+ except ValueError:
+ raise TypeError, 'new property "%s" not boolean'%propname
+
+ # nothing to do?
+ if not propvalues:
+ return propvalues
+
+ # update the activity time
+ propvalues['activity'] = date.Date()
+ propvalues['actor'] = self.db.getuid()
+
+ # do the set
+ self.db.setnode(self.classname, nodeid, propvalues, multilink_changes)
+
+ # remove the activity props now they're handled
+ del propvalues['activity']
+ del propvalues['actor']
+
+ # journal the set
+ if self.do_journal:
+ self.db.addjournal(self.classname, nodeid, ''"set", journalvalues)
+
+ return propvalues
+
+ def retire(self, nodeid):
+ '''Retire a node.
+
+ The properties on the node remain available from the get() method,
+ and the node's id is never reused.
+
+ Retired nodes are not returned by the find(), list(), or lookup()
+ methods, and other nodes may reuse the values of their key properties.
+ '''
+ if self.db.journaltag is None:
+ raise DatabaseError, 'Database open read-only'
+
+ self.fireAuditors('retire', nodeid, None)
+
+ # use the arg for __retired__ to cope with any odd database type
+ # conversion (hello, sqlite)
+ sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
+ self.db.arg, self.db.arg)
+ self.db.sql(sql, (1, nodeid))
+ if self.do_journal:
+ self.db.addjournal(self.classname, nodeid, ''"retired", None)
+
+ self.fireReactors('retire', nodeid, None)
+
+ def restore(self, nodeid):
+ '''Restore a retired node.
+
+ Make node available for all operations like it was before retirement.
+ '''
+ if self.db.journaltag is None:
+ raise DatabaseError, 'Database open read-only'
+
+ node = self.db.getnode(self.classname, nodeid)
+ # check if key property was overrided
+ key = self.getkey()
+ try:
+ id = self.lookup(node[key])
+ except KeyError:
+ pass
+ else:
+ raise KeyError, "Key property (%s) of retired node clashes with \
+ existing one (%s)" % (key, node[key])
+
+ self.fireAuditors('restore', nodeid, None)
+ # use the arg for __retired__ to cope with any odd database type
+ # conversion (hello, sqlite)
+ sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
+ self.db.arg, self.db.arg)
+ self.db.sql(sql, (0, nodeid))
+ if self.do_journal:
+ self.db.addjournal(self.classname, nodeid, ''"restored", None)
+
+ self.fireReactors('restore', nodeid, None)
+
+ def is_retired(self, nodeid):
+ '''Return true if the node is rerired
+ '''
+ sql = 'select __retired__ from _%s where id=%s'%(self.classname,
+ self.db.arg)
+ self.db.sql(sql, (nodeid,))
+ return int(self.db.sql_fetchone()[0])
+
+ def destroy(self, nodeid):
+ '''Destroy a node.
+
+ WARNING: this method should never be used except in extremely rare
+ situations where there could never be links to the node being
+ deleted
+
+ WARNING: use retire() instead
+
+ WARNING: the properties of this node will not be available ever again
+
+ WARNING: really, use retire() instead
+
+ Well, I think that's enough warnings. This method exists mostly to
+ support the session storage of the cgi interface.
+
+ The node is completely removed from the hyperdb, including all journal
+ entries. It will no longer be available, and will generally break code
+ if there are any references to the node.
+ '''
+ if self.db.journaltag is None:
+ raise DatabaseError, 'Database open read-only'
+ self.db.destroynode(self.classname, nodeid)
+
+ def history(self, nodeid):
+ '''Retrieve the journal of edits on a particular node.
+
+ 'nodeid' must be the id of an existing node of this class or an
+ IndexError is raised.
+
+ The returned list contains tuples of the form
+
+ (nodeid, date, tag, action, params)
+
+ 'date' is a Timestamp object specifying the time of the change and
+ 'tag' is the journaltag specified when the database was opened.
+ '''
+ if not self.do_journal:
+ raise ValueError, 'Journalling is disabled for this class'
+ return self.db.getjournal(self.classname, nodeid)
+
+ # Locating nodes:
+ def hasnode(self, nodeid):
+ '''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.
+
+ 'propname' must be the name of a String property of this class or
+ None, or a TypeError is raised. The values of the key property on
+ all existing nodes must be unique or a ValueError is raised.
+ '''
+ prop = self.getprops()[propname]
+ if not isinstance(prop, String):
+ raise TypeError, 'key properties must be String'
+ self.key = propname
+
+ def getkey(self):
+ '''Return the name of the key property for this class or None.'''
+ return self.key
+
+ def lookup(self, keyvalue):
+ '''Locate a particular node by its key property and return its id.
+
+ If this class has no key property, a TypeError is raised. If the
+ 'keyvalue' matches one of the values for the key property among
+ the nodes in this class, the matching node's id is returned;
+ otherwise a KeyError is raised.
+ '''
+ if not self.key:
+ raise TypeError, 'No key property set for class %s'%self.classname
+
+ # use the arg to handle any odd database type conversion (hello,
+ # 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, 1))
+
+ # see if there was a result that's not retired
+ row = self.db.sql_fetchone()
+ if not row:
+ raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key,
+ keyvalue, self.classname)
+
+ # return the id
+ # XXX numeric ids
+ return str(row[0])
+
+ def find(self, **propspec):
+ '''Get the ids of nodes in this class which link to the given nodes.
+
+ 'propspec' consists of keyword args propname=nodeid or
+ propname={nodeid:1, }
+ 'propname' must be the name of a property in this class, or a
+ KeyError is raised. That property must be a Link or
+ Multilink property, or a TypeError is raised.
+
+ Any node in this class whose 'propname' property links to any of
+ the nodeids will be returned. Examples::
+
+ db.issue.find(messages='1')
+ db.issue.find(messages={'1':1,'3':1}, files={'7':1})
+ '''
+ # shortcut
+ if not propspec:
+ return []
+
+ # validate the args
+ props = self.getprops()
+ propspec = propspec.items()
+ for propname, nodeids in propspec:
+ # check the prop is OK
+ prop = props[propname]
+ if not isinstance(prop, Link) and not isinstance(prop, Multilink):
+ raise TypeError, "'%s' not a Link/Multilink property"%propname
+
+ # first, links
+ a = self.db.arg
+ allvalues = ()
+ sql = []
+ where = []
+ for prop, values in propspec:
+ if not isinstance(props[prop], hyperdb.Link):
+ continue
+ if type(values) is type({}) and len(values) == 1:
+ values = values.keys()[0]
+ if type(values) is type(''):
+ allvalues += (values,)
+ where.append('_%s = %s'%(prop, a))
+ elif values is None:
+ where.append('_%s is NULL'%prop)
+ else:
+ values = values.keys()
+ s = ''
+ if None in values:
+ values.remove(None)
+ s = '_%s is NULL or '%prop
+ allvalues += tuple(values)
+ s += '_%s in (%s)'%(prop, ','.join([a]*len(values)))
+ where.append('(' + s +')')
+ if where:
+ allvalues = (1, ) + allvalues
+ sql.append('''select id from _%s where __retired__ <> %s
+ and %s'''%(self.classname, a, ' and '.join(where)))
+
+ # now multilinks
+ for prop, values in propspec:
+ if not isinstance(props[prop], hyperdb.Multilink):
+ continue
+ if not values:
+ continue
+ allvalues += (1, )
+ if type(values) is type(''):
+ allvalues += (values,)
+ s = a
+ else:
+ allvalues += tuple(values.keys())
+ s = ','.join([a]*len(values))
+ tn = '%s_%s'%(self.classname, prop)
+ sql.append('''select id from _%s, %s where __retired__ <> %s
+ and id = %s.nodeid and %s.linkid in (%s)'''%(self.classname,
+ tn, a, tn, tn, s))
+
+ if not sql:
+ return []
+ sql = ' union '.join(sql)
+ self.db.sql(sql, allvalues)
+ # XXX numeric ids
+ l = [str(x[0]) for x in self.db.sql_fetchall()]
+ return l
+
+ def stringFind(self, **requirements):
+ '''Locate a particular node by matching a set of its String
+ properties in a caseless search.
+
+ If the property is not a String property, a TypeError is raised.
+
+ The return is a list of the id of all nodes that match.
+ '''
+ where = []
+ args = []
+ for propname in requirements.keys():
+ prop = self.properties[propname]
+ if not isinstance(prop, String):
+ raise TypeError, "'%s' not a String property"%propname
+ where.append(propname)
+ args.append(requirements[propname].lower())
+
+ # generate the where clause
+ s = ' and '.join(['lower(_%s)=%s'%(col, self.db.arg) for col in where])
+ sql = 'select id from _%s where %s and __retired__<>%s'%(
+ self.classname, s, self.db.arg)
+ args.append(1)
+ self.db.sql(sql, tuple(args))
+ # XXX numeric ids
+ l = [str(x[0]) for x in self.db.sql_fetchall()]
+ return l
+
+ def list(self):
+ ''' Return a list of the ids of the active nodes in this class.
+ '''
+ return self.getnodeids(retired=0)
+
+ def getnodeids(self, retired=None):
+ ''' Retrieve all the ids of the nodes for a particular Class.
+
+ Set retired=None to get all nodes. Otherwise it'll get all the
+ retired or non-retired nodes, depending on the flag.
+ '''
+ # flip the sense of the 'retired' flag if we don't want all of them
+ if retired is not None:
+ if retired:
+ args = (0, )
+ else:
+ args = (1, )
+ sql = 'select id from _%s where __retired__ <> %s'%(self.classname,
+ self.db.arg)
+ else:
+ args = ()
+ sql = 'select id from _%s'%self.classname
+ self.db.sql(sql, args)
+ # XXX numeric ids
+ ids = [str(x[0]) for x in self.db.cursor.fetchall()]
+ return ids
+
+ def filter(self, search_matches, filterspec, sort=(None,None),
+ group=(None,None)):
+ '''Return a list of the ids of the active nodes in this class that
+ match the 'filter' spec, sorted by the group spec and then the
+ sort spec
+
+ "filterspec" is {propname: value(s)}
+
+ "sort" and "group" are (dir, prop) where dir is '+', '-' or None
+ and prop is a prop name or None
+
+ "search_matches" is {nodeid: marker} or None
+
+ The filter must match all properties specificed. If the property
+ value to match is a list:
+
+ 1. String properties must match all elements in the list, and
+ 2. Other properties must match any of the elements in the list.
+ '''
+ # we can't match anything if search_matches is empty
+ if search_matches == {}:
+ return []
+
+ if __debug__:
+ start_t = time.time()
+
+ cn = self.classname
+
+ # vars to hold the components of the SQL statement
+ frum = [] # FROM clauses
+ loj = [] # LEFT OUTER JOIN clauses
+ where = [] # WHERE clauses
+ args = [] # *any* positional arguments
+ a = self.db.arg
+
+ # figure the WHERE clause from the filterspec
+ props = self.getprops()
+ mlfilt = 0 # are we joining with Multilink tables?
+ for k, v in filterspec.items():
+ propclass = props[k]
+ # now do other where clause stuff
+ if isinstance(propclass, Multilink):
+ mlfilt = 1
+ tn = '%s_%s'%(cn, k)
+ if v in ('-1', ['-1']):
+ # only match rows that have count(linkid)=0 in the
+ # corresponding multilink table)
+ where.append('_%s.id not in (select nodeid from %s)'%(cn,
+ tn))
+ elif isinstance(v, type([])):
+ frum.append(tn)
+ s = ','.join([a for x in v])
+ where.append('_%s.id=%s.nodeid and %s.linkid in (%s)'%(cn,
+ tn, tn, s))
+ args = args + v
+ else:
+ frum.append(tn)
+ where.append('_%s.id=%s.nodeid and %s.linkid=%s'%(cn, tn,
+ tn, a))
+ args.append(v)
+ elif k == 'id':
+ if isinstance(v, type([])):
+ s = ','.join([a for x in v])
+ where.append('_%s.%s in (%s)'%(cn, k, s))
+ args = args + v
+ else:
+ where.append('_%s.%s=%s'%(cn, k, a))
+ args.append(v)
+ elif isinstance(propclass, String):
+ if not isinstance(v, type([])):
+ v = [v]
+
+ # Quote the bits in the string that need it and then embed
+ # in a "substring" search. Note - need to quote the '%' so
+ # they make it through the python layer happily
+ v = ['%%'+self.db.sql_stringquote(s)+'%%' for s in v]
+
+ # now add to the where clause
+ where.append('('
+ +' and '.join(["_%s._%s LIKE '%s'"%(cn, k, s) for s in v])
+ +')')
+ # note: args are embedded in the query string now
+ elif isinstance(propclass, Link):
+ if isinstance(v, type([])):
+ d = {}
+ for entry in v:
+ if entry == '-1':
+ entry = None
+ d[entry] = entry
+ l = []
+ if d.has_key(None) or not d:
+ del d[None]
+ l.append('_%s._%s is NULL'%(cn, k))
+ if d:
+ v = d.keys()
+ s = ','.join([a for x in v])
+ l.append('(_%s._%s in (%s))'%(cn, k, s))
+ args = args + v
+ if l:
+ where.append('(' + ' or '.join(l) +')')
+ else:
+ if v in ('-1', None):
+ v = None
+ where.append('_%s._%s is NULL'%(cn, k))
+ else:
+ where.append('_%s._%s=%s'%(cn, k, a))
+ args.append(v)
+ elif isinstance(propclass, Date):
+ dc = self.db.hyperdb_to_sql_value[hyperdb.Date]
+ if isinstance(v, type([])):
+ s = ','.join([a for x in v])
+ where.append('_%s._%s in (%s)'%(cn, k, s))
+ args = args + [dc(date.Date(v)) for x in v]
+ else:
+ try:
+ # Try to filter on range of dates
+ date_rng = propclass.range_from_raw(v, self.db)
+ if date_rng.from_value:
+ where.append('_%s._%s >= %s'%(cn, k, a))
+ args.append(dc(date_rng.from_value))
+ if date_rng.to_value:
+ where.append('_%s._%s <= %s'%(cn, k, a))
+ args.append(dc(date_rng.to_value))
+ except ValueError:
+ # If range creation fails - ignore that search parameter
+ pass
+ elif isinstance(propclass, Interval):
+ # filter using the __<prop>_int__ column
+ if isinstance(v, type([])):
+ s = ','.join([a for x in v])
+ where.append('_%s.__%s_int__ in (%s)'%(cn, k, s))
+ args = args + [date.Interval(x).as_seconds() for x in v]
+ else:
+ try:
+ # Try to filter on range of intervals
+ date_rng = Range(v, date.Interval)
+ if date_rng.from_value:
+ where.append('_%s.__%s_int__ >= %s'%(cn, k, a))
+ args.append(date_rng.from_value.as_seconds())
+ if date_rng.to_value:
+ where.append('_%s.__%s_int__ <= %s'%(cn, k, a))
+ args.append(date_rng.to_value.as_seconds())
+ except ValueError:
+ # If range creation fails - ignore that search parameter
+ pass
+ else:
+ if isinstance(v, type([])):
+ s = ','.join([a for x in v])
+ where.append('_%s._%s in (%s)'%(cn, k, s))
+ args = args + v
+ else:
+ where.append('_%s._%s=%s'%(cn, k, a))
+ args.append(v)
+
+ # don't match retired nodes
+ where.append('_%s.__retired__ <> 1'%cn)
+
+ # add results of full text search
+ if search_matches is not None:
+ v = search_matches.keys()
+ s = ','.join([a for x in v])
+ where.append('_%s.id in (%s)'%(cn, s))
+ args = args + v
+
+ # sanity check: sorting *and* grouping on the same property?
+ if group[1] == sort[1]:
+ sort = (None, None)
+
+ # "grouping" is just the first-order sorting in the SQL fetch
+ orderby = []
+ ordercols = []
+ mlsort = []
+ rhsnum = 0
+ for sortby in group, sort:
+ sdir, prop = sortby
+ if sdir and prop:
+ if isinstance(props[prop], Multilink):
+ mlsort.append(sortby)
+ continue
+ elif isinstance(props[prop], Interval):
+ # use the int column for sorting
+ o = '__'+prop+'_int__'
+ ordercols.append(o)
+ elif isinstance(props[prop], Link):
+ # determine whether the linked Class has an order property
+ lcn = props[prop].classname
+ link = self.db.classes[lcn]
+ o = '_%s._%s'%(cn, prop)
+ op = link.orderprop()
+ if op != 'id':
+ tn = '_' + lcn
+ rhs = 'rhs%s_'%rhsnum
+ rhsnum += 1
+ loj.append('LEFT OUTER JOIN %s as %s on %s=%s.id'%(
+ tn, rhs, o, rhs))
+ o = '%s._%s'%(rhs, op)
+ ordercols.append(o)
+ elif prop == 'id':
+ o = '_%s.id'%cn
+ else:
+ o = '_%s._%s'%(cn, prop)
+ ordercols.append(o)
+ if sdir == '-':
+ o += ' desc'
+ orderby.append(o)
+
+ # construct the SQL
+ frum.append('_'+cn)
+ frum = ','.join(frum)
+ if where:
+ where = ' where ' + (' and '.join(where))
+ else:
+ where = ''
+ if mlfilt:
+ # we're joining tables on the id, so we will get dupes if we
+ # don't distinct()
+ cols = ['distinct(_%s.id)'%cn]
+ else:
+ cols = ['_%s.id'%cn]
+ if orderby:
+ cols = cols + ordercols
+ order = ' order by %s'%(','.join(orderby))
+ else:
+ order = ''
+ cols = ','.join(cols)
+ loj = ' '.join(loj)
+ sql = 'select %s from %s %s %s%s'%(cols, frum, loj, where, order)
+ args = tuple(args)
+ __traceback_info__ = (sql, args)
+ self.db.sql(sql, args)
+ l = self.db.sql_fetchall()
+
+ # return the IDs (the first column)
+ # XXX numeric ids
+ l = [str(row[0]) for row in l]
+
+ if not mlsort:
+ if __debug__:
+ self.db.stats['filtering'] += (time.time() - start_t)
+ return l
+
+ # ergh. someone wants to sort by a multilink.
+ r = []
+ for id in l:
+ m = []
+ for ml in mlsort:
+ m.append(self.get(id, ml[1]))
+ r.append((id, m))
+ i = 0
+ for sortby in mlsort:
+ def sortfun(a, b, dir=sortby[i], i=i):
+ if dir == '-':
+ return cmp(b[1][i], a[1][i])
+ else:
+ return cmp(a[1][i], b[1][i])
+ r.sort(sortfun)
+ i += 1
+ r = [i[0] for i in r]
+
+ if __debug__:
+ self.db.stats['filtering'] += (time.time() - start_t)
+
+ return r
+
+ def count(self):
+ '''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.
+ If the "protected" flag is true, we include protected properties -
+ those which may not be modified.
+ '''
+ d = self.properties.copy()
+ if protected:
+ d['id'] = String()
+ d['creation'] = hyperdb.Date()
+ d['activity'] = hyperdb.Date()
+ d['creator'] = hyperdb.Link('user')
+ d['actor'] = hyperdb.Link('user')
+ return d
+
+ def addprop(self, **properties):
+ '''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
+ '''
+ # find all the String properties that have indexme
+ for prop, propclass in self.getprops().items():
+ if isinstance(propclass, String) and propclass.indexme:
+ self.db.indexer.add_text((self.classname, nodeid, prop),
+ str(self.get(nodeid, prop)))
+
+ #
+ # import / export support
+ #
+ def export_list(self, propnames, nodeid):
+ ''' Export a node - generate a list of CSV-able data in the order
+ specified by propnames for the given node.
+ '''
+ properties = self.getprops()
+ l = []
+ for prop in propnames:
+ proptype = properties[prop]
+ value = self.get(nodeid, prop)
+ # "marshal" data where needed
+ if value is None:
+ pass
+ elif isinstance(proptype, hyperdb.Date):
+ value = value.get_tuple()
+ elif isinstance(proptype, hyperdb.Interval):
+ value = value.get_tuple()
+ elif isinstance(proptype, hyperdb.Password):
+ value = str(value)
+ l.append(repr(value))
+ l.append(repr(self.is_retired(nodeid)))
+ return l
+
+ def import_list(self, propnames, proplist):
+ ''' Import a node - all information including "id" is present and
+ should not be sanity checked. Triggers are not triggered. The
+ journal should be initialised using the "creator" and "created"
+ information.
+
+ Return the nodeid of the node imported.
+ '''
+ if self.db.journaltag is None:
+ raise DatabaseError, 'Database open read-only'
+ properties = self.getprops()
+
+ # make the new node's property map
+ d = {}
+ retire = 0
+ if not "id" in propnames:
+ newid = self.db.newid(self.classname)
+ else:
+ newid = eval(proplist[propnames.index("id")])
+ for i in range(len(propnames)):
+ # Use eval to reverse the repr() used to output the CSV
+ value = eval(proplist[i])
+
+ # Figure the property for this column
+ propname = propnames[i]
+
+ # "unmarshal" where necessary
+ if propname == 'id':
+ continue
+ elif propname == 'is retired':
+ # is the item retired?
+ if int(value):
+ retire = 1
+ continue
+ elif value is None:
+ d[propname] = None
+ continue
+
+ prop = properties[propname]
+ if value is None:
+ # don't set Nones
+ continue
+ elif isinstance(prop, hyperdb.Date):
+ value = date.Date(value)
+ elif isinstance(prop, hyperdb.Interval):
+ value = date.Interval(value)
+ elif isinstance(prop, hyperdb.Password):
+ pwd = password.Password()
+ pwd.unpack(value)
+ value = pwd
+ elif isinstance(prop, String):
+ if isinstance(value, unicode):
+ value = value.encode('utf8')
+ if not isinstance(value, str):
+ raise TypeError, \
+ 'new property "%(propname)s" not a string: %(value)r' \
+ % locals()
+ if prop.indexme:
+ self.db.indexer.add_text((self.classname, newid, propname),
+ value)
+ d[propname] = value
+
+ # get a new id if necessary
+ if newid is None:
+ newid = self.db.newid(self.classname)
+
+ # insert new node or update existing?
+ if not self.hasnode(newid):
+ self.db.addnode(self.classname, newid, d) # insert
+ else:
+ self.db.setnode(self.classname, newid, d) # update
+
+ # retire?
+ if retire:
+ # use the arg for __retired__ to cope with any odd database type
+ # conversion (hello, sqlite)
+ sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
+ self.db.arg, self.db.arg)
+ self.db.sql(sql, (1, newid))
+ return newid
+
+ def export_journals(self):
+ '''Export a class's journal - generate a list of lists of
+ CSV-able data:
+
+ nodeid, date, user, action, params
+
+ No heading here - the columns are fixed.
+ '''
+ properties = self.getprops()
+ r = []
+ for nodeid in self.getnodeids():
+ for nodeid, date, user, action, params in self.history(nodeid):
+ date = date.get_tuple()
+ if action == 'set':
+ export_data = {}
+ for propname, value in params.items():
+ if not properties.has_key(propname):
+ # property no longer in the schema
+ continue
+
+ prop = properties[propname]
+ # make sure the params are eval()'able
+ if value is None:
+ pass
+ elif isinstance(prop, Date):
+ value = value.get_tuple()
+ elif isinstance(prop, Interval):
+ value = value.get_tuple()
+ elif isinstance(prop, Password):
+ value = str(value)
+ export_data[propname] = value
+ params = export_data
+ l = [nodeid, date, user, action, params]
+ r.append(map(repr, l))
+ return r
+
+ def import_journals(self, entries):
+ '''Import a class's journal.
+
+ Uses setjournal() to set the journal for each item.'''
+ properties = self.getprops()
+ d = {}
+ for l in entries:
+ l = map(eval, l)
+ nodeid, jdate, user, action, params = l
+ r = d.setdefault(nodeid, [])
+ if action == 'set':
+ for propname, value in params.items():
+ prop = properties[propname]
+ if value is None:
+ pass
+ elif isinstance(prop, Date):
+ value = date.Date(value)
+ elif isinstance(prop, Interval):
+ value = date.Interval(value)
+ elif isinstance(prop, Password):
+ pwd = password.Password()
+ pwd.unpack(value)
+ value = pwd
+ params[propname] = value
+ r.append((nodeid, date.Date(jdate), user, action, params))
+
+ for nodeid, l in d.items():
+ self.db.setjournal(self.classname, nodeid, l)
+
+class FileClass(hyperdb.FileClass, Class):
+ '''This class defines a large chunk of data. To support this, it has a
+ 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"
+ and "type" properties.
+ '''
+ if not properties.has_key('content'):
+ properties['content'] = hyperdb.String(indexme='yes')
+ if not properties.has_key('type'):
+ properties['type'] = hyperdb.String()
+ Class.__init__(self, db, classname, **properties)
+
+ def create(self, **propvalues):
+ ''' snaffle the file propvalue and store in a file
+ '''
+ # we need to fire the auditors now, or the content property won't
+ # be in propvalues for the auditors to play with
+ self.fireAuditors('create', None, propvalues)
+
+ # now remove the content property so it's not stored in the db
+ content = propvalues['content']
+ del propvalues['content']
+
+ # do the database create
+ newid = self.create_inner(**propvalues)
+
+ # figure the mime type
+ mime_type = propvalues.get('type', self.default_mime_type)
+
+ # and index!
+ if self.properties['content'].indexme:
+ self.db.indexer.add_text((self.classname, newid, 'content'),
+ content, mime_type)
+
+ # 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
+
+ 'cache' exists for backwards compatibility, and is not used.
+ '''
+ poss_msg = 'Possibly a access right configuration problem.'
+ if propname == 'content':
+ try:
+ return self.db.getfile(self.classname, nodeid, None)
+ except IOError, (strerror):
+ # BUG: by catching this we donot see an error in the log.
+ return 'ERROR reading file: %s%s\n%s\n%s'%(
+ self.classname, nodeid, poss_msg, strerror)
+ if default is not _marker:
+ return Class.get(self, nodeid, propname, default)
+ else:
+ return Class.get(self, nodeid, propname)
+
+ def getprops(self, protected=1):
+ '''In addition to the actual properties on the node, these methods
+ provide the "content" property. If the "protected" flag is true,
+ we include protected properties - those which may not be
+ modified.
+
+ Note that the content prop is indexed separately, hence no indexme.
+ '''
+ d = Class.getprops(self, protected=protected).copy()
+ d['content'] = hyperdb.String()
+ return d
+
+ def set(self, itemid, **propvalues):
+ ''' Snarf the "content" propvalue and update it in a file
+ '''
+ self.fireAuditors('set', itemid, propvalues)
+ oldvalues = copy.deepcopy(self.db.getnode(self.classname, itemid))
+
+ # now remove the content property so it's not stored in the db
+ content = None
+ if propvalues.has_key('content'):
+ content = propvalues['content']
+ del propvalues['content']
+
+ # do the database create
+ propvalues = self.set_inner(itemid, **propvalues)
+
+ # do content?
+ if content:
+ # store and possibly index
+ self.db.storefile(self.classname, itemid, None, content)
+ if self.properties['content'].indexme:
+ mime_type = self.get(itemid, 'type', self.default_mime_type)
+ self.db.indexer.add_text((self.classname, itemid, 'content'),
+ content, mime_type)
+ propvalues['content'] = content
+
+ # fire reactors
+ self.fireReactors('set', itemid, oldvalues)
+ return propvalues
+
+ def index(self, nodeid):
+ ''' Add (or refresh) the node to search indexes.
+
+ Use the content-type property for the content property.
+ '''
+ # find all the String properties that have indexme
+ for prop, propclass in self.getprops().items():
+ if prop == 'content' and propclass.indexme:
+ mime_type = self.get(nodeid, 'type', self.default_mime_type)
+ self.db.indexer.add_text((self.classname, nodeid, 'content'),
+ str(self.get(nodeid, 'content')), mime_type)
+ elif isinstance(propclass, hyperdb.String) and propclass.indexme:
+ # index them under (classname, nodeid, property)
+ try:
+ value = str(self.get(nodeid, prop))
+ except IndexError:
+ # node has been destroyed
+ continue
+ self.db.indexer.add_text((self.classname, nodeid, prop), value)
+
+# XXX deviation from spec - was called ItemClass
+class IssueClass(Class, roundupdb.IssueClass):
+ # Overridden methods:
+ def __init__(self, db, classname, **properties):
+ '''The newly-created class automatically includes the "messages",
+ "files", "nosy", and "superseder" properties. If the 'properties'
+ dictionary attempts to specify any of these properties or a
+ "creation", "creator", "activity" or "actor" property, a ValueError
+ is raised.
+ '''
+ if not properties.has_key('title'):
+ properties['title'] = hyperdb.String(indexme='yes')
+ if not properties.has_key('messages'):
+ properties['messages'] = hyperdb.Multilink("msg")
+ if not properties.has_key('files'):
+ properties['files'] = hyperdb.Multilink("file")
+ if not properties.has_key('nosy'):
+ # note: journalling is turned off as it really just wastes
+ # space. this behaviour may be overridden in an instance
+ properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
+ if not properties.has_key('superseder'):
+ properties['superseder'] = hyperdb.Multilink(classname)
+ Class.__init__(self, db, classname, **properties)
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/backends/sessions_dbm.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/sessions_dbm.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,157 @@
+#$Id: sessions_dbm.py,v 1.7 2006/04/27 04:59:37 richard Exp $
+"""This module defines a very basic store that's used by the CGI interface
+to store session and one-time-key information.
+
+Yes, it's called "sessions" - because originally it only defined a session
+class. It's now also used for One Time Key handling too.
+"""
+__docformat__ = 'restructuredtext'
+
+import anydbm, whichdb, os, marshal, time
+
+class BasicDatabase:
+ ''' Provide a nice encapsulation of an anydbm store.
+
+ Keys are id strings, values are automatically marshalled data.
+ '''
+ _db_type = None
+
+ def __init__(self, db):
+ self.config = db.config
+ self.dir = db.config.DATABASE
+ os.umask(db.config.UMASK)
+
+ def exists(self, infoid):
+ db = self.opendb('c')
+ try:
+ return db.has_key(infoid)
+ finally:
+ db.close()
+
+ def clear(self):
+ path = os.path.join(self.dir, self.name)
+ if os.path.exists(path):
+ os.remove(path)
+ elif os.path.exists(path+'.db'): # dbm appends .db
+ os.remove(path+'.db')
+
+ def cache_db_type(self, path):
+ ''' determine which DB wrote the class file, and cache it as an
+ attribute of __class__ (to allow for subclassed DBs to be
+ different sorts)
+ '''
+ db_type = ''
+ if os.path.exists(path):
+ db_type = whichdb.whichdb(path)
+ if not db_type:
+ raise hyperdb.DatabaseError, "Couldn't identify database type"
+ elif os.path.exists(path+'.db'):
+ # if the path ends in '.db', it's a dbm database, whether
+ # anydbm says it's dbhash or not!
+ db_type = 'dbm'
+ self.__class__._db_type = db_type
+
+ _marker = []
+ def get(self, infoid, value, default=_marker):
+ db = self.opendb('c')
+ try:
+ if db.has_key(infoid):
+ values = marshal.loads(db[infoid])
+ else:
+ if default != self._marker:
+ return default
+ raise KeyError, 'No such %s "%s"'%(self.name, infoid)
+ return values.get(value, None)
+ finally:
+ db.close()
+
+ def getall(self, infoid):
+ db = self.opendb('c')
+ try:
+ try:
+ d = marshal.loads(db[infoid])
+ del d['__timestamp']
+ return d
+ except KeyError:
+ raise KeyError, 'No such %s "%s"'%(self.name, infoid)
+ finally:
+ db.close()
+
+ def set(self, infoid, **newvalues):
+ db = self.opendb('c')
+ try:
+ if db.has_key(infoid):
+ values = marshal.loads(db[infoid])
+ else:
+ values = {'__timestamp': time.time()}
+ values.update(newvalues)
+ db[infoid] = marshal.dumps(values)
+ finally:
+ db.close()
+
+ def list(self):
+ db = self.opendb('r')
+ try:
+ return db.keys()
+ finally:
+ db.close()
+
+ def destroy(self, infoid):
+ db = self.opendb('c')
+ try:
+ if db.has_key(infoid):
+ del db[infoid]
+ finally:
+ db.close()
+
+ def opendb(self, mode):
+ '''Low-level database opener that gets around anydbm/dbm
+ eccentricities.
+ '''
+ # figure the class db type
+ path = os.path.join(os.getcwd(), self.dir, self.name)
+ if self._db_type is None:
+ self.cache_db_type(path)
+
+ db_type = self._db_type
+
+ # new database? let anydbm pick the best dbm
+ if not db_type:
+ return anydbm.open(path, 'c')
+
+ # open the database with the correct module
+ dbm = __import__(db_type)
+ return dbm.open(path, mode)
+
+ def commit(self):
+ pass
+
+ def close(self):
+ pass
+
+ def updateTimestamp(self, sessid):
+ ''' don't update every hit - once a minute should be OK '''
+ sess = self.get(sessid, '__timestamp', None)
+ now = time.time()
+ 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.
+ """
+ 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)
+ interval = now - sess
+ if interval > week:
+ self.destroy(sessid)
+
+class Sessions(BasicDatabase):
+ name = 'sessions'
+
+class OneTimeKeys(BasicDatabase):
+ name = 'otks'
+
Added: tracker/vendor/roundup/current/roundup/backends/sessions_rdbms.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/sessions_rdbms.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,99 @@
+#$Id: sessions_rdbms.py,v 1.4 2006/04/27 04:03:11 richard Exp $
+"""This module defines a very basic store that's used by the CGI interface
+to store session and one-time-key information.
+
+Yes, it's called "sessions" - because originally it only defined a session
+class. It's now also used for One Time Key handling too.
+"""
+__docformat__ = 'restructuredtext'
+
+import os, time
+
+class BasicDatabase:
+ ''' Provide a nice encapsulation of an RDBMS table.
+
+ Keys are id strings, values are automatically marshalled data.
+ '''
+ def __init__(self, db):
+ self.db = db
+ self.cursor = self.db.cursor
+
+ def clear(self):
+ self.cursor.execute('delete from %ss'%self.name)
+
+ def exists(self, infoid):
+ n = self.name
+ self.cursor.execute('select count(*) from %ss where %s_key=%s'%(n,
+ n, self.db.arg), (infoid,))
+ return int(self.cursor.fetchone()[0])
+
+ _marker = []
+ def get(self, infoid, value, default=_marker):
+ n = self.name
+ self.cursor.execute('select %s_value from %ss where %s_key=%s'%(n,
+ n, n, self.db.arg), (infoid,))
+ res = self.cursor.fetchone()
+ if not res:
+ if default != self._marker:
+ return default
+ raise KeyError, 'No such %s "%s"'%(self.name, infoid)
+ values = eval(res[0])
+ return values.get(value, None)
+
+ def getall(self, infoid):
+ n = self.name
+ self.cursor.execute('select %s_value from %ss where %s_key=%s'%(n,
+ n, n, self.db.arg), (infoid,))
+ res = self.cursor.fetchone()
+ if not res:
+ raise KeyError, 'No such %s "%s"'%(self.name, infoid)
+ return eval(res[0])
+
+ def set(self, infoid, **newvalues):
+ c = self.cursor
+ n = self.name
+ a = self.db.arg
+ c.execute('select %s_value from %ss where %s_key=%s'%(n, n, n, a),
+ (infoid,))
+ res = c.fetchone()
+ if res:
+ values = eval(res[0])
+ else:
+ values = {}
+ values.update(newvalues)
+
+ if res:
+ sql = 'update %ss set %s_value=%s where %s_key=%s'%(n, n,
+ a, n, a)
+ args = (repr(values), infoid)
+ else:
+ sql = 'insert into %ss (%s_key, %s_time, %s_value) '\
+ 'values (%s, %s, %s)'%(n, n, n, n, a, a, a)
+ args = (infoid, time.time(), repr(values))
+ c.execute(sql, args)
+
+ def destroy(self, infoid):
+ self.cursor.execute('delete from %ss where %s_key=%s'%(self.name,
+ self.name, self.db.arg), (infoid,))
+
+ def updateTimestamp(self, infoid):
+ ''' don't update every hit - once a minute should be OK '''
+ now = time.time()
+ self.cursor.execute('''update %ss set %s_time=%s where %s_key=%s
+ and %s_time < %s'''%(self.name, self.name, self.db.arg,
+ 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
+ self.cursor.execute('delete from %ss where %s_time < %s'%(self.name,
+ self.name, self.db.arg), (old, ))
+
+class Sessions(BasicDatabase):
+ name = 'session'
+
+class OneTimeKeys(BasicDatabase):
+ name = 'otk'
+
Added: tracker/vendor/roundup/current/roundup/backends/tsearch2_setup.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/backends/tsearch2_setup.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,737 @@
+#$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
+# license:
+#
+# PostgreSQL Data Base Management System
+# (formerly known as Postgres, then as Postgres95).
+#
+# Portions Copyright (c) 1996-2003, The PostgreSQL Global Development Group
+#
+# Portions Copyright (c) 1994, The Regents of the University of California
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose, without fee, and without a written agreement
+# is hereby granted, provided that the above copyright notice and this
+# paragraph and the following two paragraphs appear in all copies.
+#
+# IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
+# LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
+# DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
+# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
+# ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO
+# PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+
+tsearch_sql = """ -- Adjust this setting to control where the objects get CREATEd.
+SET search_path = public;
+
+--dict conf
+CREATE TABLE pg_ts_dict (
+ dict_name text not null primary key,
+ dict_init oid,
+ dict_initoption text,
+ dict_lexize oid not null,
+ dict_comment text
+) with oids;
+
+--dict interface
+CREATE FUNCTION lexize(oid, text)
+ returns _text
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+CREATE FUNCTION lexize(text, text)
+ returns _text
+ as '$libdir/tsearch2', 'lexize_byname'
+ language 'C'
+ with (isstrict);
+
+CREATE FUNCTION lexize(text)
+ returns _text
+ as '$libdir/tsearch2', 'lexize_bycurrent'
+ language 'C'
+ with (isstrict);
+
+CREATE FUNCTION set_curdict(int)
+ returns void
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+CREATE FUNCTION set_curdict(text)
+ returns void
+ as '$libdir/tsearch2', 'set_curdict_byname'
+ language 'C'
+ with (isstrict);
+
+--built-in dictionaries
+CREATE FUNCTION dex_init(text)
+ returns internal
+ as '$libdir/tsearch2'
+ language 'C';
+
+CREATE FUNCTION dex_lexize(internal,internal,int4)
+ returns internal
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+insert into pg_ts_dict select
+ 'simple',
+ (select oid from pg_proc where proname='dex_init'),
+ null,
+ (select oid from pg_proc where proname='dex_lexize'),
+ 'Simple example of dictionary.'
+;
+
+CREATE FUNCTION snb_en_init(text)
+ returns internal
+ as '$libdir/tsearch2'
+ language 'C';
+
+CREATE FUNCTION snb_lexize(internal,internal,int4)
+ returns internal
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+insert into pg_ts_dict select
+ 'en_stem',
+ (select oid from pg_proc where proname='snb_en_init'),
+ '/usr/share/postgresql/contrib/english.stop',
+ (select oid from pg_proc where proname='snb_lexize'),
+ 'English Stemmer. Snowball.'
+;
+
+CREATE FUNCTION snb_ru_init(text)
+ returns internal
+ as '$libdir/tsearch2'
+ language 'C';
+
+insert into pg_ts_dict select
+ 'ru_stem',
+ (select oid from pg_proc where proname='snb_ru_init'),
+ '/usr/share/postgresql/contrib/russian.stop',
+ (select oid from pg_proc where proname='snb_lexize'),
+ 'Russian Stemmer. Snowball.'
+;
+
+CREATE FUNCTION spell_init(text)
+ returns internal
+ as '$libdir/tsearch2'
+ language 'C';
+
+CREATE FUNCTION spell_lexize(internal,internal,int4)
+ returns internal
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+insert into pg_ts_dict select
+ 'ispell_template',
+ (select oid from pg_proc where proname='spell_init'),
+ null,
+ (select oid from pg_proc where proname='spell_lexize'),
+ 'ISpell interface. Must have .dict and .aff files'
+;
+
+CREATE FUNCTION syn_init(text)
+ returns internal
+ as '$libdir/tsearch2'
+ language 'C';
+
+CREATE FUNCTION syn_lexize(internal,internal,int4)
+ returns internal
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+insert into pg_ts_dict select
+ 'synonym',
+ (select oid from pg_proc where proname='syn_init'),
+ null,
+ (select oid from pg_proc where proname='syn_lexize'),
+ 'Example of synonym dictionary'
+;
+
+--dict conf
+CREATE TABLE pg_ts_parser (
+ prs_name text not null primary key,
+ prs_start oid not null,
+ prs_nexttoken oid not null,
+ prs_end oid not null,
+ prs_headline oid not null,
+ prs_lextype oid not null,
+ prs_comment text
+) with oids;
+
+--sql-level interface
+CREATE TYPE tokentype
+ as (tokid int4, alias text, descr text);
+
+CREATE FUNCTION token_type(int4)
+ returns setof tokentype
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+CREATE FUNCTION token_type(text)
+ returns setof tokentype
+ as '$libdir/tsearch2', 'token_type_byname'
+ language 'C'
+ with (isstrict);
+
+CREATE FUNCTION token_type()
+ returns setof tokentype
+ as '$libdir/tsearch2', 'token_type_current'
+ language 'C'
+ with (isstrict);
+
+CREATE FUNCTION set_curprs(int)
+ returns void
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+CREATE FUNCTION set_curprs(text)
+ returns void
+ as '$libdir/tsearch2', 'set_curprs_byname'
+ language 'C'
+ with (isstrict);
+
+CREATE TYPE tokenout
+ as (tokid int4, token text);
+
+CREATE FUNCTION parse(oid,text)
+ returns setof tokenout
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+CREATE FUNCTION parse(text,text)
+ returns setof tokenout
+ as '$libdir/tsearch2', 'parse_byname'
+ language 'C'
+ with (isstrict);
+
+CREATE FUNCTION parse(text)
+ returns setof tokenout
+ as '$libdir/tsearch2', 'parse_current'
+ language 'C'
+ with (isstrict);
+
+--default parser
+CREATE FUNCTION prsd_start(internal,int4)
+ returns internal
+ as '$libdir/tsearch2'
+ language 'C';
+
+CREATE FUNCTION prsd_getlexeme(internal,internal,internal)
+ returns int4
+ as '$libdir/tsearch2'
+ language 'C';
+
+CREATE FUNCTION prsd_end(internal)
+ returns void
+ as '$libdir/tsearch2'
+ language 'C';
+
+CREATE FUNCTION prsd_lextype(internal)
+ returns internal
+ as '$libdir/tsearch2'
+ language 'C';
+
+CREATE FUNCTION prsd_headline(internal,internal,internal)
+ returns internal
+ as '$libdir/tsearch2'
+ language 'C';
+
+insert into pg_ts_parser select
+ 'default',
+ (select oid from pg_proc where proname='prsd_start'),
+ (select oid from pg_proc where proname='prsd_getlexeme'),
+ (select oid from pg_proc where proname='prsd_end'),
+ (select oid from pg_proc where proname='prsd_headline'),
+ (select oid from pg_proc where proname='prsd_lextype'),
+ 'Parser from OpenFTS v0.34'
+;
+
+--tsearch config
+
+CREATE TABLE pg_ts_cfg (
+ ts_name text not null primary key,
+ prs_name text not null,
+ locale text
+) with oids;
+
+CREATE TABLE pg_ts_cfgmap (
+ ts_name text not null,
+ tok_alias text not null,
+ dict_name text[],
+ primary key (ts_name,tok_alias)
+) with oids;
+
+CREATE FUNCTION set_curcfg(int)
+ returns void
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+CREATE FUNCTION set_curcfg(text)
+ returns void
+ as '$libdir/tsearch2', 'set_curcfg_byname'
+ language 'C'
+ with (isstrict);
+
+CREATE FUNCTION show_curcfg()
+ returns oid
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+insert into pg_ts_cfg values ('default', 'default','C');
+insert into pg_ts_cfg values ('default_russian', 'default','ru_RU.KOI8-R');
+insert into pg_ts_cfg values ('simple', 'default');
+
+insert into pg_ts_cfgmap values ('default', 'lword', '{en_stem}');
+insert into pg_ts_cfgmap values ('default', 'nlword', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'word', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'email', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'url', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'host', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'sfloat', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'version', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'part_hword', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'nlpart_hword', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'lpart_hword', '{en_stem}');
+insert into pg_ts_cfgmap values ('default', 'hword', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'lhword', '{en_stem}');
+insert into pg_ts_cfgmap values ('default', 'nlhword', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'uri', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'file', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'float', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'int', '{simple}');
+insert into pg_ts_cfgmap values ('default', 'uint', '{simple}');
+insert into pg_ts_cfgmap values ('default_russian', 'lword', '{en_stem}');
+insert into pg_ts_cfgmap values ('default_russian', 'nlword', '{ru_stem}');
+insert into pg_ts_cfgmap values ('default_russian', 'word', '{ru_stem}');
+insert into pg_ts_cfgmap values ('default_russian', 'email', '{simple}');
+insert into pg_ts_cfgmap values ('default_russian', 'url', '{simple}');
+insert into pg_ts_cfgmap values ('default_russian', 'host', '{simple}');
+insert into pg_ts_cfgmap values ('default_russian', 'sfloat', '{simple}');
+insert into pg_ts_cfgmap values ('default_russian', 'version', '{simple}');
+insert into pg_ts_cfgmap values ('default_russian', 'part_hword', '{simple}');
+insert into pg_ts_cfgmap values ('default_russian', 'nlpart_hword', '{ru_stem}');
+insert into pg_ts_cfgmap values ('default_russian', 'lpart_hword', '{en_stem}');
+insert into pg_ts_cfgmap values ('default_russian', 'hword', '{ru_stem}');
+insert into pg_ts_cfgmap values ('default_russian', 'lhword', '{en_stem}');
+insert into pg_ts_cfgmap values ('default_russian', 'nlhword', '{ru_stem}');
+insert into pg_ts_cfgmap values ('default_russian', 'uri', '{simple}');
+insert into pg_ts_cfgmap values ('default_russian', 'file', '{simple}');
+insert into pg_ts_cfgmap values ('default_russian', 'float', '{simple}');
+insert into pg_ts_cfgmap values ('default_russian', 'int', '{simple}');
+insert into pg_ts_cfgmap values ('default_russian', 'uint', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'lword', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'nlword', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'word', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'email', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'url', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'host', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'sfloat', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'version', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'part_hword', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'nlpart_hword', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'lpart_hword', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'hword', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'lhword', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'nlhword', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'uri', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'file', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'float', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'int', '{simple}');
+insert into pg_ts_cfgmap values ('simple', 'uint', '{simple}');
+
+--tsvector type
+CREATE FUNCTION tsvector_in(cstring)
+RETURNS tsvector
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict);
+
+CREATE FUNCTION tsvector_out(tsvector)
+RETURNS cstring
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict);
+
+CREATE TYPE tsvector (
+ INTERNALLENGTH = -1,
+ INPUT = tsvector_in,
+ OUTPUT = tsvector_out,
+ STORAGE = extended
+);
+
+CREATE FUNCTION length(tsvector)
+RETURNS int4
+AS '$libdir/tsearch2', 'tsvector_length'
+LANGUAGE 'C' with (isstrict,iscachable);
+
+CREATE FUNCTION to_tsvector(oid, text)
+RETURNS tsvector
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict,iscachable);
+
+CREATE FUNCTION to_tsvector(text, text)
+RETURNS tsvector
+AS '$libdir/tsearch2', 'to_tsvector_name'
+LANGUAGE 'C' with (isstrict,iscachable);
+
+CREATE FUNCTION to_tsvector(text)
+RETURNS tsvector
+AS '$libdir/tsearch2', 'to_tsvector_current'
+LANGUAGE 'C' with (isstrict,iscachable);
+
+CREATE FUNCTION strip(tsvector)
+RETURNS tsvector
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict,iscachable);
+
+CREATE FUNCTION setweight(tsvector,"char")
+RETURNS tsvector
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict,iscachable);
+
+CREATE FUNCTION concat(tsvector,tsvector)
+RETURNS tsvector
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict,iscachable);
+
+CREATE OPERATOR || (
+ LEFTARG = tsvector,
+ RIGHTARG = tsvector,
+ PROCEDURE = concat
+);
+
+--query type
+CREATE FUNCTION tsquery_in(cstring)
+RETURNS tsquery
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict);
+
+CREATE FUNCTION tsquery_out(tsquery)
+RETURNS cstring
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict);
+
+CREATE TYPE tsquery (
+ INTERNALLENGTH = -1,
+ INPUT = tsquery_in,
+ OUTPUT = tsquery_out
+);
+
+CREATE FUNCTION querytree(tsquery)
+RETURNS text
+AS '$libdir/tsearch2', 'tsquerytree'
+LANGUAGE 'C' with (isstrict);
+
+CREATE FUNCTION to_tsquery(oid, text)
+RETURNS tsquery
+AS '$libdir/tsearch2'
+LANGUAGE 'c' with (isstrict,iscachable);
+
+CREATE FUNCTION to_tsquery(text, text)
+RETURNS tsquery
+AS '$libdir/tsearch2','to_tsquery_name'
+LANGUAGE 'c' with (isstrict,iscachable);
+
+CREATE FUNCTION to_tsquery(text)
+RETURNS tsquery
+AS '$libdir/tsearch2','to_tsquery_current'
+LANGUAGE 'c' with (isstrict,iscachable);
+
+--operations
+CREATE FUNCTION exectsq(tsvector, tsquery)
+RETURNS bool
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict, iscachable);
+
+COMMENT ON FUNCTION exectsq(tsvector, tsquery) IS 'boolean operation with text index';
+
+CREATE FUNCTION rexectsq(tsquery, tsvector)
+RETURNS bool
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict, iscachable);
+
+COMMENT ON FUNCTION rexectsq(tsquery, tsvector) IS 'boolean operation with text index';
+
+CREATE OPERATOR @@ (
+ LEFTARG = tsvector,
+ RIGHTARG = tsquery,
+ PROCEDURE = exectsq,
+ COMMUTATOR = '@@',
+ RESTRICT = contsel,
+ JOIN = contjoinsel
+);
+CREATE OPERATOR @@ (
+ LEFTARG = tsquery,
+ RIGHTARG = tsvector,
+ PROCEDURE = rexectsq,
+ COMMUTATOR = '@@',
+ RESTRICT = contsel,
+ JOIN = contjoinsel
+);
+
+--Trigger
+CREATE FUNCTION tsearch2()
+RETURNS trigger
+AS '$libdir/tsearch2'
+LANGUAGE 'C';
+
+--Relevation
+CREATE FUNCTION rank(float4[], tsvector, tsquery)
+RETURNS float4
+AS '$libdir/tsearch2'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION rank(float4[], tsvector, tsquery, int4)
+RETURNS float4
+AS '$libdir/tsearch2'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION rank(tsvector, tsquery)
+RETURNS float4
+AS '$libdir/tsearch2', 'rank_def'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION rank(tsvector, tsquery, int4)
+RETURNS float4
+AS '$libdir/tsearch2', 'rank_def'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION rank_cd(int4, tsvector, tsquery)
+RETURNS float4
+AS '$libdir/tsearch2'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION rank_cd(int4, tsvector, tsquery, int4)
+RETURNS float4
+AS '$libdir/tsearch2'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION rank_cd(tsvector, tsquery)
+RETURNS float4
+AS '$libdir/tsearch2', 'rank_cd_def'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION rank_cd(tsvector, tsquery, int4)
+RETURNS float4
+AS '$libdir/tsearch2', 'rank_cd_def'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION headline(oid, text, tsquery, text)
+RETURNS text
+AS '$libdir/tsearch2', 'headline'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION headline(oid, text, tsquery)
+RETURNS text
+AS '$libdir/tsearch2', 'headline'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION headline(text, text, tsquery, text)
+RETURNS text
+AS '$libdir/tsearch2', 'headline_byname'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION headline(text, text, tsquery)
+RETURNS text
+AS '$libdir/tsearch2', 'headline_byname'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION headline(text, tsquery, text)
+RETURNS text
+AS '$libdir/tsearch2', 'headline_current'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+CREATE FUNCTION headline(text, tsquery)
+RETURNS text
+AS '$libdir/tsearch2', 'headline_current'
+LANGUAGE 'C' WITH (isstrict, iscachable);
+
+--GiST
+--GiST key type
+CREATE FUNCTION gtsvector_in(cstring)
+RETURNS gtsvector
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict);
+
+CREATE FUNCTION gtsvector_out(gtsvector)
+RETURNS cstring
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict);
+
+CREATE TYPE gtsvector (
+ INTERNALLENGTH = -1,
+ INPUT = gtsvector_in,
+ OUTPUT = gtsvector_out
+);
+
+-- support FUNCTIONs
+CREATE FUNCTION gtsvector_consistent(gtsvector,internal,int4)
+RETURNS bool
+AS '$libdir/tsearch2'
+LANGUAGE 'C';
+
+CREATE FUNCTION gtsvector_compress(internal)
+RETURNS internal
+AS '$libdir/tsearch2'
+LANGUAGE 'C';
+
+CREATE FUNCTION gtsvector_decompress(internal)
+RETURNS internal
+AS '$libdir/tsearch2'
+LANGUAGE 'C';
+
+CREATE FUNCTION gtsvector_penalty(internal,internal,internal)
+RETURNS internal
+AS '$libdir/tsearch2'
+LANGUAGE 'C' with (isstrict);
+
+CREATE FUNCTION gtsvector_picksplit(internal, internal)
+RETURNS internal
+AS '$libdir/tsearch2'
+LANGUAGE 'C';
+
+CREATE FUNCTION gtsvector_union(bytea, internal)
+RETURNS _int4
+AS '$libdir/tsearch2'
+LANGUAGE 'C';
+
+CREATE FUNCTION gtsvector_same(gtsvector, gtsvector, internal)
+RETURNS internal
+AS '$libdir/tsearch2'
+LANGUAGE 'C';
+
+-- CREATE the OPERATOR class
+CREATE OPERATOR CLASS gist_tsvector_ops
+DEFAULT FOR TYPE tsvector USING gist
+AS
+ OPERATOR 1 @@ (tsvector, tsquery) RECHECK ,
+ FUNCTION 1 gtsvector_consistent (gtsvector, internal, int4),
+ FUNCTION 2 gtsvector_union (bytea, internal),
+ FUNCTION 3 gtsvector_compress (internal),
+ FUNCTION 4 gtsvector_decompress (internal),
+ FUNCTION 5 gtsvector_penalty (internal, internal, internal),
+ FUNCTION 6 gtsvector_picksplit (internal, internal),
+ FUNCTION 7 gtsvector_same (gtsvector, gtsvector, internal),
+ STORAGE gtsvector;
+
+
+--stat info
+CREATE TYPE statinfo
+ as (word text, ndoc int4, nentry int4);
+
+--CREATE FUNCTION tsstat_in(cstring)
+--RETURNS tsstat
+--AS '$libdir/tsearch2'
+--LANGUAGE 'C' with (isstrict);
+--
+--CREATE FUNCTION tsstat_out(tsstat)
+--RETURNS cstring
+--AS '$libdir/tsearch2'
+--LANGUAGE 'C' with (isstrict);
+--
+--CREATE TYPE tsstat (
+-- INTERNALLENGTH = -1,
+-- INPUT = tsstat_in,
+-- OUTPUT = tsstat_out,
+-- STORAGE = plain
+--);
+--
+--CREATE FUNCTION ts_accum(tsstat,tsvector)
+--RETURNS tsstat
+--AS '$libdir/tsearch2'
+--LANGUAGE 'C' with (isstrict);
+--
+--CREATE FUNCTION ts_accum_finish(tsstat)
+-- returns setof statinfo
+-- as '$libdir/tsearch2'
+-- language 'C'
+-- with (isstrict);
+--
+--CREATE AGGREGATE stat (
+-- BASETYPE=tsvector,
+-- SFUNC=ts_accum,
+-- STYPE=tsstat,
+-- FINALFUNC = ts_accum_finish,
+-- initcond = ''
+--);
+
+CREATE FUNCTION stat(text)
+ returns setof statinfo
+ as '$libdir/tsearch2', 'ts_stat'
+ language 'C'
+ with (isstrict);
+
+--reset - just for debuging
+CREATE FUNCTION reset_tsearch()
+ returns void
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+--get cover (debug for rank_cd)
+CREATE FUNCTION get_covers(tsvector,tsquery)
+ returns text
+ as '$libdir/tsearch2'
+ language 'C'
+ with (isstrict);
+
+--debug function
+create type tsdebug as (
+ ts_name text,
+ tok_type text,
+ description text,
+ token text,
+ dict_name text[],
+ "tsvector" tsvector
+);
+
+create function _get_parser_from_curcfg()
+returns text as
+' select prs_name from pg_ts_cfg where oid = show_curcfg() '
+language 'SQL' with(isstrict,iscachable);
+
+create function ts_debug(text)
+returns setof tsdebug as '
+select
+ m.ts_name,
+ t.alias as tok_type,
+ t.descr as description,
+ p.token,
+ m.dict_name,
+ strip(to_tsvector(p.token)) as tsvector
+from
+ parse( _get_parser_from_curcfg(), $1 ) as p,
+ token_type() as t,
+ pg_ts_cfgmap as m,
+ pg_ts_cfg as c
+where
+ t.tokid=p.tokid and
+ t.alias = m.tok_alias and
+ m.ts_name=c.ts_name and
+ c.oid=show_curcfg()
+' language 'SQL' with(isstrict);
+"""
+
+def setup(cursor):
+ sql = '\n'.join([line for line in tsearch_sql.split('\n')
+ if not line.startswith('--')])
+ for query in sql.split(';'):
+ if query.strip():
+ cursor.execute(query)
Added: tracker/vendor/roundup/current/roundup/cgi/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,3 @@
+*.pyc
+*.pyo
+*.cover
Added: tracker/vendor/roundup/current/roundup/cgi/MultiMapping.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/MultiMapping.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,63 @@
+class MultiMapping:
+ def __init__(self, *stores):
+ self.stores = list(stores)
+ self.stores.reverse()
+
+ def __getitem__(self, key):
+ for store in self.stores:
+ if store.has_key(key):
+ return store[key]
+ raise KeyError, key
+
+ def __setitem__(self, key, val):
+ self.stores[0][key] = val
+
+ _marker = []
+
+ def get(self, key, default=_marker):
+ for store in self.stores:
+ if store.has_key(key):
+ return store[key]
+ if default is self._marker:
+ raise KeyError, key
+ return default
+
+ def __len__(self):
+ return len(self.items())
+
+ def has_key(self, key):
+ for store in self.stores:
+ if store.has_key(key):
+ return 1
+ return 0
+
+ def push(self, store):
+ self.stores = [store] + self.stores
+
+ def pop(self):
+ if not len(self.stores):
+ return None
+ store, self.stores = self.stores[0], self.stores[1:]
+ return store
+
+ def keys(self):
+ return [ _[0] for _ in self.items() ]
+
+ def values(self):
+ return [ _[1] for _ in self.items() ]
+
+ def copy(self):
+ copy = MultiMapping()
+ copy.stores = [_.copy() for _ in self.stores]
+ return copy
+
+ def items(self):
+ l = []
+ seen = {}
+ for store in self.stores:
+ for k, v in store.items():
+ if not seen.has_key(k):
+ l.append((k, v))
+ seen[k] = 1
+ return l
+
Added: tracker/vendor/roundup/current/roundup/cgi/PageTemplates/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/PageTemplates/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2 @@
+*.pyc
+*.pyo
Added: tracker/vendor/roundup/current/roundup/cgi/PageTemplates/Expressions.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/PageTemplates/Expressions.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,343 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+# Modified for Roundup:
+#
+# 1. removed all Zope-specific code (doesn't even try to import that stuff now)
+# 2. removed all Acquisition
+# 3. removed blocking of leading-underscore URL components
+
+"""Page Template Expression Engine
+
+Page Template-specific implementation of TALES, with handlers
+for Python expressions, string literals, and paths.
+"""
+
+__version__='$Revision: 1.12 $'[11:-2]
+
+import re, sys
+from TALES import Engine, CompilerError, _valid_name, NAME_RE, \
+ Undefined, Default, _parse_expr
+
+
+_engine = None
+def getEngine():
+ global _engine
+ if _engine is None:
+ from PathIterator import Iterator
+ _engine = Engine(Iterator)
+ installHandlers(_engine)
+ return _engine
+
+def installHandlers(engine):
+ reg = engine.registerType
+ pe = PathExpr
+ for pt in ('standard', 'path', 'exists', 'nocall'):
+ reg(pt, pe)
+ reg('string', StringExpr)
+ reg('python', PythonExpr)
+ reg('not', NotExpr)
+ reg('defer', DeferExpr)
+
+from PythonExpr import getSecurityManager, PythonExpr
+guarded_getattr = getattr
+try:
+ from zExceptions import Unauthorized
+except ImportError:
+ Unauthorized = "Unauthorized"
+
+def acquisition_security_filter(orig, inst, name, v, real_validate):
+ if real_validate(orig, inst, name, v):
+ return 1
+ raise Unauthorized, name
+
+def call_with_ns(f, ns, arg=1):
+ if arg==2:
+ return f(None, ns)
+ else:
+ return f(ns)
+
+class _SecureModuleImporter:
+ """Simple version of the importer for use with trusted code."""
+ __allow_access_to_unprotected_subobjects__ = 1
+ def __getitem__(self, module):
+ __import__(module)
+ return sys.modules[module]
+
+SecureModuleImporter = _SecureModuleImporter()
+
+Undefs = (Undefined, AttributeError, KeyError,
+ TypeError, IndexError, Unauthorized)
+
+def render(ob, ns):
+ """
+ Calls the object, possibly a document template, or just returns it if
+ not callable. (From DT_Util.py)
+ """
+ if hasattr(ob, '__render_with_namespace__'):
+ ob = call_with_ns(ob.__render_with_namespace__, ns)
+ else:
+ base = ob
+ if callable(base):
+ try:
+ if getattr(base, 'isDocTemp', 0):
+ ob = call_with_ns(ob, ns, 2)
+ else:
+ ob = ob()
+ except AttributeError, n:
+ if str(n) != '__call__':
+ raise
+ return ob
+
+class SubPathExpr:
+ def __init__(self, path):
+ self._path = path = path.strip().split('/')
+ self._base = base = path.pop(0)
+ if base and not _valid_name(base):
+ raise CompilerError, 'Invalid variable name "%s"' % base
+ # Parse path
+ self._dp = dp = []
+ for i in range(len(path)):
+ e = path[i]
+ if e[:1] == '?' and _valid_name(e[1:]):
+ dp.append((i, e[1:]))
+ dp.reverse()
+
+ def _eval(self, econtext,
+ list=list, isinstance=isinstance, StringType=type('')):
+ vars = econtext.vars
+ path = self._path
+ if self._dp:
+ path = list(path) # Copy!
+ for i, varname in self._dp:
+ val = vars[varname]
+ if isinstance(val, StringType):
+ path[i] = val
+ else:
+ # If the value isn't a string, assume it's a sequence
+ # of path names.
+ path[i:i+1] = list(val)
+ base = self._base
+ __traceback_info__ = 'path expression "%s"'%('/'.join(self._path))
+ if base == 'CONTEXTS' or not base:
+ ob = econtext.contexts
+ else:
+ ob = vars[base]
+ if isinstance(ob, DeferWrapper):
+ ob = ob()
+ if path:
+ ob = restrictedTraverse(ob, path, getSecurityManager())
+ return ob
+
+class PathExpr:
+ def __init__(self, name, expr, engine):
+ self._s = expr
+ self._name = name
+ self._hybrid = 0
+ paths = expr.split('|')
+ self._subexprs = []
+ add = self._subexprs.append
+ for i in range(len(paths)):
+ path = paths[i].lstrip()
+ if _parse_expr(path):
+ # This part is the start of another expression type,
+ # so glue it back together and compile it.
+ add(engine.compile(('|'.join(paths[i:]).lstrip())))
+ self._hybrid = 1
+ break
+ add(SubPathExpr(path)._eval)
+
+ def _exists(self, econtext):
+ for expr in self._subexprs:
+ try:
+ expr(econtext)
+ except Undefs:
+ pass
+ else:
+ return 1
+ return 0
+
+ def _eval(self, econtext,
+ isinstance=isinstance, StringType=type(''), render=render):
+ for expr in self._subexprs[:-1]:
+ # Try all but the last subexpression, skipping undefined ones.
+ try:
+ ob = expr(econtext)
+ except Undefs:
+ pass
+ else:
+ break
+ else:
+ # On the last subexpression allow exceptions through, and
+ # don't autocall if the expression was not a subpath.
+ ob = self._subexprs[-1](econtext)
+ if self._hybrid:
+ return ob
+
+ if self._name == 'nocall' or isinstance(ob, StringType):
+ return ob
+ # Return the rendered object
+ return render(ob, econtext.vars)
+
+ def __call__(self, econtext):
+ if self._name == 'exists':
+ return self._exists(econtext)
+ return self._eval(econtext)
+
+ def __str__(self):
+ return '%s expression %s' % (self._name, `self._s`)
+
+ def __repr__(self):
+ return '%s:%s' % (self._name, `self._s`)
+
+
+_interp = re.compile(r'\$(%(n)s)|\${(%(n)s(?:/[^}]*)*)}' % {'n': NAME_RE})
+
+class StringExpr:
+ def __init__(self, name, expr, engine):
+ self._s = expr
+ if '%' in expr:
+ expr = expr.replace('%', '%%')
+ self._vars = vars = []
+ if '$' in expr:
+ parts = []
+ for exp in expr.split('$$'):
+ if parts: parts.append('$')
+ m = _interp.search(exp)
+ while m is not None:
+ parts.append(exp[:m.start()])
+ parts.append('%s')
+ vars.append(PathExpr('path', m.group(1) or m.group(2),
+ engine))
+ exp = exp[m.end():]
+ m = _interp.search(exp)
+ if '$' in exp:
+ raise CompilerError, (
+ '$ must be doubled or followed by a simple path')
+ parts.append(exp)
+ expr = ''.join(parts)
+ self._expr = expr
+
+ def __call__(self, econtext):
+ vvals = []
+ for var in self._vars:
+ v = var(econtext)
+ # I hope this isn't in use anymore.
+ ## if isinstance(v, Exception):
+ ## raise v
+ vvals.append(v)
+ return self._expr % tuple(vvals)
+
+ def __str__(self):
+ return 'string expression %s' % `self._s`
+
+ def __repr__(self):
+ return 'string:%s' % `self._s`
+
+class NotExpr:
+ def __init__(self, name, expr, compiler):
+ self._s = expr = expr.lstrip()
+ self._c = compiler.compile(expr)
+
+ def __call__(self, econtext):
+ # We use the (not x) and 1 or 0 formulation to avoid changing
+ # the representation of the result in Python 2.3, where the
+ # result of "not" becomes an instance of bool.
+ return (not econtext.evaluateBoolean(self._c)) and 1 or 0
+
+ def __repr__(self):
+ return 'not:%s' % `self._s`
+
+class DeferWrapper:
+ def __init__(self, expr, econtext):
+ self._expr = expr
+ self._econtext = econtext
+
+ def __str__(self):
+ return str(self())
+
+ def __call__(self):
+ return self._expr(self._econtext)
+
+class DeferExpr:
+ def __init__(self, name, expr, compiler):
+ self._s = expr = expr.lstrip()
+ self._c = compiler.compile(expr)
+
+ def __call__(self, econtext):
+ return DeferWrapper(self._c, econtext)
+
+ def __repr__(self):
+ return 'defer:%s' % `self._s`
+
+class TraversalError:
+ def __init__(self, path, name):
+ self.path = path
+ self.name = name
+
+
+
+def restrictedTraverse(object, path, securityManager,
+ get=getattr, has=hasattr, N=None, M=[],
+ TupleType=type(()) ):
+
+ REQUEST = {'path': path}
+ REQUEST['TraversalRequestNameStack'] = path = path[:] # Copy!
+ path.reverse()
+ validate = securityManager.validate
+ __traceback_info__ = REQUEST
+ done = []
+ while path:
+ name = path.pop()
+ __traceback_info__ = TraversalError(done, name)
+
+ if isinstance(name, TupleType):
+ object = object(*name)
+ continue
+
+ if not name:
+ # Skip directly to item access
+ o = object[name]
+ # Check access to the item.
+ if not validate(object, object, name, o):
+ raise Unauthorized, name
+ object = o
+ continue
+
+ # Try an attribute.
+ o = guarded_getattr(object, name, M)
+ if o is M:
+ # Try an item.
+ try:
+ # XXX maybe in Python 2.2 we can just check whether
+ # the object has the attribute "__getitem__"
+ # instead of blindly catching exceptions.
+ o = object[name]
+ except AttributeError, exc:
+ if str(exc).find('__getitem__') >= 0:
+ # The object does not support the item interface.
+ # Try to re-raise the original attribute error.
+ # XXX I think this only happens with
+ # ExtensionClass instances.
+ guarded_getattr(object, name)
+ raise
+ except TypeError, exc:
+ if str(exc).find('unsubscriptable') >= 0:
+ # The object does not support the item interface.
+ # Try to re-raise the original attribute error.
+ # XXX This is sooooo ugly.
+ guarded_getattr(object, name)
+ raise
+ done.append((name, o))
+ object = o
+
+ return object
Added: tracker/vendor/roundup/current/roundup/cgi/PageTemplates/GlobalTranslationService.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/PageTemplates/GlobalTranslationService.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,49 @@
+##############################################################################
+#
+# Copyright (c) 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+# Modifications for Roundup:
+# 1. implemented ustr as str
+# 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 $
+"""
+
+import re
+
+from roundup.cgi.TAL.TALDefs import NAME_RE
+
+ustr = str
+
+class DummyTranslationService:
+ """Translation service that doesn't know anything about translation."""
+ def translate(self, domain, msgid, mapping=None,
+ context=None, target_language=None, default=None):
+ def repl(m, mapping=mapping):
+ return ustr(mapping[m.group(m.lastindex)])
+ cre = re.compile(r'\$(?:(%s)|\{(%s)\})' % (NAME_RE, NAME_RE))
+ return cre.sub(repl, default or msgid)
+ # XXX Not all of Zope.I18n.ITranslationService is implemented.
+
+translationService = DummyTranslationService()
+
+def setGlobalTranslationService(service):
+ """Sets the global translation service, and returns the previous one."""
+ global translationService
+ old_service = translationService
+ translationService = service
+ return old_service
+
+def getGlobalTranslationService():
+ """Returns the global translation service."""
+ return translationService
Added: tracker/vendor/roundup/current/roundup/cgi/PageTemplates/MultiMapping.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/PageTemplates/MultiMapping.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,29 @@
+import operator
+
+class MultiMapping:
+ def __init__(self, *stores):
+ self.stores = list(stores)
+ def __getitem__(self, key):
+ for store in self.stores:
+ if store.has_key(key):
+ return store[key]
+ raise KeyError, key
+ _marker = []
+ def get(self, key, default=_marker):
+ for store in self.stores:
+ if store.has_key(key):
+ return store[key]
+ if default is self._marker:
+ raise KeyError, key
+ return default
+ def __len__(self):
+ return reduce(operator.add, [len(x) for x in self.stores], 0)
+ def push(self, store):
+ self.stores.append(store)
+ def pop(self):
+ return self.stores.pop()
+ def items(self):
+ l = []
+ for store in self.stores:
+ l = l + store.items()
+ return l
Added: tracker/vendor/roundup/current/roundup/cgi/PageTemplates/PageTemplate.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/PageTemplates/PageTemplate.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,218 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+# Modified for Roundup:
+#
+# 1. changed imports to import from roundup.cgi
+# 2. removed use of ExtensionClass
+# 3. removed use of ComputedAttribute
+"""Page Template module
+
+HTML- and XML-based template objects using TAL, TALES, and METAL.
+"""
+
+__version__='$Revision: 1.5 $'[11:-2]
+
+import sys
+
+from roundup.cgi.TAL.TALParser import TALParser
+from roundup.cgi.TAL.HTMLTALParser import HTMLTALParser
+from roundup.cgi.TAL.TALGenerator import TALGenerator
+# Do not use cStringIO here! It's not unicode aware. :(
+from roundup.cgi.TAL.TALInterpreter import TALInterpreter, FasterStringIO
+from Expressions import getEngine
+
+
+class PageTemplate:
+ "Page Templates using TAL, TALES, and METAL"
+
+ content_type = 'text/html'
+ expand = 0
+ _v_errors = ()
+ _v_warnings = ()
+ _v_program = None
+ _v_macros = None
+ _v_cooked = 0
+ id = '(unknown)'
+ _text = ''
+ _error_start = '<!-- Page Template Diagnostics'
+
+ def StringIO(self):
+ # Third-party products wishing to provide a full Unicode-aware
+ # StringIO can do so by monkey-patching this method.
+ return FasterStringIO()
+
+ def pt_edit(self, text, content_type):
+ if content_type:
+ self.content_type = str(content_type)
+ if hasattr(text, 'read'):
+ text = text.read()
+ self.write(text)
+
+ def pt_getContext(self):
+ c = {'template': self,
+ 'options': {},
+ 'nothing': None,
+ 'request': None,
+ 'modules': ModuleImporter,
+ }
+ parent = getattr(self, 'aq_parent', None)
+ if parent is not None:
+ c['here'] = parent
+ c['container'] = self.aq_inner.aq_parent
+ while parent is not None:
+ self = parent
+ parent = getattr(self, 'aq_parent', None)
+ c['root'] = self
+ return c
+
+ def pt_render(self, source=0, extra_context={}):
+ """Render this Page Template"""
+ if not self._v_cooked:
+ self._cook()
+
+ __traceback_supplement__ = (PageTemplateTracebackSupplement, self)
+
+ if self._v_errors:
+ raise PTRuntimeError, 'Page Template %s has errors.' % self.id
+ output = self.StringIO()
+ c = self.pt_getContext()
+ c.update(extra_context)
+
+ TALInterpreter(self._v_program, self._v_macros,
+ getEngine().getContext(c),
+ output,
+ tal=not source, strictinsert=0)()
+ return output.getvalue()
+
+ def __call__(self, *args, **kwargs):
+ if not kwargs.has_key('args'):
+ kwargs['args'] = args
+ return self.pt_render(extra_context={'options': kwargs})
+
+ def pt_errors(self):
+ if not self._v_cooked:
+ self._cook()
+ err = self._v_errors
+ if err:
+ return err
+ if not self.expand: return
+ try:
+ self.pt_render(source=1)
+ except:
+ return ('Macro expansion failed', '%s: %s' % sys.exc_info()[:2])
+
+ def pt_warnings(self):
+ if not self._v_cooked:
+ self._cook()
+ return self._v_warnings
+
+ def pt_macros(self):
+ if not self._v_cooked:
+ self._cook()
+ __traceback_supplement__ = (PageTemplateTracebackSupplement, self)
+ if self._v_errors:
+ raise PTRuntimeError, 'Page Template %s has errors.' % self.id
+ return self._v_macros
+
+ def __getattr__(self, name):
+ if name == 'macros':
+ return self.pt_macros()
+ raise AttributeError, name
+
+ def pt_source_file(self):
+ return None # Unknown.
+
+ def write(self, text):
+ assert type(text) is type('')
+ if text[:len(self._error_start)] == self._error_start:
+ errend = text.find('-->')
+ if errend >= 0:
+ text = text[errend + 4:]
+ if self._text != text:
+ self._text = text
+ self._cook()
+
+ def read(self):
+ self._cook_check()
+ if not self._v_errors:
+ if not self.expand:
+ return self._text
+ try:
+ return self.pt_render(source=1)
+ except:
+ return ('%s\n Macro expansion failed\n %s\n-->\n%s' %
+ (self._error_start, "%s: %s" % sys.exc_info()[:2],
+ self._text) )
+
+ return ('%s\n %s\n-->\n%s' % (self._error_start,
+ '\n '.join(self._v_errors),
+ self._text))
+
+ def _cook_check(self):
+ if not self._v_cooked:
+ self._cook()
+
+ def _cook(self):
+ """Compile the TAL and METAL statments.
+
+ Cooking must not fail due to compilation errors in templates.
+ """
+ source_file = self.pt_source_file()
+ if self.html():
+ gen = TALGenerator(getEngine(), xml=0, source_file=source_file)
+ parser = HTMLTALParser(gen)
+ else:
+ gen = TALGenerator(getEngine(), source_file=source_file)
+ parser = TALParser(gen)
+
+ self._v_errors = ()
+ try:
+ parser.parseString(self._text)
+ self._v_program, self._v_macros = parser.getCode()
+ except:
+ self._v_errors = ["Compilation failed",
+ "%s: %s" % sys.exc_info()[:2]]
+ self._v_warnings = parser.getWarnings()
+ self._v_cooked = 1
+
+ def html(self):
+ if not hasattr(getattr(self, 'aq_base', self), 'is_html'):
+ return self.content_type == 'text/html'
+ return self.is_html
+
+class _ModuleImporter:
+ def __getitem__(self, module):
+ mod = __import__(module)
+ path = module.split('.')
+ for name in path[1:]:
+ mod = getattr(mod, name)
+ return mod
+
+ModuleImporter = _ModuleImporter()
+
+class PTRuntimeError(RuntimeError):
+ '''The Page Template has template errors that prevent it from rendering.'''
+ pass
+
+
+class PageTemplateTracebackSupplement:
+ #__implements__ = ITracebackSupplement
+
+ def __init__(self, pt):
+ self.object = pt
+ w = pt.pt_warnings()
+ e = pt.pt_errors()
+ if e:
+ w = list(w) + list(e)
+ self.warnings = w
+
Added: tracker/vendor/roundup/current/roundup/cgi/PageTemplates/PathIterator.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/PageTemplates/PathIterator.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,46 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+"""Path Iterator
+
+A TALES Iterator with the ability to use first() and last() on
+subpaths of elements.
+"""
+
+__version__='$Revision: 1.3 $'[11:-2]
+
+import TALES
+from Expressions import restrictedTraverse, Undefs, getSecurityManager
+
+class Iterator(TALES.Iterator):
+ def __bobo_traverse__(self, REQUEST, name):
+ if name in ('first', 'last'):
+ path = REQUEST['TraversalRequestNameStack']
+ names = list(path)
+ names.reverse()
+ path[:] = [tuple(names)]
+ return getattr(self, name)
+
+ def same_part(self, name, ob1, ob2):
+ if name is None:
+ return ob1 == ob2
+ if isinstance(name, type('')):
+ name = name.split('/')
+ name = filter(None, name)
+ securityManager = getSecurityManager()
+ try:
+ ob1 = restrictedTraverse(ob1, name, securityManager)
+ ob2 = restrictedTraverse(ob2, name, securityManager)
+ except Undefs:
+ return 0
+ return ob1 == ob2
Added: tracker/vendor/roundup/current/roundup/cgi/PageTemplates/PythonExpr.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/PageTemplates/PythonExpr.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,85 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+# Modified for Roundup:
+#
+# 1. more informative traceback info
+
+"""Generic Python Expression Handler
+"""
+
+__version__='$Revision: 1.6 $'[11:-2]
+
+from TALES import CompilerError
+from sys import exc_info
+
+class getSecurityManager:
+ '''Null security manager'''
+ def validate(self, *args, **kwargs):
+ return 1
+ addContext = removeContext = validateValue = validate
+
+class PythonExpr:
+ def __init__(self, name, expr, engine):
+ self.expr = expr = expr.strip().replace('\n', ' ')
+ try:
+ d = {}
+ exec 'def f():\n return %s\n' % expr.strip() in d
+ self._f = d['f']
+ except:
+ raise CompilerError, ('Python expression error:\n'
+ '%s: %s') % exc_info()[:2]
+ self._get_used_names()
+
+ def _get_used_names(self):
+ self._f_varnames = vnames = []
+ for vname in self._f.func_code.co_names:
+ if vname[0] not in '$_':
+ vnames.append(vname)
+
+ def _bind_used_names(self, econtext, _marker=[]):
+ # Bind template variables
+ names = {'CONTEXTS': econtext.contexts}
+ vars = econtext.vars
+ getType = econtext.getCompiler().getTypes().get
+ for vname in self._f_varnames:
+ val = vars.get(vname, _marker)
+ if val is _marker:
+ has = val = getType(vname)
+ if has:
+ val = ExprTypeProxy(vname, val, econtext)
+ names[vname] = val
+ else:
+ names[vname] = val
+ return names
+
+ def __call__(self, econtext):
+ __traceback_info__ = 'python expression "%s"'%self.expr
+ f = self._f
+ f.func_globals.update(self._bind_used_names(econtext))
+ return f()
+
+ def __str__(self):
+ return 'Python expression "%s"' % self.expr
+ def __repr__(self):
+ return '<PythonExpr %s>' % self.expr
+
+class ExprTypeProxy:
+ '''Class that proxies access to an expression type handler'''
+ def __init__(self, name, handler, econtext):
+ self._name = name
+ self._handler = handler
+ self._econtext = econtext
+ def __call__(self, text):
+ return self._handler(self._name, text,
+ self._econtext.getCompiler())(self._econtext)
+
Added: tracker/vendor/roundup/current/roundup/cgi/PageTemplates/README.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/PageTemplates/README.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,7 @@
+See <a href="http://dev.zope.org/Wikis/DevSite/Projects/ZPT">the
+ZPT project Wiki</a> for more information about Page Templates, or
+<a href="http://www.zope.org/Members/4am/ZPT">the download page</a>
+for installation instructions and the most recent version of the software.
+
+This Product requires the TAL and ZTUtils packages to be installed in
+your Python path (not Products). See the links above for more information.
Added: tracker/vendor/roundup/current/roundup/cgi/PageTemplates/TALES.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/PageTemplates/TALES.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,300 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+# Modified for Roundup:
+#
+# 1. changed imports to import from roundup.cgi
+# 2. implemented ustr as str (removes import from DocumentTemplate)
+# 3. removed import and use of Unauthorized from zExceptions
+"""TALES
+
+An implementation of a generic TALES engine
+"""
+
+__version__='$Revision: 1.9 $'[11:-2]
+
+import re, sys
+from roundup.cgi import ZTUtils
+from weakref import ref
+from MultiMapping import MultiMapping
+from GlobalTranslationService import getGlobalTranslationService
+
+ustr = str
+
+StringType = type('')
+
+NAME_RE = r"[a-zA-Z][a-zA-Z0-9_]*"
+_parse_expr = re.compile(r"(%s):" % NAME_RE).match
+_valid_name = re.compile('%s$' % NAME_RE).match
+
+class TALESError(Exception):
+ """Error during TALES expression evaluation"""
+
+class Undefined(TALESError):
+ '''Exception raised on traversal of an undefined path'''
+
+class RegistrationError(Exception):
+ '''TALES Type Registration Error'''
+
+class CompilerError(Exception):
+ '''TALES Compiler Error'''
+
+class Default:
+ '''Retain Default'''
+Default = Default()
+
+class SafeMapping(MultiMapping):
+ '''Mapping with security declarations and limited method exposure.
+
+ Since it subclasses MultiMapping, this class can be used to wrap
+ one or more mapping objects. Restricted Python code will not be
+ able to mutate the SafeMapping or the wrapped mappings, but will be
+ able to read any value.
+ '''
+ __allow_access_to_unprotected_subobjects__ = 1
+ push = pop = None
+
+ _push = MultiMapping.push
+ _pop = MultiMapping.pop
+
+
+class Iterator(ZTUtils.Iterator):
+ def __init__(self, name, seq, context):
+ ZTUtils.Iterator.__init__(self, seq)
+ self.name = name
+ self._context_ref = ref(context)
+
+ def next(self):
+ if ZTUtils.Iterator.next(self):
+ context = self._context_ref()
+ if context is not None:
+ context.setLocal(self.name, self.item)
+ return 1
+ return 0
+
+
+class ErrorInfo:
+ """Information about an exception passed to an on-error handler."""
+ __allow_access_to_unprotected_subobjects__ = 1
+
+ def __init__(self, err, position=(None, None)):
+ if isinstance(err, Exception):
+ self.type = err.__class__
+ self.value = err
+ else:
+ self.type = err
+ self.value = None
+ self.lineno = position[0]
+ self.offset = position[1]
+
+
+class Engine:
+ '''Expression Engine
+
+ An instance of this class keeps a mutable collection of expression
+ type handlers. It can compile expression strings by delegating to
+ these handlers. It can provide an expression Context, which is
+ capable of holding state and evaluating compiled expressions.
+ '''
+ Iterator = Iterator
+
+ def __init__(self, Iterator=None):
+ self.types = {}
+ if Iterator is not None:
+ self.Iterator = Iterator
+
+ def registerType(self, name, handler):
+ if not _valid_name(name):
+ raise RegistrationError, 'Invalid Expression type "%s".' % name
+ types = self.types
+ if types.has_key(name):
+ raise RegistrationError, (
+ 'Multiple registrations for Expression type "%s".' %
+ name)
+ types[name] = handler
+
+ def getTypes(self):
+ return self.types
+
+ def compile(self, expression):
+ m = _parse_expr(expression)
+ if m:
+ type = m.group(1)
+ expr = expression[m.end():]
+ else:
+ type = "standard"
+ expr = expression
+ try:
+ handler = self.types[type]
+ except KeyError:
+ raise CompilerError, (
+ 'Unrecognized expression type "%s".' % type)
+ return handler(type, expr, self)
+
+ def getContext(self, contexts=None, **kwcontexts):
+ if contexts is not None:
+ if kwcontexts:
+ kwcontexts.update(contexts)
+ else:
+ kwcontexts = contexts
+ return Context(self, kwcontexts)
+
+ def getCompilerError(self):
+ return CompilerError
+
+class Context:
+ '''Expression Context
+
+ An instance of this class holds context information that it can
+ use to evaluate compiled expressions.
+ '''
+
+ _context_class = SafeMapping
+ position = (None, None)
+ source_file = None
+
+ def __init__(self, compiler, contexts):
+ self._compiler = compiler
+ self.contexts = contexts
+ contexts['nothing'] = None
+ contexts['default'] = Default
+
+ self.repeat_vars = rv = {}
+ # Wrap this, as it is visible to restricted code
+ contexts['repeat'] = rep = self._context_class(rv)
+ contexts['loop'] = rep # alias
+
+ self.global_vars = gv = contexts.copy()
+ self.local_vars = lv = {}
+ self.vars = self._context_class(gv, lv)
+
+ # Keep track of what needs to be popped as each scope ends.
+ self._scope_stack = []
+
+ def getCompiler(self):
+ return self._compiler
+
+ def beginScope(self):
+ self._scope_stack.append([self.local_vars.copy()])
+
+ def endScope(self):
+ scope = self._scope_stack.pop()
+ self.local_vars = lv = scope[0]
+ v = self.vars
+ v._pop()
+ v._push(lv)
+ # Pop repeat variables, if any
+ i = len(scope) - 1
+ while i:
+ name, value = scope[i]
+ if value is None:
+ del self.repeat_vars[name]
+ else:
+ self.repeat_vars[name] = value
+ i = i - 1
+
+ def setLocal(self, name, value):
+ self.local_vars[name] = value
+
+ def setGlobal(self, name, value):
+ self.global_vars[name] = value
+
+ def setRepeat(self, name, expr):
+ expr = self.evaluate(expr)
+ if not expr:
+ return self._compiler.Iterator(name, (), self)
+ it = self._compiler.Iterator(name, expr, self)
+ old_value = self.repeat_vars.get(name)
+ self._scope_stack[-1].append((name, old_value))
+ self.repeat_vars[name] = it
+ return it
+
+ def evaluate(self, expression,
+ isinstance=isinstance, StringType=StringType):
+ if isinstance(expression, StringType):
+ expression = self._compiler.compile(expression)
+ __traceback_supplement__ = (
+ TALESTracebackSupplement, self, expression)
+ return expression(self)
+
+ evaluateValue = evaluate
+ evaluateBoolean = evaluate
+
+ def evaluateText(self, expr):
+ text = self.evaluate(expr)
+ if text is Default or text is None:
+ return text
+ if isinstance(text, unicode):
+ return text
+ else:
+ return ustr(text)
+
+ def evaluateStructure(self, expr):
+ return self.evaluate(expr)
+ evaluateStructure = evaluate
+
+ def evaluateMacro(self, expr):
+ # XXX Should return None or a macro definition
+ return self.evaluate(expr)
+ evaluateMacro = evaluate
+
+ def createErrorInfo(self, err, position):
+ return ErrorInfo(err, position)
+
+ def getDefault(self):
+ return Default
+
+ def setSourceFile(self, source_file):
+ self.source_file = source_file
+
+ def setPosition(self, position):
+ self.position = position
+
+ def translate(self, domain, msgid, mapping=None,
+ context=None, target_language=None, default=None):
+ if context is None:
+ context = self.contexts.get('here')
+ return getGlobalTranslationService().translate(
+ domain, msgid, mapping=mapping,
+ context=context,
+ default=default,
+ target_language=target_language)
+
+class TALESTracebackSupplement:
+ """Implementation of ITracebackSupplement"""
+ def __init__(self, context, expression):
+ self.context = context
+ self.source_url = context.source_file
+ self.line = context.position[0]
+ self.column = context.position[1]
+ self.expression = repr(expression)
+
+ def getInfo(self, as_html=0):
+ import pprint
+ data = self.context.contexts.copy()
+ s = pprint.pformat(data)
+ if not as_html:
+ return ' - Names:\n %s' % s.replace('\n', '\n ')
+ else:
+ from cgi import escape
+ return '<b>Names:</b><pre>%s</pre>' % (escape(s))
+
+
+class SimpleExpr:
+ '''Simple example of an expression type handler'''
+ def __init__(self, name, expr, engine):
+ self._name = name
+ self._expr = expr
+ def __call__(self, econtext):
+ return self._name, self._expr
+ def __repr__(self):
+ return '<SimpleExpr %s %s>' % (self._name, `self._expr`)
Added: tracker/vendor/roundup/current/roundup/cgi/PageTemplates/__init__.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/PageTemplates/__init__.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,28 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+__doc__='''Package wrapper for Page Templates
+
+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 $'''
+__version__='$$'[11:-2]
+
+
+# Placeholder for Zope Product data
+misc_ = {}
+
+def initialize(context):
+ # Import lazily, and defer initialization to the module
+ import ZopePageTemplate
+ ZopePageTemplate.initialize(context)
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,3 @@
+.path
+*.pyc
+*.pyo
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/DummyEngine.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/DummyEngine.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,274 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+# Modifications for Roundup:
+# 1. commented out ITALES references
+# 2. implemented ustr as str
+"""
+Dummy TALES engine so that I can test out the TAL implementation.
+"""
+
+import re
+import sys
+
+from TALDefs import NAME_RE, TALESError, ErrorInfo
+#from ITALES import ITALESCompiler, ITALESEngine
+#from DocumentTemplate.DT_Util import ustr
+ustr = str
+
+IDomain = None
+if sys.modules.has_key('Zope'):
+ try:
+ from Zope.I18n.ITranslationService import ITranslationService
+ from Zope.I18n.IDomain import IDomain
+ except ImportError:
+ pass
+if IDomain is None:
+ # Before 2.7, or not in Zope
+ class ITranslationService: pass
+ class IDomain: pass
+
+class _Default:
+ pass
+Default = _Default()
+
+name_match = re.compile(r"(?s)(%s):(.*)\Z" % NAME_RE).match
+
+class CompilerError(Exception):
+ pass
+
+class DummyEngine:
+
+ position = None
+ source_file = None
+
+ #__implements__ = ITALESCompiler, ITALESEngine
+
+ def __init__(self, macros=None):
+ if macros is None:
+ macros = {}
+ self.macros = macros
+ dict = {'nothing': None, 'default': Default}
+ self.locals = self.globals = dict
+ self.stack = [dict]
+ self.translationService = DummyTranslationService()
+
+ def getCompilerError(self):
+ return CompilerError
+
+ def getCompiler(self):
+ return self
+
+ def setSourceFile(self, source_file):
+ self.source_file = source_file
+
+ def setPosition(self, position):
+ self.position = position
+
+ def compile(self, expr):
+ return "$%s$" % expr
+
+ def uncompile(self, expression):
+ assert (expression.startswith("$") and expression.endswith("$"),
+ expression)
+ return expression[1:-1]
+
+ def beginScope(self):
+ self.stack.append(self.locals)
+
+ def endScope(self):
+ assert len(self.stack) > 1, "more endScope() than beginScope() calls"
+ self.locals = self.stack.pop()
+
+ def setLocal(self, name, value):
+ if self.locals is self.stack[-1]:
+ # Unmerge this scope's locals from previous scope of first set
+ self.locals = self.locals.copy()
+ self.locals[name] = value
+
+ def setGlobal(self, name, value):
+ self.globals[name] = value
+
+ def evaluate(self, expression):
+ assert (expression.startswith("$") and expression.endswith("$"),
+ expression)
+ expression = expression[1:-1]
+ m = name_match(expression)
+ if m:
+ type, expr = m.group(1, 2)
+ else:
+ type = "path"
+ expr = expression
+ if type in ("string", "str"):
+ return expr
+ if type in ("path", "var", "global", "local"):
+ return self.evaluatePathOrVar(expr)
+ if type == "not":
+ return not self.evaluate(expr)
+ if type == "exists":
+ return self.locals.has_key(expr) or self.globals.has_key(expr)
+ if type == "python":
+ try:
+ return eval(expr, self.globals, self.locals)
+ except:
+ raise TALESError("evaluation error in %s" % `expr`)
+ if type == "position":
+ # Insert the current source file name, line number,
+ # and column offset.
+ if self.position:
+ lineno, offset = self.position
+ else:
+ lineno, offset = None, None
+ return '%s (%s,%s)' % (self.source_file, lineno, offset)
+ raise TALESError("unrecognized expression: " + `expression`)
+
+ def evaluatePathOrVar(self, expr):
+ expr = expr.strip()
+ if self.locals.has_key(expr):
+ return self.locals[expr]
+ elif self.globals.has_key(expr):
+ return self.globals[expr]
+ else:
+ raise TALESError("unknown variable: %s" % `expr`)
+
+ def evaluateValue(self, expr):
+ return self.evaluate(expr)
+
+ def evaluateBoolean(self, expr):
+ return self.evaluate(expr)
+
+ def evaluateText(self, expr):
+ text = self.evaluate(expr)
+ if text is not None and text is not Default:
+ text = ustr(text)
+ return text
+
+ def evaluateStructure(self, expr):
+ # XXX Should return None or a DOM tree
+ return self.evaluate(expr)
+
+ def evaluateSequence(self, expr):
+ # XXX Should return a sequence
+ return self.evaluate(expr)
+
+ def evaluateMacro(self, macroName):
+ assert (macroName.startswith("$") and macroName.endswith("$"),
+ macroName)
+ macroName = macroName[1:-1]
+ file, localName = self.findMacroFile(macroName)
+ if not file:
+ # Local macro
+ macro = self.macros[localName]
+ else:
+ # External macro
+ import driver
+ program, macros = driver.compilefile(file)
+ macro = macros.get(localName)
+ if not macro:
+ raise TALESError("macro %s not found in file %s" %
+ (localName, file))
+ return macro
+
+ def findMacroDocument(self, macroName):
+ file, localName = self.findMacroFile(macroName)
+ if not file:
+ return file, localName
+ import driver
+ doc = driver.parsefile(file)
+ return doc, localName
+
+ def findMacroFile(self, macroName):
+ if not macroName:
+ raise TALESError("empty macro name")
+ i = macroName.rfind('/')
+ if i < 0:
+ # No slash -- must be a locally defined macro
+ return None, macroName
+ else:
+ # Up to last slash is the filename
+ fileName = macroName[:i]
+ localName = macroName[i+1:]
+ return fileName, localName
+
+ def setRepeat(self, name, expr):
+ seq = self.evaluateSequence(expr)
+ return Iterator(name, seq, self)
+
+ def createErrorInfo(self, err, position):
+ return ErrorInfo(err, position)
+
+ def getDefault(self):
+ return Default
+
+ def translate(self, domain, msgid, mapping, default=None):
+ return self.translationService.translate(domain, msgid, mapping,
+ default=default)
+
+
+class Iterator:
+
+ # This is not an implementation of a Python iterator. The next()
+ # method returns true or false to indicate whether another item is
+ # available; if there is another item, the iterator instance calls
+ # setLocal() on the evaluation engine passed to the constructor.
+
+ def __init__(self, name, seq, engine):
+ self.name = name
+ self.seq = seq
+ self.engine = engine
+ self.nextIndex = 0
+
+ def next(self):
+ i = self.nextIndex
+ try:
+ item = self.seq[i]
+ except IndexError:
+ return 0
+ self.nextIndex = i+1
+ self.engine.setLocal(self.name, item)
+ return 1
+
+class DummyDomain:
+ __implements__ = IDomain
+
+ def translate(self, msgid, mapping=None, context=None,
+ target_language=None, default=None):
+ # This is a fake translation service which simply uppercases non
+ # ${name} placeholder text in the message id.
+ #
+ # First, transform a string with ${name} placeholders into a list of
+ # substrings. Then upcase everything but the placeholders, then glue
+ # things back together.
+
+ # simulate an unknown msgid by returning None
+ if msgid == "don't translate me":
+ text = default
+ else:
+ text = msgid.upper()
+
+ def repl(m, mapping=mapping):
+ return ustr(mapping[m.group(m.lastindex).lower()])
+ cre = re.compile(r'\$(?:(%s)|\{(%s)\})' % (NAME_RE, NAME_RE))
+ return cre.sub(repl, text)
+
+class DummyTranslationService:
+ __implements__ = ITranslationService
+
+ def translate(self, domain, msgid, mapping=None, context=None,
+ target_language=None, default=None):
+ return self.getDomain(domain).translate(msgid, mapping, context,
+ target_language,
+ default=default)
+
+ def getDomain(self, domain):
+ return DummyDomain()
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/HTMLParser.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/HTMLParser.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,402 @@
+"""A parser for HTML and XHTML."""
+
+# This file is based on sgmllib.py, but the API is slightly different.
+
+# XXX There should be a way to distinguish between PCDATA (parsed
+# character data -- the normal case), RCDATA (replaceable character
+# data -- only char and entity references and end tags are special)
+# and CDATA (character data -- only end tags are special).
+
+
+import markupbase
+import re
+
+# Regular expressions used for parsing
+
+interesting_normal = re.compile('[&<]')
+interesting_cdata = re.compile(r'<(/|\Z)')
+incomplete = re.compile('&[a-zA-Z#]')
+
+entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]')
+charref = re.compile('&#(?:[0-9]+|[xX][0-9a-fA-F]+)[^0-9a-fA-F]')
+
+starttagopen = re.compile('<[a-zA-Z]')
+piclose = re.compile('>')
+endtagopen = re.compile('</')
+commentclose = re.compile(r'--\s*>')
+tagfind = re.compile('[a-zA-Z][-.a-zA-Z0-9:_]*')
+attrfind = re.compile(
+ r'\s*([a-zA-Z_][-.:a-zA-Z_0-9]*)(\s*=\s*'
+ r'(\'[^\']*\'|"[^"]*"|[-a-zA-Z0-9./:;+*%?!&$\(\)_#=~]*))?')
+
+locatestarttagend = re.compile(r"""
+ <[a-zA-Z][-.a-zA-Z0-9:_]* # tag name
+ (?:\s+ # whitespace before attribute name
+ (?:[a-zA-Z_][-.:a-zA-Z0-9_]* # attribute name
+ (?:\s*=\s* # value indicator
+ (?:'[^']*' # LITA-enclosed value
+ |\"[^\"]*\" # LIT-enclosed value
+ |[^'\">\s]+ # bare value
+ )
+ )?
+ )
+ )*
+ \s* # trailing whitespace
+""", re.VERBOSE)
+endendtag = re.compile('>')
+endtagfind = re.compile('</\s*([a-zA-Z][-.a-zA-Z0-9:_]*)\s*>')
+
+
+class HTMLParseError(Exception):
+ """Exception raised for all parse errors."""
+
+ def __init__(self, msg, position=(None, None)):
+ assert msg
+ self.msg = msg
+ self.lineno = position[0]
+ self.offset = position[1]
+
+ def __str__(self):
+ result = self.msg
+ if self.lineno is not None:
+ result = result + ", at line %d" % self.lineno
+ if self.offset is not None:
+ result = result + ", column %d" % (self.offset + 1)
+ return result
+
+
+def _contains_at(s, sub, pos):
+ return s[pos:pos+len(sub)] == sub
+
+
+class HTMLParser(markupbase.ParserBase):
+ """Find tags and other markup and call handler functions.
+
+ Usage:
+ p = HTMLParser()
+ p.feed(data)
+ ...
+ p.close()
+
+ Start tags are handled by calling self.handle_starttag() or
+ self.handle_startendtag(); end tags by self.handle_endtag(). The
+ data between tags is passed from the parser to the derived class
+ by calling self.handle_data() with the data as argument (the data
+ may be split up in arbitrary chunks). Entity references are
+ passed by calling self.handle_entityref() with the entity
+ reference as the argument. Numeric character references are
+ passed to self.handle_charref() with the string containing the
+ reference as the argument.
+ """
+
+ CDATA_CONTENT_ELEMENTS = ("script", "style")
+
+
+ def __init__(self):
+ """Initialize and reset this instance."""
+ self.reset()
+
+ def reset(self):
+ """Reset this instance. Loses all unprocessed data."""
+ self.rawdata = ''
+ self.stack = []
+ self.lasttag = '???'
+ self.interesting = interesting_normal
+ markupbase.ParserBase.reset(self)
+
+ def feed(self, data):
+ """Feed data to the parser.
+
+ Call this as often as you want, with as little or as much text
+ as you want (may include '\n').
+ """
+ self.rawdata = self.rawdata + data
+ self.goahead(0)
+
+ def close(self):
+ """Handle any buffered data."""
+ self.goahead(1)
+
+ def error(self, message):
+ raise HTMLParseError(message, self.getpos())
+
+ __starttag_text = None
+
+ def get_starttag_text(self):
+ """Return full source of start tag: '<...>'."""
+ return self.__starttag_text
+
+ cdata_endtag = None
+
+ def set_cdata_mode(self, endtag=None):
+ self.cdata_endtag = endtag
+ self.interesting = interesting_cdata
+
+ def clear_cdata_mode(self):
+ self.cdata_endtag = None
+ self.interesting = interesting_normal
+
+ # Internal -- handle data as far as reasonable. May leave state
+ # and data to be processed by a subsequent call. If 'end' is
+ # true, force handling all data as if followed by EOF marker.
+ def goahead(self, end):
+ rawdata = self.rawdata
+ i = 0
+ n = len(rawdata)
+ while i < n:
+ match = self.interesting.search(rawdata, i) # < or &
+ if match:
+ j = match.start()
+ else:
+ j = n
+ if i < j: self.handle_data(rawdata[i:j])
+ i = self.updatepos(i, j)
+ if i == n: break
+ if rawdata[i] == '<':
+ if starttagopen.match(rawdata, i): # < + letter
+ k = self.parse_starttag(i)
+ elif endtagopen.match(rawdata, i): # </
+ k = self.parse_endtag(i)
+ elif _contains_at(rawdata, "<!--", i): # <!--
+ k = self.parse_comment(i)
+ elif _contains_at(rawdata, "<!", i): # <!
+ k = self.parse_declaration(i)
+ elif _contains_at(rawdata, "<?", i): # <?
+ k = self.parse_pi(i)
+ elif _contains_at(rawdata, "<?", i): # <!
+ k = self.parse_declaration(i)
+ elif (i + 1) < n:
+ self.handle_data("<")
+ k = i + 1
+ else:
+ break
+ if k < 0:
+ if end:
+ self.error("EOF in middle of construct")
+ break
+ i = self.updatepos(i, k)
+ elif rawdata[i:i+2] == "&#":
+ match = charref.match(rawdata, i)
+ if match:
+ name = match.group()[2:-1]
+ self.handle_charref(name)
+ k = match.end()
+ if rawdata[k-1] != ';':
+ k = k - 1
+ i = self.updatepos(i, k)
+ continue
+ else:
+ break
+ elif rawdata[i] == '&':
+ match = entityref.match(rawdata, i)
+ if match:
+ name = match.group(1)
+ self.handle_entityref(name)
+ k = match.end()
+ if rawdata[k-1] != ';':
+ k = k - 1
+ i = self.updatepos(i, k)
+ continue
+ match = incomplete.match(rawdata, i)
+ if match:
+ # match.group() will contain at least 2 chars
+ rest = rawdata[i:]
+ if end and match.group() == rest:
+ self.error("EOF in middle of entity or char ref")
+ # incomplete
+ break
+ elif (i + 1) < n:
+ # not the end of the buffer, and can't be confused
+ # with some other construct
+ self.handle_data("&")
+ i = self.updatepos(i, i + 1)
+ else:
+ break
+ else:
+ assert 0, "interesting.search() lied"
+ # end while
+ if end and i < n:
+ self.handle_data(rawdata[i:n])
+ i = self.updatepos(i, n)
+ self.rawdata = rawdata[i:]
+
+ # Internal -- parse comment, return end or -1 if not terminated
+ def parse_comment(self, i, report=1):
+ rawdata = self.rawdata
+ assert rawdata[i:i+4] == '<!--', 'unexpected call to parse_comment()'
+ match = commentclose.search(rawdata, i+4)
+ if not match:
+ return -1
+ if report:
+ j = match.start()
+ self.handle_comment(rawdata[i+4: j])
+ j = match.end()
+ return j
+
+ # Internal -- parse processing instr, return end or -1 if not terminated
+ def parse_pi(self, i):
+ rawdata = self.rawdata
+ assert rawdata[i:i+2] == '<?', 'unexpected call to parse_pi()'
+ match = piclose.search(rawdata, i+2) # >
+ if not match:
+ return -1
+ j = match.start()
+ self.handle_pi(rawdata[i+2: j])
+ j = match.end()
+ return j
+
+ # Internal -- handle starttag, return end or -1 if not terminated
+ def parse_starttag(self, i):
+ self.__starttag_text = None
+ endpos = self.check_for_whole_start_tag(i)
+ if endpos < 0:
+ return endpos
+ rawdata = self.rawdata
+ self.__starttag_text = rawdata[i:endpos]
+
+ # Now parse the data between i+1 and j into a tag and attrs
+ attrs = []
+ match = tagfind.match(rawdata, i+1)
+ assert match, 'unexpected call to parse_starttag()'
+ k = match.end()
+ self.lasttag = tag = rawdata[i+1:k].lower()
+
+ while k < endpos:
+ m = attrfind.match(rawdata, k)
+ if not m:
+ break
+ attrname, rest, attrvalue = m.group(1, 2, 3)
+ if not rest:
+ attrvalue = None
+ elif attrvalue[:1] == '\'' == attrvalue[-1:] or \
+ attrvalue[:1] == '"' == attrvalue[-1:]:
+ attrvalue = attrvalue[1:-1]
+ attrvalue = self.unescape(attrvalue)
+ attrs.append((attrname.lower(), attrvalue))
+ k = m.end()
+
+ end = rawdata[k:endpos].strip()
+ if end not in (">", "/>"):
+ lineno, offset = self.getpos()
+ if "\n" in self.__starttag_text:
+ lineno = lineno + self.__starttag_text.count("\n")
+ offset = len(self.__starttag_text) \
+ - self.__starttag_text.rfind("\n")
+ else:
+ offset = offset + len(self.__starttag_text)
+ self.error("junk characters in start tag: %s"
+ % `rawdata[k:endpos][:20]`)
+ if end[-2:] == '/>':
+ # XHTML-style empty tag: <span attr="value" />
+ self.handle_startendtag(tag, attrs)
+ else:
+ self.handle_starttag(tag, attrs)
+ if tag in self.CDATA_CONTENT_ELEMENTS:
+ self.set_cdata_mode(tag)
+ return endpos
+
+ # Internal -- check to see if we have a complete starttag; return end
+ # or -1 if incomplete.
+ def check_for_whole_start_tag(self, i):
+ rawdata = self.rawdata
+ m = locatestarttagend.match(rawdata, i)
+ if m:
+ j = m.end()
+ next = rawdata[j:j+1]
+ if next == ">":
+ return j + 1
+ if next == "/":
+ s = rawdata[j:j+2]
+ if s == "/>":
+ return j + 2
+ if s == "/":
+ # buffer boundary
+ return -1
+ # else bogus input
+ self.updatepos(i, j + 1)
+ self.error("malformed empty start tag")
+ if next == "":
+ # end of input
+ return -1
+ if next in ("abcdefghijklmnopqrstuvwxyz=/"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
+ # end of input in or before attribute value, or we have the
+ # '/' from a '/>' ending
+ return -1
+ self.updatepos(i, j)
+ self.error("malformed start tag")
+ raise AssertionError("we should not get here!")
+
+ # Internal -- parse endtag, return end or -1 if incomplete
+ def parse_endtag(self, i):
+ rawdata = self.rawdata
+ assert rawdata[i:i+2] == "</", "unexpected call to parse_endtag"
+ match = endendtag.search(rawdata, i+1) # >
+ if not match:
+ return -1
+ j = match.end()
+ match = endtagfind.match(rawdata, i) # </ + tag + >
+ if not match:
+ self.error("bad end tag: %s" % `rawdata[i:j]`)
+ tag = match.group(1).lower()
+ if ( self.cdata_endtag is not None
+ and tag != self.cdata_endtag):
+ # Should be a mismatched end tag, but we'll treat it
+ # as text anyway, since most HTML authors aren't
+ # interested in the finer points of syntax.
+ self.handle_data(match.group(0))
+ else:
+ self.handle_endtag(tag)
+ self.clear_cdata_mode()
+ return j
+
+ # Overridable -- finish processing of start+end tag: <tag.../>
+ def handle_startendtag(self, tag, attrs):
+ self.handle_starttag(tag, attrs)
+ self.handle_endtag(tag)
+
+ # Overridable -- handle start tag
+ def handle_starttag(self, tag, attrs):
+ pass
+
+ # Overridable -- handle end tag
+ def handle_endtag(self, tag):
+ pass
+
+ # Overridable -- handle character reference
+ def handle_charref(self, name):
+ pass
+
+ # Overridable -- handle entity reference
+ def handle_entityref(self, name):
+ pass
+
+ # Overridable -- handle data
+ def handle_data(self, data):
+ pass
+
+ # Overridable -- handle comment
+ def handle_comment(self, data):
+ pass
+
+ # Overridable -- handle declaration
+ def handle_decl(self, decl):
+ pass
+
+ # Overridable -- handle processing instruction
+ def handle_pi(self, data):
+ pass
+
+ def unknown_decl(self, data):
+ self.error("unknown declaration: " + `data`)
+
+ # Internal -- helper to remove special character quoting
+ def unescape(self, s):
+ if '&' not in s:
+ return s
+ s = s.replace("<", "<")
+ s = s.replace(">", ">")
+ s = s.replace("'", "'")
+ s = s.replace(""", '"')
+ s = s.replace("&", "&") # Must be last
+ return s
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/HTMLTALParser.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/HTMLTALParser.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,315 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+Parse HTML and compile to TALInterpreter intermediate code.
+"""
+
+import sys
+
+from TALGenerator import TALGenerator
+from HTMLParser import HTMLParser, HTMLParseError
+from TALDefs import \
+ ZOPE_METAL_NS, ZOPE_TAL_NS, ZOPE_I18N_NS, METALError, TALError, I18NError
+
+BOOLEAN_HTML_ATTRS = [
+ # List of Boolean attributes in HTML that may be given in
+ # minimized form (e.g. <img ismap> rather than <img ismap="">)
+ # From http://www.w3.org/TR/xhtml1/#guidelines (C.10)
+ "compact", "nowrap", "ismap", "declare", "noshade", "checked",
+ "disabled", "readonly", "multiple", "selected", "noresize",
+ "defer"
+ ]
+
+EMPTY_HTML_TAGS = [
+ # List of HTML tags with an empty content model; these are
+ # rendered in minimized form, e.g. <img />.
+ # From http://www.w3.org/TR/xhtml1/#dtds
+ "base", "meta", "link", "hr", "br", "param", "img", "area",
+ "input", "col", "basefont", "isindex", "frame",
+ ]
+
+PARA_LEVEL_HTML_TAGS = [
+ # List of HTML elements that close open paragraph-level elements
+ # and are themselves paragraph-level.
+ "h1", "h2", "h3", "h4", "h5", "h6", "p",
+ ]
+
+BLOCK_CLOSING_TAG_MAP = {
+ "tr": ("tr", "td", "th"),
+ "td": ("td", "th"),
+ "th": ("td", "th"),
+ "li": ("li",),
+ "dd": ("dd", "dt"),
+ "dt": ("dd", "dt"),
+ }
+
+BLOCK_LEVEL_HTML_TAGS = [
+ # List of HTML tags that denote larger sections than paragraphs.
+ "blockquote", "table", "tr", "th", "td", "thead", "tfoot", "tbody",
+ "noframe", "ul", "ol", "li", "dl", "dt", "dd", "div",
+ ]
+
+TIGHTEN_IMPLICIT_CLOSE_TAGS = (PARA_LEVEL_HTML_TAGS
+ + BLOCK_CLOSING_TAG_MAP.keys())
+
+
+class NestingError(HTMLParseError):
+ """Exception raised when elements aren't properly nested."""
+
+ def __init__(self, tagstack, endtag, position=(None, None)):
+ self.endtag = endtag
+ if tagstack:
+ if len(tagstack) == 1:
+ msg = ('Open tag <%s> does not match close tag </%s>'
+ % (tagstack[0], endtag))
+ else:
+ msg = ('Open tags <%s> do not match close tag </%s>'
+ % ('>, <'.join(tagstack), endtag))
+ else:
+ msg = 'No tags are open to match </%s>' % endtag
+ HTMLParseError.__init__(self, msg, position)
+
+class EmptyTagError(NestingError):
+ """Exception raised when empty elements have an end tag."""
+
+ def __init__(self, tag, position=(None, None)):
+ self.tag = tag
+ msg = 'Close tag </%s> should be removed' % tag
+ HTMLParseError.__init__(self, msg, position)
+
+class OpenTagError(NestingError):
+ """Exception raised when a tag is not allowed in another tag."""
+
+ def __init__(self, tagstack, tag, position=(None, None)):
+ self.tag = tag
+ msg = 'Tag <%s> is not allowed in <%s>' % (tag, tagstack[-1])
+ HTMLParseError.__init__(self, msg, position)
+
+class HTMLTALParser(HTMLParser):
+
+ # External API
+
+ def __init__(self, gen=None):
+ HTMLParser.__init__(self)
+ if gen is None:
+ gen = TALGenerator(xml=0)
+ self.gen = gen
+ self.tagstack = []
+ self.nsstack = []
+ self.nsdict = {'tal': ZOPE_TAL_NS,
+ 'metal': ZOPE_METAL_NS,
+ 'i18n': ZOPE_I18N_NS,
+ }
+
+ def parseFile(self, file):
+ f = open(file)
+ data = f.read()
+ f.close()
+ try:
+ self.parseString(data)
+ except TALError, e:
+ e.setFile(file)
+ raise
+
+ def parseString(self, data):
+ self.feed(data)
+ self.close()
+ while self.tagstack:
+ self.implied_endtag(self.tagstack[-1], 2)
+ assert self.nsstack == [], self.nsstack
+
+ def getCode(self):
+ return self.gen.getCode()
+
+ def getWarnings(self):
+ return ()
+
+ # Overriding HTMLParser methods
+
+ def handle_starttag(self, tag, attrs):
+ self.close_para_tags(tag)
+ self.scan_xmlns(attrs)
+ tag, attrlist, taldict, metaldict, i18ndict \
+ = self.process_ns(tag, attrs)
+ if tag in EMPTY_HTML_TAGS and taldict.get("content"):
+ raise TALError(
+ "empty HTML tags cannot use tal:content: %s" % `tag`,
+ self.getpos())
+ self.tagstack.append(tag)
+ self.gen.emitStartElement(tag, attrlist, taldict, metaldict, i18ndict,
+ self.getpos())
+ if tag in EMPTY_HTML_TAGS:
+ self.implied_endtag(tag, -1)
+
+ def handle_startendtag(self, tag, attrs):
+ self.close_para_tags(tag)
+ self.scan_xmlns(attrs)
+ tag, attrlist, taldict, metaldict, i18ndict \
+ = self.process_ns(tag, attrs)
+ if taldict.get("content"):
+ if tag in EMPTY_HTML_TAGS:
+ raise TALError(
+ "empty HTML tags cannot use tal:content: %s" % `tag`,
+ self.getpos())
+ self.gen.emitStartElement(tag, attrlist, taldict, metaldict,
+ i18ndict, self.getpos())
+ self.gen.emitEndElement(tag, implied=-1)
+ else:
+ self.gen.emitStartElement(tag, attrlist, taldict, metaldict,
+ i18ndict, self.getpos(), isend=1)
+ self.pop_xmlns()
+
+ def handle_endtag(self, tag):
+ if tag in EMPTY_HTML_TAGS:
+ # </img> etc. in the source is an error
+ raise EmptyTagError(tag, self.getpos())
+ self.close_enclosed_tags(tag)
+ self.gen.emitEndElement(tag)
+ self.pop_xmlns()
+ self.tagstack.pop()
+
+ def close_para_tags(self, tag):
+ if tag in EMPTY_HTML_TAGS:
+ return
+ close_to = -1
+ if BLOCK_CLOSING_TAG_MAP.has_key(tag):
+ blocks_to_close = BLOCK_CLOSING_TAG_MAP[tag]
+ for i in range(len(self.tagstack)):
+ t = self.tagstack[i]
+ if t in blocks_to_close:
+ if close_to == -1:
+ close_to = i
+ elif t in BLOCK_LEVEL_HTML_TAGS:
+ close_to = -1
+ elif tag in PARA_LEVEL_HTML_TAGS + BLOCK_LEVEL_HTML_TAGS:
+ i = len(self.tagstack) - 1
+ while i >= 0:
+ closetag = self.tagstack[i]
+ if closetag in BLOCK_LEVEL_HTML_TAGS:
+ break
+ if closetag in PARA_LEVEL_HTML_TAGS:
+ if closetag != "p":
+ raise OpenTagError(self.tagstack, tag, self.getpos())
+ close_to = i
+ i = i - 1
+ if close_to >= 0:
+ while len(self.tagstack) > close_to:
+ self.implied_endtag(self.tagstack[-1], 1)
+
+ def close_enclosed_tags(self, tag):
+ if tag not in self.tagstack:
+ raise NestingError(self.tagstack, tag, self.getpos())
+ while tag != self.tagstack[-1]:
+ self.implied_endtag(self.tagstack[-1], 1)
+ assert self.tagstack[-1] == tag
+
+ def implied_endtag(self, tag, implied):
+ assert tag == self.tagstack[-1]
+ assert implied in (-1, 1, 2)
+ isend = (implied < 0)
+ if tag in TIGHTEN_IMPLICIT_CLOSE_TAGS:
+ # Pick out trailing whitespace from the program, and
+ # insert the close tag before the whitespace.
+ white = self.gen.unEmitWhitespace()
+ else:
+ white = None
+ self.gen.emitEndElement(tag, isend=isend, implied=implied)
+ if white:
+ self.gen.emitRawText(white)
+ self.tagstack.pop()
+ self.pop_xmlns()
+
+ def handle_charref(self, name):
+ self.gen.emitRawText("&#%s;" % name)
+
+ def handle_entityref(self, name):
+ self.gen.emitRawText("&%s;" % name)
+
+ def handle_data(self, data):
+ self.gen.emitRawText(data)
+
+ def handle_comment(self, data):
+ self.gen.emitRawText("<!--%s-->" % data)
+
+ def handle_decl(self, data):
+ self.gen.emitRawText("<!%s>" % data)
+
+ def handle_pi(self, data):
+ self.gen.emitRawText("<?%s>" % data)
+
+ # Internal thingies
+
+ def scan_xmlns(self, attrs):
+ nsnew = {}
+ for key, value in attrs:
+ if key.startswith("xmlns:"):
+ nsnew[key[6:]] = value
+ if nsnew:
+ self.nsstack.append(self.nsdict)
+ self.nsdict = self.nsdict.copy()
+ self.nsdict.update(nsnew)
+ else:
+ self.nsstack.append(self.nsdict)
+
+ def pop_xmlns(self):
+ self.nsdict = self.nsstack.pop()
+
+ def fixname(self, name):
+ if ':' in name:
+ prefix, suffix = name.split(':', 1)
+ if prefix == 'xmlns':
+ nsuri = self.nsdict.get(suffix)
+ if nsuri in (ZOPE_TAL_NS, ZOPE_METAL_NS, ZOPE_I18N_NS):
+ return name, name, prefix
+ else:
+ nsuri = self.nsdict.get(prefix)
+ if nsuri == ZOPE_TAL_NS:
+ return name, suffix, 'tal'
+ elif nsuri == ZOPE_METAL_NS:
+ return name, suffix, 'metal'
+ elif nsuri == ZOPE_I18N_NS:
+ return name, suffix, 'i18n'
+ return name, name, 0
+
+ def process_ns(self, name, attrs):
+ attrlist = []
+ taldict = {}
+ metaldict = {}
+ i18ndict = {}
+ name, namebase, namens = self.fixname(name)
+ for item in attrs:
+ key, value = item
+ key, keybase, keyns = self.fixname(key)
+ ns = keyns or namens # default to tag namespace
+ if ns and ns != 'unknown':
+ item = (key, value, ns)
+ if ns == 'tal':
+ if taldict.has_key(keybase):
+ raise TALError("duplicate TAL attribute " +
+ `keybase`, self.getpos())
+ taldict[keybase] = value
+ elif ns == 'metal':
+ if metaldict.has_key(keybase):
+ raise METALError("duplicate METAL attribute " +
+ `keybase`, self.getpos())
+ metaldict[keybase] = value
+ elif ns == 'i18n':
+ if i18ndict.has_key(keybase):
+ raise I18NError("duplicate i18n attribute " +
+ `keybase`, self.getpos())
+ i18ndict[keybase] = value
+ attrlist.append(item)
+ if namens in ('metal', 'tal'):
+ taldict['tal tag'] = namens
+ return name, attrlist, taldict, metaldict, i18ndict
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/README.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/README.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,97 @@
+TAL - Template Attribute Language
+---------------------------------
+
+This is an implementation of TAL, the Zope Template Attribute
+Language. For TAL, see the Zope Presentation Templates ZWiki:
+
+ http://dev.zope.org/Wikis/DevSite/Projects/ZPT/FrontPage
+
+It is not a Zope product nor is it designed exclusively to run inside
+of Zope, but if you have a Zope checkout that includes
+Products/ParsedXML, its Expat parser will be used.
+
+Prerequisites
+-------------
+
+You need:
+
+- A recent checkout of Zope2; don't forget to run the wo_pcgi.py
+ script to compile everything. (See above -- this is now optional.)
+
+- A recent checkout of the Zope2 product ParsedXML, accessible
+ throught <Zope2>/lib/python/Products/ParsedXML; don't forget to run
+ the setup.py script to compiles Expat. (Again, optional.)
+
+- Python 1.5.2; the driver script refuses to work with other versions
+ unless you specify the -n option; this is done so that I don't
+ accidentally use Python 2.x features.
+
+- Create a .path file containing proper module search path; it should
+ point the <Zope2>/lib/python directory that you want to use.
+
+How To Play
+-----------
+
+(Don't forget to edit .path, see above!)
+
+The script driver.py takes an XML file with TAL markup as argument and
+writes the expanded version to standard output. The filename argument
+defaults to tests/input/test01.xml.
+
+Regression test
+---------------
+
+There are unit test suites in the 'tests' subdirectory; these can be
+run with tests/run.py. This should print the testcase names plus
+progress info, followed by a final line saying "OK". It requires that
+../unittest.py exists.
+
+There are a number of test files in the 'tests' subdirectory, named
+tests/input/test<number>.xml and tests/input/test<number>.html. The
+Python script ./runtest.py calls driver.main() for each test file, and
+should print "<file> OK" for each one. These tests are also run as
+part of the unit test suites, so tests/run.py is all you need.
+
+What's Here
+-----------
+
+DummyEngine.py simple-minded TALES execution engine
+TALInterpreter.py class to interpret intermediate code
+TALGenerator.py class to generate intermediate code
+XMLParser.py base class to parse XML, avoiding DOM
+TALParser.py class to parse XML with TAL into intermediate code
+HTMLTALParser.py class to parse HTML with TAL into intermediate code
+HTMLParser.py HTML-parsing base class
+driver.py script to demonstrate TAL expansion
+timer.py script to time various processing phases
+setpath.py hack to set sys.path and import ZODB
+__init__.py empty file that makes this directory a package
+runtest.py Python script to run file-comparison tests
+ndiff.py helper for runtest.py to produce diffs
+tests/ drectory with test files and output
+tests/run.py Python script to run all tests
+
+Author and License
+------------------
+
+This code is written by Guido van Rossum (project lead), Fred Drake,
+and Tim Peters. It is owned by Digital Creations and can be
+redistributed under the Zope Public License.
+
+TO DO
+-----
+
+(See also http://www.zope.org/Members/jim/ZPTIssueTracker .)
+
+- Need to remove leading whitespace and newline when omitting an
+ element (either through tal:replace with a value of nothing or
+ tal:condition with a false condition).
+
+- Empty TAL/METAL attributes are ignored: tal:replace="" is ignored
+ rather than causing an error.
+
+- HTMLTALParser.py and TALParser.py are silly names. Should be
+ HTMLTALCompiler.py and XMLTALCompiler.py (or maybe shortened,
+ without "TAL"?)
+
+- Should we preserve case of tags and attribute names in HTML?
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/TALDefs.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/TALDefs.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,193 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+# Modifications for Roundup:
+# 1. commented out ITALES references
+"""
+Common definitions used by TAL and METAL compilation an transformation.
+"""
+
+from types import ListType, TupleType
+
+#from ITALES import ITALESErrorInfo
+
+TAL_VERSION = "1.4"
+
+XML_NS = "http://www.w3.org/XML/1998/namespace" # URI for XML namespace
+XMLNS_NS = "http://www.w3.org/2000/xmlns/" # URI for XML NS declarations
+
+ZOPE_TAL_NS = "http://xml.zope.org/namespaces/tal"
+ZOPE_METAL_NS = "http://xml.zope.org/namespaces/metal"
+ZOPE_I18N_NS = "http://xml.zope.org/namespaces/i18n"
+
+# This RE must exactly match the expression of the same name in the
+# zope.i18n.simpletranslationservice module:
+NAME_RE = "[a-zA-Z_][-a-zA-Z0-9_]*"
+
+KNOWN_METAL_ATTRIBUTES = [
+ "define-macro",
+ "use-macro",
+ "define-slot",
+ "fill-slot",
+ "slot",
+ ]
+
+KNOWN_TAL_ATTRIBUTES = [
+ "define",
+ "condition",
+ "content",
+ "replace",
+ "repeat",
+ "attributes",
+ "on-error",
+ "omit-tag",
+ "tal tag",
+ ]
+
+KNOWN_I18N_ATTRIBUTES = [
+ "translate",
+ "domain",
+ "target",
+ "source",
+ "attributes",
+ "data",
+ "name",
+ ]
+
+class TALError(Exception):
+
+ def __init__(self, msg, position=(None, None)):
+ assert msg != ""
+ self.msg = msg
+ self.lineno = position[0]
+ self.offset = position[1]
+ self.filename = None
+
+ def setFile(self, filename):
+ self.filename = filename
+
+ def __str__(self):
+ result = self.msg
+ if self.lineno is not None:
+ result = result + ", at line %d" % self.lineno
+ if self.offset is not None:
+ result = result + ", column %d" % (self.offset + 1)
+ if self.filename is not None:
+ result = result + ', in file %s' % self.filename
+ return result
+
+class METALError(TALError):
+ pass
+
+class TALESError(TALError):
+ pass
+
+class I18NError(TALError):
+ pass
+
+
+class ErrorInfo:
+
+ #__implements__ = ITALESErrorInfo
+
+ def __init__(self, err, position=(None, None)):
+ if isinstance(err, Exception):
+ self.type = err.__class__
+ self.value = err
+ else:
+ self.type = err
+ self.value = None
+ self.lineno = position[0]
+ self.offset = position[1]
+
+
+
+import re
+_attr_re = re.compile(r"\s*([^\s]+)\s+([^\s].*)\Z", re.S)
+_subst_re = re.compile(r"\s*(?:(text|structure)\s+)?(.*)\Z", re.S)
+del re
+
+def parseAttributeReplacements(arg, xml):
+ dict = {}
+ for part in splitParts(arg):
+ m = _attr_re.match(part)
+ if not m:
+ raise TALError("Bad syntax in attributes: " + `part`)
+ name, expr = m.group(1, 2)
+ if not xml:
+ name = name.lower()
+ if dict.has_key(name):
+ raise TALError("Duplicate attribute name in attributes: " + `part`)
+ dict[name] = expr
+ return dict
+
+def parseSubstitution(arg, position=(None, None)):
+ m = _subst_re.match(arg)
+ if not m:
+ raise TALError("Bad syntax in substitution text: " + `arg`, position)
+ key, expr = m.group(1, 2)
+ if not key:
+ key = "text"
+ return key, expr
+
+def splitParts(arg):
+ # Break in pieces at undoubled semicolons and
+ # change double semicolons to singles:
+ arg = arg.replace(";;", "\0")
+ parts = arg.split(';')
+ parts = [p.replace("\0", ";") for p in parts]
+ if len(parts) > 1 and not parts[-1].strip():
+ del parts[-1] # It ended in a semicolon
+ return parts
+
+def isCurrentVersion(program):
+ version = getProgramVersion(program)
+ return version == TAL_VERSION
+
+def getProgramMode(program):
+ version = getProgramVersion(program)
+ if (version == TAL_VERSION and isinstance(program[1], TupleType) and
+ len(program[1]) == 2):
+ opcode, mode = program[1]
+ if opcode == "mode":
+ return mode
+ return None
+
+def getProgramVersion(program):
+ if (len(program) >= 2 and
+ isinstance(program[0], TupleType) and len(program[0]) == 2):
+ opcode, version = program[0]
+ if opcode == "version":
+ return version
+ return None
+
+import re
+_ent1_re = re.compile('&(?![A-Z#])', re.I)
+_entch_re = re.compile('&([A-Z][A-Z0-9]*)(?![A-Z0-9;])', re.I)
+_entn1_re = re.compile('&#(?![0-9X])', re.I)
+_entnx_re = re.compile('&(#X[A-F0-9]*)(?![A-F0-9;])', re.I)
+_entnd_re = re.compile('&(#[0-9][0-9]*)(?![0-9;])')
+del re
+
+def attrEscape(s):
+ """Replace special characters '&<>' by character entities,
+ except when '&' already begins a syntactically valid entity."""
+ s = _ent1_re.sub('&', s)
+ s = _entch_re.sub(r'&\1', s)
+ s = _entn1_re.sub('&#', s)
+ s = _entnx_re.sub(r'&\1', s)
+ s = _entnd_re.sub(r'&\1', s)
+ s = s.replace('<', '<')
+ s = s.replace('>', '>')
+ s = s.replace('"', '"')
+ return s
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/TALGenerator.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/TALGenerator.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,876 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+Code generator for TALInterpreter intermediate code.
+"""
+
+import re
+import cgi
+
+import TALDefs
+
+from TALDefs import NAME_RE, TAL_VERSION
+from TALDefs import I18NError, METALError, TALError
+from TALDefs import parseSubstitution
+from TranslationContext import TranslationContext, DEFAULT_DOMAIN
+
+I18N_REPLACE = 1
+I18N_CONTENT = 2
+I18N_EXPRESSION = 3
+
+_name_rx = re.compile(NAME_RE)
+
+
+class TALGenerator:
+
+ inMacroUse = 0
+ inMacroDef = 0
+ source_file = None
+
+ def __init__(self, expressionCompiler=None, xml=1, source_file=None):
+ if not expressionCompiler:
+ from DummyEngine import DummyEngine
+ expressionCompiler = DummyEngine()
+ self.expressionCompiler = expressionCompiler
+ self.CompilerError = expressionCompiler.getCompilerError()
+ # This holds the emitted opcodes representing the input
+ self.program = []
+ # The program stack for when we need to do some sub-evaluation for an
+ # intermediate result. E.g. in an i18n:name tag for which the
+ # contents describe the ${name} value.
+ self.stack = []
+ # Another stack of postponed actions. Elements on this stack are a
+ # dictionary; key/values contain useful information that
+ # emitEndElement needs to finish its calculations
+ self.todoStack = []
+ self.macros = {}
+ self.slots = {}
+ self.slotStack = []
+ self.xml = xml
+ self.emit("version", TAL_VERSION)
+ self.emit("mode", xml and "xml" or "html")
+ if source_file is not None:
+ self.source_file = source_file
+ self.emit("setSourceFile", source_file)
+ self.i18nContext = TranslationContext()
+ self.i18nLevel = 0
+
+ def getCode(self):
+ assert not self.stack
+ assert not self.todoStack
+ return self.optimize(self.program), self.macros
+
+ def optimize(self, program):
+ output = []
+ collect = []
+ cursor = 0
+ if self.xml:
+ endsep = "/>"
+ else:
+ endsep = " />"
+ for cursor in xrange(len(program)+1):
+ try:
+ item = program[cursor]
+ except IndexError:
+ item = (None, None)
+ opcode = item[0]
+ if opcode == "rawtext":
+ collect.append(item[1])
+ continue
+ if opcode == "endTag":
+ collect.append("</%s>" % item[1])
+ continue
+ if opcode == "startTag":
+ if self.optimizeStartTag(collect, item[1], item[2], ">"):
+ continue
+ if opcode == "startEndTag":
+ if self.optimizeStartTag(collect, item[1], item[2], endsep):
+ continue
+ if opcode in ("beginScope", "endScope"):
+ # Push *Scope instructions in front of any text instructions;
+ # this allows text instructions separated only by *Scope
+ # instructions to be joined together.
+ output.append(self.optimizeArgsList(item))
+ continue
+ if opcode == 'noop':
+ # This is a spacer for end tags in the face of i18n:name
+ # attributes. We can't let the optimizer collect immediately
+ # following end tags into the same rawtextOffset.
+ opcode = None
+ pass
+ text = "".join(collect)
+ if text:
+ i = text.rfind("\n")
+ if i >= 0:
+ i = len(text) - (i + 1)
+ output.append(("rawtextColumn", (text, i)))
+ else:
+ output.append(("rawtextOffset", (text, len(text))))
+ if opcode != None:
+ output.append(self.optimizeArgsList(item))
+ collect = []
+ return self.optimizeCommonTriple(output)
+
+ def optimizeArgsList(self, item):
+ if len(item) == 2:
+ return item
+ else:
+ return item[0], tuple(item[1:])
+
+ # These codes are used to indicate what sort of special actions
+ # are needed for each special attribute. (Simple attributes don't
+ # get action codes.)
+ #
+ # The special actions (which are modal) are handled by
+ # TALInterpreter.attrAction() and .attrAction_tal().
+ #
+ # Each attribute is represented by a tuple:
+ #
+ # (name, value) -- a simple name/value pair, with
+ # no special processing
+ #
+ # (name, value, action, *extra) -- attribute with special
+ # processing needs, action is a
+ # code that indicates which
+ # branch to take, and *extra
+ # contains additional,
+ # action-specific information
+ # needed by the processing
+ #
+ def optimizeStartTag(self, collect, name, attrlist, end):
+ # return true if the tag can be converted to plain text
+ if not attrlist:
+ collect.append("<%s%s" % (name, end))
+ return 1
+ opt = 1
+ new = ["<" + name]
+ for i in range(len(attrlist)):
+ item = attrlist[i]
+ if len(item) > 2:
+ opt = 0
+ name, value, action = item[:3]
+ attrlist[i] = (name, value, action) + item[3:]
+ else:
+ if item[1] is None:
+ s = item[0]
+ else:
+ s = '%s="%s"' % (item[0], TALDefs.attrEscape(item[1]))
+ attrlist[i] = item[0], s
+ new.append(" " + s)
+ # if no non-optimizable attributes were found, convert to plain text
+ if opt:
+ new.append(end)
+ collect.extend(new)
+ return opt
+
+ def optimizeCommonTriple(self, program):
+ if len(program) < 3:
+ return program
+ output = program[:2]
+ prev2, prev1 = output
+ for item in program[2:]:
+ if ( item[0] == "beginScope"
+ and prev1[0] == "setPosition"
+ and prev2[0] == "rawtextColumn"):
+ position = output.pop()[1]
+ text, column = output.pop()[1]
+ prev1 = None, None
+ closeprev = 0
+ if output and output[-1][0] == "endScope":
+ closeprev = 1
+ output.pop()
+ item = ("rawtextBeginScope",
+ (text, column, position, closeprev, item[1]))
+ output.append(item)
+ prev2 = prev1
+ prev1 = item
+ return output
+
+ def todoPush(self, todo):
+ self.todoStack.append(todo)
+
+ def todoPop(self):
+ return self.todoStack.pop()
+
+ def compileExpression(self, expr):
+ try:
+ return self.expressionCompiler.compile(expr)
+ except self.CompilerError, err:
+ raise TALError('%s in expression %s' % (err.args[0], `expr`),
+ self.position)
+
+ def pushProgram(self):
+ self.stack.append(self.program)
+ self.program = []
+
+ def popProgram(self):
+ program = self.program
+ self.program = self.stack.pop()
+ return self.optimize(program)
+
+ def pushSlots(self):
+ self.slotStack.append(self.slots)
+ self.slots = {}
+
+ def popSlots(self):
+ slots = self.slots
+ self.slots = self.slotStack.pop()
+ return slots
+
+ def emit(self, *instruction):
+ self.program.append(instruction)
+
+ def emitStartTag(self, name, attrlist, isend=0):
+ if isend:
+ opcode = "startEndTag"
+ else:
+ opcode = "startTag"
+ self.emit(opcode, name, attrlist)
+
+ def emitEndTag(self, name):
+ if self.xml and self.program and self.program[-1][0] == "startTag":
+ # Minimize empty element
+ self.program[-1] = ("startEndTag",) + self.program[-1][1:]
+ else:
+ self.emit("endTag", name)
+
+ def emitOptTag(self, name, optTag, isend):
+ program = self.popProgram() #block
+ start = self.popProgram() #start tag
+ if (isend or not program) and self.xml:
+ # Minimize empty element
+ start[-1] = ("startEndTag",) + start[-1][1:]
+ isend = 1
+ cexpr = optTag[0]
+ if cexpr:
+ cexpr = self.compileExpression(optTag[0])
+ self.emit("optTag", name, cexpr, optTag[1], isend, start, program)
+
+ def emitRawText(self, text):
+ self.emit("rawtext", text)
+
+ def emitText(self, text):
+ self.emitRawText(cgi.escape(text))
+
+ def emitDefines(self, defines):
+ for part in TALDefs.splitParts(defines):
+ m = re.match(
+ r"(?s)\s*(?:(global|local)\s+)?(%s)\s+(.*)\Z" % NAME_RE, part)
+ if not m:
+ raise TALError("invalid define syntax: " + `part`,
+ self.position)
+ scope, name, expr = m.group(1, 2, 3)
+ scope = scope or "local"
+ cexpr = self.compileExpression(expr)
+ if scope == "local":
+ self.emit("setLocal", name, cexpr)
+ else:
+ self.emit("setGlobal", name, cexpr)
+
+ def emitOnError(self, name, onError, TALtag, isend):
+ block = self.popProgram()
+ key, expr = parseSubstitution(onError)
+ cexpr = self.compileExpression(expr)
+ if key == "text":
+ self.emit("insertText", cexpr, [])
+ else:
+ assert key == "structure"
+ self.emit("insertStructure", cexpr, {}, [])
+ if TALtag:
+ self.emitOptTag(name, (None, 1), isend)
+ else:
+ self.emitEndTag(name)
+ handler = self.popProgram()
+ self.emit("onError", block, handler)
+
+ def emitCondition(self, expr):
+ cexpr = self.compileExpression(expr)
+ program = self.popProgram()
+ self.emit("condition", cexpr, program)
+
+ def emitRepeat(self, arg):
+ m = re.match("(?s)\s*(%s)\s+(.*)\Z" % NAME_RE, arg)
+ if not m:
+ raise TALError("invalid repeat syntax: " + `arg`,
+ self.position)
+ name, expr = m.group(1, 2)
+ cexpr = self.compileExpression(expr)
+ program = self.popProgram()
+ self.emit("loop", name, cexpr, program)
+
+ def emitSubstitution(self, arg, attrDict={}):
+ key, expr = parseSubstitution(arg)
+ cexpr = self.compileExpression(expr)
+ program = self.popProgram()
+ if key == "text":
+ self.emit("insertText", cexpr, program)
+ else:
+ assert key == "structure"
+ self.emit("insertStructure", cexpr, attrDict, program)
+
+ def emitI18nVariable(self, stuff):
+ # Used for i18n:name attributes. arg is extra information describing
+ # how the contents of the variable should get filled in, and it will
+ # either be a 1-tuple or a 2-tuple. If arg[0] is None, then the
+ # i18n:name value is taken implicitly from the contents of the tag,
+ # e.g. "I live in <span i18n:name="country">the USA</span>". In this
+ # case, arg[1] is the opcode sub-program describing the contents of
+ # the tag.
+ #
+ # When arg[0] is not None, it contains the tal expression used to
+ # calculate the contents of the variable, e.g.
+ # "I live in <span i18n:name="country"
+ # tal:replace="here/countryOfOrigin" />"
+ varname, action, expression = stuff
+ m = _name_rx.match(varname)
+ if m is None or m.group() != varname:
+ raise TALError("illegal i18n:name: %r" % varname, self.position)
+ key = cexpr = None
+ program = self.popProgram()
+ if action == I18N_REPLACE:
+ # This is a tag with an i18n:name and a tal:replace (implicit or
+ # explicit). Get rid of the first and last elements of the
+ # program, which are the start and end tag opcodes of the tag.
+ program = program[1:-1]
+ elif action == I18N_CONTENT:
+ # This is a tag with an i18n:name and a tal:content
+ # (explicit-only). Keep the first and last elements of the
+ # program, so we keep the start and end tag output.
+ pass
+ else:
+ assert action == I18N_EXPRESSION
+ key, expr = parseSubstitution(expression)
+ cexpr = self.compileExpression(expr)
+ # XXX Would key be anything but 'text' or None?
+ assert key in ('text', None)
+ self.emit('i18nVariable', varname, program, cexpr)
+
+ def emitTranslation(self, msgid, i18ndata):
+ program = self.popProgram()
+ if i18ndata is None:
+ self.emit('insertTranslation', msgid, program)
+ else:
+ key, expr = parseSubstitution(i18ndata)
+ cexpr = self.compileExpression(expr)
+ assert key == 'text'
+ self.emit('insertTranslation', msgid, program, cexpr)
+
+ def emitDefineMacro(self, macroName):
+ program = self.popProgram()
+ macroName = macroName.strip()
+ if self.macros.has_key(macroName):
+ raise METALError("duplicate macro definition: %s" % `macroName`,
+ self.position)
+ if not re.match('%s$' % NAME_RE, macroName):
+ raise METALError("invalid macro name: %s" % `macroName`,
+ self.position)
+ self.macros[macroName] = program
+ self.inMacroDef = self.inMacroDef - 1
+ self.emit("defineMacro", macroName, program)
+
+ def emitUseMacro(self, expr):
+ cexpr = self.compileExpression(expr)
+ program = self.popProgram()
+ self.inMacroUse = 0
+ self.emit("useMacro", expr, cexpr, self.popSlots(), program)
+
+ def emitDefineSlot(self, slotName):
+ program = self.popProgram()
+ slotName = slotName.strip()
+ if not re.match('%s$' % NAME_RE, slotName):
+ raise METALError("invalid slot name: %s" % `slotName`,
+ self.position)
+ self.emit("defineSlot", slotName, program)
+
+ def emitFillSlot(self, slotName):
+ program = self.popProgram()
+ slotName = slotName.strip()
+ if self.slots.has_key(slotName):
+ raise METALError("duplicate fill-slot name: %s" % `slotName`,
+ self.position)
+ if not re.match('%s$' % NAME_RE, slotName):
+ raise METALError("invalid slot name: %s" % `slotName`,
+ self.position)
+ self.slots[slotName] = program
+ self.inMacroUse = 1
+ self.emit("fillSlot", slotName, program)
+
+ def unEmitWhitespace(self):
+ collect = []
+ i = len(self.program) - 1
+ while i >= 0:
+ item = self.program[i]
+ if item[0] != "rawtext":
+ break
+ text = item[1]
+ if not re.match(r"\A\s*\Z", text):
+ break
+ collect.append(text)
+ i = i-1
+ del self.program[i+1:]
+ if i >= 0 and self.program[i][0] == "rawtext":
+ text = self.program[i][1]
+ m = re.search(r"\s+\Z", text)
+ if m:
+ self.program[i] = ("rawtext", text[:m.start()])
+ collect.append(m.group())
+ collect.reverse()
+ return "".join(collect)
+
+ def unEmitNewlineWhitespace(self):
+ collect = []
+ i = len(self.program)
+ while i > 0:
+ i = i-1
+ item = self.program[i]
+ if item[0] != "rawtext":
+ break
+ text = item[1]
+ if re.match(r"\A[ \t]*\Z", text):
+ collect.append(text)
+ continue
+ m = re.match(r"(?s)^(.*)(\n[ \t]*)\Z", text)
+ if not m:
+ break
+ text, rest = m.group(1, 2)
+ collect.reverse()
+ rest = rest + "".join(collect)
+ del self.program[i:]
+ if text:
+ self.emit("rawtext", text)
+ return rest
+ return None
+
+ def replaceAttrs(self, attrlist, repldict):
+ # Each entry in attrlist starts like (name, value).
+ # Result is (name, value, action, expr, xlat) if there is a
+ # tal:attributes entry for that attribute. Additional attrs
+ # defined only by tal:attributes are added here.
+ #
+ # (name, value, action, expr, xlat)
+ if not repldict:
+ return attrlist
+ newlist = []
+ for item in attrlist:
+ key = item[0]
+ if repldict.has_key(key):
+ expr, xlat, msgid = repldict[key]
+ item = item[:2] + ("replace", expr, xlat, msgid)
+ del repldict[key]
+ newlist.append(item)
+ # Add dynamic-only attributes
+ for key, (expr, xlat, msgid) in repldict.items():
+ newlist.append((key, None, "insert", expr, xlat, msgid))
+ return newlist
+
+ def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
+ position=(None, None), isend=0):
+ if not taldict and not metaldict and not i18ndict:
+ # Handle the simple, common case
+ self.emitStartTag(name, attrlist, isend)
+ self.todoPush({})
+ if isend:
+ self.emitEndElement(name, isend)
+ return
+
+ self.position = position
+ for key, value in taldict.items():
+ if key not in TALDefs.KNOWN_TAL_ATTRIBUTES:
+ raise TALError("bad TAL attribute: " + `key`, position)
+ if not (value or key == 'omit-tag'):
+ raise TALError("missing value for TAL attribute: " +
+ `key`, position)
+ for key, value in metaldict.items():
+ if key not in TALDefs.KNOWN_METAL_ATTRIBUTES:
+ raise METALError("bad METAL attribute: " + `key`,
+ position)
+ if not value:
+ raise TALError("missing value for METAL attribute: " +
+ `key`, position)
+ for key, value in i18ndict.items():
+ if key not in TALDefs.KNOWN_I18N_ATTRIBUTES:
+ raise I18NError("bad i18n attribute: " + `key`, position)
+ if not value and key in ("attributes", "data", "id"):
+ raise I18NError("missing value for i18n attribute: " +
+ `key`, position)
+ todo = {}
+ defineMacro = metaldict.get("define-macro")
+ useMacro = metaldict.get("use-macro")
+ defineSlot = metaldict.get("define-slot")
+ fillSlot = metaldict.get("fill-slot")
+ define = taldict.get("define")
+ condition = taldict.get("condition")
+ repeat = taldict.get("repeat")
+ content = taldict.get("content")
+ replace = taldict.get("replace")
+ attrsubst = taldict.get("attributes")
+ onError = taldict.get("on-error")
+ omitTag = taldict.get("omit-tag")
+ TALtag = taldict.get("tal tag")
+ i18nattrs = i18ndict.get("attributes")
+ # Preserve empty string if implicit msgids are used. We'll generate
+ # code with the msgid='' and calculate the right implicit msgid during
+ # interpretation phase.
+ msgid = i18ndict.get("translate")
+ varname = i18ndict.get('name')
+ i18ndata = i18ndict.get('data')
+
+ if varname and not self.i18nLevel:
+ raise I18NError(
+ "i18n:name can only occur inside a translation unit",
+ position)
+
+ if i18ndata and not msgid:
+ raise I18NError("i18n:data must be accompanied by i18n:translate",
+ position)
+
+ if len(metaldict) > 1 and (defineMacro or useMacro):
+ raise METALError("define-macro and use-macro cannot be used "
+ "together or with define-slot or fill-slot",
+ position)
+ if replace:
+ if content:
+ raise TALError(
+ "tal:content and tal:replace are mutually exclusive",
+ position)
+ if msgid is not None:
+ raise I18NError(
+ "i18n:translate and tal:replace are mutually exclusive",
+ position)
+
+ repeatWhitespace = None
+ if repeat:
+ # Hack to include preceding whitespace in the loop program
+ repeatWhitespace = self.unEmitNewlineWhitespace()
+ if position != (None, None):
+ # XXX at some point we should insist on a non-trivial position
+ self.emit("setPosition", position)
+ if self.inMacroUse:
+ if fillSlot:
+ self.pushProgram()
+ if self.source_file is not None:
+ self.emit("setSourceFile", self.source_file)
+ todo["fillSlot"] = fillSlot
+ self.inMacroUse = 0
+ else:
+ if fillSlot:
+ raise METALError("fill-slot must be within a use-macro",
+ position)
+ if not self.inMacroUse:
+ if defineMacro:
+ self.pushProgram()
+ self.emit("version", TAL_VERSION)
+ self.emit("mode", self.xml and "xml" or "html")
+ if self.source_file is not None:
+ self.emit("setSourceFile", self.source_file)
+ todo["defineMacro"] = defineMacro
+ self.inMacroDef = self.inMacroDef + 1
+ if useMacro:
+ self.pushSlots()
+ self.pushProgram()
+ todo["useMacro"] = useMacro
+ self.inMacroUse = 1
+ if defineSlot:
+ if not self.inMacroDef:
+ raise METALError(
+ "define-slot must be within a define-macro",
+ position)
+ self.pushProgram()
+ todo["defineSlot"] = defineSlot
+
+ if defineSlot or i18ndict:
+
+ domain = i18ndict.get("domain") or self.i18nContext.domain
+ source = i18ndict.get("source") or self.i18nContext.source
+ target = i18ndict.get("target") or self.i18nContext.target
+ if ( domain != DEFAULT_DOMAIN
+ or source is not None
+ or target is not None):
+ self.i18nContext = TranslationContext(self.i18nContext,
+ domain=domain,
+ source=source,
+ target=target)
+ self.emit("beginI18nContext",
+ {"domain": domain, "source": source,
+ "target": target})
+ todo["i18ncontext"] = 1
+ if taldict or i18ndict:
+ dict = {}
+ for item in attrlist:
+ key, value = item[:2]
+ dict[key] = value
+ self.emit("beginScope", dict)
+ todo["scope"] = 1
+ if onError:
+ self.pushProgram() # handler
+ if TALtag:
+ self.pushProgram() # start
+ self.emitStartTag(name, list(attrlist)) # Must copy attrlist!
+ if TALtag:
+ self.pushProgram() # start
+ self.pushProgram() # block
+ todo["onError"] = onError
+ if define:
+ self.emitDefines(define)
+ todo["define"] = define
+ if condition:
+ self.pushProgram()
+ todo["condition"] = condition
+ if repeat:
+ todo["repeat"] = repeat
+ self.pushProgram()
+ if repeatWhitespace:
+ self.emitText(repeatWhitespace)
+ if content:
+ if varname:
+ todo['i18nvar'] = (varname, I18N_CONTENT, None)
+ todo["content"] = content
+ self.pushProgram()
+ else:
+ todo["content"] = content
+ elif replace:
+ # tal:replace w/ i18n:name has slightly different semantics. What
+ # we're actually replacing then is the contents of the ${name}
+ # placeholder.
+ if varname:
+ todo['i18nvar'] = (varname, I18N_EXPRESSION, replace)
+ else:
+ todo["replace"] = replace
+ self.pushProgram()
+ # i18n:name w/o tal:replace uses the content as the interpolation
+ # dictionary values
+ elif varname:
+ todo['i18nvar'] = (varname, I18N_REPLACE, None)
+ self.pushProgram()
+ if msgid is not None:
+ self.i18nLevel += 1
+ todo['msgid'] = msgid
+ if i18ndata:
+ todo['i18ndata'] = i18ndata
+ optTag = omitTag is not None or TALtag
+ if optTag:
+ todo["optional tag"] = omitTag, TALtag
+ self.pushProgram()
+ if attrsubst or i18nattrs:
+ if attrsubst:
+ repldict = TALDefs.parseAttributeReplacements(attrsubst,
+ self.xml)
+ else:
+ repldict = {}
+ if i18nattrs:
+ i18nattrs = _parseI18nAttributes(i18nattrs, attrlist, repldict,
+ self.position, self.xml,
+ self.source_file)
+ else:
+ i18nattrs = {}
+ # Convert repldict's name-->expr mapping to a
+ # name-->(compiled_expr, translate) mapping
+ for key, value in repldict.items():
+ if i18nattrs.get(key, None):
+ raise I18NError(
+ ("attribute [%s] cannot both be part of tal:attributes" +
+ " and have a msgid in i18n:attributes") % key,
+ position)
+ ce = self.compileExpression(value)
+ repldict[key] = ce, key in i18nattrs, i18nattrs.get(key)
+ for key in i18nattrs:
+ if not repldict.has_key(key):
+ repldict[key] = None, 1, i18nattrs.get(key)
+ else:
+ repldict = {}
+ if replace:
+ todo["repldict"] = repldict
+ repldict = {}
+ self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend)
+ if optTag:
+ self.pushProgram()
+ if content and not varname:
+ self.pushProgram()
+ if msgid is not None:
+ self.pushProgram()
+ if content and varname:
+ self.pushProgram()
+ if todo and position != (None, None):
+ todo["position"] = position
+ self.todoPush(todo)
+ if isend:
+ self.emitEndElement(name, isend)
+
+ def emitEndElement(self, name, isend=0, implied=0):
+ todo = self.todoPop()
+ if not todo:
+ # Shortcut
+ if not isend:
+ self.emitEndTag(name)
+ return
+
+ self.position = position = todo.get("position", (None, None))
+ defineMacro = todo.get("defineMacro")
+ useMacro = todo.get("useMacro")
+ defineSlot = todo.get("defineSlot")
+ fillSlot = todo.get("fillSlot")
+ repeat = todo.get("repeat")
+ content = todo.get("content")
+ replace = todo.get("replace")
+ condition = todo.get("condition")
+ onError = todo.get("onError")
+ define = todo.get("define")
+ repldict = todo.get("repldict", {})
+ scope = todo.get("scope")
+ optTag = todo.get("optional tag")
+ msgid = todo.get('msgid')
+ i18ncontext = todo.get("i18ncontext")
+ varname = todo.get('i18nvar')
+ i18ndata = todo.get('i18ndata')
+
+ if implied > 0:
+ if defineMacro or useMacro or defineSlot or fillSlot:
+ exc = METALError
+ what = "METAL"
+ else:
+ exc = TALError
+ what = "TAL"
+ raise exc("%s attributes on <%s> require explicit </%s>" %
+ (what, name, name), position)
+
+ # If there's no tal:content or tal:replace in the tag with the
+ # i18n:name, tal:replace is the default.
+ if content:
+ self.emitSubstitution(content, {})
+ # If we're looking at an implicit msgid, emit the insertTranslation
+ # opcode now, so that the end tag doesn't become part of the implicit
+ # msgid. If we're looking at an explicit msgid, it's better to emit
+ # the opcode after the i18nVariable opcode so we can better handle
+ # tags with both of them in them (and in the latter case, the contents
+ # would be thrown away for msgid purposes).
+ #
+ # Still, we should emit insertTranslation opcode before i18nVariable
+ # in case tal:content, i18n:translate and i18n:name in the same tag
+ if msgid is not None:
+ if (not varname) or (
+ varname and (varname[1] == I18N_CONTENT)):
+ self.emitTranslation(msgid, i18ndata)
+ self.i18nLevel -= 1
+ if optTag:
+ self.emitOptTag(name, optTag, isend)
+ elif not isend:
+ # If we're processing the end tag for a tag that contained
+ # i18n:name, we need to make sure that optimize() won't collect
+ # immediately following end tags into the same rawtextOffset, so
+ # put a spacer here that the optimizer will recognize.
+ if varname:
+ self.emit('noop')
+ self.emitEndTag(name)
+ # If i18n:name appeared in the same tag as tal:replace then we're
+ # going to do the substitution a little bit differently. The results
+ # of the expression go into the i18n substitution dictionary.
+ if replace:
+ self.emitSubstitution(replace, repldict)
+ elif varname:
+ # o varname[0] is the variable name
+ # o varname[1] is either
+ # - I18N_REPLACE for implicit tal:replace
+ # - I18N_CONTENT for tal:content
+ # - I18N_EXPRESSION for explicit tal:replace
+ # o varname[2] will be None for the first two actions and the
+ # replacement tal expression for the third action.
+ assert (varname[1]
+ in [I18N_REPLACE, I18N_CONTENT, I18N_EXPRESSION])
+ self.emitI18nVariable(varname)
+ # Do not test for "msgid is not None", i.e. we only want to test for
+ # explicit msgids here. See comment above.
+ if msgid is not None:
+ # in case tal:content, i18n:translate and i18n:name in the
+ # same tag insertTranslation opcode has already been
+ # emitted
+ if varname and (varname[1] <> I18N_CONTENT):
+ self.emitTranslation(msgid, i18ndata)
+ if repeat:
+ self.emitRepeat(repeat)
+ if condition:
+ self.emitCondition(condition)
+ if onError:
+ self.emitOnError(name, onError, optTag and optTag[1], isend)
+ if scope:
+ self.emit("endScope")
+ if i18ncontext:
+ self.emit("endI18nContext")
+ assert self.i18nContext.parent is not None
+ self.i18nContext = self.i18nContext.parent
+ if defineSlot:
+ self.emitDefineSlot(defineSlot)
+ if fillSlot:
+ self.emitFillSlot(fillSlot)
+ if useMacro:
+ self.emitUseMacro(useMacro)
+ if defineMacro:
+ self.emitDefineMacro(defineMacro)
+
+
+def _parseI18nAttributes(i18nattrs, attrlist, repldict, position,
+ xml, source_file):
+
+ def addAttribute(dic, attr, msgid, position, xml):
+ if not xml:
+ attr = attr.lower()
+ if attr in dic:
+ raise TALError(
+ "attribute may only be specified once in i18n:attributes: "
+ + attr,
+ position)
+ dic[attr] = msgid
+
+ d = {}
+ if ';' in i18nattrs:
+ i18nattrlist = i18nattrs.split(';')
+ i18nattrlist = [attr.strip().split()
+ for attr in i18nattrlist if attr.strip()]
+ for parts in i18nattrlist:
+ if len(parts) > 2:
+ raise TALError("illegal i18n:attributes specification: %r"
+ % parts, position)
+ if len(parts) == 2:
+ attr, msgid = parts
+ else:
+ # len(parts) == 1
+ attr = parts[0]
+ msgid = None
+ addAttribute(d, attr, msgid, position, xml)
+ else:
+ i18nattrlist = i18nattrs.split()
+ if len(i18nattrlist) == 2:
+ staticattrs = [attr[0] for attr in attrlist if len(attr) == 2]
+ if (not i18nattrlist[1] in staticattrs) and (
+ not i18nattrlist[1] in repldict):
+ attr, msgid = i18nattrlist
+ addAttribute(d, attr, msgid, position, xml)
+ else:
+ msgid = None
+ for attr in i18nattrlist:
+ addAttribute(d, attr, msgid, position, xml)
+ else:
+ msgid = None
+ for attr in i18nattrlist:
+ addAttribute(d, attr, msgid, position, xml)
+ return d
+
+def test():
+ t = TALGenerator()
+ t.pushProgram()
+ t.emit("bar")
+ p = t.popProgram()
+ t.emit("foo", p)
+
+if __name__ == "__main__":
+ test()
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/TALInterpreter.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/TALInterpreter.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,758 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+# Modifications for Roundup:
+# 1. implemented ustr as str
+"""
+Interpreter for a pre-compiled TAL program.
+"""
+
+import sys
+import getopt
+import re
+from types import ListType
+from cgi import escape
+# Do not use cStringIO here! It's not unicode aware. :(
+from StringIO import StringIO
+#from DocumentTemplate.DT_Util import ustr
+ustr = str
+
+from TALDefs import TAL_VERSION, TALError, METALError, attrEscape
+from TALDefs import isCurrentVersion, getProgramVersion, getProgramMode
+from TALGenerator import TALGenerator
+from TranslationContext import TranslationContext
+
+BOOLEAN_HTML_ATTRS = [
+ # List of Boolean attributes in HTML that should be rendered in
+ # minimized form (e.g. <img ismap> rather than <img ismap="">)
+ # From http://www.w3.org/TR/xhtml1/#guidelines (C.10)
+ # XXX The problem with this is that this is not valid XML and
+ # can't be parsed back!
+ "compact", "nowrap", "ismap", "declare", "noshade", "checked",
+ "disabled", "readonly", "multiple", "selected", "noresize",
+ "defer"
+]
+
+def normalize(text):
+ # Now we need to normalize the whitespace in implicit message ids and
+ # implicit $name substitution values by stripping leading and trailing
+ # whitespace, and folding all internal whitespace to a single space.
+ return ' '.join(text.split())
+
+
+NAME_RE = r"[a-zA-Z][a-zA-Z0-9_]*"
+_interp_regex = re.compile(r'(?<!\$)(\$(?:%(n)s|{%(n)s}))' %({'n': NAME_RE}))
+_get_var_regex = re.compile(r'%(n)s' %({'n': NAME_RE}))
+
+def interpolate(text, mapping):
+ """Interpolate ${keyword} substitutions.
+
+ This is called when no translation is provided by the translation
+ service.
+ """
+ if not mapping:
+ return text
+ # Find all the spots we want to substitute.
+ to_replace = _interp_regex.findall(text)
+ # Now substitute with the variables in mapping.
+ for string in to_replace:
+ var = _get_var_regex.findall(string)[0]
+ if mapping.has_key(var):
+ # Call ustr because we may have an integer for instance.
+ subst = ustr(mapping[var])
+ try:
+ text = text.replace(string, subst)
+ except UnicodeError:
+ # subst contains high-bit chars...
+ # As we have no way of knowing the correct encoding,
+ # substitue something instead of raising an exception.
+ subst = `subst`[1:-1]
+ text = text.replace(string, subst)
+ return text
+
+
+class AltTALGenerator(TALGenerator):
+
+ def __init__(self, repldict, expressionCompiler=None, xml=0):
+ self.repldict = repldict
+ self.enabled = 1
+ TALGenerator.__init__(self, expressionCompiler, xml)
+
+ def enable(self, enabled):
+ self.enabled = enabled
+
+ def emit(self, *args):
+ if self.enabled:
+ TALGenerator.emit(self, *args)
+
+ def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
+ position=(None, None), isend=0):
+ metaldict = {}
+ taldict = {}
+ i18ndict = {}
+ if self.enabled and self.repldict:
+ taldict["attributes"] = "x x"
+ TALGenerator.emitStartElement(self, name, attrlist,
+ taldict, metaldict, i18ndict,
+ position, isend)
+
+ def replaceAttrs(self, attrlist, repldict):
+ if self.enabled and self.repldict:
+ repldict = self.repldict
+ self.repldict = None
+ return TALGenerator.replaceAttrs(self, attrlist, repldict)
+
+
+class TALInterpreter:
+
+ def __init__(self, program, macros, engine, stream=None,
+ debug=0, wrap=60, metal=1, tal=1, showtal=-1,
+ strictinsert=1, stackLimit=100, i18nInterpolate=1):
+ self.program = program
+ self.macros = macros
+ self.engine = engine # Execution engine (aka context)
+ self.Default = engine.getDefault()
+ self.stream = stream or sys.stdout
+ self._stream_write = self.stream.write
+ self.debug = debug
+ self.wrap = wrap
+ self.metal = metal
+ self.tal = tal
+ if tal:
+ self.dispatch = self.bytecode_handlers_tal
+ else:
+ self.dispatch = self.bytecode_handlers
+ assert showtal in (-1, 0, 1)
+ if showtal == -1:
+ showtal = (not tal)
+ self.showtal = showtal
+ self.strictinsert = strictinsert
+ self.stackLimit = stackLimit
+ self.html = 0
+ self.endsep = "/>"
+ self.endlen = len(self.endsep)
+ self.macroStack = []
+ self.position = None, None # (lineno, offset)
+ self.col = 0
+ self.level = 0
+ self.scopeLevel = 0
+ self.sourceFile = None
+ self.i18nStack = []
+ self.i18nInterpolate = i18nInterpolate
+ self.i18nContext = TranslationContext()
+
+ def StringIO(self):
+ # Third-party products wishing to provide a full Unicode-aware
+ # StringIO can do so by monkey-patching this method.
+ return FasterStringIO()
+
+ def saveState(self):
+ return (self.position, self.col, self.stream,
+ self.scopeLevel, self.level, self.i18nContext)
+
+ def restoreState(self, state):
+ (self.position, self.col, self.stream, scopeLevel, level, i18n) = state
+ self._stream_write = self.stream.write
+ assert self.level == level
+ while self.scopeLevel > scopeLevel:
+ self.engine.endScope()
+ self.scopeLevel = self.scopeLevel - 1
+ self.engine.setPosition(self.position)
+ self.i18nContext = i18n
+
+ def restoreOutputState(self, state):
+ (dummy, self.col, self.stream, scopeLevel, level, i18n) = state
+ self._stream_write = self.stream.write
+ assert self.level == level
+ assert self.scopeLevel == scopeLevel
+
+ def pushMacro(self, macroName, slots, entering=1):
+ if len(self.macroStack) >= self.stackLimit:
+ raise METALError("macro nesting limit (%d) exceeded "
+ "by %s" % (self.stackLimit, `macroName`))
+ self.macroStack.append([macroName, slots, entering, self.i18nContext])
+
+ def popMacro(self):
+ return self.macroStack.pop()
+
+ def __call__(self):
+ assert self.level == 0
+ assert self.scopeLevel == 0
+ assert self.i18nContext.parent is None
+ self.interpret(self.program)
+ assert self.level == 0
+ assert self.scopeLevel == 0
+ assert self.i18nContext.parent is None
+ if self.col > 0:
+ self._stream_write("\n")
+ self.col = 0
+
+ def stream_write(self, s,
+ len=len):
+ self._stream_write(s)
+ i = s.rfind('\n')
+ if i < 0:
+ self.col = self.col + len(s)
+ else:
+ self.col = len(s) - (i + 1)
+
+ bytecode_handlers = {}
+
+ def interpretWithStream(self, program, stream):
+ oldstream = self.stream
+ self.stream = stream
+ self._stream_write = stream.write
+ try:
+ self.interpret(program)
+ finally:
+ self.stream = oldstream
+ self._stream_write = oldstream.write
+
+ def interpret(self, program):
+ oldlevel = self.level
+ self.level = oldlevel + 1
+ handlers = self.dispatch
+ try:
+ if self.debug:
+ for (opcode, args) in program:
+ s = "%sdo_%s(%s)\n" % (" "*self.level, opcode,
+ repr(args))
+ if len(s) > 80:
+ s = s[:76] + "...\n"
+ sys.stderr.write(s)
+ handlers[opcode](self, args)
+ else:
+ for (opcode, args) in program:
+ handlers[opcode](self, args)
+ finally:
+ self.level = oldlevel
+
+ def do_version(self, version):
+ assert version == TAL_VERSION
+ bytecode_handlers["version"] = do_version
+
+ def do_mode(self, mode):
+ assert mode in ("html", "xml")
+ self.html = (mode == "html")
+ if self.html:
+ self.endsep = " />"
+ else:
+ self.endsep = "/>"
+ self.endlen = len(self.endsep)
+ bytecode_handlers["mode"] = do_mode
+
+ def do_setSourceFile(self, source_file):
+ self.sourceFile = source_file
+ self.engine.setSourceFile(source_file)
+ bytecode_handlers["setSourceFile"] = do_setSourceFile
+
+ def do_setPosition(self, position):
+ self.position = position
+ self.engine.setPosition(position)
+ bytecode_handlers["setPosition"] = do_setPosition
+
+ def do_startEndTag(self, stuff):
+ self.do_startTag(stuff, self.endsep, self.endlen)
+ bytecode_handlers["startEndTag"] = do_startEndTag
+
+ def do_startTag(self, (name, attrList),
+ end=">", endlen=1, _len=len):
+ # The bytecode generator does not cause calls to this method
+ # for start tags with no attributes; those are optimized down
+ # to rawtext events. Hence, there is no special "fast path"
+ # for that case.
+ L = ["<", name]
+ append = L.append
+ col = self.col + _len(name) + 1
+ wrap = self.wrap
+ align = col + 1
+ if align >= wrap/2:
+ align = 4 # Avoid a narrow column far to the right
+ attrAction = self.dispatch["<attrAction>"]
+ try:
+ for item in attrList:
+ if _len(item) == 2:
+ name, s = item
+ else:
+ # item[2] is the 'action' field:
+ if item[2] in ('metal', 'tal', 'xmlns', 'i18n'):
+ if not self.showtal:
+ continue
+ ok, name, s = self.attrAction(item)
+ else:
+ ok, name, s = attrAction(self, item)
+ if not ok:
+ continue
+ slen = _len(s)
+ if (wrap and
+ col >= align and
+ col + 1 + slen > wrap):
+ append("\n")
+ append(" "*align)
+ col = align + slen
+ else:
+ append(" ")
+ col = col + 1 + slen
+ append(s)
+ append(end)
+ self._stream_write("".join(L))
+ col = col + endlen
+ finally:
+ self.col = col
+ bytecode_handlers["startTag"] = do_startTag
+
+ def attrAction(self, item):
+ name, value, action = item[:3]
+ if action == 'insert':
+ return 0, name, value
+ macs = self.macroStack
+ if action == 'metal' and self.metal and macs:
+ if len(macs) > 1 or not macs[-1][2]:
+ # Drop all METAL attributes at a use-depth above one.
+ return 0, name, value
+ # Clear 'entering' flag
+ macs[-1][2] = 0
+ # Convert or drop depth-one METAL attributes.
+ i = name.rfind(":") + 1
+ prefix, suffix = name[:i], name[i:]
+ if suffix == "define-macro":
+ # Convert define-macro as we enter depth one.
+ name = prefix + "use-macro"
+ value = macs[-1][0] # Macro name
+ elif suffix == "define-slot":
+ name = prefix + "fill-slot"
+ elif suffix == "fill-slot":
+ pass
+ else:
+ return 0, name, value
+
+ if value is None:
+ value = name
+ else:
+ value = '%s="%s"' % (name, attrEscape(value))
+ return 1, name, value
+
+ def attrAction_tal(self, item):
+ name, value, action = item[:3]
+ ok = 1
+ expr, xlat, msgid = item[3:]
+ if self.html and name.lower() in BOOLEAN_HTML_ATTRS:
+ evalue = self.engine.evaluateBoolean(item[3])
+ if evalue is self.Default:
+ if action == 'insert': # Cancelled insert
+ ok = 0
+ elif evalue:
+ value = None
+ else:
+ ok = 0
+ elif expr is not None:
+ evalue = self.engine.evaluateText(item[3])
+ if evalue is self.Default:
+ if action == 'insert': # Cancelled insert
+ ok = 0
+ else:
+ if evalue is None:
+ ok = 0
+ value = evalue
+ else:
+ evalue = None
+
+ if ok:
+ if xlat:
+ translated = self.translate(msgid or value, value, {})
+ if translated is not None:
+ value = translated
+ if value is None:
+ value = name
+ elif evalue is self.Default:
+ value = attrEscape(value)
+ else:
+ value = escape(value, quote=1)
+ value = '%s="%s"' % (name, value)
+ return ok, name, value
+ bytecode_handlers["<attrAction>"] = attrAction
+
+ def no_tag(self, start, program):
+ state = self.saveState()
+ self.stream = stream = self.StringIO()
+ self._stream_write = stream.write
+ self.interpret(start)
+ self.restoreOutputState(state)
+ self.interpret(program)
+
+ def do_optTag(self, (name, cexpr, tag_ns, isend, start, program),
+ omit=0):
+ if tag_ns and not self.showtal:
+ return self.no_tag(start, program)
+
+ self.interpret(start)
+ if not isend:
+ self.interpret(program)
+ s = '</%s>' % name
+ self._stream_write(s)
+ self.col = self.col + len(s)
+
+ def do_optTag_tal(self, stuff):
+ cexpr = stuff[1]
+ if cexpr is not None and (cexpr == '' or
+ self.engine.evaluateBoolean(cexpr)):
+ self.no_tag(stuff[-2], stuff[-1])
+ else:
+ self.do_optTag(stuff)
+ bytecode_handlers["optTag"] = do_optTag
+
+ def do_rawtextBeginScope(self, (s, col, position, closeprev, dict)):
+ self._stream_write(s)
+ self.col = col
+ self.position = position
+ self.engine.setPosition(position)
+ if closeprev:
+ engine = self.engine
+ engine.endScope()
+ engine.beginScope()
+ else:
+ self.engine.beginScope()
+ self.scopeLevel = self.scopeLevel + 1
+
+ def do_rawtextBeginScope_tal(self, (s, col, position, closeprev, dict)):
+ self._stream_write(s)
+ self.col = col
+ engine = self.engine
+ self.position = position
+ engine.setPosition(position)
+ if closeprev:
+ engine.endScope()
+ engine.beginScope()
+ else:
+ engine.beginScope()
+ self.scopeLevel = self.scopeLevel + 1
+ engine.setLocal("attrs", dict)
+ bytecode_handlers["rawtextBeginScope"] = do_rawtextBeginScope
+
+ def do_beginScope(self, dict):
+ self.engine.beginScope()
+ self.scopeLevel = self.scopeLevel + 1
+
+ def do_beginScope_tal(self, dict):
+ engine = self.engine
+ engine.beginScope()
+ engine.setLocal("attrs", dict)
+ self.scopeLevel = self.scopeLevel + 1
+ bytecode_handlers["beginScope"] = do_beginScope
+
+ def do_endScope(self, notused=None):
+ self.engine.endScope()
+ self.scopeLevel = self.scopeLevel - 1
+ bytecode_handlers["endScope"] = do_endScope
+
+ def do_setLocal(self, notused):
+ pass
+
+ def do_setLocal_tal(self, (name, expr)):
+ self.engine.setLocal(name, self.engine.evaluateValue(expr))
+ bytecode_handlers["setLocal"] = do_setLocal
+
+ def do_setGlobal_tal(self, (name, expr)):
+ self.engine.setGlobal(name, self.engine.evaluateValue(expr))
+ bytecode_handlers["setGlobal"] = do_setLocal
+
+ def do_beginI18nContext(self, settings):
+ get = settings.get
+ self.i18nContext = TranslationContext(self.i18nContext,
+ domain=get("domain"),
+ source=get("source"),
+ target=get("target"))
+ bytecode_handlers["beginI18nContext"] = do_beginI18nContext
+
+ def do_endI18nContext(self, notused=None):
+ self.i18nContext = self.i18nContext.parent
+ assert self.i18nContext is not None
+ bytecode_handlers["endI18nContext"] = do_endI18nContext
+
+ def do_insertText(self, stuff):
+ self.interpret(stuff[1])
+
+ def do_insertText_tal(self, stuff):
+ text = self.engine.evaluateText(stuff[0])
+ if text is None:
+ return
+ if text is self.Default:
+ self.interpret(stuff[1])
+ return
+ s = escape(text)
+ self._stream_write(s)
+ i = s.rfind('\n')
+ if i < 0:
+ self.col = self.col + len(s)
+ else:
+ self.col = len(s) - (i + 1)
+ bytecode_handlers["insertText"] = do_insertText
+
+ def do_i18nVariable(self, stuff):
+ varname, program, expression = stuff
+ if expression is None:
+ # The value is implicitly the contents of this tag, so we have to
+ # evaluate the mini-program to get the value of the variable.
+ state = self.saveState()
+ try:
+ tmpstream = self.StringIO()
+ self.interpretWithStream(program, tmpstream)
+ value = normalize(tmpstream.getvalue())
+ finally:
+ self.restoreState(state)
+ else:
+ # Evaluate the value to be associated with the variable in the
+ # i18n interpolation dictionary.
+ value = self.engine.evaluate(expression)
+ # Either the i18n:name tag is nested inside an i18n:translate in which
+ # case the last item on the stack has the i18n dictionary and string
+ # representation, or the i18n:name and i18n:translate attributes are
+ # in the same tag, in which case the i18nStack will be empty. In that
+ # case we can just output the ${name} to the stream
+ i18ndict, srepr = self.i18nStack[-1]
+ i18ndict[varname] = value
+ placeholder = '${%s}' % varname
+ srepr.append(placeholder)
+ self._stream_write(placeholder)
+ bytecode_handlers['i18nVariable'] = do_i18nVariable
+
+ def do_insertTranslation(self, stuff):
+ i18ndict = {}
+ srepr = []
+ obj = None
+ self.i18nStack.append((i18ndict, srepr))
+ msgid = stuff[0]
+ # We need to evaluate the content of the tag because that will give us
+ # several useful pieces of information. First, the contents will
+ # include an implicit message id, if no explicit one was given.
+ # Second, it will evaluate any i18nVariable definitions in the body of
+ # the translation (necessary for $varname substitutions).
+ #
+ # Use a temporary stream to capture the interpretation of the
+ # subnodes, which should /not/ go to the output stream.
+ tmpstream = self.StringIO()
+ self.interpretWithStream(stuff[1], tmpstream)
+ default = tmpstream.getvalue()
+ # We only care about the evaluated contents if we need an implicit
+ # message id. All other useful information will be in the i18ndict on
+ # the top of the i18nStack.
+ if msgid == '':
+ msgid = normalize(default)
+ self.i18nStack.pop()
+ # See if there is was an i18n:data for msgid
+ if len(stuff) > 2:
+ obj = self.engine.evaluate(stuff[2])
+ xlated_msgid = self.translate(msgid, default, i18ndict, obj)
+ assert xlated_msgid is not None, self.position
+ self._stream_write(xlated_msgid)
+ bytecode_handlers['insertTranslation'] = do_insertTranslation
+
+ def do_insertStructure(self, stuff):
+ self.interpret(stuff[2])
+
+ def do_insertStructure_tal(self, (expr, repldict, block)):
+ structure = self.engine.evaluateStructure(expr)
+ if structure is None:
+ return
+ if structure is self.Default:
+ self.interpret(block)
+ return
+ text = ustr(structure)
+ if not (repldict or self.strictinsert):
+ # Take a shortcut, no error checking
+ self.stream_write(text)
+ return
+ if self.html:
+ self.insertHTMLStructure(text, repldict)
+ else:
+ self.insertXMLStructure(text, repldict)
+ bytecode_handlers["insertStructure"] = do_insertStructure
+
+ def insertHTMLStructure(self, text, repldict):
+ from HTMLTALParser import HTMLTALParser
+ gen = AltTALGenerator(repldict, self.engine.getCompiler(), 0)
+ p = HTMLTALParser(gen) # Raises an exception if text is invalid
+ p.parseString(text)
+ program, macros = p.getCode()
+ self.interpret(program)
+
+ def insertXMLStructure(self, text, repldict):
+ from TALParser import TALParser
+ gen = AltTALGenerator(repldict, self.engine.getCompiler(), 0)
+ p = TALParser(gen)
+ gen.enable(0)
+ p.parseFragment('<!DOCTYPE foo PUBLIC "foo" "bar"><foo>')
+ gen.enable(1)
+ p.parseFragment(text) # Raises an exception if text is invalid
+ gen.enable(0)
+ p.parseFragment('</foo>', 1)
+ program, macros = gen.getCode()
+ self.interpret(program)
+
+ def do_loop(self, (name, expr, block)):
+ self.interpret(block)
+
+ def do_loop_tal(self, (name, expr, block)):
+ iterator = self.engine.setRepeat(name, expr)
+ while iterator.next():
+ self.interpret(block)
+ bytecode_handlers["loop"] = do_loop
+
+ def translate(self, msgid, default, i18ndict, obj=None):
+ if obj:
+ i18ndict.update(obj)
+ if not self.i18nInterpolate:
+ return msgid
+ # XXX We need to pass in one of context or target_language
+ return self.engine.translate(self.i18nContext.domain,
+ msgid, i18ndict, default=default)
+
+ def do_rawtextColumn(self, (s, col)):
+ self._stream_write(s)
+ self.col = col
+ bytecode_handlers["rawtextColumn"] = do_rawtextColumn
+
+ def do_rawtextOffset(self, (s, offset)):
+ self._stream_write(s)
+ self.col = self.col + offset
+ bytecode_handlers["rawtextOffset"] = do_rawtextOffset
+
+ def do_condition(self, (condition, block)):
+ if not self.tal or self.engine.evaluateBoolean(condition):
+ self.interpret(block)
+ bytecode_handlers["condition"] = do_condition
+
+ def do_defineMacro(self, (macroName, macro)):
+ macs = self.macroStack
+ if len(macs) == 1:
+ entering = macs[-1][2]
+ if not entering:
+ macs.append(None)
+ self.interpret(macro)
+ assert macs[-1] is None
+ macs.pop()
+ return
+ self.interpret(macro)
+ bytecode_handlers["defineMacro"] = do_defineMacro
+
+ def do_useMacro(self, (macroName, macroExpr, compiledSlots, block)):
+ if not self.metal:
+ self.interpret(block)
+ return
+ macro = self.engine.evaluateMacro(macroExpr)
+ if macro is self.Default:
+ macro = block
+ else:
+ if not isCurrentVersion(macro):
+ raise METALError("macro %s has incompatible version %s" %
+ (`macroName`, `getProgramVersion(macro)`),
+ self.position)
+ mode = getProgramMode(macro)
+ if mode != (self.html and "html" or "xml"):
+ raise METALError("macro %s has incompatible mode %s" %
+ (`macroName`, `mode`), self.position)
+ self.pushMacro(macroName, compiledSlots)
+ prev_source = self.sourceFile
+ self.interpret(macro)
+ if self.sourceFile != prev_source:
+ self.engine.setSourceFile(prev_source)
+ self.sourceFile = prev_source
+ self.popMacro()
+ bytecode_handlers["useMacro"] = do_useMacro
+
+ def do_fillSlot(self, (slotName, block)):
+ # This is only executed if the enclosing 'use-macro' evaluates
+ # to 'default'.
+ self.interpret(block)
+ bytecode_handlers["fillSlot"] = do_fillSlot
+
+ def do_defineSlot(self, (slotName, block)):
+ if not self.metal:
+ self.interpret(block)
+ return
+ macs = self.macroStack
+ if macs and macs[-1] is not None:
+ macroName, slots = self.popMacro()[:2]
+ slot = slots.get(slotName)
+ if slot is not None:
+ prev_source = self.sourceFile
+ self.interpret(slot)
+ if self.sourceFile != prev_source:
+ self.engine.setSourceFile(prev_source)
+ self.sourceFile = prev_source
+ self.pushMacro(macroName, slots, entering=0)
+ return
+ self.pushMacro(macroName, slots)
+ # Falling out of the 'if' allows the macro to be interpreted.
+ self.interpret(block)
+ bytecode_handlers["defineSlot"] = do_defineSlot
+
+ def do_onError(self, (block, handler)):
+ self.interpret(block)
+
+ def do_onError_tal(self, (block, handler)):
+ state = self.saveState()
+ self.stream = stream = self.StringIO()
+ self._stream_write = stream.write
+ try:
+ self.interpret(block)
+ except:
+ exc = sys.exc_info()[1]
+ self.restoreState(state)
+ engine = self.engine
+ engine.beginScope()
+ error = engine.createErrorInfo(exc, self.position)
+ engine.setLocal('error', error)
+ try:
+ self.interpret(handler)
+ finally:
+ engine.endScope()
+ else:
+ self.restoreOutputState(state)
+ self.stream_write(stream.getvalue())
+ bytecode_handlers["onError"] = do_onError
+
+ bytecode_handlers_tal = bytecode_handlers.copy()
+ bytecode_handlers_tal["rawtextBeginScope"] = do_rawtextBeginScope_tal
+ bytecode_handlers_tal["beginScope"] = do_beginScope_tal
+ bytecode_handlers_tal["setLocal"] = do_setLocal_tal
+ bytecode_handlers_tal["setGlobal"] = do_setGlobal_tal
+ bytecode_handlers_tal["insertStructure"] = do_insertStructure_tal
+ bytecode_handlers_tal["insertText"] = do_insertText_tal
+ bytecode_handlers_tal["loop"] = do_loop_tal
+ bytecode_handlers_tal["onError"] = do_onError_tal
+ bytecode_handlers_tal["<attrAction>"] = attrAction_tal
+ bytecode_handlers_tal["optTag"] = do_optTag_tal
+
+
+class FasterStringIO(StringIO):
+ """Append-only version of StringIO.
+
+ This let's us have a much faster write() method.
+ """
+ def close(self):
+ if not self.closed:
+ self.write = _write_ValueError
+ StringIO.close(self)
+
+ def seek(self, pos, mode=0):
+ raise RuntimeError("FasterStringIO.seek() not allowed")
+
+ def write(self, s):
+ #assert self.pos == self.len
+ self.buflist.append(s)
+ self.len = self.pos = self.pos + len(s)
+
+
+def _write_ValueError(s):
+ raise ValueError, "I/O operation on closed file"
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/TALParser.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/TALParser.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,144 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+Parse XML and compile to TALInterpreter intermediate code.
+"""
+
+from XMLParser import XMLParser
+from TALDefs import XML_NS, ZOPE_I18N_NS, ZOPE_METAL_NS, ZOPE_TAL_NS
+from TALGenerator import TALGenerator
+
+class TALParser(XMLParser):
+
+ ordered_attributes = 1
+
+ def __init__(self, gen=None): # Override
+ XMLParser.__init__(self)
+ if gen is None:
+ gen = TALGenerator()
+ self.gen = gen
+ self.nsStack = []
+ self.nsDict = {XML_NS: 'xml'}
+ self.nsNew = []
+
+ def getCode(self):
+ return self.gen.getCode()
+
+ def getWarnings(self):
+ return ()
+
+ def StartNamespaceDeclHandler(self, prefix, uri):
+ self.nsStack.append(self.nsDict.copy())
+ self.nsDict[uri] = prefix
+ self.nsNew.append((prefix, uri))
+
+ def EndNamespaceDeclHandler(self, prefix):
+ self.nsDict = self.nsStack.pop()
+
+ def StartElementHandler(self, name, attrs):
+ if self.ordered_attributes:
+ # attrs is a list of alternating names and values
+ attrlist = []
+ for i in range(0, len(attrs), 2):
+ key = attrs[i]
+ value = attrs[i+1]
+ attrlist.append((key, value))
+ else:
+ # attrs is a dict of {name: value}
+ attrlist = attrs.items()
+ attrlist.sort() # For definiteness
+ name, attrlist, taldict, metaldict, i18ndict \
+ = self.process_ns(name, attrlist)
+ attrlist = self.xmlnsattrs() + attrlist
+ self.gen.emitStartElement(name, attrlist, taldict, metaldict, i18ndict)
+
+ def process_ns(self, name, attrlist):
+ taldict = {}
+ metaldict = {}
+ i18ndict = {}
+ fixedattrlist = []
+ name, namebase, namens = self.fixname(name)
+ for key, value in attrlist:
+ key, keybase, keyns = self.fixname(key)
+ ns = keyns or namens # default to tag namespace
+ item = key, value
+ if ns == 'metal':
+ metaldict[keybase] = value
+ item = item + ("metal",)
+ elif ns == 'tal':
+ taldict[keybase] = value
+ item = item + ("tal",)
+ elif ns == 'i18n':
+ assert 0, "dealing with i18n: " + `(keybase, value)`
+ i18ndict[keybase] = value
+ item = item + ('i18n',)
+ fixedattrlist.append(item)
+ if namens in ('metal', 'tal', 'i18n'):
+ taldict['tal tag'] = namens
+ return name, fixedattrlist, taldict, metaldict, i18ndict
+
+ def xmlnsattrs(self):
+ newlist = []
+ for prefix, uri in self.nsNew:
+ if prefix:
+ key = "xmlns:" + prefix
+ else:
+ key = "xmlns"
+ if uri in (ZOPE_METAL_NS, ZOPE_TAL_NS, ZOPE_I18N_NS):
+ item = (key, uri, "xmlns")
+ else:
+ item = (key, uri)
+ newlist.append(item)
+ self.nsNew = []
+ return newlist
+
+ def fixname(self, name):
+ if ' ' in name:
+ uri, name = name.split(' ')
+ prefix = self.nsDict[uri]
+ prefixed = name
+ if prefix:
+ prefixed = "%s:%s" % (prefix, name)
+ ns = 'x'
+ if uri == ZOPE_TAL_NS:
+ ns = 'tal'
+ elif uri == ZOPE_METAL_NS:
+ ns = 'metal'
+ elif uri == ZOPE_I18N_NS:
+ ns = 'i18n'
+ return (prefixed, name, ns)
+ return (name, name, None)
+
+ def EndElementHandler(self, name):
+ name = self.fixname(name)[0]
+ self.gen.emitEndElement(name)
+
+ def DefaultHandler(self, text):
+ self.gen.emitRawText(text)
+
+def test():
+ import sys
+ p = TALParser()
+ file = "tests/input/test01.xml"
+ if sys.argv[1:]:
+ file = sys.argv[1]
+ p.parseFile(file)
+ program, macros = p.getCode()
+ from TALInterpreter import TALInterpreter
+ from DummyEngine import DummyEngine
+ engine = DummyEngine(macros)
+ TALInterpreter(program, macros, engine, sys.stdout, wrap=0)()
+
+if __name__ == "__main__":
+ test()
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/TranslationContext.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/TranslationContext.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,41 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Translation context object for the TALInterpreter's I18N support.
+
+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 $
+"""
+
+DEFAULT_DOMAIN = "default"
+
+class TranslationContext:
+ """Information about the I18N settings of a TAL processor."""
+
+ def __init__(self, parent=None, domain=None, target=None, source=None):
+ if parent:
+ if not domain:
+ domain = parent.domain
+ if not target:
+ target = parent.target
+ if not source:
+ source = parent.source
+ elif domain is None:
+ domain = DEFAULT_DOMAIN
+
+ self.parent = parent
+ self.domain = domain
+ self.target = target
+ self.source = source
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/XMLParser.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/XMLParser.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,93 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+# Modifications for Roundup:
+# 1. commented out zLOG references
+"""
+Generic expat-based XML parser base class.
+"""
+
+#import zLOG
+
+class XMLParser:
+
+ ordered_attributes = 0
+
+ handler_names = [
+ "StartElementHandler",
+ "EndElementHandler",
+ "ProcessingInstructionHandler",
+ "CharacterDataHandler",
+ "UnparsedEntityDeclHandler",
+ "NotationDeclHandler",
+ "StartNamespaceDeclHandler",
+ "EndNamespaceDeclHandler",
+ "CommentHandler",
+ "StartCdataSectionHandler",
+ "EndCdataSectionHandler",
+ "DefaultHandler",
+ "DefaultHandlerExpand",
+ "NotStandaloneHandler",
+ "ExternalEntityRefHandler",
+ "XmlDeclHandler",
+ "StartDoctypeDeclHandler",
+ "EndDoctypeDeclHandler",
+ "ElementDeclHandler",
+ "AttlistDeclHandler"
+ ]
+
+ def __init__(self, encoding=None):
+ self.parser = p = self.createParser()
+ if self.ordered_attributes:
+ try:
+ self.parser.ordered_attributes = self.ordered_attributes
+ except AttributeError:
+ #zLOG.LOG("TAL.XMLParser", zLOG.INFO,
+ # "Can't set ordered_attributes")
+ self.ordered_attributes = 0
+ for name in self.handler_names:
+ method = getattr(self, name, None)
+ if method is not None:
+ try:
+ setattr(p, name, method)
+ except AttributeError:
+ #zLOG.LOG("TAL.XMLParser", zLOG.PROBLEM,
+ # "Can't set expat handler %s" % name)
+ pass
+
+ def createParser(self, encoding=None):
+ global XMLParseError
+ try:
+ from Products.ParsedXML.Expat import pyexpat
+ XMLParseError = pyexpat.ExpatError
+ return pyexpat.ParserCreate(encoding, ' ')
+ except ImportError:
+ from xml.parsers import expat
+ XMLParseError = expat.ExpatError
+ return expat.ParserCreate(encoding, ' ')
+
+ def parseFile(self, filename):
+ self.parseStream(open(filename))
+
+ def parseString(self, s):
+ self.parser.Parse(s, 1)
+
+ def parseURL(self, url):
+ import urllib
+ self.parseStream(urllib.urlopen(url))
+
+ def parseStream(self, stream):
+ self.parser.ParseFile(stream)
+
+ def parseFragment(self, s, end=0):
+ self.parser.Parse(s, end)
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/__init__.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/__init__.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,14 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+""" Template Attribute Language package """
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/markupbase.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/markupbase.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,312 @@
+"""Shared support for scanning document type declarations in HTML and XHTML."""
+
+import re, string
+
+_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*').match
+_declstringlit_match = re.compile(r'(\'[^\']*\'|"[^"]*")\s*').match
+
+del re
+
+
+class ParserBase:
+ """Parser base class which provides some common support methods used
+ by the SGML/HTML and XHTML parsers."""
+
+ def reset(self):
+ self.lineno = 1
+ self.offset = 0
+
+ def getpos(self):
+ """Return current line number and offset."""
+ return self.lineno, self.offset
+
+ def error(self, message):
+ """Return an error, showing current line number and offset.
+
+ Concrete subclasses *must* override this method.
+ """
+ raise NotImplementedError
+
+ # Internal -- update line number and offset. This should be
+ # called for each piece of data exactly once, in order -- in other
+ # words the concatenation of all the input strings to this
+ # function should be exactly the entire input.
+ def updatepos(self, i, j):
+ if i >= j:
+ return j
+ rawdata = self.rawdata
+ nlines = rawdata.count("\n", i, j)
+ if nlines:
+ self.lineno = self.lineno + nlines
+ pos = rawdata.rindex("\n", i, j) # Should not fail
+ self.offset = j-(pos+1)
+ else:
+ self.offset = self.offset + j-i
+ return j
+
+ _decl_otherchars = ''
+
+ # Internal -- parse declaration (for use by subclasses).
+ def parse_declaration(self, i):
+ # This is some sort of declaration; in "HTML as
+ # deployed," this should only be the document type
+ # declaration ("<!DOCTYPE html...>").
+ rawdata = self.rawdata
+ import sys
+ j = i + 2
+ assert rawdata[i:j] == "<!", "unexpected call to parse_declaration"
+ if rawdata[j:j+1] in ("-", ""):
+ # Start of comment followed by buffer boundary,
+ # or just a buffer boundary.
+ return -1
+ # in practice, this should look like: ((name|stringlit) S*)+ '>'
+ n = len(rawdata)
+ decltype, j = self._scan_name(j, i)
+ if j < 0:
+ return j
+ if decltype == "doctype":
+ self._decl_otherchars = ''
+ while j < n:
+ c = rawdata[j]
+ if c == ">":
+ # end of declaration syntax
+ data = rawdata[i+2:j]
+ if decltype == "doctype":
+ self.handle_decl(data)
+ else:
+ self.unknown_decl(data)
+ return j + 1
+ if c in "\"'":
+ m = _declstringlit_match(rawdata, j)
+ if not m:
+ return -1 # incomplete
+ j = m.end()
+ elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
+ name, j = self._scan_name(j, i)
+ elif c in self._decl_otherchars:
+ j = j + 1
+ elif c == "[":
+ if decltype == "doctype":
+ j = self._parse_doctype_subset(j + 1, i)
+ else:
+ self.error("unexpected '[' char in declaration")
+ else:
+ self.error(
+ "unexpected %s char in declaration" % `rawdata[j]`)
+ if j < 0:
+ return j
+ return -1 # incomplete
+
+ # Internal -- scan past the internal subset in a <!DOCTYPE declaration,
+ # returning the index just past any whitespace following the trailing ']'.
+ def _parse_doctype_subset(self, i, declstartpos):
+ rawdata = self.rawdata
+ n = len(rawdata)
+ j = i
+ while j < n:
+ c = rawdata[j]
+ if c == "<":
+ s = rawdata[j:j+2]
+ if s == "<":
+ # end of buffer; incomplete
+ return -1
+ if s != "<!":
+ self.updatepos(declstartpos, j + 1)
+ self.error("unexpected char in internal subset (in %s)"
+ % `s`)
+ if (j + 2) == n:
+ # end of buffer; incomplete
+ return -1
+ if (j + 4) > n:
+ # end of buffer; incomplete
+ return -1
+ if rawdata[j:j+4] == "<!--":
+ j = self.parse_comment(j, report=0)
+ if j < 0:
+ return j
+ continue
+ name, j = self._scan_name(j + 2, declstartpos)
+ if j == -1:
+ return -1
+ if name not in ("attlist", "element", "entity", "notation"):
+ self.updatepos(declstartpos, j + 2)
+ self.error(
+ "unknown declaration %s in internal subset" % `name`)
+ # handle the individual names
+ meth = getattr(self, "_parse_doctype_" + name)
+ j = meth(j, declstartpos)
+ if j < 0:
+ return j
+ elif c == "%":
+ # parameter entity reference
+ if (j + 1) == n:
+ # end of buffer; incomplete
+ return -1
+ s, j = self._scan_name(j + 1, declstartpos)
+ if j < 0:
+ return j
+ if rawdata[j] == ";":
+ j = j + 1
+ elif c == "]":
+ j = j + 1
+ while j < n and rawdata[j] in string.whitespace:
+ j = j + 1
+ if j < n:
+ if rawdata[j] == ">":
+ return j
+ self.updatepos(declstartpos, j)
+ self.error("unexpected char after internal subset")
+ else:
+ return -1
+ elif c in string.whitespace:
+ j = j + 1
+ else:
+ self.updatepos(declstartpos, j)
+ self.error("unexpected char %s in internal subset" % `c`)
+ # end of buffer reached
+ return -1
+
+ # Internal -- scan past <!ELEMENT declarations
+ def _parse_doctype_element(self, i, declstartpos):
+ rawdata = self.rawdata
+ n = len(rawdata)
+ name, j = self._scan_name(i, declstartpos)
+ if j == -1:
+ return -1
+ # style content model; just skip until '>'
+ if '>' in rawdata[j:]:
+ return rawdata.find(">", j) + 1
+ return -1
+
+ # Internal -- scan past <!ATTLIST declarations
+ def _parse_doctype_attlist(self, i, declstartpos):
+ rawdata = self.rawdata
+ name, j = self._scan_name(i, declstartpos)
+ c = rawdata[j:j+1]
+ if c == "":
+ return -1
+ if c == ">":
+ return j + 1
+ while 1:
+ # scan a series of attribute descriptions; simplified:
+ # name type [value] [#constraint]
+ name, j = self._scan_name(j, declstartpos)
+ if j < 0:
+ return j
+ c = rawdata[j:j+1]
+ if c == "":
+ return -1
+ if c == "(":
+ # an enumerated type; look for ')'
+ if ")" in rawdata[j:]:
+ j = rawdata.find(")", j) + 1
+ else:
+ return -1
+ while rawdata[j:j+1].isspace():
+ j = j + 1
+ if not rawdata[j:]:
+ # end of buffer, incomplete
+ return -1
+ else:
+ name, j = self._scan_name(j, declstartpos)
+ c = rawdata[j:j+1]
+ if not c:
+ return -1
+ if c in "'\"":
+ m = _declstringlit_match(rawdata, j)
+ if m:
+ j = m.end()
+ else:
+ return -1
+ c = rawdata[j:j+1]
+ if not c:
+ return -1
+ if c == "#":
+ if rawdata[j:] == "#":
+ # end of buffer
+ return -1
+ name, j = self._scan_name(j + 1, declstartpos)
+ if j < 0:
+ return j
+ c = rawdata[j:j+1]
+ if not c:
+ return -1
+ if c == '>':
+ # all done
+ return j + 1
+
+ # Internal -- scan past <!NOTATION declarations
+ def _parse_doctype_notation(self, i, declstartpos):
+ name, j = self._scan_name(i, declstartpos)
+ if j < 0:
+ return j
+ rawdata = self.rawdata
+ while 1:
+ c = rawdata[j:j+1]
+ if not c:
+ # end of buffer; incomplete
+ return -1
+ if c == '>':
+ return j + 1
+ if c in "'\"":
+ m = _declstringlit_match(rawdata, j)
+ if not m:
+ return -1
+ j = m.end()
+ else:
+ name, j = self._scan_name(j, declstartpos)
+ if j < 0:
+ return j
+
+ # Internal -- scan past <!ENTITY declarations
+ def _parse_doctype_entity(self, i, declstartpos):
+ rawdata = self.rawdata
+ if rawdata[i:i+1] == "%":
+ j = i + 1
+ while 1:
+ c = rawdata[j:j+1]
+ if not c:
+ return -1
+ if c in string.whitespace:
+ j = j + 1
+ else:
+ break
+ else:
+ j = i
+ name, j = self._scan_name(j, declstartpos)
+ if j < 0:
+ return j
+ while 1:
+ c = self.rawdata[j:j+1]
+ if not c:
+ return -1
+ if c in "'\"":
+ m = _declstringlit_match(rawdata, j)
+ if m:
+ j = m.end()
+ else:
+ return -1 # incomplete
+ elif c == ">":
+ return j + 1
+ else:
+ name, j = self._scan_name(j, declstartpos)
+ if j < 0:
+ return j
+
+ # Internal -- scan a name token and the new position and the token, or
+ # return -1 if we've reached the end of the buffer.
+ def _scan_name(self, i, declstartpos):
+ rawdata = self.rawdata
+ n = len(rawdata)
+ if i == n:
+ return None, -1
+ m = _declname_match(rawdata, i)
+ if m:
+ s = m.group()
+ name = s.strip()
+ if (i + len(s)) == n:
+ return None, -1 # end of buffer
+ return name.lower(), m.end()
+ else:
+ self.updatepos(declstartpos, i)
+ self.error("expected name token")
Added: tracker/vendor/roundup/current/roundup/cgi/TAL/talgettext.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TAL/talgettext.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,318 @@
+#!/usr/bin/env python
+##############################################################################
+#
+# Copyright (c) 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+# Modifications for Roundup:
+# 1. commented out ITALES references
+# 2. escape quotes and line feeds in msgids
+# 3. don't collect empty msgids
+
+"""Program to extract internationalization markup from Page Templates.
+
+Once you have marked up a Page Template file with i18n: namespace tags, use
+this program to extract GNU gettext .po file entries.
+
+Usage: talgettext.py [options] files
+Options:
+ -h / --help
+ Print this message and exit.
+ -o / --output <file>
+ Output the translation .po file to <file>.
+ -u / --update <file>
+ Update the existing translation <file> with any new translation strings
+ found.
+"""
+
+import sys
+import time
+import getopt
+import traceback
+
+from roundup.cgi.TAL.HTMLTALParser import HTMLTALParser
+from roundup.cgi.TAL.TALInterpreter import TALInterpreter
+from roundup.cgi.TAL.DummyEngine import DummyEngine
+#from ITALES import ITALESEngine
+from roundup.cgi.TAL.TALDefs import TALESError
+
+__version__ = '$Revision: 1.6 $'
+
+pot_header = '''\
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\\n"
+"POT-Creation-Date: %(time)s\\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"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=CHARSET\\n"
+"Content-Transfer-Encoding: ENCODING\\n"
+"Generated-By: talgettext.py %(version)s\\n"
+'''
+
+NLSTR = '"\n"'
+
+try:
+ True
+except NameError:
+ True=1
+ False=0
+
+def usage(code, msg=''):
+ # Python 2.1 required
+ print >> sys.stderr, __doc__
+ if msg:
+ print >> sys.stderr, msg
+ sys.exit(code)
+
+
+class POTALInterpreter(TALInterpreter):
+ def translate(self, msgid, default, i18ndict=None, obj=None):
+ # XXX is this right?
+ if i18ndict is None:
+ i18ndict = {}
+ if obj:
+ i18ndict.update(obj)
+ # XXX Mmmh, it seems that sometimes the msgid is None; is that really
+ # possible?
+ if msgid is None:
+ return None
+ # XXX We need to pass in one of context or target_language
+ return self.engine.translate(msgid, self.i18nContext.domain, i18ndict,
+ position=self.position, default=default)
+
+
+class POEngine(DummyEngine):
+ #__implements__ = ITALESEngine
+
+ def __init__(self, macros=None):
+ self.catalog = {}
+ DummyEngine.__init__(self, macros)
+
+ def evaluate(*args):
+ return '' # who cares
+
+ def evaluatePathOrVar(*args):
+ return '' # who cares
+
+ def evaluateSequence(self, expr):
+ return (0,) # dummy
+
+ def evaluateBoolean(self, expr):
+ return True # dummy
+
+ def translate(self, msgid, domain=None, mapping=None, default=None,
+ # XXX position is not part of the ITALESEngine
+ # interface
+ position=None):
+
+ if not msgid: return 'x'
+
+ if domain not in self.catalog:
+ self.catalog[domain] = {}
+ domain = self.catalog[domain]
+
+ if msgid not in domain:
+ domain[msgid] = []
+ domain[msgid].append((self.file, position))
+ return 'x'
+
+
+class UpdatePOEngine(POEngine):
+ """A slightly-less braindead POEngine which supports loading an existing
+ .po file first."""
+
+ def __init__ (self, macros=None, filename=None):
+ POEngine.__init__(self, macros)
+
+ self._filename = filename
+ self._loadFile()
+ self.base = self.catalog
+ self.catalog = {}
+
+ def __add(self, id, s, fuzzy):
+ "Add a non-fuzzy translation to the dictionary."
+ if not fuzzy and str:
+ # check for multi-line values and munge them appropriately
+ if '\n' in s:
+ lines = s.rstrip().split('\n')
+ s = NLSTR.join(lines)
+ self.catalog[id] = s
+
+ def _loadFile(self):
+ # shamelessly cribbed from Python's Tools/i18n/msgfmt.py
+ # 25-Mar-2003 Nathan R. Yergler (nathan at zope.org)
+ # 14-Apr-2003 Hacked by Barry Warsaw (barry at zope.com)
+
+ ID = 1
+ STR = 2
+
+ try:
+ lines = open(self._filename).readlines()
+ except IOError, msg:
+ print >> sys.stderr, msg
+ sys.exit(1)
+
+ section = None
+ fuzzy = False
+
+ # Parse the catalog
+ lno = 0
+ for l in lines:
+ lno += True
+ # If we get a comment line after a msgstr, this is a new entry
+ if l[0] == '#' and section == STR:
+ self.__add(msgid, msgstr, fuzzy)
+ section = None
+ fuzzy = False
+ # Record a fuzzy mark
+ if l[:2] == '#,' and l.find('fuzzy'):
+ fuzzy = True
+ # Skip comments
+ if l[0] == '#':
+ continue
+ # Now we are in a msgid section, output previous section
+ if l.startswith('msgid'):
+ if section == STR:
+ self.__add(msgid, msgstr, fuzzy)
+ section = ID
+ l = l[5:]
+ msgid = msgstr = ''
+ # Now we are in a msgstr section
+ elif l.startswith('msgstr'):
+ section = STR
+ l = l[6:]
+ # Skip empty lines
+ if not l.strip():
+ continue
+ # XXX: Does this always follow Python escape semantics?
+ l = eval(l)
+ if section == ID:
+ msgid += l
+ elif section == STR:
+ msgstr += '%s\n' % l
+ else:
+ print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \
+ 'before:'
+ print >> sys.stderr, l
+ sys.exit(1)
+ # Add last entry
+ if section == STR:
+ self.__add(msgid, msgstr, fuzzy)
+
+ def evaluate(self, expression):
+ try:
+ return POEngine.evaluate(self, expression)
+ except TALESError:
+ pass
+
+ def evaluatePathOrVar(self, expr):
+ return 'who cares'
+
+ def translate(self, msgid, domain=None, mapping=None, default=None,
+ position=None):
+ if msgid not in self.base:
+ POEngine.translate(self, msgid, domain, mapping, default, position)
+ return 'x'
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:],
+ 'ho:u:',
+ ['help', 'output=', 'update='])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ outfile = None
+ engine = None
+ update_mode = False
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-o', '--output'):
+ outfile = arg
+ elif opt in ('-u', '--update'):
+ update_mode = True
+ if outfile is None:
+ outfile = arg
+ engine = UpdatePOEngine(filename=arg)
+
+ if not args:
+ print 'nothing to do'
+ return
+
+ # We don't care about the rendered output of the .pt file
+ class Devnull:
+ def write(self, s):
+ pass
+
+ # check if we've already instantiated an engine;
+ # if not, use the stupidest one available
+ if not engine:
+ engine = POEngine()
+
+ # process each file specified
+ for filename in args:
+ try:
+ engine.file = filename
+ p = HTMLTALParser()
+ p.parseFile(filename)
+ program, macros = p.getCode()
+ POTALInterpreter(program, macros, engine, stream=Devnull(),
+ metal=False)()
+ except: # Hee hee, I love bare excepts!
+ print 'There was an error processing', filename
+ traceback.print_exc()
+
+ # Now output the keys in the engine. Write them to a file if --output or
+ # --update was specified; otherwise use standard out.
+ if (outfile is None):
+ outfile = sys.stdout
+ else:
+ outfile = file(outfile, update_mode and "a" or "w")
+
+ catalog = {}
+ for domain in engine.catalog.keys():
+ catalog.update(engine.catalog[domain])
+
+ messages = catalog.copy()
+ try:
+ messages.update(engine.base)
+ except AttributeError:
+ pass
+ if '' not in messages:
+ print >> outfile, pot_header % {'time': time.ctime(),
+ 'version': __version__}
+
+ msgids = catalog.keys()
+ # XXX: You should not sort by msgid, but by filename and position. (SR)
+ msgids.sort()
+ for msgid in msgids:
+ positions = catalog[msgid]
+ for filename, position in positions:
+ outfile.write('#: %s:%s\n' % (filename, position[0]))
+
+ outfile.write('msgid "%s"\n'
+ % msgid.replace('"', '\\"').replace("\n", '\\n"\n"'))
+ outfile.write('msgstr ""\n')
+ outfile.write('\n')
+
+
+if __name__ == '__main__':
+ main()
Added: tracker/vendor/roundup/current/roundup/cgi/TranslationService.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/TranslationService.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,111 @@
+# TranslationService for Roundup templates
+#
+# This module is free software, you may redistribute it
+# and/or modify under the same terms as Python.
+#
+# This module provides National Language Support
+# for Roundup templating - much like roundup.i18n
+# module for Roundup command line interface.
+# The only difference is that translator objects
+# returned by get_translation() have one additional
+# method which is used by TAL engines:
+#
+# translate(domain, msgid, mapping, context, target_language, default)
+#
+
+__version__ = "$Revision: 1.2 $"[11:-2]
+__date__ = "$Date: 2004/10/23 14:04:23 $"[7:-2]
+
+from roundup import i18n
+from roundup.cgi.PageTemplates import Expressions, PathIterator, TALES
+from roundup.cgi.TAL import TALInterpreter
+
+### Translation classes
+
+class TranslationServiceMixin:
+
+ OUTPUT_ENCODING = "utf-8"
+
+ def translate(self, domain, msgid, mapping=None,
+ context=None, target_language=None, default=None
+ ):
+ _msg = self.gettext(msgid)
+ #print ("TRANSLATE", msgid, _msg, mapping, context)
+ _msg = TALInterpreter.interpolate(_msg, mapping)
+ return _msg
+
+ def gettext(self, msgid):
+ return self.ugettext(msgid).encode(self.OUTPUT_ENCODING)
+
+ def ngettext(self, singular, plural, number):
+ return self.ungettext(singular, plural, number).encode(
+ self.OUTPUT_ENCODING)
+
+class TranslationService(TranslationServiceMixin, i18n.RoundupTranslations):
+ pass
+
+class NullTranslationService(TranslationServiceMixin,
+ i18n.RoundupNullTranslations
+):
+ pass
+
+### TAL patching
+#
+# Template Attribute Language (TAL) uses only global translation service,
+# which is not thread-safe. We will use context variable 'i18n'
+# to access request-dependent transalation service (with domain
+# and target language set during initializations of the roundup
+# client interface.
+#
+
+class Context(TALES.Context):
+
+ def __init__(self, compiler, contexts):
+ TALES.Context.__init__(self, compiler, contexts)
+ if not self.contexts.get('i18n', None):
+ # if the context contains no TranslationService,
+ # create default one
+ self.contexts['i18n'] = get_translation()
+ self.i18n = self.contexts['i18n']
+
+ def translate(self, domain, msgid, mapping=None,
+ context=None, target_language=None, default=None):
+ if context is None:
+ context = self.contexts.get('here')
+ return self.i18n.translate(domain, msgid,
+ mapping=mapping, context=context, default=default,
+ target_language=target_language)
+
+class Engine(TALES.Engine):
+
+ def getContext(self, contexts=None, **kwcontexts):
+ if contexts is not None:
+ if kwcontexts:
+ kwcontexts.update(contexts)
+ else:
+ kwcontexts = contexts
+ return Context(self, kwcontexts)
+
+# patching TAL like this is a dirty hack,
+# but i see no other way to specify different Context class
+Expressions._engine = Engine(PathIterator.Iterator)
+Expressions.installHandlers(Expressions._engine)
+
+### main API function
+
+def get_translation(language=None, tracker_home=None,
+ translation_class=TranslationService,
+ null_translation_class=NullTranslationService
+):
+ """Return Translation object for given language and domain
+
+ Arguments 'translation_class' and 'null_translation_class'
+ specify the classes that are instantiated for existing
+ and non-existing translations, respectively.
+ """
+ return i18n.get_translation(language=language,
+ tracker_home=tracker_home,
+ translation_class=translation_class,
+ null_translation_class=null_translation_class)
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/cgi/ZTUtils/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/ZTUtils/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2 @@
+*.pyc
+*.pyo
Added: tracker/vendor/roundup/current/roundup/cgi/ZTUtils/Batch.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/ZTUtils/Batch.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,120 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+__doc__='''Batch class, for iterating over a sequence in batches
+
+$Id: Batch.py,v 1.3 2004/02/11 23:55:09 richard Exp $'''
+__docformat__ = 'restructuredtext'
+__version__='$Revision: 1.3 $'[11:-2]
+
+class LazyPrevBatch:
+ def __of__(self, parent):
+ return Batch(parent._sequence, parent._size,
+ parent.first - parent._size + parent.overlap, 0,
+ parent.orphan, parent.overlap)
+
+class LazyNextBatch:
+ def __of__(self, parent):
+ try: parent._sequence[parent.end]
+ except IndexError: return None
+ return Batch(parent._sequence, parent._size,
+ parent.end - parent.overlap, 0,
+ parent.orphan, parent.overlap)
+
+class LazySequenceLength:
+ def __of__(self, parent):
+ parent.sequence_length = l = len(parent._sequence)
+ return l
+
+class Batch:
+ """Create a sequence batch"""
+ __allow_access_to_unprotected_subobjects__ = 1
+
+ previous = LazyPrevBatch()
+ next = LazyNextBatch()
+ sequence_length = LazySequenceLength()
+
+ def __init__(self, sequence, size, start=0, end=0,
+ orphan=0, overlap=0):
+ '''Encapsulate "sequence" in batches of "size".
+
+ Arguments: "start" and "end" are 0-based indexes into the
+ sequence. If the next batch would contain no more than
+ "orphan" elements, it is combined with the current batch.
+ "overlap" is the number of elements shared by adjacent
+ batches. If "size" is not specified, it is computed from
+ "start" and "end". Failing that, it is 7.
+
+ Attributes: Note that the "start" attribute, unlike the
+ argument, is a 1-based index (I know, lame). "first" is the
+ 0-based index. "length" is the actual number of elements in
+ the batch.
+
+ "sequence_length" is the length of the original, unbatched, sequence
+ '''
+
+ start = start + 1
+
+ start,end,sz = opt(start,end,size,orphan,sequence)
+
+ self._sequence = sequence
+ self.size = sz
+ self._size = size
+ self.start = start
+ self.end = end
+ self.orphan = orphan
+ self.overlap = overlap
+ self.first = max(start - 1, 0)
+ self.length = self.end - self.first
+ if self.first == 0:
+ self.previous = None
+
+
+ def __getitem__(self, index):
+ if index < 0:
+ if index + self.end < self.first: raise IndexError, index
+ return self._sequence[index + self.end]
+
+ if index >= self.length: raise IndexError, index
+ return self._sequence[index + self.first]
+
+ def __len__(self):
+ return self.length
+
+def opt(start,end,size,orphan,sequence):
+ if size < 1:
+ if start > 0 and end > 0 and end >= start:
+ size=end+1-start
+ else: size=7
+
+ if start > 0:
+
+ try: sequence[start-1]
+ except IndexError: start=len(sequence)
+
+ if end > 0:
+ if end < start: end=start
+ else:
+ end=start+size-1
+ try: sequence[end+orphan-1]
+ except IndexError: end=len(sequence)
+ elif end > 0:
+ try: sequence[end-1]
+ except IndexError: end=len(sequence)
+ start=end+1-size
+ if start - 1 < orphan: start=1
+ else:
+ start=1
+ end=start+size-1
+ try: sequence[end+orphan-1]
+ except IndexError: end=len(sequence)
+ return start,end,size
Added: tracker/vendor/roundup/current/roundup/cgi/ZTUtils/Iterator.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/ZTUtils/Iterator.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,195 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+__doc__='''Iterator class
+
+Unlike the builtin iterators of Python 2.2+, these classes are
+designed to maintain information about the state of an iteration.
+The Iterator() function accepts either a sequence or a Python
+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 $'''
+__docformat__ = 'restructuredtext'
+__version__='$Revision: 1.4 $'[11:-2]
+
+import string
+
+class Iterator:
+ '''Simple Iterator class'''
+
+ __allow_access_to_unprotected_subobjects__ = 1
+
+ nextIndex = 0
+ def __init__(self, seq):
+ self.seq = iter(seq) # force seq to be an iterator
+ self._inner = iterInner
+ self._prep_next = iterInner.prep_next
+
+ def __getattr__(self, name):
+ try:
+ inner = getattr(self._inner, 'it_' + name)
+ except AttributeError:
+ raise AttributeError, name
+ return inner(self)
+
+ def next(self):
+ if not (hasattr(self, '_next') or self._prep_next(self)):
+ return 0
+ self.index = i = self.nextIndex
+ self.nextIndex = i+1
+ self._advance(self)
+ return 1
+
+ def _advance(self, it):
+ self.item = self._next
+ del self._next
+ del self.end
+ self._advance = self._inner.advance
+ self.start = 1
+
+ def number(self): return self.nextIndex
+
+ def even(self): return not self.index % 2
+
+ def odd(self): return self.index % 2
+
+ def letter(self, base=ord('a'), radix=26):
+ index = self.index
+ s = ''
+ while 1:
+ index, off = divmod(index, radix)
+ s = chr(base + off) + s
+ if not index: return s
+
+ def Letter(self):
+ return self.letter(base=ord('A'))
+
+ def Roman(self, rnvalues=(
+ (1000,'M'),(900,'CM'),(500,'D'),(400,'CD'),
+ (100,'C'),(90,'XC'),(50,'L'),(40,'XL'),
+ (10,'X'),(9,'IX'),(5,'V'),(4,'IV'),(1,'I')) ):
+ n = self.index + 1
+ s = ''
+ for v, r in rnvalues:
+ rct, n = divmod(n, v)
+ s = s + r * rct
+ return s
+
+ def roman(self, lower=string.lower):
+ return lower(self.Roman())
+
+ def first(self, name=None):
+ if self.start: return 1
+ return not self.same_part(name, self._last, self.item)
+
+ def last(self, name=None):
+ if self.end: return 1
+ return not self.same_part(name, self.item, self._next)
+
+ def same_part(self, name, ob1, ob2):
+ if name is None:
+ return ob1 == ob2
+ no = []
+ return getattr(ob1, name, no) == getattr(ob2, name, no) is not no
+
+ def __iter__(self):
+ return IterIter(self)
+
+class InnerBase:
+ '''Base Inner class for Iterators'''
+ # Prep sets up ._next and .end
+ def prep_next(self, it):
+ it.next = self.no_next
+ it.end = 1
+ return 0
+
+ # Advance knocks them down
+ def advance(self, it):
+ it._last = it.item
+ it.item = it._next
+ del it._next
+ del it.end
+ it.start = 0
+
+ def no_next(self, it):
+ return 0
+
+ def it_end(self, it):
+ if hasattr(it, '_next'):
+ return 0
+ return not self.prep_next(it)
+
+class SeqInner(InnerBase):
+ '''Inner class for sequence Iterators'''
+
+ def _supports(self, ob):
+ try: ob[0]
+ except (TypeError, AttributeError): return 0
+ except: pass
+ return 1
+
+ def prep_next(self, it):
+ i = it.nextIndex
+ try:
+ it._next = it.seq[i]
+ except IndexError:
+ it._prep_next = self.no_next
+ it.end = 1
+ return 0
+ it.end = 0
+ return 1
+
+ def it_length(self, it):
+ it.length = l = len(it.seq)
+ return l
+
+try:
+ StopIteration=StopIteration
+except NameError:
+ StopIteration="StopIteration"
+
+class IterInner(InnerBase):
+ '''Iterator inner class for Python iterators'''
+
+ def _supports(self, ob):
+ try:
+ if hasattr(ob, 'next') and (ob is iter(ob)):
+ return 1
+ except:
+ return 0
+
+ def prep_next(self, it):
+ try:
+ it._next = it.seq.next()
+ except StopIteration:
+ it._prep_next = self.no_next
+ it.end = 1
+ return 0
+ it.end = 0
+ return 1
+
+class IterIter:
+ def __init__(self, it):
+ self.it = it
+ self.skip = it.nextIndex > 0 and not it.end
+ def next(self):
+ it = self.it
+ if self.skip:
+ self.skip = 0
+ return it.item
+ if it.next():
+ return it.item
+ raise StopIteration
+
+seqInner = SeqInner()
+iterInner = IterInner()
Added: tracker/vendor/roundup/current/roundup/cgi/ZTUtils/__init__.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/ZTUtils/__init__.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,25 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+__doc__='''Package of template utility classes and functions.
+
+Modified for Roundup 0.5 release:
+
+- removed Zope imports
+
+$Id: __init__.py,v 1.3 2004/02/11 23:55:09 richard Exp $'''
+__docformat__ = 'restructuredtext'
+__version__='$Revision: 1.3 $'[11:-2]
+
+from Batch import Batch
+from Iterator import Iterator
+
Added: tracker/vendor/roundup/current/roundup/cgi/__init__.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/__init__.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2 @@
+''' CGI interface modules '''
+__docformat__ = 'restructuredtext'
Added: tracker/vendor/roundup/current/roundup/cgi/accept_language.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/accept_language.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,75 @@
+"""Parse the Accept-Language header as defined in RFC2616.
+
+See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
+for details. This module should follow the spec.
+Author: Hernan M. Foffani (hfoffani at gmail.com)
+Some use samples:
+
+>>> parse("da, en-gb;q=0.8, en;q=0.7")
+['da', 'en_gb', 'en']
+>>> parse("en;q=0.2, fr;q=1")
+['fr', 'en']
+>>> parse("zn; q = 0.2 ,pt-br;q =1")
+['pt_br', 'zn']
+>>> parse("es-AR")
+['es_AR']
+>>> parse("es-es-cat")
+['es_es_cat']
+>>> parse("")
+[]
+>>> parse(None)
+[]
+>>> parse(" ")
+[]
+>>> parse("en,")
+['en']
+"""
+
+import re
+import heapq
+
+# regexp for languange-range search
+nqlre = "([A-Za-z]+[-[A-Za-z]+]*)$"
+# regexp for languange-range search with quality value
+qlre = "([A-Za-z]+[-[A-Za-z]+]*);q=([\d\.]+)"
+# both
+lre = re.compile(nqlre + "|" + qlre)
+
+ascii = ''.join([chr(x) for x in xrange(256)])
+whitespace = ' \t\n\r\v\f'
+
+def parse(language_header):
+ """parse(string_with_accept_header_content) -> languages list"""
+
+ if language_header is None: return []
+
+ # strip whitespaces.
+ lh = language_header.translate(ascii, whitespace)
+
+ # if nothing, return
+ if lh == "": return []
+
+ # split by commas and parse the quality values.
+ pls = [lre.findall(x) for x in lh.split(',')]
+
+ # drop uncomformant
+ qls = [x[0] for x in pls if len(x) > 0]
+
+ # use a heap queue to sort by quality values.
+ # the value of each item is 1.0 complement.
+ pq = []
+ for l in qls:
+ if l[0] != '':
+ heapq.heappush(pq, (0.0, l[0]))
+ else:
+ heapq.heappush(pq, (1.0-float(l[2]), l[1]))
+
+ # get the languages ordered by quality
+ # and replace - by _
+ return [x[1].replace('-','_') for x in pq]
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/cgi/actions.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/actions.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,997 @@
+#$Id: actions.py,v 1.60 2006/04/27 03:44:47 richard Exp $
+
+import re, cgi, StringIO, urllib, Cookie, time, random, csv, codecs
+
+from roundup import hyperdb, token, date, password
+from roundup.i18n import _
+import roundup.exceptions
+from roundup.cgi import exceptions, templating
+from roundup.mailgw import uidFromAddress
+
+__all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
+ 'EditCSVAction', 'EditItemAction', 'PassResetAction',
+ 'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction',
+ 'NewItemAction', 'ExportCSVAction']
+
+# used by a couple of routines
+chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
+
+class Action:
+ def __init__(self, client):
+ self.client = client
+ self.form = client.form
+ self.db = client.db
+ self.nodeid = client.nodeid
+ self.template = client.template
+ self.classname = client.classname
+ self.userid = client.userid
+ self.base = client.base
+ self.user = client.user
+ self.context = templating.context(client)
+
+ def handle(self):
+ """Action handler procedure"""
+ raise NotImplementedError
+
+ def execute(self):
+ """Execute the action specified by this object."""
+ self.permission()
+ return self.handle()
+
+ name = ''
+ permissionType = None
+ def permission(self):
+ """Check whether the user has permission to execute this action.
+
+ True by default. If the permissionType attribute is a string containing
+ a simple permission, check whether the user has that permission.
+ Subclasses must also define the name attribute if they define
+ permissionType.
+
+ Despite having this permission, users may still be unauthorised to
+ perform parts of actions. It is up to the subclasses to detect this.
+ """
+ if (self.permissionType and
+ not self.hasPermission(self.permissionType)):
+ info = {'action': self.name, 'classname': self.classname}
+ raise exceptions.Unauthorised, self._(
+ 'You do not have permission to '
+ '%(action)s the %(classname)s class.')%info
+
+ _marker = []
+ def hasPermission(self, permission, classname=_marker, itemid=None):
+ """Check whether the user has 'permission' on the current class."""
+ if classname is self._marker:
+ classname = self.client.classname
+ return self.db.security.hasPermission(permission, self.client.userid,
+ classname=classname, itemid=itemid)
+
+ def gettext(self, msgid):
+ """Return the localized translation of msgid"""
+ return self.client.translator.gettext(msgid)
+
+ _ = gettext
+
+class ShowAction(Action):
+
+ typere=re.compile('[@:]type')
+ numre=re.compile('[@:]number')
+
+ def handle(self):
+ """Show a node of a particular class/id."""
+ t = n = ''
+ for key in self.form.keys():
+ if self.typere.match(key):
+ t = self.form[key].value.strip()
+ elif self.numre.match(key):
+ n = self.form[key].value.strip()
+ if not t:
+ raise ValueError, self._('No type specified')
+ if not n:
+ raise exceptions.SeriousError, self._('No ID entered')
+ try:
+ int(n)
+ except ValueError:
+ d = {'input': n, 'classname': t}
+ raise exceptions.SeriousError, self._(
+ '"%(input)s" is not an ID (%(classname)s ID required)')%d
+ url = '%s%s%s'%(self.base, t, n)
+ raise exceptions.Redirect, url
+
+class RetireAction(Action):
+ name = 'retire'
+ permissionType = 'Edit'
+
+ def handle(self):
+ """Retire the context item."""
+ # if we want to view the index template now, then unset the nodeid
+ # context info (a special-case for retire actions on the index page)
+ nodeid = 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'):
+ raise ValueError, self._(
+ 'You may not retire the admin or anonymous user')
+
+ # do the retire
+ self.db.getclass(self.classname).retire(nodeid)
+ self.db.commit()
+
+ self.client.ok_message.append(
+ self._('%(classname)s %(itemid)s has been retired')%{
+ 'classname': self.classname.capitalize(), 'itemid': nodeid})
+
+ def hasPermission(self, permission, classname=Action._marker, itemid=None):
+ if itemid is None:
+ itemid = self.nodeid
+ return Action.hasPermission(self, permission, classname, itemid)
+
+class SearchAction(Action):
+ name = 'search'
+ permissionType = 'View'
+
+ def handle(self):
+ """Mangle some of the form variables.
+
+ Set the form ":filter" variable based on the values of the filter
+ variables - if they're set to anything other than "dontcare" then add
+ them to :filter.
+
+ Handle the ":queryname" variable and save off the query to the user's
+ query list.
+
+ Split any String query values on whitespace and comma.
+
+ """
+ self.fakeFilterVars()
+ queryname = self.getQueryName()
+
+ # editing existing query name?
+ old_queryname = ''
+ for key in ('@old-queryname', ':old-queryname'):
+ if self.form.has_key(key):
+ old_queryname = self.form[key].value.strip()
+
+ # handle saving the query params
+ if queryname:
+ # parse the environment and figure what the query _is_
+ req = templating.HTMLRequest(self.client)
+
+ # The [1:] strips off the '?' character, it isn't part of the
+ # query string.
+ url = req.indexargs_url('', {})[1:]
+
+ key = self.db.query.getkey()
+ if key:
+ # edit the old way, only one query per name
+ try:
+ qid = self.db.query.lookup(old_queryname)
+ if not self.hasPermission('Edit', 'query', itemid=qid):
+ raise exceptions.Unauthorised, self._(
+ "You do not have permission to edit queries")
+ self.db.query.set(qid, klass=self.classname, url=url)
+ except KeyError:
+ # create a query
+ if not self.hasPermission('Create', 'query'):
+ raise exceptions.Unauthorised, self._(
+ "You do not have permission to store queries")
+ qid = self.db.query.create(name=queryname,
+ klass=self.classname, url=url)
+ else:
+ # edit the new way, query name not a key any more
+ # see if we match an existing private query
+ uid = self.db.getuid()
+ qids = self.db.query.filter(None, {'name': old_queryname,
+ 'private_for': uid})
+ if not qids:
+ # ok, so there's not a private query for the current user
+ # - see if there's one created by them
+ qids = self.db.query.filter(None, {'name': old_queryname,
+ 'creator': uid})
+
+ if qids and old_queryname:
+ # edit query - make sure we get an exact match on the name
+ for qid in qids:
+ if old_queryname != self.db.query.get(qid, 'name'):
+ continue
+ if not self.hasPermission('Edit', 'query', itemid=qid):
+ raise exceptions.Unauthorised, self._(
+ "You do not have permission to edit queries")
+ self.db.query.set(qid, klass=self.classname,
+ url=url, name=queryname)
+ else:
+ # create a query
+ if not self.hasPermission('Create', 'query'):
+ raise exceptions.Unauthorised, self._(
+ "You do not have permission to store queries")
+ qid = self.db.query.create(name=queryname,
+ klass=self.classname, url=url, private_for=uid)
+
+ # and add it to the user's query multilink
+ queries = self.db.user.get(self.userid, 'queries')
+ if qid not in queries:
+ queries.append(qid)
+ self.db.user.set(self.userid, queries=queries)
+
+ # commit the query change to the database
+ self.db.commit()
+
+ def fakeFilterVars(self):
+ """Add a faked :filter form variable for each filtering prop."""
+ props = self.db.classes[self.classname].getprops()
+ for key in self.form.keys():
+ if not props.has_key(key):
+ continue
+ if isinstance(self.form[key], type([])):
+ # search for at least one entry which is not empty
+ for minifield in self.form[key]:
+ if minifield.value:
+ break
+ else:
+ continue
+ else:
+ if not self.form[key].value:
+ continue
+ if isinstance(props[key], hyperdb.String):
+ v = self.form[key].value
+ l = token.token_split(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:
+ self.form.value.append(cgi.MiniFieldStorage(key, v))
+
+ self.form.value.append(cgi.MiniFieldStorage('@filter', key))
+
+ def getQueryName(self):
+ for key in ('@queryname', ':queryname'):
+ if self.form.has_key(key):
+ return self.form[key].value.strip()
+ return ''
+
+class EditCSVAction(Action):
+ name = 'edit'
+ permissionType = 'Edit'
+
+ def handle(self):
+ """Performs an edit of all of a class' items in one go.
+
+ 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.
+
+ """
+ cl = self.db.classes[self.classname]
+ idlessprops = cl.getprops(protected=0).keys()
+ idlessprops.sort()
+ props = ['id'] + idlessprops
+
+ # do the edit
+ rows = StringIO.StringIO(self.form['rows'].value)
+ reader = csv.reader(rows)
+ found = {}
+ line = 0
+ for values in reader:
+ line += 1
+ if line == 1: continue
+ # skip property names header
+ if values == props:
+ continue
+
+ # extract the nodeid
+ nodeid, values = values[0], values[1:]
+ found[nodeid] = 1
+
+ # see if the node exists
+ if nodeid in ('x', 'X') or not cl.hasnode(nodeid):
+ exists = 0
+ else:
+ exists = 1
+
+ # confirm correct weight
+ if len(idlessprops) != 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):
+ prop = cl.properties[name]
+ value = value.strip()
+ # only add the property if it has a value
+ if value:
+ # if it's a multilink, split it
+ if isinstance(prop, hyperdb.Multilink):
+ value = value.split(':')
+ elif isinstance(prop, hyperdb.Password):
+ value = password.Password(value)
+ elif isinstance(prop, hyperdb.Interval):
+ value = date.Interval(value)
+ elif isinstance(prop, hyperdb.Date):
+ value = date.Date(value)
+ elif isinstance(prop, hyperdb.Boolean):
+ value = value.lower() in ('yes', 'true', 'on', '1')
+ elif isinstance(prop, hyperdb.Number):
+ value = float(value)
+ d[name] = value
+ elif exists:
+ # nuke the existing value
+ if isinstance(prop, hyperdb.Multilink):
+ d[name] = []
+ else:
+ d[name] = None
+
+ # perform the edit
+ if exists:
+ # edit existing
+ cl.set(nodeid, **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)
+
+ # all OK
+ self.db.commit()
+
+ self.client.ok_message.append(self._('Items edited OK'))
+
+class EditCommon(Action):
+ '''Utility methods for editing.'''
+
+ def _editnodes(self, all_props, all_links):
+ ''' Use the props in all_props to perform edit and creation, then
+ use the link specs in all_links to do linking.
+ '''
+ # figure dependencies and re-work links
+ deps = {}
+ links = {}
+ for cn, nodeid, propname, vlist in all_links:
+ if not all_props.has_key((cn, nodeid)):
+ # link item to link to doesn't (and won't) exist
+ continue
+ for value in vlist:
+ if not all_props.has_key(value):
+ # link item to link to doesn't (and won't) exist
+ continue
+ deps.setdefault((cn, nodeid), []).append(value)
+ links.setdefault(value, []).append((cn, nodeid, propname))
+
+ # figure chained dependencies ordering
+ order = []
+ done = {}
+ # loop detection
+ change = 0
+ while len(all_props) != len(done):
+ for needed in all_props.keys():
+ if done.has_key(needed):
+ continue
+ tlist = deps.get(needed, [])
+ for target in tlist:
+ if not done.has_key(target):
+ break
+ else:
+ done[needed] = 1
+ order.append(needed)
+ change = 1
+ if not change:
+ raise ValueError, 'linking must not loop!'
+
+ # now, edit / create
+ m = []
+ for needed in order:
+ props = all_props[needed]
+ if not props:
+ # nothing to do
+ continue
+ cn, nodeid = needed
+
+ if nodeid is not None and int(nodeid) > 0:
+ # make changes to the node
+ props = self._changenode(cn, nodeid, props)
+
+ # and some nice feedback for the user
+ if props:
+ info = ', '.join(map(self._, props.keys()))
+ m.append(
+ self._('%(class)s %(id)s %(properties)s edited ok')
+ % {'class':cn, 'id':nodeid, 'properties':info})
+ else:
+ m.append(self._('%(class)s %(id)s - nothing changed')
+ % {'class':cn, 'id':nodeid})
+ else:
+ assert props
+
+ # make a new node
+ newid = self._createnode(cn, props)
+ if nodeid is None:
+ self.nodeid = newid
+ nodeid = newid
+
+ # and some nice feedback for the user
+ m.append(self._('%(class)s %(id)s created')
+ % {'class':cn, 'id':newid})
+
+ # fill in new ids in links
+ if links.has_key(needed):
+ for linkcn, linkid, linkprop in links[needed]:
+ props = all_props[(linkcn, linkid)]
+ cl = self.db.classes[linkcn]
+ propdef = cl.getprops()[linkprop]
+ if not props.has_key(linkprop):
+ if linkid is None or linkid.startswith('-'):
+ # linking to a new item
+ if isinstance(propdef, hyperdb.Multilink):
+ props[linkprop] = [newid]
+ else:
+ props[linkprop] = newid
+ else:
+ # linking to an existing item
+ if isinstance(propdef, hyperdb.Multilink):
+ existing = cl.get(linkid, linkprop)[:]
+ existing.append(nodeid)
+ props[linkprop] = existing
+ else:
+ props[linkprop] = newid
+
+ return '<br>'.join(m)
+
+ def _changenode(self, cn, nodeid, props):
+ """Change the node based on the contents of the form."""
+ # check for permission
+ if not self.editItemPermission(props, classname=cn, itemid=nodeid):
+ raise exceptions.Unauthorised, self._(
+ 'You do not have permission to edit %(class)s'
+ ) % {'class': cn}
+
+ # make the changes
+ cl = self.db.classes[cn]
+ return cl.set(nodeid, **props)
+
+ def _createnode(self, cn, props):
+ """Create a node based on the contents of the form."""
+ # check for permission
+ if not self.newItemPermission(props, classname=cn):
+ raise exceptions.Unauthorised, self._(
+ 'You do not have permission to create %(class)s'
+ ) % {'class': cn}
+
+ # create the node and return its id
+ cl = self.db.classes[cn]
+ return cl.create(**props)
+
+ def isEditingSelf(self):
+ """Check whether a user is editing his/her own details."""
+ return (self.nodeid == self.userid
+ and self.db.user.get(self.nodeid, 'username') != 'anonymous')
+
+ _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
+ 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
+
+ def newItemPermission(self, props, classname=None):
+ """Determine whether the user has permission to create this item.
+
+ Base behaviour is to check the user can edit this class. No additional
+ property checks are made.
+ """
+ if not classname :
+ classname = self.client.classname
+ return self.hasPermission('Create', classname=classname)
+
+class EditItemAction(EditCommon):
+ def lastUserActivity(self):
+ if self.form.has_key(':lastactivity'):
+ d = date.Date(self.form[':lastactivity'].value)
+ elif self.form.has_key('@lastactivity'):
+ d = date.Date(self.form['@lastactivity'].value)
+ else:
+ return None
+ d.second = int(d.second)
+ return d
+
+ def lastNodeActivity(self):
+ cl = getattr(self.client.db, self.classname)
+ activity = cl.get(self.nodeid, 'activity').local(0)
+ activity.second = int(activity.second)
+ return activity
+
+ def detectCollision(self, user_activity, node_activity):
+ '''Check for a collision and return the list of props we edited
+ that conflict.'''
+ if user_activity and user_activity < node_activity:
+ props, links = self.client.parsePropsFromForm()
+ key = (self.classname, self.nodeid)
+ # we really only collide for direct prop edit conflicts
+ return props[key].keys()
+ else:
+ return []
+
+ def handleCollision(self, props):
+ message = self._('Edit Error: someone else has edited this %s (%s). '
+ 'View <a target="new" href="%s%s">their changes</a> '
+ 'in a new window.')%(self.classname, ', '.join(props),
+ self.classname, self.nodeid)
+ self.client.error_message.append(message)
+ return
+
+ def handle(self):
+ """Perform an edit of an item in the database.
+
+ See parsePropsFromForm and _editnodes for special variables.
+
+ """
+ user_activity = self.lastUserActivity()
+ if user_activity:
+ props = self.detectCollision(user_activity, self.lastNodeActivity())
+ if props:
+ self.handleCollision(props)
+ return
+
+ props, links = self.client.parsePropsFromForm()
+
+ # handle the props
+ try:
+ message = self._editnodes(props, links)
+ except (ValueError, KeyError, IndexError,
+ roundup.exceptions.Reject), message:
+ self.client.error_message.append(
+ self._('Edit Error: %s') % str(message))
+ return
+
+ # commit now that all the tricky stuff is done
+ self.db.commit()
+
+ # redirect to the item's edit page
+ # redirect to finish off
+ url = self.base + self.classname
+ # note that this action might have been called by an index page, so
+ # we will want to include index-page args in this URL too
+ if self.nodeid is not None:
+ url += self.nodeid
+ url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
+ urllib.quote(self.template))
+ if self.nodeid is None:
+ req = templating.HTMLRequest(self.client)
+ url += '&' + req.indexargs_url('', {})[1:]
+ raise exceptions.Redirect, url
+
+class NewItemAction(EditCommon):
+ def handle(self):
+ ''' Add a new item to the database.
+
+ This follows the same form as the EditItemAction, with the same
+ special form values.
+ '''
+ # parse the props from the form
+ try:
+ props, links = self.client.parsePropsFromForm(create=1)
+ except (ValueError, KeyError), message:
+ self.client.error_message.append(self._('Error: %s')
+ % str(message))
+ return
+
+ # handle the props - edit or create
+ try:
+ # when it hits the None element, it'll set self.nodeid
+ messages = self._editnodes(props, links)
+ except (ValueError, KeyError, IndexError,
+ roundup.exceptions.Reject), message:
+ # these errors might just be indicative of user dumbness
+ self.client.error_message.append(_('Error: %s') % str(message))
+ return
+
+ # commit now that all the tricky stuff is done
+ self.db.commit()
+
+ # redirect to the new item's page
+ raise exceptions.Redirect, '%s%s%s?@ok_message=%s&@template=%s' % (
+ self.base, self.classname, self.nodeid, urllib.quote(messages),
+ urllib.quote(self.template))
+
+class PassResetAction(Action):
+ def handle(self):
+ """Handle password reset requests.
+
+ Presence of either "name" or "address" generates email. Presence of
+ "otk" performs the reset.
+
+ """
+ otks = self.db.getOTKManager()
+ if self.form.has_key('otk'):
+ # pull the rego information out of the otk database
+ otk = self.form['otk'].value
+ uid = otks.get(otk, 'uid')
+ if uid is None:
+ self.client.error_message.append(
+ self._("Invalid One Time Key!\n"
+ "(a Mozilla bug may cause this message "
+ "to show up erroneously, please check your email)"))
+ return
+
+ # re-open the database as "admin"
+ if self.user != 'admin':
+ self.client.opendb('admin')
+ self.db = self.client.db
+ otks = self.db.getOTKManager()
+
+ # change the password
+ newpw = password.generatePassword()
+
+ cl = self.db.user
+ # XXX we need to make the "default" page be able to display errors!
+ try:
+ # set the password
+ cl.set(uid, password=password.Password(newpw))
+ # clear the props from the otk database
+ otks.destroy(otk)
+ self.db.commit()
+ except (ValueError, KeyError), message:
+ self.client.error_message.append(str(message))
+ return
+
+ # user info
+ address = self.db.user.get(uid, 'address')
+ name = self.db.user.get(uid, 'username')
+
+ # send the email
+ tracker_name = self.db.config.TRACKER_NAME
+ subject = 'Password reset for %s'%tracker_name
+ body = '''
+The password has been reset for username "%(name)s".
+
+Your password is now: %(password)s
+'''%{'name': name, 'password': newpw}
+ if not self.client.standard_message([address], subject, body):
+ return
+
+ self.client.ok_message.append(
+ self._('Password reset and email sent to %s') % address)
+ return
+
+ # no OTK, so now figure the user
+ if self.form.has_key('username'):
+ name = self.form['username'].value
+ try:
+ uid = self.db.user.lookup(name)
+ except KeyError:
+ self.client.error_message.append(self._('Unknown username'))
+ return
+ address = self.db.user.get(uid, 'address')
+ elif self.form.has_key('address'):
+ address = self.form['address'].value
+ uid = uidFromAddress(self.db, ('', address), create=0)
+ if not uid:
+ self.client.error_message.append(
+ self._('Unknown email address'))
+ return
+ name = self.db.user.get(uid, 'username')
+ else:
+ self.client.error_message.append(
+ self._('You need to specify a username or address'))
+ return
+
+ # generate the one-time-key and store the props for later
+ otk = ''.join([random.choice(chars) for x in range(32)])
+ while otks.exists(otk):
+ otk = ''.join([random.choice(chars) for x in range(32)])
+ otks.set(otk, uid=uid)
+ self.db.commit()
+
+ # send the email
+ tracker_name = self.db.config.TRACKER_NAME
+ subject = 'Confirm reset of password for %s'%tracker_name
+ body = '''
+Someone, perhaps you, has requested that the password be changed for your
+username, "%(name)s". If you wish to proceed with the change, please follow
+the link below:
+
+ %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
+
+You should then receive another email with the new password.
+'''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
+ if not self.client.standard_message([address], subject, body):
+ return
+
+ self.client.ok_message.append(self._('Email sent to %s') % address)
+
+class RegoCommon(Action):
+ def finishRego(self):
+ # log the new user in
+ self.client.userid = self.userid
+ user = self.client.user = self.db.user.get(self.userid, 'username')
+ # 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)
+
+ # nice message
+ message = self._('You are now registered, welcome!')
+ url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
+ urllib.quote(message))
+
+ # redirect to the user's page (but not 302, as some email clients seem
+ # to want to reload the page, or something)
+ return '''<html><head><title>%s</title></head>
+ <body><p><a href="%s">%s</a></p>
+ <script type="text/javascript">
+ window.setTimeout('window.location = "%s"', 1000);
+ </script>'''%(message, url, message, url)
+
+class ConfRegoAction(RegoCommon):
+ def handle(self):
+ """Grab the OTK, use it to load up the new user details."""
+ try:
+ # pull the rego information out of the otk database
+ self.userid = self.db.confirm_registration(self.form['otk'].value)
+ except (ValueError, KeyError), message:
+ self.client.error_message.append(str(message))
+ return
+ self.finishRego()
+
+class RegisterAction(RegoCommon, EditCommon):
+ name = 'register'
+ permissionType = 'Create'
+
+ def handle(self):
+ """Attempt to create a new user based on the contents of the form
+ and then set the cookie.
+
+ Return 1 on successful login.
+ """
+ # parse the props from the form
+ try:
+ props, links = self.client.parsePropsFromForm(create=1)
+ except (ValueError, KeyError), message:
+ self.client.error_message.append(self._('Error: %s')
+ % str(message))
+ return
+
+ # registration isn't allowed to supply roles
+ user_props = props[('user', None)]
+ if user_props.has_key('roles'):
+ raise exceptions.Unauthorised, self._(
+ "It is not permitted to supply roles at registration.")
+
+ # skip the confirmation step?
+ if self.db.config['INSTANT_REGISTRATION']:
+ # handle the create now
+ try:
+ # when it hits the None element, it'll set self.nodeid
+ messages = self._editnodes(props, links)
+ except (ValueError, KeyError, IndexError,
+ roundup.exceptions.Reject), message:
+ # these errors might just be indicative of user dumbness
+ self.client.error_message.append(_('Error: %s') % str(message))
+ return
+
+ # fix up the initial roles
+ self.db.user.set(self.nodeid,
+ roles=self.db.config['NEW_WEB_USER_ROLES'])
+
+ # commit now that all the tricky stuff is done
+ self.db.commit()
+
+ # finish off by logging the user in
+ self.userid = self.nodeid
+ return self.finishRego()
+
+ # generate the one-time-key and store the props for later
+ for propname, proptype in self.db.user.getprops().items():
+ value = user_props.get(propname, None)
+ if value is None:
+ pass
+ elif isinstance(proptype, hyperdb.Date):
+ user_props[propname] = str(value)
+ elif isinstance(proptype, hyperdb.Interval):
+ user_props[propname] = str(value)
+ elif isinstance(proptype, hyperdb.Password):
+ user_props[propname] = str(value)
+ otks = self.db.getOTKManager()
+ otk = ''.join([random.choice(chars) for x in range(32)])
+ while otks.exists(otk):
+ otk = ''.join([random.choice(chars) for x in range(32)])
+ otks.set(otk, **user_props)
+
+ # send the email
+ tracker_name = self.db.config.TRACKER_NAME
+ tracker_email = self.db.config.TRACKER_EMAIL
+ if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
+ subject = 'Complete your registration to %s -- key %s'%(tracker_name,
+ otk)
+ body = """To complete your registration of the user "%(name)s" with
+%(tracker)s, please do one of the following:
+
+- send a reply to %(tracker_email)s and maintain the subject line as is (the
+reply's additional "Re:" is ok),
+
+- or visit the following URL:
+
+%(url)s?@action=confrego&otk=%(otk)s
+
+""" % {'name': user_props['username'], 'tracker': tracker_name,
+ 'url': self.base, 'otk': otk, 'tracker_email': tracker_email}
+ else:
+ subject = 'Complete your registration to %s'%(tracker_name)
+ body = """To complete your registration of the user "%(name)s" with
+%(tracker)s, please visit the following URL:
+
+%(url)s?@action=confrego&otk=%(otk)s
+
+""" % {'name': user_props['username'], 'tracker': tracker_name,
+ 'url': self.base, 'otk': otk}
+ if not self.client.standard_message([user_props['address']], subject,
+ body, (tracker_name, tracker_email)):
+ return
+
+ # commit changes to the database
+ self.db.commit()
+
+ # redirect to the "you're almost there" page
+ raise exceptions.Redirect, '%suser?@template=rego_progress'%self.base
+
+class LogoutAction(Action):
+ def handle(self):
+ """Make us really anonymous - nuke the cookie 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)
+
+ # Let the user know what's going on
+ self.client.ok_message.append(self._('You are logged out'))
+
+ # reset client context to render tracker home page
+ # instead of last viewed page (may be inaccessibe for anonymous)
+ self.client.classname = None
+ self.client.nodeid = None
+ self.client.template = None
+
+class LoginAction(Action):
+ def handle(self):
+ """Attempt to log a user in.
+
+ Sets up a session for the user which contains the login credentials.
+
+ """
+ # we need the username at a minimum
+ if not self.form.has_key('__login_name'):
+ self.client.error_message.append(self._('Username required'))
+ return
+
+ # get the login info
+ self.client.user = self.form['__login_name'].value
+ if self.form.has_key('__login_password'):
+ password = self.form['__login_password'].value
+ else:
+ password = ''
+
+ try:
+ self.verifyLogin(self.client.user, password)
+ except exceptions.LoginError, err:
+ self.client.make_user_anonymous()
+ self.client.error_message.extend(list(err.args))
+ return
+
+ # now we're OK, re-open the database for real, using the user
+ self.client.opendb(self.client.user)
+
+ # set the session cookie
+ 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)
+
+ # If we came from someplace, go back there
+ if self.form.has_key('__came_from'):
+ raise exceptions.Redirect, self.form['__came_from'].value
+
+ def verifyLogin(self, username, password):
+ # make sure the user exists
+ try:
+ self.client.userid = self.db.user.lookup(username)
+ except KeyError:
+ raise exceptions.LoginError, self._('Invalid login')
+
+ # verify the password
+ if not self.verifyPassword(self.client.userid, password):
+ raise exceptions.LoginError, self._('Invalid login')
+
+ # Determine whether the user has permission to log in.
+ # Base behaviour is to check the user has "Web Access".
+ if not self.hasPermission("Web Access"):
+ raise exceptions.LoginError, self._(
+ "You do not have permission to login")
+
+ def verifyPassword(self, userid, password):
+ '''Verify the password that the user has supplied'''
+ stored = self.db.user.get(userid, 'password')
+ if password == stored:
+ return 1
+ if not password and not stored:
+ return 1
+ return 0
+
+class ExportCSVAction(Action):
+ name = 'export'
+ permissionType = 'View'
+
+ def handle(self):
+ ''' Export the specified search query as CSV. '''
+ # figure the request
+ request = templating.HTMLRequest(self.client)
+ filterspec = request.filterspec
+ sort = request.sort
+ group = request.group
+ columns = request.columns
+ klass = self.db.getclass(request.classname)
+
+ # full-text search
+ if request.search_text:
+ matches = self.db.indexer.search(
+ re.findall(r'\b\w{2,25}\b', request.search_text), klass)
+ else:
+ matches = None
+
+ h = self.client.additional_headers
+ h['Content-Type'] = 'text/csv; charset=%s' % self.client.charset
+ # some browsers will honor the filename here...
+ h['Content-Disposition'] = 'inline; filename=query.csv'
+
+ self.client.header()
+
+ if self.client.env['REQUEST_METHOD'] == 'HEAD':
+ # all done, return a dummy string
+ return 'dummy'
+
+ wfile = self.client.request.wfile
+ if self.client.charset != self.client.STORAGE_CHARSET:
+ wfile = codecs.EncodedFile(wfile,
+ self.client.STORAGE_CHARSET, self.client.charset, 'replace')
+
+ writer = csv.writer(wfile)
+ writer.writerow(columns)
+
+ # and search
+ for itemid in klass.filter(matches, filterspec, sort, group):
+ writer.writerow([str(klass.get(itemid, col)) for col in columns])
+
+ return '\n'
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/cgi/apache.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/apache.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,108 @@
+# mod_python interface for Roundup Issue Tracker
+#
+# This module is free software, you may redistribute it
+# and/or modify under the same terms as Python.
+#
+# This module provides Roundup Web User Interface
+# using mod_python Apache module. Initially written
+# with python 2.3.3, mod_python 3.1.3, roundup 0.7.0.
+#
+# This module operates with only one tracker
+# and must be placed in the tracker directory.
+#
+# History (most recent first):
+# 11-jul-2004 [als] added 'TrackerLanguage' option;
+# pass message translator to the tracker client instance
+# 04-jul-2004 [als] tracker lookup moved from module global to request handler;
+# use PythonOption TrackerHome (configured in apache)
+# to open the tracker
+# 06-may-2004 [als] use cgi.FieldStorage from Python library
+# instead of mod_python FieldStorage
+# 29-apr-2004 [als] created
+
+__version__ = "$Revision: 1.4 $"[11:-2]
+__date__ = "$Date: 2004/11/22 07:33:34 $"[7:-2]
+
+import cgi
+import os
+
+from mod_python import apache
+
+import roundup.instance
+from roundup.cgi import TranslationService
+
+class Headers(dict):
+
+ """HTTP headers wrapper"""
+
+ def __init__(self, headers):
+ """Initialize with `apache.table`"""
+ super(Headers, self).__init__(headers)
+ self.getheader = self.get
+
+class Request(object):
+
+ """`apache.Request` object wrapper providing roundup client interface"""
+
+ def __init__(self, request):
+ """Initialize with `apache.Request` object"""
+ self._req = request
+ # .headers.getheader()
+ self.headers = Headers(request.headers_in)
+ # .wfile.write()
+ self.wfile = self._req
+
+ def send_response(self, response_code):
+ """Set HTTP response code"""
+ self._req.status = response_code
+
+ def send_header(self, name, value):
+ """Set output header"""
+ # value may be an instance of roundup.cgi.exceptions.HTTPException
+ value = str(value)
+ # XXX default content_type is "text/plain",
+ # and ain't overrided by "Content-Type" header
+ if name == "Content-Type":
+ self._req.content_type = value
+ else:
+ self._req.headers_out.add(name, value)
+
+ def end_headers(self):
+ """NOOP. There aint no such thing as 'end_headers' in mod_python"""
+ pass
+
+def handler(req):
+ """HTTP request handler"""
+ _options = req.get_options()
+ _home = _options.get("TrackerHome")
+ _lang = _options.get("TrackerLanguage")
+ _timing = _options.get("TrackerTiming", "no")
+ if _timing.lower() in ("no", "false"):
+ _timing = ""
+ _debug = _options.get("TrackerDebug", "no")
+ _debug = _debug.lower not in ("no", "false")
+ if not (_home and os.path.isdir(_home)):
+ apache.log_error(
+ "PythonOption TrackerHome missing or invalid for %(uri)s"
+ % {'uri': req.uri})
+ return apache.HTTP_INTERNAL_SERVER_ERROR
+ _tracker = roundup.instance.open(_home, not _debug)
+ # create environment
+ # Note: cookies are read from HTTP variables, so we need all HTTP vars
+ req.add_common_vars()
+ _env = dict(req.subprocess_env)
+ # XXX classname must be the first item in PATH_INFO. roundup.cgi does:
+ # path = string.split(os.environ.get('PATH_INFO', '/'), '/')
+ # os.environ['PATH_INFO'] = string.join(path[2:], '/')
+ # we just remove the first character ('/')
+ _env["PATH_INFO"] = req.path_info[1:]
+ if _timing:
+ _env["CGI_SHOW_TIMING"] = _timing
+ _form = cgi.FieldStorage(req, environ=_env)
+ _client = _tracker.Client(_tracker, Request(req), _env, _form,
+ translator=TranslationService.get_translation(_lang,
+ tracker_home=_home))
+ _client.main()
+ return apache.OK
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/cgi/cgitb.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/cgitb.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,219 @@
+#
+# 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 $
+
+"""Extended CGI traceback handler by Ka-Ping Yee, <ping at lfw.org>.
+"""
+__docformat__ = 'restructuredtext'
+
+import sys, os, types, string, keyword, linecache, tokenize, inspect, cgi
+import pydoc, traceback
+
+from roundup.cgi import templating, TranslationService
+
+def get_translator(i18n=None):
+ """Return message translation function (gettext)
+
+ Parameters:
+ i18n - translation service, such as roundup.i18n module
+ or TranslationService object.
+
+ Return ``gettext`` attribute of the ``i18n`` object, if available
+ (must be a message translation function with one argument).
+ If ``gettext`` cannot be obtained from ``i18n``, take default
+ TranslationService.
+
+ """
+ try:
+ return i18n.gettext
+ except:
+ return TranslationService.get_translation().gettext
+
+def breaker():
+ return ('<body bgcolor="white">' +
+ '<font color="white" size="-5"> > </font> ' +
+ '</table>' * 5)
+
+def niceDict(indent, dict):
+ l = []
+ for k,v in dict.items():
+ l.append('<tr><td><strong>%s</strong></td><td>%s</td></tr>'%(k,
+ cgi.escape(repr(v))))
+ return '\n'.join(l)
+
+def pt_html(context=5, i18n=None):
+ _ = get_translator(i18n)
+ esc = cgi.escape
+ exc_info = [esc(str(value)) for value in sys.exc_info()[:2]]
+ l = [_('<h1>Templating Error</h1>\n'
+ '<p><b>%(exc_type)s</b>: %(exc_value)s</p>\n'
+ '<p class="help">Debugging information follows</p>'
+ ) % {'exc_type': exc_info[0], 'exc_value': exc_info[1]},
+ '<ol>',]
+ from roundup.cgi.PageTemplates.Expressions import TraversalError
+ t = inspect.trace(context)
+ t.reverse()
+ for frame, file, lnum, func, lines, index in t:
+ args, varargs, varkw, locals = inspect.getargvalues(frame)
+ if locals.has_key('__traceback_info__'):
+ ti = locals['__traceback_info__']
+ if isinstance(ti, TraversalError):
+ s = []
+ for name, info in ti.path:
+ s.append(_('<li>"%(name)s" (%(info)s)</li>')
+ % {'name': name, 'info': esc(repr(info))})
+ s = '\n'.join(s)
+ l.append(_('<li>Looking for "%(name)s", '
+ 'current path:<ol>%(path)s</ol></li>'
+ ) % {'name': ti.name, 'path': s})
+ else:
+ l.append(_('<li>In %s</li>') % esc(str(ti)))
+ if locals.has_key('__traceback_supplement__'):
+ ts = locals['__traceback_supplement__']
+ if len(ts) == 2:
+ supp, context = ts
+ s = _('A problem occurred in your template "%s".') \
+ % str(context.id)
+ if context._v_errors:
+ s = s + '<br>' + '<br>'.join(
+ [esc(x) for x in context._v_errors])
+ l.append('<li>%s</li>'%s)
+ elif len(ts) == 3:
+ supp, context, info = ts
+ l.append(_('''
+<li>While evaluating the %(info)r expression on line %(line)d
+<table class="otherinfo" style="font-size: 90%%">
+ <tr><th colspan="2" class="header">Current variables:</th></tr>
+ %(globals)s
+ %(locals)s
+</table></li>
+''') % {
+ 'info': info,
+ 'line': context.position[0],
+ 'globals': niceDict(' ', context.global_vars),
+ 'locals': niceDict(' ', context.local_vars)
+})
+
+ l.append('''
+</ol>
+<table style="font-size: 80%%; color: gray">
+ <tr><th class="header" align="left">%s</th></tr>
+ <tr><td><pre>%s</pre></td></tr>
+</table>''' % (_('Full traceback:'), cgi.escape(''.join(
+ traceback.format_exception(*sys.exc_info())
+ ))))
+ l.append('<p> </p>')
+ return '\n'.join(l)
+
+def html(context=5, i18n=None):
+ _ = get_translator(i18n)
+ etype, evalue = sys.exc_type, sys.exc_value
+ if type(etype) is types.ClassType:
+ etype = etype.__name__
+ pyver = 'Python ' + string.split(sys.version)[0] + '<br>' + sys.executable
+ head = pydoc.html.heading(
+ _('<font size=+1><strong>%(exc_type)s</strong>: %(exc_value)s</font>')
+ % {'exc_type': etype, 'exc_value': evalue},
+ '#ffffff', '#777777', pyver)
+
+ head = head + (_('<p>A problem occurred while running a Python script. '
+ 'Here is the sequence of function calls leading up to '
+ 'the error, with the most recent (innermost) call first. '
+ 'The exception attributes are:'))
+
+ indent = '<tt><small>%s</small> </tt>' % (' ' * 5)
+ traceback = []
+ for frame, file, lnum, func, lines, index in inspect.trace(context):
+ if file is None:
+ link = _("<file is None - probably inside <tt>eval</tt> "
+ "or <tt>exec</tt>>")
+ else:
+ file = os.path.abspath(file)
+ link = '<a href="file:%s">%s</a>' % (file, pydoc.html.escape(file))
+ args, varargs, varkw, locals = inspect.getargvalues(frame)
+ if func == '?':
+ call = ''
+ else:
+ call = _('in <strong>%s</strong>') % func + inspect.formatargvalues(
+ args, varargs, varkw, locals,
+ formatvalue=lambda value: '=' + pydoc.html.repr(value))
+
+ level = '''
+<table width="100%%" bgcolor="#dddddd" cellspacing=0 cellpadding=2 border=0>
+<tr><td>%s %s</td></tr></table>''' % (link, call)
+
+ if index is None or file is None:
+ traceback.append('<p>' + level)
+ continue
+
+ # do a file inspection
+ names = []
+ def tokeneater(type, token, start, end, line, names=names):
+ if type == tokenize.NAME and token not in keyword.kwlist:
+ if token not in names:
+ names.append(token)
+ if type == tokenize.NEWLINE: raise IndexError
+ def linereader(file=file, lnum=[lnum]):
+ line = linecache.getline(file, lnum[0])
+ lnum[0] = lnum[0] + 1
+ return line
+
+ try:
+ tokenize.tokenize(linereader, tokeneater)
+ except IndexError:
+ pass
+ lvals = []
+ for name in names:
+ if name in frame.f_code.co_varnames:
+ if locals.has_key(name):
+ value = pydoc.html.repr(locals[name])
+ else:
+ value = _('<em>undefined</em>')
+ name = '<strong>%s</strong>' % name
+ else:
+ if frame.f_globals.has_key(name):
+ value = pydoc.html.repr(frame.f_globals[name])
+ else:
+ value = _('<em>undefined</em>')
+ name = '<em>global</em> <strong>%s</strong>' % name
+ lvals.append('%s = %s'%(name, value))
+ if lvals:
+ lvals = string.join(lvals, ', ')
+ lvals = indent + '<small><font color="#909090">%s'\
+ '</font></small><br>'%lvals
+ else:
+ lvals = ''
+
+ excerpt = []
+ i = lnum - index
+ for line in lines:
+ number = ' ' * (5-len(str(i))) + str(i)
+ number = '<small><font color="#909090">%s</font></small>' % number
+ line = '<tt>%s %s</tt>' % (number, pydoc.html.preformat(line))
+ if i == lnum:
+ line = '''
+<table width="100%%" bgcolor="white" cellspacing=0 cellpadding=0 border=0>
+<tr><td>%s</td></tr></table>''' % line
+ excerpt.append('\n' + line)
+ if i == lnum:
+ excerpt.append(lvals)
+ i = i + 1
+ traceback.append('<p>' + level + string.join(excerpt, '\n'))
+
+ traceback.reverse()
+
+ exception = '<p><strong>%s</strong>: %s' % (str(etype), str(evalue))
+ attribs = []
+ if type(evalue) is types.InstanceType:
+ for name in dir(evalue):
+ value = pydoc.html.repr(getattr(evalue, name))
+ attribs.append('<br>%s%s = %s' % (indent, name, value))
+
+ return head + string.join(attribs) + string.join(traceback) + '<p> </p>'
+
+def handler():
+ print breaker()
+ print html()
+
+# vim: set filetype=python ts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/cgi/client.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/client.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,959 @@
+# $Id: client.py,v 1.225 2006/04/27 04:03:11 richard 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 Cookie
+
+from roundup import roundupdb, date, hyperdb, password
+from roundup.cgi import templating, cgitb, TranslationService
+from roundup.cgi.actions import *
+from roundup.exceptions import *
+from roundup.cgi.exceptions import *
+from roundup.cgi.form_parser import FormParser
+from roundup.mailer import Mailer, MessageSendError
+from roundup.cgi import accept_language
+
+def initialiseSecurity(security):
+ '''Create some Permissions and Roles on the security object
+
+ This function is directly invoked by security.Security.__init__()
+ as a part of the Security object instantiation.
+ '''
+ p = security.addPermission(name="Web Access",
+ description="User may access the web interface")
+ security.addPermissionToRole('Admin', p)
+
+ # doing Role stuff through the web - make sure Admin can
+ # TODO: deprecate this and use a property-based control
+ p = security.addPermission(name="Web Roles",
+ description="User may manipulate user Roles through the web")
+ security.addPermissionToRole('Admin', p)
+
+# used to clean messages passed through CGI variables - HTML-escape any tag
+# that isn't <a href="">, <i>, <b> and <br> (including XHTML variants) so
+# that people can't pass through nasties like <script>, <iframe>, ...
+CLEAN_MESSAGE_RE = r'(<(/?(.*?)(\s*href="[^"]")?\s*/?)>)'
+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
+ '''
+ if ok.has_key(match.group(3).lower()):
+ return match.group(1)
+ return '<%s>'%match.group(2)
+
+error_message = ""'''<html><head><title>An error has occurred</title></head>
+<body><h1>An error has occurred</h1>
+<p>A problem was encountered processing your request.
+The tracker maintainers have been notified of the problem.</p>
+</body></html>'''
+
+class Client:
+ '''Instantiate to handle one CGI request.
+
+ See inner_main for request processing.
+
+ Client attributes at instantiation:
+
+ - "path" is the PATH_INFO inside the instance (with no leading '/')
+ - "base" is the base URL for the instance
+ - "form" is the cgi form, an instance of FieldStorage from the standard
+ cgi module
+ - "additional_headers" is a dictionary of additional HTTP headers that
+ should be sent to the client
+ - "response_code" is the HTTP response code to send to the client
+ - "translator" is TranslationService instance
+
+ During the processing of a request, the following attributes are used:
+
+ - "error_message" holds a list of error messages
+ - "ok_message" holds a list of OK messages
+ - "session" is the current user session id
+ - "user" is the current user's name
+ - "userid" is the current user's id
+ - "template" is the current :template context
+ - "classname" is the current class context name
+ - "nodeid" is the current context item id
+
+ User Identification:
+ If the user has no login cookie, then they 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.
+
+ 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!
+ # XXX take this from instance.config?
+ STORAGE_CHARSET = 'utf-8'
+
+ #
+ # special form variables
+ #
+ FV_TEMPLATE = re.compile(r'[@:]template')
+ FV_OK_MESSAGE = re.compile(r'[@:]ok_message')
+ FV_ERROR_MESSAGE = re.compile(r'[@:]error_message')
+
+ # Note: index page stuff doesn't appear here:
+ # columns, sort, sortdir, filter, group, groupdir, search_text,
+ # pagesize, startwith
+
+ def __init__(self, instance, request, env, form=None, translator=None):
+ # re-seed the random number generator
+ random.seed()
+ self.start = time.time()
+ self.instance = instance
+ self.request = request
+ self.env = env
+ self.setTranslator(translator)
+ self.mailer = Mailer(instance.config)
+
+ # save off the path
+ self.path = env['PATH_INFO']
+
+ # this is the base URL for this tracker
+ self.base = self.instance.config.TRACKER_WEB
+
+ # check the tracker_we setting
+ if not self.base.endswith('/'):
+ self.base = self.base + '/'
+
+ # 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 = {}
+
+ # see if we need to re-parse the environment for the form (eg Zope)
+ if form is None:
+ self.form = cgi.FieldStorage(environ=env)
+ else:
+ self.form = form
+
+ # turn debugging on/off
+ try:
+ self.debug = int(env.get("ROUNDUP_DEBUG", 0))
+ except ValueError:
+ # someone gave us a non-int debug level, turn it off
+ self.debug = 0
+
+ # flag to indicate that the HTTP headers have been sent
+ self.headers_done = 0
+
+ # additional headers to send with the request - must be registered
+ # before the first write
+ self.additional_headers = {}
+ self.response_code = 200
+
+ # default character set
+ self.charset = self.STORAGE_CHARSET
+
+ # parse cookies (used in charset and session lookups)
+ self.cookie = Cookie.SimpleCookie(self.env.get('HTTP_COOKIE', ''))
+
+ self.user = None
+ self.userid = None
+ self.nodeid = None
+ self.classname = None
+ self.template = None
+
+ def setTranslator(self, translator=None):
+ """Replace the translation engine
+
+ 'translator'
+ is TranslationService instance.
+ It must define methods 'translate' (TAL-compatible i18n),
+ 'gettext' and 'ngettext' (gettext-compatible i18n).
+
+ If omitted, create default TranslationService.
+ """
+ if translator is None:
+ translator = TranslationService.get_translation(
+ language=self.instance.config["TRACKER_LANGUAGE"],
+ tracker_home=self.instance.config["TRACKER_HOME"])
+ self.translator = translator
+ self._ = self.gettext = translator.gettext
+ self.ngettext = translator.ngettext
+
+ def main(self):
+ ''' Wrap the real main in a try/finally so we always close off the db.
+ '''
+ try:
+ self.inner_main()
+ finally:
+ if hasattr(self, 'db'):
+ self.db.close()
+
+ def inner_main(self):
+ '''Process a request.
+
+ The most common requests are handled like so:
+
+ 1. look for charset and language preferences, set up user locale
+ see determine_charset, determine_language
+ 2. figure out who we are, defaulting to the "anonymous" user
+ see determine_user
+ 3. figure out what the request is for - the context
+ see determine_context
+ 4. handle any requested action (item edit, search, ...)
+ see handle_action
+ 5. render a template, resulting in HTML output
+
+ In some situations, exceptions occur:
+
+ - HTTP Redirect (generally raised by an action)
+ - SendFile (generally raised by determine_context)
+ serve up a FileClass "content" property
+ - SendStaticFile (generally raised by determine_context)
+ serve up a file from the tracker "html" directory
+ - Unauthorised (generally raised by an action)
+ the action is cancelled, the request is rendered and an error
+ message is displayed indicating that permission was not
+ granted for the action to take place
+ - templating.Unauthorised (templating action not permitted)
+ raised by an attempted rendering of a template when the user
+ 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()
+
+ 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
+ self.write_html(self.renderContext())
+
+ except SeriousError, message:
+ self.write_html(str(message))
+ except Redirect, url:
+ # let's redirect - if the url isn't None, then we need to do
+ # the headers, otherwise the headers have been set before the
+ # exception was raised
+ if url:
+ self.additional_headers['Location'] = 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.request.send_response(304)
+ self.request.end_headers()
+ except SendStaticFile, file:
+ try:
+ self.serve_static_file(str(file))
+ except NotModified:
+ # send the 304 response
+ self.request.send_response(304)
+ self.request.end_headers()
+ except Unauthorised, message:
+ # users may always see the front page
+ self.classname = self.nodeid = None
+ self.template = ''
+ self.error_message.append(message)
+ self.write_html(self.renderContext())
+ except NotFound:
+ # pass through
+ raise
+ except FormError, e:
+ self.error_message.append(self._('Form Error: ') + str(e))
+ self.write_html(self.renderContext())
+ except:
+ if self.instance.config.WEB_DEBUG:
+ self.write_html(cgitb.html(i18n=self.translator))
+ else:
+ self.mailer.exception_message()
+ return self.write_html(self._(error_message))
+
+ def clean_sessions(self):
+ """Age sessions, remove when they haven't been used for a week.
+
+ Do it only once an hour.
+
+ Note: also cleans One Time Keys, and other "session" based stuff.
+ """
+ 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()
+ if now - last_clean < hour:
+ return
+
+ sessions.clean(now)
+ self.db.getOTKManager().clean(now)
+ sessions.set('last_clean', last_use=time.time())
+ self.db.commit()
+
+ def determine_charset(self):
+ """Look for client charset in the form parameters or browser cookie.
+
+ If no charset requested by client, use storage charset (utf-8).
+
+ If the charset is found, and differs from the storage charset,
+ recode all form fields of type 'text/plain'
+ """
+ # look for client charset
+ charset_parameter = 0
+ if self.form.has_key('@charset'):
+ charset = self.form['@charset'].value
+ if charset.lower() == "none":
+ charset = ""
+ charset_parameter = 1
+ elif self.cookie.has_key('roundup_charset'):
+ charset = self.cookie['roundup_charset'].value
+ else:
+ charset = None
+ if charset:
+ # make sure the charset is recognized
+ try:
+ codecs.lookup(charset)
+ except LookupError:
+ self.error_message.append(self._('Unrecognized charset: %r')
+ % charset)
+ charset_parameter = 0
+ else:
+ self.charset = charset.lower()
+ # If we've got a character set in request parameters,
+ # set the browser cookie to keep the preference.
+ # This is done after codecs.lookup to make sure
+ # that we aren't keeping a wrong value.
+ if charset_parameter:
+ self.add_cookie('roundup_charset', charset)
+
+ # if client charset is different from the storage charset,
+ # recode form fields
+ # XXX this requires FieldStorage from Python library.
+ # mod_python FieldStorage is not supported!
+ if self.charset != self.STORAGE_CHARSET:
+ decoder = codecs.getdecoder(self.charset)
+ encoder = codecs.getencoder(self.STORAGE_CHARSET)
+ re_charref = re.compile('&#([0-9]+|x[0-9a-f]+);', re.IGNORECASE)
+ def _decode_charref(matchobj):
+ num = matchobj.group(1)
+ if num[0].lower() == 'x':
+ uc = int(num[1:], 16)
+ else:
+ uc = int(num)
+ return unichr(uc)
+
+ for field_name in self.form.keys():
+ field = self.form[field_name]
+ if (field.type == 'text/plain') and not field.filename:
+ try:
+ value = decoder(field.value)[0]
+ except UnicodeError:
+ continue
+ value = re_charref.sub(_decode_charref, value)
+ field.value = encoder(value)[0]
+
+ def determine_language(self):
+ """Determine the language"""
+ # look for language parameter
+ # then for language cookie
+ # last for the Accept-Language header
+ if self.form.has_key("@language"):
+ language = self.form["@language"].value
+ if language.lower() == "none":
+ language = ""
+ self.add_cookie("roundup_language", language)
+ elif self.cookie.has_key("roundup_language"):
+ language = self.cookie["roundup_language"].value
+ elif self.instance.config["WEB_USE_BROWSER_LANGUAGE"]:
+ hal = self.env.get('HTTP_ACCEPT_LANGUAGE')
+ language = accept_language.parse(hal)
+ else:
+ language = ""
+
+ self.language = language
+ if language:
+ self.setTranslator(TranslationService.get_translation(
+ language,
+ tracker_home=self.instance.config["TRACKER_HOME"]))
+
+ def determine_user(self):
+ """Determine who the user is"""
+ self.opendb('admin')
+
+ # make sure we have the session Class
+ self.clean_sessions()
+ sessions = self.db.getSessionManager()
+
+ user = None
+ # first up, try http authorization if enabled
+ if self.instance.config['WEB_HTTP_AUTH']:
+ if self.env.has_key('REMOTE_USER'):
+ # we have external auth (e.g. by Apache)
+ user = self.env['REMOTE_USER']
+ elif self.env.get('HTTP_AUTHORIZATION', ''):
+ # try handling Basic Auth ourselves
+ auth = self.env['HTTP_AUTHORIZATION']
+ scheme, challenge = auth.split(' ', 1)
+ if scheme.lower() == 'basic':
+ try:
+ decoded = base64.decodestring(challenge)
+ except TypeError:
+ # invalid challenge
+ pass
+ username, password = decoded.split(':')
+ try:
+ login = self.get_action_class('login')(self)
+ 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 no user name set by http authorization or session cookie
+ # the user is anonymous
+ if not user:
+ user = 'anonymous'
+
+ # sanity check on the user still being valid,
+ # getting the userid at the same time
+ try:
+ self.userid = self.db.user.lookup(user)
+ except (KeyError, TypeError):
+ user = 'anonymous'
+
+ # make sure the anonymous user is valid if we're using it
+ if user == 'anonymous':
+ self.make_user_anonymous()
+ if not self.db.security.hasPermission('Web Access', self.userid):
+ raise Unauthorised, self._("Anonymous users are not "
+ "allowed to use the web interface")
+ else:
+ self.user = user
+
+ # reopen the database as the correct user
+ self.opendb(self.user)
+
+ def opendb(self, username):
+ """Open the database and set the current user.
+
+ Opens a database once. On subsequent calls only the user is set on
+ the database object the instance.optimize is set. If we are in
+ "Development Mode" (cf. roundup_server) then the database is always
+ re-opened.
+ """
+ # don't do anything if the db is open and the user has not changed
+ if hasattr(self, 'db') and self.db.isCurrentUser(username):
+ return
+
+ # open the database or only set the user
+ if not hasattr(self, 'db'):
+ self.db = self.instance.open(username)
+ else:
+ if self.instance.optimize:
+ self.db.setCurrentUser(username)
+ else:
+ self.db.close()
+ self.db = self.instance.open(username)
+
+ def determine_context(self, dre=re.compile(r'([^\d]+)0*(\d+)')):
+ """Determine the context of this page from the URL:
+
+ The URL path after the instance identifier is examined. The path
+ is generally only one entry long.
+
+ - if there is no path, then we are in the "home" context.
+ - if the path is "_file", then the additional path entry
+ specifies the filename of a static file we're to serve up
+ from the instance "html" directory. Raises a SendStaticFile
+ exception.(*)
+ - if there is something in the path (eg "issue"), it identifies
+ the tracker class we're to display.
+ - if the path is an item designator (eg "issue123"), then we're
+ to display a specific item.
+ - if the path starts with an item designator and is longer than
+ one entry, then we're assumed to be handling an item of a
+ FileClass, and the extra path information gives the filename
+ that the client is going to label the download with (ie
+ "file123/image.png" is nicer to download than "file123"). This
+ raises a SendFile exception.(*)
+
+ Both of the "*" types of contexts stop before we bother to
+ determine the template we're going to use. That's because they
+ don't actually use templates.
+
+ The template used is specified by the :template CGI variable,
+ which defaults to:
+
+ - only classname suplied: "index"
+ - full item designator supplied: "item"
+
+ We set:
+
+ self.classname - the class to display, can be None
+
+ self.template - the template to render the current context with
+
+ self.nodeid - the nodeid of the class we're displaying
+ """
+ # default the optional variables
+ self.classname = None
+ self.nodeid = None
+
+ # see if a template or messages are specified
+ template_override = ok_message = error_message = None
+ for key in self.form.keys():
+ if self.FV_TEMPLATE.match(key):
+ template_override = self.form[key].value
+ elif self.FV_OK_MESSAGE.match(key):
+ ok_message = self.form[key].value
+ ok_message = clean_message(ok_message)
+ elif self.FV_ERROR_MESSAGE.match(key):
+ error_message = self.form[key].value
+ error_message = clean_message(error_message)
+
+ # see if we were passed in a message
+ if ok_message:
+ self.ok_message.append(ok_message)
+ if error_message:
+ self.error_message.append(error_message)
+
+ # determine the classname and possibly nodeid
+ path = self.path.split('/')
+ if not path or path[0] in ('', 'home', 'index'):
+ if template_override is not None:
+ self.template = template_override
+ else:
+ self.template = ''
+ return
+ elif path[0] in ('_file', '@@file'):
+ raise SendStaticFile, os.path.join(*path[1:])
+ else:
+ self.classname = path[0]
+ if len(path) > 1:
+ # send the file identified by the designator in path[0]
+ raise SendFile, path[0]
+
+ # see if we got a designator
+ m = dre.match(self.classname)
+ if m:
+ self.classname = m.group(1)
+ self.nodeid = m.group(2)
+ try:
+ klass = self.db.getclass(self.classname)
+ except KeyError:
+ 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
+ self.template = 'item'
+ else:
+ # with only a class, we default to index view
+ self.template = 'index'
+
+ # make sure the classname is valid
+ try:
+ self.db.getclass(self.classname)
+ except KeyError:
+ raise NotFound, self.classname
+
+ # see if we have a template override
+ if template_override is not None:
+ 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.
+ '''
+ m = dre.match(str(designator))
+ if not m:
+ raise NotFound, str(designator)
+ classname, nodeid = m.group(1), m.group(2)
+
+ klass = self.db.getclass(classname)
+
+ # make sure we have the appropriate properties
+ props = klass.getprops()
+ if not props.has_key('type'):
+ raise NotFound, designator
+ if not props.has_key('content'):
+ raise NotFound, designator
+
+ # make sure we have permission
+ if not self.db.security.hasPermission('View', self.userid,
+ classname, 'content', nodeid):
+ raise Unauthorised, self._("You are not allowed to view "
+ "this file.")
+
+ mime_type = klass.get(nodeid, 'type')
+ content = klass.get(nodeid, 'content')
+ lmt = klass.get(nodeid, 'activity').timestamp()
+
+ self._serve_file(lmt, mime_type, content)
+
+ def serve_static_file(self, file):
+ ''' 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]
+ if not prefix:
+ continue
+ # ensure the load doesn't try to poke outside
+ # of the static files directory
+ prefix = os.path.normpath(prefix)
+ filename = os.path.normpath(os.path.join(prefix, file))
+ if os.path.isfile(filename) and filename.startswith(prefix):
+ break
+ else:
+ raise NotFound, file
+
+ # last-modified time
+ lmt = os.stat(filename)[stat.ST_MTIME]
+
+ # detemine meta-type
+ file = str(file)
+ mime_type = mimetypes.guess_type(file)[0]
+ if not mime_type:
+ if file.endswith('.css'):
+ mime_type = 'text/css'
+ 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, content)
+
+ def _serve_file(self, lmt, mime_type, content):
+ ''' guts of serve_file() and serve_static_file()
+ '''
+ ims = None
+ # see if there's an if-modified-since...
+ if hasattr(self.request, 'headers'):
+ ims = self.request.headers.getheader('if-modified-since')
+ elif self.env.has_key('HTTP_IF_MODIFIED_SINCE'):
+ # cgi will put the header in the env var
+ ims = self.env['HTTP_IF_MODIFIED_SINCE']
+ if ims:
+ ims = rfc822.parsedate(ims)[:6]
+ lmtt = time.gmtime(lmt)[:6]
+ if lmtt <= ims:
+ raise NotModified
+
+ # spit out headers
+ self.additional_headers['Content-Type'] = mime_type
+ self.additional_headers['Content-Length'] = len(content)
+ lmt = rfc822.formatdate(lmt)
+ self.additional_headers['Last-Modified'] = lmt
+ self.write(content)
+
+ def renderContext(self):
+ ''' 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 = {
+ 'ok_message': self.ok_message,
+ 'error_message': self.error_message
+ }
+ try:
+ # let the template render figure stuff out
+ result = pt.render(self, None, None, **args)
+ self.additional_headers['Content-Type'] = pt.content_type
+ if self.env.get('CGI_SHOW_TIMING', ''):
+ if self.env['CGI_SHOW_TIMING'].upper() == 'COMMENT':
+ timings = {'starttag': '<!-- ', 'endtag': ' -->'}
+ else:
+ timings = {'starttag': '<p>', 'endtag': '</p>'}
+ timings['seconds'] = time.time()-self.start
+ s = self._('%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n'
+ ) % timings
+ if hasattr(self.db, 'stats'):
+ timings.update(self.db.stats)
+ s += self._("%(starttag)sCache hits: %(cache_hits)d,"
+ " misses %(cache_misses)d."
+ " Loading items: %(get_items)f secs."
+ " Filtering: %(filtering)f secs."
+ "%(endtag)s\n") % timings
+ s += '</body>'
+ result = result.replace('</body>', s)
+ return result
+ except templating.NoTemplate, message:
+ return '<strong>%s</strong>'%message
+ except templating.Unauthorised, message:
+ raise Unauthorised, str(message)
+ except:
+ # everything else
+ return cgitb.pt_html(i18n=self.translator)
+
+ # these are the actions that are available
+ actions = (
+ ('edit', EditItemAction),
+ ('editcsv', EditCSVAction),
+ ('new', NewItemAction),
+ ('register', RegisterAction),
+ ('confrego', ConfRegoAction),
+ ('passrst', PassResetAction),
+ ('login', LoginAction),
+ ('logout', LogoutAction),
+ ('search', SearchAction),
+ ('retire', RetireAction),
+ ('show', ShowAction),
+ ('export_csv', ExportCSVAction),
+ )
+ def handle_action(self):
+ ''' 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
+ are defined in the "actions" sequence on this class.
+
+ Actions may return a page (by default HTML) to return to the
+ user, bypassing the usual template rendering.
+
+ 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'):
+ action = self.form['@action'].value.lower()
+ else:
+ return None
+
+ try:
+ action_klass = self.get_action_class(action)
+
+ # call the mapped action
+ if isinstance(action_klass, type('')):
+ # old way of specifying actions
+ return getattr(self, action_klass)()
+ else:
+ return action_klass(self).execute()
+
+ except (ValueError, Reject), err:
+ self.error_message.append(str(err))
+
+ def get_action_class(self, action_name):
+ if (hasattr(self.instance, 'cgi_actions') and
+ self.instance.cgi_actions.has_key(action_name)):
+ # tracker-defined action
+ action_klass = self.instance.cgi_actions[action_name]
+ else:
+ # go with a default
+ for name, action_klass in self.actions:
+ if name == action_name:
+ break
+ else:
+ raise ValueError, 'No such action "%s"'%action_name
+ return action_klass
+
+ def write(self, content):
+ if not self.headers_done:
+ self.header()
+ if self.env['REQUEST_METHOD'] != 'HEAD':
+ self.request.wfile.write(content)
+
+ def write_html(self, content):
+ if not self.headers_done:
+ # at this point, we are sure about Content-Type
+ self.additional_headers['Content-Type'] = \
+ 'text/html; charset=%s' % self.charset
+ self.header()
+
+ if self.env['REQUEST_METHOD'] == 'HEAD':
+ # client doesn't care about content
+ return
+
+ if self.charset != self.STORAGE_CHARSET:
+ # recode output
+ content = content.decode(self.STORAGE_CHARSET, 'replace')
+ content = content.encode(self.charset, 'xmlcharrefreplace')
+
+ # and write
+ self.request.wfile.write(content)
+
+ def setHeader(self, header, value):
+ '''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.
+ '''
+ if headers is None:
+ headers = {'Content-Type':'text/html; charset=utf-8'}
+ if response is None:
+ response = self.response_code
+
+ # update with additional info
+ headers.update(self.additional_headers)
+
+ if headers.get('Content-Type', 'text/html') == 'text/html':
+ headers['Content-Type'] = 'text/html; charset=utf-8'
+ self.request.send_response(response)
+ for entry in headers.items():
+ self.request.send_header(*entry)
+ for ((path, name), (value, expire)) in self.add_cookies.items():
+ cookie = "%s=%s; Path=%s;"%(name, value, path)
+ if expire is not None:
+ cookie += " expires=%s;"%Cookie._getdate(expire)
+ self.request.send_header('Set-Cookie', cookie)
+ self.request.end_headers()
+ self.headers_done = 1
+ if self.debug:
+ self.headers_sent = headers
+
+ def add_cookie(self, name, value, expire=86400*365, path=None):
+ """Set a cookie value to be sent in HTTP headers
+
+ Parameters:
+ name:
+ cookie name
+ value:
+ cookie value
+ expire:
+ cookie expiration time (seconds).
+ If value is empty (meaning "delete cookie"),
+ expiration time is forced in the past
+ and this argument is ignored.
+ If None, the cookie will expire at end-of-session.
+ If omitted, the cookie will be kept for a year.
+ path:
+ cookie path (optional)
+
+ """
+ if path is None:
+ path = self.cookie_path
+ if not value:
+ expire = -1
+ self.add_cookies[(path, name)] = (value, expire)
+
+ def set_cookie(self, user, expire=None):
+ """Set up a session cookie for the user.
+
+ Also store away the user's login info against the session.
+ """
+ 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)
+
+ def make_user_anonymous(self):
+ ''' 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.
+
+ "to" - recipients list
+ "subject" - Subject
+ "body" - Message
+ "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:
+ self.error_message.append(str(e))
+ return 0
+ return 1
+
+ def parsePropsFromForm(self, create=0):
+ return FormParser(self).parse(create=create)
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/cgi/exceptions.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/exceptions.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,62 @@
+#$Id: exceptions.py,v 1.6 2004/11/18 14:10:27 a1s Exp $
+'''Exceptions for use in Roundup's web interface.
+'''
+
+__docformat__ = 'restructuredtext'
+
+import cgi
+
+class HTTPException(Exception):
+ pass
+
+class LoginError(HTTPException):
+ pass
+
+class Unauthorised(HTTPException):
+ pass
+
+class Redirect(HTTPException):
+ pass
+
+class NotFound(HTTPException):
+ pass
+
+class NotModified(HTTPException):
+ pass
+
+class FormError(ValueError):
+ """An 'expected' exception occurred during form parsing.
+
+ That is, something we know can go wrong, and don't want to alarm the user
+ with.
+
+ We trap this at the user interface level and feed back a nice error to the
+ user.
+
+ """
+ pass
+
+class SendFile(Exception):
+ """Send a file from the database."""
+
+class SendStaticFile(Exception):
+ """Send a static file from the instance html directory."""
+
+class SeriousError(Exception):
+ """Raised when we can't reasonably display an error message on a
+ templated page.
+
+ The exception value will be displayed in the error page, HTML
+ escaped.
+ """
+ def __str__(self):
+ 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])
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/cgi/form_parser.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/form_parser.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,560 @@
+import re, mimetypes
+
+from roundup import hyperdb, date, password
+from roundup.cgi import templating
+from roundup.cgi.exceptions import FormError
+
+class FormParser:
+ # edit form variable handling (see unit tests)
+ FV_LABELS = r'''
+ ^(
+ (?P<note>[@:]note)|
+ (?P<file>[@:]file)|
+ (
+ ((?P<classname>%s)(?P<id>[-\d]+))? # optional leading designator
+ ((?P<required>[@:]required$)| # :required
+ (
+ (
+ (?P<add>[@:]add[@:])| # :add:<prop>
+ (?P<remove>[@:]remove[@:])| # :remove:<prop>
+ (?P<confirm>[@:]confirm[@:])| # :confirm:<prop>
+ (?P<link>[@:]link[@:])| # :link:<prop>
+ ([@:]) # just a separator
+ )?
+ (?P<propname>[^@:]+) # <prop>
+ )
+ )
+ )
+ )$'''
+
+ def __init__(self, client):
+ self.client = client
+ self.db = client.db
+ self.form = client.form
+ self.classname = client.classname
+ self.nodeid = client.nodeid
+ try:
+ self._ = self.gettext = client.gettext
+ self.ngettext = client.ngettext
+ except AttributeError:
+ _translator = templating.translationService
+ self._ = self.gettext = _translator.gettext
+ self.ngettext = _translator.ngettext
+
+ def parse(self, create=0, num_re=re.compile('^\d+$')):
+ """ Item properties and their values are edited with html FORM
+ variables and their values. You can:
+
+ - Change the value of some property of the current item.
+ - Create a new item of any class, and edit the new item's
+ properties,
+ - Attach newly created items to a multilink property of the
+ current item.
+ - Remove items from a multilink property of the current item.
+ - Specify that some properties are required for the edit
+ operation to be successful.
+
+ In the following, <bracketed> values are variable, "@" may be
+ either ":" or "@", and other text "required" is fixed.
+
+ Most properties are specified as form variables:
+
+ <propname>
+ - property on the current context item
+
+ <designator>"@"<propname>
+ - property on the indicated item (for editing related
+ information)
+
+ Designators name a specific item of a class.
+
+ <classname><N>
+
+ Name an existing item of class <classname>.
+
+ <classname>"-"<N>
+
+ Name the <N>th new item of class <classname>. If the form
+ submission is successful, a new item of <classname> is
+ created. Within the submitted form, a particular
+ designator of this form always refers to the same new
+ item.
+
+ Once we have determined the "propname", we look at it to see
+ if it's special:
+
+ @required
+ The associated form value is a comma-separated list of
+ property names that must be specified when the form is
+ submitted for the edit operation to succeed.
+
+ When the <designator> is missing, the properties are
+ for the current context item. When <designator> is
+ present, they are for the item specified by
+ <designator>.
+
+ The "@required" specifier must come before any of the
+ properties it refers to are assigned in the form.
+
+ @remove@<propname>=id(s) or @add@<propname>=id(s)
+ The "@add@" and "@remove@" edit actions apply only to
+ Multilink properties. The form value must be a
+ comma-separate list of keys for the class specified by
+ the simple form variable. The listed items are added
+ to (respectively, removed from) the specified
+ property.
+
+ @link@<propname>=<designator>
+ If the edit action is "@link@", the simple form
+ variable must specify a Link or Multilink property.
+ The form value is a comma-separated list of
+ designators. The item corresponding to each
+ designator is linked to the property given by simple
+ form variable. These are collected up and returned in
+ all_links.
+
+ None of the above (ie. just a simple form value)
+ The value of the form variable is converted
+ appropriately, depending on the type of the property.
+
+ For a Link('klass') property, the form value is a
+ single key for 'klass', where the key field is
+ specified in dbinit.py.
+
+ For a Multilink('klass') property, the form value is a
+ comma-separated list of keys for 'klass', where the
+ key field is specified in dbinit.py.
+
+ Note that for simple-form-variables specifiying Link
+ and Multilink properties, the linked-to class must
+ have a key field.
+
+ For a String() property specifying a filename, the
+ file named by the form value is uploaded. This means we
+ try to set additional properties "filename" and "type" (if
+ they are valid for the class). Otherwise, the property
+ is set to the form value.
+
+ For Date(), Interval(), Boolean(), and Number()
+ properties, the form value is converted to the
+ appropriate
+
+ Any of the form variables may be prefixed with a classname or
+ designator.
+
+ Two special form values are supported for backwards
+ compatibility:
+
+ @note
+ This is equivalent to::
+
+ @link at messages=msg-1
+ msg-1 at content=value
+
+ except that in addition, the "author" and "date"
+ properties of "msg-1" are set to the userid of the
+ submitter, and the current time, respectively.
+
+ @file
+ This is equivalent to::
+
+ @link at files=file-1
+ file-1 at content=value
+
+ The String content value is handled as described above for
+ file uploads.
+
+ If both the "@note" and "@file" form variables are
+ specified, the action::
+
+ @link at msg-1@files=file-1
+
+ is also performed.
+
+ We also check that FileClass items have a "content" property with
+ actual content, otherwise we remove them from all_props before
+ returning.
+
+ The return from this method is a dict of
+ (classname, id): properties
+ ... this dict _always_ has an entry for the current context,
+ even if it's empty (ie. a submission for an existing issue that
+ doesn't result in any changes would return {('issue','123'): {}})
+ The id may be None, which indicates that an item should be
+ created.
+ """
+ # some very useful variables
+ db = self.db
+ form = self.form
+
+ if not hasattr(self, 'FV_SPECIAL'):
+ # generate the regexp for handling special form values
+ classes = '|'.join(db.classes.keys())
+ # specials for parsePropsFromForm
+ # handle the various forms (see unit tests)
+ self.FV_SPECIAL = re.compile(self.FV_LABELS%classes, re.VERBOSE)
+ self.FV_DESIGNATOR = re.compile(r'(%s)([-\d]+)'%classes)
+
+ # these indicate the default class / item
+ default_cn = self.classname
+ default_cl = self.db.classes[default_cn]
+ default_nodeid = self.nodeid
+
+ # we'll store info about the individual class/item edit in these
+ all_required = {} # required props per class/item
+ all_props = {} # props to set per class/item
+ got_props = {} # props received per class/item
+ all_propdef = {} # note - only one entry per class
+ all_links = [] # as many as are required
+
+ # we should always return something, even empty, for the context
+ all_props[(default_cn, default_nodeid)] = {}
+
+ keys = form.keys()
+ timezone = db.getUserTimezone()
+
+ # sentinels for the :note and :file props
+ have_note = have_file = 0
+
+ # extract the usable form labels from the form
+ matches = []
+ for key in keys:
+ m = self.FV_SPECIAL.match(key)
+ if m:
+ matches.append((key, m.groupdict()))
+
+ # now handle the matches
+ for key, d in matches:
+ if d['classname']:
+ # we got a designator
+ cn = d['classname']
+ cl = self.db.classes[cn]
+ nodeid = d['id']
+ propname = d['propname']
+ elif d['note']:
+ # the special note field
+ cn = 'msg'
+ cl = self.db.classes[cn]
+ nodeid = '-1'
+ propname = 'content'
+ all_links.append((default_cn, default_nodeid, 'messages',
+ [('msg', '-1')]))
+ have_note = 1
+ elif d['file']:
+ # the special file field
+ cn = 'file'
+ cl = self.db.classes[cn]
+ nodeid = '-1'
+ propname = 'content'
+ all_links.append((default_cn, default_nodeid, 'files',
+ [('file', '-1')]))
+ have_file = 1
+ else:
+ # default
+ cn = default_cn
+ cl = default_cl
+ nodeid = default_nodeid
+ propname = d['propname']
+
+ # the thing this value relates to is...
+ this = (cn, nodeid)
+
+ # skip implicit create if this isn't a create action
+ if not create and nodeid is None:
+ continue
+
+ # get more info about the class, and the current set of
+ # form props for it
+ if not all_propdef.has_key(cn):
+ all_propdef[cn] = cl.getprops()
+ propdef = all_propdef[cn]
+ if not all_props.has_key(this):
+ all_props[this] = {}
+ props = all_props[this]
+ if not got_props.has_key(this):
+ got_props[this] = {}
+
+ # is this a link command?
+ if d['link']:
+ value = []
+ for entry in self.extractFormList(form[key]):
+ m = self.FV_DESIGNATOR.match(entry)
+ if not m:
+ raise FormError, self._('link "%(key)s" '
+ 'value "%(value)s" not a designator') % locals()
+ value.append((m.group(1), m.group(2)))
+
+ # make sure the link property is valid
+ if (not isinstance(propdef[propname], hyperdb.Multilink) and
+ not isinstance(propdef[propname], hyperdb.Link)):
+ raise FormError, self._('%(class)s %(property)s '
+ 'is not a link or multilink property') % {
+ 'class':cn, 'property':propname}
+
+ all_links.append((cn, nodeid, propname, value))
+ continue
+
+ # detect the special ":required" variable
+ if d['required']:
+ all_required[this] = self.extractFormList(form[key])
+ continue
+
+ # see if we're performing a special multilink action
+ mlaction = 'set'
+ if d['remove']:
+ mlaction = 'remove'
+ elif d['add']:
+ mlaction = 'add'
+
+ # does the property exist?
+ if not propdef.has_key(propname):
+ if mlaction != 'set':
+ raise FormError, self._('You have submitted a %(action)s '
+ 'action for the property "%(property)s" '
+ 'which doesn\'t exist') % {
+ 'action': mlaction, 'property':propname}
+ # the form element is probably just something we don't care
+ # about - ignore it
+ continue
+ proptype = propdef[propname]
+
+ # Get the form value. This value may be a MiniFieldStorage
+ # or a list of MiniFieldStorages.
+ value = form[key]
+
+ # handle unpacking of the MiniFieldStorage / list form value
+ if isinstance(proptype, hyperdb.Multilink):
+ value = self.extractFormList(value)
+ else:
+ # multiple values are not OK
+ if isinstance(value, type([])):
+ raise FormError, self._('You have submitted more than one '
+ 'value for the %s property') % propname
+ # value might be a file upload...
+ if not hasattr(value, 'filename') or value.filename is None:
+ # nope, pull out the value and strip it
+ value = value.value.strip()
+
+ # now that we have the props field, we need a teensy little
+ # extra bit of help for the old :note field...
+ if d['note'] and value:
+ props['author'] = self.db.getuid()
+ props['date'] = date.Date()
+
+ # handle by type now
+ if isinstance(proptype, hyperdb.Password):
+ if not value:
+ # ignore empty password values
+ continue
+ for key, d in matches:
+ if d['confirm'] and d['propname'] == propname:
+ confirm = form[key]
+ break
+ else:
+ raise FormError, self._('Password and confirmation text '
+ 'do not match')
+ if isinstance(confirm, type([])):
+ raise FormError, self._('You have submitted more than one '
+ 'value for the %s property') % propname
+ if value != confirm.value:
+ raise FormError, self._('Password and confirmation text '
+ 'do not match')
+ try:
+ value = password.Password(value)
+ except hyperdb.HyperdbValueError, msg:
+ raise FormError, msg
+
+ elif isinstance(proptype, hyperdb.Multilink):
+ # convert input to list of ids
+ try:
+ l = hyperdb.rawToHyperdb(self.db, cl, nodeid,
+ propname, value)
+ except hyperdb.HyperdbValueError, msg:
+ raise FormError, msg
+
+ # now use that list of ids to modify the multilink
+ if mlaction == 'set':
+ value = l
+ else:
+ # we're modifying the list - get the current list of ids
+ if props.has_key(propname):
+ existing = props[propname]
+ elif nodeid and not nodeid.startswith('-'):
+ existing = cl.get(nodeid, propname, [])
+ else:
+ existing = []
+
+ # now either remove or add
+ if mlaction == 'remove':
+ # remove - handle situation where the id isn't in
+ # the list
+ for entry in l:
+ try:
+ existing.remove(entry)
+ except ValueError:
+ raise FormError, self._('property '
+ '"%(propname)s": "%(value)s" '
+ 'not currently in list') % {
+ 'propname': propname, 'value': entry}
+ else:
+ # add - easy, just don't dupe
+ for entry in l:
+ if entry not in existing:
+ existing.append(entry)
+ value = existing
+ value.sort()
+
+ elif value == '':
+ # other types should be None'd if there's no value
+ value = None
+ else:
+ # handle all other types
+ try:
+ if isinstance(proptype, hyperdb.String):
+ if (hasattr(value, 'filename') and
+ value.filename is not None):
+ # skip if the upload is empty
+ if not value.filename:
+ continue
+ # this String is actually a _file_
+ # try to determine the file content-type
+ fn = value.filename.split('\\')[-1]
+ if propdef.has_key('name'):
+ props['name'] = fn
+ # use this info as the type/filename properties
+ if propdef.has_key('type'):
+ if hasattr(value, 'type') and value.type:
+ props['type'] = value.type
+ elif mimetypes.guess_type(fn)[0]:
+ props['type'] = mimetypes.guess_type(fn)[0]
+ else:
+ props['type'] = "application/octet-stream"
+ # finally, read the content RAW
+ value = value.value
+ else:
+ value = hyperdb.rawToHyperdb(self.db, cl,
+ nodeid, propname, value)
+
+ else:
+ value = hyperdb.rawToHyperdb(self.db, cl, nodeid,
+ propname, value)
+ except hyperdb.HyperdbValueError, msg:
+ raise FormError, msg
+
+ # register that we got this property
+ if value:
+ got_props[this][propname] = 1
+
+ # get the old value
+ if nodeid and not nodeid.startswith('-'):
+ try:
+ existing = cl.get(nodeid, propname)
+ except KeyError:
+ # this might be a new property for which there is
+ # no existing value
+ if not propdef.has_key(propname):
+ raise
+ except IndexError, message:
+ raise FormError(str(message))
+
+ # make sure the existing multilink is sorted
+ if isinstance(proptype, hyperdb.Multilink):
+ existing.sort()
+
+ # "missing" existing values may not be None
+ if not existing:
+ if isinstance(proptype, hyperdb.String):
+ # some backends store "missing" Strings as empty strings
+ if existing == self.db.BACKEND_MISSING_STRING:
+ existing = None
+ elif isinstance(proptype, hyperdb.Number):
+ # some backends store "missing" Numbers as 0 :(
+ if existing == self.db.BACKEND_MISSING_NUMBER:
+ existing = None
+ elif isinstance(proptype, hyperdb.Boolean):
+ # likewise Booleans
+ if existing == self.db.BACKEND_MISSING_BOOLEAN:
+ existing = None
+
+ # if changed, set it
+ if value != existing:
+ props[propname] = value
+ else:
+ # don't bother setting empty/unset values
+ if value is None:
+ continue
+ elif isinstance(proptype, hyperdb.Multilink) and value == []:
+ continue
+ elif isinstance(proptype, hyperdb.String) and value == '':
+ continue
+
+ props[propname] = value
+
+ # check to see if we need to specially link a file to the note
+ if have_note and have_file:
+ all_links.append(('msg', '-1', 'files', [('file', '-1')]))
+
+ # see if all the required properties have been supplied
+ s = []
+ for thing, required in all_required.items():
+ # register the values we got
+ got = got_props.get(thing, {})
+ for entry in required[:]:
+ if got.has_key(entry):
+ required.remove(entry)
+
+ # any required values not present?
+ if not required:
+ continue
+
+ # tell the user to entry the values required
+ s.append(self.ngettext(
+ 'Required %(class)s property %(property)s not supplied',
+ 'Required %(class)s properties %(property)s not supplied',
+ len(required)
+ ) % {
+ 'class': self._(thing[0]),
+ 'property': ', '.join(map(self.gettext, required))
+ })
+ if s:
+ raise FormError, '\n'.join(s)
+
+ # When creating a FileClass node, it should have a non-empty content
+ # property to be created. When editing a FileClass node, it should
+ # 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:
+ # 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 not props.get('content', ''):
+ del all_props[(cn, id)]
+ elif props.has_key('content') and not props['content']:
+ raise FormError, self._('File is empty')
+ return all_props, all_links
+
+ def extractFormList(self, value):
+ ''' Extract a list of values from the form value.
+
+ It may be one of:
+ [MiniFieldStorage('value'), MiniFieldStorage('value','value',...), ...]
+ MiniFieldStorage('value,value,...')
+ MiniFieldStorage('value')
+ '''
+ # multiple values are OK
+ if isinstance(value, type([])):
+ # it's a list of MiniFieldStorages - join then into
+ values = ','.join([i.value.strip() for i in value])
+ else:
+ # it's a MiniFieldStorage, but may be a comma-separated list
+ # of values
+ values = value.value
+
+ value = [i.strip() for i in values.split(',')]
+
+ # filter out the empty bits
+ return filter(None, value)
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/cgi/templating.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/templating.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2595 @@
+"""Implements the API used in the HTML templating for the web interface.
+"""
+
+todo = '''
+- Most methods should have a "default" arg to supply a value
+ when none appears in the hyperdb or request.
+- Multilink property additions: change_note and new_upload
+- Add class.find() too
+- NumberHTMLProperty should support numeric operations
+- LinkHTMLProperty should handle comparisons to strings (cf. linked name)
+- HTMLRequest.default(self, sort, group, filter, columns, **filterspec):
+ """Set the request's view arguments to the given values when no
+ values are found in the CGI environment.
+ """
+- have menu() methods accept filtering arguments
+'''
+
+__docformat__ = 'restructuredtext'
+
+from __future__ import nested_scopes
+
+import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes, csv
+import calendar
+
+from roundup import hyperdb, date, support
+from roundup import i18n
+from roundup.i18n import _
+
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+try:
+ from StructuredText.StructuredText import HTML as StructuredText
+except ImportError:
+ try: # older version
+ import StructuredText
+ except ImportError:
+ StructuredText = None
+
+# bring in the templating support
+from roundup.cgi.PageTemplates import PageTemplate, GlobalTranslationService
+from roundup.cgi.PageTemplates.Expressions import getEngine
+from roundup.cgi.TAL import TALInterpreter
+from roundup.cgi import TranslationService, ZTUtils
+
+### i18n services
+# this global translation service is not thread-safe.
+# it is left here for backward compatibility
+# until all Web UI translations are done via client.translator object
+translationService = TranslationService.get_translation()
+GlobalTranslationService.setGlobalTranslationService(translationService)
+
+### templating
+
+class NoTemplate(Exception):
+ pass
+
+class Unauthorised(Exception):
+ def __init__(self, action, klass, translator=None):
+ self.action = action
+ self.klass = klass
+ if translator:
+ self._ = translator.gettext
+ else:
+ self._ = TranslationService.get_translation().gettext
+ def __str__(self):
+ return self._('You are not allowed to %(action)s '
+ 'items of class %(class)s') % {
+ 'action': self.action, 'class': self.klass}
+
+def find_template(dir, name, view):
+ ''' Find a template in the nominated dir
+ '''
+ # find the source
+ if view:
+ filename = '%s.%s'%(name, view)
+ else:
+ filename = name
+
+ # try old-style
+ src = os.path.join(dir, filename)
+ if os.path.exists(src):
+ return (src, filename)
+
+ # try with a .html or .xml extension (new-style)
+ for extension in '.html', '.xml':
+ f = filename + extension
+ src = os.path.join(dir, f)
+ if os.path.exists(src):
+ return (src, f)
+
+ # no view == no generic template is possible
+ if not view:
+ raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
+
+ # try for a _generic template
+ generic = '_generic.%s'%view
+ src = os.path.join(dir, generic)
+ if os.path.exists(src):
+ return (src, generic)
+
+ # finally, try _generic.html
+ generic = generic + '.html'
+ src = os.path.join(dir, generic)
+ if os.path.exists(src):
+ return (src, generic)
+
+ raise NoTemplate, 'No template file exists for templating "%s" '\
+ 'with template "%s" (neither "%s" nor "%s")'%(name, view,
+ filename, generic)
+
+class Templates:
+ templates = {}
+
+ def __init__(self, dir):
+ self.dir = dir
+
+ def precompileTemplates(self):
+ ''' Go through a directory and precompile all the templates therein
+ '''
+ for filename in os.listdir(self.dir):
+ # skip subdirs
+ if os.path.isdir(filename):
+ continue
+
+ # skip files without ".html" or ".xml" extension - .css, .js etc.
+ for extension in '.html', '.xml':
+ if filename.endswith(extension):
+ break
+ else:
+ continue
+
+ # remove extension
+ filename = filename[:-len(extension)]
+
+ # load the template
+ if '.' in filename:
+ name, extension = filename.split('.', 1)
+ self.get(name, extension)
+ else:
+ self.get(filename, None)
+
+ def get(self, name, extension=None):
+ ''' Interface to get a template, possibly loading a compiled template.
+
+ "name" and "extension" indicate the template we're after, which in
+ most cases will be "name.extension". If "extension" is None, then
+ we look for a template just called "name" with no extension.
+
+ If the file "name.extension" doesn't exist, we look for
+ "_generic.extension" as a fallback.
+ '''
+ # default the name to "home"
+ if name is None:
+ name = 'home'
+ elif extension is None and '.' in name:
+ # split name
+ name, extension = name.split('.')
+
+ # find the source
+ src, filename = find_template(self.dir, name, extension)
+
+ # has it changed?
+ try:
+ stime = os.stat(src)[os.path.stat.ST_MTIME]
+ except os.error, error:
+ if error.errno != errno.ENOENT:
+ raise
+
+ if self.templates.has_key(src) and \
+ stime <= self.templates[src].mtime:
+ # compiled template is up to date
+ return self.templates[src]
+
+ # compile the template
+ self.templates[src] = pt = RoundupPageTemplate()
+ # use pt_edit so we can pass the content_type guess too
+ content_type = mimetypes.guess_type(filename)[0] or 'text/html'
+ pt.pt_edit(open(src).read(), content_type)
+ pt.id = filename
+ pt.mtime = stime
+ return pt
+
+ def __getitem__(self, name):
+ name, extension = os.path.splitext(name)
+ if extension:
+ extension = extension[1:]
+ try:
+ return self.get(name, extension)
+ except NoTemplate, message:
+ raise KeyError, message
+
+def context(client, template=None, classname=None, request=None):
+ """Return the rendering context dictionary
+
+ The dictionary includes following symbols:
+
+ *context*
+ this is one of three things:
+
+ 1. None - we're viewing a "home" page
+ 2. The current class of item being displayed. This is an HTMLClass
+ instance.
+ 3. The current item from the database, if we're viewing a specific
+ item, as an HTMLItem instance.
+
+ *request*
+ Includes information about the current request, including:
+
+ - the url
+ - the current index information (``filterspec``, ``filter`` args,
+ ``properties``, etc) parsed out of the form.
+ - methods for easy filterspec link generation
+ - *user*, the current user node as an HTMLItem instance
+ - *form*, the current CGI form information as a FieldStorage
+
+ *config*
+ The current tracker config.
+
+ *db*
+ The current database, used to access arbitrary database items.
+
+ *utils*
+ This is a special class that has its base in the TemplatingUtils
+ class in this file. If the tracker interfaces module defines a
+ TemplatingUtils class then it is mixed in, overriding the methods
+ in the base class.
+
+ *templates*
+ Access to all the tracker templates by name.
+ Used mainly in *use-macro* commands.
+
+ *template*
+ Current rendering template.
+
+ *true*
+ Logical True value.
+
+ *false*
+ Logical False value.
+
+ *i18n*
+ Internationalization service, providing string translation
+ methods ``gettext`` and ``ngettext``.
+
+ """
+ # construct the TemplatingUtils class
+ utils = TemplatingUtils
+ if (hasattr(client.instance, 'interfaces') and
+ hasattr(client.instance.interfaces, 'TemplatingUtils')):
+ class utils(client.instance.interfaces.TemplatingUtils, utils):
+ pass
+
+ # if template, classname and/or request are not passed explicitely,
+ # compute form client
+ if template is None:
+ template = client.template
+ if classname is None:
+ classname = client.classname
+ if request is None:
+ request = HTMLRequest(client)
+
+ c = {
+ 'context': None,
+ 'options': {},
+ 'nothing': None,
+ 'request': request,
+ 'db': HTMLDatabase(client),
+ 'config': client.instance.config,
+ 'tracker': client.instance,
+ 'utils': utils(client),
+ 'templates': client.instance.templates,
+ 'template': template,
+ 'true': 1,
+ 'false': 0,
+ 'i18n': client.translator
+ }
+ # add in the item if there is one
+ if client.nodeid:
+ c['context'] = HTMLItem(client, classname, client.nodeid,
+ anonymous=1)
+ elif client.db.classes.has_key(classname):
+ c['context'] = HTMLClass(client, classname, anonymous=1)
+ return c
+
+class RoundupPageTemplate(PageTemplate.PageTemplate):
+ '''A Roundup-specific PageTemplate.
+
+ Interrogate the client to set up Roundup-specific template variables
+ to be available. See 'context' function for the list of variables.
+
+ '''
+
+ # 06-jun-2004 [als] i am not sure if this method is used yet
+ def getContext(self, client, classname, request):
+ return context(client, self, classname, request)
+
+ def render(self, client, classname, request, **options):
+ """Render this Page Template"""
+
+ if not self._v_cooked:
+ self._cook()
+
+ __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
+
+ if self._v_errors:
+ raise PageTemplate.PTRuntimeError, \
+ 'Page Template %s has errors.'%self.id
+
+ # figure the context
+ c = context(client, self, classname, request)
+ c.update({'options': options})
+
+ # and go
+ output = StringIO.StringIO()
+ TALInterpreter.TALInterpreter(self._v_program, self.macros,
+ getEngine().getContext(c), output, tal=1, strictinsert=0)()
+ return output.getvalue()
+
+ def __repr__(self):
+ return '<Roundup PageTemplate %r>'%self.id
+
+class HTMLDatabase:
+ ''' Return HTMLClasses for valid class fetches
+ '''
+ def __init__(self, client):
+ self._client = client
+ self._ = client._
+ self._db = client.db
+
+ # 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]+)')):
+ # check to see if we're actually accessing an item
+ m = desre.match(item)
+ if m:
+ cl = m.group('cl')
+ self._client.db.getclass(cl)
+ return HTMLItem(self._client, cl, m.group('id'))
+ else:
+ self._client.db.getclass(item)
+ return HTMLClass(self._client, item)
+
+ def __getattr__(self, attr):
+ try:
+ return self[attr]
+ except KeyError:
+ raise AttributeError, attr
+
+ def classes(self):
+ l = self._client.db.classes.keys()
+ l.sort()
+ m = []
+ for item in l:
+ m.append(HTMLClass(self._client, item))
+ return m
+
+def lookupIds(db, prop, ids, fail_ok=0, num_re=re.compile('^-?\d+$')):
+ ''' "fail_ok" should be specified if we wish to pass through bad values
+ (most likely form values that we wish to represent back to the user)
+ '''
+ cl = db.getclass(prop.classname)
+ l = []
+ for entry in ids:
+ try:
+ l.append(cl.lookup(entry))
+ except (TypeError, KeyError):
+ # if fail_ok, ignore lookup error
+ # otherwise entry must be existing object id rather than key value
+ if fail_ok or num_re.match(entry):
+ l.append(entry)
+ return l
+
+def lookupKeys(linkcl, key, ids, num_re=re.compile('^-?\d+$')):
+ ''' Look up the "key" values for "ids" list - though some may already
+ be key values, not ids.
+ '''
+ l = []
+ for entry in ids:
+ if num_re.match(entry):
+ label = linkcl.get(entry, key)
+ # fall back to designator if label is None
+ if label is None: label = '%s%s'%(linkcl.classname, entry)
+ l.append(label)
+ else:
+ l.append(entry)
+ return l
+
+def input_html4(**attrs):
+ """Generate an 'input' (html4) element with given attributes"""
+ return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
+
+def input_xhtml(**attrs):
+ """Generate an 'input' (xhtml) element with given attributes"""
+ return '<input %s/>'%' '.join(['%s="%s"'%item for item in attrs.items()])
+
+class HTMLInputMixin:
+ ''' requires a _client property '''
+ def __init__(self):
+ html_version = 'html4'
+ if hasattr(self._client.instance.config, 'HTML_VERSION'):
+ html_version = self._client.instance.config.HTML_VERSION
+ if html_version == 'xhtml':
+ self.input = input_xhtml
+ else:
+ self.input = input_html4
+ # self._context is used for translations.
+ # will be initialized by the first call to .gettext()
+ self._context = None
+
+ def gettext(self, msgid):
+ """Return the localized translation of msgid"""
+ if self._context is None:
+ self._context = context(self._client)
+ return self._client.translator.translate(domain="roundup",
+ msgid=msgid, context=self._context)
+
+ _ = gettext
+
+class HTMLPermissions:
+
+ def view_check(self):
+ ''' Raise the Unauthorised exception if the user's not permitted to
+ view this class.
+ '''
+ if not self.is_view_ok():
+ raise Unauthorised("view", self._classname,
+ translator=self._client.translator)
+
+ def edit_check(self):
+ ''' Raise the Unauthorised exception if the user's not permitted to
+ edit items of this class.
+ '''
+ if not self.is_edit_ok():
+ raise Unauthorised("edit", self._classname,
+ translator=self._client.translator)
+
+
+class HTMLClass(HTMLInputMixin, HTMLPermissions):
+ ''' Accesses through a class (either through *class* or *db.<classname>*)
+ '''
+ def __init__(self, client, classname, anonymous=0):
+ self._client = client
+ self._ = client._
+ self._db = client.db
+ self._anonymous = anonymous
+
+ # we want classname to be exposed, but _classname gives a
+ # consistent API for extending Class/Item
+ self._classname = self.classname = classname
+ self._klass = self._db.getclass(self.classname)
+ self._props = self._klass.getprops()
+
+ HTMLInputMixin.__init__(self)
+
+ def is_edit_ok(self):
+ ''' Is the user allowed to Create the current class?
+ '''
+ return self._db.security.hasPermission('Create', self._client.userid,
+ self._classname)
+
+ def is_view_ok(self):
+ ''' Is the user allowed to View the current class?
+ '''
+ return self._db.security.hasPermission('View', self._client.userid,
+ self._classname)
+
+ def is_only_view_ok(self):
+ ''' Is the user only allowed to View (ie. not Create) the current class?
+ '''
+ return self.is_view_ok() and not self.is_edit_ok()
+
+ def __repr__(self):
+ return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
+
+ def __getitem__(self, item):
+ ''' return an HTMLProperty instance
+ '''
+ #print 'HTMLClass.getitem', (self, item)
+
+ # we don't exist
+ if item == 'id':
+ return None
+
+ # get the property
+ try:
+ prop = self._props[item]
+ except KeyError:
+ raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
+
+ # look up the correct HTMLProperty class
+ form = self._client.form
+ 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
+ else:
+ if isinstance(prop, hyperdb.Multilink):
+ value = []
+ else:
+ value = None
+ return htmlklass(self._client, self._classname, '', prop, item,
+ value, self._anonymous)
+
+ # no good
+ raise KeyError, item
+
+ def __getattr__(self, attr):
+ ''' convenience access '''
+ try:
+ return self[attr]
+ except KeyError:
+ raise AttributeError, attr
+
+ def designator(self):
+ ''' Return this class' designator (classname) '''
+ return self._classname
+
+ def getItem(self, itemid, num_re=re.compile('^-?\d+$')):
+ ''' Get an item of this class by its item id.
+ '''
+ # make sure we're looking at an itemid
+ if not isinstance(itemid, type(1)) and not num_re.match(itemid):
+ itemid = self._klass.lookup(itemid)
+
+ return HTMLItem(self._client, self.classname, itemid)
+
+ def properties(self, sort=1):
+ ''' Return HTMLProperty for all of this class' properties.
+ '''
+ l = []
+ for name, prop in self._props.items():
+ for klass, htmlklass in propclasses:
+ if isinstance(prop, hyperdb.Multilink):
+ value = []
+ else:
+ value = None
+ if isinstance(prop, klass):
+ l.append(htmlklass(self._client, self._classname, '',
+ prop, name, value, self._anonymous))
+ if sort:
+ l.sort(lambda a,b:cmp(a._name, b._name))
+ return l
+
+ def list(self, sort_on=None):
+ ''' List all items in this class.
+ '''
+ # get the list and sort it nicely
+ l = self._klass.list()
+ sortfunc = make_sort_function(self._db, self._classname, sort_on)
+ l.sort(sortfunc)
+
+ # check perms
+ check = self._client.db.security.hasPermission
+ userid = self._client.userid
+
+ l = [HTMLItem(self._client, self._classname, id) for id in l
+ if check('View', userid, self._classname, itemid=id)]
+
+ return l
+
+ def csv(self):
+ ''' Return the items of this class as a chunk of CSV text.
+ '''
+ props = self.propnames()
+ s = StringIO.StringIO()
+ writer = csv.writer(s)
+ writer.writerow(props)
+ for nodeid in self._klass.list():
+ l = []
+ for name in props:
+ value = self._klass.get(nodeid, name)
+ if value is None:
+ l.append('')
+ elif isinstance(value, type([])):
+ l.append(':'.join(map(str, value)))
+ else:
+ l.append(str(self._klass.get(nodeid, name)))
+ writer.writerow(l)
+ return s.getvalue()
+
+ def propnames(self):
+ ''' Return the list of the names of the properties of this class.
+ '''
+ idlessprops = self._klass.getprops(protected=0).keys()
+ idlessprops.sort()
+ return ['id'] + idlessprops
+
+ def filter(self, request=None, filterspec={}, sort=(None,None),
+ group=(None,None)):
+ ''' Return a list of items from this class, filtered and sorted
+ by the current requested filterspec/filter/sort/group args
+
+ "request" takes precedence over the other three arguments.
+ '''
+ if request is not None:
+ filterspec = request.filterspec
+ sort = request.sort
+ group = request.group
+
+ check = self._db.security.hasPermission
+ userid = self._client.userid
+
+ l = [HTMLItem(self._client, self.classname, id)
+ for id in self._klass.filter(None, filterspec, sort, group)
+ if check('View', userid, self.classname, itemid=id)]
+ return l
+
+ def classhelp(self, properties=None, label=''"(list)", width='500',
+ height='400', property='', form='itemSynopsis',
+ pagesize=50, inputtype="checkbox", sort=None, filter=None):
+ '''Pop up a javascript window with class help
+
+ This generates a link to a popup window which displays the
+ properties indicated by "properties" of the class named by
+ "classname". The "properties" should be a comma-separated list
+ (eg. 'id,name,description'). Properties defaults to all the
+ properties of a class (excluding id, creator, created and
+ activity).
+
+ You may optionally override the label displayed, the width,
+ the height, the number of items per page and the field on which
+ the list is sorted (defaults to username if in the displayed
+ properties).
+
+ With the "filter" arg it is possible to specify a filter for
+ which items are supposed to be displayed. It has to be of
+ the format "<field>=<values>;<field>=<values>;...".
+
+ The popup window will be resizable and scrollable.
+
+ If the "property" arg is given, it's passed through to the
+ javascript help_window function.
+
+ You can use inputtype="radio" to display a radio box instead
+ of the default checkbox (useful for entering Link-properties)
+
+ If the "form" arg is given, it's passed through to the
+ javascript help_window function. - it's the name of the form
+ the "property" belongs to.
+ '''
+ if properties is None:
+ properties = self._klass.getprops(protected=0).keys()
+ properties.sort()
+ properties = ','.join(properties)
+ if sort is None:
+ if 'username' in properties.split( ',' ):
+ sort = 'username'
+ else:
+ sort = find_sort_key(self._klass)
+ sort = '&@sort=' + sort
+ if property:
+ property = '&property=%s'%property
+ if form:
+ form = '&form=%s'%form
+ if inputtype:
+ type= '&type=%s'%inputtype
+ if filter:
+ filterprops = filter.split(';')
+ filtervalues = []
+ names = []
+ for x in filterprops:
+ (name, values) = x.split('=')
+ names.append(name)
+ filtervalues.append('&%s=%s' % (name, urllib.quote(values)))
+ filter = '&@filter=%s%s' % (','.join(names), ''.join(filtervalues))
+ else:
+ filter = ''
+ help_url = "%s?@startwith=0&@template=help&"\
+ "properties=%s%s%s%s%s&@pagesize=%s%s" % \
+ (self.classname, properties, property, form, type,
+ sort, pagesize, filter)
+ onclick = "javascript:help_window('%s', '%s', '%s');return false;" % \
+ (help_url, width, height)
+ return '<a class="classhelp" href="%s" onclick="%s">%s</a>' % \
+ (help_url, onclick, self._(label))
+
+ def submit(self, label=''"Submit New Entry"):
+ ''' Generate a submit button (and action hidden element)
+
+ Generate nothing if we're not editable.
+ '''
+ if not self.is_edit_ok():
+ return ''
+
+ return self.input(type="hidden", name="@action", value="new") + \
+ '\n' + \
+ self.input(type="submit", name="submit", value=self._(label))
+
+ def history(self):
+ if not self.is_view_ok():
+ return self._('[hidden]')
+ return self._('New node - no history')
+
+ def renderWith(self, name, **kwargs):
+ ''' Render this class with the given template.
+ '''
+ # create a new request and override the specified args
+ req = HTMLRequest(self._client)
+ req.classname = self.classname
+ req.update(kwargs)
+
+ # new template, using the specified classname and request
+ pt = self._client.instance.templates.get(self.classname, name)
+
+ # use our fabricated request
+ args = {
+ 'ok_message': self._client.ok_message,
+ 'error_message': self._client.error_message
+ }
+ return pt.render(self._client, self.classname, req, **args)
+
+class _HTMLItem(HTMLInputMixin, HTMLPermissions):
+ ''' Accesses through an *item*
+ '''
+ def __init__(self, client, classname, nodeid, anonymous=0):
+ self._client = client
+ self._db = client.db
+ self._classname = classname
+ self._nodeid = nodeid
+ self._klass = self._db.getclass(classname)
+ self._props = self._klass.getprops()
+
+ # do we prefix the form items with the item's identification?
+ self._anonymous = anonymous
+
+ HTMLInputMixin.__init__(self)
+
+ def is_edit_ok(self):
+ ''' Is the user allowed to Edit the current class?
+ '''
+ return self._db.security.hasPermission('Edit', self._client.userid,
+ self._classname, itemid=self._nodeid)
+
+ def is_view_ok(self):
+ ''' Is the user allowed to View the current class?
+ '''
+ if self._db.security.hasPermission('View', self._client.userid,
+ self._classname, itemid=self._nodeid):
+ return 1
+ return self.is_edit_ok()
+
+ def is_only_view_ok(self):
+ ''' Is the user only allowed to View (ie. not Edit) the current class?
+ '''
+ return self.is_view_ok() and not self.is_edit_ok()
+
+ def __repr__(self):
+ return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
+ self._nodeid)
+
+ def __getitem__(self, item):
+ ''' return an HTMLProperty instance
+ '''
+ #print 'HTMLItem.getitem', (self, item)
+ if item == 'id':
+ return self._nodeid
+
+ # get the property
+ prop = self._props[item]
+
+ # get the value, handling missing values
+ value = None
+ if int(self._nodeid) > 0:
+ value = self._klass.get(self._nodeid, item, None)
+ if value is None:
+ if isinstance(self._props[item], hyperdb.Multilink):
+ value = []
+
+ # look up the correct HTMLProperty class
+ for klass, htmlklass in propclasses:
+ if isinstance(prop, klass):
+ return htmlklass(self._client, self._classname,
+ self._nodeid, prop, item, value, self._anonymous)
+
+ raise KeyError, item
+
+ def __getattr__(self, attr):
+ ''' convenience access to properties '''
+ try:
+ return self[attr]
+ except KeyError:
+ raise AttributeError, attr
+
+ def designator(self):
+ """Return this item's designator (classname + id)."""
+ return '%s%s'%(self._classname, self._nodeid)
+
+ def is_retired(self):
+ """Is this item retired?"""
+ return self._klass.is_retired(self._nodeid)
+
+ def submit(self, label=''"Submit Changes"):
+ """Generate a submit button.
+
+ Also sneak in the lastactivity and action hidden elements.
+ """
+ return self.input(type="hidden", name="@lastactivity",
+ value=self.activity.local(0)) + '\n' + \
+ self.input(type="hidden", name="@action", value="edit") + '\n' + \
+ self.input(type="submit", name="submit", value=self._(label))
+
+ def journal(self, direction='descending'):
+ ''' Return a list of HTMLJournalEntry instances.
+ '''
+ # XXX do this
+ return []
+
+ def history(self, direction='descending', dre=re.compile('^\d+$')):
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ # pre-load the history with the current state
+ current = {}
+ for prop_n in self._props.keys():
+ prop = self[prop_n]
+ if not isinstance(prop, HTMLProperty):
+ continue
+ current[prop_n] = prop.plain()
+ # make link if hrefable
+ if (self._props.has_key(prop_n) and
+ isinstance(self._props[prop_n], hyperdb.Link)):
+ classname = self._props[prop_n].classname
+ try:
+ template = find_template(self._db.config.TEMPLATES,
+ classname, 'item')
+ if template[1].startswith('_generic'):
+ raise NoTemplate, 'not really...'
+ except NoTemplate:
+ pass
+ else:
+ id = self._klass.get(self._nodeid, prop_n, None)
+ current[prop_n] = '<a href="%s%s">%s</a>'%(
+ classname, id, current[prop_n])
+
+ # get the journal, sort and reverse
+ history = self._klass.history(self._nodeid)
+ history.sort()
+ history.reverse()
+
+ timezone = self._db.getUserTimezone()
+ l = []
+ comments = {}
+ for id, evt_date, user, action, args in history:
+ date_s = str(evt_date.local(timezone)).replace("."," ")
+ arg_s = ''
+ if action == 'link' and type(args) == type(()):
+ if len(args) == 3:
+ linkcl, linkid, key = args
+ arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
+ linkcl, linkid, key)
+ else:
+ arg_s = str(args)
+
+ elif action == 'unlink' and type(args) == type(()):
+ if len(args) == 3:
+ linkcl, linkid, key = args
+ arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
+ linkcl, linkid, key)
+ else:
+ arg_s = str(args)
+
+ elif type(args) == type({}):
+ cell = []
+ for k in args.keys():
+ # try to get the relevant property and treat it
+ # specially
+ try:
+ prop = self._props[k]
+ except KeyError:
+ prop = None
+ if prop is None:
+ # property no longer exists
+ comments['no_exist'] = self._(
+ "<em>The indicated property no longer exists</em>")
+ cell.append(self._('<em>%s: %s</em>\n')
+ % (self._(k), str(args[k])))
+ continue
+
+ if args[k] and (isinstance(prop, hyperdb.Multilink) or
+ isinstance(prop, hyperdb.Link)):
+ # figure what the link class is
+ classname = prop.classname
+ try:
+ linkcl = self._db.getclass(classname)
+ except KeyError:
+ labelprop = None
+ comments[classname] = self._(
+ "The linked class %(classname)s no longer exists"
+ ) % locals()
+ labelprop = linkcl.labelprop(1)
+ try:
+ template = find_template(self._db.config.TEMPLATES,
+ classname, 'item')
+ if template[1].startswith('_generic'):
+ raise NoTemplate, 'not really...'
+ hrefable = 1
+ except NoTemplate:
+ hrefable = 0
+
+ if isinstance(prop, hyperdb.Multilink) and args[k]:
+ ml = []
+ for linkid in args[k]:
+ if isinstance(linkid, type(())):
+ sublabel = linkid[0] + ' '
+ linkids = linkid[1]
+ else:
+ sublabel = ''
+ linkids = [linkid]
+ subml = []
+ for linkid in linkids:
+ label = classname + linkid
+ # if we have a label property, try to use it
+ # TODO: test for node existence even when
+ # there's no labelprop!
+ try:
+ if labelprop is not None and \
+ labelprop != 'id':
+ label = linkcl.get(linkid, labelprop)
+ except IndexError:
+ comments['no_link'] = self._(
+ "<strike>The linked node"
+ " no longer exists</strike>")
+ subml.append('<strike>%s</strike>'%label)
+ else:
+ if hrefable:
+ subml.append('<a href="%s%s">%s</a>'%(
+ classname, linkid, label))
+ elif label is None:
+ subml.append('%s%s'%(classname,
+ linkid))
+ else:
+ subml.append(label)
+ ml.append(sublabel + ', '.join(subml))
+ cell.append('%s:\n %s'%(self._(k), ', '.join(ml)))
+ elif isinstance(prop, hyperdb.Link) and args[k]:
+ label = classname + args[k]
+ # if we have a label property, try to use it
+ # TODO: test for node existence even when
+ # there's no labelprop!
+ if labelprop is not None and labelprop != 'id':
+ try:
+ label = linkcl.get(args[k], labelprop)
+ except IndexError:
+ comments['no_link'] = self._(
+ "<strike>The linked node"
+ " no longer exists</strike>")
+ cell.append(' <strike>%s</strike>,\n'%label)
+ # "flag" this is done .... euwww
+ label = None
+ if label is not None:
+ if hrefable:
+ old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
+ else:
+ old = label;
+ cell.append('%s: %s' % (self._(k), old))
+ if current.has_key(k):
+ cell[-1] += ' -> %s'%current[k]
+ current[k] = old
+
+ elif isinstance(prop, hyperdb.Date) and args[k]:
+ if args[k] is None:
+ d = ''
+ else:
+ d = date.Date(args[k],
+ translator=self._client).local(timezone)
+ cell.append('%s: %s'%(self._(k), str(d)))
+ if current.has_key(k):
+ cell[-1] += ' -> %s' % current[k]
+ current[k] = str(d)
+
+ elif isinstance(prop, hyperdb.Interval) and args[k]:
+ val = str(date.Interval(args[k],
+ translator=self._client))
+ cell.append('%s: %s'%(self._(k), val))
+ if current.has_key(k):
+ cell[-1] += ' -> %s'%current[k]
+ current[k] = val
+
+ elif isinstance(prop, hyperdb.String) and args[k]:
+ val = cgi.escape(args[k])
+ cell.append('%s: %s'%(self._(k), val))
+ if current.has_key(k):
+ cell[-1] += ' -> %s'%current[k]
+ current[k] = val
+
+ elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
+ val = args[k] and ''"Yes" or ''"No"
+ cell.append('%s: %s'%(self._(k), val))
+ if current.has_key(k):
+ cell[-1] += ' -> %s'%current[k]
+ current[k] = val
+
+ elif not args[k]:
+ if current.has_key(k):
+ cell.append('%s: %s'%(self._(k), current[k]))
+ current[k] = '(no value)'
+ else:
+ cell.append(self._('%s: (no value)')%self._(k))
+
+ else:
+ cell.append('%s: %s'%(self._(k), str(args[k])))
+ if current.has_key(k):
+ cell[-1] += ' -> %s'%current[k]
+ current[k] = str(args[k])
+
+ arg_s = '<br />'.join(cell)
+ else:
+ # unkown event!!
+ comments['unknown'] = self._(
+ "<strong><em>This event is not handled"
+ " by the history display!</em></strong>")
+ arg_s = '<strong><em>' + str(args) + '</em></strong>'
+ date_s = date_s.replace(' ', ' ')
+ # if the user's an itemid, figure the username (older journals
+ # have the username)
+ if dre.match(user):
+ user = self._db.user.get(user, 'username')
+ l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
+ date_s, user, self._(action), arg_s))
+ if comments:
+ l.append(self._(
+ '<tr><td colspan=4><strong>Note:</strong></td></tr>'))
+ for entry in comments.values():
+ l.append('<tr><td colspan=4>%s</td></tr>'%entry)
+
+ if direction == 'ascending':
+ l.reverse()
+
+ l[0:0] = ['<table class="history">'
+ '<tr><th colspan="4" class="header">',
+ self._('History'),
+ '</th></tr><tr>',
+ self._('<th>Date</th>'),
+ self._('<th>User</th>'),
+ self._('<th>Action</th>'),
+ self._('<th>Args</th>'),
+ '</tr>']
+ l.append('</table>')
+ return '\n'.join(l)
+
+ def renderQueryForm(self):
+ ''' Render this item, which is a query, as a search form.
+ '''
+ # create a new request and override the specified args
+ req = HTMLRequest(self._client)
+ req.classname = self._klass.get(self._nodeid, 'klass')
+ name = self._klass.get(self._nodeid, 'name')
+ req.updateFromURL(self._klass.get(self._nodeid, 'url') +
+ '&@queryname=%s'%urllib.quote(name))
+
+ # new template, using the specified classname and request
+ pt = self._client.instance.templates.get(req.classname, 'search')
+
+ # use our fabricated request
+ return pt.render(self._client, req.classname, req)
+
+ def download_url(self):
+ ''' Assume that this item is a FileClass and that it has a name
+ and content. Construct a URL for the download of the content.
+ '''
+ name = self._klass.get(self._nodeid, 'name')
+ url = '%s%s/%s'%(self._classname, self._nodeid, name)
+ return urllib.quote(url)
+
+ def copy_url(self, exclude=("messages", "files")):
+ """Construct a URL for creating a copy of this item
+
+ "exclude" is an optional list of properties that should
+ not be copied to the new object. By default, this list
+ includes "messages" and "files" properties. Note that
+ "id" property cannot be copied.
+
+ """
+ exclude = ("id", "activity", "actor", "creation", "creator") \
+ + tuple(exclude)
+ query = {
+ "@template": "item",
+ "@note": self._("Copy of %(class)s %(id)s") % {
+ "class": self._(self._classname), "id": self._nodeid},
+ }
+ for name in self._props.keys():
+ if name not in exclude:
+ query[name] = self[name].plain()
+ return self._classname + "?" + "&".join(
+ ["%s=%s" % (key, urllib.quote(value))
+ for key, value in query.items()])
+
+class _HTMLUser(_HTMLItem):
+ '''Add ability to check for permissions on users.
+ '''
+ _marker = []
+ def hasPermission(self, permission, classname=_marker,
+ property=None, itemid=None):
+ '''Determine if the user has the Permission.
+
+ The class being tested defaults to the template's class, but may
+ be overidden for this test by suppling an alternate classname.
+ '''
+ if classname is self._marker:
+ classname = self._client.classname
+ return self._db.security.hasPermission(permission,
+ self._nodeid, classname, property, itemid)
+
+ def hasRole(self, rolename):
+ '''Determine whether the user has the Role.'''
+ roles = self._db.user.get(self._nodeid, 'roles').split(',')
+ for role in roles:
+ if role.strip() == rolename: return True
+ return False
+
+def HTMLItem(client, classname, nodeid, anonymous=0):
+ if classname == 'user':
+ return _HTMLUser(client, classname, nodeid, anonymous)
+ else:
+ return _HTMLItem(client, classname, nodeid, anonymous)
+
+class HTMLProperty(HTMLInputMixin, HTMLPermissions):
+ ''' String, Number, Date, Interval HTMLProperty
+
+ Has useful attributes:
+
+ _name the name of the property
+ _value the value of the property if any
+
+ A wrapper object which may be stringified for the plain() behaviour.
+ '''
+ def __init__(self, client, classname, nodeid, prop, name, value,
+ anonymous=0):
+ self._client = client
+ self._db = client.db
+ self._ = client._
+ self._classname = classname
+ self._nodeid = nodeid
+ self._prop = prop
+ self._value = value
+ self._anonymous = anonymous
+ self._name = name
+ if not anonymous:
+ self._formname = '%s%s@%s'%(classname, nodeid, name)
+ else:
+ self._formname = name
+
+ HTMLInputMixin.__init__(self)
+
+ def __repr__(self):
+ return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
+ self._prop, self._value)
+ def __str__(self):
+ return self.plain()
+ def __cmp__(self, other):
+ if isinstance(other, HTMLProperty):
+ return cmp(self._value, other._value)
+ return cmp(self._value, other)
+
+ def __nonzero__(self):
+ return not not self._value
+
+ def isset(self):
+ '''Is my _value not None?'''
+ return self._value is not None
+
+ def is_edit_ok(self):
+ '''Should the user be allowed to use an edit form field for this
+ property. Check "Create" for new items, or "Edit" for existing
+ ones.
+ '''
+ if self._nodeid:
+ return self._db.security.hasPermission('Edit', self._client.userid,
+ self._classname, self._name, self._nodeid)
+ return self._db.security.hasPermission('Create', self._client.userid,
+ self._classname, self._name)
+
+ def is_view_ok(self):
+ ''' Is the user allowed to View the current class?
+ '''
+ if self._db.security.hasPermission('View', self._client.userid,
+ self._classname, self._name, self._nodeid):
+ return 1
+ return self.is_edit_ok()
+
+class StringHTMLProperty(HTMLProperty):
+ hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
+ r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
+ r'(?P<item>(?P<class>[A-Za-z_]+)(\s*)(?P<id>\d+)))')
+ def _hyper_repl(self, match):
+ if match.group('url'):
+ s = match.group('url')
+ return '<a href="%s">%s</a>'%(s, s)
+ elif match.group('email'):
+ s = match.group('email')
+ return '<a href="mailto:%s">%s</a>'%(s, s)
+ else:
+ s = match.group('item')
+ s1 = match.group('class').lower()
+ s2 = match.group('id')
+ try:
+ # make sure s1 is a valid tracker classname
+ cl = self._db.getclass(s1)
+ if not cl.hasnode(s2):
+ return s
+ return '<a href="%s%s">%s</a>'%(s1, s2, s)
+ except KeyError:
+ return s
+
+ def hyperlinked(self):
+ ''' Render a "hyperlinked" version of the text '''
+ return self.plain(hyperlink=1)
+
+ def plain(self, escape=0, hyperlink=0):
+ '''Render a "plain" representation of the property
+
+ - "escape" turns on/off HTML quoting
+ - "hyperlink" turns on/off in-text hyperlinking of URLs, email
+ addresses and designators
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ if self._value is None:
+ return ''
+ if escape:
+ s = cgi.escape(str(self._value))
+ else:
+ s = str(self._value)
+ if hyperlink:
+ # no, we *must* escape this text
+ if not escape:
+ s = cgi.escape(s)
+ s = self.hyper_re.sub(self._hyper_repl, s)
+ return s
+
+ def stext(self, escape=0, hyperlink=1):
+ ''' Render the value of the property as StructuredText.
+
+ This requires the StructureText module to be installed separately.
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ s = self.plain(escape=escape, hyperlink=hyperlink)
+ if not StructuredText:
+ return s
+ return StructuredText(s,level=1,header=0)
+
+ def field(self, **kwargs):
+ ''' Render the property as a field in HTML.
+
+ If not editable, just display the value via plain().
+ '''
+ if not self.is_edit_ok():
+ return self.plain()
+
+ if self._value is None:
+ value = ''
+ else:
+ value = cgi.escape(str(self._value))
+
+ value = '"'.join(value.split('"'))
+
+ kwargs.setdefault("size", 30)
+ kwargs.update({"name": self._formname, "value": value})
+ return self.input(**kwargs)
+
+ def multiline(self, escape=0, rows=5, cols=40):
+ ''' Render a multiline form edit field for the property.
+
+ If not editable, just display the plain() value in a <pre> tag.
+ '''
+ if not self.is_edit_ok():
+ return '<pre>%s</pre>'%self.plain()
+
+ if self._value is None:
+ value = ''
+ else:
+ value = cgi.escape(str(self._value))
+
+ value = '"'.join(value.split('"'))
+ return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
+ self._formname, rows, cols, value)
+
+ def email(self, escape=1):
+ ''' Render the value of the property as an obscured email address
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ if self._value is None:
+ value = ''
+ else:
+ value = str(self._value)
+ split = value.split('@')
+ if len(split) == 2:
+ name, domain = split
+ domain = ' '.join(domain.split('.')[:-1])
+ name = name.replace('.', ' ')
+ value = '%s at %s ...'%(name, domain)
+ else:
+ value = value.replace('.', ' ')
+ if escape:
+ value = cgi.escape(value)
+ return value
+
+class PasswordHTMLProperty(HTMLProperty):
+ def plain(self):
+ ''' Render a "plain" representation of the property
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ if self._value is None:
+ return ''
+ return self._('*encrypted*')
+
+ def field(self, size = 30):
+ ''' Render a form edit field for the property.
+
+ If not editable, just display the value via plain().
+ '''
+ if not self.is_edit_ok():
+ return self.plain()
+
+ return self.input(type="password", name=self._formname, size=size)
+
+ def confirm(self, size = 30):
+ ''' Render a second form edit field for the property, used for
+ confirmation that the user typed the password correctly. Generates
+ a field with name "@confirm at name".
+
+ If not editable, display nothing.
+ '''
+ if not self.is_edit_ok():
+ return ''
+
+ return self.input(type="password",
+ name="@confirm@%s"%self._formname, size=size)
+
+class NumberHTMLProperty(HTMLProperty):
+ def plain(self):
+ ''' Render a "plain" representation of the property
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ if self._value is None:
+ return ''
+
+ return str(self._value)
+
+ def field(self, size = 30):
+ ''' Render a form edit field for the property.
+
+ If not editable, just display the value via plain().
+ '''
+ if not self.is_edit_ok():
+ return self.plain()
+
+ if self._value is None:
+ value = ''
+ else:
+ value = cgi.escape(str(self._value))
+
+ value = '"'.join(value.split('"'))
+ return self.input(name=self._formname,value=value,size=size)
+
+ def __int__(self):
+ ''' Return an int of me
+ '''
+ return int(self._value)
+
+ def __float__(self):
+ ''' Return a float of me
+ '''
+ return float(self._value)
+
+
+class BooleanHTMLProperty(HTMLProperty):
+ def plain(self):
+ ''' Render a "plain" representation of the property
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ if self._value is None:
+ return ''
+ return self._value and self._("Yes") or self._("No")
+
+ def field(self):
+ ''' Render a form edit field for the property
+
+ If not editable, just display the value via plain().
+ '''
+ if not self.is_edit_ok():
+ return self.plain()
+
+ value = self._value
+ if isinstance(value, str) or isinstance(value, unicode):
+ value = value.strip().lower() in ('checked', 'yes', 'true',
+ 'on', '1')
+
+ checked = value and "checked" or ""
+ if value:
+ s = self.input(type="radio", name=self._formname, value="yes",
+ checked="checked")
+ s += self._('Yes')
+ s +=self.input(type="radio", name=self._formname, value="no")
+ s += self._('No')
+ else:
+ s = self.input(type="radio", name=self._formname, value="yes")
+ s += self._('Yes')
+ s +=self.input(type="radio", name=self._formname, value="no",
+ checked="checked")
+ s += self._('No')
+ return s
+
+class DateHTMLProperty(HTMLProperty):
+
+ _marker = []
+
+ def __init__(self, client, classname, nodeid, prop, name, value,
+ anonymous=0, offset=None):
+ HTMLProperty.__init__(self, client, classname, nodeid, prop, name,
+ value, anonymous=anonymous)
+ if self._value and not (isinstance(self._value, str) or
+ isinstance(self._value, unicode)):
+ self._value.setTranslator(self._client.translator)
+ self._offset = offset
+ if self._offset is None :
+ self._offset = self._prop.offset (self._db)
+
+ def plain(self):
+ ''' Render a "plain" representation of the property
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ if self._value is None:
+ return ''
+ if self._offset is None:
+ offset = self._db.getUserTimezone()
+ else:
+ offset = self._offset
+ return str(self._value.local(offset))
+
+ def now(self, str_interval=None):
+ ''' Return the current time.
+
+ This is useful for defaulting a new value. Returns a
+ DateHTMLProperty.
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ ret = date.Date('.', translator=self._client)
+
+ if isinstance(str_interval, basestring):
+ sign = 1
+ if str_interval[0] == '-':
+ sign = -1
+ str_interval = str_interval[1:]
+ interval = date.Interval(str_interval, translator=self._client)
+ if sign > 0:
+ ret = ret + interval
+ else:
+ ret = ret - interval
+
+ return DateHTMLProperty(self._client, self._classname, self._nodeid,
+ self._prop, self._formname, ret)
+
+ def field(self, size=30, default=None, format=_marker):
+ '''Render a form edit field for the property
+
+ If not editable, just display the value via plain().
+
+ The format string is a standard python strftime format string.
+ '''
+ if not self.is_edit_ok():
+ if format is self._marker:
+ return self.plain()
+ else:
+ return self.pretty(format)
+
+ value = self._value
+
+ if value is None:
+ if default is None:
+ raw_value = None
+ else:
+ if isinstance(default, basestring):
+ raw_value = Date(default, translator=self._client)
+ elif isinstance(default, date.Date):
+ raw_value = default
+ elif isinstance(default, DateHTMLProperty):
+ raw_value = default._value
+ else:
+ raise ValueError, self._('default value for '
+ 'DateHTMLProperty must be either DateHTMLProperty '
+ 'or string date representation.')
+ elif isinstance(value, str) or isinstance(value, unicode):
+ # most likely erroneous input to be passed back to user
+ value = cgi.escape(str(value), 1)
+ return self.input(name=self._formname, value=value, size=size)
+ else:
+ raw_value = value
+
+ if raw_value is None:
+ value = ''
+ elif isinstance(raw_value, str) or isinstance(raw_value, unicode):
+ if format is self._marker:
+ value = raw_value
+ else:
+ value = date.Date(raw_value).pretty(format)
+ else:
+ if self._offset is None :
+ offset = self._db.getUserTimezone()
+ else :
+ offset = self._offset
+ value = raw_value.local(offset)
+ if format is not self._marker:
+ value = value.pretty(format)
+
+ value = cgi.escape(str(value), 1)
+ return self.input(name=self._formname, value=value, size=size)
+
+ def reldate(self, pretty=1):
+ ''' Render the interval between the date and now.
+
+ If the "pretty" flag is true, then make the display pretty.
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ if not self._value:
+ return ''
+
+ # figure the interval
+ interval = self._value - date.Date('.', translator=self._client)
+ if pretty:
+ return interval.pretty()
+ return str(interval)
+
+ def pretty(self, format=_marker):
+ ''' Render the date in a pretty format (eg. month names, spaces).
+
+ The format string is a standard python strftime format string.
+ Note that if the day is zero, and appears at the start of the
+ string, then it'll be stripped from the output. This is handy
+ for the situation when a date only specifies a month and a year.
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ if self._offset is None:
+ offset = self._db.getUserTimezone()
+ else:
+ offset = self._offset
+
+ if not self._value:
+ return ''
+ elif format is not self._marker:
+ return self._value.local(offset).pretty(format)
+ else:
+ return self._value.local(offset).pretty()
+
+ def local(self, offset):
+ ''' Return the date/time as a local (timezone offset) date/time.
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ return DateHTMLProperty(self._client, self._classname, self._nodeid,
+ self._prop, self._formname, self._value, offset=offset)
+
+ def popcal(self, width=300, height=200, label= "(cal)",
+ form="itemSynopsis"):
+ """Generate a link to a calendar pop-up window.
+
+ item: HTMLProperty e.g.: context.deadline
+ """
+ if self.isset():
+ date = "&date=%s"%self._value
+ else :
+ date = ""
+ return ('<a class="classhelp" href="javascript:help_window('
+ "'%s?@template=calendar&property=%s&form=%s%s', %d, %d)"
+ '">%s</a>'%(self._classname, self._name, form, date, width,
+ height, label))
+
+class IntervalHTMLProperty(HTMLProperty):
+ def __init__(self, client, classname, nodeid, prop, name, value,
+ anonymous=0):
+ HTMLProperty.__init__(self, client, classname, nodeid, prop,
+ name, value, anonymous)
+ if self._value and not isinstance(self._value, (str, unicode)):
+ self._value.setTranslator(self._client.translator)
+
+ def plain(self):
+ ''' Render a "plain" representation of the property
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ if self._value is None:
+ return ''
+ return str(self._value)
+
+ def pretty(self):
+ ''' Render the interval in a pretty format (eg. "yesterday")
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ return self._value.pretty()
+
+ def field(self, size = 30):
+ ''' Render a form edit field for the property
+
+ If not editable, just display the value via plain().
+ '''
+ if not self.is_edit_ok():
+ return self.plain()
+
+ if self._value is None:
+ value = ''
+ else:
+ value = cgi.escape(str(self._value))
+
+ value = '"'.join(value.split('"'))
+ return self.input(name=self._formname,value=value,size=size)
+
+class LinkHTMLProperty(HTMLProperty):
+ ''' Link HTMLProperty
+ Include the above as well as being able to access the class
+ information. Stringifying the object itself results in the value
+ from the item being displayed. Accessing attributes of this object
+ result in the appropriate entry from the class being queried for the
+ property accessed (so item/assignedto/name would look up the user
+ entry identified by the assignedto property on item, and then the
+ name property of that user)
+ '''
+ def __init__(self, *args, **kw):
+ HTMLProperty.__init__(self, *args, **kw)
+ # if we're representing a form value, then the -1 from the form really
+ # should be a None
+ if str(self._value) == '-1':
+ self._value = None
+
+ def __getattr__(self, attr):
+ ''' return a new HTMLItem '''
+ if not self._value:
+ # handle a special page templates lookup
+ if attr == '__render_with_namespace__':
+ def nothing(*args, **kw):
+ return ''
+ return nothing
+ msg = self._('Attempt to look up %(attr)s on a missing value')
+ return MissingValue(msg%locals())
+ i = HTMLItem(self._client, self._prop.classname, self._value)
+ return getattr(i, attr)
+
+ def plain(self, escape=0):
+ ''' Render a "plain" representation of the property
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ if self._value is None:
+ return ''
+ linkcl = self._db.classes[self._prop.classname]
+ k = linkcl.labelprop(1)
+ value = str(linkcl.get(self._value, k))
+ if escape:
+ value = cgi.escape(value)
+ return value
+
+ def field(self, showid=0, size=None):
+ ''' Render a form edit field for the property
+
+ If not editable, just display the value via plain().
+ '''
+ if not self.is_edit_ok():
+ return self.plain()
+
+ # edit field
+ linkcl = self._db.getclass(self._prop.classname)
+ if self._value is None:
+ value = ''
+ else:
+ k = linkcl.getkey()
+ if k:
+ value = linkcl.get(self._value, k)
+ else:
+ value = self._value
+ value = cgi.escape(str(value))
+ value = '"'.join(value.split('"'))
+ return '<input name="%s" value="%s" size="%s">'%(self._formname,
+ value, size)
+
+ def menu(self, size=None, height=None, showid=0, additional=[], value=None,
+ sort_on=None, **conditions):
+ ''' Render a form select list for this property
+
+ "size" is used to limit the length of the list labels
+ "height" is used to set the <select> tag's "size" attribute
+ "showid" includes the item ids in the list labels
+ "value" specifies which item is pre-selected
+ "additional" lists properties which should be included in the
+ label
+ "sort_on" indicates the property to sort the list on as
+ (direction, property) where direction is '+' or '-'. A
+ single string with the direction prepended may be used.
+ For example: ('-', 'order'), '+name'.
+
+ The remaining keyword arguments are used as conditions for
+ filtering the items in the list - they're passed as the
+ "filterspec" argument to a Class.filter() call.
+
+ If not editable, just display the value via plain().
+ '''
+ if not self.is_edit_ok():
+ return self.plain()
+
+ if value is None:
+ value = self._value
+
+ linkcl = self._db.getclass(self._prop.classname)
+ l = ['<select name="%s">'%self._formname]
+ k = linkcl.labelprop(1)
+ s = ''
+ if value is None:
+ s = 'selected="selected" '
+ l.append(self._('<option %svalue="-1">- no selection -</option>')%s)
+
+ if sort_on is not None:
+ if not isinstance(sort_on, tuple):
+ if sort_on[0] in '+-':
+ sort_on = (sort_on[0], sort_on[1:])
+ else:
+ sort_on = ('+', sort_on)
+ else:
+ sort_on = ('+', find_sort_key(linkcl))
+
+ options = [opt
+ for opt in linkcl.filter(None, conditions, sort_on, (None, None))
+ if self._db.security.hasPermission("View", self._client.userid,
+ linkcl.classname, itemid=opt)]
+
+ # make sure we list the current value if it's retired
+ if value and value not in options:
+ options.insert(0, value)
+
+ for optionid in options:
+ # get the option value, and if it's None use an empty string
+ option = linkcl.get(optionid, k) or ''
+
+ # figure if this option is selected
+ s = ''
+ if value in [optionid, option]:
+ s = 'selected="selected" '
+
+ # figure the label
+ if showid:
+ lab = '%s%s: %s'%(self._prop.classname, optionid, option)
+ elif not option:
+ lab = '%s%s'%(self._prop.classname, optionid)
+ else:
+ lab = option
+
+ # truncate if it's too long
+ if size is not None and len(lab) > size:
+ lab = lab[:size-3] + '...'
+ if additional:
+ m = []
+ for propname in additional:
+ m.append(linkcl.get(optionid, propname))
+ lab = lab + ' (%s)'%', '.join(map(str, m))
+
+ # and generate
+ lab = cgi.escape(lab)
+ l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
+ l.append('</select>')
+ return '\n'.join(l)
+# def checklist(self, ...)
+
+
+
+class MultilinkHTMLProperty(HTMLProperty):
+ ''' Multilink HTMLProperty
+
+ Also be iterable, returning a wrapper object like the Link case for
+ each entry in the multilink.
+ '''
+ def __init__(self, *args, **kwargs):
+ HTMLProperty.__init__(self, *args, **kwargs)
+ if self._value:
+ display_value = lookupIds(self._db, self._prop, self._value,
+ fail_ok=1)
+ sortfun = make_sort_function(self._db, self._prop.classname)
+ # sorting fails if the value contains
+ # items not yet stored in the database
+ # ignore these errors to preserve user input
+ try:
+ display_value.sort(sortfun)
+ except:
+ pass
+ self._value = display_value
+
+ def __len__(self):
+ ''' length of the multilink '''
+ return len(self._value)
+
+ def __getattr__(self, attr):
+ ''' no extended attribute accesses make sense here '''
+ raise AttributeError, attr
+
+ def viewableGenerator(self, values):
+ '''Used to iterate over only the View'able items in a class.'''
+ check = self._db.security.hasPermission
+ userid = self._client.userid
+ classname = self._prop.classname
+ for value in values:
+ if check('View', userid, classname, itemid=value):
+ yield HTMLItem(self._client, classname, value)
+
+ def __iter__(self):
+ ''' iterate and return a new HTMLItem
+ '''
+ return self.viewableGenerator(self._value)
+
+ def reverse(self):
+ ''' return the list in reverse order
+ '''
+ l = self._value[:]
+ l.reverse()
+ return self.viewableGenerator(l)
+
+ def sorted(self, property):
+ ''' Return this multilink sorted by the given property '''
+ value = list(self.__iter__())
+ value.sort(lambda a,b:cmp(a[property], b[property]))
+ return value
+
+ def __contains__(self, value):
+ ''' Support the "in" operator. We have to make sure the passed-in
+ value is a string first, not a HTMLProperty.
+ '''
+ return str(value) in self._value
+
+ def isset(self):
+ '''Is my _value not []?'''
+ return self._value != []
+
+ def plain(self, escape=0):
+ ''' Render a "plain" representation of the property
+ '''
+ if not self.is_view_ok():
+ return self._('[hidden]')
+
+ linkcl = self._db.classes[self._prop.classname]
+ 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)
+ labels.append(label)
+ value = ', '.join(labels)
+ if escape:
+ value = cgi.escape(value)
+ return value
+
+ def field(self, size=30, showid=0):
+ ''' Render a form edit field for the property
+
+ If not editable, just display the value via plain().
+ '''
+ if not self.is_edit_ok():
+ return self.plain()
+
+ linkcl = self._db.getclass(self._prop.classname)
+ value = self._value[:]
+ # map the id to the label property
+ if not linkcl.getkey():
+ showid=1
+ if not showid:
+ k = linkcl.labelprop(1)
+ value = lookupKeys(linkcl, k, value)
+ value = cgi.escape(','.join(value))
+ return self.input(name=self._formname,size=size,value=value)
+
+ def menu(self, size=None, height=None, showid=0, additional=[],
+ value=None, sort_on=None, **conditions):
+ ''' Render a form <select> list for this property.
+
+ "size" is used to limit the length of the list labels
+ "height" is used to set the <select> tag's "size" attribute
+ "showid" includes the item ids in the list labels
+ "additional" lists properties which should be included in the
+ label
+ "value" specifies which item is pre-selected
+ "sort_on" indicates the property to sort the list on as
+ (direction, property) where direction is '+' or '-'. A
+ single string with the direction prepended may be used.
+ For example: ('-', 'order'), '+name'.
+
+ The remaining keyword arguments are used as conditions for
+ filtering the items in the list - they're passed as the
+ "filterspec" argument to a Class.filter() call.
+
+ If not editable, just display the value via plain().
+ '''
+ if not self.is_edit_ok():
+ return self.plain()
+
+ if value is None:
+ value = self._value
+
+ linkcl = self._db.getclass(self._prop.classname)
+
+ if sort_on is not None:
+ if not isinstance(sort_on, tuple):
+ if sort_on[0] in '+-':
+ sort_on = (sort_on[0], sort_on[1:])
+ else:
+ sort_on = ('+', sort_on)
+ else:
+ sort_on = ('+', find_sort_key(linkcl))
+
+ 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)
+
+ for optionid in options:
+ # get the option value, and if it's None use an empty string
+ option = linkcl.get(optionid, k) or ''
+
+ # figure if this option is selected
+ s = ''
+ if optionid in value or option in value:
+ s = 'selected="selected" '
+
+ # figure the label
+ if showid:
+ lab = '%s%s: %s'%(self._prop.classname, optionid, option)
+ else:
+ lab = option
+ # truncate if it's too long
+ if size is not None and len(lab) > size:
+ lab = lab[:size-3] + '...'
+ if additional:
+ m = []
+ for propname in additional:
+ m.append(linkcl.get(optionid, propname))
+ lab = lab + ' (%s)'%', '.join(m)
+
+ # and generate
+ lab = cgi.escape(lab)
+ l.append('<option %svalue="%s">%s</option>'%(s, optionid,
+ lab))
+ l.append('</select>')
+ return '\n'.join(l)
+
+# set the propclasses for HTMLItem
+propclasses = (
+ (hyperdb.String, StringHTMLProperty),
+ (hyperdb.Number, NumberHTMLProperty),
+ (hyperdb.Boolean, BooleanHTMLProperty),
+ (hyperdb.Date, DateHTMLProperty),
+ (hyperdb.Interval, IntervalHTMLProperty),
+ (hyperdb.Password, PasswordHTMLProperty),
+ (hyperdb.Link, LinkHTMLProperty),
+ (hyperdb.Multilink, MultilinkHTMLProperty),
+)
+
+def make_sort_function(db, classname, sort_on=None):
+ '''Make a sort function for a given class
+ '''
+ linkcl = db.getclass(classname)
+ if sort_on is None:
+ sort_on = find_sort_key(linkcl)
+ 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.
+ '''
+ if isinstance(value, type([])):
+ return [value.value for value in value]
+ else:
+ value = value.value.strip()
+ if not value:
+ return []
+ return value.split(',')
+
+class HTMLRequest(HTMLInputMixin):
+ '''The *request*, holding the CGI form and environment.
+
+ - "form" the CGI form as a cgi.FieldStorage
+ - "env" the CGI environment variables
+ - "base" the base URL for this instance
+ - "user" a HTMLItem instance for this user
+ - "classname" the current classname (possibly None)
+ - "template" the current template (suffix, also possibly None)
+
+ Index args:
+
+ - "columns" dictionary of the columns to display in an index page
+ - "show" a convenience access to columns - request/show/colname will
+ be true if the columns should be displayed, false otherwise
+ - "sort" index sort column (direction, column name)
+ - "group" index grouping property (direction, column name)
+ - "filter" properties to filter the index on
+ - "filterspec" values to filter the index on
+ - "search_text" text to perform a full-text search on for an index
+ '''
+ def __repr__(self):
+ return '<HTMLRequest %r>'%self.form
+
+ def __init__(self, client):
+ # _client is needed by HTMLInputMixin
+ self._client = self.client = client
+
+ # easier access vars
+ self.form = client.form
+ self.env = client.env
+ self.base = client.base
+ self.user = HTMLItem(client, 'user', client.userid)
+
+ # store the current class name and action
+ self.classname = client.classname
+ self.nodeid = client.nodeid
+ self.template = client.template
+
+ # the special char to use for special vars
+ self.special_char = '@'
+
+ HTMLInputMixin.__init__(self)
+
+ self._post_init()
+
+ def current_url(self):
+ url = self.base
+ if self.classname:
+ url += self.classname
+ if self.nodeid:
+ url += self.nodeid
+ args = {}
+ if self.template:
+ args['@template'] = self.template
+ return self.indexargs_url(url, args)
+
+ def _post_init(self):
+ ''' Set attributes based on self.form
+ '''
+ # extract the index display information from the form
+ self.columns = []
+ for name in ':columns @columns'.split():
+ if self.form.has_key(name):
+ self.special_char = name[0]
+ self.columns = handleListCGIValue(self.form[name])
+ break
+ self.show = support.TruthDict(self.columns)
+
+ # sorting
+ self.sort = (None, None)
+ for name in ':sort @sort'.split():
+ if self.form.has_key(name):
+ self.special_char = name[0]
+ sort = self.form[name].value
+ if sort.startswith('-'):
+ self.sort = ('-', sort[1:])
+ else:
+ self.sort = ('+', sort)
+ if self.form.has_key(self.special_char+'sortdir'):
+ self.sort = ('-', self.sort[1])
+
+ # grouping
+ self.group = (None, None)
+ for name in ':group @group'.split():
+ if self.form.has_key(name):
+ self.special_char = name[0]
+ group = self.form[name].value
+ if group.startswith('-'):
+ self.group = ('-', group[1:])
+ else:
+ self.group = ('+', group)
+ if self.form.has_key(self.special_char+'groupdir'):
+ self.group = ('-', self.group[1])
+
+ # filtering
+ self.filter = []
+ for name in ':filter @filter'.split():
+ if self.form.has_key(name):
+ self.special_char = name[0]
+ self.filter = handleListCGIValue(self.form[name])
+
+ self.filterspec = {}
+ db = self.client.db
+ if self.classname is not None:
+ props = db.getclass(self.classname).getprops()
+ for name in self.filter:
+ if not self.form.has_key(name):
+ continue
+ prop = props[name]
+ fv = self.form[name]
+ if (isinstance(prop, hyperdb.Link) or
+ isinstance(prop, hyperdb.Multilink)):
+ self.filterspec[name] = lookupIds(db, prop,
+ handleListCGIValue(fv))
+ else:
+ if isinstance(fv, type([])):
+ self.filterspec[name] = [v.value for v in fv]
+ elif name == 'id':
+ # special case "id" property
+ self.filterspec[name] = handleListCGIValue(fv)
+ else:
+ self.filterspec[name] = fv.value
+
+ # full-text search argument
+ self.search_text = None
+ for name in ':search_text @search_text'.split():
+ if self.form.has_key(name):
+ self.special_char = name[0]
+ self.search_text = self.form[name].value
+
+ # pagination - size and start index
+ # figure batch args
+ self.pagesize = 50
+ 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.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)
+
+ # dispname
+ self.dispname = None
+ if self.form.has_key('@dispname'):
+ self.dispname = self.form['@dispname'].value
+
+ def updateFromURL(self, url):
+ ''' Parse the URL for query args, and update my attributes using the
+ values.
+ '''
+ env = {'QUERY_STRING': url}
+ self.form = cgi.FieldStorage(environ=env)
+
+ self._post_init()
+
+ def update(self, kwargs):
+ ''' Update my attributes using the keyword args
+ '''
+ self.__dict__.update(kwargs)
+ if kwargs.has_key('columns'):
+ self.show = support.TruthDict(self.columns)
+
+ def description(self):
+ ''' Return a description of the request - handle for the page title.
+ '''
+ s = [self.client.db.config.TRACKER_NAME]
+ if self.classname:
+ if self.client.nodeid:
+ s.append('- %s%s'%(self.classname, self.client.nodeid))
+ else:
+ if self.template == 'item':
+ s.append('- new %s'%self.classname)
+ elif self.template == 'index':
+ s.append('- %s index'%self.classname)
+ else:
+ s.append('- %s %s'%(self.classname, self.template))
+ else:
+ s.append('- home')
+ return ' '.join(s)
+
+ def __str__(self):
+ d = {}
+ d.update(self.__dict__)
+ f = ''
+ for k in self.form.keys():
+ f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
+ d['form'] = f
+ e = ''
+ for k,v in self.env.items():
+ e += '\n %r=%r'%(k, v)
+ d['env'] = e
+ return '''
+form: %(form)s
+base: %(base)r
+classname: %(classname)r
+template: %(template)r
+columns: %(columns)r
+sort: %(sort)r
+group: %(group)r
+filter: %(filter)r
+search_text: %(search_text)r
+pagesize: %(pagesize)r
+startwith: %(startwith)r
+env: %(env)s
+'''%d
+
+ def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
+ filterspec=1, search_text=1):
+ ''' return the current index args as form elements '''
+ l = []
+ sc = self.special_char
+ s = self.input(type="hidden",name="%s",value="%s")
+ if columns and self.columns:
+ l.append(s%(sc+'columns', ','.join(self.columns)))
+ if sort and self.sort[1] is not None:
+ if self.sort[0] == '-':
+ val = '-'+self.sort[1]
+ else:
+ val = self.sort[1]
+ l.append(s%(sc+'sort', val))
+ if group and self.group[1] is not None:
+ if self.group[0] == '-':
+ val = '-'+self.group[1]
+ else:
+ val = self.group[1]
+ l.append(s%(sc+'group', val))
+ if filter and self.filter:
+ l.append(s%(sc+'filter', ','.join(self.filter)))
+ if self.classname and filterspec:
+ props = self.client.db.getclass(self.classname).getprops()
+ for k,v in self.filterspec.items():
+ if type(v) == type([]):
+ if isinstance(props[k], hyperdb.String):
+ l.append(s%(k, ' '.join(v)))
+ else:
+ l.append(s%(k, ','.join(v)))
+ else:
+ l.append(s%(k, v))
+ if search_text and self.search_text:
+ l.append(s%(sc+'search_text', self.search_text))
+ l.append(s%(sc+'pagesize', self.pagesize))
+ l.append(s%(sc+'startwith', self.startwith))
+ return '\n'.join(l)
+
+ def indexargs_url(self, url, args):
+ ''' Embed the current index args in a URL
+ '''
+ q = urllib.quote
+ sc = self.special_char
+ l = ['%s=%s'%(k,v) for k,v in args.items()]
+
+ # pull out the special values (prefixed by @ or :)
+ specials = {}
+ for key in args.keys():
+ if key[0] in '@:':
+ specials[key[1:]] = args[key]
+
+ # ok, now handle the specials we received in the request
+ if self.columns and not specials.has_key('columns'):
+ l.append(sc+'columns=%s'%(','.join(self.columns)))
+ if self.sort[1] is not None and not specials.has_key('sort'):
+ if self.sort[0] == '-':
+ val = '-'+self.sort[1]
+ else:
+ val = self.sort[1]
+ l.append(sc+'sort=%s'%val)
+ if self.group[1] is not None and not specials.has_key('group'):
+ if self.group[0] == '-':
+ val = '-'+self.group[1]
+ else:
+ val = self.group[1]
+ l.append(sc+'group=%s'%val)
+ if self.filter and not specials.has_key('filter'):
+ l.append(sc+'filter=%s'%(','.join(self.filter)))
+ if self.search_text and not specials.has_key('search_text'):
+ l.append(sc+'search_text=%s'%q(self.search_text))
+ if not specials.has_key('pagesize'):
+ l.append(sc+'pagesize=%s'%self.pagesize)
+ if not specials.has_key('startwith'):
+ l.append(sc+'startwith=%s'%self.startwith)
+
+ # finally, the remainder of the filter args in the request
+ if self.classname and self.filterspec:
+ props = self.client.db.getclass(self.classname).getprops()
+ for k,v in self.filterspec.items():
+ if not args.has_key(k):
+ if type(v) == type([]):
+ if isinstance(props[k], hyperdb.String):
+ l.append('%s=%s'%(k, '%20'.join([q(i) for i in v])))
+ else:
+ l.append('%s=%s'%(k, ','.join([q(i) for i in v])))
+ else:
+ l.append('%s=%s'%(k, q(v)))
+ return '%s?%s'%(url, '&'.join(l))
+ indexargs_href = indexargs_url
+
+ def base_javascript(self):
+ return '''
+<script type="text/javascript">
+submitted = false;
+function submit_once() {
+ if (submitted) {
+ alert("Your request is being processed.\\nPlease be patient.");
+ event.returnValue = 0; // work-around for IE
+ return 0;
+ }
+ submitted = true;
+ return 1;
+}
+
+function help_window(helpurl, width, height) {
+ HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
+}
+</script>
+'''%self.base
+
+ def batch(self):
+ ''' Return a batch object for results from the "current search"
+ '''
+ filterspec = self.filterspec
+ sort = self.sort
+ group = self.group
+
+ # get the list of ids we're batching over
+ klass = self.client.db.getclass(self.classname)
+ if self.search_text:
+ matches = self.client.db.indexer.search(
+ [w.upper().encode("utf-8", "replace") for w in re.findall(
+ r'(?u)\b\w{2,25}\b',
+ unicode(self.search_text, "utf-8", "replace")
+ )], klass)
+ else:
+ matches = None
+
+ # filter for visibility
+ check = self._client.db.security.hasPermission
+ userid = self._client.userid
+ l = [id for id in klass.filter(matches, filterspec, sort, group)
+ if check('View', userid, self.classname, itemid=id)]
+
+ # return the batch object, using IDs only
+ return Batch(self.client, l, self.pagesize, self.startwith,
+ classname=self.classname)
+
+# extend the standard ZTUtils Batch object to remove dependency on
+# Acquisition and add a couple of useful methods
+class Batch(ZTUtils.Batch):
+ ''' Use me to turn a list of items, or item ids of a given class, into a
+ series of batches.
+
+ ========= ========================================================
+ Parameter Usage
+ ========= ========================================================
+ sequence a list of HTMLItems or item ids
+ classname if sequence is a list of ids, this is the class of item
+ size how big to make the sequence.
+ start where to start (0-indexed) in the sequence.
+ end where to end (0-indexed) in the sequence.
+ orphan if the next batch would contain less items than this
+ value, then it is combined with this batch
+ overlap the number of items shared between adjacent batches
+ ========= ========================================================
+
+ Attributes: Note that the "start" attribute, unlike the
+ argument, is a 1-based index (I know, lame). "first" is the
+ 0-based index. "length" is the actual number of elements in
+ the batch.
+
+ "sequence_length" is the length of the original, unbatched, sequence.
+ '''
+ def __init__(self, client, sequence, size, start, end=0, orphan=0,
+ overlap=0, classname=None):
+ self.client = client
+ self.last_index = self.last_item = None
+ self.current_item = None
+ self.classname = classname
+ self.sequence_length = len(sequence)
+ ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
+ overlap)
+
+ # overwrite so we can late-instantiate the HTMLItem instance
+ def __getitem__(self, index):
+ if index < 0:
+ if index + self.end < self.first: raise IndexError, index
+ return self._sequence[index + self.end]
+
+ if index >= self.length:
+ raise IndexError, index
+
+ # move the last_item along - but only if the fetched index changes
+ # (for some reason, index 0 is fetched twice)
+ if index != self.last_index:
+ self.last_item = self.current_item
+ self.last_index = index
+
+ item = self._sequence[index + self.first]
+ if self.classname:
+ # map the item ids to instances
+ item = HTMLItem(self.client, self.classname, item)
+ self.current_item = item
+ return item
+
+ def propchanged(self, property):
+ ''' Detect if the property marked as being the group property
+ changed in the last iteration fetch
+ '''
+ # we poke directly at the _value here since MissingValue can screw
+ # us up and cause Nones to compare strangely
+ if (self.last_item is None or
+ self.last_item[property]._value !=
+ self.current_item[property]._value):
+ return 1
+ return 0
+
+ # override these 'cos we don't have access to acquisition
+ def previous(self):
+ if self.start == 1:
+ return None
+ return Batch(self.client, self._sequence, self._size,
+ self.first - self._size + self.overlap, 0, self.orphan,
+ self.overlap)
+
+ def next(self):
+ try:
+ self._sequence[self.end]
+ except IndexError:
+ return None
+ return Batch(self.client, self._sequence, self._size,
+ self.end - self.overlap, 0, self.orphan, self.overlap)
+
+class TemplatingUtils:
+ ''' Utilities for templating
+ '''
+ def __init__(self, client):
+ self.client = client
+ def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
+ return Batch(self.client, sequence, size, start, end, orphan,
+ overlap)
+
+ def url_quote(self, url):
+ '''URL-quote the supplied text.'''
+ return urllib.quote(url)
+
+ def html_quote(self, html):
+ '''HTML-quote the supplied text.'''
+ return cgi.escape(url)
+
+ def __getattr__(self, name):
+ '''Try the tracker's templating_utils.'''
+ if not hasattr(self.client.instance, 'templating_utils'):
+ # backwards-compatibility
+ raise AttributeError, name
+ if not self.client.instance.templating_utils.has_key(name):
+ raise AttributeError, name
+ return self.client.instance.templating_utils[name]
+
+ def html_calendar(self, request):
+ """Generate a HTML calendar.
+
+ `request` the roundup.request object
+ - @template : name of the template
+ - form : name of the form to store back the date
+ - property : name of the property of the form to store
+ back the date
+ - date : current date
+ - display : when browsing, specifies year and month
+
+ html will simply be a table.
+ """
+ date_str = request.form.getfirst("date", ".")
+ display = request.form.getfirst("display", date_str)
+ template = request.form.getfirst("@template", "calendar")
+ form = request.form.getfirst("form")
+ property = request.form.getfirst("property")
+ curr_date = date.Date(date_str) # to highlight
+ display = date.Date(display) # to show
+ day = display.day
+
+ # for navigation
+ date_prev_month = display + date.Interval("-1m")
+ date_next_month = display + date.Interval("+1m")
+ date_prev_year = display + date.Interval("-1y")
+ date_next_year = display + date.Interval("+1y")
+
+ res = []
+
+ base_link = "%s?@template=%s&property=%s&form=%s&date=%s" % \
+ (request.classname, template, property, form, curr_date)
+
+ # navigation
+ # month
+ res.append('<table class="calendar"><tr><td>')
+ res.append(' <table width="100%" class="calendar_nav"><tr>')
+ link = "&display=%s"%date_prev_month
+ res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
+ date_prev_month))
+ res.append(' <td>%s</td>'%calendar.month_name[display.month])
+ res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
+ date_next_month))
+ # spacer
+ res.append(' <td width="100%"></td>')
+ # year
+ res.append(' <td><a href="%s&display=%s"><</a></td>'%(base_link,
+ date_prev_year))
+ res.append(' <td>%s</td>'%display.year)
+ res.append(' <td><a href="%s&display=%s">></a></td>'%(base_link,
+ date_next_year))
+ res.append(' </tr></table>')
+ res.append(' </td></tr>')
+
+ # the calendar
+ res.append(' <tr><td><table class="calendar_display">')
+ res.append(' <tr class="weekdays">')
+ for day in calendar.weekheader(3).split():
+ res.append(' <td>%s</td>'%day)
+ res.append(' </tr>')
+ for week in calendar.monthcalendar(display.year, display.month):
+ res.append(' <tr>')
+ for day in week:
+ link = "javascript:form[field].value = '%d-%02d-%02d'; " \
+ "window.close ();"%(display.year, display.month, day)
+ if (day == curr_date.day and display.month == curr_date.month
+ and display.year == curr_date.year):
+ # highlight
+ style = "today"
+ else :
+ style = ""
+ if day:
+ res.append(' <td class="%s"><a href="%s">%s</a></td>'%(
+ style, link, day))
+ else :
+ res.append(' <td></td>')
+ res.append(' </tr>')
+ res.append('</table></td></tr></table>')
+ return "\n".join(res)
+
+class MissingValue:
+ def __init__(self, description, **kwargs):
+ self.__description = description
+ for key, value in kwargs.items():
+ self.__dict__[key] = value
+
+ def __call__(self, *args, **kwargs): return MissingValue(self.__description)
+ def __getattr__(self, name):
+ # This allows assignments which assume all intermediate steps are Null
+ # objects if they don't exist yet.
+ #
+ # For example (with just 'client' defined):
+ #
+ # client.db.config.TRACKER_WEB = 'BASE/'
+ self.__dict__[name] = MissingValue(self.__description)
+ return getattr(self, name)
+
+ def __getitem__(self, key): return self
+ def __nonzero__(self): return 0
+ def __str__(self): return '[%s]'%self.__description
+ def __repr__(self): return '<MissingValue 0x%x "%s">'%(id(self),
+ self.__description)
+ def gettext(self, str): return str
+ _ = gettext
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/cgi/zLOG.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/cgi/zLOG.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,2 @@
+def LOG(*args, **kw):
+ pass
Added: tracker/vendor/roundup/current/roundup/configuration.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/configuration.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,1236 @@
+# Roundup Issue Tracker configuration support
+#
+# $Id: configuration.py,v 1.34 2006/04/27 04:59:37 richard Exp $
+#
+__docformat__ = "restructuredtext"
+
+import getopt
+import imp
+import os
+import time
+import ConfigParser
+import logging, logging.config
+import sys
+
+# XXX i don't think this module needs string translation, does it?
+
+### Exceptions
+
+class ConfigurationError(Exception):
+
+ # without this, pychecker complains about missing class attribute...
+ args = ()
+
+class NoConfigError(ConfigurationError):
+
+ """Raised when configuration loading fails
+
+ Constructor parameters: path to the directory that was used as HOME
+
+ """
+
+ def __str__(self):
+ return "No valid configuration files found in directory %s" \
+ % self.args[0]
+
+class InvalidOptionError(ConfigurationError, KeyError, AttributeError):
+
+ """Attempted access to non-existing configuration option
+
+ Configuration options may be accessed as configuration object
+ attributes or items. So this exception instances also are
+ instances of KeyError (invalid item access) and AttrributeError
+ (invalid attribute access).
+
+ Constructor parameter: option name
+
+ """
+
+ def __str__(self):
+ return "Unsupported configuration option: %s" % self.args[0]
+
+class OptionValueError(ConfigurationError, ValueError):
+
+ """Raised upon attempt to assign an invalid value to config option
+
+ Constructor parameters: Option instance, offending value
+ and optional info string.
+
+ """
+
+ def __str__(self):
+ _args = self.args
+ _rv = "Invalid value for %(option)s: %(value)r" % {
+ "option": _args[0].name, "value": _args[1]}
+ if len(_args) > 2:
+ _rv += "\n".join(("",) + _args[2:])
+ return _rv
+
+class OptionUnsetError(ConfigurationError):
+
+ """Raised when no Option value is available - neither set, nor default
+
+ Constructor parameters: Option instance.
+
+ """
+
+ def __str__(self):
+ return "%s is not set and has no default" % self.args[0].name
+
+class UnsetDefaultValue:
+
+ """Special object meaning that default value for Option is not specified"""
+
+ def __str__(self):
+ return "NO DEFAULT"
+
+NODEFAULT = UnsetDefaultValue()
+
+### Option classes
+
+class Option:
+
+ """Single configuration option.
+
+ Options have following attributes:
+
+ config
+ reference to the containing Config object
+ section
+ name of the section in the tracker .ini file
+ setting
+ option name in the tracker .ini file
+ default
+ default option value
+ description
+ option description. Makes a comment in the tracker .ini file
+ name
+ "canonical name" of the configuration option.
+ For items in the 'main' section this is uppercased
+ 'setting' name. For other sections, the name is
+ composed of the section name and the setting name,
+ joined with underscore.
+ aliases
+ list of "also known as" names. Used to access the settings
+ by old names used in previous Roundup versions.
+ "Canonical name" is also included.
+
+ The name and aliases are forced to be uppercase.
+ The setting name is forced to lowercase.
+
+ """
+
+ class_description = None
+
+ def __init__(self, config, section, setting,
+ default=NODEFAULT, description=None, aliases=None
+ ):
+ self.config = config
+ self.section = section
+ self.setting = setting.lower()
+ self.default = default
+ self.description = description
+ self.name = setting.upper()
+ if section != "main":
+ self.name = "_".join((section.upper(), self.name))
+ if aliases:
+ self.aliases = [alias.upper() for alias in list(aliases)]
+ else:
+ self.aliases = []
+ self.aliases.insert(0, self.name)
+ # convert default to internal representation
+ if default is NODEFAULT:
+ _value = default
+ else:
+ _value = self.str2value(default)
+ # value is private. use get() and set() to access
+ self._value = self._default_value = _value
+
+ def str2value(self, value):
+ """Return 'value' argument converted to internal representation"""
+ return value
+
+ def _value2str(self, value):
+ """Return 'value' argument converted to external representation
+
+ This is actual conversion method called only when value
+ is not NODEFAULT. Heirs with different conversion rules
+ override this method, not the public .value2str().
+
+ """
+ return str(value)
+
+ def value2str(self, value=NODEFAULT, current=0):
+ """Return 'value' argument converted to external representation
+
+ If 'current' is True, use current option value.
+
+ """
+ if current:
+ value = self._value
+ if value is NODEFAULT:
+ return str(value)
+ else:
+ return self._value2str(value)
+
+ def get(self):
+ """Return current option value"""
+ if self._value is NODEFAULT:
+ raise OptionUnsetError(self)
+ return self._value
+
+ def set(self, value):
+ """Update the value"""
+ self._value = self.str2value(value)
+
+ def reset(self):
+ """Reset the value to default"""
+ self._value = self._default_value
+
+ def isdefault(self):
+ """Return True if current value is the default one"""
+ return self._value == self._default_value
+
+ def isset(self):
+ """Return True if the value is avaliable (either set or default)"""
+ return self._value != NODEFAULT
+
+ def __str__(self):
+ return self.value2str(self._value)
+
+ def __repr__(self):
+ if self.isdefault():
+ _format = "<%(class)s %(name)s (default): %(value)s>"
+ else:
+ _format = "<%(class)s %(name)s (default: %(default)s): %(value)s>"
+ return _format % {
+ "class": self.__class__.__name__,
+ "name": self.name,
+ "default": self.value2str(self._default_value),
+ "value": self.value2str(self._value),
+ }
+
+ def format(self):
+ """Return .ini file fragment for this option"""
+ _desc_lines = []
+ for _description in (self.description, self.class_description):
+ if _description:
+ _desc_lines.extend(_description.split("\n"))
+ # comment out the setting line if there is no value
+ if self.isset():
+ _is_set = ""
+ else:
+ _is_set = "#"
+ _rv = "# %(description)s\n# Default: %(default)s\n" \
+ "%(is_set)s%(name)s = %(value)s\n" % {
+ "description": "\n# ".join(_desc_lines),
+ "default": self.value2str(self._default_value),
+ "name": self.setting,
+ "value": self.value2str(self._value),
+ "is_set": _is_set
+ }
+ return _rv
+
+ def load_ini(self, config):
+ """Load value from ConfigParser object"""
+ if config.has_option(self.section, self.setting):
+ self.set(config.get(self.section, self.setting))
+
+ def load_pyconfig(self, config):
+ """Load value from old-style config (python module)"""
+ for _name in self.aliases:
+ if hasattr(config, _name):
+ self.set(getattr(config, _name))
+ break
+
+class BooleanOption(Option):
+
+ """Boolean option: yes or no"""
+
+ class_description = "Allowed values: yes, no"
+
+ def _value2str(self, value):
+ if value:
+ return "yes"
+ else:
+ return "no"
+
+ def str2value(self, value):
+ if type(value) == type(""):
+ _val = value.lower()
+ if _val in ("yes", "true", "on", "1"):
+ _val = 1
+ elif _val in ("no", "false", "off", "0"):
+ _val = 0
+ else:
+ raise OptionValueError(self, value, self.class_description)
+ else:
+ _val = value and 1 or 0
+ return _val
+
+class WordListOption(Option):
+
+ """List of strings"""
+
+ class_description = "Allowed values: comma-separated list of words"
+
+ def _value2str(self, value):
+ return ','.join(value)
+
+ def str2value(self, value):
+ return value.split(',')
+
+class RunDetectorOption(Option):
+
+ """When a detector is run: always, never or for new items only"""
+
+ class_description = "Allowed values: yes, no, new"
+
+ def str2value(self, value):
+ _val = value.lower()
+ if _val in ("yes", "no", "new"):
+ return _val
+ else:
+ raise OptionValueError(self, value, self.class_description)
+
+class MailAddressOption(Option):
+
+ """Email address
+
+ Email addresses may be either fully qualified or local.
+ In the latter case MAIL_DOMAIN is automatically added.
+
+ """
+
+ def get(self):
+ _val = Option.get(self)
+ if "@" not in _val:
+ _val = "@".join((_val, self.config["MAIL_DOMAIN"]))
+ return _val
+
+class FilePathOption(Option):
+
+ """File or directory path name
+
+ Paths may be either absolute or relative to the HOME.
+
+ """
+
+ class_description = "The path may be either absolute or relative\n" \
+ "to the directory containig this config file."
+
+ def get(self):
+ _val = Option.get(self)
+ if _val and not os.path.isabs(_val):
+ _val = os.path.join(self.config["HOME"], _val)
+ return _val
+
+class FloatNumberOption(Option):
+
+ """Floating point numbers"""
+
+ def str2value(self, value):
+ try:
+ return float(value)
+ except ValueError:
+ raise OptionValueError(self, value,
+ "Floating point number required")
+
+ def _value2str(self, value):
+ _val = str(value)
+ # strip fraction part from integer numbers
+ if _val.endswith(".0"):
+ _val = _val[:-2]
+ return _val
+
+class IntegerNumberOption(Option):
+
+ """Integer numbers"""
+
+ def str2value(self, value):
+ try:
+ return int(value)
+ except ValueError:
+ raise OptionValueError(self, value, "Integer number required")
+
+class OctalNumberOption(Option):
+
+ """Octal Integer numbers"""
+
+ def str2value(self, value):
+ try:
+ return int(value, 8)
+ except ValueError:
+ raise OptionValueError(self, value, "Octal Integer number required")
+
+ def _value2str(self, value):
+ return oct(value)
+
+class NullableOption(Option):
+
+ """Option that is set to None if it's string value is one of NULL strings
+
+ Default nullable strings list contains empty string only.
+ There is constructor parameter allowing to specify different nullables.
+
+ Conversion to external representation returns the first of the NULL
+ strings list when the value is None.
+
+ """
+
+ NULL_STRINGS = ("",)
+
+ def __init__(self, config, section, setting,
+ default=NODEFAULT, description=None, aliases=None,
+ null_strings=NULL_STRINGS
+ ):
+ self.null_strings = list(null_strings)
+ Option.__init__(self, config, section, setting, default,
+ description, aliases)
+
+ def str2value(self, value):
+ if value in self.null_strings:
+ return None
+ else:
+ return value
+
+ def _value2str(self, value):
+ if value is None:
+ return self.null_strings[0]
+ else:
+ return value
+
+class NullableFilePathOption(NullableOption, FilePathOption):
+
+ # .get() and class_description are from FilePathOption,
+ get = FilePathOption.get
+ class_description = FilePathOption.class_description
+ # everything else taken from NullableOption (inheritance order)
+
+### Main configuration layout.
+# Config is described as a sequence of sections,
+# where each section name is followed by a sequence
+# of Option definitions. Each Option definition
+# is a sequence containing class name and constructor
+# parameters, starting from the setting name:
+# setting, default, [description, [aliases]]
+# Note: aliases should only exist in historical options for backwards
+# compatibility - new options should *not* have aliases!
+SETTINGS = (
+ ("main", (
+ (FilePathOption, "database", "db", "Database directory path."),
+ (FilePathOption, "templates", "html",
+ "Path to the HTML templates directory."),
+ (NullableFilePathOption, "static_files", "",
+ "Path to directory holding additional static files\n"
+ "available via Web UI. This directory may contain\n"
+ "sitewide images, CSS stylesheets etc. and is searched\n"
+ "for these files prior to the TEMPLATES directory\n"
+ "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."),
+ (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."),
+ (Option, "email_from_tag", "",
+ "Additional text to include in the \"name\" part\n"
+ "of the From: address used in nosy messages.\n"
+ "If the sending user is \"Foo Bar\", the From: line\n"
+ "is usually: \"Foo Bar\" <issue_tracker at tracker.example>\n"
+ "the EMAIL_FROM_TAG goes inside the \"Foo Bar\" quotes like so:\n"
+ "\"Foo Bar EMAIL_FROM_TAG\" <issue_tracker at tracker.example>"),
+ (Option, "new_web_user_roles", "User",
+ "Roles that a user gets when they register"
+ " with Web User Interface.\n"
+ "This is a comma-separated string of role names"
+ " (e.g. 'Admin,User')."),
+ (Option, "new_email_user_roles", "User",
+ "Roles that a user gets when they register"
+ " with Email Gateway.\n"
+ "This is a comma-separated string of role names"
+ " (e.g. 'Admin,User')."),
+ (Option, "error_messages_to", "user",
+ # XXX This description needs better wording,
+ # with explicit allowed values list.
+ "Send error message emails to the dispatcher, user, or both?\n"
+ "The dispatcher is configured using the DISPATCHER_EMAIL"
+ " setting."),
+ (Option, "html_version", "html4",
+ "HTML version to generate. The templates are html4 by default.\n"
+ "If you wish to make them xhtml, then you'll need to change this\n"
+ "var to 'xhtml' too so all auto-generated HTML is compliant.\n"
+ "Allowed values: html4, xhtml"),
+ # It seems to me that all timezone offsets in the modern world
+ # are integral hours. However, there were fractional hour offsets
+ # in the past. Use float number for sure.
+ (FloatNumberOption, "timezone", "0",
+ "Numeric timezone offset used when users do not choose their own\n"
+ "in their settings.",
+ ["DEFAULT_TIMEZONE"]),
+ (BooleanOption, "instant_registration", "no",
+ "Register new users instantly, or require confirmation via\n"
+ "email?"),
+ (BooleanOption, "email_registration_confirmation", "yes",
+ "Offer registration confirmation by email or only through the web?"),
+ (WordListOption, "indexer_stopwords", "",
+ "Additional stop-words for the full-text indexer specific to\n"
+ "your tracker. See the indexer source for the default list of\n"
+ "stop-words (eg. A,AND,ARE,AS,AT,BE,BUT,BY, ...)"),
+ (OctalNumberOption, "umask", "02",
+ "Defines the file creation mode mask."),
+ )),
+ ("tracker", (
+ (Option, "name", "Roundup issue tracker",
+ "A descriptive name for your roundup instance."),
+ (Option, "web", NODEFAULT,
+ "The web address that the tracker is viewable at.\n"
+ "This will be included in information"
+ " sent to users of the tracker.\n"
+ "The URL MUST include the cgi-bin part or anything else\n"
+ "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."),
+ (NullableOption, "language", "",
+ "Default locale name for this tracker.\n"
+ "If this option is not set, the language is determined\n"
+ "by OS environment variable LANGUAGE, LC_ALL, LC_MESSAGES,\n"
+ "or LANG, in that order of preference."),
+ )),
+ ("web", (
+ (BooleanOption, 'http_auth', "yes",
+ "Whether to use HTTP Basic Authentication, if present.\n"
+ "Roundup will use either the REMOTE_USER or HTTP_AUTHORIZATION\n"
+ "variables supplied by your web server (in that order).\n"
+ "Set this option to 'no' if you do not wish to use HTTP Basic\n"
+ "Authentication in your web interface."),
+ (BooleanOption, 'use_browser_language', "yes",
+ "Whether to use HTTP Accept-Language, if present.\n"
+ "Browsers send a language-region preference list.\n"
+ "It's usually set in the client's browser or in their\n"
+ "Operating System.\n"
+ "Set this option to 'no' if you want to ignore it."),
+ (BooleanOption, "debug", "no",
+ "Setting this option makes Roundup display error tracebacks\n"
+ "in the user's browser rather than emailing them to the\n"
+ "tracker admin."),
+ )),
+ ("rdbms", (
+ (Option, 'name', 'roundup',
+ "Name of the database to use.",
+ ['MYSQL_DBNAME']),
+ (NullableOption, 'host', 'localhost',
+ "Database server host.",
+ ['MYSQL_DBHOST']),
+ (NullableOption, 'port', '',
+ "TCP port number of the database server.\n"
+ "Postgresql usually resides on port 5432 (if any),\n"
+ "for MySQL default port number is 3306.\n"
+ "Leave this option empty to use backend default"),
+ (NullableOption, 'user', 'roundup',
+ "Database user name that Roundup should use.",
+ ['MYSQL_DBUSER']),
+ (NullableOption, 'password', 'roundup',
+ "Database user password.",
+ ['MYSQL_DBPASSWORD']),
+ (NullableOption, 'read_default_file', '~/.my.cnf',
+ "Name of the MySQL defaults file.\n"
+ "Only used in MySQL connections."),
+ (NullableOption, 'read_default_group', 'roundup',
+ "Name of the group to use in the MySQL defaults file (.my.cnf).\n"
+ "Only used in MySQL connections."),
+ ), "Settings in this section are used"
+ " by Postgresql and MySQL backends only"
+ ),
+ ("logging", (
+ (FilePathOption, "config", "",
+ "Path to configuration file for standard Python logging module.\n"
+ "If this option is set, logging configuration is loaded\n"
+ "from specified file; options 'filename' and 'level'\n"
+ "in this section are ignored."),
+ (FilePathOption, "filename", "",
+ "Log file name for minimal logging facility built into Roundup.\n"
+ "If no file name specified, log messages are written on stderr.\n"
+ "If above 'config' option is set, this option has no effect."),
+ (Option, "level", "ERROR",
+ "Minimal severity level of messages written to log file.\n"
+ "If above 'config' option is set, this option has no effect.\n"
+ "Allowed values: DEBUG, INFO, WARNING, ERROR"),
+ )),
+ ("mail", (
+ (Option, "domain", NODEFAULT, "Domain name used for email addresses."),
+ (Option, "host", NODEFAULT,
+ "SMTP mail host that roundup will use to send mail",
+ ["MAILHOST"],),
+ (Option, "username", "", "SMTP login name.\n"
+ "Set this if your mail host requires authenticated access.\n"
+ "If username is not empty, password (below) MUST be set!"),
+ (Option, "password", NODEFAULT, "SMTP login password.\n"
+ "Set this if your mail host requires authenticated access."),
+ (BooleanOption, "tls", "no",
+ "If your SMTP mail host provides or requires TLS\n"
+ "(Transport Layer Security) then set this option to 'yes'."),
+ (NullableFilePathOption, "tls_keyfile", "",
+ "If TLS is used, you may set this option to the name\n"
+ "of a PEM formatted file that contains your private key."),
+ (NullableFilePathOption, "tls_certfile", "",
+ "If TLS is used, you may set this option to the name\n"
+ "of a PEM formatted certificate chain file."),
+ (Option, "charset", "utf-8",
+ "Character set to encode email headers with.\n"
+ "We use utf-8 by default, as it's the most flexible.\n"
+ "Some mail readers (eg. Eudora) can't cope with that,\n"
+ "so you might need to specify a more limited character set\n"
+ "(eg. iso-8859-1).",
+ ["EMAIL_CHARSET"]),
+ (FilePathOption, "debug", "",
+ "Setting this option makes Roundup to write all outgoing email\n"
+ "messages to this file *instead* of sending them.\n"
+ "This option has the same effect as environment variable"
+ " SENDMAILDEBUG.\nEnvironment variable takes precedence."),
+ ), "Outgoing email options.\nUsed for nozy messages and approval requests"),
+ ("mailgw", (
+ (BooleanOption, "keep_quoted_text", "yes",
+ "Keep email citations when accepting messages.\n"
+ "Setting this to \"no\" strips out \"quoted\" text"
+ " from the message.\n"
+ "Signatures are also stripped.",
+ ["EMAIL_KEEP_QUOTED_TEXT"]),
+ (BooleanOption, "leave_body_unchanged", "no",
+ "Preserve the email body as is - that is,\n"
+ "keep the citations _and_ signatures.",
+ ["EMAIL_LEAVE_BODY_UNCHANGED"]),
+ (Option, "default_class", "issue",
+ "Default class to use in the mailgw\n"
+ "if one isn't supplied in email subjects.\n"
+ "To disable, leave the value blank.",
+ ["MAIL_DEFAULT_CLASS"]),
+ (NullableOption, "language", "",
+ "Default locale name for the tracker mail gateway.\n"
+ "If this option is not set, mail gateway will use\n"
+ "the language of the tracker instance."),
+ (Option, "subject_prefix_parsing", "strict",
+ "Controls the parsing of the [prefix] on subject\n"
+ "lines in incoming emails. \"strict\" will return an\n"
+ "error to the sender if the [prefix] is not recognised.\n"
+ "\"loose\" will attempt to parse the [prefix] but just\n"
+ "pass it through as part of the issue title if not\n"
+ "recognised. \"none\" will always pass any [prefix]\n"
+ "through as part of the issue title."),
+ (Option, "subject_suffix_parsing", "strict",
+ "Controls the parsing of the [suffix] on subject\n"
+ "lines in incoming emails. \"strict\" will return an\n"
+ "error to the sender if the [suffix] is not recognised.\n"
+ "\"loose\" will attempt to parse the [suffix] but just\n"
+ "pass it through as part of the issue title if not\n"
+ "recognised. \"none\" will always pass any [suffix]\n"
+ "through as part of the issue title."),
+ (Option, "subject_suffix_delimiters", "[]",
+ "Defines the brackets used for delimiting the commands\n"
+ "suffix in a subject line."),
+ (Option, "subject_content_match", "always",
+ "Controls matching of the incoming email subject line\n"
+ "against issue titles in the case where there is no\n"
+ "designator [prefix]. \"never\" turns off matching.\n"
+ "\"creation + interval\" or \"activity + interval\"\n"
+ "will match an issue for the interval after the issue's\n"
+ "creation or last activity. The interval is a standard\n"
+ "Roundup interval."),
+ ), "Roundup Mail Gateway options"),
+ ("nosy", (
+ (RunDetectorOption, "messages_to_author", "no",
+ "Send nosy messages to the author of the message.",
+ ["MESSAGES_TO_AUTHOR"]),
+ (Option, "signature_position", "bottom",
+ "Where to place the email signature.\n"
+ "Allowed values: top, bottom, none",
+ ["EMAIL_SIGNATURE_POSITION"]),
+ (RunDetectorOption, "add_author", "new",
+ "Does the author of a message get placed on the nosy list\n"
+ "automatically? If 'new' is used, then the author will\n"
+ "only be added when a message creates a new issue.\n"
+ "If 'yes', then the author will be added on followups too.\n"
+ "If 'no', they're never added to the nosy.\n",
+ ["ADD_AUTHOR_TO_NOSY"]),
+ (RunDetectorOption, "add_recipients", "new",
+ "Do the recipients (To:, Cc:) of a message get placed on the\n"
+ "nosy list? If 'new' is used, then the recipients will\n"
+ "only be added when a message creates a new issue.\n"
+ "If 'yes', then the recipients will be added on followups too.\n"
+ "If 'no', they're never added to the nosy.\n",
+ ["ADD_RECIPIENTS_TO_NOSY"]),
+ (Option, "email_sending", "single",
+ "Controls the email sending from the nosy reactor. If\n"
+ "\"multiple\" then a separate email is sent to each\n"
+ "recipient. If \"single\" then a single email is sent with\n"
+ "each recipient as a CC address."),
+ ), "Nosy messages sending"),
+)
+
+### Configuration classes
+
+class Config:
+
+ """Base class for configuration objects.
+
+ Configuration options may be accessed as attributes or items
+ of instances of this class. All option names are uppercased.
+
+ """
+
+ # Config file name
+ INI_FILE = "config.ini"
+
+ # Object attributes that should not be taken as common configuration
+ # options in __setattr__ (most of them are initialized in constructor):
+ # builtin pseudo-option - package home directory
+ HOME = "."
+ # names of .ini file sections, in order
+ sections = None
+ # section comments
+ section_descriptions = None
+ # lists of option names for each section, in order
+ section_options = None
+ # mapping from option names and aliases to Option instances
+ options = None
+ # actual name of the config file. set on load.
+ filepath = os.path.join(HOME, INI_FILE)
+
+ def __init__(self, config_path=None, layout=None, settings={}):
+ """Initialize confing instance
+
+ Parameters:
+ config_path:
+ optional directory or file name of the config file.
+ If passed, load the config after processing layout (if any).
+ If config_path is a directory name, use default base name
+ of the config file.
+ layout:
+ optional configuration layout, a sequence of
+ section definitions suitable for .add_section()
+ settings:
+ optional setting overrides (dictionary).
+ The overrides are applied after loading config file.
+
+ """
+ # initialize option containers:
+ self.sections = []
+ self.section_descriptions = {}
+ self.section_options = {}
+ self.options = {}
+ # add options from the layout structure
+ if layout:
+ for section in layout:
+ self.add_section(*section)
+ if config_path is not None:
+ self.load(config_path)
+ for (name, value) in settings.items():
+ self[name.upper()] = value
+
+ def add_section(self, section, options, description=None):
+ """Define new config section
+
+ Parameters:
+ section - name of the config.ini section
+ options - a sequence of Option definitions.
+ Each Option definition is a sequence
+ containing class object and constructor
+ parameters, starting from the setting name:
+ setting, default, [description, [aliases]]
+ description - optional section comment
+
+ Note: aliases should only exist in historical options
+ for backwards compatibility - new options should
+ *not* have aliases!
+
+ """
+ if description or not self.section_descriptions.has_key(section):
+ self.section_descriptions[section] = description
+ for option_def in options:
+ klass = option_def[0]
+ args = option_def[1:]
+ option = klass(self, section, *args)
+ self.add_option(option)
+
+ def add_option(self, option):
+ """Adopt a new Option object"""
+ _section = option.section
+ _name = option.setting
+ if _section not in self.sections:
+ self.sections.append(_section)
+ _options = self._get_section_options(_section)
+ if _name not in _options:
+ _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
+ for _name in option.aliases:
+ self.options[_name] = option
+
+ def update_option(self, name, klass,
+ default=NODEFAULT, description=None
+ ):
+ """Override behaviour of early created option.
+
+ Parameters:
+ name:
+ option name
+ klass:
+ one of the Option classes
+ default:
+ optional default value for the option
+ description:
+ optional new description for the option
+
+ Conversion from current option value to new class value
+ is done via string representation.
+
+ This method may be used to attach some brains
+ to options autocreated by UserConfig.
+
+ """
+ # fetch current option
+ option = self._get_option(name)
+ # compute constructor parameters
+ if default is NODEFAULT:
+ default = option.default
+ if description is None:
+ description = option.description
+ value = option.value2str(current=1)
+ # resurrect the option
+ option = klass(self, option.section, option.setting,
+ default=default, description=description)
+ # apply the value
+ option.set(value)
+ # incorporate new option
+ del self[name]
+ self.add_option(option)
+
+ def reset(self):
+ """Set all options to their default values"""
+ for _option in self.items():
+ _option.reset()
+
+ # Meant for commandline tools.
+ # Allows automatic creation of configuration files like this:
+ # roundup-server -p 8017 -u roundup --save-config
+ def getopt(self, args, short_options="", long_options=(),
+ config_load_options=("C", "config"), **options
+ ):
+ """Apply options specified in command line arguments.
+
+ Parameters:
+ args:
+ command line to parse (sys.argv[1:])
+ short_options:
+ optional string of letters for command line options
+ that are not config options
+ long_options:
+ optional list of names for long options
+ that are not config options
+ config_load_options:
+ two-element sequence (letter, long_option) defining
+ the options for config file. If unset, don't load
+ config file; otherwise config file is read prior
+ to applying other options. Short option letter
+ must not have a colon and long_option name must
+ not have an equal sign or '--' prefix.
+ options:
+ mapping from option names to command line option specs.
+ e.g. server_port="p:", server_user="u:"
+ Names are forced to lower case for commandline parsing
+ (long options) and to upper case to find config options.
+ Command line options accepting no value are assumed
+ to be binary and receive value 'yes'.
+
+ Return value: same as for python standard getopt(), except that
+ processed options are removed from returned option list.
+
+ """
+ # take a copy of long_options
+ long_options = list(long_options)
+ # build option lists
+ cfg_names = {}
+ booleans = []
+ for (name, letter) in options.items():
+ cfg_name = name.upper()
+ short_opt = "-" + letter[0]
+ name = name.lower().replace("_", "-")
+ cfg_names.update({short_opt: cfg_name, "--" + name: cfg_name})
+
+ short_options += letter
+ if letter[-1] == ":":
+ long_options.append(name + "=")
+ else:
+ booleans.append(short_opt)
+ long_options.append(name)
+
+ if config_load_options:
+ short_options += config_load_options[0] + ":"
+ long_options.append(config_load_options[1] + "=")
+ # compute names that will be searched in getopt return value
+ config_load_options = (
+ "-" + config_load_options[0],
+ "--" + config_load_options[1],
+ )
+ # parse command line arguments
+ optlist, args = getopt.getopt(args, short_options, long_options)
+ # load config file if requested
+ if config_load_options:
+ for option in optlist:
+ if option[0] in config_load_options:
+ self.load_ini(option[1])
+ optlist.remove(option)
+ break
+ # apply options
+ extra_options = []
+ for (opt, arg) in optlist:
+ if (opt in booleans): # and not arg
+ arg = "yes"
+ try:
+ name = cfg_names[opt]
+ except KeyError:
+ extra_options.append((opt, arg))
+ else:
+ self[name] = arg
+ return (extra_options, args)
+
+ # option and section locators (used in option access methods)
+
+ def _get_option(self, name):
+ try:
+ return self.options[name]
+ except KeyError:
+ raise InvalidOptionError(name)
+
+ def _get_section_options(self, name):
+ return self.section_options.setdefault(name, [])
+
+ def _get_unset_options(self):
+ """Return options that need manual adjustments
+
+ Return value is a dictionary where keys are section
+ names and values are lists of option names as they
+ appear in the config file.
+
+ """
+ need_set = {}
+ for option in self.items():
+ if not option.isset():
+ need_set.setdefault(option.section, []).append(option.setting)
+ return need_set
+
+ def _adjust_options(self, config):
+ """Load ad-hoc option definitions from ConfigParser instance."""
+ pass
+
+ def _get_name(self):
+ """Return the service name for config file heading"""
+ return ""
+
+ # file operations
+
+ def load_ini(self, config_path, defaults=None):
+ """Set options from config.ini file in given home_dir
+
+ Parameters:
+ config_path:
+ directory or file name of the config file.
+ If config_path is a directory name, use default
+ base name of the config file
+ defaults:
+ optional dictionary of defaults for ConfigParser
+
+ Note: if home_dir does not contain config.ini file,
+ no error is raised. Config will be reset to defaults.
+
+ """
+ if os.path.isdir(config_path):
+ home_dir = config_path
+ config_path = os.path.join(config_path, self.INI_FILE)
+ else:
+ home_dir = os.path.dirname(config_path)
+ # parse the file
+ config_defaults = {"HOME": home_dir}
+ if defaults:
+ config_defaults.update(defaults)
+ config = ConfigParser.ConfigParser(config_defaults)
+ config.read([config_path])
+ # .ini file loaded ok.
+ self.HOME = home_dir
+ self.filepath = config_path
+ self._adjust_options(config)
+ # set the options, starting from HOME
+ self.reset()
+ for option in self.items():
+ option.load_ini(config)
+
+ def load(self, home_dir):
+ """Load configuration settings from home_dir"""
+ self.load_ini(home_dir)
+
+ def save(self, ini_file=None):
+ """Write current configuration to .ini file
+
+ 'ini_file' argument, if passed, must be valid full path
+ to the file to write. If omitted, default file in current
+ HOME is created.
+
+ If the file to write already exists, it is saved with '.bak'
+ extension.
+
+ """
+ if ini_file is None:
+ ini_file = self.filepath
+ _tmp_file = os.path.splitext(ini_file)[0]
+ _bak_file = _tmp_file + ".bak"
+ _tmp_file = _tmp_file + ".tmp"
+ _fp = file(_tmp_file, "wt")
+ _fp.write("# %s configuration file\n" % self._get_name())
+ _fp.write("# Autogenerated at %s\n" % time.asctime())
+ need_set = self._get_unset_options()
+ if need_set:
+ _fp.write("\n# WARNING! Following options need adjustments:\n")
+ for section, options in need_set.items():
+ _fp.write("# [%s]: %s\n" % (section, ", ".join(options)))
+ for section in self.sections:
+ comment = self.section_descriptions.get(section, None)
+ if comment:
+ _fp.write("\n# ".join([""] + comment.split("\n")) +"\n")
+ else:
+ # no section comment - just leave a blank line between sections
+ _fp.write("\n")
+ _fp.write("[%s]\n" % section)
+ for option in self._get_section_options(section):
+ _fp.write("\n" + self.options[(section, option)].format())
+ _fp.close()
+ if os.access(ini_file, os.F_OK):
+ if os.access(_bak_file, os.F_OK):
+ os.remove(_bak_file)
+ os.rename(ini_file, _bak_file)
+ os.rename(_tmp_file, ini_file)
+
+ # container emulation
+
+ def __len__(self):
+ return len(self.items())
+
+ def __getitem__(self, name):
+ if name == "HOME":
+ return self.HOME
+ else:
+ return self._get_option(name).get()
+
+ def __setitem__(self, name, value):
+ if name == "HOME":
+ self.HOME = value
+ else:
+ self._get_option(name).set(value)
+
+ def __delitem__(self, name):
+ _option = self._get_option(name)
+ _section = _option.section
+ _name = _option.setting
+ self._get_section_options(_section).remove(_name)
+ del self.options[(_section, _name)]
+ for _alias in _option.aliases:
+ del self.options[_alias]
+
+ def items(self):
+ """Return the list of Option objects, in .ini file order
+
+ Note that HOME is not included in this list
+ because it is builtin pseudo-option, not a real Option
+ object loaded from or saved to .ini file.
+
+ """
+ return [self.options[(_section, _name)]
+ for _section in self.sections
+ for _name in self._get_section_options(_section)
+ ]
+
+ def keys(self):
+ """Return the list of "canonical" names of the options
+
+ Unlike .items(), this list also includes HOME
+
+ """
+ return ["HOME"] + [_option.name for _option in self.items()]
+
+ # .values() is not implemented because i am not sure what should be
+ # the values returned from this method: Option instances or config values?
+
+ # attribute emulation
+
+ def __setattr__(self, name, value):
+ if self.__dict__.has_key(name) or hasattr(self.__class__, name):
+ self.__dict__[name] = value
+ else:
+ self._get_option(name).set(value)
+
+ # Note: __getattr__ is not symmetric to __setattr__:
+ # self.__dict__ lookup is done before calling this method
+ def __getattr__(self, name):
+ return self[name]
+
+class UserConfig(Config):
+
+ """Configuration for user extensions.
+
+ Instances of this class have no predefined configuration layout.
+ Options are created on the fly for each setting present in the
+ config file.
+
+ """
+
+ def _adjust_options(self, config):
+ # config defaults appear in all sections.
+ # we'll need to filter them out.
+ defaults = config.defaults().keys()
+ # see what options are already defined and add missing ones
+ preset = [(option.section, option.setting) for option in self.items()]
+ for section in config.sections():
+ for name in config.options(section):
+ if ((section, name) not in preset) \
+ and (name not in defaults):
+ self.add_option(Option(self, section, name))
+
+class CoreConfig(Config):
+
+ """Roundup instance configuration.
+
+ Core config has a predefined layout (see the SETTINGS structure),
+ supports loading of old-style pythonic configurations and holds
+ three additional attributes:
+ logging:
+ instance logging engine, from standard python logging module
+ or minimalistic logger implemented in Roundup
+ detectors:
+ user-defined configuration for detectors
+ ext:
+ user-defined configuration for extensions
+
+ """
+
+ # module name for old style configuration
+ PYCONFIG = "config"
+ # user configs
+ ext = None
+ detectors = None
+
+ def __init__(self, home_dir=None, settings={}):
+ Config.__init__(self, home_dir, layout=SETTINGS, settings=settings)
+ # load the config if home_dir given
+ if home_dir is None:
+ self.init_logging()
+
+ def _get_unset_options(self):
+ need_set = Config._get_unset_options(self)
+ # remove MAIL_PASSWORD if MAIL_USER is empty
+ if "password" in need_set.get("mail", []):
+ if not self["MAIL_USERNAME"]:
+ settings = need_set["mail"]
+ settings.remove("password")
+ if not settings:
+ del need_set["mail"]
+ return need_set
+
+ def _get_name(self):
+ return self["TRACKER_NAME"]
+
+ def reset(self):
+ Config.reset(self)
+ if self.ext:
+ self.ext.reset()
+ if self.detectors:
+ self.detectors.reset()
+ self.init_logging()
+
+ def init_logging(self):
+ _file = self["LOGGING_CONFIG"]
+ if _file and os.path.isfile(_file):
+ logging.config.fileConfig(_file)
+ return
+
+ _file = self["LOGGING_FILENAME"]
+ # set file & level on the root logger
+ logger = logging.getLogger()
+ if _file:
+ hdlr = logging.FileHandler(_file)
+ else:
+ hdlr = logging.StreamHandler(sys.stdout)
+ formatter = logging.Formatter(
+ '%(asctime)s %(levelname)s %(message)s')
+ hdlr.setFormatter(formatter)
+ # no logging API to remove all existing handlers!?!
+ logger.handlers = [hdlr]
+ logger.setLevel(logging._levelNames[self["LOGGING_LEVEL"] or "ERROR"])
+
+ def load(self, home_dir):
+ """Load configuration from path designated by home_dir argument"""
+ if os.path.isfile(os.path.join(home_dir, self.INI_FILE)):
+ self.load_ini(home_dir)
+ else:
+ self.load_pyconfig(home_dir)
+ self.init_logging()
+ self.ext = UserConfig(os.path.join(home_dir, "extensions"))
+ self.detectors = UserConfig(os.path.join(home_dir, "detectors"))
+
+ def load_ini(self, home_dir, defaults=None):
+ """Set options from config.ini file in given home_dir directory"""
+ config_defaults = {"TRACKER_HOME": home_dir}
+ if defaults:
+ config_defaults.update(defaults)
+ Config.load_ini(self, home_dir, config_defaults)
+
+ def load_pyconfig(self, home_dir):
+ """Set options from config.py file in given home_dir directory"""
+ # try to locate and import the module
+ _mod_fp = None
+ try:
+ try:
+ _module = imp.find_module(self.PYCONFIG, [home_dir])
+ _mod_fp = _module[0]
+ _config = imp.load_module(self.PYCONFIG, *_module)
+ except ImportError:
+ raise NoConfigError(home_dir)
+ finally:
+ if _mod_fp is not None:
+ _mod_fp.close()
+ # module loaded ok. set the options, starting from HOME
+ self.reset()
+ self.HOME = home_dir
+ for _option in self.items():
+ _option.load_pyconfig(_config)
+ # backward compatibility:
+ # SMTP login parameters were specified as a tuple in old style configs
+ # convert them to new plain string options
+ _mailuser = getattr(_config, "MAILUSER", ())
+ if len(_mailuser) > 0:
+ self.MAIL_USERNAME = _mailuser[0]
+ if len(_mailuser) > 1:
+ self.MAIL_PASSWORD = _mailuser[1]
+
+ # in this config, HOME is also known as TRACKER_HOME
+ def __getitem__(self, name):
+ if name == "TRACKER_HOME":
+ return self.HOME
+ else:
+ return Config.__getitem__(self, name)
+
+ def __setitem__(self, name, value):
+ if name == "TRACKER_HOME":
+ self.HOME = value
+ else:
+ self._get_option(name).set(value)
+
+ def __setattr__(self, name, value):
+ if name == "TRACKER_HOME":
+ self.__dict__["HOME"] = value
+ else:
+ Config.__setattr__(self, name, value)
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/date.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/date.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,950 @@
+#
+# 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: date.py,v 1.86 2006/04/27 05:15:16 richard Exp $
+
+"""Date, time and time interval handling.
+"""
+__docformat__ = 'restructuredtext'
+
+import time, re, calendar
+import i18n
+
+try:
+ import datetime
+ have_datetime = 1
+except:
+ have_datetime = 0
+
+def _add_granularity(src, order, value = 1):
+ '''Increment first non-None value in src dictionary ordered by 'order'
+ parameter
+ '''
+ for gran in order:
+ if src[gran]:
+ src[gran] = int(src[gran]) + value
+ break
+
+# no, I don't know why we must anchor the date RE when we only ever use it
+# in a match()
+date_re = re.compile(r'''^
+ ((?P<y>\d\d\d\d)([/-](?P<m>\d\d?)([/-](?P<d>\d\d?))?)? # yyyy[-mm[-dd]]
+ |(?P<a>\d\d?)[/-](?P<b>\d\d?))? # or mm-dd
+ (?P<n>\.)? # .
+ (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d?(\.\d+)?))?)? # hh:mm:ss
+ (?P<o>[\d\smywd\-+]+)? # offset
+$''', re.VERBOSE)
+serialised_date_re = re.compile(r'''
+ (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d?(\.\d+)?)
+''', re.VERBOSE)
+
+class Date:
+ '''
+ As strings, date-and-time stamps are specified with the date in
+ international standard format (yyyy-mm-dd) joined to the time
+ (hh:mm:ss) by a period ("."). Dates in this form can be easily compared
+ and are fairly readable when printed. An example of a valid stamp is
+ "2000-06-24.13:03:59". We'll call this the "full date format". When
+ Timestamp objects are printed as strings, they appear in the full date
+ format with the time always given in GMT. The full date format is
+ always exactly 19 characters long.
+
+ For user input, some partial forms are also permitted: the whole time
+ or just the seconds may be omitted; and the whole date may be omitted
+ or just the year may be omitted. If the time is given, the time is
+ interpreted in the user's local time zone. The Date constructor takes
+ care of these conversions. In the following examples, suppose that yyyy
+ is the current year, mm is the current month, and dd is the current day
+ of the month; and suppose that the user is on Eastern Standard Time.
+ Examples::
+
+ "2000-04-17" means <Date 2000-04-17.00:00:00>
+ "01-25" means <Date yyyy-01-25.00:00:00>
+ "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
+ "08-13.22:13" means <Date yyyy-08-14.03:13:00>
+ "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
+ "14:25" means <Date yyyy-mm-dd.19:25:00>
+ "8:47:11" means <Date yyyy-mm-dd.13:47:11>
+ "2003" means <Date 2003-01-01.00:00:00>
+ "2003-06" means <Date 2003-06-01.00:00:00>
+ "." means "right now"
+
+ The Date class should understand simple date expressions of the form
+ stamp + interval and stamp - interval. When adding or subtracting
+ intervals involving months or years, the components are handled
+ separately. For example, when evaluating "2000-06-25 + 1m 10d", we
+ first add one month to get 2000-07-25, then add 10 days to get
+ 2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40
+ or 41 days). Example usage::
+
+ >>> Date(".")
+ <Date 2000-06-26.00:34:02>
+ >>> _.local(-5)
+ "2000-06-25.19:34:02"
+ >>> Date(". + 2d")
+ <Date 2000-06-28.00:34:02>
+ >>> Date("1997-04-17", -5)
+ <Date 1997-04-17.00:00:00>
+ >>> Date("01-25", -5)
+ <Date 2000-01-25.00:00:00>
+ >>> Date("08-13.22:13", -5)
+ <Date 2000-08-14.03:13:00>
+ >>> Date("14:25", -5)
+ <Date 2000-06-25.19:25:00>
+
+ The date format 'yyyymmddHHMMSS' (year, month, day, hour,
+ minute, second) is the serialisation format returned by the serialise()
+ method, and is accepted as an argument on instatiation.
+
+ The date class handles basic arithmetic::
+
+ >>> d1=Date('.')
+ >>> d1
+ <Date 2004-04-06.22:04:20.766830>
+ >>> d2=Date('2003-07-01')
+ >>> d2
+ <Date 2003-07-01.00:00:0.000000>
+ >>> d1-d2
+ <Interval + 280d 22:04:20>
+ >>> i1=_
+ >>> d2+i1
+ <Date 2004-04-06.22:04:20.000000>
+ >>> d1-i1
+ <Date 2003-07-01.00:00:0.000000>
+ '''
+
+ def __init__(self, spec='.', offset=0, add_granularity=0, translator=i18n):
+ """Construct a date given a specification and a time zone offset.
+
+ 'spec'
+ is a full date or a partial form, with an optional added or
+ subtracted interval. Or a date 9-tuple.
+ 'offset'
+ is the local time zone offset from GMT in hours.
+ 'translator'
+ is i18n module or one of gettext translation classes.
+ It must have attributes 'gettext' and 'ngettext',
+ serving as translation functions.
+ """
+ self.setTranslator(translator)
+ if type(spec) == type(''):
+ self.set(spec, offset=offset, add_granularity=add_granularity)
+ return
+ elif have_datetime and isinstance(spec, datetime.datetime):
+ # Python 2.3+ datetime object
+ y,m,d,H,M,S,x,x,x = spec.timetuple()
+ if y < 1970: raise ValueError, 'year must be > 1970'
+ S += spec.microsecond/1000000.
+ spec = (y,m,d,H,M,S,x,x,x)
+ elif hasattr(spec, 'tuple'):
+ spec = spec.tuple()
+ elif isinstance(spec, Date):
+ spec = spec.get_tuple()
+ try:
+ y,m,d,H,M,S,x,x,x = spec
+ if y < 1970: raise ValueError, 'year must be > 1970'
+ frac = S - int(S)
+ ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
+ self.year, self.month, self.day, self.hour, self.minute, \
+ self.second, x, x, x = time.gmtime(ts)
+ # we lost the fractional part
+ self.second = self.second + frac
+ except:
+ raise ValueError, 'Unknown spec %r' % (spec,)
+
+ def set(self, spec, offset=0, date_re=date_re,
+ serialised_re=serialised_date_re, add_granularity=0):
+ ''' set the date to the value in spec
+ '''
+
+ m = serialised_re.match(spec)
+ if m is not None:
+ # we're serialised - easy!
+ g = m.groups()
+ (self.year, self.month, self.day, self.hour, self.minute) = \
+ map(int, g[:5])
+ self.second = float(g[5])
+ return
+
+ # not serialised data, try usual format
+ m = date_re.match(spec)
+ if m is None:
+ raise ValueError, self._('Not a date spec: '
+ '"yyyy-mm-dd", "mm-dd", "HH:MM", "HH:MM:SS" or '
+ '"yyyy-mm-dd.HH:MM:SS.SSS"')
+
+ info = m.groupdict()
+
+ if add_granularity:
+ _add_granularity(info, 'SMHdmyab')
+
+ # get the current date as our default
+ ts = time.time()
+ frac = ts - int(ts)
+ y,m,d,H,M,S,x,x,x = time.gmtime(ts)
+ # gmtime loses the fractional seconds
+ S = S + frac
+
+ if info['y'] is not None or info['a'] is not None:
+ if info['y'] is not None:
+ y = int(info['y'])
+ if y < 1970: raise ValueError, 'year must be > 1970'
+ m,d = (1,1)
+ if info['m'] is not None:
+ m = int(info['m'])
+ if info['d'] is not None:
+ d = int(info['d'])
+ if info['a'] is not None:
+ m = int(info['a'])
+ d = int(info['b'])
+ H = -offset
+ M = S = 0
+
+ # override hour, minute, second parts
+ if info['H'] is not None and info['M'] is not None:
+ H = int(info['H']) - offset
+ M = int(info['M'])
+ S = 0
+ if info['S'] is not None:
+ S = float(info['S'])
+
+ if add_granularity:
+ S = S - 1
+
+ # now handle the adjustment of hour
+ frac = S - int(S)
+ ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
+ self.year, self.month, self.day, self.hour, self.minute, \
+ self.second, x, x, x = time.gmtime(ts)
+ # we lost the fractional part along the way
+ self.second = self.second + frac
+
+ if info.get('o', None):
+ try:
+ self.applyInterval(Interval(info['o'], allowdate=0))
+ except ValueError:
+ raise ValueError, self._('%r not a date / time spec '
+ '"yyyy-mm-dd", "mm-dd", "HH:MM", "HH:MM:SS" or '
+ '"yyyy-mm-dd.HH:MM:SS.SSS"')%(spec,)
+
+ def addInterval(self, interval):
+ ''' Add the interval to this date, returning the date tuple
+ '''
+ # do the basic calc
+ sign = interval.sign
+ year = self.year + sign * interval.year
+ month = self.month + sign * interval.month
+ day = self.day + sign * interval.day
+ hour = self.hour + sign * interval.hour
+ minute = self.minute + sign * interval.minute
+ # Intervals work on whole seconds
+ second = int(self.second) + sign * interval.second
+
+ # now cope with under- and over-flow
+ # first do the time
+ while (second < 0 or second > 59 or minute < 0 or minute > 59 or
+ hour < 0 or hour > 23):
+ if second < 0: minute -= 1; second += 60
+ elif second > 59: minute += 1; second -= 60
+ if minute < 0: hour -= 1; minute += 60
+ elif minute > 59: hour += 1; minute -= 60
+ if hour < 0: day -= 1; hour += 24
+ elif hour > 23: day += 1; hour -= 24
+
+ # fix up the month so we're within range
+ while month < 1 or month > 12:
+ if month < 1: year -= 1; month += 12
+ if month > 12: year += 1; month -= 12
+
+ # now do the days, now that we know what month we're in
+ def get_mdays(year, month):
+ if month == 2 and calendar.isleap(year): return 29
+ else: return calendar.mdays[month]
+
+ while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month):
+ # now to day under/over
+ if day < 1:
+ # When going backwards, decrement month, then increment days
+ month -= 1
+ day += get_mdays(year,month)
+ elif day > get_mdays(year,month):
+ # When going forwards, decrement days, then increment month
+ day -= get_mdays(year,month)
+ month += 1
+
+ # possibly fix up the month so we're within range
+ while month < 1 or month > 12:
+ if month < 1: year -= 1; month += 12 ; day += 31
+ if month > 12: year += 1; month -= 12
+
+ return (year, month, day, hour, minute, second, 0, 0, 0)
+
+ def differenceDate(self, other):
+ "Return the difference between this date and another date"
+ return self - other
+
+ def applyInterval(self, interval):
+ ''' Apply the interval to this date
+ '''
+ self.year, self.month, self.day, self.hour, self.minute, \
+ self.second, x, x, x = self.addInterval(interval)
+
+ def __add__(self, interval):
+ """Add an interval to this date to produce another date.
+ """
+ return Date(self.addInterval(interval), translator=self.translator)
+
+ # deviates from spec to allow subtraction of dates as well
+ def __sub__(self, other):
+ """ Subtract:
+ 1. an interval from this date to produce another date.
+ 2. a date from this date to produce an interval.
+ """
+ if isinstance(other, Interval):
+ other = Interval(other.get_tuple())
+ other.sign *= -1
+ return self.__add__(other)
+
+ assert isinstance(other, Date), 'May only subtract Dates or Intervals'
+
+ return self.dateDelta(other)
+
+ def dateDelta(self, other):
+ """ Produce an Interval of the difference between this date
+ and another date. Only returns days:hours:minutes:seconds.
+ """
+ # Returning intervals larger than a day is almost
+ # impossible - months, years, weeks, are all so imprecise.
+ a = calendar.timegm((self.year, self.month, self.day, self.hour,
+ self.minute, self.second, 0, 0, 0))
+ b = calendar.timegm((other.year, other.month, other.day,
+ other.hour, other.minute, other.second, 0, 0, 0))
+ # intervals work in whole seconds
+ diff = int(a - b)
+ if diff > 0:
+ sign = 1
+ else:
+ sign = -1
+ diff = -diff
+ S = diff%60
+ M = (diff/60)%60
+ H = (diff/(60*60))%24
+ d = diff/(24*60*60)
+ return Interval((0, 0, d, H, M, S), sign=sign,
+ translator=self.translator)
+
+ def __cmp__(self, other, int_seconds=0):
+ """Compare this date to another date."""
+ if other is None:
+ return 1
+ for attr in ('year', 'month', 'day', 'hour', 'minute'):
+ if not hasattr(other, attr):
+ return 1
+ r = cmp(getattr(self, attr), getattr(other, attr))
+ if r: return r
+ if not hasattr(other, 'second'):
+ return 1
+ if int_seconds:
+ return cmp(int(self.second), int(other.second))
+ return cmp(self.second, other.second)
+
+ def __str__(self):
+ """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
+ return self.formal()
+
+ def formal(self, sep='.', sec='%02d'):
+ f = '%%4d-%%02d-%%02d%s%%02d:%%02d:%s'%(sep, sec)
+ return f%(self.year, self.month, self.day, self.hour, self.minute,
+ self.second)
+
+ def pretty(self, format='%d %B %Y'):
+ ''' print up the date date using a pretty format...
+
+ Note that if the day is zero, and the day appears first in the
+ format, then the day number will be removed from output.
+ '''
+ # Python2.4 strftime() enforces the non-zero-ness of the day-of-year
+ # component of the time tuple, so we need to figure it out
+ t = (self.year, self.month, self.day, self.hour, self.minute,
+ int(self.second), 0, 0, 0)
+ t = calendar.timegm(t)
+ t = time.gmtime(t)
+ str = time.strftime(format, t)
+
+ # handle zero day by removing it
+ if format.startswith('%d') and str[0] == '0':
+ return ' ' + str[1:]
+ return str
+
+ def __repr__(self):
+ return '<Date %s>'%self.formal(sec='%06.3f')
+
+ def local(self, offset):
+ """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
+ """
+ return Date((self.year, self.month, self.day, self.hour + offset,
+ self.minute, self.second, 0, 0, 0), translator=self.translator)
+
+ def __deepcopy__(self, memo):
+ return Date((self.year, self.month, self.day, self.hour,
+ self.minute, self.second, 0, 0, 0), translator=self.translator)
+
+ def get_tuple(self):
+ return (self.year, self.month, self.day, self.hour, self.minute,
+ self.second, 0, 0, 0)
+
+ def serialise(self):
+ return '%4d%02d%02d%02d%02d%06.3f'%(self.year, self.month,
+ self.day, self.hour, self.minute, self.second)
+
+ def timestamp(self):
+ ''' return a UNIX timestamp for this date '''
+ frac = self.second - int(self.second)
+ ts = calendar.timegm((self.year, self.month, self.day, self.hour,
+ self.minute, self.second, 0, 0, 0))
+ # we lose the fractional part
+ return ts + frac
+
+ def setTranslator(self, translator):
+ """Replace the translation engine
+
+ 'translator'
+ is i18n module or one of gettext translation classes.
+ It must have attributes 'gettext' and 'ngettext',
+ serving as translation functions.
+ """
+ self.translator = translator
+ self._ = translator.gettext
+ self.ngettext = translator.ngettext
+
+class Interval:
+ '''
+ Date intervals are specified using the suffixes "y", "m", and "d". The
+ suffix "w" (for "week") means 7 days. Time intervals are specified in
+ hh:mm:ss format (the seconds may be omitted, but the hours and minutes
+ may not).
+
+ "3y" means three years
+ "2y 1m" means two years and one month
+ "1m 25d" means one month and 25 days
+ "2w 3d" means two weeks and three days
+ "1d 2:50" means one day, two hours, and 50 minutes
+ "14:00" means 14 hours
+ "0:04:33" means four minutes and 33 seconds
+
+ Example usage:
+ >>> Interval(" 3w 1 d 2:00")
+ <Interval + 22d 2:00>
+ >>> Date(". + 2d") + Interval("- 3w")
+ <Date 2000-06-07.00:34:02>
+ >>> Interval('1:59:59') + Interval('00:00:01')
+ <Interval + 2:00>
+ >>> Interval('2:00') + Interval('- 00:00:01')
+ <Interval + 1:59:59>
+ >>> Interval('1y')/2
+ <Interval + 6m>
+ >>> Interval('1:00')/2
+ <Interval + 0:30>
+ >>> Interval('2003-03-18')
+ <Interval + [number of days between now and 2003-03-18]>
+ >>> Interval('-4d 2003-03-18')
+ <Interval + [number of days between now and 2003-03-14]>
+
+ Interval arithmetic is handled in a couple of special ways, trying
+ to cater for the most common cases. Fundamentally, Intervals which
+ have both date and time parts will result in strange results in
+ arithmetic - because of the impossibility of handling day->month->year
+ over- and under-flows. Intervals may also be divided by some number.
+
+ Intervals are added to Dates in order of:
+ seconds, minutes, hours, years, months, days
+
+ Calculations involving months (eg '+2m') have no effect on days - only
+ days (or over/underflow from hours/mins/secs) will do that, and
+ days-per-month and leap years are accounted for. Leap seconds are not.
+
+ The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
+ minute, second) is the serialisation format returned by the serialise()
+ method, and is accepted as an argument on instatiation.
+
+ TODO: more examples, showing the order of addition operation
+ '''
+ def __init__(self, spec, sign=1, allowdate=1, add_granularity=0,
+ translator=i18n
+ ):
+ """Construct an interval given a specification."""
+ self.setTranslator(translator)
+ if isinstance(spec, (int, float, long)):
+ self.from_seconds(spec)
+ elif isinstance(spec, basestring):
+ self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
+ elif isinstance(spec, Interval):
+ (self.sign, self.year, self.month, self.day, self.hour,
+ self.minute, self.second) = spec.get_tuple()
+ else:
+ if len(spec) == 7:
+ self.sign, self.year, self.month, self.day, self.hour, \
+ self.minute, self.second = spec
+ self.second = int(self.second)
+ else:
+ # old, buggy spec form
+ self.sign = sign
+ self.year, self.month, self.day, self.hour, self.minute, \
+ self.second = spec
+ self.second = int(self.second)
+
+ def __deepcopy__(self, memo):
+ return Interval((self.sign, self.year, self.month, self.day,
+ self.hour, self.minute, self.second), translator=self.translator)
+
+ def set(self, spec, allowdate=1, interval_re=re.compile('''
+ \s*(?P<s>[-+])? # + or -
+ \s*((?P<y>\d+\s*)y)? # year
+ \s*((?P<m>\d+\s*)m)? # month
+ \s*((?P<w>\d+\s*)w)? # week
+ \s*((?P<d>\d+\s*)d)? # day
+ \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)? # time
+ \s*(?P<D>
+ (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)? # [yyyy-]mm-dd
+ \.? # .
+ (\d?\d:\d\d)?(:\d\d)? # hh:mm:ss
+ )?''', re.VERBOSE), serialised_re=re.compile('''
+ (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
+ (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
+ add_granularity=0):
+ ''' set the date to the value in spec
+ '''
+ self.year = self.month = self.week = self.day = self.hour = \
+ self.minute = self.second = 0
+ self.sign = 1
+ m = serialised_re.match(spec)
+ if not m:
+ m = interval_re.match(spec)
+ if not m:
+ raise ValueError, self._('Not an interval spec:'
+ ' [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]')
+ else:
+ allowdate = 0
+
+ # pull out all the info specified
+ info = m.groupdict()
+ if add_granularity:
+ _add_granularity(info, 'SMHdwmy', (info['s']=='-' and -1 or 1))
+
+ valid = 0
+ for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
+ 'H':'hour', 'M':'minute', 'S':'second'}.items():
+ if info.get(group, None) is not None:
+ valid = 1
+ setattr(self, attr, int(info[group]))
+
+ # make sure it's valid
+ if not valid and not info['D']:
+ raise ValueError, self._('Not an interval spec:'
+ ' [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]')
+
+ if self.week:
+ self.day = self.day + self.week*7
+
+ if info['s'] is not None:
+ self.sign = {'+':1, '-':-1}[info['s']]
+
+ # use a date spec if one is given
+ if allowdate and info['D'] is not None:
+ now = Date('.')
+ date = Date(info['D'])
+ # if no time part was specified, nuke it in the "now" date
+ if not date.hour or date.minute or date.second:
+ now.hour = now.minute = now.second = 0
+ if date != now:
+ y = now - (date + self)
+ self.__init__(y.get_tuple())
+
+ def __cmp__(self, other):
+ """Compare this interval to another interval."""
+ if other is None:
+ # we are always larger than None
+ return 1
+ for attr in 'sign year month day hour minute second'.split():
+ r = cmp(getattr(self, attr), getattr(other, attr))
+ if r:
+ return r
+ return 0
+
+ def __str__(self):
+ """Return this interval as a string."""
+ l = []
+ if self.year: l.append('%sy'%self.year)
+ if self.month: l.append('%sm'%self.month)
+ if self.day: l.append('%sd'%self.day)
+ if self.second:
+ l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
+ elif self.hour or self.minute:
+ l.append('%d:%02d'%(self.hour, self.minute))
+ if l:
+ l.insert(0, {1:'+', -1:'-'}[self.sign])
+ else:
+ l.append('00:00')
+ return ' '.join(l)
+
+ def __add__(self, other):
+ if isinstance(other, Date):
+ # the other is a Date - produce a Date
+ return Date(other.addInterval(self), translator=self.translator)
+ elif isinstance(other, Interval):
+ # add the other Interval to this one
+ a = self.get_tuple()
+ as = a[0]
+ b = other.get_tuple()
+ bs = b[0]
+ i = [as*x + bs*y for x,y in zip(a[1:],b[1:])]
+ i.insert(0, 1)
+ i = fixTimeOverflow(i)
+ return Interval(i, translator=self.translator)
+ # nope, no idea what to do with this other...
+ raise TypeError, "Can't add %r"%other
+
+ def __sub__(self, other):
+ if isinstance(other, Date):
+ # the other is a Date - produce a Date
+ interval = Interval(self.get_tuple())
+ interval.sign *= -1
+ return Date(other.addInterval(interval),
+ translator=self.translator)
+ elif isinstance(other, Interval):
+ # add the other Interval to this one
+ a = self.get_tuple()
+ as = a[0]
+ b = other.get_tuple()
+ bs = b[0]
+ i = [as*x - bs*y for x,y in zip(a[1:],b[1:])]
+ i.insert(0, 1)
+ i = fixTimeOverflow(i)
+ return Interval(i, translator=self.translator)
+ # nope, no idea what to do with this other...
+ raise TypeError, "Can't add %r"%other
+
+ def __div__(self, other):
+ """ Divide this interval by an int value.
+
+ Can't divide years and months sensibly in the _same_
+ calculation as days/time, so raise an error in that situation.
+ """
+ try:
+ other = float(other)
+ except TypeError:
+ raise ValueError, "Can only divide Intervals by numbers"
+
+ y, m, d, H, M, S = (self.year, self.month, self.day,
+ self.hour, self.minute, self.second)
+ if y or m:
+ if d or H or M or S:
+ raise ValueError, "Can't divide Interval with date and time"
+ months = self.year*12 + self.month
+ months *= self.sign
+
+ months = int(months/other)
+
+ sign = months<0 and -1 or 1
+ m = months%12
+ y = months / 12
+ return Interval((sign, y, m, 0, 0, 0, 0),
+ translator=self.translator)
+
+ else:
+ # handle a day/time division
+ seconds = S + M*60 + H*60*60 + d*60*60*24
+ seconds *= self.sign
+
+ seconds = int(seconds/other)
+
+ sign = seconds<0 and -1 or 1
+ seconds *= sign
+ S = seconds%60
+ seconds /= 60
+ M = seconds%60
+ seconds /= 60
+ H = seconds%24
+ d = seconds / 24
+ return Interval((sign, 0, 0, d, H, M, S),
+ translator=self.translator)
+
+ def __repr__(self):
+ return '<Interval %s>'%self.__str__()
+
+ def pretty(self):
+ ''' print up the date date using one of these nice formats..
+ '''
+ _quarters = self.minute / 15
+ if self.year:
+ s = self.ngettext("%(number)s year", "%(number)s years",
+ self.year) % {'number': self.year}
+ elif self.month or self.day > 28:
+ _months = max(1, int(((self.month * 30) + self.day) / 30))
+ s = self.ngettext("%(number)s month", "%(number)s months",
+ _months) % {'number': _months}
+ elif self.day > 7:
+ _weeks = int(self.day / 7)
+ s = self.ngettext("%(number)s week", "%(number)s weeks",
+ _weeks) % {'number': _weeks}
+ elif self.day > 1:
+ # Note: singular form is not used
+ s = self.ngettext('%(number)s day', '%(number)s days',
+ self.day) % {'number': self.day}
+ elif self.day == 1 or self.hour > 12:
+ if self.sign > 0:
+ return self._('tomorrow')
+ else:
+ return self._('yesterday')
+ elif self.hour > 1:
+ # Note: singular form is not used
+ s = self.ngettext('%(number)s hour', '%(number)s hours',
+ self.hour) % {'number': self.hour}
+ elif self.hour == 1:
+ if self.minute < 15:
+ s = self._('an hour')
+ elif _quarters == 2:
+ s = self._('1 1/2 hours')
+ else:
+ s = self.ngettext('1 %(number)s/4 hours',
+ '1 %(number)s/4 hours', _quarters)%{'number': _quarters}
+ elif self.minute < 1:
+ if self.sign > 0:
+ return self._('in a moment')
+ else:
+ return self._('just now')
+ elif self.minute == 1:
+ # Note: used in expressions "in 1 minute" or "1 minute ago"
+ s = self._('1 minute')
+ elif self.minute < 15:
+ # Note: used in expressions "in 2 minutes" or "2 minutes ago"
+ s = self.ngettext('%(number)s minute', '%(number)s minutes',
+ self.minute) % {'number': self.minute}
+ elif _quarters == 2:
+ s = self._('1/2 an hour')
+ else:
+ s = self.ngettext('%(number)s/4 hour', '%(number)s/4 hours',
+ _quarters) % {'number': _quarters}
+ # XXX this is internationally broken
+ if self.sign < 0:
+ s = self._('%s ago') % s
+ else:
+ s = self._('in %s') % s
+ return s
+
+ def get_tuple(self):
+ return (self.sign, self.year, self.month, self.day, self.hour,
+ self.minute, self.second)
+
+ def serialise(self):
+ sign = self.sign > 0 and '+' or '-'
+ return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
+ self.day, self.hour, self.minute, self.second)
+
+ def as_seconds(self):
+ '''Calculate the Interval as a number of seconds.
+
+ Months are counted as 30 days, years as 365 days. Returns a Long
+ int.
+ '''
+ n = self.year * 365L
+ n = n + self.month * 30
+ n = n + self.day
+ n = n * 24
+ n = n + self.hour
+ n = n * 60
+ n = n + self.minute
+ n = n * 60
+ n = n + self.second
+ return n * self.sign
+
+ def from_seconds(self, val):
+ '''Figure my second, minute, hour and day values using a seconds
+ value.
+ '''
+ val = int(val)
+ if val < 0:
+ self.sign = -1
+ val = -val
+ else:
+ self.sign = 1
+ self.second = val % 60
+ val = val / 60
+ self.minute = val % 60
+ val = val / 60
+ self.hour = val % 24
+ val = val / 24
+ self.day = val
+ self.month = self.year = 0
+
+ def setTranslator(self, translator):
+ """Replace the translation engine
+
+ 'translator'
+ is i18n module or one of gettext translation classes.
+ It must have attributes 'gettext' and 'ngettext',
+ serving as translation functions.
+ """
+ self.translator = translator
+ self._ = translator.gettext
+ self.ngettext = translator.ngettext
+
+
+def fixTimeOverflow(time):
+ """ Handle the overflow in the time portion (H, M, S) of "time":
+ (sign, y,m,d,H,M,S)
+
+ Overflow and underflow will at most affect the _days_ portion of
+ the date. We do not overflow days to months as we don't know _how_
+ to, generally.
+ """
+ # XXX we could conceivably use this function for handling regular dates
+ # XXX too - we just need to interrogate the month/year for the day
+ # XXX overflow...
+
+ sign, y, m, d, H, M, S = time
+ seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
+ if seconds:
+ sign = seconds<0 and -1 or 1
+ seconds *= sign
+ S = seconds%60
+ seconds /= 60
+ M = seconds%60
+ seconds /= 60
+ H = seconds%24
+ d = seconds / 24
+ else:
+ months = y*12 + m
+ sign = months<0 and -1 or 1
+ months *= sign
+ m = months%12
+ y = months/12
+
+ return (sign, y, m, d, H, M, S)
+
+class Range:
+ """Represents range between two values
+ Ranges can be created using one of theese two alternative syntaxes:
+
+ 1. Native english syntax::
+
+ [[From] <value>][ To <value>]
+
+ Keywords "From" and "To" are case insensitive. Keyword "From" is
+ optional.
+
+ 2. "Geek" syntax::
+
+ [<value>][; <value>]
+
+ Either first or second <value> can be omitted in both syntaxes.
+
+ Examples (consider local time is Sat Mar 8 22:07:48 EET 2003)::
+
+ >>> Range("from 2-12 to 4-2")
+ <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
+
+ >>> Range("18:00 TO +2m")
+ <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
+
+ >>> Range("12:00")
+ <Range from 2003-03-08.12:00:00 to None>
+
+ >>> Range("tO +3d")
+ <Range from None to 2003-03-11.20:07:48>
+
+ >>> Range("2002-11-10; 2002-12-12")
+ <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
+
+ >>> Range("; 20:00 +1d")
+ <Range from None to 2003-03-09.20:00:00>
+
+ """
+ def __init__(self, spec, Type, allow_granularity=1, **params):
+ """Initializes Range of type <Type> from given <spec> string.
+
+ Sets two properties - from_value and to_value. None assigned to any of
+ this properties means "infinitum" (-infinitum to from_value and
+ +infinitum to to_value)
+
+ The Type parameter here should be class itself (e.g. Date), not a
+ class instance.
+
+ """
+ self.range_type = Type
+ re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
+ re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
+ # Check which syntax to use
+ if spec.find(';') == -1:
+ # Native english
+ mch_range = re.search(re_range, spec.strip(), re.IGNORECASE)
+ else:
+ # Geek
+ mch_range = re.search(re_geek_range, spec.strip())
+ if mch_range:
+ self.from_value, self.to_value = mch_range.groups()
+ if self.from_value:
+ self.from_value = Type(self.from_value.strip(), **params)
+ if self.to_value:
+ self.to_value = Type(self.to_value.strip(), **params)
+ else:
+ if allow_granularity:
+ self.from_value = Type(spec, **params)
+ self.to_value = Type(spec, add_granularity=1, **params)
+ else:
+ raise ValueError, "Invalid range"
+
+ def __str__(self):
+ return "from %s to %s" % (self.from_value, self.to_value)
+
+ def __repr__(self):
+ return "<Range %s>" % self.__str__()
+
+def test_range():
+ rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
+ "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
+ rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
+ for rspec in rspecs:
+ print '>>> Range("%s")' % rspec
+ print `Range(rspec, Date)`
+ print
+ for rspec in rispecs:
+ print '>>> Range("%s")' % rspec
+ print `Range(rspec, Interval)`
+ print
+
+def test():
+ intervals = (" 3w 1 d 2:00", " + 2d", "3w")
+ for interval in intervals:
+ print '>>> Interval("%s")'%interval
+ print `Interval(interval)`
+
+ dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
+ "08-13.22:13", "14:25", '2002-12')
+ for date in dates:
+ print '>>> Date("%s")'%date
+ print `Date(date)`
+
+ sums = ((". + 2d", "3w"), (".", " 3w 1 d 2:00"))
+ for date, interval in sums:
+ print '>>> Date("%s") + Interval("%s")'%(date, interval)
+ print `Date(date) + Interval(interval)`
+
+if __name__ == '__main__':
+ test()
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/exceptions.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/exceptions.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,19 @@
+#$Id: exceptions.py,v 1.1 2004/03/26 00:44:11 richard Exp $
+'''Exceptions for use across all Roundup components.
+'''
+
+__docformat__ = 'restructuredtext'
+
+class Reject(Exception):
+ '''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
+ handle this exception sanely. For example:
+
+ - mailgw will trap and ignore Reject for file attachments and messages
+ - cgi will trap and present the exception in a nice format
+ '''
+ pass
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/roundup/hyperdb.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/hyperdb.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,964 @@
+#
+# 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: hyperdb.py,v 1.119 2006/04/27 01:39:47 richard Exp $
+
+"""Hyperdatabase implementation, especially field types.
+"""
+__docformat__ = 'restructuredtext'
+
+# standard python modules
+import sys, os, time, re, shutil, weakref
+
+# roundup modules
+import date, password
+from support import ensureParentsExist, PrioList
+
+#
+# Types
+#
+class String:
+ """An object designating a String property."""
+ def __init__(self, indexme='no'):
+ self.indexme = indexme == 'yes'
+ def __repr__(self):
+ ' more useful for dumps '
+ return '<%s>'%self.__class__
+ def from_raw(self, value, **kw):
+ """fix the CRLF/CR -> LF stuff"""
+ return fixNewlines(value)
+
+class Password:
+ """An object designating a Password property."""
+ def __repr__(self):
+ ' more useful for dumps '
+ return '<%s>'%self.__class__
+ def from_raw(self, value, **kw):
+ m = password.Password.pwre.match(value)
+ if m:
+ # password is being given to us encrypted
+ p = password.Password()
+ p.scheme = m.group(1)
+ if p.scheme not in 'SHA crypt plaintext'.split():
+ raise HyperdbValueError, 'property %s: unknown encryption '\
+ 'scheme %r'%(kw['propname'], p.scheme)
+ p.password = m.group(2)
+ value = p
+ else:
+ try:
+ value = password.Password(value)
+ except password.PasswordValueError, message:
+ raise HyperdbValueError, 'property %s: %s'%(propname, message)
+ return value
+
+class Date:
+ """An object designating a Date property."""
+ def __init__(self, offset = None):
+ self._offset = offset
+ def __repr__(self):
+ ' more useful for dumps '
+ return '<%s>'%self.__class__
+ def offset (self, db) :
+ if self._offset is not None :
+ return self._offset
+ return db.getUserTimezone ()
+ def from_raw(self, value, db, **kw):
+ try:
+ value = date.Date(value).local(-self.offset(db))
+ except ValueError, message:
+ raise HyperdbValueError, 'property %s: %r is an invalid '\
+ 'date (%s)'%(kw['propname'], value, message)
+ return value
+ def range_from_raw(self, value, db):
+ """return Range value from given raw value with offset correction"""
+ return date.Range(value, date.Date, offset=self.offset(db))
+
+class Interval:
+ """An object designating an Interval property."""
+ def __repr__(self):
+ ' more useful for dumps '
+ return '<%s>'%self.__class__
+ def from_raw(self, value, **kw):
+ try:
+ value = date.Interval(value)
+ except ValueError, message:
+ raise HyperdbValueError, 'property %s: %r is an invalid '\
+ 'date interval (%s)'%(kw['propname'], value, message)
+ return value
+
+class Link:
+ """An object designating a Link property that links to a
+ node in a specified class."""
+ def __init__(self, classname, do_journal='yes'):
+ ''' Default is to not journal link and unlink events
+ '''
+ self.classname = classname
+ self.do_journal = do_journal == 'yes'
+ def __repr__(self):
+ ' more useful for dumps '
+ return '<%s to "%s">'%(self.__class__, self.classname)
+ def from_raw(self, value, db, propname, **kw):
+ if value == '-1' or not value:
+ value = None
+ else:
+ value = convertLinkValue(db, propname, self, value)
+ return value
+
+class Multilink:
+ """An object designating a Multilink property that links
+ to nodes in a specified class.
+
+ "classname" indicates the class to link to
+
+ "do_journal" indicates whether the linked-to nodes should have
+ 'link' and 'unlink' events placed in their journal
+ """
+ def __init__(self, classname, do_journal='yes'):
+ ''' Default is to not journal link and unlink events
+ '''
+ self.classname = classname
+ self.do_journal = do_journal == 'yes'
+ def __repr__(self):
+ ' more useful for dumps '
+ return '<%s to "%s">'%(self.__class__, self.classname)
+ def from_raw(self, value, db, klass, propname, itemid, **kw):
+ # get the current item value if it's not a new item
+ if itemid and not itemid.startswith('-'):
+ curvalue = klass.get(itemid, propname)
+ else:
+ curvalue = []
+
+ # if the value is a comma-separated string then split it now
+ if isinstance(value, type('')):
+ value = value.split(',')
+
+ # handle each add/remove in turn
+ # keep an extra list for all items that are
+ # definitely in the new list (in case of e.g.
+ # <propname>=A,+B, which should replace the old
+ # list with A,B)
+ set = 1
+ newvalue = []
+ for item in value:
+ item = item.strip()
+
+ # skip blanks
+ if not item: continue
+
+ # handle +/-
+ remove = 0
+ if item.startswith('-'):
+ remove = 1
+ item = item[1:]
+ set = 0
+ elif item.startswith('+'):
+ item = item[1:]
+ set = 0
+
+ # look up the value
+ itemid = convertLinkValue(db, propname, self, item)
+
+ # perform the add/remove
+ if remove:
+ try:
+ curvalue.remove(itemid)
+ except ValueError:
+ raise HyperdbValueError, 'property %s: %r is not ' \
+ 'currently an element'%(propname, item)
+ else:
+ newvalue.append(itemid)
+ if itemid not in curvalue:
+ curvalue.append(itemid)
+
+ # that's it, set the new Multilink property value,
+ # or overwrite it completely
+ if set:
+ value = newvalue
+ else:
+ value = curvalue
+
+ # TODO: one day, we'll switch to numeric ids and this will be
+ # unnecessary :(
+ value = [int(x) for x in value]
+ value.sort()
+ value = [str(x) for x in value]
+ return value
+
+class Boolean:
+ """An object designating a boolean property"""
+ def __repr__(self):
+ 'more useful for dumps'
+ return '<%s>' % self.__class__
+ def from_raw(self, value, **kw):
+ value = value.strip()
+ # checked is a common HTML checkbox value
+ value = value.lower() in ('checked', 'yes', 'true', 'on', '1')
+ return value
+
+class Number:
+ """An object designating a numeric property"""
+ def __repr__(self):
+ 'more useful for dumps'
+ return '<%s>' % self.__class__
+ def from_raw(self, value, **kw):
+ value = value.strip()
+ try:
+ value = float(value)
+ except ValueError:
+ raise HyperdbValueError, 'property %s: %r is not a number'%(
+ kw['propname'], value)
+ return value
+#
+# Support for splitting designators
+#
+class DesignatorError(ValueError):
+ pass
+def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
+ ''' 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)
+
+#
+# the base Database class
+#
+class DatabaseError(ValueError):
+ '''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.
+
+This class defines a hyperdatabase storage layer, which the Classes use to
+store their data.
+
+
+Transactions
+------------
+The Database should support transactions through the commit() and
+rollback() methods. All other Database methods should be transaction-aware,
+using data from the current transaction before looking up the database.
+
+An implementation must provide an override for the get() method so that the
+in-database value is returned in preference to the in-transaction value.
+This is necessary to determine if any values have changed during a
+transaction.
+
+
+Implementation
+--------------
+
+All methods except __repr__ must be implemented by a concrete backend Database.
+
+'''
+
+ # flag to set on retired entries
+ RETIRED_FLAG = '__hyperdb_retired'
+
+ BACKEND_MISSING_STRING = None
+ BACKEND_MISSING_NUMBER = None
+ BACKEND_MISSING_BOOLEAN = None
+
+ def __init__(self, config, journaltag=None):
+ """Open a hyperdatabase given a specifier to some storage.
+
+ The 'storagelocator' is obtained from config.DATABASE.
+ The meaning of 'storagelocator' depends on the particular
+ implementation of the hyperdatabase. It could be a file name,
+ a directory path, a socket descriptor for a connection to a
+ database over the network, etc.
+
+ The 'journaltag' is a token that will be attached to the journal
+ entries for any edits done on the database. If 'journaltag' is
+ None, the database is opened in read-only mode: the Class.create(),
+ Class.set(), and Class.retire() methods are disabled.
+ """
+ raise NotImplementedError
+
+ def post_init(self):
+ """Called once the schema initialisation has finished.
+ If 'refresh' is true, we want to rebuild the backend
+ structures.
+ """
+ raise NotImplementedError
+
+ def refresh_database(self):
+ """Called to indicate that the backend should rebuild all tables
+ and structures. Not called in normal usage."""
+ raise NotImplementedError
+
+ def __getattr__(self, classname):
+ """A convenient way of calling self.getclass(classname)."""
+ raise NotImplementedError
+
+ def addclass(self, cl):
+ '''Add a Class to the hyperdatabase.
+ '''
+ raise NotImplementedError
+
+ def getclasses(self):
+ """Return a list of the names of all existing classes."""
+ raise NotImplementedError
+
+ def getclass(self, classname):
+ """Get the Class object representing a particular class.
+
+ If 'classname' is not a valid class name, a KeyError is raised.
+ """
+ raise NotImplementedError
+
+ def clear(self):
+ '''Delete all database contents.
+ '''
+ raise NotImplementedError
+
+ def getclassdb(self, classname, mode='r'):
+ '''Obtain a connection to the class db that will be used for
+ multiple actions.
+ '''
+ raise NotImplementedError
+
+ def addnode(self, classname, nodeid, node):
+ """Add the specified node to its class's db.
+ """
+ raise NotImplementedError
+
+ def serialise(self, classname, node):
+ '''Copy the node contents, converting non-marshallable data into
+ marshallable data.
+ '''
+ return node
+
+ def setnode(self, classname, nodeid, node):
+ '''Change the specified node.
+ '''
+ raise NotImplementedError
+
+ def unserialise(self, classname, node):
+ '''Decode the marshalled node data
+ '''
+ return node
+
+ def getnode(self, classname, nodeid):
+ '''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.
+ '''
+ raise NotImplementedError
+
+ def countnodes(self, classname):
+ '''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.
+
+ 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.
+ '''
+ raise NotImplementedError
+
+ def addjournal(self, classname, nodeid, action, params):
+ ''' 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
+ '''
+ raise NotImplementedError
+
+ def pack(self, pack_before):
+ ''' pack the database
+ '''
+ raise NotImplementedError
+
+ def commit(self):
+ ''' Commit the current transactions.
+
+ Save all data changed since the database was opened or since the
+ last commit() or rollback().
+ '''
+ raise NotImplementedError
+
+ def rollback(self):
+ ''' 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):
+ """Close the database.
+
+ This method must be called at the end of processing.
+
+ """
+
+#
+# The base Class class
+#
+class Class:
+ """ The handle to a particular class of nodes in a hyperdatabase.
+
+ All methods except __repr__ and getnode must be implemented by a
+ concrete backend Class.
+ """
+
+ def __init__(self, db, classname, **properties):
+ """Create a new class with a given name and property specification.
+
+ 'classname' must not collide with the name of an existing class,
+ or a ValueError is raised. The keyword arguments in 'properties'
+ must map names to property objects, or a TypeError is raised.
+ """
+ for name in 'creation activity creator actor'.split():
+ if properties.has_key(name):
+ raise ValueError, '"creation", "activity", "creator" and '\
+ '"actor" are reserved'
+
+ self.classname = classname
+ self.properties = properties
+ self.db = weakref.proxy(db) # use a weak ref to avoid circularity
+ self.key = ''
+
+ # should we journal changes (default yes)
+ self.do_journal = 1
+
+ # do the db-related init stuff
+ db.addclass(self)
+
+ actions = "create set retire restore".split ()
+ self.auditors = dict ([(a, PrioList ()) for a in actions])
+ self.reactors = dict ([(a, PrioList ()) for a in actions])
+
+ def __repr__(self):
+ '''Slightly more useful representation
+ '''
+ return '<hyperdb.Class "%s">'%self.classname
+
+ # Editing nodes:
+
+ def create(self, **propvalues):
+ """Create a new node of this class and return its id.
+
+ The keyword arguments in 'propvalues' map property names to values.
+
+ The values of arguments must be acceptable for the types of their
+ corresponding properties or a TypeError is raised.
+
+ If this class has a key property, it must be present and its value
+ must not collide with other key strings or a ValueError is raised.
+
+ Any other properties on this class that are missing from the
+ 'propvalues' dictionary are set to None.
+
+ If an id in a link or multilink property does not refer to a valid
+ node, an IndexError is raised.
+ """
+ raise NotImplementedError
+
+ _marker = []
+ def get(self, nodeid, propname, default=_marker, cache=1):
+ """Get the value of a property on an existing node of this class.
+
+ 'nodeid' must be the id of an existing node of this class or an
+ IndexError is raised. 'propname' must be the name of a property
+ of this class or a KeyError is raised.
+
+ 'cache' exists for backwards compatibility, and is not used.
+ """
+ raise NotImplementedError
+
+ # not in spec
+ def getnode(self, nodeid):
+ ''' 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.
+ '''
+ raise NotImplementedError
+
+ def set(self, nodeid, **propvalues):
+ """Modify a property on an existing node of this class.
+
+ 'nodeid' must be the id of an existing node of this class or an
+ IndexError is raised.
+
+ Each key in 'propvalues' must be the name of a property of this
+ class or a KeyError is raised.
+
+ All values in 'propvalues' must be acceptable types for their
+ corresponding properties or a TypeError is raised.
+
+ If the value of the key property is set, it must not collide with
+ other key strings or a ValueError is raised.
+
+ If the value of a Link or Multilink property contains an invalid
+ node id, a ValueError is raised.
+ """
+ raise NotImplementedError
+
+ def retire(self, nodeid):
+ """Retire a node.
+
+ The properties on the node remain available from the get() method,
+ and the node's id is never reused.
+
+ Retired nodes are not returned by the find(), list(), or lookup()
+ methods, and other nodes may reuse the values of their key properties.
+ """
+ raise NotImplementedError
+
+ def restore(self, nodeid):
+ '''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
+ '''
+ raise NotImplementedError
+
+ def destroy(self, nodeid):
+ """Destroy a node.
+
+ WARNING: this method should never be used except in extremely rare
+ situations where there could never be links to the node being
+ deleted
+
+ WARNING: use retire() instead
+
+ WARNING: the properties of this node will not be available ever again
+
+ WARNING: really, use retire() instead
+
+ Well, I think that's enough warnings. This method exists mostly to
+ support the session storage of the cgi interface.
+
+ The node is completely removed from the hyperdb, including all journal
+ entries. It will no longer be available, and will generally break code
+ if there are any references to the node.
+ """
+
+ def history(self, nodeid):
+ """Retrieve the journal of edits on a particular node.
+
+ 'nodeid' must be the id of an existing node of this class or an
+ IndexError is raised.
+
+ The returned list contains tuples of the form
+
+ (date, tag, action, params)
+
+ 'date' is a Timestamp object specifying the time of the change and
+ 'tag' is the journaltag specified when the database was opened.
+ """
+ raise NotImplementedError
+
+ # Locating nodes:
+ def hasnode(self, nodeid):
+ '''Determine if the given nodeid actually exists
+ '''
+ raise NotImplementedError
+
+ def setkey(self, propname):
+ """Select a String property of this class to be the key property.
+
+ 'propname' must be the name of a String property of this class or
+ None, or a TypeError is raised. The values of the key property on
+ all existing nodes must be unique or a ValueError is raised.
+ """
+ raise NotImplementedError
+
+ def setlabelprop (self, labelprop):
+ """Set the label property. Used for override of labelprop
+ resolution order.
+ """
+ if labelprop not in self.getprops () :
+ raise ValueError, "Not a property name: %s" % labelprop
+ self._labelprop = labelprop
+
+ def setorderprop (self, orderprop):
+ """Set the order property. Used for override of orderprop
+ resolution order
+ """
+ if orderprop not in self.getprops () :
+ raise ValueError, "Not a property name: %s" % orderprop
+ self._orderprop = orderprop
+
+ def getkey(self):
+ """Return the name of the key property for this class or None."""
+ raise NotImplementedError
+
+ def labelprop(self, default_to_id=0):
+ """Return the property name for a label for the given node.
+
+ This method attempts to generate a consistent label for the node.
+ It tries the following in order:
+
+ 0. self._labelprop if set
+ 1. key property
+ 2. "name" property
+ 3. "title" property
+ 4. first property from the sorted property name list
+ """
+ if hasattr (self, '_labelprop') :
+ return self._labelprop
+ k = self.getkey()
+ if k:
+ return k
+ props = self.getprops()
+ if props.has_key('name'):
+ return 'name'
+ elif props.has_key('title'):
+ return 'title'
+ if default_to_id:
+ return 'id'
+ props = props.keys()
+ props.sort()
+ return props[0]
+
+ def orderprop (self):
+ """Return the property name to use for sorting for the given node.
+
+ This method computes the property for sorting.
+ It tries the following in order:
+
+ 0. self._orderprop if set
+ 1. "order" property
+ 2. self.labelprop ()
+ """
+
+ if hasattr (self, '_orderprop') :
+ return self._orderprop
+ props = self.getprops ()
+ if props.has_key ('order'):
+ return 'order'
+ return self.labelprop ()
+
+ def lookup(self, keyvalue):
+ """Locate a particular node by its key property and return its id.
+
+ If this class has no key property, a TypeError is raised. If the
+ 'keyvalue' matches one of the values for the key property among
+ the nodes in this class, the matching node's id is returned;
+ otherwise a KeyError is raised.
+ """
+ raise NotImplementedError
+
+ def find(self, **propspec):
+ """Get the ids of nodes in this class which link to the given nodes.
+
+ 'propspec' consists of keyword args propname={nodeid:1,}
+ 'propname' must be the name of a property in this class, or a
+ KeyError is raised. That property must be a Link or Multilink
+ property, or a TypeError is raised.
+
+ Any node in this class whose 'propname' property links to any of the
+ nodeids will be returned. Used by the full text indexing, which knows
+ that "foo" occurs in msg1, msg3 and file7, so we have hits on these
+ issues:
+
+ db.issue.find(messages={'1':1,'3':1}, files={'7':1})
+ """
+ raise NotImplementedError
+
+ def filter(self, search_matches, filterspec, sort=(None,None),
+ group=(None,None)):
+ """Return a list of the ids of the active nodes in this class that
+ match the 'filter' spec, sorted by the group spec and then the
+ sort spec.
+
+ "filterspec" is {propname: value(s)}
+
+ "sort" and "group" are (dir, prop) where dir is '+', '-' or None
+ and prop is a prop name or None
+
+ "search_matches" is {nodeid: marker}
+
+ The filter must match all properties specificed. If the property
+ value to match is a list:
+
+ 1. String properties must match all elements in the list, and
+ 2. Other properties must match any of the elements in the list.
+ """
+ raise NotImplementedError
+
+ def count(self):
+ """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.
+ """
+ raise NotImplementedError
+
+ # Manipulating properties:
+ def getprops(self, protected=1):
+ """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.
+ """
+ raise NotImplementedError
+
+ def addprop(self, **properties):
+ """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.
+ """
+ raise NotImplementedError
+
+ def index(self, nodeid):
+ """Add (or refresh) the node to search indexes"""
+ raise NotImplementedError
+
+ #
+ # Detector interface
+ #
+ def audit(self, event, detector, priority = 100):
+ """Register an auditor detector"""
+ self.auditors[event].append((priority, detector))
+
+ def fireAuditors(self, event, nodeid, newvalues):
+ """Fire all registered auditors"""
+ for prio, audit in self.auditors[event]:
+ audit(self.db, self, nodeid, newvalues)
+
+ def react(self, event, detector, priority = 100):
+ """Register a reactor detector"""
+ self.reactors[event].append((priority, detector))
+
+ def fireReactors(self, event, nodeid, oldvalues):
+ """Fire all registered reactors"""
+ for prio, react in self.reactors[event]:
+ react(self.db, self, nodeid, oldvalues)
+
+ #
+ # import / export support
+ #
+ def export_propnames(self):
+ """List the property names for export from this Class"""
+ propnames = self.getprops().keys()
+ propnames.sort()
+ return propnames
+
+
+class HyperdbValueError(ValueError):
+ ''' 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. '''
+ linkcl = db.classes[prop.classname]
+ if not idre.match(value):
+ if linkcl.getkey():
+ try:
+ value = linkcl.lookup(value)
+ except KeyError, message:
+ raise HyperdbValueError, 'property %s: %r is not a %s.'%(
+ propname, value, prop.classname)
+ else:
+ raise HyperdbValueError, 'you may only enter ID values '\
+ 'for property %s'%propname
+ return value
+
+def fixNewlines(text):
+ """ Homogenise line endings.
+
+ Different web clients send different line ending values, but
+ other systems (eg. email) don't necessarily handle those line
+ endings. Our solution is to convert all line endings to LF.
+ """
+ text = text.replace('\r\n', '\n')
+ 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
+ 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
+ propname = propname.strip()
+ try:
+ proptype = properties[propname]
+ except KeyError:
+ raise HyperdbValueError, '%r is not a property of %s'%(propname,
+ klass.classname)
+
+ # if we got a string, strip it now
+ if isinstance(value, type('')):
+ value = value.strip()
+ # convert the input value to a real property value
+ value = proptype.from_raw \
+ ( value
+ , db = db
+ , klass = klass
+ , propname = propname
+ , itemid = itemid
+ , **kw
+ )
+ return value
+
+class FileClass:
+ ''' 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"
+ property.
+ '''
+ if not properties.has_key('content'):
+ properties['content'] = hyperdb.String(indexme='yes')
+
+ def export_propnames(self):
+ ''' Don't export the "content" property
+ '''
+ propnames = self.getprops().keys()
+ propnames.remove('content')
+ propnames.sort()
+ return propnames
+
+ def exportFilename(self, dirname, nodeid):
+ subdir_filename = self.db.subdirFilename(self.classname, nodeid)
+ 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
+ '''
+ source = self.db.filename(self.classname, nodeid)
+
+ dest = self.exportFilename(dirname, nodeid)
+ ensureParentsExist(dest)
+ shutil.copyfile(source, dest)
+
+ def import_files(self, dirname, nodeid):
+ ''' Import the "content" property as a file
+ '''
+ source = self.exportFilename(dirname, nodeid)
+
+ dest = self.db.filename(self.classname, nodeid, create=1)
+ ensureParentsExist(dest)
+ shutil.copyfile(source, dest)
+
+ mime_type = None
+ if self.getprops().has_key('type'):
+ mime_type = self.get(nodeid, 'type')
+ if not mime_type:
+ mime_type = self.default_mime_type
+ self.db.indexer.add_text((self.classname, nodeid, 'content'),
+ self.get(nodeid, 'content'), mime_type)
+
+class Node:
+ ''' A convenience wrapper for the given node
+ '''
+ def __init__(self, cl, nodeid, cache=1):
+ self.__dict__['cl'] = cl
+ self.__dict__['nodeid'] = nodeid
+ def keys(self, protected=1):
+ return self.cl.getprops(protected=protected).keys()
+ def values(self, protected=1):
+ l = []
+ for name in self.cl.getprops(protected=protected).keys():
+ l.append(self.cl.get(self.nodeid, name))
+ return l
+ def items(self, protected=1):
+ l = []
+ for name in self.cl.getprops(protected=protected).keys():
+ l.append((name, self.cl.get(self.nodeid, name)))
+ return l
+ def has_key(self, name):
+ return self.cl.getprops().has_key(name)
+ def get(self, name, default=None):
+ if self.has_key(name):
+ return self[name]
+ else:
+ return default
+ def __getattr__(self, name):
+ if self.__dict__.has_key(name):
+ return self.__dict__[name]
+ try:
+ return self.cl.get(self.nodeid, name)
+ except KeyError, value:
+ # we trap this but re-raise it as AttributeError - all other
+ # exceptions should pass through untrapped
+ pass
+ # nope, no such attribute
+ raise AttributeError, str(value)
+ def __getitem__(self, name):
+ return self.cl.get(self.nodeid, name)
+ def __setattr__(self, name, value):
+ try:
+ return self.cl.set(self.nodeid, **{name: value})
+ except KeyError, value:
+ raise AttributeError, str(value)
+ def __setitem__(self, name, value):
+ self.cl.set(self.nodeid, **{name: value})
+ def history(self):
+ return self.cl.history(self.nodeid)
+ def retire(self):
+ return self.cl.retire(self.nodeid)
+
+
+def Choice(name, db, *options):
+ '''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)
+ return hyperdb.Link(name)
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/i18n.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/i18n.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,229 @@
+#
+# 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: i18n.py,v 1.15 2005/06/14 05:33:32 a1s Exp $
+
+"""
+RoundUp Internationalization (I18N)
+
+To use this module, the following code should be used::
+
+ from roundup.i18n import _
+ ...
+ print _("Some text that can be translated")
+
+Note that to enable re-ordering of inserted texts in formatting strings
+(which can easily happen if a sentence has to be re-ordered due to
+grammatical changes), translatable formats should use named format specs::
+
+ ... _('Index of %(classname)s') % {'classname': cn} ...
+
+Also, this eases the job of translators since they have some context what
+the dynamic portion of a message really means.
+"""
+__docformat__ = 'restructuredtext'
+
+import errno
+import gettext as gettext_module
+import os
+
+from roundup import msgfmt
+
+# List of directories for mo file search (see SF bug 1219689)
+LOCALE_DIRS = [
+ gettext_module._default_localedir,
+]
+# compute mo location relative to roundup installation directory
+# (prefix/lib/python/site-packages/roundup/msgfmt.py on posix systems,
+# prefix/lib/site-packages/roundup/msgfmt.py on windows).
+# locale root is prefix/share/locale.
+if os.name == "nt":
+ _mo_path = [".."] * 4 + ["share", "locale"]
+else:
+ _mo_path = [".."] * 5 + ["share", "locale"]
+_mo_path = os.path.normpath(os.path.join(msgfmt.__file__, *_mo_path))
+if _mo_path not in LOCALE_DIRS:
+ LOCALE_DIRS.append(_mo_path)
+del _mo_path
+
+# Roundup text domain
+DOMAIN = "roundup"
+
+if hasattr(gettext_module.GNUTranslations, "ngettext"):
+ # gettext_module has everything needed
+ RoundupNullTranslations = gettext_module.NullTranslations
+ RoundupTranslations = gettext_module.GNUTranslations
+else:
+ # prior to 2.3, there was no plural forms. mix simple emulation in
+ class PluralFormsMixIn:
+ def ngettext(self, singular, plural, count):
+ if count == 1:
+ _msg = singular
+ else:
+ _msg = plural
+ return self.gettext(_msg)
+ def ungettext(self, singular, plural, count):
+ if count == 1:
+ _msg = singular
+ else:
+ _msg = plural
+ return self.ugettext(_msg)
+ class RoundupNullTranslations(
+ gettext_module.NullTranslations, PluralFormsMixIn
+ ):
+ pass
+ class RoundupTranslations(
+ gettext_module.GNUTranslations, PluralFormsMixIn
+ ):
+ pass
+
+def find_locales(language=None):
+ """Return normalized list of locale names to try for given language
+
+ Argument 'language' may be a single language code or a list of codes.
+ If 'language' is omitted or None, use locale settings in OS environment.
+
+ """
+ # body of this function is borrowed from gettext_module.find()
+ if language is None:
+ languages = []
+ for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
+ val = os.environ.get(envar)
+ if val:
+ languages = val.split(':')
+ break
+ elif isinstance(language, str) or isinstance(language, unicode):
+ languages = [language]
+ else:
+ # 'language' must be iterable
+ languages = language
+ # now normalize and expand the languages
+ nelangs = []
+ for lang in languages:
+ for nelang in gettext_module._expand_lang(lang):
+ if nelang not in nelangs:
+ nelangs.append(nelang)
+ return nelangs
+
+def get_mofile(languages, localedir, domain=None):
+ """Return the first of .mo files found in localedir for languages
+
+ Parameters:
+ languages:
+ list of locale names to try
+ localedir:
+ path to directory containing locale files.
+ Usually this is either gettext_module._default_localedir
+ or 'locale' subdirectory in the tracker home.
+ domain:
+ optional name of messages domain.
+ If omitted or None, work with simplified
+ locale directory, as used in tracker homes:
+ message catalogs are kept in files locale.po
+ instead of locale/LC_MESSAGES/domain.po
+
+ Return the path of the first .mo file found.
+ If nothing found, return None.
+
+ Automatically compile .po files if necessary.
+
+ """
+ for locale in languages:
+ if locale == "C":
+ break
+ if domain:
+ basename = os.path.join(localedir, locale, "LC_MESSAGES", domain)
+ else:
+ basename = os.path.join(localedir, locale)
+ # look for message catalog files, check timestamps
+ mofile = basename + ".mo"
+ if os.path.isfile(mofile):
+ motime = os.path.getmtime(mofile)
+ else:
+ motime = 0
+ pofile = basename + ".po"
+ if os.path.isfile(pofile):
+ potime = os.path.getmtime(pofile)
+ else:
+ potime = 0
+ # see what we've found
+ if motime < potime:
+ # compile
+ msgfmt.make(pofile, mofile)
+ elif motime == 0:
+ # no files found - proceed to the next locale name
+ continue
+ # .mo file found or made
+ return mofile
+ return None
+
+def get_translation(language=None, tracker_home=None,
+ translation_class=RoundupTranslations,
+ null_translation_class=RoundupNullTranslations
+):
+ """Return Translation object for given language and domain
+
+ Argument 'language' may be a single language code or a list of codes.
+ If 'language' is omitted or None, use locale settings in OS environment.
+
+ Arguments 'translation_class' and 'null_translation_class'
+ specify the classes that are instantiated for existing
+ and non-existing translations, respectively.
+
+ """
+ mofiles = []
+ # locale directory paths
+ if tracker_home is None:
+ tracker_locale = None
+ else:
+ tracker_locale = os.path.join(tracker_home, "locale")
+ # get the list of locales
+ locales = find_locales(language)
+ # add mofiles found in the tracker, then in the system locale directory
+ if tracker_locale:
+ mofiles.append(get_mofile(locales, tracker_locale))
+ for system_locale in LOCALE_DIRS:
+ mofiles.append(get_mofile(locales, system_locale, DOMAIN))
+ # we want to fall back to english unless english is selected language
+ if "en" not in locales:
+ locales = find_locales("en")
+ # add mofiles found in the tracker, then in the system locale directory
+ if tracker_locale:
+ mofiles.append(get_mofile(locales, tracker_locale))
+ for system_locale in LOCALE_DIRS:
+ mofiles.append(get_mofile(locales, system_locale, DOMAIN))
+ # filter out elements that are not found
+ mofiles = filter(None, mofiles)
+ if mofiles:
+ translator = translation_class(open(mofiles[0], "rb"))
+ for mofile in mofiles[1:]:
+ # note: current implementation of gettext_module
+ # always adds fallback to the end of the fallback chain.
+ translator.add_fallback(translation_class(open(mofile, "rb")))
+ else:
+ translator = null_translation_class()
+ return translator
+
+# static translations object
+translation = get_translation()
+# static translation functions
+_ = gettext = translation.gettext
+ugettext = translation.ugettext
+ngettext = translation.ngettext
+ungettext = translation.ungettext
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/init.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/init.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,189 @@
+#
+# 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: init.py,v 1.36 2005/12/03 11:22:50 a1s Exp $
+
+"""Init (create) a roundup instance.
+"""
+__docformat__ = 'restructuredtext'
+
+import os, errno, rfc822
+
+from roundup import install_util, password
+from roundup.configuration import CoreConfig
+from roundup.i18n import _
+
+def copytree(src, dst, symlinks=0):
+ """Recursively copy a directory tree using copyDigestedFile().
+
+ The destination directory os 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
+ it is false, the contents of the files pointed to by symbolic
+ links are copied.
+
+ This was copied from shutil.py in std lib.
+ """
+ names = os.listdir(src)
+ try:
+ os.mkdir(dst)
+ except OSError, error:
+ if error.errno != errno.EEXIST: raise
+ for name in names:
+ srcname = os.path.join(src, name)
+ dstname = os.path.join(dst, name)
+ if symlinks and os.path.islink(srcname):
+ linkto = os.readlink(srcname)
+ os.symlink(linkto, dstname)
+ elif os.path.isdir(srcname):
+ copytree(srcname, dstname, symlinks)
+ else:
+ install_util.copyDigestedFile(srcname, dstname)
+
+def install(instance_home, template, settings={}):
+ '''Install an instance using the named template and backend.
+
+ 'instance_home'
+ the directory to place the instance data in
+ 'template'
+ the directory holding the template to use in creating the instance data
+ 'settings'
+ config.ini setting overrides (dictionary)
+
+ The instance_home directory will be created using the files found in
+ the named template (roundup.templates.<name>). A usual instance_home
+ contains:
+
+ config.ini
+ tracker configuration file
+ schema.py
+ database schema definition
+ initial_data.py
+ database initialization script, used to populate the database
+ with 'roundup-admin init' command
+ interfaces.py
+ (optional, not installed from standard templates) defines
+ the CGI Client and mail gateway MailGW classes that are
+ used by roundup.cgi, roundup-server and roundup-mailgw.
+ db/
+ the actual database that stores the instance's data
+ html/
+ the html templates that are used by the CGI Client
+ detectors/
+ the auditor and reactor modules for this instance
+ extensions/
+ code extensions to Roundup
+ '''
+ # At the moment, it's just a copy
+ copytree(template, instance_home)
+
+ # rename the tempate in the TEMPLATE-INFO.txt file
+ ti = loadTemplateInfo(instance_home)
+ ti['name'] = ti['name'] + '-' + os.path.split(instance_home)[1]
+ saveTemplateInfo(instance_home, ti)
+
+ # if there is no config.ini or old-style config.py
+ # installed from the template, write default config text
+ config_ini_file = os.path.join(instance_home, CoreConfig.INI_FILE)
+ if not os.path.isfile(config_ini_file):
+ config = CoreConfig(settings=settings)
+ config.save(config_ini_file)
+
+
+def listTemplates(dir):
+ ''' List all the Roundup template directories in a given directory.
+
+ Find all the dirs that contain a TEMPLATE-INFO.txt and parse it.
+
+ Return a list of dicts of info about the templates.
+ '''
+ ret = {}
+ for idir in os.listdir(dir):
+ idir = os.path.join(dir, idir)
+ ti = loadTemplateInfo(idir)
+ if ti:
+ ret[ti['name']] = ti
+ return ret
+
+def loadTemplateInfo(dir):
+ ''' Attempt to load a Roundup template from the indicated directory.
+
+ Return None if there's no template, otherwise a template info
+ dictionary.
+ '''
+ ti = os.path.join(dir, 'TEMPLATE-INFO.txt')
+ if not os.path.exists(ti):
+ return None
+
+ if os.path.exists(os.path.join(dir, 'config.py')):
+ print _("WARNING: directory '%s'\n"
+ "\tcontains old-style template - ignored"
+ ) % os.path.abspath(dir)
+ return None
+
+ # load up the template's information
+ f = open(ti)
+ try:
+ m = rfc822.Message(open(ti))
+ ti = {}
+ ti['name'] = m['name']
+ ti['description'] = m['description']
+ ti['intended-for'] = m['intended-for']
+ ti['path'] = dir
+ finally:
+ f.close()
+ return ti
+
+def writeHeader(name, value):
+ ''' Write an rfc822-compatible header line, making it wrap reasonably
+ '''
+ out = [name.capitalize() + ':']
+ n = len(out[0])
+ for word in value.split():
+ if len(word) + n > 74:
+ out.append('\n')
+ n = 0
+ out.append(' ' + word)
+ n += len(out[-1])
+ return ''.join(out) + '\n'
+
+def saveTemplateInfo(dir, info):
+ ''' Save the template info (dict of values) to the TEMPLATE-INFO.txt
+ file in the indicated directory.
+ '''
+ ti = os.path.join(dir, 'TEMPLATE-INFO.txt')
+ f = open(ti, 'w')
+ try:
+ for name in 'name description intended-for path'.split():
+ f.write(writeHeader(name, info[name]))
+ finally:
+ f.close()
+
+def write_select_db(instance_home, backend):
+ ''' Write the file that selects the backend for the tracker
+ '''
+ dbdir = os.path.join(instance_home, 'db')
+ if not os.path.exists(dbdir):
+ os.makedirs(dbdir)
+ f = open(os.path.join(dbdir, 'backend_name'), 'w')
+ f.write(backend+'\n')
+ f.close()
+
+
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/install_util.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/install_util.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,158 @@
+#
+# 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: 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
+
+sgml_file_types = [".xml", ".ent", ".html"]
+hash_file_types = [".py", ".sh", ".conf", ".cgi"]
+slast_file_types = [".css"]
+
+digested_file_types = sgml_file_types + hash_file_types + slast_file_types
+
+def extractFingerprint(lines):
+ # get fingerprint from last line
+ if lines[-1].startswith("#SHA: "):
+ # handle .py/.sh comment
+ return lines[-1][6:].strip()
+ elif lines[-1].startswith("<!-- SHA: "):
+ # handle xml/html files
+ fingerprint = lines[-1][10:]
+ fingerprint = fingerprint.replace('-->', '')
+ return fingerprint.strip()
+ elif lines[-1].startswith("/* SHA: "):
+ # handle css files
+ fingerprint = lines[-1][8:]
+ fingerprint = fingerprint.replace('*/', '')
+ return fingerprint.strip()
+ return None
+
+def checkDigest(filename):
+ """Read file, check for valid fingerprint, return TRUE if ok"""
+ # open and read file
+ inp = open(filename, "r")
+ lines = inp.readlines()
+ inp.close()
+
+ fingerprint = extractFingerprint(lines)
+ if fingerprint is None:
+ return 0
+ del lines[-1]
+
+ # calculate current digest
+ digest = sha.new()
+ for line in lines:
+ digest.update(line)
+
+ # compare current to stored digest
+ return fingerprint == digest.hexdigest()
+
+
+class DigestFile:
+ """ A class that you can use like open() and that calculates
+ and writes a SHA digest to the target file.
+ """
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.digest = sha.new()
+ self.file = open(self.filename, "w")
+
+ def write(self, data):
+ lines = data.splitlines()
+ # if the file is coming from an installed tracker being used as a
+ # template, then we will want to re-calculate the SHA
+ fingerprint = extractFingerprint(lines)
+ if fingerprint is not None:
+ data = '\n'.join(lines[:-1]) + '\n'
+ self.file.write(data)
+ self.digest.update(data)
+
+ def close(self):
+ file, ext = os.path.splitext(self.filename)
+
+ if ext in sgml_file_types:
+ self.file.write("<!-- SHA: %s -->\n" % (self.digest.hexdigest(),))
+ elif ext in hash_file_types:
+ self.file.write("#SHA: %s\n" % (self.digest.hexdigest(),))
+ elif ext in slast_file_types:
+ self.file.write("/* SHA: %s */\n" % (self.digest.hexdigest(),))
+
+ self.file.close()
+
+
+def copyDigestedFile(src, dst, copystat=1):
+ """ Copy data from `src` to `dst`, adding a fingerprint to `dst`.
+ If `copystat` is true, the file status is copied, too
+ (like shutil.copy2).
+ """
+ if os.path.isdir(dst):
+ dst = os.path.join(dst, os.path.basename(src))
+
+ dummy, ext = os.path.splitext(src)
+ if ext not in digested_file_types:
+ if copystat:
+ return shutil.copy2(src, dst)
+ else:
+ return shutil.copyfile(src, dst)
+
+ fsrc = None
+ fdst = None
+ try:
+ fsrc = open(src, 'r')
+ fdst = DigestFile(dst)
+ shutil.copyfileobj(fsrc, fdst)
+ finally:
+ if fdst: fdst.close()
+ if fsrc: fsrc.close()
+
+ if copystat: shutil.copystat(src, dst)
+
+
+def test():
+ import sys
+
+ testdata = open(sys.argv[0], 'r').read()
+
+ for ext in digested_file_types:
+ testfile = "__digest_test" + ext
+
+ out = DigestFile(testfile)
+ out.write(testdata)
+ out.close()
+
+ assert checkDigest(testfile), "digest ok w/o modification"
+
+ mod = open(testfile, 'r+')
+ mod.seek(0)
+ mod.write('# changed!')
+ mod.close()
+
+ assert not checkDigest(testfile), "digest fails after modification"
+
+ os.remove(testfile)
+
+
+if __name__ == '__main__':
+ test()
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/roundup/instance.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/instance.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,246 @@
+#
+# 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: instance.py,v 1.33 2004/11/29 02:55:46 richard Exp $
+
+'''Tracker handling (open tracker).
+
+Backwards compatibility for the old-style "imported" trackers.
+'''
+__docformat__ = 'restructuredtext'
+
+import os
+from roundup import configuration, mailgw
+from roundup import hyperdb, backends
+from roundup.cgi import client, templating
+
+class Vars:
+ def __init__(self, vars):
+ self.__dict__.update(vars)
+
+class Tracker:
+ def __init__(self, tracker_home, optimize=0):
+ """New-style tracker instance constructor
+
+ Parameters:
+ tracker_home:
+ tracker home directory
+ optimize:
+ if set, precompile html templates
+
+ """
+ self.tracker_home = tracker_home
+ self.optimize = optimize
+ self.config = configuration.CoreConfig(tracker_home)
+ self.cgi_actions = {}
+ self.templating_utils = {}
+ self.load_interfaces()
+ self.templates = templating.Templates(self.config["TEMPLATES"])
+ self.backend = backends.get_backend(self.get_backend_name())
+ if self.optimize:
+ self.templates.precompileTemplates()
+ # initialize tracker extensions
+ for extension in self.get_extensions('extensions'):
+ extension(self)
+ # load database schema
+ schemafilename = os.path.join(self.tracker_home, 'schema.py')
+ # Note: can't use built-in open()
+ # because of the global function with the same name
+ schemafile = file(schemafilename, 'rt')
+ self.schema = compile(schemafile.read(), schemafilename, 'exec')
+ schemafile.close()
+ # load database detectors
+ self.detectors = self.get_extensions('detectors')
+ # db_open is set to True after first open()
+ self.db_open = 0
+
+ def get_backend_name(self):
+ o = __builtins__['open']
+ f = o(os.path.join(self.tracker_home, 'db', 'backend_name'))
+ name = f.readline().strip()
+ f.close()
+ return name
+
+ def open(self, name=None):
+ # load the database schema
+ # we cannot skip this part even if self.optimize is set
+ # because the schema has security settings that must be
+ # applied to each database instance
+ backend = self.backend
+ vars = {
+ 'Class': backend.Class,
+ 'FileClass': backend.FileClass,
+ 'IssueClass': backend.IssueClass,
+ 'String': hyperdb.String,
+ 'Password': hyperdb.Password,
+ 'Date': hyperdb.Date,
+ 'Link': hyperdb.Link,
+ 'Multilink': hyperdb.Multilink,
+ 'Interval': hyperdb.Interval,
+ 'Boolean': hyperdb.Boolean,
+ 'Number': hyperdb.Number,
+ 'db': backend.Database(self.config, name)
+ }
+
+ if self.optimize:
+ # execute preloaded schema object
+ exec(self.schema, vars)
+ # use preloaded detectors
+ detectors = self.detectors
+ else:
+ # execute the schema file
+ self._load_python('schema.py', vars)
+ # reload extensions and detectors
+ for extension in self.get_extensions('extensions'):
+ extension(self)
+ detectors = self.get_extensions('detectors')
+ db = vars['db']
+ # apply the detectors
+ for detector in detectors:
+ detector(db)
+ # if we are running in debug mode
+ # or this is the first time the database is opened,
+ # do database upgrade checks
+ if not (self.optimize and self.db_open):
+ db.post_init()
+ self.db_open = 1
+ return db
+
+ def load_interfaces(self):
+ """load interfaces.py (if any), initialize Client and MailGW attrs"""
+ vars = {}
+ if os.path.isfile(os.path.join(self.tracker_home, 'interfaces.py')):
+ self._load_python('interfaces.py', vars)
+ self.Client = vars.get('Client', client.Client)
+ self.MailGW = vars.get('MailGW', mailgw.MailGW)
+
+ def get_extensions(self, dirname):
+ """Load python extensions
+
+ Parameters:
+ dirname:
+ extension directory name relative to tracker home
+
+ Return value:
+ list of init() functions for each extension
+
+ """
+ extensions = []
+ dirpath = os.path.join(self.tracker_home, dirname)
+ if os.path.isdir(dirpath):
+ for name in os.listdir(dirpath):
+ if not name.endswith('.py'):
+ continue
+ vars = {}
+ self._load_python(os.path.join(dirname, name), vars)
+ extensions.append(vars['init'])
+ return extensions
+
+ def init(self, adminpw):
+ db = self.open('admin')
+ self._load_python('initial_data.py', {'db': db, 'adminpw': adminpw,
+ 'admin_email': self.config['ADMIN_EMAIL']})
+ db.commit()
+ db.close()
+
+ def exists(self):
+ return self.backend.db_exists(self.config)
+
+ def nuke(self):
+ self.backend.db_nuke(self.config)
+
+ def _load_python(self, file, vars):
+ file = os.path.join(self.tracker_home, file)
+ execfile(file, vars)
+ return vars
+
+ def registerAction(self, name, action):
+ self.cgi_actions[name] = action
+
+ def registerUtil(self, name, function):
+ self.templating_utils[name] = function
+
+class TrackerError(Exception):
+ pass
+
+
+class OldStyleTrackers:
+ def __init__(self):
+ self.number = 0
+ self.trackers = {}
+
+ def open(self, tracker_home, optimize=0):
+ """Open the tracker.
+
+ Parameters:
+ tracker_home:
+ tracker home directory
+ optimize:
+ if set, precompile html templates
+
+ Raise ValueError if the tracker home doesn't exist.
+
+ """
+ import imp
+ # sanity check existence of tracker home
+ if not os.path.exists(tracker_home):
+ raise ValueError, 'no such directory: "%s"'%tracker_home
+
+ # sanity check tracker home contents
+ for reqd in 'config dbinit select_db interfaces'.split():
+ if not os.path.exists(os.path.join(tracker_home, '%s.py'%reqd)):
+ raise TrackerError, 'File "%s.py" missing from tracker '\
+ 'home "%s"'%(reqd, tracker_home)
+
+ if self.trackers.has_key(tracker_home):
+ return imp.load_package(self.trackers[tracker_home],
+ tracker_home)
+ # register all available backend modules
+ backends.list_backends()
+ self.number = self.number + 1
+ modname = '_roundup_tracker_%s'%self.number
+ self.trackers[tracker_home] = modname
+
+ # load the tracker
+ tracker = imp.load_package(modname, tracker_home)
+
+ # ensure the tracker has all the required bits
+ for required in 'open init Client MailGW'.split():
+ if not hasattr(tracker, required):
+ raise TrackerError, \
+ 'Required tracker attribute "%s" missing'%required
+
+ # load and apply the config
+ tracker.config = configuration.CoreConfig(tracker_home)
+ tracker.dbinit.config = tracker.config
+
+ tracker.optimize = optimize
+ tracker.templates = templating.Templates(tracker.config["TEMPLATES"])
+ if optimize:
+ tracker.templates.precompileTemplates()
+
+ return tracker
+
+OldStyleTrackers = OldStyleTrackers()
+def open(tracker_home, optimize=0):
+ if os.path.exists(os.path.join(tracker_home, 'dbinit.py')):
+ # user should upgrade...
+ return OldStyleTrackers.open(tracker_home, optimize=optimize)
+
+ return Tracker(tracker_home, optimize=optimize)
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/mailer.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/mailer.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,215 @@
+"""Sending Roundup-specific mail over SMTP.
+"""
+__docformat__ = 'restructuredtext'
+# $Id: mailer.py,v 1.17 2006/02/21 05:48:23 a1s Exp $
+
+import time, quopri, os, socket, smtplib, re, sys, traceback
+
+from cStringIO import StringIO
+from MimeWriter import MimeWriter
+
+from roundup.rfc2822 import encode_header
+from roundup import __version__
+
+try:
+ from email.Utils import formatdate
+except ImportError:
+ def formatdate():
+ return time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())
+
+class MessageSendError(RuntimeError):
+ pass
+
+class Mailer:
+ """Roundup-specific mail sending."""
+ def __init__(self, config):
+ self.config = config
+
+ # set to indicate to roundup not to actually _send_ email
+ # this var must contain a file to write the mail to
+ self.debug = os.environ.get('SENDMAILDEBUG', '') \
+ or config["MAIL_DEBUG"]
+
+ def get_standard_message(self, to, subject, author=None):
+ '''Form a standard email message from Roundup.
+
+ "to" - recipients list
+ "subject" - Subject
+ "author" - (name, address) tuple or None for admin email
+
+ Subject and author are encoded using the EMAIL_CHARSET from the
+ config (default UTF-8).
+
+ Returns a Message object and body part writer.
+ '''
+ # 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)
+ if not author:
+ author = straddr((tracker_name, self.config.ADMIN_EMAIL))
+ 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())
+
+ # Add a unique Roundup header to help filtering
+ writer.addheader('X-Roundup-Name', encode_header(tracker_name,
+ charset))
+ # and another one to avoid loops
+ writer.addheader('X-Roundup-Loop', 'hello')
+ # finally, an aid to debugging problems
+ writer.addheader('X-Roundup-Version', __version__)
+
+ writer.addheader('MIME-Version', '1.0')
+
+ return message, writer
+
+ def standard_message(self, to, subject, content, author=None):
+ """Send a standard message.
+
+ Arguments:
+ - to: a list of addresses usable by rfc822.parseaddr().
+ - 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)
+
+ def bounce_message(self, bounced_message, to, error,
+ subject='Failed issue tracker submission'):
+ """Bounce a message, attaching the failed submission.
+
+ Arguments:
+ - bounced_message: an RFC822 Message object.
+ - to: a list of addresses usable by rfc822.parseaddr(). Might be
+ extended or overridden according to the config
+ ERROR_MESSAGES_TO setting.
+ - error: the reason of failure as a string.
+ - subject: the subject as a string.
+
+ """
+ # see whether we should send to the dispatcher or not
+ dispatcher_email = getattr(self.config, "DISPATCHER_EMAIL",
+ getattr(self.config, "ADMIN_EMAIL"))
+ error_messages_to = getattr(self.config, "ERROR_MESSAGES_TO", "user")
+ if error_messages_to == "dispatcher":
+ to = [dispatcher_email]
+ elif error_messages_to == "both":
+ to.append(dispatcher_email)
+
+ message, writer = 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)))
+
+ # 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:
+ body.write("*** couldn't include message body: %s ***"
+ % bounced_message)
+ else:
+ body.write(bounced_message.fp.read())
+
+ writer.lastpart()
+
+ self.smtp_send(to, message)
+
+ 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, content)
+
+ def smtp_send(self, to, message):
+ """Send a message over SMTP, using roundup's config.
+
+ Arguments:
+ - to: a list of addresses usable by rfc822.parseaddr().
+ - message: a StringIO instance with a full message.
+ """
+ if self.debug:
+ # 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()))
+ 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())
+ except socket.error, value:
+ raise MessageSendError("Error: couldn't send email: "
+ "mailhost %s"%value)
+ except smtplib.SMTPException, msg:
+ raise MessageSendError("Error: couldn't send email: %s"%msg)
+
+class SMTPConnection(smtplib.SMTP):
+ ''' Open an SMTP connection to the mailhost specified in the config
+ '''
+ def __init__(self, config):
+
+ smtplib.SMTP.__init__(self, config.MAILHOST)
+
+ # start the TLS if requested
+ if config["MAIL_TLS"]:
+ self.starttls(config["MAIL_TLS_KEYFILE"],
+ config["MAIL_TLS_CERTFILE"])
+
+ # ok, now do we also need to log in?
+ mailuser = config["MAIL_USERNAME"]
+ 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 :
Added: tracker/vendor/roundup/current/roundup/mailgw.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/mailgw.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,1237 @@
+#
+# 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.
+#
+
+"""An e-mail gateway for Roundup.
+
+Incoming messages are examined for multiple parts:
+ . In a multipart/mixed message or part, each subpart is extracted and
+ examined. The text/plain subparts are assembled to form the textual
+ body of the message, to be stored in the file associated with a "msg"
+ class node. Any parts of other types are each stored in separate files
+ and given "file" class nodes that are linked to the "msg" node.
+ . In a multipart/alternative message or part, we look for a text/plain
+ subpart and ignore the other parts.
+
+Summary
+-------
+The "summary" property on message nodes is taken from the first non-quoting
+section in the message body. The message body is divided into sections by
+blank lines. Sections where the second and all subsequent lines begin with
+a ">" or "|" character are considered "quoting sections". The first line of
+the first non-quoting section becomes the summary of the message.
+
+Addresses
+---------
+All of the addresses in the To: and Cc: headers of the incoming message are
+looked up among the user nodes, and the corresponding users are placed in
+the "recipients" property on the new "msg" node. The address in the From:
+header similarly determines the "author" property of the new "msg"
+node. The default handling for addresses that don't have corresponding
+users is to create new users with no passwords and a username equal to the
+address. (The web interface does not permit logins for users with no
+passwords.) If we prefer to reject mail from outside sources, we can simply
+register an auditor on the "user" class that prevents the creation of user
+nodes with no passwords.
+
+Actions
+-------
+The subject line of the incoming message is examined to determine whether
+the message is an attempt to create a new item or to discuss an existing
+item. A designator enclosed in square brackets is sought as the first thing
+on the subject line (after skipping any "Fwd:" or "Re:" prefixes).
+
+If an item designator (class name and id number) is found there, the newly
+created "msg" node is added to the "messages" property for that item, and
+any new "file" nodes are added to the "files" property for the item.
+
+If just an item class name is found there, we attempt to create a new item
+of that class with its "messages" property initialized to contain the new
+"msg" node and its "files" property initialized to contain any new "file"
+nodes.
+
+Triggers
+--------
+Both cases may trigger detectors (in the first case we are calling the
+set() method to add the message to the item's spool; in the second case we
+are calling the create() method to create a new node). If an auditor raises
+an exception, the original message is bounced back to the sender with the
+explanatory message given in the exception.
+
+$Id: mailgw.py,v 1.175 2006/04/06 06:01:35 a1s Exp $
+"""
+__docformat__ = 'restructuredtext'
+
+import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
+import time, random, sys, logging
+import traceback, MimeWriter, rfc822
+
+from roundup import hyperdb, date, password, rfc2822, exceptions
+from roundup.mailer import Mailer, MessageSendError
+from roundup.i18n import _
+
+SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
+
+class MailGWError(ValueError):
+ pass
+
+class MailUsageError(ValueError):
+ pass
+
+class MailUsageHelp(Exception):
+ """ We need to send the help message to the user. """
+ pass
+
+class Unauthorized(Exception):
+ """ Access denied """
+ pass
+
+class IgnoreMessage(Exception):
+ """ A general class of message that we should ignore. """
+ pass
+class IgnoreBulk(IgnoreMessage):
+ """ This is email from a mailing list or from a vacation program. """
+ pass
+class IgnoreLoop(IgnoreMessage):
+ """ We've seen this message before... """
+ pass
+
+def initialiseSecurity(security):
+ ''' Create some Permissions and Roles on the security object
+
+ This function is directly invoked by security.Security.__init__()
+ as a part of the Security object instantiation.
+ '''
+ p = security.addPermission(name="Email Access",
+ description="User may use the email interface")
+ security.addPermissionToRole('Admin', p)
+
+def getparam(str, param):
+ ''' From the rfc822 "header" string, extract "param" if it appears.
+ '''
+ if ';' not in str:
+ return None
+ str = str[str.index(';'):]
+ while str[:1] == ';':
+ str = str[1:]
+ if ';' in str:
+ # XXX Should parse quotes!
+ end = str.index(';')
+ else:
+ end = len(str)
+ f = str[:end]
+ if '=' in f:
+ i = f.index('=')
+ if f[:i].strip().lower() == param:
+ return rfc822.unquote(f[i+1:].strip())
+ return None
+
+class Message(mimetools.Message):
+ ''' subclass mimetools.Message so we can retrieve the parts of the
+ message...
+ '''
+ def getpart(self):
+ ''' Get a single part of a multipart message and return it as a new
+ Message instance.
+ '''
+ boundary = self.getparam('boundary')
+ mid, end = '--'+boundary, '--'+boundary+'--'
+ s = cStringIO.StringIO()
+ while 1:
+ line = self.fp.readline()
+ if not line:
+ break
+ if line.strip() in (mid, end):
+ break
+ s.write(line)
+ if not s.getvalue().strip():
+ return None
+ s.seek(0)
+ return Message(s)
+
+ def getparts(self):
+ """Get all parts of this multipart message."""
+ # skip over the intro to the first boundary
+ self.getpart()
+
+ # accumulate the other parts
+ parts = []
+ while 1:
+ part = self.getpart()
+ if part is None:
+ break
+ parts.append(part)
+ return parts
+
+ def getheader(self, name, default=None):
+ hdr = mimetools.Message.getheader(self, name, default)
+ if hdr:
+ hdr = hdr.replace('\n','') # Inserted by rfc822.readheaders
+ return rfc2822.decode_header(hdr)
+
+ def getname(self):
+ """Find an appropriate name for this message."""
+ if self.gettype() == 'message/rfc822':
+ # handle message/rfc822 specially - the name should be
+ # the subject of the actual e-mail embedded here
+ self.fp.seek(0)
+ name = Message(self.fp).getheader('subject')
+ else:
+ # try name on Content-Type
+ name = self.getparam('name')
+ if not name:
+ disp = self.getheader('content-disposition', None)
+ if disp:
+ name = getparam(disp, 'filename')
+
+ if name:
+ return name.strip()
+
+ def getbody(self):
+ """Get the decoded message body."""
+ self.rewindbody()
+ encoding = self.getencoding()
+ data = None
+ if encoding == 'base64':
+ # BUG: is base64 really used for text encoding or
+ # are we inserting zip files here.
+ data = binascii.a2b_base64(self.fp.read())
+ elif encoding == 'quoted-printable':
+ # the quopri module wants to work with files
+ decoded = cStringIO.StringIO()
+ quopri.decode(self.fp, decoded)
+ data = decoded.getvalue()
+ elif encoding == 'uuencoded':
+ data = binascii.a2b_uu(self.fp.read())
+ else:
+ # take it as text
+ data = self.fp.read()
+
+ # Encode message to unicode
+ charset = rfc2822.unaliasCharset(self.getparam("charset"))
+ if charset:
+ # Do conversion only if charset specified - handle
+ # badly-specified charsets
+ edata = unicode(data, charset, 'replace').encode('utf-8')
+ # Convert from dos eol to unix
+ edata = edata.replace('\r\n', '\n')
+ else:
+ # Leave message content as is
+ edata = data
+
+ return edata
+
+ # General multipart handling:
+ # Take the first text/plain part, anything else is considered an
+ # attachment.
+ # multipart/mixed:
+ # Multiple "unrelated" parts.
+ # multipart/Alternative (rfc 1521):
+ # Like multipart/mixed, except that we'd only want one of the
+ # alternatives. Generally a top-level part from MUAs sending HTML
+ # mail - there will be a text/plain version.
+ # multipart/signed (rfc 1847):
+ # The control information is carried in the second of the two
+ # required body parts.
+ # ACTION: Default, so if content is text/plain we get it.
+ # multipart/encrypted (rfc 1847):
+ # The control information is carried in the first of the two
+ # required body parts.
+ # ACTION: Not handleable as the content is encrypted.
+ # multipart/related (rfc 1872, 2112, 2387):
+ # The Multipart/Related content-type addresses the MIME
+ # representation of compound objects, usually HTML mail with embedded
+ # images. Usually appears as an alternative.
+ # ACTION: Default, if we must.
+ # multipart/report (rfc 1892):
+ # e.g. mail system delivery status reports.
+ # ACTION: Default. Could be ignored or used for Delivery Notification
+ # flagging.
+ # multipart/form-data:
+ # For web forms only.
+
+ def extract_content(self, parent_type=None):
+ """Extract the body and the attachments recursively."""
+ content_type = self.gettype()
+ content = None
+ attachments = []
+
+ if content_type == 'text/plain':
+ content = self.getbody()
+ elif content_type[:10] == 'multipart/':
+ for part in self.getparts():
+ new_content, new_attach = part.extract_content(content_type)
+
+ # If we haven't found a text/plain part yet, take this one,
+ # otherwise make it an attachment.
+ if not content:
+ content = new_content
+ elif new_content:
+ attachments.append(part.as_attachment())
+
+ attachments.extend(new_attach)
+ elif (parent_type == 'multipart/signed' and
+ content_type == 'application/pgp-signature'):
+ # ignore it so it won't be saved as an attachment
+ pass
+ else:
+ attachments.append(self.as_attachment())
+ return content, attachments
+
+ def as_attachment(self):
+ """Return this message as an attachment."""
+ return (self.getname(), self.gettype(), self.getbody())
+
+class MailGW:
+
+ def __init__(self, instance, db, arguments=()):
+ self.instance = instance
+ self.db = db
+ self.arguments = arguments
+ self.default_class = None
+ for option, value in self.arguments:
+ if option == '-c':
+ self.default_class = value.strip()
+
+ self.mailer = Mailer(instance.config)
+ self.logger = logging.getLogger('mailgw')
+
+ # should we trap exceptions (normal usage) or pass them through
+ # (for testing)
+ self.trapExceptions = 1
+
+ def do_pipe(self):
+ """ Read a message from standard input and pass it to the mail handler.
+
+ Read into an internal structure that we can seek on (in case
+ there's an error).
+
+ XXX: we may want to read this into a temporary file instead...
+ """
+ s = cStringIO.StringIO()
+ s.write(sys.stdin.read())
+ s.seek(0)
+ self.main(s)
+ return 0
+
+ def do_mailbox(self, filename):
+ """ Read a series of messages from the specified unix mailbox file and
+ pass each to the mail handler.
+ """
+ # open the spool file and lock it
+ import fcntl
+ # FCNTL is deprecated in py2.3 and fcntl takes over all the symbols
+ if hasattr(fcntl, 'LOCK_EX'):
+ FCNTL = fcntl
+ else:
+ import FCNTL
+ f = open(filename, 'r+')
+ fcntl.flock(f.fileno(), FCNTL.LOCK_EX)
+
+ # handle and clear the mailbox
+ try:
+ from mailbox import UnixMailbox
+ mailbox = UnixMailbox(f, factory=Message)
+ # grab one message
+ message = mailbox.next()
+ while message:
+ # handle this message
+ self.handle_Message(message)
+ message = mailbox.next()
+ # nuke the file contents
+ os.ftruncate(f.fileno(), 0)
+ except:
+ import traceback
+ traceback.print_exc()
+ return 1
+ fcntl.flock(f.fileno(), FCNTL.LOCK_UN)
+ return 0
+
+ def do_imap(self, server, user='', password='', mailbox='', ssl=0):
+ ''' Do an IMAP connection
+ '''
+ import getpass, imaplib, socket
+ try:
+ if not user:
+ user = raw_input('User: ')
+ if not password:
+ password = getpass.getpass()
+ except (KeyboardInterrupt, EOFError):
+ # Ctrl C or D maybe also Ctrl Z under Windows.
+ print "\nAborted by user."
+ return 1
+ # open a connection to the server and retrieve all messages
+ try:
+ if ssl:
+ self.logger.debug('Trying server %r with ssl'%server)
+ server = imaplib.IMAP4_SSL(server)
+ else:
+ self.logger.debug('Trying server %r without ssl'%server)
+ server = imaplib.IMAP4(server)
+ except (imaplib.IMAP4.error, socket.error, socket.sslerror):
+ self.logger.exception('IMAP server error')
+ return 1
+
+ try:
+ server.login(user, password)
+ except imaplib.IMAP4.error, e:
+ self.logger.exception('IMAP login failure')
+ return 1
+
+ try:
+ if not mailbox:
+ (typ, data) = server.select()
+ else:
+ (typ, data) = server.select(mailbox=mailbox)
+ if typ != 'OK':
+ self.logger.error('Failed to get mailbox %r: %s'%(mailbox,
+ data))
+ return 1
+ try:
+ numMessages = int(data[0])
+ except ValueError, value:
+ self.logger.error('Invalid message count from mailbox %r'%
+ data[0])
+ return 1
+ for i in range(1, numMessages+1):
+ (typ, data) = server.fetch(str(i), '(RFC822)')
+
+ # mark the message as deleted.
+ server.store(str(i), '+FLAGS', r'(\Deleted)')
+
+ # process the message
+ s = cStringIO.StringIO(data[0][1])
+ s.seek(0)
+ self.handle_Message(Message(s))
+ server.close()
+ finally:
+ try:
+ server.expunge()
+ except:
+ pass
+ server.logout()
+
+ return 0
+
+
+ def do_apop(self, server, user='', password=''):
+ ''' Do authentication POP
+ '''
+ self.do_pop(server, user, password, apop=1)
+
+ def do_pop(self, server, user='', password='', apop=0):
+ '''Read a series of messages from the specified POP server.
+ '''
+ import getpass, poplib, socket
+ try:
+ if not user:
+ user = raw_input('User: ')
+ if not password:
+ password = getpass.getpass()
+ except (KeyboardInterrupt, EOFError):
+ # Ctrl C or D maybe also Ctrl Z under Windows.
+ print "\nAborted by user."
+ return 1
+
+ # open a connection to the server and retrieve all messages
+ try:
+ server = poplib.POP3(server)
+ except socket.error:
+ self.logger.exception('POP server error')
+ return 1
+ if apop:
+ server.apop(user, password)
+ else:
+ server.user(user)
+ server.pass_(password)
+ numMessages = len(server.list()[1])
+ for i in range(1, numMessages+1):
+ # retr: returns
+ # [ pop response e.g. '+OK 459 octets',
+ # [ array of message lines ],
+ # number of octets ]
+ lines = server.retr(i)[1]
+ s = cStringIO.StringIO('\n'.join(lines))
+ s.seek(0)
+ self.handle_Message(Message(s))
+ # delete the message
+ server.dele(i)
+
+ # quit the server to commit changes.
+ server.quit()
+ return 0
+
+ def main(self, fp):
+ ''' fp - the file from which to read the Message.
+ '''
+ return self.handle_Message(Message(fp))
+
+ def handle_Message(self, message):
+ """Handle an RFC822 Message
+
+ Handle the Message object by calling handle_message() and then cope
+ with any errors raised by handle_message.
+ This method's job is to make that call and handle any
+ errors in a sane manner. It should be replaced if you wish to
+ handle errors in a different manner.
+ """
+ # in some rare cases, a particularly stuffed-up e-mail will make
+ # its way into here... try to handle it gracefully
+
+ sendto = message.getaddrlist('resent-from')
+ if not sendto:
+ sendto = message.getaddrlist('from')
+ if not sendto:
+ # very bad-looking message - we don't even know who sent it
+ msg = ['Badly formed message from mail gateway. Headers:']
+ msg.extend(message.headers)
+ msg = '\n'.join(map(str, msg))
+ self.logger.error(msg)
+ return
+
+ msg = 'Handling message'
+ if message.getheader('message-id'):
+ msg += ' (Message-id=%r)'%message.getheader('message-id')
+ self.logger.info(msg)
+
+ # try normal message-handling
+ if not self.trapExceptions:
+ return self.handle_message(message)
+
+ # no, we want to trap exceptions
+ try:
+ return self.handle_message(message)
+ except MailUsageHelp:
+ # bounce the message back to the sender with the usage message
+ fulldoc = '\n'.join(string.split(__doc__, '\n')[2:])
+ m = ['']
+ m.append('\n\nMail Gateway Help\n=================')
+ m.append(fulldoc)
+ self.mailer.bounce_message(message, [sendto[0][1]], m,
+ subject="Mail Gateway Help")
+ except MailUsageError, value:
+ # bounce the message back to the sender with the usage message
+ fulldoc = '\n'.join(string.split(__doc__, '\n')[2:])
+ m = ['']
+ m.append(str(value))
+ m.append('\n\nMail Gateway Help\n=================')
+ m.append(fulldoc)
+ self.mailer.bounce_message(message, [sendto[0][1]], m)
+ except Unauthorized, value:
+ # just inform the user that he is not authorized
+ m = ['']
+ m.append(str(value))
+ try:
+ self.mailer.bounce_message(message, [sendto[0][1]], m)
+ except MessageSendError, error:
+ # if the only reason the bounce failed is because of
+ # invalid addresses to bounce the message back to, then
+ # just discard the message - it's just not worth bothering
+ # with (most likely spam / otherwise forged)
+ invalid = True
+ for address, (code, reason) in error.keys():
+ if code != 550:
+ invalid = False
+ if not invalid:
+ raise
+ except IgnoreMessage:
+ # do not take any action
+ # this exception is thrown when email should be ignored
+ msg = 'IgnoreMessage raised'
+ if message.getheader('message-id'):
+ msg += ' (Message-id=%r)'%message.getheader('message-id')
+ self.logger.info(msg)
+ return
+ except:
+ msg = 'Exception handling message'
+ if message.getheader('message-id'):
+ msg += ' (Message-id=%r)'%message.getheader('message-id')
+ self.logger.exception(msg)
+
+ # bounce the message back to the sender with the error message
+ # let the admin know that something very bad is happening
+ sendto = [sendto[0][1], self.instance.config.ADMIN_EMAIL]
+ m = ['']
+ m.append('An unexpected error occurred during the processing')
+ m.append('of your message. The tracker administrator is being')
+ m.append('notified.\n')
+ self.mailer.bounce_message(message, sendto, m)
+
+ def handle_message(self, message):
+ ''' message - a Message instance
+
+ Parse the message as per the module docstring.
+ '''
+ # detect loops
+ if message.getheader('x-roundup-loop', ''):
+ raise IgnoreLoop
+
+ # handle the subject line
+ subject = message.getheader('subject', '')
+ if not subject:
+ raise MailUsageError, _("""
+Emails to Roundup trackers must include a Subject: line!
+""")
+
+ # detect Precedence: Bulk, or Microsoft Outlook autoreplies
+ if (message.getheader('precedence', '') == 'bulk'
+ or subject.lower().find("autoreply") > 0):
+ raise IgnoreBulk
+
+ if subject.strip().lower() == 'help':
+ raise MailUsageHelp
+
+ # config is used many times in this method.
+ # make local variable for easier access
+ config = self.instance.config
+
+ # determine the sender's address
+ from_list = message.getaddrlist('resent-from')
+ if not from_list:
+ from_list = message.getaddrlist('from')
+
+ # XXX Don't enable. This doesn't work yet.
+# "[^A-z.]tracker\+(?P<classname>[^\d\s]+)(?P<nodeid>\d+)\@some.dom.ain[^A-z.]"
+ # handle delivery to addresses like:tracker+issue25 at some.dom.ain
+ # use the embedded issue number as our issue
+# issue_re = config['MAILGW_ISSUE_ADDRESS_RE']
+# if issue_re:
+# for header in ['to', 'cc', 'bcc']:
+# addresses = message.getheader(header, '')
+# if addresses:
+# # FIXME, this only finds the first match in the addresses.
+# issue = re.search(issue_re, addresses, 'i')
+# if issue:
+# classname = issue.group('classname')
+# nodeid = issue.group('nodeid')
+# break
+
+ # Matches subjects like:
+ # Re: "[issue1234] title of issue [status=resolved]"
+ open, close = config['MAILGW_SUBJECT_SUFFIX_DELIMITERS']
+ delim_open = re.escape(open)
+ delim_close = re.escape(close)
+ subject_re = re.compile(r'''
+ (?P<refwd>\s*\W?\s*(fw|fwd|re|aw)\W\s*)*\s* # Re:
+ (?P<quote>")? # Leading "
+ (\[(?P<classname>[^\d\s]+) # [issue..
+ (?P<nodeid>\d+)? # ..1234]
+ \])?\s*
+ (?P<title>[^%s]+)? # issue title
+ "? # Trailing "
+ (?P<argswhole>%s(?P<args>.+?)%s)? # [prop=value]
+ '''%(delim_open, delim_open, delim_close),
+ re.IGNORECASE|re.VERBOSE)
+
+ # figure subject line parsing modes
+ pfxmode = config['MAILGW_SUBJECT_PREFIX_PARSING']
+ sfxmode = config['MAILGW_SUBJECT_SUFFIX_PARSING']
+
+ # check for well-formed subject line
+ m = subject_re.match(subject)
+ if m:
+ # check for registration OTK
+ # or fallback on the default class
+ if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
+ otk_re = re.compile('-- key (?P<otk>[a-zA-Z0-9]{32})')
+ otk = otk_re.search(m.group('title') or '')
+ if otk:
+ self.db.confirm_registration(otk.group('otk'))
+ subject = 'Your registration to %s is complete' % \
+ config['TRACKER_NAME']
+ sendto = [from_list[0][1]]
+ self.mailer.standard_message(sendto, subject, '')
+ return
+ # get the classname
+ if pfxmode == 'none':
+ classname = None
+ else:
+ classname = m.group('classname')
+ if classname is None:
+ if self.default_class:
+ classname = self.default_class
+ else:
+ classname = config['MAILGW_DEFAULT_CLASS']
+ if not classname:
+ # fail
+ m = None
+
+ if not m and pfxmode == 'strict':
+ raise MailUsageError, _("""
+The message you sent to roundup did not contain a properly formed subject
+line. The subject must contain a class name or designator to indicate the
+'topic' of the message. For example:
+ Subject: [issue] This is a new issue
+ - this will create a new issue in the tracker with the title 'This is
+ a new issue'.
+ Subject: [issue1234] This is a followup to issue 1234
+ - this will append the message's contents to the existing issue 1234
+ in the tracker.
+
+Subject was: '%(subject)s'
+""") % locals()
+
+ # try to get the class specified - if "loose" then fall back on the
+ # default
+ attempts = [classname]
+ if pfxmode == 'loose':
+ if self.default_class:
+ attempts.append(self.default_class)
+ else:
+ attempts.append(config['MAILGW_DEFAULT_CLASS'])
+ cl = None
+ for trycl in attempts:
+ try:
+ cl = self.db.getclass(classname)
+ break
+ except KeyError:
+ pass
+ if not cl:
+ validname = ', '.join(self.db.getclasses())
+ raise MailUsageError, _("""
+The class name you identified in the subject line ("%(classname)s") does not exist in the
+database.
+
+Valid class names are: %(validname)s
+Subject was: "%(subject)s"
+""") % locals()
+
+ # get the optional nodeid
+ if pfxmode == 'none':
+ nodeid = None
+ else:
+ nodeid = m.group('nodeid')
+
+ # title is optional too
+ title = m.group('title')
+ if title:
+ title = title.strip()
+ else:
+ title = ''
+
+ # strip off the quotes that dumb emailers put around the subject, like
+ # Re: "[issue1] bla blah"
+ if m.group('quote') and title.endswith('"'):
+ title = title[:-1]
+
+ # but we do need either a title or a nodeid...
+ if nodeid is None and not title:
+ raise MailUsageError, _("""
+I cannot match your message to a node in the database - you need to either
+supply a full designator (with number, eg "[issue123]" or keep the
+previous subject title intact so I can match that.
+
+Subject was: "%(subject)s"
+""") % locals()
+
+ # If there's no nodeid, check to see if this is a followup and
+ # maybe someone's responded to the initial mail that created an
+ # entry. Try to find the matching nodes with the same title, and
+ # use the _last_ one matched (since that'll _usually_ be the most
+ # recent...). The subject_content_match config may specify an
+ # additional restriction based on the matched node's creation or
+ # activity.
+ tmatch_mode = config['MAILGW_SUBJECT_CONTENT_MATCH']
+ if tmatch_mode != 'never' and nodeid is None and m.group('refwd'):
+ l = cl.stringFind(title=title)
+ limit = None
+ if (tmatch_mode.startswith('creation') or
+ tmatch_mode.startswith('activity')):
+ limit, interval = tmatch_mode.split(' ', 1)
+ threshold = date.Date('.') - date.Interval(interval)
+ for id in l:
+ if limit:
+ if threshold < cl.get(id, limit):
+ nodeid = id
+ else:
+ nodeid = id
+
+ # if a nodeid was specified, make sure it's valid
+ if nodeid is not None and not cl.hasnode(nodeid):
+ if pfxmode == 'strict':
+ raise MailUsageError, _("""
+The node specified by the designator in the subject of your message
+("%(nodeid)s") does not exist.
+
+Subject was: "%(subject)s"
+""") % locals()
+ else:
+ title = subject
+ nodeid = None
+
+ # Handle the arguments specified by the email gateway command line.
+ # We do this by looping over the list of self.arguments looking for
+ # a -C to tell us what class then the -S setting string.
+ msg_props = {}
+ user_props = {}
+ file_props = {}
+ issue_props = {}
+ # so, if we have any arguments, use them
+ if self.arguments:
+ current_class = 'msg'
+ for option, propstring in self.arguments:
+ if option in ( '-C', '--class'):
+ current_class = propstring.strip()
+ # XXX this is not flexible enough.
+ # we should chect for subclasses of these classes,
+ # not for the class name...
+ if current_class not in ('msg', 'file', 'user', 'issue'):
+ mailadmin = config['ADMIN_EMAIL']
+ raise MailUsageError, _("""
+The mail gateway is not properly set up. Please contact
+%(mailadmin)s and have them fix the incorrect class specified as:
+ %(current_class)s
+""") % locals()
+ if option in ('-S', '--set'):
+ if current_class == 'issue' :
+ errors, issue_props = setPropArrayFromString(self,
+ cl, propstring.strip(), nodeid)
+ elif current_class == 'file' :
+ temp_cl = self.db.getclass('file')
+ errors, file_props = setPropArrayFromString(self,
+ temp_cl, propstring.strip())
+ elif current_class == 'msg' :
+ temp_cl = self.db.getclass('msg')
+ errors, msg_props = setPropArrayFromString(self,
+ temp_cl, propstring.strip())
+ elif current_class == 'user' :
+ temp_cl = self.db.getclass('user')
+ errors, user_props = setPropArrayFromString(self,
+ temp_cl, propstring.strip())
+ if errors:
+ mailadmin = config['ADMIN_EMAIL']
+ raise MailUsageError, _("""
+The mail gateway is not properly set up. Please contact
+%(mailadmin)s and have them fix the incorrect properties:
+ %(errors)s
+""") % locals()
+
+ #
+ # handle the users
+ #
+ # Don't create users if anonymous isn't allowed to register
+ create = 1
+ anonid = self.db.user.lookup('anonymous')
+ if not (self.db.security.hasPermission('Create', anonid, 'user')
+ and self.db.security.hasPermission('Email Access', anonid)):
+ create = 0
+
+ # ok, now figure out who the author is - create a new user if the
+ # "create" flag is true
+ author = uidFromAddress(self.db, from_list[0], create=create)
+
+ # if we're not recognised, and we don't get added as a user, then we
+ # must be anonymous
+ if not author:
+ author = anonid
+
+ # make sure the author has permission to use the email interface
+ if not self.db.security.hasPermission('Email Access', author):
+ if author == anonid:
+ # we're anonymous and we need to be a registered user
+ from_address = from_list[0][1]
+ raise Unauthorized, _("""
+You are not a registered user.
+
+Unknown address: %(from_address)s
+""") % locals()
+ else:
+ # we're registered and we're _still_ not allowed access
+ raise Unauthorized, _(
+ 'You are not permitted to access this tracker.')
+
+ # make sure they're allowed to edit or create this class of information
+ if nodeid:
+ if not self.db.security.hasPermission('Edit', author, classname,
+ itemid=nodeid):
+ raise Unauthorized, _(
+ 'You are not permitted to edit %(classname)s.') % locals()
+ else:
+ if not self.db.security.hasPermission('Create', author, classname):
+ raise Unauthorized, _(
+ 'You are not permitted to create %(classname)s.'
+ ) % locals()
+
+ # the author may have been created - make sure the change is
+ # committed before we reopen the database
+ self.db.commit()
+
+ # set the database user as the author
+ username = self.db.user.get(author, 'username')
+ self.db.setCurrentUser(username)
+
+ # re-get the class with the new database connection
+ cl = self.db.getclass(classname)
+
+ # now update the recipients list
+ recipients = []
+ tracker_email = config['TRACKER_EMAIL'].lower()
+ for recipient in message.getaddrlist('to') + message.getaddrlist('cc'):
+ r = recipient[1].strip().lower()
+ if r == tracker_email or not r:
+ continue
+
+ # look up the recipient - create if necessary (and we're
+ # allowed to)
+ recipient = uidFromAddress(self.db, recipient, create, **user_props)
+
+ # if all's well, add the recipient to the list
+ if recipient:
+ recipients.append(recipient)
+
+ #
+ # handle the subject argument list
+ #
+ # figure what the properties of this Class are
+ properties = cl.getprops()
+ props = {}
+ args = m.group('args')
+ argswhole = m.group('argswhole')
+ if args:
+ if sfxmode == 'none':
+ title += ' ' + argswhole
+ else:
+ errors, props = setPropArrayFromString(self, cl, args, nodeid)
+ # handle any errors parsing the argument list
+ if errors:
+ if sfxmode == 'strict':
+ errors = '\n- '.join(map(str, errors))
+ raise MailUsageError, _("""
+There were problems handling your subject line argument list:
+- %(errors)s
+
+Subject was: "%(subject)s"
+""") % locals()
+ else:
+ title += ' ' + argswhole
+
+
+ # set the issue title to the subject
+ title = title.strip()
+ if (title and properties.has_key('title') and not
+ issue_props.has_key('title')):
+ issue_props['title'] = title
+
+ #
+ # handle message-id and in-reply-to
+ #
+ messageid = message.getheader('message-id')
+ inreplyto = message.getheader('in-reply-to') or ''
+ # generate a messageid if there isn't one
+ if not messageid:
+ messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
+ classname, nodeid, config['MAIL_DOMAIN'])
+
+ # now handle the body - find the message
+ content, attachments = message.extract_content()
+ if content is None:
+ raise MailUsageError, _("""
+Roundup requires the submission to be plain text. The message parser could
+not find a text/plain part to use.
+""")
+
+ # figure how much we should muck around with the email body
+ keep_citations = config['MAILGW_KEEP_QUOTED_TEXT']
+ keep_body = config['MAILGW_LEAVE_BODY_UNCHANGED']
+
+ # parse the body of the message, stripping out bits as appropriate
+ summary, content = parseContent(content, keep_citations,
+ keep_body)
+ content = content.strip()
+
+ #
+ # handle the attachments
+ #
+ if properties.has_key('files'):
+ files = []
+ for (name, mime_type, data) in attachments:
+ if not self.db.security.hasPermission('Create', author, 'file'):
+ raise Unauthorized, _(
+ 'You are not permitted to create files.')
+ if not name:
+ name = "unnamed"
+ try:
+ fileid = self.db.file.create(type=mime_type, name=name,
+ content=data, **file_props)
+ except exceptions.Reject:
+ pass
+ else:
+ files.append(fileid)
+ # attach the files to the issue
+ if not self.db.security.hasPermission('Edit', author,
+ classname, 'files'):
+ raise Unauthorized, _(
+ 'You are not permitted to add files to %(classname)s.'
+ ) % locals()
+
+ if nodeid:
+ # extend the existing files list
+ fileprop = cl.get(nodeid, 'files')
+ fileprop.extend(files)
+ props['files'] = fileprop
+ else:
+ # pre-load the files list
+ props['files'] = files
+
+ #
+ # create the message if there's a message body (content)
+ #
+ if (content and properties.has_key('messages')):
+ if not self.db.security.hasPermission('Create', author, 'msg'):
+ raise Unauthorized, _(
+ 'You are not permitted to create messages.')
+
+ try:
+ message_id = self.db.msg.create(author=author,
+ recipients=recipients, date=date.Date('.'),
+ summary=summary, content=content, files=files,
+ messageid=messageid, inreplyto=inreplyto, **msg_props)
+ except exceptions.Reject, error:
+ raise MailUsageError, _("""
+Mail message was rejected by a detector.
+%(error)s
+""") % locals()
+ # attach the message to the node
+ if not self.db.security.hasPermission('Edit', author,
+ classname, 'messages'):
+ raise Unauthorized, _(
+ 'You are not permitted to add messages to %(classname)s.'
+ ) % locals()
+
+ if nodeid:
+ # add the message to the node's list
+ messages = cl.get(nodeid, 'messages')
+ messages.append(message_id)
+ props['messages'] = messages
+ else:
+ # pre-load the messages list
+ props['messages'] = [message_id]
+
+ #
+ # perform the node change / create
+ #
+ try:
+ # merge the command line props defined in issue_props into
+ # the props dictionary because function(**props, **issue_props)
+ # is a syntax error.
+ for prop in issue_props.keys() :
+ if not props.has_key(prop) :
+ props[prop] = issue_props[prop]
+
+ # Check permissions for each property
+ for prop in props.keys():
+ if not self.db.security.hasPermission('Edit', author,
+ classname, prop):
+ raise Unauthorized, _('You are not permitted to edit '
+ 'property %(prop)s of class %(classname)s.') % locals()
+
+ if nodeid:
+ cl.set(nodeid, **props)
+ else:
+ nodeid = cl.create(**props)
+ except (TypeError, IndexError, ValueError), message:
+ raise MailUsageError, _("""
+There was a problem with the message you sent:
+ %(message)s
+""") % locals()
+
+ # commit the changes to the DB
+ self.db.commit()
+
+ return nodeid
+
+
+def setPropArrayFromString(self, cl, propString, nodeid=None):
+ ''' takes string of form prop=value,value;prop2=value
+ and returns (error, prop[..])
+ '''
+ props = {}
+ errors = []
+ for prop in string.split(propString, ';'):
+ # extract the property name and value
+ try:
+ propname, value = prop.split('=')
+ except ValueError, message:
+ errors.append(_('not of form [arg=value,value,...;'
+ 'arg=value,value,...]'))
+ return (errors, props)
+ # convert the value to a hyperdb-usable value
+ propname = propname.strip()
+ try:
+ props[propname] = hyperdb.rawToHyperdb(self.db, cl, nodeid,
+ propname, value)
+ except hyperdb.HyperdbValueError, message:
+ errors.append(str(message))
+ return errors, props
+
+
+def extractUserFromList(userClass, users):
+ '''Given a list of users, try to extract the first non-anonymous user
+ and return that user, otherwise return None
+ '''
+ if len(users) > 1:
+ for user in users:
+ # make sure we don't match the anonymous or admin user
+ if userClass.get(user, 'username') in ('admin', 'anonymous'):
+ continue
+ # first valid match will do
+ return user
+ # well, I guess we have no choice
+ return user[0]
+ elif users:
+ return users[0]
+ return None
+
+
+def uidFromAddress(db, address, create=1, **user_props):
+ ''' address is from the rfc822 module, and therefore is (name, addr)
+
+ user is created if they don't exist in the db already
+ user_props may supply additional user information
+ '''
+ (realname, address) = address
+
+ # try a straight match of the address
+ user = extractUserFromList(db.user, db.user.stringFind(address=address))
+ if user is not None:
+ return user
+
+ # try the user alternate addresses if possible
+ props = db.user.getprops()
+ if props.has_key('alternate_addresses'):
+ users = db.user.filter(None, {'alternate_addresses': address})
+ user = extractUserFromList(db.user, users)
+ if user is not None:
+ return user
+
+ # try to match the username to the address (for local
+ # submissions where the address is empty)
+ user = extractUserFromList(db.user, db.user.stringFind(username=address))
+
+ # couldn't match address or username, so create a new user
+ if create:
+ # generate a username
+ if '@' in address:
+ username = address.split('@')[0]
+ else:
+ username = address
+ trying = username
+ n = 0
+ while 1:
+ try:
+ # does this username exist already?
+ db.user.lookup(trying)
+ except KeyError:
+ break
+ n += 1
+ trying = username + str(n)
+
+ # create!
+ try:
+ return db.user.create(username=trying, address=address,
+ realname=realname, roles=db.config.NEW_EMAIL_USER_ROLES,
+ password=password.Password(password.generatePassword()),
+ **user_props)
+ except exceptions.Reject:
+ return 0
+ else:
+ return 0
+
+
+def parseContent(content, keep_citations, keep_body,
+ blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
+ eol=re.compile(r'[\r\n]+'),
+ signature=re.compile(r'^[>|\s]*-- ?$'),
+ original_msg=re.compile(r'^[>|\s]*-----\s?Original Message\s?-----$')):
+ ''' The message body is divided into sections by blank lines.
+ Sections where the second and all subsequent lines begin with a ">"
+ or "|" character are considered "quoting sections". The first line of
+ the first non-quoting section becomes the summary of the message.
+
+ If keep_citations is true, then we keep the "quoting sections" in the
+ content.
+ If keep_body is true, we even keep the signature sections.
+ '''
+ # strip off leading carriage-returns / newlines
+ i = 0
+ for i in range(len(content)):
+ if content[i] not in '\r\n':
+ break
+ if i > 0:
+ sections = blank_line.split(content[i:])
+ else:
+ sections = blank_line.split(content)
+
+ # extract out the summary from the message
+ summary = ''
+ l = []
+ for section in sections:
+ #section = section.strip()
+ if not section:
+ continue
+ lines = eol.split(section)
+ if (lines[0] and lines[0][0] in '>|') or (len(lines) > 1 and
+ lines[1] and lines[1][0] in '>|'):
+ # see if there's a response somewhere inside this section (ie.
+ # no blank line between quoted message and response)
+ for line in lines[1:]:
+ if line and line[0] not in '>|':
+ break
+ else:
+ # we keep quoted bits if specified in the config
+ if keep_citations:
+ l.append(section)
+ continue
+ # keep this section - it has reponse stuff in it
+ lines = lines[lines.index(line):]
+ section = '\n'.join(lines)
+ # and while we're at it, use the first non-quoted bit as
+ # our summary
+ summary = section
+
+ if not summary:
+ # if we don't have our summary yet use the first line of this
+ # section
+ summary = section
+ elif signature.match(lines[0]) and 2 <= len(lines) <= 10:
+ # lose any signature
+ break
+ elif original_msg.match(lines[0]):
+ # ditch the stupid Outlook quoting of the entire original message
+ break
+
+ # and add the section to the output
+ l.append(section)
+
+ # figure the summary - find the first sentence-ending punctuation or the
+ # first whole line, whichever is longest
+ sentence = re.search(r'^([^!?\.]+[!?\.])', summary)
+ if sentence:
+ sentence = sentence.group(1)
+ else:
+ sentence = ''
+ first = eol.split(summary)[0]
+ summary = max(sentence, first)
+
+ # Now reconstitute the message content minus the bits we don't care
+ # about.
+ if not keep_body:
+ content = '\n\n'.join(l)
+
+ return summary, content
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/msgfmt.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/msgfmt.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,224 @@
+#! /usr/bin/env python
+# -*- coding: iso-8859-1 -*-
+# Written by Martin v. Löwis <loewis at informatik.hu-berlin.de>
+# Plural forms support added by alexander smishlajev <alex at tycobka.lv>
+
+"""Generate binary message catalog from textual translation description.
+
+This program converts a textual Uniforum-style message catalog (.po file) into
+a binary GNU catalog (.mo file). This is essentially the same function as the
+GNU msgfmt program, however, it is a simpler implementation.
+
+Usage: msgfmt.py [OPTIONS] filename.po
+
+Options:
+ -o file
+ --output-file=file
+ Specify the output file to write to. If omitted, output will go to a
+ file named filename.mo (based off the input file name).
+
+ -h
+ --help
+ Print this message and exit.
+
+ -V
+ --version
+ Display version information and exit.
+"""
+
+import sys
+import os
+import getopt
+import struct
+import array
+
+__version__ = "1.1"
+
+MESSAGES = {}
+
+
+
+def usage(code, msg=''):
+ print >> sys.stderr, __doc__
+ if msg:
+ print >> sys.stderr, msg
+ sys.exit(code)
+
+
+
+def add(id, str, fuzzy):
+ "Add a non-fuzzy translation to the dictionary."
+ global MESSAGES
+ if not fuzzy and str and not str.startswith('\0'):
+ MESSAGES[id] = str
+
+
+
+def generate():
+ "Return the generated output."
+ global MESSAGES
+ keys = MESSAGES.keys()
+ # the keys are sorted in the .mo file
+ keys.sort()
+ offsets = []
+ ids = strs = ''
+ for id in keys:
+ # For each string, we need size and file offset. Each string is NUL
+ # terminated; the NUL does not count into the size.
+ offsets.append((len(ids), len(id), len(strs), len(MESSAGES[id])))
+ ids += id + '\0'
+ strs += MESSAGES[id] + '\0'
+ output = ''
+ # The header is 7 32-bit unsigned integers. We don't use hash tables, so
+ # the keys start right after the index tables.
+ # translated string.
+ keystart = 7*4+16*len(keys)
+ # and the values start after the keys
+ valuestart = keystart + len(ids)
+ koffsets = []
+ voffsets = []
+ # The string table first has the list of keys, then the list of values.
+ # Each entry has first the size of the string, then the file offset.
+ for o1, l1, o2, l2 in offsets:
+ koffsets += [l1, o1+keystart]
+ voffsets += [l2, o2+valuestart]
+ offsets = koffsets + voffsets
+ output = struct.pack("Iiiiiii",
+ 0x950412deL, # Magic
+ 0, # Version
+ len(keys), # # of entries
+ 7*4, # start of key index
+ 7*4+len(keys)*8, # start of value index
+ 0, 0) # size and offset of hash table
+ output += array.array("i", offsets).tostring()
+ output += ids
+ output += strs
+ return output
+
+
+
+def make(filename, outfile):
+ ID = 1
+ STR = 2
+ global MESSAGES
+ MESSAGES = {}
+
+ # Compute .mo name from .po name and arguments
+ if filename.endswith('.po'):
+ infile = filename
+ else:
+ infile = filename + '.po'
+ if outfile is None:
+ outfile = os.path.splitext(infile)[0] + '.mo'
+
+ try:
+ lines = open(infile).readlines()
+ except IOError, msg:
+ print >> sys.stderr, msg
+ sys.exit(1)
+
+ # remove UTF-8 Byte Order Mark, if any.
+ # (UCS2 BOMs are not handled because messages in UCS2 cannot be handled)
+ if lines[0].startswith('\xEF\xBB\xBF'):
+ lines[0] = lines[0][3:]
+
+ section = None
+ fuzzy = 0
+
+ # Parse the catalog
+ lno = 0
+ for l in lines:
+ lno += 1
+ # If we get a comment line after a msgstr, this is a new entry
+ if l[0] == '#' and section == STR:
+ add(msgid, msgstr, fuzzy)
+ section = None
+ fuzzy = 0
+ # Record a fuzzy mark
+ if l[:2] == '#,' and (l.find('fuzzy') >= 0):
+ fuzzy = 1
+ # Skip comments
+ if l[0] == '#':
+ continue
+ # Start of msgid_plural section, separate from singular form with \0
+ if l.startswith('msgid_plural'):
+ msgid += '\0'
+ l = l[12:]
+ # Now we are in a msgid section, output previous section
+ elif l.startswith('msgid'):
+ if section == STR:
+ add(msgid, msgstr, fuzzy)
+ section = ID
+ l = l[5:]
+ msgid = msgstr = ''
+ # Now we are in a msgstr section
+ elif l.startswith('msgstr'):
+ section = STR
+ l = l[6:]
+ # Check for plural forms
+ if l.startswith('['):
+ # Separate plural forms with \0
+ if not l.startswith('[0]'):
+ msgstr += '\0'
+ # Ignore the index - must come in sequence
+ l = l[l.index(']') + 1:]
+ # Skip empty lines
+ l = l.strip()
+ if not l:
+ continue
+ # XXX: Does this always follow Python escape semantics?
+ l = eval(l)
+ if section == ID:
+ msgid += l
+ elif section == STR:
+ msgstr += l
+ else:
+ print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \
+ 'before:'
+ print >> sys.stderr, l
+ sys.exit(1)
+ # Add last entry
+ if section == STR:
+ add(msgid, msgstr, fuzzy)
+
+ # Compute output
+ output = generate()
+
+ try:
+ open(outfile,"wb").write(output)
+ except IOError,msg:
+ print >> sys.stderr, msg
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'hVo:',
+ ['help', 'version', 'output-file='])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ outfile = None
+ # parse options
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-V', '--version'):
+ print >> sys.stderr, "msgfmt.py", __version__
+ sys.exit(0)
+ elif opt in ('-o', '--output-file'):
+ outfile = arg
+ # do it
+ if not args:
+ print >> sys.stderr, 'No input file given'
+ print >> sys.stderr, "Try `msgfmt --help' for more information."
+ return
+
+ for filename in args:
+ make(filename, outfile)
+
+
+if __name__ == '__main__':
+ main()
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/password.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/password.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,166 @@
+#
+# 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: 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
+try:
+ import crypt
+except:
+ crypt = None
+ pass
+
+class PasswordValueError(ValueError):
+ ''' The password value is not valid '''
+ pass
+
+def encodePassword(plaintext, scheme, other=None):
+ '''Encrypt the plaintext password.
+ '''
+ if plaintext is None:
+ plaintext = ""
+ if scheme == 'SHA':
+ s = sha.sha(plaintext).hexdigest()
+ elif scheme == 'MD5':
+ s = md5.md5(plaintext).hexdigest()
+ elif scheme == 'crypt' and crypt is not None:
+ if other is not None:
+ salt = other
+ else:
+ saltchars = './0123456789'+string.letters
+ salt = random.choice(saltchars) + random.choice(saltchars)
+ s = crypt.crypt(plaintext, salt)
+ elif scheme == 'plaintext':
+ s = plaintext
+ else:
+ raise PasswordValueError, 'unknown encryption scheme %r'%scheme
+ return s
+
+def generatePassword(length=8):
+ chars = string.letters+string.digits
+ return ''.join([random.choice(chars) for x in range(length)])
+
+class Password:
+ '''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
+ plaintext. The None encoding is used in legacy databases where no
+ encoding scheme is identified.
+
+ The scheme is stored with the encoded data in the database:
+ {scheme}data
+
+ Example usage:
+ >>> p = Password('sekrit')
+ >>> p == 'sekrit'
+ 1
+ >>> p != 'not sekrit'
+ 1
+ >>> 'sekrit' == p
+ 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.'''
+ if scheme is None:
+ scheme = self.default_scheme
+ if plaintext is not None:
+ self.setPassword (plaintext, scheme)
+ elif encrypted is not None:
+ self.unpack(encrypted, scheme)
+ else:
+ self.scheme = self.default_scheme
+ self.password = None
+ self.plaintext = None
+
+ def unpack(self, encrypted, scheme=None):
+ '''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)
+ self.password = m.group(2)
+ self.plaintext = None
+ else:
+ # currently plaintext - encrypt
+ self.setPassword(encrypted, scheme)
+
+ def setPassword(self, plaintext, scheme=None):
+ '''Sets encrypts plaintext.'''
+ if scheme is None:
+ scheme = self.default_scheme
+ self.scheme = scheme
+ self.password = encodePassword(plaintext, scheme)
+ self.plaintext = plaintext
+
+ def __cmp__(self, other):
+ '''Compare this password against another password.'''
+ # check to see if we're comparing instances
+ if isinstance(other, Password):
+ if self.scheme != other.scheme:
+ return cmp(self.scheme, other.scheme)
+ return cmp(self.password, other.password)
+
+ # assume password is plaintext
+ if self.password is None:
+ raise ValueError, 'Password not set'
+ return cmp(self.password, encodePassword(other, self.scheme,
+ self.password))
+
+ def __str__(self):
+ '''Stringify the encrypted password for database storage.'''
+ if self.password is None:
+ raise ValueError, 'Password not set'
+ return '{%s}%s'%(self.scheme, self.password)
+
+def test():
+ # SHA
+ p = Password('sekrit')
+ assert p == 'sekrit'
+ assert p != 'not sekrit'
+ assert 'sekrit' == p
+ assert 'not sekrit' != p
+
+ # MD5
+ p = Password('sekrit', 'MD5')
+ assert p == 'sekrit'
+ assert p != 'not sekrit'
+ assert 'sekrit' == p
+ assert 'not sekrit' != p
+
+ # crypt
+ p = Password('sekrit', 'crypt')
+ assert p == 'sekrit'
+ assert p != 'not sekrit'
+ assert 'sekrit' == p
+ assert 'not sekrit' != p
+
+if __name__ == '__main__':
+ test()
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/rfc2822.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/rfc2822.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,166 @@
+"""Some rfc822 functions taken from the new (python2.3) "email" module.
+"""
+__docformat__ = 'restructuredtext'
+
+import re
+from string import letters, digits
+from binascii import b2a_base64, a2b_base64
+
+ecre = re.compile(r'''
+ =\? # literal =?
+ (?P<charset>[^?]*?) # non-greedy up to the next ? is the charset
+ \? # literal ?
+ (?P<encoding>[qb]) # either a "q" or a "b", case insensitive
+ \? # literal ?
+ (?P<encoded>.*?) # non-greedy up to the next ?= is the encoded string
+ \?= # literal ?=
+ ''', re.VERBOSE | re.IGNORECASE)
+
+hqre = re.compile(r'^[A-z0-9!"#$%%&\'()*+,-./:;<=>?@\[\]^_`{|}~ ]+$')
+
+CRLF = '\r\n'
+
+def base64_decode(s, convert_eols=None):
+ """Decode a raw base64 string.
+
+ If convert_eols is set to a string value, all canonical email linefeeds,
+ e.g. "\\r\\n", in the decoded text will be converted to the value of
+ convert_eols. os.linesep is a good choice for convert_eols if you are
+ decoding a text attachment.
+
+ This function does not parse a full MIME header value encoded with
+ base64 (like =?iso-8895-1?b?bmloISBuaWgh?=) -- please use the high
+ level email.Header class for that functionality.
+
+ Taken from 'email' module
+ """
+ if not s:
+ return s
+
+ dec = a2b_base64(s)
+ if convert_eols:
+ return dec.replace(CRLF, convert_eols)
+ return dec
+
+def unquote_match(match):
+ """Turn a match in the form ``=AB`` to the ASCII character with value
+ 0xab.
+
+ Taken from 'email' module
+ """
+ s = match.group(0)
+ return chr(int(s[1:3], 16))
+
+def qp_decode(s):
+ """Decode a string encoded with RFC 2045 MIME header 'Q' encoding.
+
+ This function does not parse a full MIME header value encoded with
+ quoted-printable (like =?iso-8895-1?q?Hello_World?=) -- please use
+ the high level email.Header class for that functionality.
+
+ Taken from 'email' module
+ """
+ s = s.replace('_', ' ')
+ return re.sub(r'=\w{2}', unquote_match, s)
+
+def _decode_header(header):
+ """Decode a message header value without converting charset.
+
+ Returns a list of (decoded_string, charset) pairs containing each of the
+ decoded parts of the header. Charset is None for non-encoded parts of the
+ header, otherwise a lower-case string containing the name of the character
+ set specified in the encoded string.
+
+ Taken from 'email' module
+ """
+ # If no encoding, just return the header
+ header = str(header)
+ if not ecre.search(header):
+ return [(header, None)]
+
+ decoded = []
+ dec = ''
+ for line in header.splitlines():
+ # This line might not have an encoding in it
+ if not ecre.search(line):
+ decoded.append((line, None))
+ continue
+
+ parts = ecre.split(line)
+ while parts:
+ unenc = parts.pop(0)
+ if unenc:
+ if unenc.strip():
+ decoded.append((unenc, None))
+ if parts:
+ charset, encoding = [s.lower() for s in parts[0:2]]
+ encoded = parts[2]
+ dec = ''
+ if encoding == 'q':
+ dec = qp_decode(encoded)
+ elif encoding == 'b':
+ dec = base64_decode(encoded)
+ else:
+ dec = encoded
+
+ if decoded and decoded[-1][1] == charset:
+ decoded[-1] = (decoded[-1][0] + dec, decoded[-1][1])
+ else:
+ decoded.append((dec, charset))
+ del parts[0:3]
+ return decoded
+
+def decode_header(hdr):
+ """ Decodes rfc2822 encoded header and return utf-8 encoded string
+ """
+ if not hdr:
+ return None
+ outs = u""
+ for section in _decode_header(hdr):
+ charset = unaliasCharset(section[1])
+ outs += unicode(section[0], charset or 'iso-8859-1', 'replace')
+ return outs.encode('utf-8')
+
+def encode_header(header, charset='utf-8'):
+ """ Will encode in quoted-printable encoding only if header
+ contains non latin characters
+ """
+
+ # Return empty headers unchanged
+ if not header:
+ return header
+
+ # return plain header if it is not contains non-ascii characters
+ if hqre.match(header):
+ return header
+
+ quoted = ''
+ #max_encoded = 76 - len(charset) - 7
+ for c in header:
+ # Space may be represented as _ instead of =20 for readability
+ if c == ' ':
+ quoted += '_'
+ # These characters can be included verbatim
+ elif hqre.match(c) and c not in '_=':
+ quoted += c
+ # Otherwise, replace with hex value like =E2
+ else:
+ quoted += "=%02X" % ord(c)
+ plain = 0
+
+ return '=?%s?q?%s?=' % (charset, quoted)
+
+def unaliasCharset(charset):
+ if charset:
+ return charset.lower().replace("windows-", 'cp')
+ #return charset_table.get(charset.lower(), charset)
+ return None
+
+def test():
+ print encode_header("Contrary, Mary")
+ #print unaliasCharset('Windows-1251')
+
+if __name__ == '__main__':
+ test()
+
+# vim: et
Added: tracker/vendor/roundup/current/roundup/roundupdb.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/roundupdb.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,592 @@
+#
+# 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: roundupdb.py,v 1.123 2006/04/27 01:39:47 richard Exp $
+
+"""Extending hyperdb with types specific to issue-tracking.
+"""
+__docformat__ = 'restructuredtext'
+
+from __future__ import nested_scopes
+
+import re, os, smtplib, socket, time, random
+import cStringIO, base64, quopri, mimetypes
+
+from rfc2822 import encode_header
+
+from roundup import password, date, hyperdb
+from roundup.i18n import _
+
+# MessageSendError is imported for backwards compatibility
+from roundup.mailer import Mailer, straddr, MessageSendError
+
+class Database:
+
+ # remember the journal uid for the current journaltag so that:
+ # a. we don't have to look it up every time we need it, and
+ # b. if the journaltag disappears during a transaction, we don't barf
+ # (eg. the current user edits their username)
+ journal_uid = None
+ def getuid(self):
+ """Return the id of the "user" node associated with the user
+ that owns this connection to the hyperdatabase."""
+ if self.journaltag is None:
+ return None
+ elif self.journaltag == 'admin':
+ # admin user may not exist, but always has ID 1
+ return '1'
+ else:
+ if (self.journal_uid is None or self.journal_uid[0] !=
+ self.journaltag):
+ uid = self.user.lookup(self.journaltag)
+ self.journal_uid = (self.journaltag, uid)
+ return self.journal_uid[1]
+
+ def setCurrentUser(self, username):
+ """Set the user that is responsible for current database
+ activities.
+ """
+ self.journaltag = username
+
+ def isCurrentUser(self, username):
+ """Check if a given username equals the already active user.
+ """
+ return self.journaltag == username
+
+ def getUserTimezone(self):
+ """Return user timezone defined in 'timezone' property of user class.
+ If no such property exists return 0
+ """
+ userid = self.getuid()
+ try:
+ timezone = int(self.user.get(userid, 'timezone'))
+ except (KeyError, ValueError, TypeError):
+ # If there is no class 'user' or current user doesn't have timezone
+ # property or that property is not numeric assume he/she lives in
+ # Greenwich :)
+ timezone = getattr(self.config, 'DEFAULT_TIMEZONE', 0)
+ return timezone
+
+ def confirm_registration(self, otk):
+ props = self.getOTKManager().getall(otk)
+ for propname, proptype in self.user.getprops().items():
+ value = props.get(propname, None)
+ if value is None:
+ pass
+ elif isinstance(proptype, hyperdb.Date):
+ props[propname] = date.Date(value)
+ elif isinstance(proptype, hyperdb.Interval):
+ props[propname] = date.Interval(value)
+ elif isinstance(proptype, hyperdb.Password):
+ props[propname] = password.Password()
+ props[propname].unpack(value)
+
+ # tag new user creation with 'admin'
+ self.journaltag = 'admin'
+
+ # create the new user
+ cl = self.user
+
+ props['roles'] = self.config.NEW_WEB_USER_ROLES
+ userid = cl.create(**props)
+ # clear the props from the otk database
+ self.getOTKManager().destroy(otk)
+ self.commit()
+
+ return userid
+
+
+class DetectorError(RuntimeError):
+ """ Raised by detectors that want to indicate that something's amiss
+ """
+ pass
+
+# deviation from spec - was called IssueClass
+class IssueClass:
+ """This class is intended to be mixed-in with a hyperdb backend
+ implementation. The backend should provide a mechanism that
+ enforces the title, messages, files, nosy and superseder
+ properties:
+
+ - title = hyperdb.String(indexme='yes')
+ - messages = hyperdb.Multilink("msg")
+ - files = hyperdb.Multilink("file")
+ - nosy = hyperdb.Multilink("user")
+ - superseder = hyperdb.Multilink(classname)
+ """
+
+ # The tuple below does not affect the class definition.
+ # It just lists all names of all issue properties
+ # marked for message extraction tool.
+ #
+ # XXX is there better way to get property names into message catalog??
+ #
+ # Note that this list also includes properties
+ # defined in the classic template:
+ # assignedto, topic, priority, status.
+ (
+ ''"title", ''"messages", ''"files", ''"nosy", ''"superseder",
+ ''"assignedto", ''"topic", ''"priority", ''"status",
+ # following properties are common for all hyperdb classes
+ # they are listed here to keep things in one place
+ ''"actor", ''"activity", ''"creator", ''"creation",
+ )
+
+ # New methods:
+ def addmessage(self, nodeid, summary, text):
+ """Add a message to an issue's mail spool.
+
+ A new "msg" node is constructed using the current date, the user that
+ owns the database connection as the author, and the specified summary
+ text.
+
+ The "files" and "recipients" fields are left empty.
+
+ The given text is saved as the body of the message and the node is
+ appended to the "messages" field of the specified issue.
+ """
+
+ def nosymessage(self, nodeid, msgid, oldvalues, whichnosy='nosy',
+ from_address=None, cc=[], bcc=[]):
+ """Send a message to the members of an issue's nosy list.
+
+ The message is sent only to users on the nosy list who are not
+ already on the "recipients" list for the message.
+
+ These users are then added to the message's "recipients" list.
+
+ If 'msgid' is None, the message gets sent only to the nosy
+ list, and it's called a 'System Message'.
+
+ The "cc" argument indicates additional recipients to send the
+ message to that may not be specified in the message's recipients
+ list.
+
+ The "bcc" argument also indicates additional recipients to send the
+ message to that may not be specified in the message's recipients
+ list. These recipients will not be included in the To: or Cc:
+ address lists.
+ """
+ if msgid:
+ authid = self.db.msg.get(msgid, 'author')
+ recipients = self.db.msg.get(msgid, 'recipients', [])
+ else:
+ # "system message"
+ authid = None
+ recipients = []
+
+ sendto = []
+ bcc_sendto = []
+ seen_message = {}
+ for recipient in recipients:
+ seen_message[recipient] = 1
+
+ def add_recipient(userid, to):
+ # make sure they have an address
+ address = self.db.user.get(userid, 'address')
+ if address:
+ to.append(address)
+ recipients.append(userid)
+
+ def good_recipient(userid):
+ # Make sure we don't send mail to either the anonymous
+ # user or a user who has already seen the message.
+ return (userid and
+ (self.db.user.get(userid, 'username') != 'anonymous') and
+ not seen_message.has_key(userid))
+
+ # possibly send the message to the author, as long as they aren't
+ # anonymous
+ if (good_recipient(authid) and
+ (self.db.config.MESSAGES_TO_AUTHOR == 'yes' or
+ (self.db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues))):
+ add_recipient(authid, sendto)
+
+ if authid:
+ seen_message[authid] = 1
+
+ # now deal with the nosy and cc people who weren't recipients.
+ for userid in cc + self.get(nodeid, whichnosy):
+ if good_recipient(userid):
+ add_recipient(userid, sendto)
+
+ # now deal with bcc people.
+ for userid in bcc:
+ if good_recipient(userid):
+ add_recipient(userid, bcc_sendto)
+
+ if oldvalues:
+ note = self.generateChangeNote(nodeid, oldvalues)
+ else:
+ note = self.generateCreateNote(nodeid)
+
+ # If we have new recipients, update the message's recipients
+ # and send the mail.
+ if sendto or bcc_sendto:
+ if msgid is not None:
+ self.db.msg.set(msgid, recipients=recipients)
+ self.send_message(nodeid, msgid, note, sendto, from_address,
+ bcc_sendto)
+
+ # backwards compatibility - don't remove
+ sendmessage = nosymessage
+
+ def send_message(self, nodeid, msgid, note, sendto, from_address=None,
+ bcc_sendto=[]):
+ '''Actually send the nominated message from this node to the sendto
+ recipients, with the note appended.
+ '''
+ users = self.db.user
+ messages = self.db.msg
+ files = self.db.file
+
+ if msgid is None:
+ inreplyto = None
+ messageid = None
+ else:
+ inreplyto = messages.get(msgid, 'inreplyto')
+ messageid = messages.get(msgid, 'messageid')
+
+ # make up a messageid if there isn't one (web edit)
+ if not messageid:
+ # this is an old message that didn't get a messageid, so
+ # create one
+ messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
+ self.classname, nodeid,
+ self.db.config.MAIL_DOMAIN)
+ if msgid is not None:
+ messages.set(msgid, messageid=messageid)
+
+ # compose title
+ cn = self.classname
+ title = self.get(nodeid, 'title') or '%s message copy'%cn
+
+ # figure author information
+ if msgid:
+ authid = messages.get(msgid, 'author')
+ authname = users.get(authid, 'realname')
+ if not authname:
+ authname = users.get(authid, 'username', '')
+ authaddr = users.get(authid, 'address', '')
+ else:
+ # "system message"
+ authid = None
+ authname = 'admin'
+ authaddr = self.db.config.ADMIN_EMAIL
+
+ if authaddr:
+ authaddr = " <%s>" % straddr( ('',authaddr) )
+
+ # make the message body
+ m = ['']
+
+ # put in roundup's signature
+ if self.db.config.EMAIL_SIGNATURE_POSITION == 'top':
+ m.append(self.email_signature(nodeid, msgid))
+
+ # add author information
+ if authid:
+ if len(self.get(nodeid,'messages')) == 1:
+ m.append(_("New submission from %(authname)s%(authaddr)s:")
+ % locals())
+ else:
+ m.append(_("%(authname)s%(authaddr)s added the comment:")
+ % locals())
+ else:
+ m.append(_("System message:"))
+ m.append('')
+
+ # add the content
+ if msgid is not None:
+ m.append(messages.get(msgid, 'content', ''))
+
+ # add the change note
+ if note:
+ m.append(note)
+
+ # put in roundup's signature
+ if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom':
+ m.append(self.email_signature(nodeid, msgid))
+
+ # encode the content as quoted-printable
+ 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()
+
+ # get the files for this message
+ if msgid is None:
+ message_files = None
+ else:
+ message_files = messages.get(msgid, 'files')
+
+ # make sure the To line is always the same (for testing mostly)
+ sendto.sort()
+
+ # make sure we have a from address
+ if from_address is None:
+ from_address = self.db.config.TRACKER_EMAIL
+
+ # additional bit for after the From: "name"
+ from_tag = getattr(self.db.config, 'EMAIL_FROM_TAG', '')
+ if from_tag:
+ from_tag = ' ' + from_tag
+
+ subject = '[%s%s] %s'%(cn, nodeid, title)
+ author = (authname + from_tag, from_address)
+
+ # send an individual message per recipient?
+ if self.db.config.NOSY_EMAIL_SENDING != 'single':
+ sendto = [[address] for address in sendto]
+ else:
+ sendto = [sendto]
+
+ # 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
+ # and/or fixing some day
+ first = True
+ for sendto in sendto:
+ # create the message
+ mailer = Mailer(self.db.config)
+ message, writer = mailer.get_standard_message(sendto, subject,
+ author)
+
+ # 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 ids
+ if messageid:
+ writer.addheader('Message-Id', messageid)
+ if inreplyto:
+ writer.addheader('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)
+ 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))
+ else:
+ part.addheader('Content-Transfer-Encoding', '7bit')
+ body = part.startbody('text/plain')
+ body.write(content)
+ else:
+ # some other type, so encode it
+ if not mime_type:
+ # this should have been done when the file was saved
+ 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()
+ else:
+ writer.addheader('Content-Transfer-Encoding',
+ 'quoted-printable')
+ body = writer.startbody('text/plain; charset=%s'%charset)
+ body.write(content_encoded)
+
+ if first:
+ mailer.smtp_send(sendto + bcc_sendto, message)
+ else:
+ mailer.smtp_send(sendto, message)
+ first = False
+
+ def email_signature(self, nodeid, msgid):
+ ''' Add a signature to the e-mail with some useful information
+ '''
+ # simplistic check to see if the url is valid,
+ # then append a trailing slash if it is missing
+ base = self.db.config.TRACKER_WEB
+ if (not isinstance(base , type('')) or
+ not (base.startswith('http://') or base.startswith('https://'))):
+ web = "Configuration Error: TRACKER_WEB isn't a " \
+ "fully-qualified URL"
+ else:
+ if not base.endswith('/'):
+ base = base + '/'
+ web = base + self.classname + nodeid
+
+ # ensure the email address is properly quoted
+ email = straddr((self.db.config.TRACKER_NAME,
+ self.db.config.TRACKER_EMAIL))
+
+ line = '_' * max(len(web)+2, len(email))
+ return '\n%s\n%s\n<%s>\n%s'%(line, email, web, line)
+
+
+ def generateCreateNote(self, nodeid):
+ """Generate a create note that lists initial property values
+ """
+ cn = self.classname
+ cl = self.db.classes[cn]
+ props = cl.getprops(protected=0)
+
+ # list the values
+ m = []
+ prop_items = props.items()
+ prop_items.sort()
+ for propname, prop in prop_items:
+ value = cl.get(nodeid, propname, None)
+ # skip boring entries
+ if not value:
+ continue
+ if isinstance(prop, hyperdb.Link):
+ link = self.db.classes[prop.classname]
+ if value:
+ key = link.labelprop(default_to_id=1)
+ if key:
+ value = link.get(value, key)
+ else:
+ value = ''
+ elif isinstance(prop, hyperdb.Multilink):
+ if value is None: value = []
+ l = []
+ link = self.db.classes[prop.classname]
+ key = link.labelprop(default_to_id=1)
+ if key:
+ value = [link.get(entry, key) for entry in value]
+ value.sort()
+ value = ', '.join(value)
+ m.append('%s: %s'%(propname, value))
+ m.insert(0, '----------')
+ m.insert(0, '')
+ return '\n'.join(m)
+
+ def generateChangeNote(self, nodeid, oldvalues):
+ """Generate a change note that lists property changes
+ """
+ if not isinstance(oldvalues, type({})):
+ raise TypeError("'oldvalues' must be dict-like, not %s."%
+ type(oldvalues))
+
+ cn = self.classname
+ cl = self.db.classes[cn]
+ changed = {}
+ props = cl.getprops(protected=0)
+
+ # determine what changed
+ for key in oldvalues.keys():
+ if key in ['files','messages']:
+ continue
+ if key in ('actor', 'activity', 'creator', 'creation'):
+ continue
+ # not all keys from oldvalues might be available in database
+ # this happens when property was deleted
+ try:
+ new_value = cl.get(nodeid, key)
+ except KeyError:
+ continue
+ # the old value might be non existent
+ # this happens when property was added
+ try:
+ old_value = oldvalues[key]
+ if type(new_value) is type([]):
+ new_value.sort()
+ old_value.sort()
+ if new_value != old_value:
+ changed[key] = old_value
+ except:
+ changed[key] = new_value
+
+ # list the changes
+ m = []
+ changed_items = changed.items()
+ changed_items.sort()
+ for propname, oldvalue in changed_items:
+ prop = props[propname]
+ value = cl.get(nodeid, propname, None)
+ if isinstance(prop, hyperdb.Link):
+ link = self.db.classes[prop.classname]
+ key = link.labelprop(default_to_id=1)
+ if key:
+ if value:
+ value = link.get(value, key)
+ else:
+ value = ''
+ if oldvalue:
+ oldvalue = link.get(oldvalue, key)
+ else:
+ oldvalue = ''
+ change = '%s -> %s'%(oldvalue, value)
+ elif isinstance(prop, hyperdb.Multilink):
+ change = ''
+ if value is None: value = []
+ if oldvalue is None: oldvalue = []
+ l = []
+ link = self.db.classes[prop.classname]
+ key = link.labelprop(default_to_id=1)
+ # check for additions
+ for entry in value:
+ if entry in oldvalue: continue
+ if key:
+ l.append(link.get(entry, key))
+ else:
+ l.append(entry)
+ if l:
+ l.sort()
+ change = '+%s'%(', '.join(l))
+ l = []
+ # check for removals
+ for entry in oldvalue:
+ if entry in value: continue
+ if key:
+ l.append(link.get(entry, key))
+ else:
+ l.append(entry)
+ if l:
+ l.sort()
+ change += ' -%s'%(', '.join(l))
+ else:
+ change = '%s -> %s'%(oldvalue, value)
+ m.append('%s: %s'%(propname, change))
+ if m:
+ m.insert(0, '----------')
+ m.insert(0, '')
+ return '\n'.join(m)
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/scripts/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/scripts/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,3 @@
+*.pyc
+*.pyo
+*.cover
Added: tracker/vendor/roundup/current/roundup/scripts/__init__.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/scripts/__init__.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,26 @@
+#
+# 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: __init__.py,v 1.3 2004/02/11 23:55:10 richard Exp $
+
+'''Subpackage containing the modules that implement the command
+line tools.
+
+Note that these are imported by script stubs generated by "setup.py".
+'''
+__docformat__ = 'restructuredtext'
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/roundup/scripts/roundup_admin.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/scripts/roundup_admin.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,43 @@
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: roundup_admin.py,v 1.6 2004/02/11 23:55:10 richard Exp $
+
+"""Command-line script stub that calls the roundup.admin functions.
+"""
+__docformat__ = 'restructuredtext'
+
+# python version check
+from roundup import version_check
+
+# import the admin tool guts and make it go
+from roundup.admin import AdminTool
+from roundup.i18n import _
+
+import sys
+
+def run():
+ # time out after a minute if we can
+ import socket
+ if hasattr(socket, 'setdefaulttimeout'):
+ socket.setdefaulttimeout(60)
+ tool = AdminTool()
+ sys.exit(tool.main())
+
+if __name__ == '__main__':
+ run()
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/roundup/scripts/roundup_demo.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/scripts/roundup_demo.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,44 @@
+#! /usr/bin/env python
+#
+# 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
+
+from roundup import admin, configuration, demo, instance
+from roundup.i18n import _
+
+DEFAULT_HOME = './demo'
+
+def run():
+ home = DEFAULT_HOME
+ nuke = sys.argv[-1] == 'nuke'
+ # if there is no tracker in home, force nuke
+ try:
+ instance.open(home)
+ except configuration.NoConfigError:
+ nuke = 1
+ # if we are to create the tracker, prompt for home
+ if nuke:
+ if len(sys.argv) > 2:
+ backend = sys.argv[-2]
+ else:
+ backend = 'anydbm'
+ # FIXME: i'd like to have an option to abort the tracker creation
+ # say, by entering a single dot. but i cannot think of
+ # appropriate prompt for that.
+ home = raw_input(
+ _('Enter directory path to create demo tracker [%s]: ') % home)
+ if not home:
+ home = DEFAULT_HOME
+ # install
+ demo.install_demo(home, backend,
+ admin.AdminTool().listTemplates()['classic']['path'])
+ # run
+ demo.run_demo(home)
+
+if __name__ == '__main__':
+ run()
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/scripts/roundup_gettext.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/scripts/roundup_gettext.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,54 @@
+#! /usr/bin/env python
+#
+# Copyright 2004 Richard Jones (richard at mechanicalcat.net)
+#
+# $Id: roundup_gettext.py,v 1.1 2004/10/20 10:25:23 a1s Exp $
+
+"""Extract translatable strings from tracker templates"""
+
+import os
+import sys
+
+from roundup.i18n import _
+from roundup.cgi.TAL import talgettext
+
+# name of message template file.
+# i don't think this will ever need to be changed, but still...
+TEMPLATE_FILE = "messages.pot"
+
+def run():
+ # return unless command line arguments contain single directory path
+ if (len(sys.argv) != 2) or (sys.argv[1] in ("-h", "--help")):
+ print _("Usage: %(program)s <tracker home>") % {"program": sys.argv[0]}
+ return
+ # collect file paths of html templates
+ home = os.path.abspath(sys.argv[1])
+ htmldir = os.path.join(home, "html")
+ if os.path.isdir(htmldir):
+ # glob is not used because i want to match file names
+ # without case sensitivity, and that is easier done this way.
+ htmlfiles = [filename for filename in os.listdir(htmldir)
+ if os.path.isfile(os.path.join(htmldir, filename))
+ and filename.lower().endswith(".html")]
+ else:
+ htmlfiles = []
+ # return if no html files found
+ if not htmlfiles:
+ print _("No tracker templates found in directory %s") % home
+ return
+ # change to locale dir to have relative source references
+ locale = os.path.join(home, "locale")
+ if not os.path.isdir(locale):
+ os.mkdir(locale)
+ os.chdir(locale)
+ # tweak sys.argv as this is the only way to tell talgettext what to do
+ # Note: unix-style paths used instead of os.path.join deliberately
+ sys.argv[1:] = ["-o", TEMPLATE_FILE] \
+ + ["../html/" + filename for filename in htmlfiles]
+ # run
+ talgettext.main()
+
+if __name__ == "__main__":
+ run()
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/scripts/roundup_mailgw.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/scripts/roundup_mailgw.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,197 @@
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: roundup_mailgw.py,v 1.21 2005/07/18 01:19:57 richard Exp $
+
+"""Command-line script stub that calls the roundup.mailgw.
+"""
+__docformat__ = 'restructuredtext'
+
+# python version check
+from roundup import version_check
+from roundup import __version__ as roundup_version
+
+import sys, os, re, cStringIO, getopt, socket
+
+from roundup import mailgw
+from roundup.i18n import _
+
+def usage(args, message=None):
+ if message is not None:
+ print message
+ print _(
+"""Usage: %(program)s [-v] [-c] [[-C class] -S field=value]* <instance home> [method]
+
+Options:
+ -v: print version and exit
+ -c: default class of item to create (else the tracker's MAIL_DEFAULT_CLASS)
+ -C / -S: see below
+
+The roundup mail gateway may be called in one of four ways:
+ . with an instance home as the only argument,
+ . with both an instance home and a mail spool file,
+ . with both an instance home and a POP/APOP server account, or
+ . with both an instance home and a IMAP/IMAPS server account.
+
+It also supports optional -C and -S arguments that allows you to set a
+fields for a class created by the roundup-mailgw. The default class if
+not specified is msg, but the other classes: issue, file, user can
+also be used. The -S or --set options uses the same
+property=value[;property=value] notation accepted by the command line
+roundup command or the commands that can be given on the Subject line
+of an email message.
+
+It can let you set the type of the message on a per email address basis.
+
+PIPE:
+ In the first case, the mail gateway reads a single message from the
+ standard input and submits the message to the roundup.mailgw module.
+
+UNIX mailbox:
+ In the second case, the gateway reads all messages from the mail spool
+ file and submits each in turn to the roundup.mailgw module. The file is
+ emptied once all messages have been successfully handled. The file is
+ specified as:
+ mailbox /path/to/mailbox
+
+POP:
+ In the third case, the gateway reads all messages from the POP server
+ specified and submits each in turn to the roundup.mailgw module. The
+ server is specified as:
+ pop username:password at server
+ The username and password may be omitted:
+ 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.
+
+APOP:
+ Same as POP, but using Authenticated POP:
+ apop username:password at server
+
+IMAP:
+ Connect to an IMAP server. This supports the same notation as that of
+ POP mail.
+ imap username:password at server
+ It also allows you to specify a specific mailbox other than INBOX using
+ this format:
+ imap username:password at server mailbox
+
+IMAPS:
+ Connect to an IMAP server over ssl.
+ This supports the same notation as IMAP.
+ imaps username:password at server [mailbox]
+
+""")%{'program': args[0]}
+ return 1
+
+def main(argv):
+ '''Handle the arguments to the program and initialise environment.
+ '''
+ # take the argv array and parse it leaving the non-option
+ # arguments in the args array.
+ try:
+ optionsList, args = getopt.getopt(argv[1:], 'vcC:S:', ['set=',
+ 'class='])
+ except getopt.GetoptError:
+ # print help information and exit:
+ usage(argv)
+ sys.exit(2)
+
+ for (opt, arg) in optionsList:
+ if opt == '-v':
+ print '%s (python %s)'%(roundup_version, sys.version.split()[0])
+ return
+
+ # figure the instance home
+ if len(args) > 0:
+ instance_home = args[0]
+ else:
+ instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
+ if not (instance_home and os.path.isdir(instance_home)):
+ return usage(argv)
+
+ # get the instance
+ import roundup.instance
+ instance = roundup.instance.open(instance_home)
+
+ # get a mail handler
+ db = instance.open('admin')
+
+ # now wrap in try/finally so we always close the database
+ try:
+ if hasattr(instance, 'MailGW'):
+ handler = instance.MailGW(instance, db, optionsList)
+ else:
+ handler = mailgw.MailGW(instance, db, optionsList)
+
+ # if there's no more arguments, read a single message from stdin
+ if len(args) == 1:
+ return handler.do_pipe()
+
+ # otherwise, figure what sort of mail source to handle
+ if len(args) < 3:
+ return usage(argv, _('Error: not enough source specification information'))
+ source, specification = args[1:3]
+
+ # time out net connections after a minute if we can
+ if source not in ('mailbox', 'imaps'):
+ if hasattr(socket, 'setdefaulttimeout'):
+ socket.setdefaulttimeout(60)
+
+ if source == 'mailbox':
+ return handler.do_mailbox(specification)
+ elif source == 'pop':
+ m = re.match(r'((?P<user>[^:]+)(:(?P<pass>.+))?@)?(?P<server>.+)',
+ specification)
+ if m:
+ return handler.do_pop(m.group('server'), m.group('user'),
+ m.group('pass'))
+ return usage(argv, _('Error: pop specification not valid'))
+ 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 = 0
+ if source == 'imaps':
+ ssl = 1
+ mailbox = ''
+ if len(args) > 3:
+ mailbox = args[3]
+ return handler.do_imap(m.group('server'), m.group('user'),
+ m.group('pass'), mailbox, ssl)
+
+ return usage(argv, _('Error: The source must be either "mailbox",'
+ ' "pop", "apop", "imap" or "imaps"'))
+ finally:
+ # handler might have closed the initial db and opened a new one
+ handler.db.close()
+
+def run():
+ sys.exit(main(sys.argv))
+
+# call main
+if __name__ == '__main__':
+ run()
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/roundup/scripts/roundup_server.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/scripts/roundup_server.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,778 @@
+# 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.
+#
+
+"""Command-line script that runs a server over roundup.cgi.client.
+
+$Id: roundup_server.py,v 1.83 2006/04/27 04:59:37 richard Exp $
+"""
+__docformat__ = 'restructuredtext'
+
+import errno, cgi, getopt, os, socket, sys, traceback, urllib, time
+import ConfigParser, BaseHTTPServer, SocketServer, StringIO
+
+# python version check
+from roundup import configuration, version_check
+from roundup import __version__ as roundup_version
+
+# Roundup modules of use here
+from roundup.cgi import cgitb, client
+import roundup.instance
+from roundup.i18n import _
+
+# "default" favicon.ico
+# generate by using "icotool" and tools/base64
+import zlib, base64
+favico = zlib.decompress(base64.decodestring('''
+eJztjr1PmlEUh59XgVoshdYPWorFIhaRFq0t9pNq37b60lYSTRzcTFw6GAfj5gDYaF0dTB0MxMSE
+gQQd3FzKJiEC0UCIUUN1M41pV2JCXySg/0ITn5tfzvmdc+85FwT56HSc81UJjXJsk1UsNcsSqCk1
+BS64lK+vr7OyssLJyQl2ux2j0cjU1BQajYZIJEIwGMRms+H3+zEYDExOTjI2Nsbm5iZWqxWv18vW
+1hZDQ0Ok02kmJiY4Ojpienqa3d1dxsfHUSqVeDwe5ufnyeVyrK6u4nK5ODs7Y3FxEYfDwdzcHCaT
+icPDQ5LJJIIgMDIyQj6fZ39/n+3tbdbW1pAkiYWFBWZmZtjb2yMejzM8PEwgEMDn85HNZonFYqjV
+asLhMMvLy2QyGfR6PaOjowwODmKxWDg+PkalUhEKhSgUCiwtLWE2m9nZ2UGhULCxscHp6SmpVIpo
+NMrs7CwHBwdotVoSiQRXXPG/IzY7RHtt922xjFRb01H1XhKfPBNbi/7my7rrLXJ88eppvxwEfV3f
+NY3Y6exofVdsV3+2wnPFDdPjB83n7xuVpcFvygPbGwxF31LZIKrQDfR2Xvh7lmrX654L/7bvlnng
+bn3Zuj8M9Hepux6VfZtW1yA6K7cfGqVu8TL325u+fHTb71QKbk+7TZQ+lTc6RcnpqW8qmVQBoj/g
+23eo0sr/NIGvB37K+lOWXMvJ+uWFeKGU/03Cb7n3D4M3wxI=
+'''.strip()))
+
+DEFAULT_PORT = 8080
+
+# See what types of multiprocess server are available
+# Note: the order is important. Preferred multiprocess type
+# is the last element of this list.
+# "debug" means "none" + no tracker/template cache
+MULTIPROCESS_TYPES = ["debug", "none"]
+try:
+ import thread
+except ImportError:
+ pass
+else:
+ MULTIPROCESS_TYPES.append("thread")
+if hasattr(os, 'fork'):
+ MULTIPROCESS_TYPES.append("fork")
+DEFAULT_MULTIPROCESS = MULTIPROCESS_TYPES[-1]
+
+class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ TRACKER_HOMES = {}
+ TRACKERS = None
+ LOG_IPADDRESS = 1
+ DEBUG_MODE = False
+ CONFIG = None
+
+ def get_tracker(self, name):
+ """Return a tracker instance for given tracker name"""
+ # Note: try/except KeyError works faster that has_key() check
+ # if the key is usually found in the dictionary
+ #
+ # Return cached tracker instance if we have a tracker cache
+ if self.TRACKERS:
+ try:
+ return self.TRACKERS[name]
+ except KeyError:
+ pass
+ # No cached tracker. Look for home path.
+ try:
+ tracker_home = self.TRACKER_HOMES[name]
+ except KeyError:
+ raise client.NotFound
+ # open the instance
+ tracker = roundup.instance.open(tracker_home)
+ # and cache it if we have a tracker cache
+ if self.TRACKERS:
+ self.TRACKERS[name] = tracker
+ return tracker
+
+ def run_cgi(self):
+ """ Execute the CGI command. Wrap an innner call in an error
+ handler so all errors can be caught.
+ """
+ save_stdin = sys.stdin
+ sys.stdin = self.rfile
+ try:
+ self.inner_run_cgi()
+ except client.NotFound:
+ self.send_error(404, self.path)
+ except client.Unauthorised, message:
+ self.send_error(403, '%s (%s)'%(self.path, message))
+ except:
+ exc, val, tb = sys.exc_info()
+ if hasattr(socket, 'timeout') and isinstance(val, socket.timeout):
+ self.log_error('timeout')
+ else:
+ # it'd be nice to be able to detect if these are going to have
+ # any effect...
+ self.send_response(400)
+ self.send_header('Content-Type', 'text/html')
+ self.end_headers()
+ if self.DEBUG_MODE:
+ try:
+ reload(cgitb)
+ self.wfile.write(cgitb.breaker())
+ self.wfile.write(cgitb.html())
+ except:
+ s = StringIO.StringIO()
+ traceback.print_exc(None, s)
+ self.wfile.write("<pre>")
+ self.wfile.write(cgi.escape(s.getvalue()))
+ self.wfile.write("</pre>\n")
+ else:
+ # user feedback
+ self.wfile.write(cgitb.breaker())
+ ts = time.ctime()
+ self.wfile.write('''<p>%s: An error occurred. Please check
+ the server log for more infomation.</p>'''%ts)
+ # out to the logfile
+ print 'EXCEPTION AT', ts
+ traceback.print_exc()
+ sys.stdin = save_stdin
+
+ do_GET = do_POST = do_HEAD = run_cgi
+
+ def index(self):
+ ''' Print up an index of the available trackers
+ '''
+ keys = self.TRACKER_HOMES.keys()
+ if len(keys) == 1:
+ self.send_response(302)
+ self.send_header('Location', urllib.quote(keys[0]) + '/index')
+ else:
+ self.send_response(200)
+ self.send_header('Content-Type', 'text/html')
+ self.end_headers()
+ w = self.wfile.write
+ w(_('<html><head><title>Roundup trackers index</title></head>\n'
+ '<body><h1>Roundup trackers index</h1><ol>\n'))
+ keys.sort()
+ for tracker in keys:
+ w('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n'%{
+ 'tracker_url': urllib.quote(tracker),
+ 'tracker_name': cgi.escape(tracker)})
+ w('</ol></body></html>')
+
+ def inner_run_cgi(self):
+ ''' This is the inner part of the CGI handling
+ '''
+ rest = self.path
+
+ # file-like object for the favicon.ico file information
+ favicon_fileobj = None
+
+ if rest == '/favicon.ico':
+ # check to see if a custom favicon was specified, and set
+ # favicon_fileobj to the input file
+ if self.CONFIG is not None:
+ favicon_filepath = os.path.abspath(self.CONFIG['FAVICON'])
+
+ if os.access(favicon_filepath, os.R_OK):
+ favicon_fileobj = open(favicon_filepath, 'rb')
+
+
+ if favicon_fileobj is None:
+ favicon_fileobj = StringIO.StringIO(favico)
+
+ self.send_response(200)
+ self.send_header('Content-Type', 'image/x-icon')
+ self.end_headers()
+
+ # this bufsize is completely arbitrary, I picked 4K because it sounded good.
+ # if someone knows of a better buffer size, feel free to plug it in.
+ bufsize = 4 * 1024
+ Processing = True
+ while Processing:
+ data = favicon_fileobj.read(bufsize)
+ if len(data) > 0:
+ self.wfile.write(data)
+ else:
+ Processing = False
+
+ favicon_fileobj.close()
+
+ return
+
+ i = rest.rfind('?')
+ if i >= 0:
+ rest, query = rest[:i], rest[i+1:]
+ else:
+ query = ''
+
+ # no tracker - spit out the index
+ if rest == '/':
+ self.index()
+ return
+
+ # figure the tracker
+ l_path = rest.split('/')
+ tracker_name = urllib.unquote(l_path[1])
+
+ # handle missing trailing '/'
+ if len(l_path) == 2:
+ self.send_response(301)
+ # redirect - XXX https??
+ protocol = 'http'
+ url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
+ self.send_header('Location', url)
+ self.end_headers()
+ self.wfile.write('Moved Permanently')
+ return
+
+ # figure out what the rest of the path is
+ if len(l_path) > 2:
+ rest = '/'.join(l_path[2:])
+ else:
+ rest = '/'
+
+ # Set up the CGI environment
+ env = {}
+ env['TRACKER_NAME'] = tracker_name
+ env['REQUEST_METHOD'] = self.command
+ env['PATH_INFO'] = urllib.unquote(rest)
+ if query:
+ env['QUERY_STRING'] = query
+ if self.headers.typeheader is None:
+ env['CONTENT_TYPE'] = self.headers.type
+ else:
+ env['CONTENT_TYPE'] = self.headers.typeheader
+ length = self.headers.getheader('content-length')
+ if length:
+ env['CONTENT_LENGTH'] = length
+ co = filter(None, self.headers.getheaders('cookie'))
+ if co:
+ env['HTTP_COOKIE'] = ', '.join(co)
+ env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
+ env['SCRIPT_NAME'] = ''
+ env['SERVER_NAME'] = self.server.server_name
+ env['SERVER_PORT'] = str(self.server.server_port)
+ env['HTTP_HOST'] = self.headers['host']
+ if os.environ.has_key('CGI_SHOW_TIMING'):
+ env['CGI_SHOW_TIMING'] = os.environ['CGI_SHOW_TIMING']
+ env['HTTP_ACCEPT_LANGUAGE'] = self.headers.get('accept-language')
+
+ # do the roundup thing
+ tracker = self.get_tracker(tracker_name)
+ tracker.Client(tracker, self, env).main()
+
+ def address_string(self):
+ if self.LOG_IPADDRESS:
+ return self.client_address[0]
+ else:
+ host, port = self.client_address
+ return socket.getfqdn(host)
+
+ def log_message(self, format, *args):
+ ''' Try to *safely* log to stderr.
+ '''
+ try:
+ BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,
+ format, *args)
+ except IOError:
+ # stderr is no longer viable
+ pass
+
+def error():
+ exc_type, exc_value = sys.exc_info()[:2]
+ return _('Error: %s: %s' % (exc_type, exc_value))
+
+def setgid(group):
+ if group is None:
+ return
+ if not hasattr(os, 'setgid'):
+ return
+
+ # if root, setgid to the running user
+ if os.getuid():
+ print _('WARNING: ignoring "-g" argument, not root')
+ return
+
+ try:
+ import grp
+ except ImportError:
+ raise ValueError, _("Can't change groups - no grp module")
+ try:
+ try:
+ gid = int(group)
+ except ValueError:
+ gid = grp.getgrnam(group)[2]
+ else:
+ grp.getgrgid(gid)
+ except KeyError:
+ raise ValueError,_("Group %(group)s doesn't exist")%locals()
+ os.setgid(gid)
+
+def setuid(user):
+ if not hasattr(os, 'getuid'):
+ return
+
+ # People can remove this check if they're really determined
+ if user is None:
+ if os.getuid():
+ return
+ raise ValueError, _("Can't run as root!")
+
+ if os.getuid():
+ print _('WARNING: ignoring "-u" argument, not root')
+
+ try:
+ import pwd
+ except ImportError:
+ raise ValueError, _("Can't change users - no pwd module")
+ try:
+ try:
+ uid = int(user)
+ except ValueError:
+ uid = pwd.getpwnam(user)[2]
+ else:
+ pwd.getpwuid(uid)
+ except KeyError:
+ raise ValueError, _("User %(user)s doesn't exist")%locals()
+ os.setuid(uid)
+
+class TrackerHomeOption(configuration.FilePathOption):
+
+ # Tracker homes do not need any description strings
+ def format(self):
+ return "%(name)s = %(value)s\n" % {
+ "name": self.setting,
+ "value": self.value2str(self._value),
+ }
+
+class ServerConfig(configuration.Config):
+
+ SETTINGS = (
+ ("main", (
+ (configuration.Option, "host", "",
+ "Host name of the Roundup web server instance.\n"
+ "If empty, listen on all network interfaces."),
+ (configuration.IntegerNumberOption, "port", DEFAULT_PORT,
+ "Port to listen on."),
+ (configuration.NullableFilePathOption, "favicon", "favicon.ico",
+ "Path to favicon.ico image file."
+ " If unset, built-in favicon.ico is used."),
+ (configuration.NullableOption, "user", "",
+ "User ID as which the server will answer requests.\n"
+ "In order to use this option, "
+ "the server must be run initially as root.\n"
+ "Availability: Unix."),
+ (configuration.NullableOption, "group", "",
+ "Group ID as which the server will answer requests.\n"
+ "In order to use this option, "
+ "the server must be run initially as root.\n"
+ "Availability: Unix."),
+ (configuration.BooleanOption, "log_hostnames", "no",
+ "Log client machine names instead of IP addresses "
+ "(much slower)"),
+ (configuration.NullableFilePathOption, "pidfile", "",
+ "File to which the server records "
+ "the process id of the daemon.\n"
+ "If this option is not set, "
+ "the server will run in foreground\n"),
+ (configuration.NullableFilePathOption, "logfile", "",
+ "Log file path. If unset, log to stderr."),
+ (configuration.Option, "multiprocess", DEFAULT_MULTIPROCESS,
+ "Set processing of each request in separate subprocess.\n"
+ "Allowed values: %s." % ", ".join(MULTIPROCESS_TYPES)),
+ )),
+ ("trackers", (), "Roundup trackers to serve.\n"
+ "Each option in this section defines single Roundup tracker.\n"
+ "Option name identifies the tracker and will appear in the URL.\n"
+ "Option value is tracker home directory path.\n"
+ "The path may be either absolute or relative\n"
+ "to the directory containig this config file."),
+ )
+
+ # options recognized by config
+ OPTIONS = {
+ "host": "n:",
+ "port": "p:",
+ "group": "g:",
+ "user": "u:",
+ "logfile": "l:",
+ "pidfile": "d:",
+ "log_hostnames": "N",
+ "multiprocess": "t:",
+ }
+
+ def __init__(self, config_file=None):
+ configuration.Config.__init__(self, config_file, self.SETTINGS)
+ self.sections.append("trackers")
+
+ def _adjust_options(self, config):
+ """Add options for tracker homes"""
+ # return early if there are no tracker definitions.
+ # trackers must be specified on the command line.
+ if not config.has_section("trackers"):
+ return
+ # config defaults appear in all sections.
+ # filter them out.
+ defaults = config.defaults().keys()
+ for name in config.options("trackers"):
+ if name not in defaults:
+ self.add_option(TrackerHomeOption(self, "trackers", name))
+
+ def getopt(self, args, short_options="", long_options=(),
+ config_load_options=("C", "config"), **options
+ ):
+ options.update(self.OPTIONS)
+ return configuration.Config.getopt(self, args,
+ short_options, long_options, config_load_options, **options)
+
+ def _get_name(self):
+ return "Roundup server"
+
+ def trackers(self):
+ """Return tracker definitions as a list of (name, home) pairs"""
+ trackers = []
+ for option in self._get_section_options("trackers"):
+ trackers.append((option, os.path.abspath(
+ self["TRACKERS_" + option.upper()])))
+ return trackers
+
+ def set_logging(self):
+ """Initialise logging to the configured file, if any."""
+ # appending, unbuffered
+ sys.stdout = sys.stderr = open(self["LOGFILE"], 'a', 0)
+
+ def get_server(self):
+ """Return HTTP server object to run"""
+ # we don't want the cgi module interpreting the command-line args ;)
+ sys.argv = sys.argv[:1]
+
+ # preload all trackers unless we are in "debug" mode
+ tracker_homes = self.trackers()
+ if self["MULTIPROCESS"] == "debug":
+ trackers = None
+ else:
+ trackers = dict([(name, roundup.instance.open(home, optimize=1))
+ for (name, home) in tracker_homes])
+
+ # build customized request handler class
+ class RequestHandler(RoundupRequestHandler):
+ LOG_IPADDRESS = not self["LOG_HOSTNAMES"]
+ TRACKER_HOMES = dict(tracker_homes)
+ TRACKERS = trackers
+ DEBUG_MODE = self["MULTIPROCESS"] == "debug"
+ CONFIG = self
+
+ # obtain request server class
+ if self["MULTIPROCESS"] not in MULTIPROCESS_TYPES:
+ print _("Multiprocess mode \"%s\" is not available, "
+ "switching to single-process") % self["MULTIPROCESS"]
+ self["MULTIPROCESS"] = "none"
+ server_class = BaseHTTPServer.HTTPServer
+ elif self["MULTIPROCESS"] == "fork":
+ class ForkingServer(SocketServer.ForkingMixIn,
+ BaseHTTPServer.HTTPServer):
+ pass
+ server_class = ForkingServer
+ elif self["MULTIPROCESS"] == "thread":
+ class ThreadingServer(SocketServer.ThreadingMixIn,
+ BaseHTTPServer.HTTPServer):
+ pass
+ server_class = ThreadingServer
+ else:
+ server_class = BaseHTTPServer.HTTPServer
+ # obtain server before changing user id - allows to
+ # use port < 1024 if started as root
+ try:
+ httpd = server_class((self["HOST"], self["PORT"]), RequestHandler)
+ except socket.error, e:
+ if e[0] == errno.EADDRINUSE:
+ raise socket.error, \
+ _("Unable to bind to port %s, port already in use.") \
+ % self["PORT"]
+ raise
+ # change user and/or group
+ setgid(self["GROUP"])
+ setuid(self["USER"])
+ # return the server
+ return httpd
+
+try:
+ import win32serviceutil
+except:
+ RoundupService = None
+else:
+
+ # allow the win32
+ import win32service
+
+ class SvcShutdown(Exception):
+ pass
+
+ class RoundupService(win32serviceutil.ServiceFramework):
+
+ _svc_name_ = "roundup"
+ _svc_display_name_ = "Roundup Bug Tracker"
+
+ running = 0
+ server = None
+
+ def SvcDoRun(self):
+ import servicemanager
+ self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
+ config = ServerConfig()
+ (optlist, args) = config.getopt(sys.argv[1:])
+ if not config["LOGFILE"]:
+ servicemanager.LogMsg(servicemanager.EVENTLOG_ERROR_TYPE,
+ servicemanager.PYS_SERVICE_STOPPED,
+ (self._svc_display_name_, "\r\nMissing logfile option"))
+ self.ReportServiceStatus(win32service.SERVICE_STOPPED)
+ return
+ config.set_logging()
+ self.server = config.get_server()
+ self.running = 1
+ self.ReportServiceStatus(win32service.SERVICE_RUNNING)
+ servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
+ servicemanager.PYS_SERVICE_STARTED, (self._svc_display_name_,
+ " at %s:%s" % (config["HOST"], config["PORT"])))
+ while self.running:
+ self.server.handle_request()
+ servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
+ servicemanager.PYS_SERVICE_STOPPED,
+ (self._svc_display_name_, ""))
+ self.ReportServiceStatus(win32service.SERVICE_STOPPED)
+
+ def SvcStop(self):
+ self.running = 0
+ # make dummy connection to self to terminate blocking accept()
+ addr = self.server.socket.getsockname()
+ if addr[0] == "0.0.0.0":
+ addr = ("127.0.0.1", addr[1])
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect(addr)
+ sock.close()
+ self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
+
+def usage(message=''):
+ if RoundupService:
+ os_part = \
+""''' -c <Command> Windows Service options.
+ If you want to run the server as a Windows Service, you
+ must use configuration file to specify tracker homes.
+ Logfile option is required to run Roundup Tracker service.
+ Typing "roundup-server -c help" shows Windows Services
+ specifics.'''
+ else:
+ os_part = ""''' -u <UID> runs the Roundup web server as this UID
+ -g <GID> runs the Roundup web server as this GID
+ -d <PIDfile> run the server in the background and write the server's PID
+ to the file indicated by PIDfile. The -l option *must* be
+ specified if -d is used.'''
+ if message:
+ message += '\n'
+ print _('''%(message)sUsage: roundup-server [options] [name=tracker home]*
+
+Options:
+ -v print the Roundup version number and exit
+ -h print this text and exit
+ -S create or update configuration file and exit
+ -C <fname> use configuration file <fname>
+ -n <name> set the host name of the Roundup web server instance
+ -p <port> set the port to listen on (default: %(port)s)
+ -l <fname> log to the file indicated by fname instead of stderr/stdout
+ -N log client machine names instead of IP addresses (much slower)
+ -t <mode> multiprocess mode (default: %(mp_def)s).
+ Allowed values: %(mp_types)s.
+%(os_part)s
+
+Long options:
+ --version print the Roundup version number and exit
+ --help print this text and exit
+ --save-config create or update configuration file and exit
+ --config <fname> use configuration file <fname>
+ All settings of the [main] section of the configuration file
+ also may be specified in form --<name>=<value>
+
+Examples:
+
+ roundup-server -S -C /opt/roundup/etc/roundup-server.ini \\
+ -n localhost -p 8917 -l /var/log/roundup.log \\
+ support=/var/spool/roundup-trackers/support
+
+ roundup-server -C /opt/roundup/etc/roundup-server.ini
+
+ roundup-server support=/var/spool/roundup-trackers/support
+
+ roundup-server -d /var/run/roundup.pid -l /var/log/roundup.log \\
+ support=/var/spool/roundup-trackers/support
+
+Configuration file format:
+ Roundup Server configuration file has common .ini file format.
+ Configuration file created with 'roundup-server -S' contains
+ detailed explanations for each option. Please see that file
+ for option descriptions.
+
+How to use "name=tracker home":
+ These arguments set the tracker home(s) to use. The name is how the
+ tracker is identified in the URL (it's the first part of the URL path).
+ The tracker home is the directory that was identified when you did
+ "roundup-admin init". You may specify any number of these name=home
+ pairs on the command-line. Make sure the name part doesn't include
+ any url-unsafe characters like spaces, as these confuse IE.
+''') % {
+ "message": message,
+ "os_part": os_part,
+ "port": DEFAULT_PORT,
+ "mp_def": DEFAULT_MULTIPROCESS,
+ "mp_types": ", ".join(MULTIPROCESS_TYPES),
+}
+
+
+def daemonize(pidfile):
+ ''' Turn this process into a daemon.
+ - make sure the sys.std(in|out|err) are completely cut off
+ - make our parent PID 1
+
+ Write our new PID to the pidfile.
+
+ From A.M. Kuuchling (possibly originally Greg Ward) with
+ modification from Oren Tirosh, and finally a small mod from me.
+ '''
+ # Fork once
+ if os.fork() != 0:
+ os._exit(0)
+
+ # Create new session
+ os.setsid()
+
+ # Second fork to force PPID=1
+ pid = os.fork()
+ if pid:
+ pidfile = open(pidfile, 'w')
+ pidfile.write(str(pid))
+ pidfile.close()
+ os._exit(0)
+
+ os.chdir("/")
+
+ # close off std(in|out|err), redirect to devnull so the file
+ # descriptors can't be used again
+ devnull = os.open('/dev/null', 0)
+ os.dup2(devnull, 0)
+ os.dup2(devnull, 1)
+ os.dup2(devnull, 2)
+
+undefined = []
+def run(port=undefined, success_message=None):
+ ''' Script entry point - handle args and figure out what to to.
+ '''
+ # time out after a minute if we can
+ if hasattr(socket, 'setdefaulttimeout'):
+ socket.setdefaulttimeout(60)
+
+ config = ServerConfig()
+ # additional options
+ short_options = "hvS"
+ if RoundupService:
+ short_options += 'c'
+ try:
+ (optlist, args) = config.getopt(sys.argv[1:],
+ short_options, ("help", "version", "save-config",))
+ except (getopt.GetoptError, configuration.ConfigurationError), e:
+ usage(str(e))
+ return
+
+ # if running in windows service mode, don't do any other stuff
+ if ("-c", "") in optlist:
+ # acquire command line options recognized by service
+ short_options = "cC:"
+ long_options = ["config"]
+ for (long_name, short_name) in config.OPTIONS.items():
+ short_options += short_name
+ long_name = long_name.lower().replace("_", "-")
+ if short_name[-1] == ":":
+ long_name += "="
+ long_options.append(long_name)
+ optlist = getopt.getopt(sys.argv[1:], short_options, long_options)[0]
+ svc_args = []
+ for (opt, arg) in optlist:
+ if opt in ("-C", "-l"):
+ # make sure file name is absolute
+ svc_args.extend((opt, os.path.abspath(arg)))
+ elif opt in ("--config", "--logfile"):
+ # ditto, for long options
+ svc_args.append("=".join(opt, os.path.abspath(arg)))
+ elif opt != "-c":
+ svc_args.extend(opt)
+ RoundupService._exe_args_ = " ".join(svc_args)
+ # pass the control to serviceutil
+ win32serviceutil.HandleCommandLine(RoundupService,
+ argv=sys.argv[:1] + args)
+ return
+
+ # add tracker names from command line.
+ # this is done early to let '--save-config' handle the trackers.
+ if args:
+ for arg in args:
+ try:
+ name, home = arg.split('=')
+ except ValueError:
+ raise ValueError, _("Instances must be name=home")
+ config.add_option(TrackerHomeOption(config, "trackers", name))
+ config["TRACKERS_" + name.upper()] = home
+
+ # handle remaining options
+ if optlist:
+ for (opt, arg) in optlist:
+ if opt in ("-h", "--help"):
+ usage()
+ elif opt in ("-v", "--version"):
+ print '%s (python %s)' % (roundup_version,
+ sys.version.split()[0])
+ elif opt in ("-S", "--save-config"):
+ config.save()
+ print _("Configuration saved to %s") % config.filepath
+ # any of the above options prevent server from running
+ return
+
+ # port number in function arguments overrides config and command line
+ if port is not undefined:
+ config.PORT = port
+
+ if config["LOGFILE"]:
+ config["LOGFILE"] = os.path.abspath(config["LOGFILE"])
+ # switch logging from stderr/stdout to logfile
+ config.set_logging()
+ if config["PIDFILE"]:
+ config["PIDFILE"] = os.path.abspath(config["PIDFILE"])
+
+ # fork the server from our parent if a pidfile is specified
+ if config["PIDFILE"]:
+ if not hasattr(os, 'fork'):
+ print _("Sorry, you can't run the server as a daemon"
+ " on this Operating System")
+ sys.exit(0)
+ else:
+ daemonize(config["PIDFILE"])
+
+ # create the server
+ httpd = config.get_server()
+
+ if success_message:
+ print success_message
+ else:
+ print _('Roundup server started on %(HOST)s:%(PORT)s') \
+ % config
+
+ try:
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ print 'Keyboard Interrupt: exiting'
+
+if __name__ == '__main__':
+ run()
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/security.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/security.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,218 @@
+"""Handle the security declarations used in Roundup trackers.
+"""
+__docformat__ = 'restructuredtext'
+
+import weakref
+
+from roundup import hyperdb, support
+
+class Permission:
+ ''' Defines a Permission with the attributes
+ - name
+ - description
+ - klass (optional)
+ - properties (optional)
+ - check function (optional)
+
+ The klass may be unset, indicating that this permission is not
+ locked to a particular class. That means there may be multiple
+ Permissions for the same name for different classes.
+
+ If property names are set, permission is restricted to those
+ properties only.
+
+ If check function is set, permission is granted only when
+ the function returns value interpreted as boolean true.
+ The function is called with arguments db, userid, itemid.
+ '''
+ def __init__(self, name='', description='', klass=None,
+ properties=None, check=None):
+ self.name = name
+ self.description = description
+ self.klass = klass
+ self.properties = properties
+ self._properties_dict = support.TruthDict(properties)
+ self.check = check
+
+ def test(self, db, permission, classname, property, userid, itemid):
+ if permission != self.name:
+ return 0
+
+ # are we checking the correct class
+ if self.klass is not None and self.klass != classname:
+ return 0
+
+ # what about property?
+ if property is not None and not self._properties_dict[property]:
+ return 0
+
+ # check code
+ if itemid is not None and self.check is not None:
+ if not self.check(db, userid, itemid):
+ return 0
+
+ # we have a winner
+ return 1
+
+ def __repr__(self):
+ return '<Permission 0x%x %r,%r,%r,%r>'%(id(self), self.name,
+ self.klass, self.properties, self.check)
+
+ def __cmp__(self, other):
+ if self.name != other.name:
+ return cmp(self.name, other.name)
+
+ if self.klass != other.klass: return 1
+ if self.properties != other.properties: return 1
+ if self.check != other.check: return 1
+
+ # match
+ return 0
+
+class Role:
+ ''' Defines a Role with the attributes
+ - name
+ - description
+ - permissions
+ '''
+ def __init__(self, name='', description='', permissions=None):
+ self.name = name.lower()
+ self.description = description
+ if permissions is None:
+ permissions = []
+ self.permissions = permissions
+
+ def __repr__(self):
+ return '<Role 0x%x %r,%r>'%(id(self), self.name, self.permissions)
+
+class Security:
+ def __init__(self, db):
+ ''' Initialise the permission and role classes, and add in the
+ base roles (for admin user).
+ '''
+ self.db = weakref.proxy(db) # use a weak ref to avoid circularity
+
+ # permssions are mapped by name to a list of Permissions by class
+ self.permission = {}
+
+ # roles are mapped by name to the Role
+ self.role = {}
+
+ # the default Roles
+ self.addRole(name="User", description="A regular user, no privs")
+ 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)
+
+ # initialise the permissions and roles needed for the UIs
+ from roundup.cgi import client
+ client.initialiseSecurity(self)
+ from roundup import mailgw
+ mailgw.initialiseSecurity(self)
+
+ def getPermission(self, permission, classname=None, properties=None,
+ check=None):
+ ''' Find the Permission matching the name and for the class, if the
+ classname is specified.
+
+ Raise ValueError if there is no exact match.
+ '''
+ if not self.permission.has_key(permission):
+ raise ValueError, 'No permission "%s" defined'%permission
+
+ if classname:
+ try:
+ self.db.getclass(classname)
+ except KeyError:
+ raise ValueError, 'No class "%s" defined'%classname
+
+ # look through all the permissions of the given name
+ tester = Permission(permission, klass=classname, properties=properties,
+ check=check)
+ for perm in self.permission[permission]:
+ if perm == tester:
+ return perm
+ raise ValueError, 'No permission "%s" defined for "%s"'%(permission,
+ classname)
+
+ def hasPermission(self, permission, userid, classname=None,
+ property=None, itemid=None):
+ '''Look through all the Roles, and hence Permissions, and
+ see if "permission" exists given the constraints of
+ classname, property and itemid.
+
+ If classname is specified (and only classname) then the
+ search will match if there is *any* Permission for that
+ classname, even if the Permission has additional
+ constraints.
+
+ If property is specified, the Permission matched must have
+ either no properties listed or the property must appear in
+ the list.
+
+ If itemid is specified, the Permission matched must have
+ either no check function defined or the check function,
+ when invoked, must return a True value.
+
+ Note that this functionality is actually implemented by the
+ Permission.test() method.
+ '''
+ roles = self.db.user.get(userid, 'roles')
+ if roles is None:
+ return 0
+ if itemid and classname is None:
+ raise ValueError, 'classname must accompany itemid'
+ for rolename in [x.lower().strip() for x in roles.split(',')]:
+ if not rolename or not self.role.has_key(rolename):
+ continue
+ # for each of the user's Roles, check the permissions
+ for perm in self.role[rolename].permissions:
+ # permission match?
+ if perm.test(self.db, permission, classname, property,
+ userid, itemid):
+ return 1
+ return 0
+
+ def addPermission(self, **propspec):
+ ''' Create a new Permission with the properties defined in
+ 'propspec'. See the Permission class for the possible
+ keyword args.
+ '''
+ perm = Permission(**propspec)
+ self.permission.setdefault(perm.name, []).append(perm)
+ return perm
+
+ def addRole(self, **propspec):
+ ''' Create a new Role with the properties defined in 'propspec'
+ '''
+ role = Role(**propspec)
+ self.role[role.name] = role
+ return role
+
+ def addPermissionToRole(self, rolename, permission, classname=None,
+ properties=None, check=None):
+ ''' Add the permission to the role's permission list.
+
+ 'rolename' is the name of the role to add the permission to.
+
+ 'permission' is either a Permission *or* a permission name
+ accompanied by 'classname' (thus in the second case a Permission
+ is obtained by passing 'permission' and 'classname' to
+ self.getPermission)
+ '''
+ if not isinstance(permission, Permission):
+ permission = self.getPermission(permission, classname,
+ properties, check)
+ role = self.role[rolename.lower()]
+ role.permissions.append(permission)
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/roundup/support.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/support.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,121 @@
+"""Implements various support classes and functions used in a number of
+places in Roundup code.
+"""
+
+__docformat__ = 'restructuredtext'
+
+import os, time, sys
+
+class TruthDict:
+ '''Returns True for valid keys, False for others.
+ '''
+ def __init__(self, keys):
+ if keys:
+ self.keys = {}
+ for col in keys:
+ self.keys[col] = 1
+ else:
+ self.__getitem__ = lambda name: 1
+
+ def __getitem__(self, name):
+ return self.keys.has_key(name)
+
+def ensureParentsExist(dest):
+ if not os.path.exists(os.path.dirname(dest)):
+ os.makedirs(os.path.dirname(dest))
+
+class PrioList:
+ '''Manages a sorted list.
+
+ Currently only implements method 'append' and iteration from a
+ full list interface.
+ Implementation: We manage a "sorted" status and sort on demand.
+ Appending to the list will require re-sorting before use.
+ >>> p = PrioList ()
+ >>> for i in 5,7,1,-1 :
+ ... p.append (i)
+ ...
+ >>> for k in p :
+ ... print k
+ ...
+ -1
+ 1
+ 5
+ 7
+
+ '''
+ def __init__(self):
+ self.list = []
+ self.sorted = True
+
+ def append(self, item):
+ self.list.append (item)
+ self.sorted = False
+
+ def __iter__(self):
+ if not self.sorted :
+ self.list.sort ()
+ self.sorted = True
+ return iter (self.list)
+
+class Progress:
+ '''Progress display for console applications.
+
+ See __main__ block at end of file for sample usage.
+ '''
+ def __init__(self, info, sequence):
+ self.info = info
+ self.sequence = iter(sequence)
+ self.total = len(sequence)
+ self.start = self.now = time.time()
+ self.num = 0
+ self.stepsize = self.total / 100 or 1
+ self.steptimes = []
+ self.display()
+
+ def __iter__(self): return self
+
+ def next(self):
+ self.num += 1
+
+ if self.num > self.total:
+ print self.info, 'done', ' '*(75-len(self.info)-6)
+ sys.stdout.flush()
+ return self.sequence.next()
+
+ if self.num % self.stepsize:
+ return self.sequence.next()
+
+ self.display()
+ return self.sequence.next()
+
+ def display(self):
+ # figure how long we've spent - guess how long to go
+ now = time.time()
+ steptime = now - self.now
+ self.steptimes.insert(0, steptime)
+ if len(self.steptimes) > 5:
+ self.steptimes.pop()
+ steptime = sum(self.steptimes) / len(self.steptimes)
+ self.now = now
+ eta = steptime * ((self.total - self.num)/self.stepsize)
+
+ # tell it like it is (or might be)
+ if now - self.start > 3:
+ M = eta / 60
+ H = M / 60
+ M = M % 60
+ S = eta % 60
+ if self.total:
+ s = '%s %2d%% (ETA %02d:%02d:%02d)'%(self.info,
+ self.num * 100. / self.total, H, M, S)
+ else:
+ s = '%s 0%% (ETA %02d:%02d:%02d)'%(self.info, H, M, S)
+ elif self.total:
+ s = '%s %2d%%'%(self.info, self.num * 100. / self.total)
+ else:
+ s = '%s %d done'%(self.info, self.num)
+ sys.stdout.write(s + ' '*(75-len(s)) + '\r')
+ sys.stdout.flush()
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/roundup/token.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/token.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,118 @@
+#
+# Copyright (c) 2001 Richard Jones, richard at bofh.asn.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.
+#
+# This module is distributed in the hope that it will be useful,
+# 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 $
+#
+
+"""This module provides the tokeniser used by roundup-admin.
+"""
+__docformat__ = 'restructuredtext'
+
+def token_split(s, whitespace=' \r\n\t', quotes='\'"',
+ escaped={'r':'\r', 'n':'\n', 't':'\t'}):
+ '''Split the string up into tokens. An occurence of a ``'`` or ``"`` in
+ the input will cause the splitter to ignore whitespace until a matching
+ quote char is found. Embedded non-matching quote chars are also skipped.
+
+ Whitespace and quoting characters may be escaped using a backslash.
+ ``\r``, ``\n`` and ``\t`` are converted to carriage-return, newline and
+ tab. All other backslashed characters are left as-is.
+
+ Valid examples::
+
+ hello world (2 tokens: hello, world)
+ "hello world" (1 token: hello world)
+ "Roch'e" Compaan (2 tokens: Roch'e Compaan)
+ Roch\'e Compaan (2 tokens: Roch'e Compaan)
+ address="1 2 3" (1 token: address=1 2 3)
+ \\ (1 token: \)
+ \n (1 token: a newline)
+ \o (1 token: \o)
+
+ Invalid examples::
+
+ "hello world (no matching quote)
+ Roch'e Compaan (no matching quote)
+ '''
+ l = []
+ pos = 0
+ NEWTOKEN = 'newtoken'
+ TOKEN = 'token'
+ QUOTE = 'quote'
+ ESCAPE = 'escape'
+ quotechar = ''
+ state = NEWTOKEN
+ oldstate = '' # one-level state stack ;)
+ length = len(s)
+ finish = 0
+ token = ''
+ while 1:
+ # end of string, finish off the current token
+ if pos == length:
+ if state == QUOTE: raise ValueError, "unmatched quote"
+ elif state == TOKEN: l.append(token)
+ break
+ c = s[pos]
+ if state == NEWTOKEN:
+ # looking for a new token
+ if c in quotes:
+ # quoted token
+ state = QUOTE
+ quotechar = c
+ pos = pos + 1
+ continue
+ elif c in whitespace:
+ # skip whitespace
+ pos = pos + 1
+ continue
+ elif c == '\\':
+ pos = pos + 1
+ oldstate = TOKEN
+ state = ESCAPE
+ continue
+ # otherwise we have a token
+ state = TOKEN
+ elif state == TOKEN:
+ if c in whitespace:
+ # have a token, and have just found a whitespace terminator
+ l.append(token)
+ pos = pos + 1
+ state = NEWTOKEN
+ token = ''
+ continue
+ elif c in quotes:
+ # have a token, just found embedded quotes
+ state = QUOTE
+ quotechar = c
+ pos = pos + 1
+ continue
+ elif c == '\\':
+ pos = pos + 1
+ oldstate = state
+ state = ESCAPE
+ continue
+ elif state == QUOTE and c == quotechar:
+ # in a quoted token and found a matching quote char
+ pos = pos + 1
+ # now we're looking for whitespace
+ state = TOKEN
+ continue
+ elif state == ESCAPE:
+ # escaped-char conversions (t, r, n)
+ # TODO: octal, hexdigit
+ state = oldstate
+ if escaped.has_key(c):
+ c = escaped[c]
+ # just add this char to the token and move along
+ token = token + c
+ pos = pos + 1
+ return l
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/roundup/version_check.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/roundup/version_check.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: version_check.py,v 1.4 2004/02/11 23:55:08 richard Exp $
+
+"""Enforces the minimum Python version that Roundup requires.
+"""
+__docformat__ = 'restructuredtext'
+
+import sys
+if not hasattr(sys, 'version_info') or sys.version_info[:3] < (2,1,1):
+ print "Content-Type: text/plain\n"
+ print "Roundup requires Python 2.1.1 or newer."
+ sys.exit(0)
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/run_tests.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/run_tests.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,887 @@
+#! /usr/bin/env python
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+test.py [-aBbcdDfgGhLmprtTuv] [modfilter [testfilter]]
+
+Test harness.
+
+-a level
+--all
+ Run the tests at the given level. Any test at a level at or below this is
+ run, any test at a level above this is not run. Level 0 runs all tests.
+ The default is to run tests at level 1. --all is a shortcut for -a 0.
+
+-b
+--build
+ Run "python setup.py build" before running tests, where "python"
+ is the version of python used to run test.py. Highly recommended.
+ Tests will be run from the build directory. (Note: In Python < 2.3
+ the -q flag is added to the setup.py command line.)
+
+-B
+ Run "python setup.py build_ext -i" before running tests. Tests will be
+ run from the source directory.
+
+-c use pychecker
+
+-d
+ Instead of the normal test harness, run a debug version which
+ doesn't catch any exceptions. This is occasionally handy when the
+ unittest code catching the exception doesn't work right.
+ Unfortunately, the debug harness doesn't print the name of the
+ test, so Use With Care.
+
+--dir directory
+ Option to limit where tests are searched for. This is
+ important when you *really* want to limit the code that gets run.
+ For example, if refactoring interfaces, you don't want to see the way
+ you have broken setups for tests in other packages. You *just* want to
+ run the interface tests.
+
+-D
+ Works like -d, except that it loads pdb when an exception occurs.
+
+-f
+ Run functional tests instead of unit tests.
+
+-g threshold
+ Set the garbage collector generation0 threshold. This can be used to
+ stress memory and gc correctness. Some crashes are only reproducible when
+ the threshold is set to 1 (agressive garbage collection). Do "-g 0" to
+ disable garbage collection altogether.
+
+-G gc_option
+ Set the garbage collection debugging flags. The argument must be one
+ of the DEBUG_ flags defined bythe Python gc module. Multiple options
+ can be specified by using "-G OPTION1 -G OPTION2."
+
+--libdir test_root
+ Search for tests starting in the specified start directory
+ (useful for testing components being developed outside the main
+ "src" or "build" trees).
+
+--keepbytecode
+ Do not delete all stale bytecode before running tests
+
+-L
+ Keep running the selected tests in a loop. You may experience
+ memory leakage.
+
+-t
+ Time the individual tests and print a list of the top 50, sorted from
+ longest to shortest.
+
+-p
+ Show running progress. It can be combined with -v or -vv.
+
+-r
+ Look for refcount problems.
+ This requires that Python was built --with-pydebug.
+
+-T
+ Use the trace module from Python for code coverage. XXX This only works
+ if trace.py is explicitly added to PYTHONPATH. The current utility writes
+ coverage files to a directory named `coverage' that is parallel to
+ `build'. It also prints a summary to stdout.
+
+-v
+ Verbose output. With one -v, unittest prints a dot (".") for each test
+ run. With -vv, unittest prints the name of each test (for some definition
+ of "name" ...). With no -v, unittest is silent until the end of the run,
+ except when errors occur.
+
+ When -p is also specified, the meaning of -v is sligtly changed. With
+ -p and no -v only the percent indicator is displayed. With -p and -v
+ the test name of the current test is shown to the right of the percent
+ indicator. With -p and -vv the test name is not truncated to fit into
+ 80 columns and it is not cleared after the test finishes.
+
+-u
+-m
+ Use the PyUnit GUI instead of output to the command line. The GUI imports
+ tests on its own, taking care to reload all dependencies on each run. The
+ debug (-d), verbose (-v), progress (-p), and Loop (-L) options will be
+ ignored. The testfilter filter is also not applied.
+
+ -m starts the gui minimized. Double-clicking the progress bar will start
+ the import and run all tests.
+
+
+modfilter
+testfilter
+ Case-sensitive regexps to limit which tests are run, used in search
+ (not match) mode.
+ In an extension of Python regexp notation, a leading "!" is stripped
+ and causes the sense of the remaining regexp to be negated (so "!bc"
+ matches any string that does not match "bc", and vice versa).
+ By default these act like ".", i.e. nothing is excluded.
+
+ modfilter is applied to a test file's path, starting at "build" and
+ including (OS-dependent) path separators.
+
+ testfilter is applied to the (method) name of the unittest methods
+ contained in the test files whose paths modfilter matched.
+
+Extreme (yet useful) examples:
+
+ test.py -vvb . "^testWriteClient$"
+
+ Builds the project silently, then runs unittest in verbose mode on all
+ tests whose names are precisely "testWriteClient". Useful when
+ debugging a specific test.
+
+ test.py -vvb . "!^testWriteClient$"
+
+ As before, but runs all tests whose names aren't precisely
+ "testWriteClient". Useful to avoid a specific failing test you don't
+ want to deal with just yet.
+
+ test.py -m . "!^testWriteClient$"
+
+ As before, but now opens up a minimized PyUnit GUI window (only showing
+ the progress bar). Useful for refactoring runs where you continually want
+ to make sure all tests still pass.
+"""
+
+import gc
+import os
+import re
+import pdb
+import sys
+import threading # just to get at Thread objects created by tests
+import time
+import traceback
+import unittest
+import warnings
+
+from distutils.util import get_platform
+
+PLAT_SPEC = "%s-%s" % (get_platform(), sys.version[0:3])
+
+class ImmediateTestResult(unittest._TextTestResult):
+
+ __super_init = unittest._TextTestResult.__init__
+ __super_startTest = unittest._TextTestResult.startTest
+ __super_printErrors = unittest._TextTestResult.printErrors
+
+ def __init__(self, stream, descriptions, verbosity, debug=0,
+ count=None, progress=0):
+ self.__super_init(stream, descriptions, verbosity)
+ self._debug = debug
+ self._progress = progress
+ self._progressWithNames = 0
+ self._count = count
+ self._testtimes = {}
+ if progress and verbosity == 1:
+ self.dots = 0
+ self._progressWithNames = 1
+ self._lastWidth = 0
+ self._maxWidth = 80
+ try:
+ import curses
+ except ImportError:
+ pass
+ else:
+ curses.setupterm()
+ self._maxWidth = curses.tigetnum('cols')
+ self._maxWidth -= len("xxxx/xxxx (xxx.x%): ") + 1
+
+ def stopTest(self, test):
+ self._testtimes[test] = time.time() - self._testtimes[test]
+ if gc.garbage:
+ print "The following test left garbage:"
+ print test
+ print gc.garbage
+ # eat the garbage here, so that the garbage isn't
+ # printed for every subsequent test.
+ gc.garbage[:] = []
+
+ # Did the test leave any new threads behind?
+ new_threads = [t for t in threading.enumerate()
+ if (t.isAlive()
+ and
+ t not in self._threads)]
+ if new_threads:
+ print "The following test left new threads behind:"
+ print test
+ print "New thread(s):", new_threads
+
+ def print_times(self, stream, count=None):
+ results = self._testtimes.items()
+ results.sort(lambda x, y: cmp(y[1], x[1]))
+ if count:
+ n = min(count, len(results))
+ if n:
+ print >>stream, "Top %d longest tests:" % n
+ else:
+ n = len(results)
+ if not n:
+ return
+ for i in range(n):
+ print >>stream, "%6dms" % int(results[i][1] * 1000), results[i][0]
+
+ def _print_traceback(self, msg, err, test, errlist):
+ if self.showAll or self.dots or self._progress:
+ self.stream.writeln("\n")
+ self._lastWidth = 0
+
+ tb = "".join(traceback.format_exception(*err))
+ self.stream.writeln(msg)
+ self.stream.writeln(tb)
+ errlist.append((test, tb))
+
+ def startTest(self, test):
+ if self._progress:
+ self.stream.write("\r%4d" % (self.testsRun + 1))
+ if self._count:
+ self.stream.write("/%d (%5.1f%%)" % (self._count,
+ (self.testsRun + 1) * 100.0 / self._count))
+ if self.showAll:
+ self.stream.write(": ")
+ elif self._progressWithNames:
+ # XXX will break with multibyte strings
+ name = self.getShortDescription(test)
+ width = len(name)
+ if width < self._lastWidth:
+ name += " " * (self._lastWidth - width)
+ self.stream.write(": %s" % name)
+ self._lastWidth = width
+ self.stream.flush()
+ self._threads = threading.enumerate()
+ self.__super_startTest(test)
+ self._testtimes[test] = time.time()
+
+ def getShortDescription(self, test):
+ s = self.getDescription(test)
+ if len(s) > self._maxWidth:
+ pos = s.find(" (")
+ if pos >= 0:
+ w = self._maxWidth - (pos + 5)
+ if w < 1:
+ # first portion (test method name) is too long
+ s = s[:self._maxWidth-3] + "..."
+ else:
+ pre = s[:pos+2]
+ post = s[-w:]
+ s = "%s...%s" % (pre, post)
+ return s[:self._maxWidth]
+
+ def addError(self, test, err):
+ if self._progress:
+ self.stream.write("\r")
+ if self._debug:
+ raise err[0], err[1], err[2]
+ self._print_traceback("Error in test %s" % test, err,
+ test, self.errors)
+
+ def addFailure(self, test, err):
+ if self._progress:
+ self.stream.write("\r")
+ if self._debug:
+ raise err[0], err[1], err[2]
+ self._print_traceback("Failure in test %s" % test, err,
+ test, self.failures)
+
+ def printErrors(self):
+ if self._progress and not (self.dots or self.showAll):
+ self.stream.writeln()
+ self.__super_printErrors()
+
+ def printErrorList(self, flavor, errors):
+ for test, err in errors:
+ self.stream.writeln(self.separator1)
+ self.stream.writeln("%s: %s" % (flavor, self.getDescription(test)))
+ self.stream.writeln(self.separator2)
+ self.stream.writeln(err)
+
+
+class ImmediateTestRunner(unittest.TextTestRunner):
+
+ __super_init = unittest.TextTestRunner.__init__
+
+ def __init__(self, **kwarg):
+ debug = kwarg.get("debug")
+ if debug is not None:
+ del kwarg["debug"]
+ progress = kwarg.get("progress")
+ if progress is not None:
+ del kwarg["progress"]
+ self.__super_init(**kwarg)
+ self._debug = debug
+ self._progress = progress
+
+ def _makeResult(self):
+ return ImmediateTestResult(self.stream, self.descriptions,
+ self.verbosity, debug=self._debug,
+ count=self._count, progress=self._progress)
+
+ def run(self, test):
+ self._count = test.countTestCases()
+ return unittest.TextTestRunner.run(self, test)
+
+# setup list of directories to put on the path
+class PathInit:
+ def __init__(self, build, build_inplace, libdir=None):
+ self.inplace = None
+ # Figure out if we should test in-place or test in-build. If the -b
+ # or -B option was given, test in the place we were told to build in.
+ # Otherwise, we'll look for a build directory and if we find one,
+ # we'll test there, otherwise we'll test in-place.
+ if build:
+ self.inplace = build_inplace
+ if self.inplace is None:
+ # Need to figure it out
+ if os.path.isdir(os.path.join("build", "lib.%s" % PLAT_SPEC)):
+ self.inplace = 0
+ else:
+ self.inplace = 1
+ # Calculate which directories we're going to add to sys.path, and cd
+ # to the appropriate working directory
+ org_cwd = os.getcwd()
+ if self.inplace:
+ self.libdir = "src"
+ else:
+ self.libdir = "lib.%s" % PLAT_SPEC
+ os.chdir("build")
+ # Hack sys.path
+ self.cwd = os.getcwd()
+ sys.path.insert(0, os.path.join(self.cwd, self.libdir))
+ # Hack again for external products.
+ global functional
+ kind = functional and "functional" or "unit"
+ if libdir:
+ extra = os.path.join(org_cwd, libdir)
+ print "Running %s tests from %s" % (kind, extra)
+ self.libdir = extra
+ sys.path.insert(0, extra)
+ else:
+ print "Running %s tests from %s" % (kind, self.cwd)
+ # Make sure functional tests find ftesting.zcml
+ if functional:
+ config_file = 'ftesting.zcml'
+ if not self.inplace:
+ # We chdired into build, so ftesting.zcml is in the
+ # parent directory
+ config_file = os.path.join('..', 'ftesting.zcml')
+ print "Parsing %s" % config_file
+ from zope.testing.functional import FunctionalTestSetup
+ FunctionalTestSetup(config_file)
+
+def match(rx, s):
+ if not rx:
+ return 1
+ if rx[0] == "!":
+ return re.search(rx[1:], s) is None
+ else:
+ return re.search(rx, s) is not None
+
+class TestFileFinder:
+ def __init__(self, prefix):
+ self.files = []
+ self._plen = len(prefix)
+ if not prefix.endswith(os.sep):
+ self._plen += 1
+ global functional
+ if functional:
+ self.dirname = "ftest"
+ else:
+ self.dirname = "test"
+
+ def visit(self, rx, dir, files):
+ if os.path.split(dir)[1] != self.dirname:
+ return
+ # ignore tests that aren't in packages
+ if not "__init__.py" in files:
+ if not files or files == ["CVS"]:
+ return
+ print "not a package", dir
+ return
+
+ # Put matching files in matches. If matches is non-empty,
+ # then make sure that the package is importable.
+ matches = []
+ for file in files:
+ if file.startswith('test') and os.path.splitext(file)[-1] == '.py':
+ path = os.path.join(dir, file)
+ if match(rx, path):
+ matches.append(path)
+
+ # ignore tests when the package can't be imported, possibly due to
+ # dependency failures.
+ pkg = dir[self._plen:].replace(os.sep, '.')
+ try:
+ __import__(pkg)
+ # We specifically do not want to catch ImportError since that's useful
+ # information to know when running the tests.
+ except RuntimeError, e:
+ if VERBOSE:
+ print "skipping %s because: %s" % (pkg, e)
+ return
+ else:
+ self.files.extend(matches)
+
+ def module_from_path(self, path):
+ """Return the Python package name indicated by the filesystem path."""
+ assert path.endswith(".py")
+ path = path[self._plen:-3]
+ mod = path.replace(os.sep, ".")
+ return mod
+
+def walk_with_symlinks(top, func, arg):
+ """Like os.path.walk, but follows symlinks on POSIX systems.
+
+ This could theoreticaly result in an infinite loop, if you create symlink
+ cycles in your Zope sandbox, so don't do that.
+ """
+ try:
+ names = os.listdir(top)
+ except os.error:
+ return
+ func(arg, top, names)
+ exceptions = ('.', '..')
+ for name in names:
+ if name not in exceptions:
+ name = os.path.join(top, name)
+ if os.path.isdir(name):
+ walk_with_symlinks(name, func, arg)
+
+
+def check_test_dir():
+ global test_dir
+ if test_dir and not os.path.exists(test_dir):
+ d = pathinit.libdir
+ d = os.path.join(d, test_dir)
+ if os.path.exists(d):
+ if not os.path.isdir(d):
+ raise ValueError(
+ "%s does not exist and %s is not a directory"
+ % (test_dir, d)
+ )
+ test_dir = d
+ else:
+ raise ValueError("%s does not exist!" % test_dir)
+
+
+def find_tests(rx):
+ global finder
+ finder = TestFileFinder(pathinit.libdir)
+
+ check_test_dir()
+ walkdir = test_dir or pathinit.libdir
+ walk_with_symlinks(walkdir, finder.visit, rx)
+ return finder.files
+
+def package_import(modname):
+ mod = __import__(modname)
+ for part in modname.split(".")[1:]:
+ mod = getattr(mod, part)
+ return mod
+
+def get_suite(file):
+ modname = finder.module_from_path(file)
+ try:
+ mod = package_import(modname)
+ except ImportError, err:
+ # print traceback
+ print "Error importing %s\n%s" % (modname, err)
+ traceback.print_exc()
+ if debug:
+ raise
+ return None
+ try:
+ suite_func = mod.test_suite
+ except AttributeError:
+ print "No test_suite() in %s" % file
+ return None
+ return suite_func()
+
+def filter_testcases(s, rx):
+ new = unittest.TestSuite()
+ for test in s._tests:
+ # See if the levels match
+ dolevel = (level == 0) or level >= getattr(test, "level", 0)
+ if not dolevel:
+ continue
+ if isinstance(test, unittest.TestCase):
+ name = test.id() # Full test name: package.module.class.method
+ name = name[1 + name.rfind("."):] # extract method name
+ if not rx or match(rx, name):
+ new.addTest(test)
+ else:
+ filtered = filter_testcases(test, rx)
+ if filtered:
+ new.addTest(filtered)
+ return new
+
+def gui_runner(files, test_filter):
+ if build_inplace:
+ utildir = os.path.join(os.getcwd(), "utilities")
+ else:
+ utildir = os.path.join(os.getcwd(), "..", "utilities")
+ sys.path.append(utildir)
+ import unittestgui
+ suites = []
+ for file in files:
+ suites.append(finder.module_from_path(file) + ".test_suite")
+
+ suites = ", ".join(suites)
+ minimal = (GUI == "minimal")
+ # unittestgui apparently doesn't take the minimal flag anymore
+ unittestgui.main(suites)
+
+class TrackRefs:
+ """Object to track reference counts across test runs."""
+
+ def __init__(self):
+ self.type2count = {}
+ self.type2all = {}
+
+ def update(self):
+ obs = sys.getobjects(0)
+ type2count = {}
+ type2all = {}
+ for o in obs:
+ all = sys.getrefcount(o)
+ t = type(o)
+ if t in type2count:
+ type2count[t] += 1
+ type2all[t] += all
+ else:
+ type2count[t] = 1
+ type2all[t] = all
+
+ ct = [(type2count[t] - self.type2count.get(t, 0),
+ type2all[t] - self.type2all.get(t, 0),
+ t)
+ for t in type2count.iterkeys()]
+ ct.sort()
+ ct.reverse()
+ for delta1, delta2, t in ct:
+ if delta1 or delta2:
+ print "%-55s %8d %8d" % (t, delta1, delta2)
+
+ self.type2count = type2count
+ self.type2all = type2all
+
+def runner(files, test_filter, debug):
+ runner = ImmediateTestRunner(verbosity=VERBOSE, debug=debug,
+ progress=progress)
+ suite = unittest.TestSuite()
+ for file in files:
+ s = get_suite(file)
+ # See if the levels match
+ dolevel = (level == 0) or level >= getattr(s, "level", 0)
+ if s is not None and dolevel:
+ s = filter_testcases(s, test_filter)
+ suite.addTest(s)
+ try:
+ r = runner.run(suite)
+ if timesfn:
+ r.print_times(open(timesfn, "w"))
+ if VERBOSE:
+ print "Wrote timing data to", timesfn
+ if timetests:
+ r.print_times(sys.stdout, timetests)
+ except:
+ if debugger:
+ print "%s:" % (sys.exc_info()[0], )
+ print sys.exc_info()[1]
+ pdb.post_mortem(sys.exc_info()[2])
+ else:
+ raise
+
+def remove_stale_bytecode(arg, dirname, names):
+ names = map(os.path.normcase, names)
+ for name in names:
+ if name.endswith(".pyc") or name.endswith(".pyo"):
+ srcname = name[:-1]
+ if srcname not in names:
+ fullname = os.path.join(dirname, name)
+ print "Removing stale bytecode file", fullname
+ os.unlink(fullname)
+
+def main(module_filter, test_filter, libdir):
+ if not keepStaleBytecode:
+ os.path.walk(os.curdir, remove_stale_bytecode, None)
+
+ # Get the log.ini file from the current directory instead of possibly
+ # buried in the build directory. XXX This isn't perfect because if
+ # log.ini specifies a log file, it'll be relative to the build directory.
+ # 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.
+
+# import logging.config
+# logging.basicConfig()
+
+# 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)
+
+ files = find_tests(module_filter)
+ files.sort()
+
+ if GUI:
+ gui_runner(files, test_filter)
+ elif LOOP:
+ if REFCOUNT:
+ rc = sys.gettotalrefcount()
+ track = TrackRefs()
+ while 1:
+ runner(files, test_filter, debug)
+ gc.collect()
+ if gc.garbage:
+ print "GARBAGE:", len(gc.garbage), gc.garbage
+ return
+ if REFCOUNT:
+ prev = rc
+ rc = sys.gettotalrefcount()
+ print "totalrefcount=%-8d change=%-6d" % (rc, rc - prev)
+ track.update()
+ else:
+ runner(files, test_filter, debug)
+
+
+def process_args(argv=None):
+ import getopt
+ global module_filter
+ global test_filter
+ global VERBOSE
+ global LOOP
+ global GUI
+ global TRACE
+ global REFCOUNT
+ global debug
+ global debugger
+ global build
+ global level
+ global libdir
+ global timesfn
+ global timetests
+ global progress
+ global build_inplace
+ global keepStaleBytecode
+ global functional
+ global test_dir
+
+ if argv is None:
+ argv = sys.argv
+
+ module_filter = None
+ test_filter = None
+ VERBOSE = 2
+ LOOP = 0
+ GUI = 0
+ TRACE = 0
+ REFCOUNT = 0
+ debug = 0 # Don't collect test results; simply let tests crash
+ debugger = 0
+ build = 0
+ build_inplace = 0
+ gcthresh = None
+ gcdebug = 0
+ gcflags = []
+ level = 1
+ libdir = '.'
+ progress = 0
+ timesfn = None
+ timetests = 0
+ keepStaleBytecode = 0
+ functional = 0
+ test_dir = None
+
+ try:
+ opts, args = getopt.getopt(argv[1:], "a:bBcdDfg:G:hLmprtTuv",
+ ["all", "help", "libdir=", "times=",
+ "keepbytecode", "dir=", "build"])
+ except getopt.error, msg:
+ print msg
+ print "Try `python %s -h' for more information." % argv[0]
+ sys.exit(2)
+
+ for k, v in opts:
+ if k == "-a":
+ level = int(v)
+ elif k == "--all":
+ level = 0
+ os.environ["COMPLAIN_IF_TESTS_MISSED"]='1'
+ elif k in ("-b", "--build"):
+ build = 1
+ elif k == "-B":
+ build = build_inplace = 1
+ elif k == "-c":
+ # make sure you have a recent version of pychecker
+ if not os.environ.get("PYCHECKER"):
+ os.environ["PYCHECKER"] = "-q"
+ import pychecker.checker
+ elif k == "-d":
+ debug = 1
+ elif k == "-D":
+ debug = 1
+ debugger = 1
+ elif k == "-f":
+ functional = 1
+ elif k in ("-h", "--help"):
+ print __doc__
+ sys.exit(0)
+ elif k == "-g":
+ gcthresh = int(v)
+ elif k == "-G":
+ if not v.startswith("DEBUG_"):
+ print "-G argument must be DEBUG_ flag, not", repr(v)
+ sys.exit(1)
+ gcflags.append(v)
+ elif k == '--keepbytecode':
+ keepStaleBytecode = 1
+ elif k == '--libdir':
+ libdir = v
+ elif k == "-L":
+ LOOP = 1
+ elif k == "-m":
+ GUI = "minimal"
+ elif k == "-p":
+ progress = 1
+ elif k == "-r":
+ if hasattr(sys, "gettotalrefcount"):
+ REFCOUNT = 1
+ else:
+ print "-r ignored, because it needs a debug build of Python"
+ elif k == "-T":
+ TRACE = 1
+ elif k == "-t":
+ if not timetests:
+ timetests = 50
+ elif k == "-u":
+ GUI = 1
+ elif k == "-v":
+ VERBOSE += 1
+ elif k == "--times":
+ try:
+ timetests = int(v)
+ except ValueError:
+ # must be a filename to write
+ timesfn = v
+ elif k == '--dir':
+ test_dir = v
+
+ if gcthresh is not None:
+ if gcthresh == 0:
+ gc.disable()
+ print "gc disabled"
+ else:
+ gc.set_threshold(gcthresh)
+ print "gc threshold:", gc.get_threshold()
+
+ if gcflags:
+ val = 0
+ for flag in gcflags:
+ v = getattr(gc, flag, None)
+ if v is None:
+ print "Unknown gc flag", repr(flag)
+ print gc.set_debug.__doc__
+ sys.exit(1)
+ val |= v
+ gcdebug |= v
+
+ if gcdebug:
+ gc.set_debug(gcdebug)
+
+ if build:
+ # Python 2.3 is more sane in its non -q output
+ if sys.hexversion >= 0x02030000:
+ qflag = ""
+ else:
+ qflag = "-q"
+ cmd = sys.executable + " setup.py " + qflag + " build"
+ if build_inplace:
+ cmd += "_ext -i"
+ if VERBOSE:
+ print cmd
+ sts = os.system(cmd)
+ if sts:
+ print "Build failed", hex(sts)
+ sys.exit(1)
+
+ if VERBOSE:
+ kind = functional and "functional" or "unit"
+ if level == 0:
+ print "Running %s tests at all levels" % kind
+ else:
+ print "Running %s tests at level %d" % (kind, level)
+
+ # XXX We want to change *visible* warnings into errors. The next
+ # line changes all warnings into errors, including warnings we
+ # normally never see. In particular, test_datetime does some
+ # short-integer arithmetic that overflows to long ints, and, by
+ # default, Python doesn't display the overflow warning that can
+ # be enabled when this happens. The next line turns that into an
+ # error instead. Guido suggests that a better to get what we're
+ # after is to replace warnings.showwarning() with our own thing
+ # that raises an error.
+## warnings.filterwarnings("error")
+ warnings.filterwarnings("ignore", module="logging")
+
+ if args:
+ if len(args) > 1:
+ test_filter = args[1]
+ module_filter = args[0]
+ try:
+ if TRACE:
+ # if the trace module is used, then we don't exit with
+ # status if on a false return value from main.
+ coverdir = os.path.join(os.getcwd(), "coverage")
+ import trace
+ ignoremods = ["os", "posixpath", "stat"]
+ tracer = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix],
+ ignoremods=ignoremods,
+ trace=0, count=1)
+
+ tracer.runctx("main(module_filter, test_filter, libdir)",
+ globals=globals(), locals=vars())
+ r = tracer.results()
+ path = "/tmp/trace.%s" % os.getpid()
+ import cPickle
+ f = open(path, "wb")
+ cPickle.dump(r, f)
+ f.close()
+ print path
+ r.write_results(show_missing=1, summary=1, coverdir=coverdir)
+ else:
+ bad = main(module_filter, test_filter, libdir)
+ if bad:
+ sys.exit(1)
+ except ImportError, err:
+ print err
+ print sys.path
+ raise
+
+
+if __name__ == "__main__":
+ process_args()
Added: tracker/vendor/roundup/current/scripts/README.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/scripts/README.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,29 @@
+Scripts in this directory:
+
+add-issue
+ Add a single issue, as specified on the command line, to your tracker. The
+ initial message for the issue is taken from standard input.
+
+roundup-reminder
+ Generate an email that lists outstanding issues. Send in both plain text
+ and HTML formats.
+
+schema_diagram.py
+ Generate a schema diagram for a roundup tracker. It generates a 'dot file'
+ that is then fed into the 'dot' tool (http://www.graphviz.org) to generate
+ a graph.
+
+server-ctl
+ Control the roundup-server daemon from the command line with start, stop,
+ restart, condstart (conditional start - only if server is stopped) and
+ status commands.
+
+roundup.rc-debian
+ An control script that may be installed in /etc/init.d on Debian systems.
+ Offers start, stop and restart commands and integrates with the Debian
+ init process.
+
+imapServer.py
+ This IMAP server script that runs in the background and checks for new
+ email from a variety of mailboxes.
+
Added: tracker/vendor/roundup/current/scripts/add-issue
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/scripts/add-issue Sun Nov 5 21:30:25 2006
@@ -0,0 +1,60 @@
+#! /usr/bin/env python
+# $Id: add-issue,v 1.2 2003/04/30 01:28:37 richard Exp $
+
+'''
+Usage: %s <tracker home> <priority> <issue title>
+
+Create a new issue in the given tracker. Input is taken from STDIN to
+create the initial issue message (which may be empty). Issues will be
+created as the current user (%s) if they exist as a Roundup
+user, or "admin" otherwise.
+'''
+
+import sys, os, pwd
+
+from roundup import instance, mailgw, date
+
+# open the instance
+username = pwd.getpwuid(os.getuid())[0]
+if len(sys.argv) < 3:
+ print "Error: Not enough arguments"
+ print __doc__.strip()%(sys.argv[0], username)
+ sys.exit(1)
+tracker_home = sys.argv[1]
+issue_priority = sys.argv[2]
+issue_title = ' '.join(sys.argv[3:])
+
+# get the message, if any
+message_text = sys.stdin.read().strip()
+
+# open the tracker
+tracker = instance.open(tracker_home)
+db = tracker.open('admin')
+uid = db.user.lookup('admin')
+try:
+ # try to open the tracker as the current user
+ uid = db.user.lookup(username)
+ db.close()
+ db = tracker.open(username)
+except KeyError:
+ pass
+
+try:
+
+ # handle the message
+ messages = []
+ if message_text:
+ summary, x = mailgw.parseContent(message_text, 0, 0)
+ msg = db.msg.create(content=message_text, summary=summary, author=uid,
+ date=date.Date())
+ messages = [msg]
+
+ # now create the issue
+ db.issue.create(title=issue_title, priority=issue_priority,
+ messages=messages)
+
+ db.commit()
+finally:
+ db.close()
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/scripts/copy-user.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/scripts/copy-user.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+# Copyright (C) 2003 by Intevation GmbH
+# Author:
+# Thomas Arendsen Hein <thomas at intevation.de>
+#
+# This program is free software dual licensed under the GPL (>=v2)
+# and the Roundup Licensing (see COPYING.txt in the roundup distribution).
+
+"""
+copy-user <instance-home> <instance-home> <userid> [<userid>...]
+
+Copy one or more Roundup users from one tracker instance to another.
+Example:
+ copy-user /roundup/tracker1 /roundup/tracker2 `seq 3 10` 14 16
+ (copies users 3, 4, 5, 6, 7, 8, 9, 10, 14 and 16)
+"""
+
+__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 $
+
+import sys
+import roundup.instance
+
+
+def copy_user(home1, home2, *userids):
+ """Copy users which are listed by userids from home1 to home2"""
+
+ copyattribs = ['username', 'password', 'address', 'realname', 'phone',
+ 'organisation', 'alternate_addresses', 'roles', 'timezone']
+
+ try:
+ instance1 = roundup.instance.open(home1)
+ print "Opened source instance: %s" % home1
+ except:
+ print "Can't open source instance: %s" % home1
+ sys.exit(1)
+
+ try:
+ instance2 = roundup.instance.open(home2)
+ print "Opened target instance: %s" % home2
+ except:
+ print "Can't open target instance: %s" % home2
+ sys.exit(1)
+
+ db1 = instance1.open('admin')
+ db2 = instance2.open('admin')
+
+ userlist = db1.user.list()
+ for userid in userids:
+ try:
+ userid = str(int(userid))
+ except ValueError, why:
+ print "Not a numeric user id: %s Skipping ..." % (userid,)
+ continue
+ if userid not in userlist:
+ print "User %s not in source instance. Skipping ..." % userid
+ continue
+
+ user = {}
+ for attrib in copyattribs:
+ value = db1.user.get(userid, attrib)
+ if value:
+ user[attrib] = value
+ try:
+ db2.user.lookup(user['username'])
+ print "User %s: Username '%s' exists in target instance. Skipping ..." % (userid, user['username'])
+ continue
+ except KeyError, why:
+ pass
+ print "Copying user %s (%s) ..." % (userid, user['username'])
+ db2.user.create(**user)
+
+ db2.commit()
+ db2.close()
+ print "Closed target instance."
+ db1.close()
+ print "Closed source instance."
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 4:
+ print __doc__
+ sys.exit(1)
+ else:
+ copy_user(*sys.argv[1:])
+
Added: tracker/vendor/roundup/current/scripts/hyperdb_example.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/scripts/hyperdb_example.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,19 @@
+from roundup.hyperdb import String, Number, Multilink
+from roundup.backends.back_bsddb import Database, Class
+
+class config:
+ DATABASE='/tmp/hyperdb_example'
+
+db = Database(config, 'admin')
+spam = Class(db, 'spam', name=String(), size=Number())
+widget = Class(db, 'widget', title=String(), spam=Multilink('spam'))
+
+oneid = spam.create(name='one', size=1)
+twoid = spam.create(name='two', size=2)
+
+widgetid = widget.create(title='a widget', spam=[oneid, twoid])
+
+# dumb, simple query
+print widget.find(spam=oneid)
+print widget.history(widgetid)
+print widget.search_text(
Added: tracker/vendor/roundup/current/scripts/imapServer.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/scripts/imapServer.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,383 @@
+#!/usr/bin/env python
+"""\
+This script is a wrapper around the mailgw.py script that exists in roundup.
+It runs as service instead of running as a one-time shot.
+It also connects to a secure IMAP server. The main reasons for this script are:
+
+1) The roundup-mailgw script isn't designed to run as a server. It
+ expects that you either run it by hand, and enter the password each
+ time, or you supply the password on the command line. I prefer to
+ run a server that I initialize with the password, and then it just
+ runs. I don't want to have to pass it on the command line, so
+ running through crontab isn't a possibility. (This wouldn't be a
+ problem on a local machine running through a mailspool.)
+2) mailgw.py somehow screws up SSL support so IMAP4_SSL doesn't work. So
+ hopefully running that work outside of the mailgw will allow it to work.
+3) I wanted to be able to check multiple projects at the same time.
+ roundup-mailgw is only for 1 mailbox and 1 project.
+
+
+*TODO*:
+ For the first round, the program spawns a new roundup-mailgw for
+ each imap message that it finds and pipes the result in. In the
+ future it might be more practical to actually include the roundup
+ files and run the appropriate commands using python.
+
+*TODO*:
+ Look into supporting a logfile instead of using 2>/logfile
+
+*TODO*:
+ Add an option for changing the uid/gid of the running process.
+"""
+
+import getpass
+import logging
+import imaplib
+import optparse
+import os
+import re
+import time
+
+logging.basicConfig()
+log = logging.getLogger('IMAPServer')
+
+version = '0.1.2'
+
+class RoundupMailbox:
+ """This contains all the info about each mailbox.
+ Username, Password, server, security, roundup database
+ """
+ def __init__(self, dbhome='', username=None, password=None, mailbox=None
+ , server=None, protocol='imaps'):
+ self.username = username
+ self.password = password
+ self.mailbox = mailbox
+ self.server = server
+ self.protocol = protocol
+ self.dbhome = dbhome
+
+ try:
+ if not self.dbhome:
+ self.dbhome = raw_input('Tracker home: ')
+ if not os.path.exists(self.dbhome):
+ raise ValueError, 'Invalid home address: ' \
+ 'directory "%s" does not exist.' % self.dbhome
+
+ if not self.server:
+ self.server = raw_input('Server: ')
+ if not self.server:
+ raise ValueError, 'No Servername supplied'
+ protocol = raw_input('protocol [imaps]? ')
+ self.protocol = protocol
+
+ if not self.username:
+ self.username = raw_input('Username: ')
+ if not self.username:
+ raise ValueError, 'Invalid Username'
+
+ if not self.password:
+ print 'For server %s, user %s' % (self.server, self.username)
+ self.password = getpass.getpass()
+ # password can be empty because it could be superceeded
+ # by a later entry
+
+ #if self.mailbox is None:
+ # self.mailbox = raw_input('Mailbox [INBOX]: ')
+ # # We allow an empty mailbox because that will
+ # # select the INBOX, whatever it is called
+
+ except (KeyboardInterrupt, EOFError):
+ raise ValueError, 'Canceled by User'
+
+ def __str__(self):
+ return 'Mailbox{ server:%(server)s, protocol:%(protocol)s, ' \
+ 'username:%(username)s, mailbox:%(mailbox)s, ' \
+ 'dbhome:%(dbhome)s }' % self.__dict__
+
+
+# [als] class name is misleading. this is imap client, not imap server
+class IMAPServer:
+
+ """IMAP mail gatherer.
+
+ This class runs as a server process. It is configured with a list of
+ mailboxes to connect to, along with the roundup database directories
+ that correspond with each email address. It then connects to each
+ mailbox at a specified interval, and if there are new messages it
+ reads them, and sends the result to the roundup.mailgw.
+
+ *TODO*:
+ Try to be smart about how you access the mailboxes so that you can
+ connect once, and access multiple mailboxes and possibly multiple
+ usernames.
+
+ *NOTE*:
+ This assumes that if you are using the same user on the same
+ server, you are using the same password. (the last one supplied is
+ used.) Empty passwords are ignored. Only the last protocol
+ supplied is used.
+ """
+
+ def __init__(self, pidfile=None, delay=5, daemon=False):
+ #This is sorted by servername, then username, then mailboxes
+ self.mailboxes = {}
+ self.delay = float(delay)
+ self.pidfile = pidfile
+ self.daemon = daemon
+
+ def setDelay(self, delay):
+ self.delay = delay
+
+ def addMailbox(self, mailbox):
+ """ The linkage is as follows:
+ servers -- users - mailbox:dbhome
+ So there can be multiple servers, each with multiple users.
+ Each username can be associated with multiple mailboxes.
+ each mailbox is associated with 1 database home
+ """
+ log.info('Adding mailbox %s', mailbox)
+ if not self.mailboxes.has_key(mailbox.server):
+ self.mailboxes[mailbox.server] = {'protocol':'imaps', 'users':{}}
+ server = self.mailboxes[mailbox.server]
+ if mailbox.protocol:
+ server['protocol'] = mailbox.protocol
+
+ if not server['users'].has_key(mailbox.username):
+ server['users'][mailbox.username] = {'password':'', 'mailboxes':{}}
+ user = server['users'][mailbox.username]
+ if mailbox.password:
+ user['password'] = mailbox.password
+
+ if user['mailboxes'].has_key(mailbox.mailbox):
+ raise ValueError, 'Mailbox is already defined'
+
+ user['mailboxes'][mailbox.mailbox] = mailbox.dbhome
+
+ def _process(self, message, dbhome):
+ """Actually process one of the email messages"""
+ child = os.popen('roundup-mailgw %s' % dbhome, 'wb')
+ child.write(message)
+ child.close()
+ #print message
+
+ def _getMessages(self, serv, count, dbhome):
+ """This assumes that you currently have a mailbox open, and want to
+ process all messages that are inside.
+ """
+ for n in range(1, count+1):
+ (t, data) = serv.fetch(n, '(RFC822)')
+ if t == 'OK':
+ self._process(data[0][1], dbhome)
+ serv.store(n, '+FLAGS', r'(\Deleted)')
+
+ def checkBoxes(self):
+ """This actually goes out and does all the checking.
+ Returns False if there were any errors, otherwise returns true.
+ """
+ noErrors = True
+ for server in self.mailboxes:
+ log.info('Connecting to server: %s', server)
+ s_vals = self.mailboxes[server]
+
+ try:
+ for user in s_vals['users']:
+ u_vals = s_vals['users'][user]
+ # TODO: As near as I can tell, you can only
+ # login with 1 username for each connection to a server.
+ protocol = s_vals['protocol'].lower()
+ if protocol == 'imaps':
+ serv = imaplib.IMAP4_SSL(server)
+ elif protocol == 'imap':
+ serv = imaplib.IMAP4(server)
+ else:
+ raise ValueError, 'Unknown protocol %s' % protocol
+
+ password = u_vals['password']
+
+ try:
+ log.info('Connecting as user: %s', user)
+ serv.login(user, password)
+
+ for mbox in u_vals['mailboxes']:
+ dbhome = u_vals['mailboxes'][mbox]
+ log.info('Using mailbox: %s, home: %s',
+ mbox, dbhome)
+ #access a specific mailbox
+ if mbox:
+ (t, data) = serv.select(mbox)
+ else:
+ # Select the default mailbox (INBOX)
+ (t, data) = serv.select()
+ try:
+ nMessages = int(data[0])
+ except ValueError:
+ nMessages = 0
+
+ log.info('Found %s messages', nMessages)
+
+ if nMessages:
+ self._getMessages(serv, nMessages, dbhome)
+ serv.expunge()
+
+ # We are done with this mailbox
+ serv.close()
+ except:
+ log.exception('Exception with server %s user %s',
+ server, user)
+ noErrors = False
+
+ serv.logout()
+ serv.shutdown()
+ del serv
+ except:
+ log.exception('Exception while connecting to %s', server)
+ noErrors = False
+ return noErrors
+
+
+ def makeDaemon(self):
+ """Turn this process into a daemon.
+
+ - make our parent PID 1
+
+ Write our new PID to the pidfile.
+
+ From A.M. Kuuchling (possibly originally Greg Ward) with
+ modification from Oren Tirosh, and finally a small mod from me.
+ Originally taken from roundup.scripts.roundup_server.py
+ """
+ log.info('Running as Daemon')
+ # Fork once
+ if os.fork() != 0:
+ os._exit(0)
+
+ # Create new session
+ os.setsid()
+
+ # Second fork to force PPID=1
+ pid = os.fork()
+ if pid:
+ if self.pidfile:
+ pidfile = open(self.pidfile, 'w')
+ pidfile.write(str(pid))
+ pidfile.close()
+ os._exit(0)
+
+ def run(self):
+ """Run email gathering daemon.
+
+ This spawns itself as a daemon, and then runs continually, just
+ sleeping inbetween checks. It is recommended that you run
+ checkBoxes once first before you select run. That way you can
+ know if there were any failures.
+ """
+ if self.daemon:
+ self.makeDaemon()
+ while True:
+
+ time.sleep(self.delay * 60.0)
+ log.info('Time: %s', time.strftime('%Y-%m-%d %H:%M:%S'))
+ self.checkBoxes()
+
+def getItems(s):
+ """Parse a string looking for userame at server"""
+ myRE = re.compile(
+ r'((?P<protocol>[^:]+)://)?'#You can supply a protocol if you like
+ r'(' #The username part is optional
+ r'(?P<username>[^:]+)' #You can supply the password as
+ r'(:(?P<password>.+))?' #username:password at server
+ r'@)?'
+ r'(?P<server>[^/]+)'
+ r'(/(?P<mailbox>.+))?$'
+ )
+ m = myRE.match(s)
+ if m:
+ return m.groupdict()
+ else:
+ return None
+
+def main():
+ """This is what is called if run at the prompt"""
+ parser = optparse.OptionParser(
+ version=('%prog ' + version),
+ usage="""usage: %prog [options] (home server)...
+
+So each entry has a home, and then the server configuration. Home is just
+a path to the roundup issue tracker. The server is something of the form:
+
+ imaps://user:password@server/mailbox
+
+If you don't supply the protocol, imaps is assumed. Without user or
+password, you will be prompted for them. The server must be supplied.
+Without mailbox the INBOX is used.
+
+Examples:
+ %prog /home/roundup/trackers/test imaps://test@imap.example.com/test
+ %prog /home/roundup/trackers/test imap.example.com \
+/home/roundup/trackers/test2 imap.example.com/test2
+"""
+ )
+ parser.add_option('-d', '--delay', dest='delay', type='float',
+ metavar='<sec>', default=5,
+ help="Set the delay between checks in minutes. (default 5)"
+ )
+ parser.add_option('-p', '--pid-file', dest='pidfile',
+ metavar='<file>', default=None,
+ help="The pid of the server process will be written to <file>"
+ )
+ parser.add_option('-n', '--no-daemon', dest='daemon',
+ action='store_false', default=True,
+ help="Do not fork into the background after running the first check."
+ )
+ parser.add_option('-v', '--verbose', dest='verbose',
+ action='store_const', const=logging.INFO,
+ help="Be more verbose in letting you know what is going on."
+ " Enables informational messages."
+ )
+ parser.add_option('-V', '--very-verbose', dest='verbose',
+ action='store_const', const=logging.DEBUG,
+ help="Be very verbose in letting you know what is going on."
+ " Enables debugging messages."
+ )
+ parser.add_option('-q', '--quiet', dest='verbose',
+ action='store_const', const=logging.ERROR,
+ help="Be less verbose. Ignores warnings, only prints errors."
+ )
+ parser.add_option('-Q', '--very-quiet', dest='verbose',
+ action='store_const', const=logging.CRITICAL,
+ help="Be much less verbose. Ignores warnings and errors."
+ " Only print CRITICAL messages."
+ )
+
+ (opts, args) = parser.parse_args()
+ if (len(args) == 0) or (len(args) % 2 == 1):
+ parser.error('Invalid number of arguments. '
+ 'Each site needs a home and a server.')
+
+ log.setLevel(opts.verbose)
+ myServer = IMAPServer(delay=opts.delay, pidfile=opts.pidfile,
+ daemon=opts.daemon)
+ for i in range(0,len(args),2):
+ home = args[i]
+ server = args[i+1]
+ if not os.path.exists(home):
+ parser.error('Home: "%s" does not exist' % home)
+
+ info = getItems(server)
+ if not info:
+ parser.error('Invalid server string: "%s"' % server)
+
+ myServer.addMailbox(
+ RoundupMailbox(dbhome=home, mailbox=info['mailbox']
+ , username=info['username'], password=info['password']
+ , server=info['server'], protocol=info['protocol']
+ )
+ )
+
+ if myServer.checkBoxes():
+ myServer.run()
+
+if __name__ == '__main__':
+ main()
+
+# vim: et ft=python si sts=4 sw=4
Added: tracker/vendor/roundup/current/scripts/import_sf.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/scripts/import_sf.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,402 @@
+''' Import tracker data from Sourceforge.NET
+
+This script needs four steps to work:
+
+1. Export the project XML data using the admin web interface at sf.net
+2. Run the file fetching (these are not included in the XML):
+
+ import_sf.py files <path to XML> <path to files dir>
+
+ this will place all the downloaded files in the files dir by file id.
+3. Convert the sf.net XML to Roundup "export" format:
+
+ import_sf.py import <tracker home> <path to XML> <path to files dir>
+
+ this will generate a directory "/tmp/imported" which contains the
+ data to be imported into a Roundup tracker.
+4. Import the data:
+
+ roundup-admin -i <tracker home> import /tmp/imported
+
+And you're done!
+'''
+
+import sys, sets, os, csv, time, urllib2, httplib, mimetypes, urlparse
+
+try:
+ import cElementTree as ElementTree
+except ImportError:
+ from elementtree import ElementTree
+
+from roundup import instance, hyperdb, date, support, password
+
+today = date.Date('.')
+
+DL_URL = 'http://sourceforge.net/tracker/download.php?group_id=%(group_id)s&atid=%(atid)s&aid=%(aid)s'
+
+def get_url(aid):
+ """ so basically we have to jump through hoops, given an artifact id, to
+ figure what the URL should be to access that artifact, and hence any
+ attached files."""
+ # first we hit this URL...
+ conn = httplib.HTTPConnection("sourceforge.net")
+ conn.request("GET", "/support/tracker.php?aid=%s"%aid)
+ response = conn.getresponse()
+ # which should respond with a redirect to the correct url which has the
+ # magic "group_id" and "atid" values in it that we need
+ assert response.status == 302, 'response code was %s'%response.status
+ location = response.getheader('location')
+ query = urlparse.urlparse(response.getheader('location'))[-2]
+ info = dict([param.split('=') for param in query.split('&')])
+ return DL_URL%info
+
+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()
+ for artifact in root.find('artifacts'):
+ for field in artifact.findall('field'):
+ if field.get('name') == 'artifact_id':
+ aid = field.text
+ for field in artifact.findall('field'):
+ if field.get('name') != 'artifact_history': continue
+ for event in field.findall('history'):
+ d = {}
+ for field in event.findall('field'):
+ d[field.get('name')] = field.text
+ if d['field_name'] == 'File Added':
+ fid = d['old_value'].split(':')[0]
+ to_fetch.add((aid, fid))
+ if d['field_name'] == 'File Deleted':
+ fid = d['old_value'].split(':')[0]
+ deleted.add((aid, fid))
+ to_fetch = to_fetch - deleted
+
+ got = sets.Set(os.listdir(file_dir))
+ to_fetch = to_fetch - got
+
+ # load cached urls (sigh)
+ urls = {}
+ if os.path.exists(os.path.join(file_dir, 'urls.txt')):
+ for line in open(os.path.join(file_dir, 'urls.txt')):
+ aid, url = line.strip().split()
+ urls[aid] = url
+
+ for aid, fid in support.Progress('Fetching files', list(to_fetch)):
+ if fid in got: continue
+ if not urls.has_key(aid):
+ urls[aid] = get_url(aid)
+ f = open(os.path.join(file_dir, 'urls.txt'), 'a')
+ f.write('%s %s\n'%(aid, urls[aid]))
+ f.close()
+ url = urls[aid] + '&file_id=' + fid
+ f = urllib2.urlopen(url)
+ data = f.read()
+ n = open(os.path.join(file_dir, fid), 'w')
+ n.write(data)
+ f.close()
+ n.close()
+
+def import_xml(tracker_home, xml_file, file_dir):
+ """ Generate Roundup tracker import files based on the tracker schema,
+ sf.net xml export and downloaded files from sf.net. """
+ tracker = instance.open(tracker_home)
+ db = tracker.open('admin')
+
+ resolved = db.status.lookup('resolved')
+ unread = db.status.lookup('unread')
+ chatting = db.status.lookup('unread')
+ critical = db.priority.lookup('critical')
+ urgent = db.priority.lookup('urgent')
+ bug = db.priority.lookup('bug')
+ feature = db.priority.lookup('feature')
+ wish = db.priority.lookup('wish')
+ adminuid = db.user.lookup('admin')
+ anonuid = db.user.lookup('anonymous')
+
+ root = ElementTree.parse(xml_file).getroot()
+
+ def to_date(ts):
+ return date.Date(time.gmtime(float(ts)))
+
+ # parse out the XML
+ artifacts = []
+ categories = sets.Set()
+ users = sets.Set()
+ add_files = sets.Set()
+ remove_files = sets.Set()
+ for artifact in root.find('artifacts'):
+ d = {}
+ op = {}
+ artifacts.append(d)
+ for field in artifact.findall('field'):
+ name = field.get('name')
+ if name == 'artifact_messages':
+ for message in field.findall('message'):
+ l = d.setdefault('messages', [])
+ m = {}
+ l.append(m)
+ for field in message.findall('field'):
+ name = field.get('name')
+ if name == 'adddate':
+ m[name] = to_date(field.text)
+ else:
+ m[name] = field.text
+ if name == 'user_name': users.add(field.text)
+ elif name == 'artifact_history':
+ for event in field.findall('history'):
+ l = d.setdefault('history', [])
+ e = {}
+ l.append(e)
+ for field in event.findall('field'):
+ name = field.get('name')
+ if name == 'entrydate':
+ e[name] = to_date(field.text)
+ else:
+ e[name] = field.text
+ if name == 'mod_by': users.add(field.text)
+ if e['field_name'] == 'File Added':
+ add_files.add(e['old_value'].split(':')[0])
+ elif e['field_name'] == 'File Deleted':
+ remove_files.add(e['old_value'].split(':')[0])
+ elif name == 'details':
+ op['body'] = field.text
+ elif name == 'submitted_by':
+ op['user_name'] = field.text
+ d[name] = field.text
+ users.add(field.text)
+ elif name == 'open_date':
+ thedate = to_date(field.text)
+ op['adddate'] = thedate
+ d[name] = thedate
+ else:
+ d[name] = field.text
+
+ categories.add(d['category'])
+
+ if op.has_key('body'):
+ l = d.setdefault('messages', [])
+ l.insert(0, op)
+
+ add_files -= remove_files
+
+ # create users
+ userd = {'nobody': '2'}
+ users.remove('nobody')
+ data = [
+ {'id': '1', 'username': 'admin', 'password': password.Password('admin'),
+ 'roles': 'Admin', 'address': 'richard at python.org'},
+ {'id': '2', 'username': 'anonymous', 'roles': 'Anonymous'},
+ ]
+ for n, user in enumerate(list(users)):
+ userd[user] = n+3
+ data.append({'id': str(n+3), 'username': user, 'roles': 'User',
+ 'address': '%s at users.sourceforge.net'%user})
+ write_csv(db.user, data)
+ users=userd
+
+ # create categories
+ categoryd = {'None': None}
+ categories.remove('None')
+ data = []
+ for n, category in enumerate(list(categories)):
+ categoryd[category] = n
+ data.append({'id': str(n), 'name': category})
+ write_csv(db.keyword, data)
+ categories = categoryd
+
+ # create issues
+ issue_data = []
+ file_data = []
+ message_data = []
+ issue_journal = []
+ message_id = 0
+ for artifact in artifacts:
+ d = {}
+ d['id'] = artifact['artifact_id']
+ d['title'] = artifact['summary']
+ d['assignedto'] = users[artifact['assigned_to']]
+ if d['assignedto'] == '2':
+ d['assignedto'] = None
+ d['creation'] = artifact['open_date']
+ activity = artifact['open_date']
+ d['creator'] = users[artifact['submitted_by']]
+ actor = d['creator']
+ if categories[artifact['category']]:
+ d['topic'] = [categories[artifact['category']]]
+ issue_journal.append((
+ d['id'], d['creation'].get_tuple(), d['creator'], "'create'", {}
+ ))
+
+ p = int(artifact['priority'])
+ if artifact['artifact_type'] == 'Feature Requests':
+ if p > 3:
+ d['priority'] = feature
+ else:
+ d['priority'] = wish
+ else:
+ if p > 7:
+ d['priority'] = critical
+ elif p > 5:
+ d['priority'] = urgent
+ elif p > 3:
+ d['priority'] = bug
+ else:
+ d['priority'] = feature
+
+ s = artifact['status']
+ if s == 'Closed':
+ d['status'] = resolved
+ elif s == 'Deleted':
+ d['status'] = resolved
+ d['is retired'] = True
+ else:
+ d['status'] = unread
+
+ nosy = sets.Set()
+ for message in artifact.get('messages', []):
+ authid = users[message['user_name']]
+ if not message['body']: continue
+ body = convert_message(message['body'], message_id)
+ if not body: continue
+ m = {'content': body, 'author': authid,
+ 'date': message['adddate'],
+ 'creation': message['adddate'], }
+ message_data.append(m)
+ if authid not in (None, '2'):
+ nosy.add(authid)
+ activity = message['adddate']
+ actor = authid
+ if d['status'] == unread:
+ d['status'] = chatting
+
+ # add import message
+ m = {'content': 'IMPORT FROM SOURCEFORGE', 'author': '1',
+ 'date': today, 'creation': today}
+ message_data.append(m)
+
+ # sort messages and assign ids
+ d['messages'] = []
+ message_data.sort(lambda a,b:cmp(a['date'],b['date']))
+ for message in message_data:
+ message_id += 1
+ message['id'] = str(message_id)
+ d['messages'].append(message_id)
+
+ d['nosy'] = list(nosy)
+
+ files = []
+ for event in artifact.get('history', []):
+ if event['field_name'] == 'File Added':
+ fid, name = event['old_value'].split(':', 1)
+ if fid in add_files:
+ files.append(fid)
+ name = name.strip()
+ try:
+ f = open(os.path.join(file_dir, fid))
+ content = f.read()
+ f.close()
+ except:
+ content = 'content missing'
+ file_data.append({
+ 'id': fid,
+ 'creation': event['entrydate'],
+ 'creator': users[event['mod_by']],
+ 'name': name,
+ 'type': mimetypes.guess_type(name)[0],
+ 'content': content,
+ })
+ continue
+ elif event['field_name'] == 'close_date':
+ action = "'set'"
+ info = { 'status': unread }
+ elif event['field_name'] == 'summary':
+ action = "'set'"
+ info = { 'title': event['old_value'] }
+ else:
+ # not an interesting / translatable event
+ continue
+ row = [ d['id'], event['entrydate'].get_tuple(),
+ users[event['mod_by']], action, info ]
+ if event['entrydate'] > activity:
+ activity = event['entrydate']
+ issue_journal.append(row)
+ d['files'] = files
+
+ d['activity'] = activity
+ d['actor'] = actor
+ issue_data.append(d)
+
+ write_csv(db.issue, issue_data)
+ write_csv(db.msg, message_data)
+ write_csv(db.file, file_data)
+
+ f = open('/tmp/imported/issue-journals.csv', 'w')
+ writer = csv.writer(f, colon_separated)
+ writer.writerows(issue_journal)
+ f.close()
+
+def convert_message(content, id):
+ ''' Strip off the useless sf message header crap '''
+ if content[:14] == 'Logged In: YES':
+ return '\n'.join(content.splitlines()[3:]).strip()
+ return content
+
+class colon_separated(csv.excel):
+ delimiter = ':'
+
+def write_csv(klass, data):
+ props = klass.getprops()
+ if not os.path.exists('/tmp/imported'):
+ os.mkdir('/tmp/imported')
+ f = open('/tmp/imported/%s.csv'%klass.classname, 'w')
+ writer = csv.writer(f, colon_separated)
+ propnames = klass.export_propnames()
+ propnames.append('is retired')
+ writer.writerow(propnames)
+ for entry in data:
+ row = []
+ for name in propnames:
+ if name == 'is retired':
+ continue
+ prop = props[name]
+ if entry.has_key(name):
+ if isinstance(prop, hyperdb.Date) or \
+ isinstance(prop, hyperdb.Interval):
+ row.append(repr(entry[name].get_tuple()))
+ elif isinstance(prop, hyperdb.Password):
+ row.append(repr(str(entry[name])))
+ else:
+ row.append(repr(entry[name]))
+ elif isinstance(prop, hyperdb.Multilink):
+ row.append('[]')
+ elif name in ('creator', 'actor'):
+ row.append("'1'")
+ elif name in ('created', 'activity'):
+ row.append(repr(today.get_tuple()))
+ else:
+ row.append('None')
+ row.append(entry.get('is retired', False))
+ writer.writerow(row)
+
+ if isinstance(klass, hyperdb.FileClass) and entry.get('content'):
+ fname = klass.exportFilename('/tmp/imported/', entry['id'])
+ support.ensureParentsExist(fname)
+ c = open(fname, 'w')
+ if isinstance(entry['content'], unicode):
+ c.write(entry['content'].encode('utf8'))
+ else:
+ c.write(entry['content'])
+ c.close()
+
+ f.close()
+ f = open('/tmp/imported/%s-journals.csv'%klass.classname, 'w')
+ f.close()
+
+if __name__ == '__main__':
+ if sys.argv[1] == 'import':
+ import_xml(*sys.argv[2:])
+ elif sys.argv[1] == 'files':
+ fetch_files(*sys.argv[2:])
+
Added: tracker/vendor/roundup/current/scripts/roundup-reminder
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/scripts/roundup-reminder Sun Nov 5 21:30:25 2006
@@ -0,0 +1,170 @@
+#! /usr/bin/env python2.2
+# 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: roundup-reminder,v 1.8 2004/02/11 00:21:46 richard Exp $
+
+'''
+Simple script that emails all users of a tracker with the issues that
+are currently assigned to them.
+
+TODO: introduce some structure ;)
+TODO: possibly make this more general and configurable...
+'''
+
+import sys, cStringIO, MimeWriter, smtplib
+from roundup import instance, date
+from roundup.mailer import SMTPConnection
+
+# open the instance
+if len(sys.argv) != 2:
+ print 'You need to specify an instance home dir'
+instance_home = sys.argv[1]
+instance = instance.open(instance_home)
+db = instance.open('admin')
+
+resolved_id = db.status.lookup('resolved')
+
+def listCompare(x, y):
+ "compare two tuples such that order is positive on [0] and negative on [1]"
+ if x[0] < y[0]:
+ return -1
+ if x[0] > y[0]:
+ return 1
+ if x[1] > y[1]:
+ return -1
+ if x[1] < y[1]:
+ return 1
+ return 0
+
+# loop through all the users
+for user_id in db.user.list():
+ # make sure we care aboue this user
+ name = db.user.get(user_id, 'realname')
+ if name is None:
+ name = db.user.get(user_id, 'username')
+ address = db.user.get(user_id, 'address')
+ if address is None:
+ continue
+
+ # extract this user's issues
+ l = []
+ for issue_id in db.issue.find(assignedto=user_id):
+ if db.issue.get(issue_id, 'status') == resolved_id:
+ continue
+ order = db.priority.get(db.issue.get(issue_id, 'priority'), 'order')
+ l.append((order, db.issue.get(issue_id, 'activity'),
+ db.issue.get(issue_id, 'creation'), issue_id))
+
+ # sort the issues by timeliness and creation date
+ l.sort(listCompare)
+ if not l:
+ continue
+
+ # generate the email message
+ message = cStringIO.StringIO()
+ writer = MimeWriter.MimeWriter(message)
+ writer.addheader('Subject', 'Your active %s issues'%db.config.TRACKER_NAME)
+ writer.addheader('To', address)
+ writer.addheader('From', '%s <%s>'%(db.config.TRACKER_NAME,
+ db.config.ADMIN_EMAIL))
+ writer.addheader('Reply-To', '%s <%s>'%(db.config.TRACKER_NAME,
+ db.config.ADMIN_EMAIL))
+ writer.addheader('MIME-Version', '1.0')
+ writer.addheader('X-Roundup-Name', db.config.TRACKER_NAME)
+
+ # start the multipart
+ part = writer.startmultipartbody('alternative')
+ part = writer.nextpart()
+ body = part.startbody('text/plain')
+
+ # do the plain text bit
+ print >>body, 'Created ID Urgency Title'
+ print >>body, '='*75
+ # '2 months 213 immediate cc_daemon barfage
+ old_priority = None
+ for priority_order, activity_date, creation_date, issue_id in l:
+ priority = db.issue.get(issue_id, 'priority')
+ if (priority != old_priority):
+ old_priority = priority
+ print >>body, ' ', db.priority.get(priority,'name')
+ # pretty creation
+ creation = (date.Date('.') - creation_date).pretty()
+ if creation is None:
+ creation = creation_date.pretty()
+ activity = (date.Date('.') - activity_date).pretty()
+ title = db.issue.get(issue_id, 'title')
+ if len(title) > 42:
+ title = title[:38] + ' ...'
+ print >>body, '%-11s %-4s %-9s %-42s'%(creation, issue_id,
+ activity, title)
+
+ # some help to finish off
+ print >>body, '''
+To view or respond to any of the issues listed above, visit the URL
+
+ %s
+
+and click on "My Issues". Do NOT respond to this message.
+'''%db.config.TRACKER_WEB
+
+
+ # now the HTML one
+ part = writer.nextpart()
+ body = part.startbody('text/html')
+ colours = {
+ 'immediate': ' bgcolor="#ffcdcd"',
+ 'day': ' bgcolor="#ffdecd"',
+ 'week': ' bgcolor="#ffeecd"',
+ 'month': ' bgcolor="#ffffcd"',
+ 'whenever': ' bgcolor="#ffffff"',
+ }
+ print >>body, '''<table border>
+<tr><th>Created</th> <th>ID</th> <th>Activity</th> <th>Title</th></tr>
+'''
+ old_priority = None
+ for priority_order, activity_date, creation_date, issue_id in l:
+ priority = db.issue.get(issue_id,'priority')
+ if (priority != old_priority):
+ old_priority = priority
+ print >>body, '<tr><td>-></td><td>-></td><td>-></td><td><b>%s</b></td></tr>'%db.priority.get(priority,'name')
+ creation = (date.Date('.') - creation_date).pretty()
+ if creation is None:
+ creation = creation_date.pretty()
+ title = db.issue.get(issue_id, 'title')
+ issue_id = '<a href="%sissue%s">%s</a>'%(db.config.TRACKER_WEB,
+ issue_id, issue_id)
+ activity = (date.Date('.') - activity_date).pretty()
+ print >>body, '''<tr><td>%s</td><td>%s</td><td>%s</td>
+ <td>%s</td></tr>'''%(creation, issue_id, activity, title)
+ print >>body, '</table>'
+
+ print >>body, '''<p>To view or respond to any of the issues listed
+ above, simply click on the issue ID. Do <b>not</b> respond to
+ this message.</p>'''
+
+ # finish of the multipart
+ writer.lastpart()
+
+ # all done, send!
+ smtp = SMTPConnection(db.config)
+ smtp.sendmail(db.config.ADMIN_EMAIL, address, message.getvalue())
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/scripts/roundup.rc-debian
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/scripts/roundup.rc-debian Sun Nov 5 21:30:25 2006
@@ -0,0 +1,51 @@
+#!/bin/sh -e
+#
+# roundup Startup script for the roundup http server.
+#
+# Version: $Id: roundup.rc-debian,v 1.1 2003/10/07 23:02:58 richard Exp $
+
+DESC='Roundup HTTP-Server'
+
+BINFILE=roundup-server
+EXECUTABLE=/usr/local/bin/$BINFILE
+PIDFILE=/var/run/roundup/server.pid
+LOGFILE=/var/log/roundup/roundup.log
+TRACKERS=tttech=/tttech/org/software/roundup/tttech/
+OPTIONS="-- -p 8080 -u roundup -d $PIDFILE -l $LOGFILE $TRACKERS"
+
+
+test -x $EXECUTABLE || exit 0
+
+start_stop() {
+ case "$1" in
+ start)
+ printf "Starting $DESC:"
+ start-stop-daemon --start --oknodo --quiet \
+ --pidfile $PIDFILE \
+ --exec $EXECUTABLE $OPTIONS
+ printf " $BINFILE"
+ printf ".\n"
+ ;;
+ stop)
+ printf "Stopping $DESC:"
+ start-stop-daemon --stop --oknodo --quiet \
+ --pidfile $PIDFILE \
+ --exec $EXECUTABLE $OPTIONS
+ printf " $BINFILE"
+ printf ".\n"
+ ;;
+ restart | force-reload)
+ start_stop stop
+ sleep 1
+ start_stop start
+ ;;
+ *)
+ printf "Usage: $0 {start|stop|restart|force-reload}\n" >&2
+ exit 1
+ ;;
+ esac
+}
+
+start_stop "$@"
+
+exit 0
Added: tracker/vendor/roundup/current/scripts/schema_diagram.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/scripts/schema_diagram.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,48 @@
+#! /usr/bin/python
+#
+# Schema diagram generator contributed by Stefan Seefeld of the fresco
+# project http://www.fresco.org/.
+#
+# It generates a 'dot file' that is then fed into the 'dot'
+# tool (http://www.graphviz.org) to generate a graph:
+#
+# %> ./schema.py
+# %> dot -Tps schema.dot -o schema.ps
+# %> gv schema.ps
+#
+import sys
+import roundup.instance
+
+# open the instance
+instance = roundup.instance.open(sys.argv[1])
+db = instance.open()
+
+# diagram preamble
+print 'digraph schema {'
+print 'size="8,6"'
+print 'node [shape="record" bgcolor="#ffe4c4" style=filled]'
+print 'edge [taillabel="1" headlabel="1" dir=back arrowtail=ediamond]'
+
+# get all the classes
+types = db.classes.keys()
+
+# one record node per class
+for i in range(len(types)):
+ print 'node%d [label=\"{%s|}"]'%(i, types[i])
+
+# now draw in the relations
+for name in db.classes.keys():
+ type = db.classes[name]
+ attributes = type.getprops()
+ for a in attributes.keys():
+ attribute = attributes[a]
+ if isinstance(attribute, roundup.hyperdb.Link):
+ print 'node%d -> node%d [label=%s]'%(types.index(name),
+ types.index(attribute.classname),
+ a)
+ elif isinstance(attribute, roundup.hyperdb.Multilink):
+ print 'node%d -> node%d [taillabel="*" label=%s]'%(types.index(name),
+ types.index(attribute.classname),
+ a)
+# all done
+print '}'
Added: tracker/vendor/roundup/current/scripts/server-ctl
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/scripts/server-ctl Sun Nov 5 21:30:25 2006
@@ -0,0 +1,96 @@
+#!/bin/sh
+
+#
+# Configuration
+#
+CONFFILE="/var/roundup/server-config.ini"
+
+# this will end up with extra space, but it should be ignored in the script
+PIDFILE=`grep '^pidfile' ${CONFFILE} | awk -F = '{print $2}' `
+SERVER="/usr/local/bin/roundup-server -C ${CONFFILE}"
+ERROR=0
+ARGV="$@"
+if [ "x$ARGV" = "x" ] ; then
+ ARGS="help"
+fi
+
+if [ -z "${PIDFILE}" ] ; then
+ echo "pidfile option must be set in configuration file"
+ exit 1
+fi
+
+for ARG in $@ $ARGS
+do
+ # check for pidfile
+ if [ -f $PIDFILE ] ; then
+ PID=`cat $PIDFILE`
+ if [ "x$PID" != "x" ] && kill -0 $PID 2>/dev/null ; then
+ STATUS="roundup-server (pid $PID) running"
+ RUNNING=1
+ else
+ STATUS="roundup-server (pid $PID?) not running"
+ RUNNING=0
+ fi
+ else
+ STATUS="roundup-server (no pid file) not running"
+ RUNNING=0
+ fi
+
+ case $ARG in
+ start)
+ if [ $RUNNING -eq 1 ] ; then
+ echo "$0 $ARG: roundup-server (pid $PID) already running"
+ continue
+ fi
+ if $SERVER ; then
+ echo "$0 $ARG: roundup-server started"
+ else
+ echo "$0 $ARG: roundup-server could not be started"
+ ERROR=1
+ fi
+ ;;
+ condstart)
+ if [ $RUNNING -eq 1 ] ; then
+ continue
+ fi
+ if $SERVER ; then
+ echo "$0 $ARG: roundup-server started"
+ else
+ echo "$0 $ARG: roundup-server could not be started"
+ ERROR=1
+ fi
+ ;;
+ stop)
+ if [ $RUNNING -eq 0 ] ; then
+ echo "$0 $ARG: $STATUS"
+ continue
+ fi
+ if kill $PID ; then
+ echo "$0 $ARG: roundup-server stopped"
+ else
+ echo "$0 $ARG: roundup-server could not be stopped"
+ ERROR=2
+ fi
+ ;;
+ status)
+ echo $STATUS
+ ;;
+ *)
+ echo "usage: $0 (start|condstart|stop|status)"
+ cat <<EOF
+
+ start - start roundup-server
+ condstart - start roundup-server if it's not running
+ stop - stop roundup-server
+ status - display roundup-server status
+
+EOF
+ ERROR=3
+ ;;
+
+ esac
+
+done
+
+exit $ERROR
+
Added: tracker/vendor/roundup/current/setup.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/setup.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,418 @@
+#! /usr/bin/env python
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: setup.py,v 1.89 2006/03/03 02:29:09 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 glob import glob
+
+# patch distutils if it can't cope with the "classifiers" keyword
+from distutils.dist import DistributionMetadata
+if not hasattr(DistributionMetadata, 'classifiers'):
+ 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)
+
+
+class build_scripts_roundup(build_scripts_create):
+ package_name = 'roundup'
+
+
+def scriptname(path):
+ """ Helper for building a list of script names from a list of
+ module files.
+ """
+ script = os.path.splitext(os.path.basename(path))[0]
+ script = string.replace(script, '_', '-')
+ 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)]
+ 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 = [
+ 'roundup',
+ 'roundup.cgi',
+ 'roundup.cgi.PageTemplates',
+ 'roundup.cgi.TAL',
+ 'roundup.cgi.ZTUtils',
+ 'roundup.backends',
+ 'roundup.scripts',
+ ]
+ installdatafiles = [
+ ('share/roundup/cgi-bin', ['cgi-bin/roundup.cgi']),
+ ]
+ py_modules = ['roundup.demo',]
+
+ # 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']))
+
+ # add the templates to the data files lists
+ from roundup.init import listTemplates
+ templates = [t['path'] for t in listTemplates('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)
+ )
+
+ # 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)]))
+
+ # perform the setup action
+ from roundup import __version__
+ setup_args = {
+ 'name': "roundup",
+ 'version': __version__,
+ 'description': "A simple-to-use and -install issue-tracking system"
+ " with command-line, web and e-mail interfaces. Highly"
+ " customisable.",
+ 'long_description':
+'''Roundup is a simple-to-use and -install issue-tracking system with
+command-line, web and e-mail interfaces. It is based on the winning design
+from Ka-Ping Yee in the Software Carpentry "Track" design competition.
+
+If you're upgrading from an older version of Roundup you *must* follow
+the "Software Upgrade" guidelines given in the maintenance documentation.
+
+Fixed:
+
+- failure with browsers not sending "Accept-Language" header
+ (sf bugs 1429646 and 1435335)
+- translate class name in "required property not supplied" error message
+ (sf bug 1429669)
+- error in link property lookups with numeric-alike key values (sf bug 1424550)
+- ignore UTF-8 BOM in .po files
+- add permission filter to menu() implementations (sf bug 1431188)
+- lithuanian translation updated by Nerijus Baliunas (sf patch 1411175)
+- incompatibility with python2.3 in the mailer module (sf bug 1432602)
+- typo in SMTP TLS option name: "MAIL_TLS_CERFILE" (sf bug 1435452)
+- email obfuscation code in html templating is more robust
+- blank-title subject line handling (sf bug 1442121)
+- "All users may only view and edit issues, files and messages they
+ create" example in docs (sf bug 1439086)
+- saving of queries (sf bug 1436169)
+- "Adding a new constrained field to the classic schema" example in docs
+ (sf bug 1433118)
+- security check in mailgw (sf bug 1442145)
+- "clear this message" (sf bug 1429367)
+- escape all uses of "schema" in mysql backend (sf bug 1397569)
+- date spec wasn't allowing week intervals
+''',
+ '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)
+
+if __name__ == '__main__':
+ main()
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/templates/classic/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+htmlbase.py
+*.cover
Added: tracker/vendor/roundup/current/templates/classic/TEMPLATE-INFO.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/TEMPLATE-INFO.txt Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/classic/detectors/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/detectors/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,3 @@
+*.pyc
+*.pyo
+*.cover
Added: tracker/vendor/roundup/current/templates/classic/detectors/messagesummary.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/detectors/messagesummary.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,19 @@
+#$Id: messagesummary.py,v 1.1 2003/04/17 03:26:38 richard Exp $
+
+from roundup.mailgw import parseContent
+
+def summarygenerator(db, cl, nodeid, newvalues):
+ ''' If the message doesn't have a summary, make one for it.
+ '''
+ if newvalues.has_key('summary') or not newvalues.has_key('content'):
+ return
+
+ summary, content = parseContent(newvalues['content'], 1, 1)
+ newvalues['summary'] = summary
+
+
+def init(db):
+ # fire before changes are made
+ db.msg.audit('create', summarygenerator)
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/templates/classic/detectors/nosyreaction.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/detectors/nosyreaction.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,143 @@
+#
+# 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: nosyreaction.py,v 1.4 2005/04/04 08:47:14 richard Exp $
+
+import sets
+
+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 = sets.Set()
+ if nodeid is None:
+ ok = ('new', 'yes')
+ else:
+ ok = ('yes',)
+ # old node, get the current values from the node if they haven't
+ # changed
+ if not newvalues.has_key('nosy'):
+ nosy = cl.get(nodeid, 'nosy')
+ for value in nosy:
+ current_nosy.add(value)
+
+ # if the nosy list changed in this transaction, init from the new value
+ if newvalues.has_key('nosy'):
+ nosy = newvalues.get('nosy', [])
+ for value in nosy:
+ if not db.hasnode('user', value):
+ continue
+ current_nosy.add(value)
+
+ new_nosy = sets.Set(current_nosy)
+
+ # add 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/vendor/roundup/current/templates/classic/detectors/statusauditor.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/detectors/statusauditor.py Sun Nov 5 21:30:25 2006
@@ -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: statusauditor.py,v 1.5 2004/03/27 00:01:48 richard Exp $
+
+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/vendor/roundup/current/templates/classic/detectors/userauditor.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/detectors/userauditor.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,44 @@
+# 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: userauditor.py,v 1.2 2003/11/11 22:25:37 richard Exp $
+
+def audit_user_fields(db, cl, nodeid, newvalues):
+ ''' Make sure user properties are valid.
+
+ - email address has no spaces in it
+ - roles specified exist
+ '''
+ if newvalues.has_key('address') and ' ' in newvalues['address']:
+ raise ValueError, 'Email address must not contain spaces'
+
+ if newvalues.has_key('roles'):
+ roles = [x.lower().strip() for x in newvalues['roles'].split(',')]
+ for rolename in roles:
+ if not db.security.role.has_key(rolename):
+ raise ValueError, 'Role "%s" does not exist'%rolename
+
+
+def init(db):
+ # fire before changes are made
+ db.user.audit('set', audit_user_fields)
+ db.user.audit('create', audit_user_fields)
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/templates/classic/extensions/README.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/extensions/README.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,6 @@
+This directory is for tracker extensions:
+
+- CGI Actions
+- Templating functions
+
+See the customisation doc for more information.
Added: tracker/vendor/roundup/current/templates/classic/html/_generic.calendar.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/_generic.calendar.html Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/classic/html/_generic.collision.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/_generic.collision.html Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/classic/html/_generic.help.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/_generic.help.html Sun Nov 5 21:30:25 2006
@@ -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="" ><< previous</a>
+
+ </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 >></a>
+
+ </th>
+ </tr>
+ </table>
+
+ <table class="classhelp">
+ <tr>
+ <th> <b>x</b></th>
+ <th tal:repeat="prop props" tal:content="prop" i18n:translate=""></th>
+ </tr>
+ <tr tal:repeat="item batch">
+ <tal:block tal:define="attr python:item[props[0]]" >
+ <td>
+ <input name="check"
+ onclick="updatePreview();"
+ tal:attributes="type python:request.form['type'].value;
+ value attr; id string:id_$attr" />
+ </td>
+ <td tal:repeat="prop props">
+ <label class="classhelp-label"
+ tal:attributes="for string:id_$attr"
+ tal:content="structure python:item[prop]"></label>
+ </td>
+ </tal:block>
+ </tr>
+ <tr>
+ <th> <b>x</b></th>
+ <th tal:repeat="prop props" tal:content="prop" i18n:translate=""></th>
+ </tr>
+ </table>
+
+ </form>
+ </body>
+</html>
Added: tracker/vendor/roundup/current/templates/classic/html/_generic.index.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/_generic.index.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,64 @@
+<!-- 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())"
+ tal:omit-tag="python:1" i18n:translate=""
+>You are not allowed to view this page.</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"> </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"
+ > </td>
+ </tr>
+ </tal:block>
+</table>
+
+</td>
+
+</tal:block>
Added: tracker/vendor/roundup/current/templates/classic/html/_generic.item.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/_generic.item.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,48 @@
+<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="not:context/is_view_ok" i18n:translate="">You are not
+ allowed to view this page.</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> </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/vendor/roundup/current/templates/classic/html/file.index.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/file.index.html Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/classic/html/file.item.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/file.item.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,48 @@
+<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="not:context/is_view_ok" i18n:translate="">You are not
+ allowed to view this page.</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>
+
+ <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/vendor/roundup/current/templates/classic/html/help_controls.js
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/help_controls.js Sun Nov 5 21:30:25 2006
@@ -0,0 +1,123 @@
+// initial values for either Nosy, Superseder, Topic 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;
+}
+
+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/vendor/roundup/current/templates/classic/html/home.classlist.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/home.classlist.html Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/classic/html/home.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/home.html Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/classic/html/issue.index.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/issue.index.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,149 @@
+<!-- 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="" >
+ List of issues -
+ <span tal:condition="request/dispname"
+ tal:replace="python:' %s - '%request.dispname" />
+ <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 issues
+ <span tal:condition="request/dispname"
+ tal:replace="python:' - %s' % request.dispname" />
+</span>
+<td class="content" metal:fill-slot="content">
+
+<p tal:condition="not:context/is_view_ok" i18n:translate="">You are not
+ allowed to view this page.</p>
+
+<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/topic" i18n:translate="">Topic</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 To</th>
+ </tr>
+ <tal:block tal:repeat="i batch">
+ <tr tal:define="group python:request.group[1]"
+ tal:condition="python:group and batch.propchanged(group)">
+ <th tal:attributes="colspan python:len(request.columns)"
+ tal:content="python:str(i[group]) or '(no %s set)'%group" class="group">
+ </th>
+ </tr>
+
+ <tr>
+ <td tal:condition="request/show/priority"
+ tal:content="python:i.priority.plain() or default"> </td>
+ <td tal:condition="request/show/id" tal:content="i/id"> </td>
+ <td class="date" tal:condition="request/show/creation"
+ tal:content="i/creation/reldate"> </td>
+ <td class="date" tal:condition="request/show/activity"
+ tal:content="i/activity/reldate"> </td>
+ <td class="date" tal:condition="request/show/actor"
+ tal:content="python:i.actor.plain() or default"> </td>
+ <td tal:condition="request/show/topic"
+ tal:content="python:i.topic.plain() or default"> </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"
+ tal:content="python:i.status.plain() or default"> </td>
+ <td tal:condition="request/show/creator"
+ tal:content="python:i.creator.plain() or default"> </td>
+ <td tal:condition="request/show/assignedto"
+ tal:content="python:i.assignedto.plain() or default"> </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=""><< previous</a>
+
+ </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 >></a>
+
+ </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">
+ <tr tal:condition="batch">
+ <th i18n:translate="">Sort on:</th>
+ <td>
+ <select name="@sort">
+ <option value="" i18n:translate="">- nothing -</option>
+ <option tal:repeat="col context/properties"
+ tal:attributes="value col/_name;
+ selected python:col._name == request.sort[1]"
+ tal:content="col/_name"
+ i18n:translate="">column</option>
+ </select>
+ </td>
+ <th i18n:translate="">Descending:</th>
+ <td><input type="checkbox" name="@sortdir"
+ tal:attributes="checked python:request.sort[0] == '-'">
+ </td>
+ </tr>
+ <tr>
+ <th i18n:translate="">Group on:</th>
+ <td>
+ <select name="@group">
+ <option value="" i18n:translate="">- nothing -</option>
+ <option tal:repeat="col context/properties"
+ tal:attributes="value col/_name;
+ selected python:col._name == request.group[1]"
+ tal:content="col/_name"
+ i18n:translate="">column</option>
+ </select>
+ </td>
+ <th i18n:translate="">Descending:</th>
+ <td><input type="checkbox" name="@groupdir"
+ tal:attributes="checked python:request.group[0] == '-'">
+ </td>
+ </tr>
+ <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>
Added: tracker/vendor/roundup/current/templates/classic/html/issue.item.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/issue.item.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,190 @@
+<!-- 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 <span tal:replace="context/id" i18n:name="id"
+ />: <span tal:replace="context/title" i18n:name="title"
+ /> - <span tal:replace="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="not:context/is_view_ok" i18n:translate="">You are not
+ allowed to view this page.</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" tal:repeat="sup context/superseder">
+ <br><span i18n:translate="">View: <a i18n:name="link" tal:content="sup/id"
+ tal:attributes="href string:issue${sup/id}"></a></span>
+ </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="">Topics</th>
+ <td>
+ <span tal:replace="structure context/topic/field" />
+ <span tal:condition="context/is_edit_ok" tal:replace="structure python:db.keyword.classhelp(property='topic')" />
+ </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>
+
+ <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: </td>
+ <th class="required">highlighted</th>
+ <td> fields are required.</td>
+</tr>
+</table>
+</tal:block>
+
+<p tal:condition="context/id" i18n:translate="">
+ Created on <b><tal:x replace="context/creation" i18n:name="creation" /></b>
+ by <b><tal:x replace="context/creator" i18n:name="creator" /></b>,
+ last changed <b><tal:x replace="context/activity" i18n:name="activity" /></b>
+ by <b><tal:x replace="context/actor" i18n:name="actor" /></b>.
+</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/vendor/roundup/current/templates/classic/html/issue.search.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/issue.search.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,225 @@
+<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[1] or 'activity';
+ group_on python:request.group[1] or 'priority';
+
+ search_input templates/page/macros/search_input;
+ 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_multiselect templates/page/macros/search_multiselect;">
+
+<tr>
+ <th class="header"> </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> </td>
+ <td> </td>
+ <td> </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> </td>
+</tr>
+
+<tr tal:define="name string:topic;
+ db_klass string:keyword;
+ db_content string:name;">
+ <th i18n:translate="">Topic:</th>
+ <td metal:use-macro="search_select"></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> </td>
+</tr>
+
+<tr tal:define="name string:creation">
+ <th i18n:translate="">Creation Date:</th>
+ <td metal:use-macro="search_input"></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_input"></td>
+ <td metal:use-macro="column_input"></td>
+ <td metal:use-macro="sort_input"></td>
+ <td> </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> </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">
+ <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">
+ <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> </td>
+ <td> </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 python:request.sort[0] == '-' or request.sort[0] is None">
+</td>
+</tr>
+
+<tr>
+<th i18n:translate="">Group Descending:</th>
+<td><input type="checkbox" name="@groupdir"
+ tal:attributes="checked python:request.group[0] == '-'">
+</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>
+
+ <input type="hidden" name="@action" value="search">
+ </td>
+ <td><input type="submit" value="Search" i18n:attributes="value"></td>
+</tr>
+
+<tr><td> </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/vendor/roundup/current/templates/classic/html/keyword.item.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/keyword.item.html Sun Nov 5 21:30:25 2006
@@ -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>
+
+ <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/vendor/roundup/current/templates/classic/html/msg.index.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/msg.index.html Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/classic/html/msg.item.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/msg.item.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,78 @@
+<!-- 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="not:context/is_view_ok" i18n:translate="">You are not
+ allowed to view this page.</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/vendor/roundup/current/templates/classic/html/page.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/page.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,232 @@
+<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>
+
+</head>
+<body class="body">
+
+<table class="body">
+
+<tr>
+ <td class="page-header-left"> </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"
+ 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"/>
+ <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 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="issue?@sort=-activity&@group=priority&@filter=status,assignedto&@columns=id,activity,title,creator,status&status=-1,1,2,3,4,5,6,7&assignedto=-1&@dispname=Show%20Unassigned"
+ i18n:translate="">Show Unassigned</a><br>
+ <a href="issue?@sort=-activity&@group=priority&@filter=status&@columns=id,activity,title,creator,assignedto,status&status=-1,1,2,3,4,5,6,7&@dispname=Show%20All"
+ 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:request.user.hasPermission('Edit', 'keyword')
+ or request.user.hasPermission('Create', 'keyword')">
+ <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="python:db.keyword.list() and
+ request.user.hasPermission('Edit', 'keyword')">
+ <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 your 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="request/user/username">username</span></b><br>
+ <a tal:attributes="href string:issue?@sort=-activity&@group=priority&@filter=status,assignedto&@columns=id,activity,title,creator,status&status=-1,1,2,3,4,5,6,7&assignedto=${request/user/id}" i18n:translate="">Your Issues</a><br>
+ <a tal:attributes="href string:user${request/user/id}"
+ i18n:translate="">Your Details</a><br>
+ <a 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.
+-->
+<td metal:define-macro="search_input">
+ <input tal:attributes="value python:request.form.getvalue(name) or nothing;
+ name name">
+</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">
+ <span tal:replace="structure python:db.issue.classhelp(columns,
+ property=name)" />
+</td>
+
+<td metal:define-macro="search_select">
+ <select tal:attributes="name name"
+ tal:define="value python:request.form.getvalue(name)">
+ <option value="" i18n:translate="">don't care</option>
+ <tal:block metal:define-slot="extra_options"></tal:block>
+ <option value="" i18n:translate="">------------</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>
+
+<td metal:define-macro="search_multiselect">
+ <input tal:attributes="value python:request.form.getvalue(name) or nothing;
+ name 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>
+
Added: tracker/vendor/roundup/current/templates/classic/html/query.edit.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/query.edit.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,109 @@
+<!-- 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> </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:define="queries python:db.query.filter(filterspec={'private_for':uid})"
+ tal:repeat="query queries">
+ <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>
+</tr>
+
+<tr tal:define="queries python:db.query.filter(filterspec={'private_for':None})"
+ tal:repeat="query queries">
+ <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>
+
+</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/vendor/roundup/current/templates/classic/html/query.item.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/query.item.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,3 @@
+<!-- query.item -->
+<span tal:replace="structure context/renderQueryForm" />
+
Added: tracker/vendor/roundup/current/templates/classic/html/style.css
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/style.css Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/classic/html/user.forgotten.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/user.forgotten.html Sun Nov 5 21:30:25 2006
@@ -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> </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/vendor/roundup/current/templates/classic/html/user.index.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/user.index.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,40 @@
+<!-- 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="not:context/is_view_ok"
+ i18n:translate="">You are not allowed to view this page.</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"> </td>
+ <td tal:content="python:user.organisation.plain() or default"> </td>
+ <td tal:content="python:user.address.email() or default"> </td>
+ <td tal:content="python:user.phone.plain() or default"> </td>
+ <td tal:condition="context/is_edit_ok">
+ <a tal:attributes="href string:user${user/id}?@action=retire&@template=index"
+ i18n:translate="">retire</a>
+ </td>
+</tr>
+</tal:block>
+</table>
+</td>
+
+</tal:block>
Added: tracker/vendor/roundup/current/templates/classic/html/user.item.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/user.item.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,122 @@
+<!-- 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">
+<tal:block condition="context/id" i18n:translate=""
+ >User <span tal:replace="context/id" i18n:name="id"
+ />: <span tal:replace="context/username" i18n:name="title"
+ /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:block>
+<tal:block condition="not:context/id" i18n:translate=""
+ >New User - <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 User</span>
+ <span tal:condition="python: not context.id and context.is_edit_ok()"
+ tal:omit-tag="python:1" i18n:translate="">New User Editing</span>
+ <span tal:condition="python: context.id and not context.is_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 context.is_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="not:context/is_view_ok" i18n:translate="">You are not
+ allowed to view this page.</p>
+
+<div tal:condition="context/is_view_ok">
+
+<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 tal:condition="context/is_edit_ok">
+ <th i18n:translate="">Login Password</th>
+ <td tal:content="structure context/password/field">password</td>
+ </tr>
+ <tr tal:condition="context/is_edit_ok">
+ <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>
+ <input tal:condition="context/id"
+ tal:replace="structure context/roles/field">
+ <input name="roles" tal:condition="not:context/id"
+ tal:attributes="value db/config/NEW_WEB_USER_ROLES">
+ <tal:block i18n:translate="">(to give the user more than one role,
+ enter a comma,separated,list)</tal:block>
+ </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 i18n:translate="">Timezone</th>
+ <td>
+ <input tal:replace="structure context/timezone/field">
+ <tal:block 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>
+ <th class="required" i18n:translate="">E-mail address</th>
+ <td tal:define="mailto context/address/field">
+ <a tal:condition="not:context/is_edit_ok"
+ tal:attributes="href string:mailto:${mailto}" tal:content="mailto"
+ /><span tal:condition="context/is_edit_ok" tal:replace="structure mailto" />
+ </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 tal:condition="context/is_edit_ok">
+ <td>
+
+ <input type="hidden" name="@template" value="item">
+ <input type="hidden" name="@required" value="username,address">
+ </td>
+ <td tal:content="structure context/submit">submit button here</td>
+ </tr>
+</table>
+</form>
+
+<tal:block tal:condition="not:context/id" i18n:translate="">
+<table class="form">
+<tr>
+ <td>Note: </td>
+ <th class="required">highlighted</th>
+ <td> fields are required.</td>
+</tr>
+</table>
+</tal:block>
+
+<tal:block tal:condition="context/id" tal:replace="structure context/history" />
+
+</div>
+
+</td>
+
+</tal:block>
Added: tracker/vendor/roundup/current/templates/classic/html/user.register.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/user.register.html Sun Nov 5 21:30:25 2006
@@ -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> </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: </td>
+ <th class="required">highlighted</th>
+ <td> fields are required.</td>
+</tr>
+</table>
+</tal:block>
+
+</td>
+
+</tal:block>
Added: tracker/vendor/roundup/current/templates/classic/html/user.rego_progress.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/html/user.rego_progress.html Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/classic/initial_data.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/initial_data.py Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/classic/schema.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/classic/schema.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,169 @@
+
+#
+# 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"),
+ topic=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='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/vendor/roundup/current/templates/minimal/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+htmlbase.py
+*.cover
Added: tracker/vendor/roundup/current/templates/minimal/TEMPLATE-INFO.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/TEMPLATE-INFO.txt Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/minimal/detectors/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/detectors/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,3 @@
+*.pyc
+*.pyo
+*.cover
Added: tracker/vendor/roundup/current/templates/minimal/detectors/userauditor.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/detectors/userauditor.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,44 @@
+# 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: userauditor.py,v 1.2 2003/11/11 22:25:37 richard Exp $
+
+def audit_user_fields(db, cl, nodeid, newvalues):
+ ''' Make sure user properties are valid.
+
+ - email address has no spaces in it
+ - roles specified exist
+ '''
+ if newvalues.has_key('address') and ' ' in newvalues['address']:
+ raise ValueError, 'Email address must not contain spaces'
+
+ if newvalues.has_key('roles'):
+ roles = [x.lower().strip() for x in newvalues['roles'].split(',')]
+ for rolename in roles:
+ if not db.security.role.has_key(rolename):
+ raise ValueError, 'Role "%s" does not exist'%rolename
+
+
+def init(db):
+ # fire before changes are made
+ db.user.audit('set', audit_user_fields)
+ db.user.audit('create', audit_user_fields)
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/templates/minimal/extensions/README.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/extensions/README.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,6 @@
+This directory is for tracker extensions:
+
+- CGI Actions
+- Templating functions
+
+See the customisation doc for more information.
Added: tracker/vendor/roundup/current/templates/minimal/html/_generic.calendar.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/_generic.calendar.html Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/minimal/html/_generic.collision.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/_generic.collision.html Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/minimal/html/_generic.help.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/_generic.help.html Sun Nov 5 21:30:25 2006
@@ -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="" ><< previous</a>
+
+ </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 >></a>
+
+ </th>
+ </tr>
+ </table>
+
+ <table class="classhelp">
+ <tr>
+ <th> <b>x</b></th>
+ <th tal:repeat="prop props" tal:content="prop" i18n:translate=""></th>
+ </tr>
+ <tr tal:repeat="item batch">
+ <tal:block tal:define="attr python:item[props[0]]" >
+ <td>
+ <input name="check"
+ onclick="updatePreview();"
+ tal:attributes="type python:request.form['type'].value;
+ value attr; id string:id_$attr" />
+ </td>
+ <td tal:repeat="prop props">
+ <label class="classhelp-label"
+ tal:attributes="for string:id_$attr"
+ tal:content="structure python:item[prop]"></label>
+ </td>
+ </tal:block>
+ </tr>
+ <tr>
+ <th> <b>x</b></th>
+ <th tal:repeat="prop props" tal:content="prop" i18n:translate=""></th>
+ </tr>
+ </table>
+
+ </form>
+ </body>
+</html>
Added: tracker/vendor/roundup/current/templates/minimal/html/_generic.index.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/_generic.index.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,64 @@
+<!-- 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())"
+ tal:omit-tag="python:1" i18n:translate=""
+>You are not allowed to view this page.</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"> </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"
+ > </td>
+ </tr>
+ </tal:block>
+</table>
+
+</td>
+
+</tal:block>
Added: tracker/vendor/roundup/current/templates/minimal/html/_generic.item.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/_generic.item.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,59 @@
+<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())"
+ tal:omit-tag="python:1" i18n:translate=""
+>You are not allowed to view this page.</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> </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/vendor/roundup/current/templates/minimal/html/help_controls.js
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/help_controls.js Sun Nov 5 21:30:25 2006
@@ -0,0 +1,111 @@
+// initial values for either Nosy, Superseder, Topic 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/vendor/roundup/current/templates/minimal/html/home.classlist.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/home.classlist.html Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/minimal/html/home.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/home.html Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/minimal/html/page.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/page.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,97 @@
+<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>
+
+</head>
+<body class="body">
+
+<table class="body">
+
+<tr>
+ <td class="page-header-left"> </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"
+ 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"/>
+ <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="userblock" tal:condition="python:request.user.username=='anonymous'">
+ <form method="POST" action="">
+ <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">
+ <input type="hidden" name="__came_from" tal:attributes="value string:${request/base}${request/env/PATH_INFO}">
+ <span tal:replace="structure request/indexargs_form" />
+ </form>
+ <a tal:condition="python:request.user.hasPermission('Web Registration')"
+ href="user?@template=register" i18n:translate="">Register</a>
+ </p>
+
+ <p class="userblock" tal:condition="python:request.user.username != 'anonymous'">
+ <b i18n:translate="">Hello,<br><span i18n:name="user"
+ tal:content="request/user/username">username</span></b><br>
+ <a tal:attributes="href string:user${request/user/id}"
+ i18n:translate="">Your Details</a><br>
+ <a tal:attributes="href python:request.indexargs_url('',
+ {'@action':'logout'})" i18n:translate="">Logout</a>
+ </p>
+
+ <p class="classblock"
+ tal:condition="python:request.user.username != 'anonymous'">
+ <b i18n:translate="">Administration</b><br>
+ <a tal:condition="python:request.user.hasPermission('Edit', None)"
+ href="home?@template=classlist" i18n:translate="">Class List</a><br>
+ <a tal:condition="python:request.user.hasPermission('View', 'user')
+ or request.user.hasPermission('Edit', 'user')"
+ href="user" i18n:translate="">User List</a><br>
+ <a tal:condition="python:request.user.hasPermission('Edit', 'user')"
+ href="user?@template=item" i18n:translate="">Add User</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>
Added: tracker/vendor/roundup/current/templates/minimal/html/style.css
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/style.css Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/minimal/html/user.index.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/user.index.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,30 @@
+<!-- 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="not:context/is_view_ok"
+ i18n:translate="">You are not allowed to view this page.</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/vendor/roundup/current/templates/minimal/html/user.item.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/user.item.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,102 @@
+<!-- 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">
+<tal:block condition="context/id" i18n:translate=""
+ >User <span tal:replace="context/id" i18n:name="id"
+ />: <span tal:replace="context/username" i18n:name="title"
+ /> - <span tal:replace="config/TRACKER_NAME" i18n:name="tracker"
+/></tal:block>
+<tal:block condition="not:context/id" i18n:translate=""
+ >New User - <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 User</span>
+ <span tal:condition="python: not context.id and context.is_edit_ok()"
+ tal:omit-tag="python:1" i18n:translate="">New User Editing</span>
+ <span tal:condition="python: context.id and not context.is_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 context.is_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="not:context/is_view_ok" i18n:translate="">
+ You are not allowed to view this page.
+</p>
+
+<div tal:condition="context/is_view_ok">
+
+<form method="POST" onSubmit="return submit_once()"
+ enctype="multipart/form-data"
+ tal:attributes="action context/designator">
+
+<table class="form">
+ <tr>
+ <th class="required" i18n:translate="">Login Name</th>
+ <td tal:content="structure context/username/field">username</td>
+ </tr>
+ <tr tal:condition="context/is_edit_ok">
+ <th i18n:translate="">Login Password</th>
+ <td tal:content="structure context/password/field">password</td>
+ </tr>
+ <tr tal:condition="context/is_edit_ok">
+ <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>
+ <input tal:condition="context/id"
+ tal:replace="structure context/roles/field">
+ <input name="roles" tal:condition="not:context/id"
+ tal:attributes="value db/config/NEW_WEB_USER_ROLES">
+ <tal:block i18n:translate="">(to give the user more than one role,
+ enter a comma,separated,list)</tal:block>
+ </td>
+ </tr>
+ <tr>
+ <th class="required" i18n:translate="">E-mail address</th>
+ <td tal:define="mailto context/address/field">
+ <a tal:condition="not:context/is_edit_ok"
+ tal:attributes="href string:mailto:${mailto}" tal:content="mailto"
+ /><span tal:condition="context/is_edit_ok" tal:replace="structure mailto" />
+ </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 tal:condition="context/is_edit_ok">
+ <td>
+
+ <input type="hidden" name="@template" value="item">
+ <input type="hidden" name="@required" value="username,address">
+ </td>
+ <td tal:content="structure context/submit">submit button here</td>
+ </tr>
+</table>
+</form>
+
+<tal:block tal:condition="not:context/id" i18n:translate="">
+<table class="form">
+<tr>
+ <td>Note: </td>
+ <th class="required">highlighted</th>
+ <td> fields are required.</td>
+</tr>
+</table>
+</tal:block>
+
+<tal:block tal:condition="context/id" tal:replace="structure context/history" />
+
+</div>
+
+</td>
+
+</tal:block>
Added: tracker/vendor/roundup/current/templates/minimal/html/user.register.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/user.register.html Sun Nov 5 21:30:25 2006
@@ -0,0 +1,70 @@
+<!-- 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"
+ i18n:translate="">You are not allowed to view this page.</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> </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/vendor/roundup/current/templates/minimal/html/user.rego_progress.html
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/html/user.rego_progress.html Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/minimal/initial_data.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/initial_data.py Sun Nov 5 21:30:25 2006
@@ -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/vendor/roundup/current/templates/minimal/schema.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/templates/minimal/schema.py Sun Nov 5 21:30:25 2006
@@ -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 :
Added: tracker/vendor/roundup/current/test/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,5 @@
+*.pyc
+*.pyo
+localconfig.py
+db
+*.cover
Added: tracker/vendor/roundup/current/test/README.txt
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/README.txt Sun Nov 5 21:30:25 2006
@@ -0,0 +1,18 @@
+$Id: README.txt,v 1.3 2004/10/24 08:37:58 a1s Exp $
+
+Structure of the tests:
+
+ 1 Test date classes
+ 1.1 Date
+ 1.2 Interval
+ 2 Set up schema
+ 3 Open with specific backend
+ 3.1 anydbm
+ 4 Create database base set (stati, priority, etc)
+ 5 Perform some actions
+ 6 Perform mail import
+ 6.1 text/plain
+ 6.2 multipart/mixed (with one text/plain)
+ 6.3 text/html
+ 6.4 multipart/alternative (with one text/plain)
+ 6.5 multipart/alternative (with no text/plain)
Added: tracker/vendor/roundup/current/test/__init__.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/__init__.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1 @@
+# make this dir a package
Added: tracker/vendor/roundup/current/test/benchmark.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/benchmark.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,134 @@
+import sys, os, time
+
+from roundup.hyperdb import String, Password, Link, Multilink, Date, \
+ Interval, DatabaseError, Boolean, Number
+from roundup import date, password
+
+from db_test_base import config
+
+def setupSchema(db, module):
+ status = module.Class(db, "status", name=String())
+ status.setkey("name")
+ user = module.Class(db, "user", username=String(), password=Password(),
+ assignable=Boolean(), age=Number(), roles=String())
+ user.setkey("username")
+ file = module.FileClass(db, "file", name=String(), type=String(),
+ comment=String(indexme="yes"))
+ 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'))
+ session = module.Class(db, 'session', title=String())
+ session.disableJournalling()
+ db.post_init()
+ db.commit()
+
+def main(backendname, time=time.time, numissues=10):
+ try:
+ exec('from roundup.backends import %s as backend'%backendname)
+ except ImportError:
+ return
+
+ times = []
+
+ config.DATABASE = os.path.join('_benchmark', '%s-%s'%(backendname,
+ numissues))
+ if not os.path.exists(config.DATABASE):
+ db = backend.Database(config, 'admin')
+ setupSchema(db, backend)
+ # create a whole bunch of stuff
+ db.user.create(**{'username': 'admin'})
+ db.status.create(name="unread")
+ db.status.create(name="in-progress")
+ db.status.create(name="testing")
+ db.status.create(name="resolved")
+ pc = -1
+ for i in range(numissues):
+ db.user.create(**{'username': 'user %s'%i})
+ for j in range(10):
+ db.user.set(str(i+1), assignable=1)
+ db.user.set(str(i+1), assignable=0)
+ db.issue.create(**{'title': 'issue %s'%i})
+ for j in range(10):
+ db.issue.set(str(i+1), status='2', assignedto='2', nosy=[])
+ db.issue.set(str(i+1), status='1', assignedto='1',
+ nosy=['1','2'])
+ if (i*100/numissues) != pc:
+ pc = (i*100/numissues)
+ sys.stdout.write("%d%%\r"%pc)
+ sys.stdout.flush()
+ db.commit()
+ else:
+ db = backend.Database(config, 'admin')
+ setupSchema(db, backend)
+
+ sys.stdout.write('%7s: %-6d'%(backendname, numissues))
+ sys.stdout.flush()
+
+ times.append(('start', time()))
+
+ # fetch
+ db.clearCache()
+ for i in db.issue.list():
+ db.issue.get(i, 'title')
+ times.append(('fetch', time()))
+
+ # journals
+ db.clearCache()
+ for i in db.issue.list():
+ db.issue.history(i)
+ times.append(('journal', time()))
+
+ # "calculated" props
+ db.clearCache()
+ for i in db.issue.list():
+ db.issue.get(i, 'activity')
+ db.issue.get(i, 'creator')
+ db.issue.get(i, 'creation')
+ times.append(('jprops', time()))
+
+ # lookup
+ db.clearCache()
+ for i in range(numissues):
+ db.user.lookup('user %s'%i)
+ times.append(('lookup', time()))
+
+ # filter
+ db.clearCache()
+ for i in range(100):
+ db.issue.filter(None, {'assignedto': '1', 'title':'issue'},
+ ('+', 'activity'), ('+', 'status'))
+ times.append(('filter', time()))
+
+ # filter with multilink
+ db.clearCache()
+ for i in range(100):
+ db.issue.filter(None, {'nosy': ['1'], 'assignedto': '1',
+ 'title':'issue'}, ('+', 'activity'), ('+', 'status'))
+ times.append(('filtml', time()))
+
+ # results
+ last = None
+ for event, stamp in times:
+ if last is None:
+ first = stamp
+ else:
+ sys.stdout.write(' %-6.2f'%(stamp-last))
+ last = stamp
+ print ' %-6.2f'%(last-first)
+ sys.stdout.flush()
+
+if __name__ == '__main__':
+ # 0 1 2 3 4 5 6
+ # 01234567890123456789012345678901234567890123456789012345678901234
+ print 'Test name fetch journl jprops lookup filter filtml TOTAL '
+ for name in 'anydbm metakit sqlite'.split():
+ main(name)
+ for name in 'anydbm metakit sqlite'.split():
+ main(name, numissues=20)
+ for name in 'anydbm metakit sqlite'.split():
+ main(name, numissues=100)
+ # don't even bother benchmarking the dbm backends > 100!
+ for name in 'metakit sqlite'.split():
+ main(name, numissues=1000)
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/test/db_test_base.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/db_test_base.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,1524 @@
+#
+# 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: db_test_base.py,v 1.69 2006/04/27 01:39:47 richard Exp $
+
+import unittest, os, shutil, errno, imp, sys, time, pprint, sets
+
+from roundup.hyperdb import String, Password, Link, Multilink, Date, \
+ Interval, DatabaseError, Boolean, Number, Node
+from roundup import date, password, init, instance, configuration
+
+from mocknull import MockNull
+
+config = configuration.CoreConfig()
+config.DATABASE = "db"
+config.RDBMS_NAME = "rounduptest"
+config.RDBMS_HOST = "localhost"
+config.RDBMS_USER = "rounduptest"
+config.RDBMS_PASSWORD = "rounduptest"
+#config.logging = MockNull()
+# these TRACKER_WEB and MAIL_DOMAIN values are used in mailgw tests
+config.MAIL_DOMAIN = "your.tracker.email.domain.example"
+config.TRACKER_WEB = "http://tracker.example/cgi-bin/roundup.cgi/bugs/"
+# uncomment the following to have excessive debug output from test cases
+# FIXME: tracker logging level should be increased by -v arguments
+# to 'run_tests.py' script
+#config.LOGGING_FILENAME = "/tmp/logfile"
+#config.LOGGING_LEVEL = "DEBUG"
+config.init_logging()
+
+def setupTracker(dirname, backend="anydbm"):
+ """Install and initialize new tracker in dirname; return tracker instance.
+
+ If the directory exists, it is wiped out before the operation.
+
+ """
+ global config
+ try:
+ shutil.rmtree(dirname)
+ except OSError, error:
+ if error.errno not in (errno.ENOENT, errno.ESRCH): raise
+ # create the instance
+ init.install(dirname, '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()
+ tracker.init(password.Password('sekrit'))
+ return tracker
+
+def setupSchema(db, create, module):
+ status = module.Class(db, "status", name=String())
+ status.setkey("name")
+ priority = module.Class(db, "priority", name=String(), order=String())
+ priority.setkey("name")
+ user = module.Class(db, "user", username=String(), password=Password(),
+ assignable=Boolean(), age=Number(), roles=String())
+ user.setkey("username")
+ file = module.FileClass(db, "file", name=String(), type=String(),
+ comment=String(indexme="yes"), fooz=Password())
+ file_nidx = module.FileClass(db, "file_nidx", content=String(indexme='no'))
+ 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'))
+ stuff = module.Class(db, "stuff", stuff=String())
+ session = module.Class(db, 'session', title=String())
+ msg = module.FileClass(db, "msg",
+ author=Link("user", do_journal='no'))
+ session.disableJournalling()
+ db.post_init()
+ if create:
+ user.create(username="admin", roles='Admin',
+ password=password.Password('sekrit'))
+ user.create(username="fred", roles='User',
+ password=password.Password('sekrit'))
+ status.create(name="unread")
+ status.create(name="in-progress")
+ status.create(name="testing")
+ status.create(name="resolved")
+ priority.create(name="feature", order="2")
+ priority.create(name="wish", order="3")
+ priority.create(name="bug", order="1")
+ db.commit()
+
+class MyTestCase(unittest.TestCase):
+ def tearDown(self):
+ if hasattr(self, 'db'):
+ self.db.close()
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+
+if os.environ.has_key('LOGGING_LEVEL'):
+ from roundup import rlog
+ config.logging = rlog.BasicLogging()
+ config.logging.setLevel(os.environ['LOGGING_LEVEL'])
+ config.logging.getLogger('hyperdb').setFormat('%(message)s')
+
+class DBTest(MyTestCase):
+ def setUp(self):
+ # remove previous test, ignore errors
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+ os.makedirs(config.DATABASE + '/files')
+ self.db = self.module.Database(config, 'admin')
+ setupSchema(self.db, 1, self.module)
+
+ def testRefresh(self):
+ self.db.refresh_database()
+
+ #
+ # automatic properties (well, the two easy ones anyway)
+ #
+ def testCreatorProperty(self):
+ i = self.db.issue
+ id1 = i.create(title='spam')
+ self.db.commit()
+ self.db.close()
+ self.db = self.module.Database(config, 'fred')
+ setupSchema(self.db, 0, self.module)
+ i = self.db.issue
+ id2 = i.create(title='spam')
+ self.assertNotEqual(id1, id2)
+ self.assertNotEqual(i.get(id1, 'creator'), i.get(id2, 'creator'))
+
+ def testActorProperty(self):
+ i = self.db.issue
+ id1 = i.create(title='spam')
+ self.db.commit()
+ self.db.close()
+ self.db = self.module.Database(config, 'fred')
+ setupSchema(self.db, 0, self.module)
+ i = self.db.issue
+ i.set(id1, title='asfasd')
+ self.assertNotEqual(i.get(id1, 'creator'), i.get(id1, 'actor'))
+
+ # ID number controls
+ def testIDGeneration(self):
+ id1 = self.db.issue.create(title="spam", status='1')
+ id2 = self.db.issue.create(title="eggs", status='2')
+ self.assertNotEqual(id1, id2)
+ def testIDSetting(self):
+ # XXX numeric ids
+ self.db.setid('issue', 10)
+ id2 = self.db.issue.create(title="eggs", status='2')
+ self.assertEqual('11', id2)
+
+ #
+ # basic operations
+ #
+ def testEmptySet(self):
+ id1 = self.db.issue.create(title="spam", status='1')
+ self.db.issue.set(id1)
+
+ # String
+ def testStringChange(self):
+ for commit in (0,1):
+ # test set & retrieve
+ nid = self.db.issue.create(title="spam", status='1')
+ self.assertEqual(self.db.issue.get(nid, 'title'), 'spam')
+
+ # change and make sure we retrieve the correct value
+ self.db.issue.set(nid, title='eggs')
+ if commit: self.db.commit()
+ self.assertEqual(self.db.issue.get(nid, 'title'), 'eggs')
+
+ def testStringUnset(self):
+ for commit in (0,1):
+ nid = self.db.issue.create(title="spam", status='1')
+ if commit: self.db.commit()
+ self.assertEqual(self.db.issue.get(nid, 'title'), 'spam')
+ # make sure we can unset
+ self.db.issue.set(nid, title=None)
+ if commit: self.db.commit()
+ self.assertEqual(self.db.issue.get(nid, "title"), None)
+
+ # FileClass "content" property (no unset test)
+ def testFileClassContentChange(self):
+ for commit in (0,1):
+ # test set & retrieve
+ nid = self.db.file.create(content="spam")
+ self.assertEqual(self.db.file.get(nid, 'content'), 'spam')
+
+ # change and make sure we retrieve the correct value
+ self.db.file.set(nid, content='eggs')
+ if commit: self.db.commit()
+ self.assertEqual(self.db.file.get(nid, 'content'), 'eggs')
+
+ # Link
+ def testLinkChange(self):
+ self.assertRaises(IndexError, self.db.issue.create, title="spam",
+ status='100')
+ for commit in (0,1):
+ nid = self.db.issue.create(title="spam", status='1')
+ if commit: self.db.commit()
+ self.assertEqual(self.db.issue.get(nid, "status"), '1')
+ self.db.issue.set(nid, status='2')
+ if commit: self.db.commit()
+ self.assertEqual(self.db.issue.get(nid, "status"), '2')
+
+ def testLinkUnset(self):
+ for commit in (0,1):
+ nid = self.db.issue.create(title="spam", status='1')
+ if commit: self.db.commit()
+ self.db.issue.set(nid, status=None)
+ if commit: self.db.commit()
+ self.assertEqual(self.db.issue.get(nid, "status"), None)
+
+ # Multilink
+ def testMultilinkChange(self):
+ for commit in (0,1):
+ self.assertRaises(IndexError, self.db.issue.create, title="spam",
+ nosy=['foo%s'%commit])
+ u1 = self.db.user.create(username='foo%s'%commit)
+ u2 = self.db.user.create(username='bar%s'%commit)
+ nid = self.db.issue.create(title="spam", nosy=[u1])
+ if commit: self.db.commit()
+ self.assertEqual(self.db.issue.get(nid, "nosy"), [u1])
+ self.db.issue.set(nid, nosy=[])
+ if commit: self.db.commit()
+ self.assertEqual(self.db.issue.get(nid, "nosy"), [])
+ self.db.issue.set(nid, nosy=[u1,u2])
+ if commit: self.db.commit()
+ l = [u1,u2]; l.sort()
+ m = self.db.issue.get(nid, "nosy"); m.sort()
+ self.assertEqual(l, m)
+
+# XXX one day, maybe...
+# def testMultilinkOrdering(self):
+# for i in range(10):
+# self.db.user.create(username='foo%s'%i)
+# i = self.db.issue.create(title="spam", nosy=['5','3','12','4'])
+# self.db.commit()
+# l = self.db.issue.get(i, "nosy")
+# # all backends should return the Multilink numeric-id-sorted
+# self.assertEqual(l, ['3', '4', '5', '12'])
+
+ # Date
+ def testDateChange(self):
+ self.assertRaises(TypeError, self.db.issue.create,
+ title='spam', deadline=1)
+ for commit in (0,1):
+ nid = self.db.issue.create(title="spam", status='1')
+ self.assertRaises(TypeError, self.db.issue.set, nid, deadline=1)
+ a = self.db.issue.get(nid, "deadline")
+ if commit: self.db.commit()
+ self.db.issue.set(nid, deadline=date.Date())
+ b = self.db.issue.get(nid, "deadline")
+ if commit: self.db.commit()
+ self.assertNotEqual(a, b)
+ self.assertNotEqual(b, date.Date('1970-1-1.00:00:00'))
+
+ def testDateUnset(self):
+ for commit in (0,1):
+ nid = self.db.issue.create(title="spam", status='1')
+ self.db.issue.set(nid, deadline=date.Date())
+ if commit: self.db.commit()
+ self.assertNotEqual(self.db.issue.get(nid, "deadline"), None)
+ self.db.issue.set(nid, deadline=None)
+ if commit: self.db.commit()
+ self.assertEqual(self.db.issue.get(nid, "deadline"), None)
+
+ # Interval
+ def testIntervalChange(self):
+ self.assertRaises(TypeError, self.db.issue.create,
+ title='spam', foo=1)
+ for commit in (0,1):
+ nid = self.db.issue.create(title="spam", status='1')
+ self.assertRaises(TypeError, self.db.issue.set, nid, foo=1)
+ if commit: self.db.commit()
+ a = self.db.issue.get(nid, "foo")
+ i = date.Interval('-1d')
+ self.db.issue.set(nid, foo=i)
+ if commit: self.db.commit()
+ self.assertNotEqual(self.db.issue.get(nid, "foo"), a)
+ self.assertEqual(i, self.db.issue.get(nid, "foo"))
+ j = date.Interval('1y')
+ self.db.issue.set(nid, foo=j)
+ if commit: self.db.commit()
+ self.assertNotEqual(self.db.issue.get(nid, "foo"), i)
+ self.assertEqual(j, self.db.issue.get(nid, "foo"))
+
+ def testIntervalUnset(self):
+ for commit in (0,1):
+ nid = self.db.issue.create(title="spam", status='1')
+ self.db.issue.set(nid, foo=date.Interval('-1d'))
+ if commit: self.db.commit()
+ self.assertNotEqual(self.db.issue.get(nid, "foo"), None)
+ self.db.issue.set(nid, foo=None)
+ if commit: self.db.commit()
+ self.assertEqual(self.db.issue.get(nid, "foo"), None)
+
+ # Boolean
+ def testBooleanSet(self):
+ nid = self.db.user.create(username='one', assignable=1)
+ self.assertEqual(self.db.user.get(nid, "assignable"), 1)
+ nid = self.db.user.create(username='two', assignable=0)
+ self.assertEqual(self.db.user.get(nid, "assignable"), 0)
+
+ def testBooleanChange(self):
+ userid = self.db.user.create(username='foo', assignable=1)
+ self.assertEqual(1, self.db.user.get(userid, 'assignable'))
+ self.db.user.set(userid, assignable=0)
+ self.assertEqual(self.db.user.get(userid, 'assignable'), 0)
+ self.db.user.set(userid, assignable=1)
+ self.assertEqual(self.db.user.get(userid, 'assignable'), 1)
+
+ def testBooleanUnset(self):
+ nid = self.db.user.create(username='foo', assignable=1)
+ self.db.user.set(nid, assignable=None)
+ self.assertEqual(self.db.user.get(nid, "assignable"), None)
+
+ # Number
+ def testNumberChange(self):
+ nid = self.db.user.create(username='foo', age=1)
+ self.assertEqual(1, self.db.user.get(nid, 'age'))
+ self.db.user.set(nid, age=3)
+ self.assertNotEqual(self.db.user.get(nid, 'age'), 1)
+ self.db.user.set(nid, age=1.0)
+ self.assertEqual(self.db.user.get(nid, 'age'), 1)
+ self.db.user.set(nid, age=0)
+ self.assertEqual(self.db.user.get(nid, 'age'), 0)
+
+ nid = self.db.user.create(username='bar', age=0)
+ self.assertEqual(self.db.user.get(nid, 'age'), 0)
+
+ def testNumberUnset(self):
+ nid = self.db.user.create(username='foo', age=1)
+ self.db.user.set(nid, age=None)
+ self.assertEqual(self.db.user.get(nid, "age"), None)
+
+ # Password
+ def testPasswordChange(self):
+ x = password.Password('x')
+ userid = self.db.user.create(username='foo', password=x)
+ self.assertEqual(x, self.db.user.get(userid, 'password'))
+ self.assertEqual(self.db.user.get(userid, 'password'), 'x')
+ y = password.Password('y')
+ self.db.user.set(userid, password=y)
+ self.assertEqual(self.db.user.get(userid, 'password'), 'y')
+ self.assertRaises(TypeError, self.db.user.create, userid,
+ username='bar', password='x')
+ self.assertRaises(TypeError, self.db.user.set, userid, password='x')
+
+ def testPasswordUnset(self):
+ x = password.Password('x')
+ nid = self.db.user.create(username='foo', password=x)
+ self.db.user.set(nid, assignable=None)
+ self.assertEqual(self.db.user.get(nid, "assignable"), None)
+
+ # key value
+ def testKeyValue(self):
+ self.assertRaises(ValueError, self.db.user.create)
+
+ newid = self.db.user.create(username="spam")
+ self.assertEqual(self.db.user.lookup('spam'), newid)
+ self.db.commit()
+ self.assertEqual(self.db.user.lookup('spam'), newid)
+ self.db.user.retire(newid)
+ self.assertRaises(KeyError, self.db.user.lookup, 'spam')
+
+ # use the key again now that the old is retired
+ newid2 = self.db.user.create(username="spam")
+ self.assertNotEqual(newid, newid2)
+ # try to restore old node. this shouldn't succeed!
+ self.assertRaises(KeyError, self.db.user.restore, newid)
+
+ self.assertRaises(TypeError, self.db.issue.lookup, 'fubar')
+
+ # label property
+ def testLabelProp(self):
+ # key prop
+ self.assertEqual(self.db.status.labelprop(), 'name')
+ self.assertEqual(self.db.user.labelprop(), 'username')
+ # title
+ self.assertEqual(self.db.issue.labelprop(), 'title')
+ # name
+ self.assertEqual(self.db.file.labelprop(), 'name')
+ # id
+ self.assertEqual(self.db.stuff.labelprop(default_to_id=1), 'id')
+
+ # retirement
+ def testRetire(self):
+ self.db.issue.create(title="spam", status='1')
+ b = self.db.status.get('1', 'name')
+ a = self.db.status.list()
+ nodeids = self.db.status.getnodeids()
+ self.db.status.retire('1')
+ 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.assert_(self.db.status.is_retired('1'))
+
+ # make sure the list is different
+ self.assertNotEqual(a, self.db.status.list())
+
+ # can still access the node if necessary
+ self.assertEqual(self.db.status.get('1', 'name'), b)
+ self.assertRaises(IndexError, self.db.status.set, '1', name='hello')
+ self.db.commit()
+ self.assert_(self.db.status.is_retired('1'))
+ self.assertEqual(self.db.status.get('1', 'name'), b)
+ self.assertNotEqual(a, self.db.status.list())
+
+ # try to restore retired node
+ self.db.status.restore('1')
+
+ self.assert_(not self.db.status.is_retired('1'))
+
+ def testCacheCreateSet(self):
+ self.db.issue.create(title="spam", status='1')
+ a = self.db.issue.get('1', 'title')
+ self.assertEqual(a, 'spam')
+ self.db.issue.set('1', title='ham')
+ b = self.db.issue.get('1', 'title')
+ self.assertEqual(b, 'ham')
+
+ def testSerialisation(self):
+ nid = self.db.issue.create(title="spam", status='1',
+ deadline=date.Date(), foo=date.Interval('-1d'))
+ self.db.commit()
+ assert isinstance(self.db.issue.get(nid, 'deadline'), date.Date)
+ assert isinstance(self.db.issue.get(nid, 'foo'), date.Interval)
+ uid = self.db.user.create(username="fozzy",
+ password=password.Password('t. bear'))
+ self.db.commit()
+ assert isinstance(self.db.user.get(uid, 'password'), password.Password)
+
+ def testTransactions(self):
+ # remember the number of items we started
+ num_issues = len(self.db.issue.list())
+ num_files = self.db.numfiles()
+ self.db.issue.create(title="don't commit me!", status='1')
+ self.assertNotEqual(num_issues, len(self.db.issue.list()))
+ self.db.rollback()
+ self.assertEqual(num_issues, len(self.db.issue.list()))
+ self.db.issue.create(title="please commit me!", status='1')
+ self.assertNotEqual(num_issues, len(self.db.issue.list()))
+ self.db.commit()
+ self.assertNotEqual(num_issues, len(self.db.issue.list()))
+ self.db.rollback()
+ self.assertNotEqual(num_issues, len(self.db.issue.list()))
+ self.db.file.create(name="test", type="text/plain", content="hi")
+ self.db.rollback()
+ self.assertEqual(num_files, self.db.numfiles())
+ for i in range(10):
+ self.db.file.create(name="test", type="text/plain",
+ content="hi %d"%(i))
+ self.db.commit()
+ num_files2 = self.db.numfiles()
+ self.assertNotEqual(num_files, num_files2)
+ self.db.file.create(name="test", type="text/plain", content="hi")
+ self.db.rollback()
+ self.assertNotEqual(num_files, self.db.numfiles())
+ self.assertEqual(num_files2, self.db.numfiles())
+
+ # rollback / cache interaction
+ name1 = self.db.user.get('1', 'username')
+ self.db.user.set('1', username = name1+name1)
+ # get the prop so the info's forced into the cache (if there is one)
+ self.db.user.get('1', 'username')
+ self.db.rollback()
+ name2 = self.db.user.get('1', 'username')
+ self.assertEqual(name1, name2)
+
+ def testDestroyNoJournalling(self):
+ self.innerTestDestroy(klass=self.db.session)
+
+ def testDestroyJournalling(self):
+ self.innerTestDestroy(klass=self.db.issue)
+
+ def innerTestDestroy(self, klass):
+ newid = klass.create(title='Mr Friendly')
+ n = len(klass.list())
+ self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
+ count = klass.count()
+ klass.destroy(newid)
+ self.assertNotEqual(count, klass.count())
+ self.assertRaises(IndexError, klass.get, newid, 'title')
+ self.assertNotEqual(len(klass.list()), n)
+ if klass.do_journal:
+ self.assertRaises(IndexError, klass.history, newid)
+
+ # now with a commit
+ newid = klass.create(title='Mr Friendly')
+ n = len(klass.list())
+ self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
+ self.db.commit()
+ count = klass.count()
+ klass.destroy(newid)
+ self.assertNotEqual(count, klass.count())
+ self.assertRaises(IndexError, klass.get, newid, 'title')
+ self.db.commit()
+ self.assertRaises(IndexError, klass.get, newid, 'title')
+ self.assertNotEqual(len(klass.list()), n)
+ if klass.do_journal:
+ self.assertRaises(IndexError, klass.history, newid)
+
+ # now with a rollback
+ newid = klass.create(title='Mr Friendly')
+ n = len(klass.list())
+ self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
+ self.db.commit()
+ count = klass.count()
+ klass.destroy(newid)
+ self.assertNotEqual(len(klass.list()), n)
+ self.assertRaises(IndexError, klass.get, newid, 'title')
+ self.db.rollback()
+ self.assertEqual(count, klass.count())
+ self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
+ self.assertEqual(len(klass.list()), n)
+ if klass.do_journal:
+ self.assertNotEqual(klass.history(newid), [])
+
+ def testExceptions(self):
+ # this tests the exceptions that should be raised
+ ar = self.assertRaises
+
+ ar(KeyError, self.db.getclass, 'fubar')
+
+ #
+ # class create
+ #
+ # string property
+ ar(TypeError, self.db.status.create, name=1)
+ # id, creation, creator and activity properties are reserved
+ ar(KeyError, self.db.status.create, id=1)
+ ar(KeyError, self.db.status.create, creation=1)
+ ar(KeyError, self.db.status.create, creator=1)
+ ar(KeyError, self.db.status.create, activity=1)
+ ar(KeyError, self.db.status.create, actor=1)
+ # invalid property name
+ ar(KeyError, self.db.status.create, foo='foo')
+ # key name clash
+ ar(ValueError, self.db.status.create, name='unread')
+ # invalid link index
+ ar(IndexError, self.db.issue.create, title='foo', status='bar')
+ # invalid link value
+ ar(ValueError, self.db.issue.create, title='foo', status=1)
+ # invalid multilink type
+ ar(TypeError, self.db.issue.create, title='foo', status='1',
+ nosy='hello')
+ # invalid multilink index type
+ ar(ValueError, self.db.issue.create, title='foo', status='1',
+ nosy=[1])
+ # invalid multilink index
+ ar(IndexError, self.db.issue.create, title='foo', status='1',
+ nosy=['10'])
+
+ #
+ # key property
+ #
+ # key must be a String
+ ar(TypeError, self.db.file.setkey, 'fooz')
+ # key must exist
+ ar(KeyError, self.db.file.setkey, 'fubar')
+
+ #
+ # class get
+ #
+ # invalid node id
+ ar(IndexError, self.db.issue.get, '99', 'title')
+ # invalid property name
+ ar(KeyError, self.db.status.get, '2', 'foo')
+
+ #
+ # class set
+ #
+ # invalid node id
+ ar(IndexError, self.db.issue.set, '99', title='foo')
+ # invalid property name
+ ar(KeyError, self.db.status.set, '1', foo='foo')
+ # string property
+ ar(TypeError, self.db.status.set, '1', name=1)
+ # key name clash
+ ar(ValueError, self.db.status.set, '2', name='unread')
+ # set up a valid issue for me to work on
+ id = self.db.issue.create(title="spam", status='1')
+ # invalid link index
+ ar(IndexError, self.db.issue.set, id, title='foo', status='bar')
+ # invalid link value
+ ar(ValueError, self.db.issue.set, id, title='foo', status=1)
+ # invalid multilink type
+ ar(TypeError, self.db.issue.set, id, title='foo', status='1',
+ nosy='hello')
+ # invalid multilink index type
+ ar(ValueError, self.db.issue.set, id, title='foo', status='1',
+ nosy=[1])
+ # invalid multilink index
+ ar(IndexError, self.db.issue.set, id, title='foo', status='1',
+ nosy=['10'])
+ # NOTE: the following increment the username to avoid problems
+ # within metakit's backend (it creates the node, and then sets the
+ # info, so the create (and by a fluke the username set) go through
+ # before the age/assignable/etc. set, which raises the exception)
+ # invalid number value
+ ar(TypeError, self.db.user.create, username='foo', age='a')
+ # invalid boolean value
+ ar(TypeError, self.db.user.create, username='foo2', assignable='true')
+ nid = self.db.user.create(username='foo3')
+ # invalid number value
+ ar(TypeError, self.db.user.set, nid, age='a')
+ # invalid boolean value
+ ar(TypeError, self.db.user.set, nid, assignable='true')
+
+ def testJournals(self):
+ muid = self.db.user.create(username="mary")
+ self.db.user.create(username="pete")
+ self.db.issue.create(title="spam", status='1')
+ self.db.commit()
+
+ # journal entry for issue create
+ journal = self.db.getjournal('issue', '1')
+ self.assertEqual(1, len(journal))
+ (nodeid, date_stamp, journaltag, action, params) = journal[0]
+ self.assertEqual(nodeid, '1')
+ self.assertEqual(journaltag, self.db.user.lookup('admin'))
+ self.assertEqual(action, 'create')
+ keys = params.keys()
+ keys.sort()
+ self.assertEqual(keys, [])
+
+ # journal entry for link
+ journal = self.db.getjournal('user', '1')
+ self.assertEqual(1, len(journal))
+ self.db.issue.set('1', assignedto='1')
+ self.db.commit()
+ journal = self.db.getjournal('user', '1')
+ self.assertEqual(2, len(journal))
+ (nodeid, date_stamp, journaltag, action, params) = journal[1]
+ self.assertEqual('1', nodeid)
+ self.assertEqual('1', journaltag)
+ self.assertEqual('link', action)
+ self.assertEqual(('issue', '1', 'assignedto'), params)
+
+ # wait a bit to keep proper order of journal entries
+ time.sleep(0.01)
+ # journal entry for unlink
+ self.db.setCurrentUser('mary')
+ self.db.issue.set('1', assignedto='2')
+ self.db.commit()
+ journal = self.db.getjournal('user', '1')
+ self.assertEqual(3, len(journal))
+ (nodeid, date_stamp, journaltag, action, params) = journal[2]
+ self.assertEqual('1', nodeid)
+ self.assertEqual(muid, journaltag)
+ self.assertEqual('unlink', action)
+ self.assertEqual(('issue', '1', 'assignedto'), params)
+
+ # test disabling journalling
+ # ... get the last entry
+ jlen = len(self.db.getjournal('user', '1'))
+ self.db.issue.disableJournalling()
+ self.db.issue.set('1', title='hello world')
+ self.db.commit()
+ # see if the change was journalled when it shouldn't have been
+ self.assertEqual(jlen, len(self.db.getjournal('user', '1')))
+ jlen = len(self.db.getjournal('issue', '1'))
+ self.db.issue.enableJournalling()
+ self.db.issue.set('1', title='hello world 2')
+ self.db.commit()
+ # see if the change was journalled
+ self.assertNotEqual(jlen, len(self.db.getjournal('issue', '1')))
+
+ def testJournalPreCommit(self):
+ id = self.db.user.create(username="mary")
+ self.assertEqual(len(self.db.getjournal('user', id)), 1)
+ self.db.commit()
+
+ def testPack(self):
+ id = self.db.issue.create(title="spam", status='1')
+ self.db.commit()
+ time.sleep(1)
+ self.db.issue.set(id, status='2')
+ self.db.commit()
+
+ # sleep for at least a second, then get a date to pack at
+ time.sleep(1)
+ pack_before = date.Date('.')
+
+ # wait another second and add one more entry
+ time.sleep(1)
+ self.db.issue.set(id, status='3')
+ self.db.commit()
+ jlen = len(self.db.getjournal('issue', id))
+
+ # pack
+ self.db.pack(pack_before)
+
+ # we should have the create and last set entries now
+ self.assertEqual(jlen-1, len(self.db.getjournal('issue', id)))
+
+ def testIndexerSearching(self):
+ f1 = self.db.file.create(content='hello', type="text/plain")
+ # content='world' has the wrong content-type and won't be indexed
+ f2 = self.db.file.create(content='world', type="text/frozz",
+ comment='blah blah')
+ i1 = self.db.issue.create(files=[f1, f2], title="flebble plop")
+ i2 = self.db.issue.create(title="flebble the frooz")
+ self.db.commit()
+ self.assertEquals(self.db.indexer.search([], self.db.issue), {})
+ self.assertEquals(self.db.indexer.search(['hello'], self.db.issue),
+ {i1: {'files': [f1]}})
+ self.assertEquals(self.db.indexer.search(['world'], self.db.issue), {})
+ self.assertEquals(self.db.indexer.search(['frooz'], self.db.issue),
+ {i2: {}})
+ self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
+ {i1: {}, i2: {}})
+
+ # test AND'ing of search terms
+ self.assertEquals(self.db.indexer.search(['frooz', 'flebble'],
+ self.db.issue), {i2: {}})
+
+ # unindexed stopword
+ self.assertEquals(self.db.indexer.search(['the'], self.db.issue), {})
+
+ def testReindexingChange(self):
+ search = self.db.indexer.search
+ issue = self.db.issue
+ i1 = issue.create(title="flebble plop")
+ i2 = issue.create(title="flebble frooz")
+ self.db.commit()
+ self.assertEquals(search(['plop'], issue), {i1: {}})
+ self.assertEquals(search(['flebble'], issue), {i1: {}, i2: {}})
+
+ # change i1's title
+ issue.set(i1, title="plop")
+ self.db.commit()
+ self.assertEquals(search(['plop'], issue), {i1: {}})
+ self.assertEquals(search(['flebble'], issue), {i2: {}})
+
+ def testReindexingClear(self):
+ search = self.db.indexer.search
+ issue = self.db.issue
+ i1 = issue.create(title="flebble plop")
+ i2 = issue.create(title="flebble frooz")
+ self.db.commit()
+ self.assertEquals(search(['plop'], issue), {i1: {}})
+ self.assertEquals(search(['flebble'], issue), {i1: {}, i2: {}})
+
+ # unset i1's title
+ issue.set(i1, title="")
+ self.db.commit()
+ self.assertEquals(search(['plop'], issue), {})
+ self.assertEquals(search(['flebble'], issue), {i2: {}})
+
+ def testFileClassReindexing(self):
+ f1 = self.db.file.create(content='hello')
+ f2 = self.db.file.create(content='hello, world')
+ i1 = self.db.issue.create(files=[f1, f2])
+ self.db.commit()
+ d = self.db.indexer.search(['hello'], self.db.issue)
+ self.assert_(d.has_key(i1))
+ d[i1]['files'].sort()
+ self.assertEquals(d, {i1: {'files': [f1, f2]}})
+ self.assertEquals(self.db.indexer.search(['world'], self.db.issue),
+ {i1: {'files': [f2]}})
+ self.db.file.set(f1, content="world")
+ self.db.commit()
+ d = self.db.indexer.search(['world'], self.db.issue)
+ d[i1]['files'].sort()
+ self.assertEquals(d, {i1: {'files': [f1, f2]}})
+ self.assertEquals(self.db.indexer.search(['hello'], self.db.issue),
+ {i1: {'files': [f2]}})
+
+ def testFileClassIndexingNoNoNo(self):
+ f1 = self.db.file.create(content='hello')
+ self.db.commit()
+ self.assertEquals(self.db.indexer.search(['hello'], self.db.file),
+ {'1': {}})
+
+ f1 = self.db.file_nidx.create(content='hello')
+ self.db.commit()
+ self.assertEquals(self.db.indexer.search(['hello'], self.db.file_nidx),
+ {})
+
+ def testForcedReindexing(self):
+ self.db.issue.create(title="flebble frooz")
+ self.db.commit()
+ self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
+ {'1': {}})
+ self.db.indexer.quiet = 1
+ self.db.indexer.force_reindex()
+ self.db.post_init()
+ self.db.indexer.quiet = 9
+ self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
+ {'1': {}})
+
+ #
+ # searching tests follow
+ #
+ def testFindIncorrectProperty(self):
+ self.assertRaises(TypeError, self.db.issue.find, title='fubar')
+
+ def _find_test_setup(self):
+ self.db.file.create(content='')
+ self.db.file.create(content='')
+ self.db.user.create(username='')
+ one = self.db.issue.create(status="1", nosy=['1'])
+ two = self.db.issue.create(status="2", nosy=['2'], files=['1'],
+ assignedto='2')
+ three = self.db.issue.create(status="1", nosy=['1','2'])
+ four = self.db.issue.create(status="3", assignedto='1',
+ files=['1','2'])
+ return one, two, three, four
+
+ def testFindLink(self):
+ one, two, three, four = self._find_test_setup()
+ got = self.db.issue.find(status='1')
+ got.sort()
+ self.assertEqual(got, [one, three])
+ got = self.db.issue.find(status={'1':1})
+ got.sort()
+ self.assertEqual(got, [one, three])
+
+ def testFindLinkFail(self):
+ self._find_test_setup()
+ self.assertEqual(self.db.issue.find(status='4'), [])
+ self.assertEqual(self.db.issue.find(status={'4':1}), [])
+
+ def testFindLinkUnset(self):
+ one, two, three, four = self._find_test_setup()
+ got = self.db.issue.find(assignedto=None)
+ got.sort()
+ self.assertEqual(got, [one, three])
+ got = self.db.issue.find(assignedto={None:1})
+ got.sort()
+ self.assertEqual(got, [one, three])
+
+ def testFindMultipleLink(self):
+ one, two, three, four = self._find_test_setup()
+ l = self.db.issue.find(status={'1':1, '3':1})
+ l.sort()
+ self.assertEqual(l, [one, three, four])
+ l = self.db.issue.find(assignedto={None:1, '1':1})
+ l.sort()
+ self.assertEqual(l, [one, three, four])
+
+ def testFindMultilink(self):
+ one, two, three, four = self._find_test_setup()
+ got = self.db.issue.find(nosy='2')
+ got.sort()
+ self.assertEqual(got, [two, three])
+ got = self.db.issue.find(nosy={'2':1})
+ got.sort()
+ self.assertEqual(got, [two, three])
+ got = self.db.issue.find(nosy={'2':1}, files={})
+ got.sort()
+ self.assertEqual(got, [two, three])
+
+ def testFindMultiMultilink(self):
+ one, two, three, four = self._find_test_setup()
+ got = self.db.issue.find(nosy='2', files='1')
+ got.sort()
+ self.assertEqual(got, [two, three, four])
+ got = self.db.issue.find(nosy={'2':1}, files={'1':1})
+ got.sort()
+ self.assertEqual(got, [two, three, four])
+
+ def testFindMultilinkFail(self):
+ self._find_test_setup()
+ self.assertEqual(self.db.issue.find(nosy='3'), [])
+ self.assertEqual(self.db.issue.find(nosy={'3':1}), [])
+
+ def testFindMultilinkUnset(self):
+ self._find_test_setup()
+ self.assertEqual(self.db.issue.find(nosy={}), [])
+
+ def testFindLinkAndMultilink(self):
+ one, two, three, four = self._find_test_setup()
+ got = self.db.issue.find(status='1', nosy='2')
+ got.sort()
+ self.assertEqual(got, [one, two, three])
+ got = self.db.issue.find(status={'1':1}, nosy={'2':1})
+ got.sort()
+ self.assertEqual(got, [one, two, three])
+
+ def testFindRetired(self):
+ one, two, three, four = self._find_test_setup()
+ self.assertEqual(len(self.db.issue.find(status='1')), 2)
+ self.db.issue.retire(one)
+ self.assertEqual(len(self.db.issue.find(status='1')), 1)
+
+ def testStringFind(self):
+ self.assertRaises(TypeError, self.db.issue.stringFind, status='1')
+
+ ids = []
+ ids.append(self.db.issue.create(title="spam"))
+ self.db.issue.create(title="not spam")
+ ids.append(self.db.issue.create(title="spam"))
+ ids.sort()
+ got = self.db.issue.stringFind(title='spam')
+ got.sort()
+ self.assertEqual(got, ids)
+ self.assertEqual(self.db.issue.stringFind(title='fubar'), [])
+
+ # test retiring a node
+ self.db.issue.retire(ids[0])
+ self.assertEqual(len(self.db.issue.stringFind(title='spam')), 1)
+
+ def filteringSetup(self):
+ for user in (
+ {'username': 'bleep', 'age': 1},
+ {'username': 'blop', 'age': 1.5},
+ {'username': 'blorp', 'age': 2}):
+ self.db.user.create(**user)
+ iss = self.db.issue
+ for issue in (
+ {'title': 'issue one', 'status': '2', 'assignedto': '1',
+ 'foo': date.Interval('1:10'), 'priority': '3',
+ 'deadline': date.Date('2003-02-16.22:50')},
+ {'title': 'issue two', 'status': '1', 'assignedto': '2',
+ 'foo': date.Interval('1d'), 'priority': '3',
+ 'deadline': date.Date('2003-01-01.00:00')},
+ {'title': 'issue three', 'status': '1', 'priority': '2',
+ 'nosy': ['1','2'], 'deadline': date.Date('2003-02-18')},
+ {'title': 'non four', 'status': '3',
+ 'foo': date.Interval('0:10'), 'priority': '2',
+ 'nosy': ['1'], 'deadline': date.Date('2004-03-08')}):
+ self.db.issue.create(**issue)
+ file_content = ''.join([chr(i) for i in range(255)])
+ self.db.file.create(content=file_content)
+ self.db.commit()
+ return self.assertEqual, self.db.issue.filter
+
+ def testFilteringID(self):
+ ae, filt = self.filteringSetup()
+ ae(filt(None, {'id': '1'}, ('+','id'), (None,None)), ['1'])
+ ae(filt(None, {'id': '2'}, ('+','id'), (None,None)), ['2'])
+ ae(filt(None, {'id': '10'}, ('+','id'), (None,None)), [])
+
+ def testFilteringNumber(self):
+ self.filteringSetup()
+ ae, filt = self.assertEqual, self.db.user.filter
+ ae(filt(None, {'age': '1'}, ('+','id'), (None,None)), ['3'])
+ ae(filt(None, {'age': '1.5'}, ('+','id'), (None,None)), ['4'])
+ ae(filt(None, {'age': '2'}, ('+','id'), (None,None)), ['5'])
+ ae(filt(None, {'age': ['1','2']}, ('+','id'), (None,None)), ['3','5'])
+
+ def testFilteringString(self):
+ ae, filt = self.filteringSetup()
+ ae(filt(None, {'title': ['one']}, ('+','id'), (None,None)), ['1'])
+ ae(filt(None, {'title': ['issue one']}, ('+','id'), (None,None)),
+ ['1'])
+ ae(filt(None, {'title': ['issue', 'one']}, ('+','id'), (None,None)),
+ ['1'])
+ ae(filt(None, {'title': ['issue']}, ('+','id'), (None,None)),
+ ['1','2','3'])
+ ae(filt(None, {'title': ['one', 'two']}, ('+','id'), (None,None)),
+ [])
+
+ def testFilteringLink(self):
+ ae, filt = self.filteringSetup()
+ ae(filt(None, {'status': '1'}, ('+','id'), (None,None)), ['2','3'])
+ ae(filt(None, {'assignedto': '-1'}, ('+','id'), (None,None)), ['3','4'])
+ ae(filt(None, {'assignedto': None}, ('+','id'), (None,None)), ['3','4'])
+ ae(filt(None, {'assignedto': ['-1', None]}, ('+','id'), (None,None)),
+ ['3','4'])
+ ae(filt(None, {'assignedto': ['1', None]}, ('+','id'), (None,None)),
+ ['1', '3','4'])
+
+ def testFilteringRetired(self):
+ ae, filt = self.filteringSetup()
+ self.db.issue.retire('2')
+ ae(filt(None, {'status': '1'}, ('+','id'), (None,None)), ['3'])
+
+ def testFilteringMultilink(self):
+ ae, filt = self.filteringSetup()
+ ae(filt(None, {'nosy': '2'}, ('+','id'), (None,None)), ['3'])
+ ae(filt(None, {'nosy': '-1'}, ('+','id'), (None,None)), ['1', '2'])
+ ae(filt(None, {'nosy': ['1','2']}, ('+', 'status'),
+ ('-', 'deadline')), ['4', '3'])
+
+ def testFilteringMany(self):
+ ae, filt = self.filteringSetup()
+ ae(filt(None, {'nosy': '2', 'status': '1'}, ('+','id'), (None,None)),
+ ['3'])
+
+ def testFilteringRange(self):
+ ae, filt = self.filteringSetup()
+ # Date ranges
+ ae(filt(None, {'deadline': 'from 2003-02-10 to 2003-02-23'}), ['1','3'])
+ ae(filt(None, {'deadline': '2003-02-10; 2003-02-23'}), ['1','3'])
+ ae(filt(None, {'deadline': '; 2003-02-16'}), ['2'])
+ # Lets assume people won't invent a time machine, otherwise this test
+ # may fail :)
+ ae(filt(None, {'deadline': 'from 2003-02-16'}), ['1', '3', '4'])
+ ae(filt(None, {'deadline': '2003-02-16;'}), ['1', '3', '4'])
+ ae(filt(None, {'deadline': '2003-02-16;'}), ['1', '3', '4'])
+ # year and month granularity
+ ae(filt(None, {'deadline': '2002'}), [])
+ ae(filt(None, {'deadline': '2003'}), ['1', '2', '3'])
+ ae(filt(None, {'deadline': '2004'}), ['4'])
+ ae(filt(None, {'deadline': '2003-02'}), ['1', '3'])
+ ae(filt(None, {'deadline': '2003-03'}), [])
+ ae(filt(None, {'deadline': '2003-02-16'}), ['1'])
+ ae(filt(None, {'deadline': '2003-02-17'}), [])
+ # Interval ranges
+ ae(filt(None, {'foo': 'from 0:50 to 2:00'}), ['1'])
+ ae(filt(None, {'foo': 'from 0:50 to 1d 2:00'}), ['1', '2'])
+ ae(filt(None, {'foo': 'from 5:50'}), ['2'])
+ ae(filt(None, {'foo': 'to 0:05'}), [])
+
+ # further
+ for issue in (
+ { 'deadline': date.Date('. -2d')},
+ { 'deadline': date.Date('. -1d')},
+ { 'deadline': date.Date('. -8d')},
+ ):
+ self.db.issue.create(**issue)
+ ae(filt(None, {'deadline': '-2d;'}), ['5', '6'])
+ ae(filt(None, {'deadline': '-1d;'}), ['6'])
+ ae(filt(None, {'deadline': '-1w;'}), ['5', '6'])
+
+ def testFilteringIntervalSort(self):
+ # 1: '1:10'
+ # 2: '1d'
+ # 3: None
+ # 4: '0:10'
+ ae, filt = self.filteringSetup()
+ # ascending should sort None, 1:10, 1d
+ ae(filt(None, {}, ('+','foo'), (None,None)), ['3', '4', '1', '2'])
+ # descending should sort 1d, 1:10, None
+ ae(filt(None, {}, ('-','foo'), (None,None)), ['2', '1', '4', '3'])
+
+ def testFilteringMultilinkSort(self):
+ # 1: []
+ # 2: []
+ # 3: ['1','2']
+ # 4: ['1']
+ ae, filt = self.filteringSetup()
+ ae(filt(None, {}, ('+','nosy'), (None,None)), ['1', '2', '4', '3'])
+ ae(filt(None, {}, ('-','nosy'), (None,None)), ['3', '4', '1', '2'])
+
+ def testFilteringLinkSortGroup(self):
+ # 1: status: 2, priority: 3
+ # 2: status: 1, priority: 3
+ # 3: status: 1, priority: 2,
+ # 4: status: 3, priority: 2,
+ ae, filt = self.filteringSetup()
+ ae(filt(None, {}, ('+','status'), ('+','priority')),
+ ['1', '2', '4', '3'])
+
+ def testFilteringDateSort(self):
+ # '1': '2003-02-16.22:50'
+ # '2': '2003-01-01.00:00'
+ # '3': '2003-02-18'
+ # '4': '2004-03-08'
+ ae, filt = self.filteringSetup()
+ # ascending
+ ae(filt(None, {}, ('+','deadline'), (None,None)), ['2', '1', '3', '4'])
+ # descending
+ ae(filt(None, {}, ('-','deadline'), (None,None)), ['4', '3', '1', '2'])
+
+ def testFilteringDateSortPriorityGroup(self):
+ # '1': '2003-02-16.22:50' 1 => 2
+ # '2': '2003-01-01.00:00' 3 => 1
+ # '3': '2003-02-18' 2 => 3
+ # '4': '2004-03-08' 1 => 2
+ ae, filt = self.filteringSetup()
+
+ # ascending
+ ae(filt(None, {}, ('+','deadline'), ('+','priority')),
+ ['2', '1', '3', '4'])
+ ae(filt(None, {}, ('-','deadline'), ('+','priority')),
+ ['1', '2', '4', '3'])
+ # descending
+ ae(filt(None, {}, ('+','deadline'), ('-','priority')),
+ ['3', '4', '2', '1'])
+ ae(filt(None, {}, ('-','deadline'), ('-','priority')),
+ ['4', '3', '1', '2'])
+
+# XXX add sorting tests for other types
+# XXX test auditors and reactors
+
+ def testImportExport(self):
+ # use the filtering setup to create a bunch of items
+ ae, filt = self.filteringSetup()
+ self.db.user.retire('3')
+ self.db.issue.retire('2')
+
+ # grab snapshot of the current database
+ orig = {}
+ for cn,klass in self.db.classes.items():
+ cl = orig[cn] = {}
+ for id in klass.list():
+ it = cl[id] = {}
+ for name in klass.getprops().keys():
+ it[name] = klass.get(id, name)
+
+ os.mkdir('_test_export')
+ try:
+ # grab the export
+ export = {}
+ journals = {}
+ for cn,klass in self.db.classes.items():
+ names = klass.export_propnames()
+ cl = export[cn] = [names+['is retired']]
+ for id in klass.getnodeids():
+ cl.append(klass.export_list(names, id))
+ if hasattr(klass, 'export_files'):
+ klass.export_files('_test_export', id)
+ journals[cn] = klass.export_journals()
+
+ # shut down this db and nuke it
+ self.db.close()
+ self.nuke_database()
+
+ # open a new, empty database
+ os.makedirs(config.DATABASE + '/files')
+ self.db = self.module.Database(config, 'admin')
+ setupSchema(self.db, 0, self.module)
+
+ # import
+ for cn, items in export.items():
+ klass = self.db.classes[cn]
+ names = items[0]
+ maxid = 1
+ for itemprops in items[1:]:
+ id = int(klass.import_list(names, itemprops))
+ if hasattr(klass, 'import_files'):
+ klass.import_files('_test_export', str(id))
+ maxid = max(maxid, id)
+ self.db.setid(cn, str(maxid+1))
+ klass.import_journals(journals[cn])
+ finally:
+ shutil.rmtree('_test_export')
+
+ # compare with snapshot of the database
+ for cn, items in orig.items():
+ klass = self.db.classes[cn]
+ propdefs = klass.getprops(1)
+ # ensure retired items are retired :)
+ l = items.keys(); l.sort()
+ m = klass.list(); m.sort()
+ ae(l, m, '%s id list wrong %r vs. %r'%(cn, l, m))
+ for id, props in items.items():
+ for name, value in props.items():
+ l = klass.get(id, name)
+ if isinstance(value, type([])):
+ value.sort()
+ l.sort()
+ try:
+ ae(l, value)
+ except AssertionError:
+ if not isinstance(propdefs[name], Date):
+ raise
+ # don't get hung up on rounding errors
+ assert not l.__cmp__(value, int_seconds=1)
+
+ # make sure the retired items are actually imported
+ ae(self.db.user.get('4', 'username'), 'blop')
+ ae(self.db.issue.get('2', 'title'), 'issue two')
+
+ # make sure id counters are set correctly
+ maxid = max([int(id) for id in self.db.user.list()])
+ newid = self.db.user.create(username='testing')
+ assert newid > maxid
+
+ def testAddProperty(self):
+ self.db.issue.create(title="spam", status='1')
+ self.db.commit()
+
+ self.db.issue.addprop(fixer=Link("user"))
+ # force any post-init stuff to happen
+ self.db.post_init()
+ props = self.db.issue.getprops()
+ keys = props.keys()
+ keys.sort()
+ self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
+ 'creator', 'deadline', 'files', 'fixer', 'foo', 'id', 'messages',
+ 'nosy', 'priority', 'status', 'superseder', 'title'])
+ self.assertEqual(self.db.issue.get('1', "fixer"), None)
+
+ def testRemoveProperty(self):
+ self.db.issue.create(title="spam", status='1')
+ self.db.commit()
+
+ del self.db.issue.properties['title']
+ self.db.post_init()
+ props = self.db.issue.getprops()
+ keys = props.keys()
+ keys.sort()
+ self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
+ 'creator', 'deadline', 'files', 'foo', 'id', 'messages',
+ 'nosy', 'priority', 'status', 'superseder'])
+ self.assertEqual(self.db.issue.list(), ['1'])
+
+ def testAddRemoveProperty(self):
+ self.db.issue.create(title="spam", status='1')
+ self.db.commit()
+
+ self.db.issue.addprop(fixer=Link("user"))
+ del self.db.issue.properties['title']
+ self.db.post_init()
+ props = self.db.issue.getprops()
+ keys = props.keys()
+ keys.sort()
+ self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
+ 'creator', 'deadline', 'files', 'fixer', 'foo', 'id', 'messages',
+ 'nosy', 'priority', 'status', 'superseder'])
+ self.assertEqual(self.db.issue.list(), ['1'])
+
+class ROTest(MyTestCase):
+ def setUp(self):
+ # remove previous test, ignore errors
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+ os.makedirs(config.DATABASE + '/files')
+ self.db = self.module.Database(config, 'admin')
+ setupSchema(self.db, 1, self.module)
+ self.db.close()
+
+ self.db = self.module.Database(config)
+ setupSchema(self.db, 0, self.module)
+
+ def testExceptions(self):
+ # this tests the exceptions that should be raised
+ ar = self.assertRaises
+
+ # this tests the exceptions that should be raised
+ ar(DatabaseError, self.db.status.create, name="foo")
+ ar(DatabaseError, self.db.status.set, '1', name="foo")
+ ar(DatabaseError, self.db.status.retire, '1')
+
+
+class SchemaTest(MyTestCase):
+ def setUp(self):
+ # remove previous test, ignore errors
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+ os.makedirs(config.DATABASE + '/files')
+
+ def test_reservedProperties(self):
+ self.db = self.module.Database(config, 'admin')
+ self.assertRaises(ValueError, self.module.Class, self.db, "a",
+ creation=String())
+ self.assertRaises(ValueError, self.module.Class, self.db, "a",
+ activity=String())
+ self.assertRaises(ValueError, self.module.Class, self.db, "a",
+ creator=String())
+ self.assertRaises(ValueError, self.module.Class, self.db, "a",
+ actor=String())
+
+ def init_a(self):
+ self.db = self.module.Database(config, 'admin')
+ a = self.module.Class(self.db, "a", name=String())
+ a.setkey("name")
+ self.db.post_init()
+
+ def test_fileClassProps(self):
+ self.db = self.module.Database(config, 'admin')
+ a = self.module.FileClass(self.db, 'a')
+ l = a.getprops().keys()
+ l.sort()
+ self.assert_(l, ['activity', 'actor', 'content', 'created',
+ 'creation', 'type'])
+
+ def init_ab(self):
+ self.db = self.module.Database(config, 'admin')
+ a = self.module.Class(self.db, "a", name=String())
+ a.setkey("name")
+ b = self.module.Class(self.db, "b", name=String(),
+ fooz=Multilink('a'))
+ b.setkey("name")
+ self.db.post_init()
+
+ def test_addNewClass(self):
+ self.init_a()
+
+ self.assertRaises(ValueError, self.module.Class, self.db, "a",
+ name=String())
+
+ aid = self.db.a.create(name='apple')
+ self.db.commit(); self.db.close()
+
+ # add a new class to the schema and check creation of new items
+ # (and existence of old ones)
+ self.init_ab()
+ bid = self.db.b.create(name='bear', fooz=[aid])
+ self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
+ self.db.commit()
+ self.db.close()
+
+ # now check we can recall the added class' items
+ self.init_ab()
+ self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
+ self.assertEqual(self.db.a.lookup('apple'), aid)
+ self.assertEqual(self.db.b.get(bid, 'name'), 'bear')
+ self.assertEqual(self.db.b.get(bid, 'fooz'), [aid])
+ self.assertEqual(self.db.b.lookup('bear'), bid)
+
+ # confirm journal's ok
+ self.db.getjournal('a', aid)
+ self.db.getjournal('b', bid)
+
+ def init_amod(self):
+ self.db = self.module.Database(config, 'admin')
+ a = self.module.Class(self.db, "a", name=String(), newstr=String(),
+ newint=Interval(), newnum=Number(), newbool=Boolean(),
+ newdate=Date())
+ a.setkey("name")
+ b = self.module.Class(self.db, "b", name=String())
+ b.setkey("name")
+ self.db.post_init()
+
+ def test_modifyClass(self):
+ self.init_ab()
+
+ # add item to user and issue class
+ aid = self.db.a.create(name='apple')
+ bid = self.db.b.create(name='bear')
+ self.db.commit(); self.db.close()
+
+ # modify "a" schema
+ self.init_amod()
+ self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
+ self.assertEqual(self.db.a.get(aid, 'newstr'), None)
+ self.assertEqual(self.db.a.get(aid, 'newint'), None)
+ self.assertEqual(self.db.a.get(aid, 'newnum'), None)
+ self.assertEqual(self.db.a.get(aid, 'newbool'), None)
+ self.assertEqual(self.db.a.get(aid, 'newdate'), None)
+ self.assertEqual(self.db.b.get(aid, 'name'), 'bear')
+ aid2 = self.db.a.create(name='aardvark', newstr='booz')
+ self.db.commit(); self.db.close()
+
+ # test
+ self.init_amod()
+ self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
+ self.assertEqual(self.db.a.get(aid, 'newstr'), None)
+ self.assertEqual(self.db.b.get(aid, 'name'), 'bear')
+ self.assertEqual(self.db.a.get(aid2, 'name'), 'aardvark')
+ self.assertEqual(self.db.a.get(aid2, 'newstr'), 'booz')
+
+ # confirm journal's ok
+ self.db.getjournal('a', aid)
+ self.db.getjournal('a', aid2)
+
+ def init_amodkey(self):
+ self.db = self.module.Database(config, 'admin')
+ a = self.module.Class(self.db, "a", name=String(), newstr=String())
+ a.setkey("newstr")
+ b = self.module.Class(self.db, "b", name=String())
+ b.setkey("name")
+ self.db.post_init()
+
+ def test_changeClassKey(self):
+ self.init_amod()
+ aid = self.db.a.create(name='apple')
+ self.assertEqual(self.db.a.lookup('apple'), aid)
+ self.db.commit(); self.db.close()
+
+ # change the key to newstr on a
+ self.init_amodkey()
+ self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
+ self.assertEqual(self.db.a.get(aid, 'newstr'), None)
+ self.assertRaises(KeyError, self.db.a.lookup, 'apple')
+ aid2 = self.db.a.create(name='aardvark', newstr='booz')
+ self.db.commit(); self.db.close()
+
+ # check
+ self.init_amodkey()
+ self.assertEqual(self.db.a.lookup('booz'), aid2)
+
+ # confirm journal's ok
+ self.db.getjournal('a', aid)
+
+ def test_removeClassKey(self):
+ self.init_amod()
+ aid = self.db.a.create(name='apple')
+ self.assertEqual(self.db.a.lookup('apple'), aid)
+ self.db.commit(); self.db.close()
+
+ self.db = self.module.Database(config, 'admin')
+ a = self.module.Class(self.db, "a", name=String(), newstr=String())
+ self.db.post_init()
+
+ aid2 = self.db.a.create(name='apple', newstr='booz')
+ self.db.commit()
+
+
+ def init_amodml(self):
+ self.db = self.module.Database(config, 'admin')
+ a = self.module.Class(self.db, "a", name=String(),
+ newml=Multilink('a'))
+ a.setkey('name')
+ self.db.post_init()
+
+ def test_makeNewMultilink(self):
+ self.init_a()
+ aid = self.db.a.create(name='apple')
+ self.assertEqual(self.db.a.lookup('apple'), aid)
+ self.db.commit(); self.db.close()
+
+ # add a multilink prop
+ self.init_amodml()
+ bid = self.db.a.create(name='bear', newml=[aid])
+ self.assertEqual(self.db.a.find(newml=aid), [bid])
+ self.assertEqual(self.db.a.lookup('apple'), aid)
+ self.db.commit(); self.db.close()
+
+ # check
+ self.init_amodml()
+ self.assertEqual(self.db.a.find(newml=aid), [bid])
+ self.assertEqual(self.db.a.lookup('apple'), aid)
+ self.assertEqual(self.db.a.lookup('bear'), bid)
+
+ # confirm journal's ok
+ self.db.getjournal('a', aid)
+ self.db.getjournal('a', bid)
+
+ def test_removeMultilink(self):
+ # add a multilink prop
+ self.init_amodml()
+ aid = self.db.a.create(name='apple')
+ bid = self.db.a.create(name='bear', newml=[aid])
+ self.assertEqual(self.db.a.find(newml=aid), [bid])
+ self.assertEqual(self.db.a.lookup('apple'), aid)
+ self.assertEqual(self.db.a.lookup('bear'), bid)
+ self.db.commit(); self.db.close()
+
+ # remove the multilink
+ self.init_a()
+ self.assertEqual(self.db.a.lookup('apple'), aid)
+ self.assertEqual(self.db.a.lookup('bear'), bid)
+
+ # confirm journal's ok
+ self.db.getjournal('a', aid)
+ self.db.getjournal('a', bid)
+
+ def test_removeClass(self):
+ self.init_ab()
+ aid = self.db.a.create(name='apple')
+ bid = self.db.b.create(name='bear')
+ self.db.commit(); self.db.close()
+
+ # drop the b class
+ self.init_a()
+ self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
+ self.assertEqual(self.db.a.lookup('apple'), aid)
+ self.db.commit(); self.db.close()
+
+ # now check we can recall the added class' items
+ self.init_a()
+ self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
+ self.assertEqual(self.db.a.lookup('apple'), aid)
+
+ # confirm journal's ok
+ self.db.getjournal('a', aid)
+
+class RDBMSTest:
+ ''' 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)
+
+
+class ClassicInitTest(unittest.TestCase):
+ count = 0
+ db = None
+
+ def setUp(self):
+ ClassicInitTest.count = ClassicInitTest.count + 1
+ self.dirname = '_test_init_%s'%self.count
+ try:
+ shutil.rmtree(self.dirname)
+ except OSError, error:
+ if error.errno not in (errno.ENOENT, errno.ESRCH): raise
+
+ def testCreation(self):
+ ae = self.assertEqual
+
+ # set up and open a tracker
+ tracker = setupTracker(self.dirname, self.backend)
+ # open the database
+ db = self.db = tracker.open('test')
+
+ # check the basics of the schema and initial data set
+ l = db.priority.list()
+ ae(l, ['1', '2', '3', '4', '5'])
+ l = db.status.list()
+ ae(l, ['1', '2', '3', '4', '5', '6', '7', '8'])
+ l = db.keyword.list()
+ ae(l, [])
+ l = db.user.list()
+ ae(l, ['1', '2'])
+ l = db.msg.list()
+ ae(l, [])
+ l = db.file.list()
+ ae(l, [])
+ l = db.issue.list()
+ ae(l, [])
+
+ def tearDown(self):
+ if self.db is not None:
+ self.db.close()
+ try:
+ shutil.rmtree(self.dirname)
+ except OSError, error:
+ if error.errno not in (errno.ENOENT, errno.ESRCH): raise
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/test/mocknull.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/mocknull.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,23 @@
+
+class MockNull:
+ def __init__(self, **kwargs):
+ for key, value in kwargs.items():
+ self.__dict__[key] = value
+
+ def __call__(self, *args, **kwargs): return MockNull()
+ def __getattr__(self, name):
+ # This allows assignments which assume all intermediate steps are Null
+ # objects if they don't exist yet.
+ #
+ # For example (with just 'client' defined):
+ #
+ # client.db.config.TRACKER_WEB = 'BASE/'
+ self.__dict__[name] = MockNull()
+ return getattr(self, name)
+
+ def __getitem__(self, key): return self
+ def __nonzero__(self): return 0
+ def __str__(self): return ''
+ def __repr__(self): return '<MockNull 0x%x>'%id(self)
+ def gettext(self, str): return str
+ _ = gettext
Added: tracker/vendor/roundup/current/test/session_common.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/session_common.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,40 @@
+import os, shutil, unittest
+
+from db_test_base import config
+
+class SessionTest(unittest.TestCase):
+ def setUp(self):
+ # remove previous test, ignore errors
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+ os.makedirs(config.DATABASE + '/files')
+ self.db = self.module.Database(config, 'admin')
+ self.sessions = self.sessions_module.Sessions(self.db)
+ self.otks = self.sessions_module.OneTimeKeys(self.db)
+
+ def tearDown(self):
+ del self.otks
+ del self.sessions
+ if hasattr(self, 'db'):
+ self.db.close()
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+
+ def testSetSession(self):
+ self.sessions.set('random_key', text='hello, world!')
+ self.assertEqual(self.sessions.get('random_key', 'text'),
+ 'hello, world!')
+
+ def testUpdateSession(self):
+ self.sessions.set('random_key', text='hello, world!')
+ self.assertEqual(self.sessions.get('random_key', 'text'),
+ 'hello, world!')
+ self.sessions.set('random_key', text='nope')
+ self.assertEqual(self.sessions.get('random_key', 'text'), 'nope')
+
+class DBMTest(SessionTest):
+ import roundup.backends.sessions_dbm as sessions_module
+
+class RDBMSTest(SessionTest):
+ import roundup.backends.sessions_rdbms as sessions_module
+
Added: tracker/vendor/roundup/current/test/test_actions.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_actions.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,242 @@
+from __future__ import nested_scopes
+
+import unittest
+from cgi import FieldStorage, MiniFieldStorage
+
+from roundup import hyperdb
+from roundup.date import Date, Interval
+from roundup.cgi.actions import *
+from roundup.cgi.exceptions import Redirect, Unauthorised, SeriousError
+
+from mocknull import MockNull
+
+def true(*args, **kwargs):
+ return 1
+
+class ActionTestCase(unittest.TestCase):
+ def setUp(self):
+ self.form = FieldStorage()
+ self.client = MockNull()
+ self.client.form = self.form
+ class TemplatingUtils:
+ pass
+ self.client.instance.interfaces.TemplatingUtils = TemplatingUtils
+
+class ShowActionTestCase(ActionTestCase):
+ def assertRaisesMessage(self, exception, callable, message, *args,
+ **kwargs):
+ """An extension of assertRaises, which also checks the exception
+ message. We need this because we rely on exception messages when
+ redirecting.
+ """
+ try:
+ callable(*args, **kwargs)
+ except exception, msg:
+ self.assertEqual(str(msg), message)
+ else:
+ if hasattr(exception, '__name__'):
+ excName = exception.__name__
+ else:
+ excName = str(exception)
+ raise self.failureException, excName
+
+ def testShowAction(self):
+ self.client.base = 'BASE/'
+
+ action = ShowAction(self.client)
+ self.assertRaises(ValueError, action.handle)
+
+ self.form.value.append(MiniFieldStorage('@type', 'issue'))
+ self.assertRaises(SeriousError, action.handle)
+
+ self.form.value.append(MiniFieldStorage('@number', '1'))
+ self.assertRaisesMessage(Redirect, action.handle, 'BASE/issue1')
+
+ def testShowActionNoType(self):
+ action = ShowAction(self.client)
+ self.assertRaises(ValueError, action.handle)
+ self.form.value.append(MiniFieldStorage('@number', '1'))
+ self.assertRaisesMessage(ValueError, action.handle,
+ 'No type specified')
+
+class RetireActionTestCase(ActionTestCase):
+ def testRetireAction(self):
+ self.client.db.security.hasPermission = true
+ self.client.ok_message = []
+ RetireAction(self.client).handle()
+ self.assert_(len(self.client.ok_message) == 1)
+
+ def testNoPermission(self):
+ self.assertRaises(Unauthorised, RetireAction(self.client).execute)
+
+ def testDontRetireAdminOrAnonymous(self):
+ self.client.db.security.hasPermission=true
+ # look up the user class
+ self.client.classname = 'user'
+ # but always look up admin, regardless of nodeid
+ self.client.db.user.get = lambda a,b: 'admin'
+ self.assertRaises(ValueError, RetireAction(self.client).handle)
+ # .. or anonymous
+ self.client.db.user.get = lambda a,b: 'anonymous'
+ self.assertRaises(ValueError, RetireAction(self.client).handle)
+
+class SearchActionTestCase(ActionTestCase):
+ def setUp(self):
+ ActionTestCase.setUp(self)
+ self.action = SearchAction(self.client)
+
+class StandardSearchActionTestCase(SearchActionTestCase):
+ def testNoPermission(self):
+ self.assertRaises(Unauthorised, self.action.execute)
+
+ def testQueryName(self):
+ self.assertEqual(self.action.getQueryName(), '')
+
+ self.form.value.append(MiniFieldStorage('@queryname', 'foo'))
+ self.assertEqual(self.action.getQueryName(), 'foo')
+
+class FakeFilterVarsTestCase(SearchActionTestCase):
+ def setUp(self):
+ SearchActionTestCase.setUp(self)
+ self.client.db.classes.getprops = lambda: {'foo':
+ hyperdb.Multilink('foo')}
+
+ def assertFilterEquals(self, expected):
+ self.action.fakeFilterVars()
+ self.assertEqual(self.form.getvalue('@filter'), expected)
+
+ def testEmptyMultilink(self):
+ self.form.value.append(MiniFieldStorage('foo', ''))
+ self.form.value.append(MiniFieldStorage('foo', ''))
+
+ self.assertFilterEquals(None)
+
+ def testNonEmptyMultilink(self):
+ self.form.value.append(MiniFieldStorage('foo', ''))
+ self.form.value.append(MiniFieldStorage('foo', '1'))
+
+ self.assertFilterEquals('foo')
+
+ def testEmptyKey(self):
+ self.form.value.append(MiniFieldStorage('foo', ''))
+ self.assertFilterEquals(None)
+
+ def testStandardKey(self):
+ self.form.value.append(MiniFieldStorage('foo', '1'))
+ self.assertFilterEquals('foo')
+
+ def testStringKey(self):
+ self.client.db.classes.getprops = lambda: {'foo': hyperdb.String()}
+ self.form.value.append(MiniFieldStorage('foo', 'hello'))
+ self.assertFilterEquals('foo')
+
+ def testTokenizedStringKey(self):
+ self.client.db.classes.getprops = lambda: {'foo': hyperdb.String()}
+ self.form.value.append(MiniFieldStorage('foo', 'hello world'))
+
+ self.assertFilterEquals('foo')
+
+ # The single value gets replaced with the tokenized list.
+ self.assertEqual([x.value for x in self.form['foo']],
+ ['hello', 'world'])
+
+class CollisionDetectionTestCase(ActionTestCase):
+ def setUp(self):
+ ActionTestCase.setUp(self)
+ self.action = EditItemAction(self.client)
+ self.now = Date('.')
+ # round off for testing
+ self.now.second = int(self.now.second)
+
+ def testLastUserActivity(self):
+ self.assertEqual(self.action.lastUserActivity(), None)
+
+ self.client.form.value.append(
+ MiniFieldStorage('@lastactivity', str(self.now)))
+ self.assertEqual(self.action.lastUserActivity(), self.now)
+
+ def testLastNodeActivity(self):
+ self.action.classname = 'issue'
+ self.action.nodeid = '1'
+
+ def get(nodeid, propname):
+ self.assertEqual(nodeid, '1')
+ self.assertEqual(propname, 'activity')
+ return self.now
+ self.client.db.issue.get = get
+
+ self.assertEqual(self.action.lastNodeActivity(), self.now)
+
+ def testCollision(self):
+ # fake up an actual change
+ self.action.classname = 'test'
+ self.action.nodeid = '1'
+ self.client.parsePropsFromForm = lambda: ({('test','1'):{1:1}}, [])
+ self.failUnless(self.action.detectCollision(self.now,
+ self.now + Interval("1d")))
+ self.failIf(self.action.detectCollision(self.now,
+ self.now - Interval("1d")))
+ self.failIf(self.action.detectCollision(None, self.now))
+
+class LoginTestCase(ActionTestCase):
+ def setUp(self):
+ ActionTestCase.setUp(self)
+ self.client.error_message = []
+
+ # set the db password to 'right'
+ self.client.db.user.get = lambda a,b: 'right'
+
+ # unless explicitly overridden, we should never get here
+ self.client.opendb = lambda a: self.fail(
+ "Logged in, but we shouldn't be.")
+
+ def assertLoginLeavesMessages(self, messages, username=None, password=None):
+ if username is not None:
+ self.form.value.append(MiniFieldStorage('__login_name', username))
+ if password is not None:
+ self.form.value.append(
+ MiniFieldStorage('__login_password', password))
+
+ LoginAction(self.client).handle()
+ self.assertEqual(self.client.error_message, messages)
+
+ def testNoUsername(self):
+ self.assertLoginLeavesMessages(['Username required'])
+
+ def testInvalidUsername(self):
+ def raiseKeyError(a):
+ raise KeyError
+ self.client.db.user.lookup = raiseKeyError
+ self.assertLoginLeavesMessages(['Invalid login'], 'foo')
+
+ def testInvalidPassword(self):
+ self.assertLoginLeavesMessages(['Invalid login'], 'foo', 'wrong')
+
+ def testNoWebAccess(self):
+ self.assertLoginLeavesMessages(['You do not have permission to login'],
+ 'foo', 'right')
+
+ def testCorrectLogin(self):
+ self.client.db.security.hasPermission = lambda *args, **kwargs: True
+
+ def opendb(username):
+ self.assertEqual(username, 'foo')
+ self.client.opendb = opendb
+
+ self.assertLoginLeavesMessages([], 'foo', 'right')
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(RetireActionTestCase))
+ suite.addTest(unittest.makeSuite(StandardSearchActionTestCase))
+ suite.addTest(unittest.makeSuite(FakeFilterVarsTestCase))
+ suite.addTest(unittest.makeSuite(ShowActionTestCase))
+ suite.addTest(unittest.makeSuite(CollisionDetectionTestCase))
+ suite.addTest(unittest.makeSuite(LoginTestCase))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/test/test_anydbm.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_anydbm.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,62 @@
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: test_anydbm.py,v 1.4 2004/11/03 01:34:21 richard Exp $
+
+import unittest, os, shutil, time
+from roundup.backends import get_backend
+
+from db_test_base import DBTest, ROTest, SchemaTest, ClassicInitTest, config
+
+class anydbmOpener:
+ module = get_backend('anydbm')
+
+ def nuke_database(self):
+ shutil.rmtree(config.DATABASE)
+
+class anydbmDBTest(anydbmOpener, DBTest):
+ pass
+
+class anydbmROTest(anydbmOpener, ROTest):
+ pass
+
+class anydbmSchemaTest(anydbmOpener, SchemaTest):
+ pass
+
+class anydbmClassicInitTest(ClassicInitTest):
+ backend = 'anydbm'
+
+from session_common import DBMTest
+class anydbmSessionTest(anydbmOpener, DBMTest):
+ pass
+
+def test_suite():
+ suite = unittest.TestSuite()
+ print 'Including anydbm tests'
+ suite.addTest(unittest.makeSuite(anydbmDBTest))
+ suite.addTest(unittest.makeSuite(anydbmROTest))
+ suite.addTest(unittest.makeSuite(anydbmSchemaTest))
+ suite.addTest(unittest.makeSuite(anydbmClassicInitTest))
+ suite.addTest(unittest.makeSuite(anydbmSessionTest))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/test/test_cgi.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_cgi.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,551 @@
+#
+# Copyright (c) 2003 Richard Jones, rjones at ekit-inc.com
+# 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.
+#
+# This module is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+#
+# $Id: test_cgi.py,v 1.27 2006/01/25 02:24:28 richard Exp $
+
+import unittest, os, shutil, errno, sys, difflib, cgi, re
+
+from roundup.cgi import client
+from roundup.cgi.exceptions import FormError
+from roundup.cgi.form_parser import FormParser
+from roundup import init, instance, password, hyperdb, date
+
+import db_test_base
+
+NEEDS_INSTANCE = 1
+
+class FileUpload:
+ def __init__(self, content, filename):
+ self.content = content
+ self.filename = filename
+
+def makeForm(args):
+ form = cgi.FieldStorage()
+ for k,v in args.items():
+ if type(v) is type([]):
+ [form.list.append(cgi.MiniFieldStorage(k, x)) for x in v]
+ elif isinstance(v, FileUpload):
+ x = cgi.MiniFieldStorage(k, v.content)
+ x.filename = v.filename
+ form.list.append(x)
+ else:
+ form.list.append(cgi.MiniFieldStorage(k, v))
+ return form
+
+cm = client.clean_message
+class MessageTestCase(unittest.TestCase):
+ def testCleanMessageOK(self):
+ self.assertEqual(cm('<br>x<br />'), '<br>x<br />')
+ self.assertEqual(cm('<i>x</i>'), '<i>x</i>')
+ self.assertEqual(cm('<b>x</b>'), '<b>x</b>')
+ self.assertEqual(cm('<a href="y">x</a>'),
+ '<a href="y">x</a>')
+ self.assertEqual(cm('<BR>x<BR />'), '<BR>x<BR />')
+ self.assertEqual(cm('<I>x</I>'), '<I>x</I>')
+ self.assertEqual(cm('<B>x</B>'), '<B>x</B>')
+ self.assertEqual(cm('<A HREF="y">x</A>'),
+ '<A HREF="y">x</A>')
+
+ def testCleanMessageBAD(self):
+ self.assertEqual(cm('<script>x</script>'),
+ '<script>x</script>')
+ self.assertEqual(cm('<iframe>x</iframe>'),
+ '<iframe>x</iframe>')
+
+class FormTestCase(unittest.TestCase):
+ def setUp(self):
+ self.dirname = '_test_cgi_form'
+ # set up and open a tracker
+ self.instance = db_test_base.setupTracker(self.dirname)
+
+ # open the database
+ self.db = self.instance.open('admin')
+ self.db.user.create(username='Chef', address='chef at bork.bork.bork',
+ realname='Bork, Chef', roles='User')
+ self.db.user.create(username='mary', address='mary at test',
+ roles='User', realname='Contrary, Mary')
+
+ test = self.instance.backend.Class(self.db, "test",
+ string=hyperdb.String(), number=hyperdb.Number(),
+ boolean=hyperdb.Boolean(), link=hyperdb.Link('test'),
+ multilink=hyperdb.Multilink('test'), date=hyperdb.Date(),
+ interval=hyperdb.Interval())
+
+ # compile the labels re
+ classes = '|'.join(self.db.classes.keys())
+ self.FV_SPECIAL = re.compile(FormParser.FV_LABELS%classes,
+ re.VERBOSE)
+
+ def parseForm(self, form, classname='test', nodeid=None):
+ cl = client.Client(self.instance, None, {'PATH_INFO':'/'},
+ makeForm(form))
+ cl.classname = classname
+ cl.nodeid = nodeid
+ cl.db = self.db
+ return cl.parsePropsFromForm(create=1)
+
+ def tearDown(self):
+ self.db.close()
+ try:
+ shutil.rmtree(self.dirname)
+ except OSError, error:
+ if error.errno not in (errno.ENOENT, errno.ESRCH): raise
+
+ #
+ # form label extraction
+ #
+ def tl(self, s, c, i, a, p):
+ m = self.FV_SPECIAL.match(s)
+ self.assertNotEqual(m, None)
+ d = m.groupdict()
+ self.assertEqual(d['classname'], c)
+ self.assertEqual(d['id'], i)
+ for action in 'required add remove link note file'.split():
+ if a == action:
+ self.assertNotEqual(d[action], None)
+ else:
+ self.assertEqual(d[action], None)
+ self.assertEqual(d['propname'], p)
+
+ def testLabelMatching(self):
+ self.tl('<propname>', None, None, None, '<propname>')
+ self.tl(':required', None, None, 'required', None)
+ self.tl(':confirm:<propname>', None, None, 'confirm', '<propname>')
+ self.tl(':add:<propname>', None, None, 'add', '<propname>')
+ self.tl(':remove:<propname>', None, None, 'remove', '<propname>')
+ self.tl(':link:<propname>', None, None, 'link', '<propname>')
+ self.tl('test1:<prop>', 'test', '1', None, '<prop>')
+ self.tl('test1:required', 'test', '1', 'required', None)
+ self.tl('test1:add:<prop>', 'test', '1', 'add', '<prop>')
+ self.tl('test1:remove:<prop>', 'test', '1', 'remove', '<prop>')
+ self.tl('test1:link:<prop>', 'test', '1', 'link', '<prop>')
+ self.tl('test1:confirm:<prop>', 'test', '1', 'confirm', '<prop>')
+ self.tl('test-1:<prop>', 'test', '-1', None, '<prop>')
+ self.tl('test-1:required', 'test', '-1', 'required', None)
+ self.tl('test-1:add:<prop>', 'test', '-1', 'add', '<prop>')
+ self.tl('test-1:remove:<prop>', 'test', '-1', 'remove', '<prop>')
+ self.tl('test-1:link:<prop>', 'test', '-1', 'link', '<prop>')
+ self.tl('test-1:confirm:<prop>', 'test', '-1', 'confirm', '<prop>')
+ self.tl(':note', None, None, 'note', None)
+ self.tl(':file', None, None, 'file', None)
+
+ #
+ # Empty form
+ #
+ def testNothing(self):
+ self.assertEqual(self.parseForm({}), ({('test', None): {}}, []))
+
+ def testNothingWithRequired(self):
+ self.assertRaises(FormError, self.parseForm, {':required': 'string'})
+ self.assertRaises(FormError, self.parseForm,
+ {':required': 'title,status', 'status':'1'}, 'issue')
+ self.assertRaises(FormError, self.parseForm,
+ {':required': ['title','status'], 'status':'1'}, 'issue')
+ self.assertRaises(FormError, self.parseForm,
+ {':required': 'status', 'status':''}, 'issue')
+ self.assertRaises(FormError, self.parseForm,
+ {':required': 'nosy', 'nosy':''}, 'issue')
+
+ #
+ # Nonexistant edit
+ #
+ def testEditNonexistant(self):
+ self.assertRaises(FormError, self.parseForm, {'boolean': ''},
+ 'test', '1')
+
+ #
+ # String
+ #
+ def testEmptyString(self):
+ self.assertEqual(self.parseForm({'string': ''}),
+ ({('test', None): {}}, []))
+ self.assertEqual(self.parseForm({'string': ' '}),
+ ({('test', None): {}}, []))
+ self.assertRaises(FormError, self.parseForm, {'string': ['', '']})
+
+ def testSetString(self):
+ self.assertEqual(self.parseForm({'string': 'foo'}),
+ ({('test', None): {'string': 'foo'}}, []))
+ self.assertEqual(self.parseForm({'string': 'a\r\nb\r\n'}),
+ ({('test', None): {'string': 'a\nb'}}, []))
+ nodeid = self.db.issue.create(title='foo')
+ self.assertEqual(self.parseForm({'title': 'foo'}, 'issue', nodeid),
+ ({('issue', nodeid): {}}, []))
+
+ def testEmptyStringSet(self):
+ nodeid = self.db.issue.create(title='foo')
+ self.assertEqual(self.parseForm({'title': ''}, 'issue', nodeid),
+ ({('issue', nodeid): {'title': None}}, []))
+ nodeid = self.db.issue.create(title='foo')
+ self.assertEqual(self.parseForm({'title': ' '}, 'issue', nodeid),
+ ({('issue', nodeid): {'title': None}}, []))
+
+ def testFileUpload(self):
+ file = FileUpload('foo', 'foo.txt')
+ self.assertEqual(self.parseForm({'content': file}, 'file'),
+ ({('file', None): {'content': 'foo', 'name': 'foo.txt',
+ 'type': 'text/plain'}}, []))
+
+ def testEditFileClassAttributes(self):
+ self.assertEqual(self.parseForm({'name': 'foo.txt',
+ 'type': 'application/octet-stream'},
+ 'file'),
+ ({('file', None): {'name': 'foo.txt',
+ 'type': 'application/octet-stream'}},[]))
+
+ #
+ # Link
+ #
+ def testEmptyLink(self):
+ self.assertEqual(self.parseForm({'link': ''}),
+ ({('test', None): {}}, []))
+ self.assertEqual(self.parseForm({'link': ' '}),
+ ({('test', None): {}}, []))
+ self.assertRaises(FormError, self.parseForm, {'link': ['', '']})
+ self.assertEqual(self.parseForm({'link': '-1'}),
+ ({('test', None): {}}, []))
+
+ def testSetLink(self):
+ self.assertEqual(self.parseForm({'status': 'unread'}, 'issue'),
+ ({('issue', None): {'status': '1'}}, []))
+ self.assertEqual(self.parseForm({'status': '1'}, 'issue'),
+ ({('issue', None): {'status': '1'}}, []))
+ nodeid = self.db.issue.create(status='unread')
+ self.assertEqual(self.parseForm({'status': 'unread'}, 'issue', nodeid),
+ ({('issue', nodeid): {}}, []))
+
+ def testUnsetLink(self):
+ nodeid = self.db.issue.create(status='unread')
+ self.assertEqual(self.parseForm({'status': '-1'}, 'issue', nodeid),
+ ({('issue', nodeid): {'status': None}}, []))
+
+ def testInvalidLinkValue(self):
+# XXX This is not the current behaviour - should we enforce this?
+# self.assertRaises(IndexError, self.parseForm,
+# {'status': '4'}))
+ self.assertRaises(FormError, self.parseForm, {'link': 'frozzle'})
+ self.assertRaises(FormError, self.parseForm, {'status': 'frozzle'},
+ 'issue')
+
+ #
+ # Multilink
+ #
+ def testEmptyMultilink(self):
+ self.assertEqual(self.parseForm({'nosy': ''}),
+ ({('test', None): {}}, []))
+ self.assertEqual(self.parseForm({'nosy': ' '}),
+ ({('test', None): {}}, []))
+
+ def testSetMultilink(self):
+ self.assertEqual(self.parseForm({'nosy': '1'}, 'issue'),
+ ({('issue', None): {'nosy': ['1']}}, []))
+ self.assertEqual(self.parseForm({'nosy': 'admin'}, 'issue'),
+ ({('issue', None): {'nosy': ['1']}}, []))
+ self.assertEqual(self.parseForm({'nosy': ['1','2']}, 'issue'),
+ ({('issue', None): {'nosy': ['1','2']}}, []))
+ self.assertEqual(self.parseForm({'nosy': '1,2'}, 'issue'),
+ ({('issue', None): {'nosy': ['1','2']}}, []))
+ self.assertEqual(self.parseForm({'nosy': 'admin,2'}, 'issue'),
+ ({('issue', None): {'nosy': ['1','2']}}, []))
+
+ def testMixedMultilink(self):
+ form = cgi.FieldStorage()
+ form.list.append(cgi.MiniFieldStorage('nosy', '1,2'))
+ form.list.append(cgi.MiniFieldStorage('nosy', '3'))
+ cl = client.Client(self.instance, None, {'PATH_INFO':'/'}, form)
+ cl.classname = 'issue'
+ cl.nodeid = None
+ cl.db = self.db
+ self.assertEqual(cl.parsePropsFromForm(create=1),
+ ({('issue', None): {'nosy': ['1','2', '3']}}, []))
+
+ def testEmptyMultilinkSet(self):
+ nodeid = self.db.issue.create(nosy=['1','2'])
+ self.assertEqual(self.parseForm({'nosy': ''}, 'issue', nodeid),
+ ({('issue', nodeid): {'nosy': []}}, []))
+ nodeid = self.db.issue.create(nosy=['1','2'])
+ self.assertEqual(self.parseForm({'nosy': ' '}, 'issue', nodeid),
+ ({('issue', nodeid): {'nosy': []}}, []))
+ self.assertEqual(self.parseForm({'nosy': '1,2'}, 'issue', nodeid),
+ ({('issue', nodeid): {}}, []))
+
+ def testInvalidMultilinkValue(self):
+# XXX This is not the current behaviour - should we enforce this?
+# self.assertRaises(IndexError, self.parseForm,
+# {'nosy': '4'}))
+ self.assertRaises(FormError, self.parseForm, {'nosy': 'frozzle'},
+ 'issue')
+ self.assertRaises(FormError, self.parseForm, {'nosy': '1,frozzle'},
+ 'issue')
+ self.assertRaises(FormError, self.parseForm, {'multilink': 'frozzle'})
+
+ def testMultilinkAdd(self):
+ nodeid = self.db.issue.create(nosy=['1'])
+ # do nothing
+ self.assertEqual(self.parseForm({':add:nosy': ''}, 'issue', nodeid),
+ ({('issue', nodeid): {}}, []))
+
+ # do something ;)
+ self.assertEqual(self.parseForm({':add:nosy': '2'}, 'issue', nodeid),
+ ({('issue', nodeid): {'nosy': ['1','2']}}, []))
+ self.assertEqual(self.parseForm({':add:nosy': '2,mary'}, 'issue',
+ nodeid), ({('issue', nodeid): {'nosy': ['1','2','4']}}, []))
+ self.assertEqual(self.parseForm({':add:nosy': ['2','3']}, 'issue',
+ nodeid), ({('issue', nodeid): {'nosy': ['1','2','3']}}, []))
+
+ def testMultilinkAddNew(self):
+ self.assertEqual(self.parseForm({':add:nosy': ['2','3']}, 'issue'),
+ ({('issue', None): {'nosy': ['2','3']}}, []))
+
+ def testMultilinkRemove(self):
+ nodeid = self.db.issue.create(nosy=['1','2'])
+ # do nothing
+ self.assertEqual(self.parseForm({':remove:nosy': ''}, 'issue', nodeid),
+ ({('issue', nodeid): {}}, []))
+
+ # do something ;)
+ self.assertEqual(self.parseForm({':remove:nosy': '1'}, 'issue',
+ nodeid), ({('issue', nodeid): {'nosy': ['2']}}, []))
+ self.assertEqual(self.parseForm({':remove:nosy': 'admin,2'},
+ 'issue', nodeid), ({('issue', nodeid): {'nosy': []}}, []))
+ self.assertEqual(self.parseForm({':remove:nosy': ['1','2']},
+ 'issue', nodeid), ({('issue', nodeid): {'nosy': []}}, []))
+
+ # add and remove
+ self.assertEqual(self.parseForm({':add:nosy': ['3'],
+ ':remove:nosy': ['1','2']},
+ 'issue', nodeid), ({('issue', nodeid): {'nosy': ['3']}}, []))
+
+ # remove one that doesn't exist?
+ self.assertRaises(FormError, self.parseForm, {':remove:nosy': '4'},
+ 'issue', nodeid)
+
+ def testMultilinkRetired(self):
+ self.db.user.retire('2')
+ self.assertEqual(self.parseForm({'nosy': ['2','3']}, 'issue'),
+ ({('issue', None): {'nosy': ['2','3']}}, []))
+ nodeid = self.db.issue.create(nosy=['1','2'])
+ self.assertEqual(self.parseForm({':remove:nosy': '2'}, 'issue',
+ nodeid), ({('issue', nodeid): {'nosy': ['1']}}, []))
+ self.assertEqual(self.parseForm({':add:nosy': '3'}, 'issue', nodeid),
+ ({('issue', nodeid): {'nosy': ['1','2','3']}}, []))
+
+ def testAddRemoveNonexistant(self):
+ self.assertRaises(FormError, self.parseForm, {':remove:foo': '2'},
+ 'issue')
+ self.assertRaises(FormError, self.parseForm, {':add:foo': '2'},
+ 'issue')
+
+ #
+ # Password
+ #
+ def testEmptyPassword(self):
+ self.assertEqual(self.parseForm({'password': ''}, 'user'),
+ ({('user', None): {}}, []))
+ self.assertEqual(self.parseForm({'password': ''}, 'user'),
+ ({('user', None): {}}, []))
+ self.assertRaises(FormError, self.parseForm, {'password': ['', '']},
+ 'user')
+ self.assertRaises(FormError, self.parseForm, {'password': 'foo',
+ ':confirm:password': ['', '']}, 'user')
+
+ def testSetPassword(self):
+ self.assertEqual(self.parseForm({'password': 'foo',
+ ':confirm:password': 'foo'}, 'user'),
+ ({('user', None): {'password': 'foo'}}, []))
+
+ def testSetPasswordConfirmBad(self):
+ self.assertRaises(FormError, self.parseForm, {'password': 'foo'},
+ 'user')
+ self.assertRaises(FormError, self.parseForm, {'password': 'foo',
+ ':confirm:password': 'bar'}, 'user')
+
+ def testEmptyPasswordNotSet(self):
+ nodeid = self.db.user.create(username='1',
+ password=password.Password('foo'))
+ self.assertEqual(self.parseForm({'password': ''}, 'user', nodeid),
+ ({('user', nodeid): {}}, []))
+ nodeid = self.db.user.create(username='2',
+ password=password.Password('foo'))
+ self.assertEqual(self.parseForm({'password': '',
+ ':confirm:password': ''}, 'user', nodeid),
+ ({('user', nodeid): {}}, []))
+
+ #
+ # Boolean
+ #
+ def testEmptyBoolean(self):
+ self.assertEqual(self.parseForm({'boolean': ''}),
+ ({('test', None): {}}, []))
+ self.assertEqual(self.parseForm({'boolean': ' '}),
+ ({('test', None): {}}, []))
+ self.assertRaises(FormError, self.parseForm, {'boolean': ['', '']})
+
+ def testSetBoolean(self):
+ self.assertEqual(self.parseForm({'boolean': 'yes'}),
+ ({('test', None): {'boolean': 1}}, []))
+ self.assertEqual(self.parseForm({'boolean': 'a\r\nb\r\n'}),
+ ({('test', None): {'boolean': 0}}, []))
+ nodeid = self.db.test.create(boolean=1)
+ self.assertEqual(self.parseForm({'boolean': 'yes'}, 'test', nodeid),
+ ({('test', nodeid): {}}, []))
+ nodeid = self.db.test.create(boolean=0)
+ self.assertEqual(self.parseForm({'boolean': 'no'}, 'test', nodeid),
+ ({('test', nodeid): {}}, []))
+
+ def testEmptyBooleanSet(self):
+ nodeid = self.db.test.create(boolean=0)
+ self.assertEqual(self.parseForm({'boolean': ''}, 'test', nodeid),
+ ({('test', nodeid): {'boolean': None}}, []))
+ nodeid = self.db.test.create(boolean=1)
+ self.assertEqual(self.parseForm({'boolean': ' '}, 'test', nodeid),
+ ({('test', nodeid): {'boolean': None}}, []))
+
+ #
+ # Number
+ #
+ def testEmptyNumber(self):
+ self.assertEqual(self.parseForm({'number': ''}),
+ ({('test', None): {}}, []))
+ self.assertEqual(self.parseForm({'number': ' '}),
+ ({('test', None): {}}, []))
+ self.assertRaises(FormError, self.parseForm, {'number': ['', '']})
+
+ def testInvalidNumber(self):
+ self.assertRaises(FormError, self.parseForm, {'number': 'hi, mum!'})
+
+ def testSetNumber(self):
+ self.assertEqual(self.parseForm({'number': '1'}),
+ ({('test', None): {'number': 1}}, []))
+ self.assertEqual(self.parseForm({'number': '0'}),
+ ({('test', None): {'number': 0}}, []))
+ self.assertEqual(self.parseForm({'number': '\n0\n'}),
+ ({('test', None): {'number': 0}}, []))
+
+ def testSetNumberReplaceOne(self):
+ nodeid = self.db.test.create(number=1)
+ self.assertEqual(self.parseForm({'number': '1'}, 'test', nodeid),
+ ({('test', nodeid): {}}, []))
+ self.assertEqual(self.parseForm({'number': '0'}, 'test', nodeid),
+ ({('test', nodeid): {'number': 0}}, []))
+
+ def testSetNumberReplaceZero(self):
+ nodeid = self.db.test.create(number=0)
+ self.assertEqual(self.parseForm({'number': '0'}, 'test', nodeid),
+ ({('test', nodeid): {}}, []))
+
+ def testSetNumberReplaceNone(self):
+ nodeid = self.db.test.create()
+ self.assertEqual(self.parseForm({'number': '0'}, 'test', nodeid),
+ ({('test', nodeid): {'number': 0}}, []))
+ self.assertEqual(self.parseForm({'number': '1'}, 'test', nodeid),
+ ({('test', nodeid): {'number': 1}}, []))
+
+ def testEmptyNumberSet(self):
+ nodeid = self.db.test.create(number=0)
+ self.assertEqual(self.parseForm({'number': ''}, 'test', nodeid),
+ ({('test', nodeid): {'number': None}}, []))
+ nodeid = self.db.test.create(number=1)
+ self.assertEqual(self.parseForm({'number': ' '}, 'test', nodeid),
+ ({('test', nodeid): {'number': None}}, []))
+
+ #
+ # Date
+ #
+ def testEmptyDate(self):
+ self.assertEqual(self.parseForm({'date': ''}),
+ ({('test', None): {}}, []))
+ self.assertEqual(self.parseForm({'date': ' '}),
+ ({('test', None): {}}, []))
+ self.assertRaises(FormError, self.parseForm, {'date': ['', '']})
+
+ def testInvalidDate(self):
+ self.assertRaises(FormError, self.parseForm, {'date': '12'})
+
+ def testSetDate(self):
+ self.assertEqual(self.parseForm({'date': '2003-01-01'}),
+ ({('test', None): {'date': date.Date('2003-01-01')}}, []))
+ nodeid = self.db.test.create(date=date.Date('2003-01-01'))
+ self.assertEqual(self.parseForm({'date': '2003-01-01'}, 'test',
+ nodeid), ({('test', nodeid): {}}, []))
+
+ def testEmptyDateSet(self):
+ nodeid = self.db.test.create(date=date.Date('.'))
+ self.assertEqual(self.parseForm({'date': ''}, 'test', nodeid),
+ ({('test', nodeid): {'date': None}}, []))
+ nodeid = self.db.test.create(date=date.Date('1970-01-01.00:00:00'))
+ self.assertEqual(self.parseForm({'date': ' '}, 'test', nodeid),
+ ({('test', nodeid): {'date': None}}, []))
+
+ #
+ # Test multiple items in form
+ #
+ def testMultiple(self):
+ self.assertEqual(self.parseForm({'string': 'a', 'issue-1 at title': 'b'}),
+ ({('test', None): {'string': 'a'},
+ ('issue', '-1'): {'title': 'b'}
+ }, []))
+
+ def testMultipleExistingContext(self):
+ nodeid = self.db.test.create()
+ self.assertEqual(self.parseForm({'string': 'a', 'issue-1 at title': 'b'},
+ 'test', nodeid),({('test', nodeid): {'string': 'a'},
+ ('issue', '-1'): {'title': 'b'}}, []))
+
+ def testLinking(self):
+ self.assertEqual(self.parseForm({
+ 'string': 'a',
+ 'issue-1 at add@nosy': '1',
+ 'issue-2 at link@superseder': 'issue-1',
+ }),
+ ({('test', None): {'string': 'a'},
+ ('issue', '-1'): {'nosy': ['1']},
+ ('issue', '-2'): {}
+ },
+ [('issue', '-2', 'superseder', [('issue', '-1')])
+ ]
+ )
+ )
+
+ def testLinkBadDesignator(self):
+ self.assertRaises(FormError, self.parseForm,
+ {'test-1 at link@link': 'blah'})
+ self.assertRaises(FormError, self.parseForm,
+ {'test-1 at link@link': 'issue'})
+
+ def testLinkNotLink(self):
+ self.assertRaises(FormError, self.parseForm,
+ {'test-1 at link@boolean': 'issue-1'})
+ self.assertRaises(FormError, self.parseForm,
+ {'test-1 at link@string': 'issue-1'})
+
+ def testBackwardsCompat(self):
+ res = self.parseForm({':note': 'spam'}, 'issue')
+ date = res[0][('msg', '-1')]['date']
+ self.assertEqual(res, ({('issue', None): {}, ('msg', '-1'):
+ {'content': 'spam', 'author': '1', 'date': date}},
+ [('issue', None, 'messages', [('msg', '-1')])]))
+ file = FileUpload('foo', 'foo.txt')
+ self.assertEqual(self.parseForm({':file': file}, 'issue'),
+ ({('issue', None): {}, ('file', '-1'): {'content': 'foo',
+ 'name': 'foo.txt', 'type': 'text/plain'}},
+ [('issue', None, 'files', [('file', '-1')])]))
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(FormTestCase))
+ suite.addTest(unittest.makeSuite(MessageTestCase))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/test/test_dates.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_dates.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,402 @@
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: test_dates.py,v 1.38 2006/03/03 02:02:50 richard Exp $
+from __future__ import nested_scopes
+
+import unittest, time
+
+from roundup.date import Date, Interval, Range, fixTimeOverflow
+
+class DateTestCase(unittest.TestCase):
+ def testDateInterval(self):
+ ae = self.assertEqual
+ date = Date("2000-06-26.00:34:02 + 2d")
+ ae(str(date), '2000-06-28.00:34:02')
+ date = Date("2000-02-27 + 2d")
+ ae(str(date), '2000-02-29.00:00:00')
+ date = Date("2001-02-27 + 2d")
+ ae(str(date), '2001-03-01.00:00:00')
+
+ def testDate(self):
+ ae = self.assertEqual
+ date = Date("2000-04-17")
+ ae(str(date), '2000-04-17.00:00:00')
+ date = Date("2000/04/17")
+ ae(str(date), '2000-04-17.00:00:00')
+ date = Date("2000-4-7")
+ ae(str(date), '2000-04-07.00:00:00')
+ date = Date("2000-4-17")
+ ae(str(date), '2000-04-17.00:00:00')
+ date = Date("01-25")
+ y, m, d, x, x, x, x, x, x = time.gmtime(time.time())
+ ae(str(date), '%s-01-25.00:00:00'%y)
+ date = Date("2000-04-17.03:45")
+ ae(str(date), '2000-04-17.03:45:00')
+ date = Date("2000/04/17.03:45")
+ ae(str(date), '2000-04-17.03:45:00')
+ date = Date("08-13.22:13")
+ ae(str(date), '%s-08-13.22:13:00'%y)
+ date = Date("11-07.09:32:43")
+ ae(str(date), '%s-11-07.09:32:43'%y)
+ date = Date("14:25")
+ ae(str(date), '%s-%02d-%02d.14:25:00'%(y, m, d))
+ date = Date("8:47:11")
+ ae(str(date), '%s-%02d-%02d.08:47:11'%(y, m, d))
+ ae(str(Date('2003')), '2003-01-01.00:00:00')
+ ae(str(Date('2004-06')), '2004-06-01.00:00:00')
+
+ def testDateError(self):
+ self.assertRaises(ValueError, Date, "12")
+ # Date cannot handle dates before UNIX epoch
+ self.assertRaises(ValueError, Date, (1, 1, 1, 0, 0, 0.0, 0, 1, -1))
+ self.assertRaises(ValueError, Date, "1/1/06")
+
+ def testOffset(self):
+ ae = self.assertEqual
+ date = Date("2000-04-17", -5)
+ ae(str(date), '2000-04-17.05:00:00')
+ date = Date("01-25", -5)
+ y, m, d, x, x, x, x, x, x = time.gmtime(time.time())
+ ae(str(date), '%s-01-25.05:00:00'%y)
+ date = Date("2000-04-17.03:45", -5)
+ ae(str(date), '2000-04-17.08:45:00')
+ date = Date("08-13.22:13", -5)
+ ae(str(date), '%s-08-14.03:13:00'%y)
+ date = Date("11-07.09:32:43", -5)
+ ae(str(date), '%s-11-07.14:32:43'%y)
+ date = Date("14:25", -5)
+ ae(str(date), '%s-%02d-%02d.19:25:00'%(y, m, d))
+ date = Date("8:47:11", -5)
+ ae(str(date), '%s-%02d-%02d.13:47:11'%(y, m, d))
+
+ # just make sure we parse these, m'kay?
+ date = Date('-1d')
+ date = Date('-1w')
+ date = Date('-1m')
+ date = Date('-1y')
+
+ def testOffsetRandom(self):
+ ae = self.assertEqual
+ # XXX unsure of the usefulness of these, they're pretty random
+ date = Date('2000-01-01') + Interval('- 2y 2m')
+ ae(str(date), '1997-11-01.00:00:00')
+ date = Date('2000-01-01 - 2y 2m')
+ ae(str(date), '1997-11-01.00:00:00')
+ date = Date('2000-01-01') + Interval('2m')
+ ae(str(date), '2000-03-01.00:00:00')
+ date = Date('2000-01-01 + 2m')
+ ae(str(date), '2000-03-01.00:00:00')
+
+ date = Date('2000-01-01') + Interval('60d')
+ ae(str(date), '2000-03-01.00:00:00')
+ date = Date('2001-01-01') + Interval('60d')
+ ae(str(date), '2001-03-02.00:00:00')
+
+ def testOffsetAdd(self):
+ ae = self.assertEqual
+ date = Date('2000-02-28.23:59:59') + Interval('00:00:01')
+ ae(str(date), '2000-02-29.00:00:00')
+ date = Date('2001-02-28.23:59:59') + Interval('00:00:01')
+ ae(str(date), '2001-03-01.00:00:00')
+
+ date = Date('2000-02-28.23:58:59') + Interval('00:01:01')
+ ae(str(date), '2000-02-29.00:00:00')
+ date = Date('2001-02-28.23:58:59') + Interval('00:01:01')
+ ae(str(date), '2001-03-01.00:00:00')
+
+ date = Date('2000-02-28.22:58:59') + Interval('01:01:01')
+ ae(str(date), '2000-02-29.00:00:00')
+ date = Date('2001-02-28.22:58:59') + Interval('01:01:01')
+ ae(str(date), '2001-03-01.00:00:00')
+
+ date = Date('2000-02-28.22:58:59') + Interval('00:00:3661')
+ ae(str(date), '2000-02-29.00:00:00')
+ date = Date('2001-02-28.22:58:59') + Interval('00:00:3661')
+ ae(str(date), '2001-03-01.00:00:00')
+
+ def testOffsetSub(self):
+ ae = self.assertEqual
+ date = Date('2000-12-01') - Interval('- 1d')
+
+ date = Date('2000-01-01') - Interval('- 2y 2m')
+ ae(str(date), '2002-03-01.00:00:00')
+ date = Date('2000-01-01') - Interval('2m')
+ ae(str(date), '1999-11-01.00:00:00')
+
+ date = Date('2000-03-01') - Interval('60d')
+ ae(str(date), '2000-01-01.00:00:00')
+ date = Date('2001-03-02') - Interval('60d')
+ ae(str(date), '2001-01-01.00:00:00')
+
+ date = Date('2000-02-29.00:00:00') - Interval('00:00:01')
+ ae(str(date), '2000-02-28.23:59:59')
+ date = Date('2001-03-01.00:00:00') - Interval('00:00:01')
+ ae(str(date), '2001-02-28.23:59:59')
+
+ date = Date('2000-02-29.00:00:00') - Interval('00:01:01')
+ ae(str(date), '2000-02-28.23:58:59')
+ date = Date('2001-03-01.00:00:00') - Interval('00:01:01')
+ ae(str(date), '2001-02-28.23:58:59')
+
+ date = Date('2000-02-29.00:00:00') - Interval('01:01:01')
+ ae(str(date), '2000-02-28.22:58:59')
+ date = Date('2001-03-01.00:00:00') - Interval('01:01:01')
+ ae(str(date), '2001-02-28.22:58:59')
+
+ date = Date('2000-02-29.00:00:00') - Interval('00:00:3661')
+ ae(str(date), '2000-02-28.22:58:59')
+ date = Date('2001-03-01.00:00:00') - Interval('00:00:3661')
+ ae(str(date), '2001-02-28.22:58:59')
+
+ def testDateLocal(self):
+ ae = self.assertEqual
+ date = Date("02:42:20")
+ date = date.local(10)
+ y, m, d, x, x, x, x, x, x = time.gmtime(time.time())
+ ae(str(date), '%s-%02d-%02d.12:42:20'%(y, m, d))
+
+ def testIntervalInit(self):
+ ae = self.assertEqual
+ ae(str(Interval('3y')), '+ 3y')
+ ae(str(Interval('2 y 1 m')), '+ 2y 1m')
+ ae(str(Interval('1m 25d')), '+ 1m 25d')
+ ae(str(Interval('-2w 3 d ')), '- 17d')
+ ae(str(Interval(' - 1 d 2:50 ')), '- 1d 2:50')
+ ae(str(Interval(' 14:00 ')), '+ 14:00')
+ ae(str(Interval(' 0:04:33 ')), '+ 0:04:33')
+ ae(str(Interval(8.*3600)), '+ 8:00')
+
+ def testIntervalInitDate(self):
+ ae = self.assertEqual
+ now = Date('.')
+ now.hour = now.minute = now.second = 0
+ then = now + Interval('2d')
+ ae((Interval(str(then))), Interval('- 2d'))
+ then = now - Interval('2d')
+ ae(Interval(str(then)), Interval('+ 2d'))
+
+ def testIntervalAddMonthBoundary(self):
+ # force the transition over a month boundary
+ now = Date('2003-10-30.00:00:00')
+ then = now + Interval('2d')
+ self.assertEqual(str(then), '2003-11-01.00:00:00')
+ now = Date('2004-02-28.00:00:00')
+ then = now + Interval('1d')
+ self.assertEqual(str(then), '2004-02-29.00:00:00')
+ now = Date('2003-02-28.00:00:00')
+ then = now + Interval('1d')
+ self.assertEqual(str(then), '2003-03-01.00:00:00')
+ now = Date('2003-01-01.00:00:00')
+ then = now + Interval('59d')
+ self.assertEqual(str(then), '2003-03-01.00:00:00')
+ now = Date('2004-01-01.00:00:00')
+ then = now + Interval('59d')
+ self.assertEqual(str(then), '2004-02-29.00:00:00')
+
+ def testIntervalSubtractMonthBoundary(self):
+ # force the transition over a month boundary
+ now = Date('2003-11-01.00:00:00')
+ then = now - Interval('2d')
+ self.assertEqual(str(then), '2003-10-30.00:00:00')
+ now = Date('2004-02-29.00:00:00')
+ then = now - Interval('1d')
+ self.assertEqual(str(then), '2004-02-28.00:00:00')
+ now = Date('2003-03-01.00:00:00')
+ then = now - Interval('1d')
+ self.assertEqual(str(then), '2003-02-28.00:00:00')
+ now = Date('2003-03-01.00:00:00')
+ then = now - Interval('59d')
+ self.assertEqual(str(then), '2003-01-01.00:00:00')
+ now = Date('2004-02-29.00:00:00')
+ then = now - Interval('59d')
+ self.assertEqual(str(then), '2004-01-01.00:00:00')
+
+ def testIntervalAddYearBoundary(self):
+ # force the transition over a year boundary
+ now = Date('2003-12-30.00:00:00')
+ then = now + Interval('2d')
+ self.assertEqual(str(then), '2004-01-01.00:00:00')
+ now = Date('2003-01-01.00:00:00')
+ then = now + Interval('365d')
+ self.assertEqual(str(then), '2004-01-01.00:00:00')
+ now = Date('2004-01-01.00:00:00')
+ then = now + Interval('366d')
+ self.assertEqual(str(then), '2005-01-01.00:00:00')
+
+ def testIntervalSubtractYearBoundary(self):
+ # force the transition over a year boundary
+ now = Date('2003-01-01.00:00:00')
+ then = now - Interval('2d')
+ self.assertEqual(str(then), '2002-12-30.00:00:00')
+ now = Date('2004-02-01.00:00:00')
+ then = now - Interval('365d')
+ self.assertEqual(str(then), '2003-02-01.00:00:00')
+ now = Date('2005-02-01.00:00:00')
+ then = now - Interval('365d')
+ self.assertEqual(str(then), '2004-02-02.00:00:00')
+
+ def testDateSubtract(self):
+ # These are thoroughly broken right now.
+ i = Date('2003-03-15.00:00:00') - Date('2003-03-10.00:00:00')
+ self.assertEqual(i, Interval('5d'))
+ i = Date('2003-02-01.00:00:00') - Date('2003-03-01.00:00:00')
+ self.assertEqual(i, Interval('-28d'))
+ i = Date('2003-03-01.00:00:00') - Date('2003-02-01.00:00:00')
+ self.assertEqual(i, Interval('28d'))
+ i = Date('2003-03-03.00:00:00') - Date('2003-02-01.00:00:00')
+ self.assertEqual(i, Interval('30d'))
+ i = Date('2003-03-03.00:00:00') - Date('2002-02-01.00:00:00')
+ self.assertEqual(i, Interval('395d'))
+ i = Date('2003-03-03.00:00:00') - Date('2003-04-01.00:00:00')
+ self.assertEqual(i, Interval('-29d'))
+ i = Date('2003-03-01.00:00:00') - Date('2003-02-01.00:00:00')
+ self.assertEqual(i, Interval('28d'))
+ # force the transition over a year boundary
+ i = Date('2003-01-01.00:00:00') - Date('2002-01-01.00:00:00')
+ self.assertEqual(i, Interval('365d'))
+
+ def testIntervalAdd(self):
+ ae = self.assertEqual
+ ae(str(Interval('1y') + Interval('1y')), '+ 2y')
+ ae(str(Interval('1y') + Interval('1m')), '+ 1y 1m')
+ ae(str(Interval('1y') + Interval('2:40')), '+ 1y 2:40')
+ ae(str(Interval('1y') + Interval('- 1y')), '00:00')
+ ae(str(Interval('- 1y') + Interval('1y')), '00:00')
+ ae(str(Interval('- 1y') + Interval('- 1y')), '- 2y')
+ ae(str(Interval('1y') + Interval('- 1m')), '+ 11m')
+ ae(str(Interval('1:00') + Interval('1:00')), '+ 2:00')
+ ae(str(Interval('0:50') + Interval('0:50')), '+ 1:40')
+ ae(str(Interval('1:50') + Interval('- 1:50')), '00:00')
+ ae(str(Interval('- 1:50') + Interval('1:50')), '00:00')
+ ae(str(Interval('- 1:50') + Interval('- 1:50')), '- 3:40')
+ ae(str(Interval('1:59:59') + Interval('00:00:01')), '+ 2:00')
+ ae(str(Interval('2:00') + Interval('- 00:00:01')), '+ 1:59:59')
+
+ def testIntervalSub(self):
+ ae = self.assertEqual
+ ae(str(Interval('1y') - Interval('- 1y')), '+ 2y')
+ ae(str(Interval('1y') - Interval('- 1m')), '+ 1y 1m')
+ ae(str(Interval('1y') - Interval('- 2:40')), '+ 1y 2:40')
+ ae(str(Interval('1y') - Interval('1y')), '00:00')
+ ae(str(Interval('1y') - Interval('1m')), '+ 11m')
+ ae(str(Interval('1:00') - Interval('- 1:00')), '+ 2:00')
+ ae(str(Interval('0:50') - Interval('- 0:50')), '+ 1:40')
+ ae(str(Interval('1:50') - Interval('1:50')), '00:00')
+ ae(str(Interval('1:59:59') - Interval('- 00:00:01')), '+ 2:00')
+ ae(str(Interval('2:00') - Interval('00:00:01')), '+ 1:59:59')
+
+ def testOverflow(self):
+ ae = self.assertEqual
+ ae(fixTimeOverflow((1,0,0,0, 0, 0, 60)), (1,0,0,0, 0, 1, 0))
+ ae(fixTimeOverflow((1,0,0,0, 0, 0, 100)), (1,0,0,0, 0, 1, 40))
+ ae(fixTimeOverflow((1,0,0,0, 0, 0, 60*60)), (1,0,0,0, 1, 0, 0))
+ ae(fixTimeOverflow((1,0,0,0, 0, 0, 24*60*60)), (1,0,0,1, 0, 0, 0))
+ ae(fixTimeOverflow((1,0,0,0, 0, 0, -1)), (-1,0,0,0, 0, 0, 1))
+ ae(fixTimeOverflow((1,0,0,0, 0, 0, -100)), (-1,0,0,0, 0, 1, 40))
+ ae(fixTimeOverflow((1,0,0,0, 0, 0, -60*60)), (-1,0,0,0, 1, 0, 0))
+ ae(fixTimeOverflow((1,0,0,0, 0, 0, -24*60*60)), (-1,0,0,1, 0, 0, 0))
+ ae(fixTimeOverflow((-1,0,0,0, 0, 0, 1)), (-1,0,0,0, 0, 0, 1))
+ ae(fixTimeOverflow((-1,0,0,0, 0, 0, 100)), (-1,0,0,0, 0, 1, 40))
+ ae(fixTimeOverflow((-1,0,0,0, 0, 0, 60*60)), (-1,0,0,0, 1, 0, 0))
+ ae(fixTimeOverflow((-1,0,0,0, 0, 0, 24*60*60)), (-1,0,0,1, 0, 0, 0))
+
+ def testDivision(self):
+ ae = self.assertEqual
+ ae(str(Interval('1y')/2), '+ 6m')
+ ae(str(Interval('1:00')/2), '+ 0:30')
+ ae(str(Interval('00:01')/2), '+ 0:00:30')
+
+ def testSorting(self):
+ ae = self.assertEqual
+ i1 = Interval('1y')
+ i2 = Interval('1d')
+ l = [i1, i2]; l.sort()
+ ae(l, [i2, i1])
+ l = [i2, i1]; l.sort()
+ ae(l, [i2, i1])
+ i1 = Interval('- 2d')
+ i2 = Interval('1d')
+ l = [i1, i2]; l.sort()
+ ae(l, [i1, i2])
+
+ i1 = Interval("1:20")
+ i2 = Interval("2d")
+ i3 = Interval("3:30")
+ l = [i1, i2, i3]; l.sort()
+ ae(l, [i1, i3, i2])
+
+ def testGranularity(self):
+ ae = self.assertEqual
+ ae(str(Date('2003-2-12', add_granularity=1)), '2003-02-12.23:59:59')
+ ae(str(Date('2003-1-1.23:00', add_granularity=1)), '2003-01-01.23:00:59')
+ ae(str(Date('2003', add_granularity=1)), '2003-12-31.23:59:59')
+ ae(str(Date('2003-5', add_granularity=1)), '2003-05-31.23:59:59')
+ ae(str(Interval('+1w', add_granularity=1)), '+ 14d')
+ ae(str(Interval('-2m 3w', add_granularity=1)), '- 2m 14d')
+
+ def testIntervalPretty(self):
+ def ae(spec, pretty):
+ self.assertEqual(Interval(spec).pretty(), pretty)
+ ae('2y', 'in 2 years')
+ ae('1y', 'in 1 year')
+ ae('2m', 'in 2 months')
+ ae('1m 30d', 'in 2 months')
+ ae('60d', 'in 2 months')
+ ae('59d', 'in 1 month')
+ ae('1m', 'in 1 month')
+ ae('29d', 'in 1 month')
+ ae('28d', 'in 4 weeks')
+ ae('8d', 'in 1 week')
+ ae('7d', 'in 7 days')
+ ae('1w', 'in 7 days')
+ ae('2d', 'in 2 days')
+ ae('1d', 'tomorrow')
+ ae('02:00:00', 'in 2 hours')
+ ae('01:59:00', 'in 1 3/4 hours')
+ ae('01:45:00', 'in 1 3/4 hours')
+ ae('01:30:00', 'in 1 1/2 hours')
+ ae('01:29:00', 'in 1 1/4 hours')
+ ae('01:00:00', 'in an hour')
+ ae('00:30:00', 'in 1/2 an hour')
+ ae('00:15:00', 'in 1/4 hour')
+ ae('00:02:00', 'in 2 minutes')
+ ae('00:01:00', 'in 1 minute')
+ ae('00:00:30', 'in a moment')
+ ae('-00:00:30', 'just now')
+ ae('-1d', 'yesterday')
+ ae('-1y', '1 year ago')
+ ae('-2y', '2 years ago')
+
+ def testPyDatetime(self):
+ try:
+ import datetime
+ except:
+ return
+ d = datetime.datetime.now()
+ Date(d)
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(DateTestCase))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/test/test_hyperdbvals.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_hyperdbvals.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,129 @@
+#
+# Copyright (c) 2003 Richard Jones, richard at commonground.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.
+#
+# This module is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+#
+# $Id: test_hyperdbvals.py,v 1.1 2003/11/11 00:35:14 richard Exp $
+
+import unittest, os, shutil, errno, sys, difflib, cgi, re, sha
+
+from roundup import init, instance, password, hyperdb, date
+
+class TestClass:
+ def getprops(self):
+ return {
+ 'string': hyperdb.String(),
+ 'number': hyperdb.Number(),
+ 'boolean': hyperdb.Boolean(),
+ 'password': hyperdb.Password(),
+ 'date': hyperdb.Date(),
+ 'interval': hyperdb.Interval(),
+ 'link': hyperdb.Link('test'),
+ 'link2': hyperdb.Link('test2'),
+ 'multilink': hyperdb.Multilink('test'),
+ 'multilink2': hyperdb.Multilink('test2'),
+ }
+ def getkey(self):
+ return 'string'
+ def lookup(self, value):
+ if value == 'valid':
+ return '1'
+ raise KeyError
+ def get(self, nodeid, propname):
+ assert propname.startswith('multilink')
+ assert nodeid is not None
+ return ['2', '3']
+
+class TestClass2:
+ def properties(self):
+ return {
+ 'string': hyperdb.String(),
+ }
+ def getkey(self):
+ return None
+ def labelprop(self, default_to_id=1):
+ return 'id'
+
+class TestDatabase:
+ classes = {'test': TestClass(), 'test2': TestClass2()}
+ def getUserTimezone(self):
+ return 0
+
+class RawToHyperdbTest(unittest.TestCase):
+ def _test(self, propname, value, itemid=None):
+ return hyperdb.rawToHyperdb(TestDatabase(), TestClass(), itemid,
+ propname, value)
+ def testString(self):
+ self.assertEqual(self._test('string', ' a string '), 'a string')
+ def testNumber(self):
+ self.assertEqual(self._test('number', ' 10 '), 10)
+ self.assertEqual(self._test('number', ' 1.5 '), 1.5)
+ def testBoolean(self):
+ for true in 'yes true on 1'.split():
+ self.assertEqual(self._test('boolean', ' %s '%true), 1)
+ for false in 'no false off 0'.split():
+ self.assertEqual(self._test('boolean', ' %s '%false), 0)
+ def testPassword(self):
+ self.assertEqual(self._test('password', ' a string '), 'a string')
+ val = self._test('password', ' a string ')
+ self.assert_(isinstance(val, password.Password))
+ val = self._test('password', '{plaintext}a string')
+ 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()
+ val = self._test('password', '{SHA}'+s)
+ self.assert_(isinstance(val, password.Password))
+ self.assertEqual(val, 'a string')
+ self.assertRaises(hyperdb.HyperdbValueError, self._test,
+ 'password', '{fubar}a string')
+ def testDate(self):
+ val = self._test('date', ' 2003-01-01 ')
+ self.assert_(isinstance(val, date.Date))
+ val = self._test('date', ' 2003/01/01 ')
+ self.assert_(isinstance(val, date.Date))
+ val = self._test('date', ' 2003/1/1 ')
+ self.assert_(isinstance(val, date.Date))
+ val = self._test('date', ' 2003-1-1 ')
+ self.assert_(isinstance(val, date.Date))
+ self.assertRaises(hyperdb.HyperdbValueError, self._test, 'date',
+ 'fubar')
+ def testInterval(self):
+ val = self._test('interval', ' +1d ')
+ self.assert_(isinstance(val, date.Interval))
+ self.assertRaises(hyperdb.HyperdbValueError, self._test, 'interval',
+ 'fubar')
+ def testLink(self):
+ self.assertEqual(self._test('link', '1'), '1')
+ self.assertEqual(self._test('link', 'valid'), '1')
+ self.assertRaises(hyperdb.HyperdbValueError, self._test, 'link',
+ 'invalid')
+ def testMultilink(self):
+ self.assertEqual(self._test('multilink', '', '1'), [])
+ self.assertEqual(self._test('multilink', '1', '1'), ['1'])
+ self.assertEqual(self._test('multilink', 'valid', '1'), ['1'])
+ self.assertRaises(hyperdb.HyperdbValueError, self._test, 'multilink',
+ 'invalid', '1')
+ self.assertEqual(self._test('multilink', '+1', '1'), ['1', '2', '3'])
+ self.assertEqual(self._test('multilink', '+valid', '1'), ['1', '2',
+ '3'])
+ self.assertEqual(self._test('multilink', '+1,-2', '1'), ['1', '3'])
+ self.assertEqual(self._test('multilink', '+valid,-3', '1'), ['1', '2'])
+ self.assertEqual(self._test('multilink', '+1', None), ['1'])
+ self.assertEqual(self._test('multilink', '+valid', None), ['1'])
+ self.assertEqual(self._test('multilink', '', None), [])
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(RawToHyperdbTest))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/test/test_indexer.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_indexer.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,93 @@
+# 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: test_indexer.py,v 1.10 2006/02/07 04:59:05 richard Exp $
+
+import os, unittest, shutil
+
+class db:
+ class config(dict):
+ DATABASE = 'test-index'
+ config = config()
+ config[('main', 'indexer_stopwords')] = []
+
+class IndexerTest(unittest.TestCase):
+ def setUp(self):
+ if os.path.exists('test-index'):
+ shutil.rmtree('test-index')
+ os.mkdir('test-index')
+ os.mkdir('test-index/files')
+ from roundup.backends.indexer_dbm import Indexer
+ self.dex = Indexer(db)
+ self.dex.load_index()
+
+ 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'),
+ ('test', '2', 'foo')])
+ self.assertEqual(self.dex.find(['blah']), [('test', '2', 'foo')])
+ self.assertEqual(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'),
+ ('test', '2', 'foo')])
+ self.dex.add_text(('test', '1', 'foo'), 'a the hello')
+ self.assertEqual(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'),
+ ('test', '2', 'foo')])
+ self.dex.add_text(('test', '1', 'foo'), '')
+ self.assertEqual(self.dex.find(['world']), [('test', '2', 'foo')])
+
+ def tearDown(self):
+ shutil.rmtree('test-index')
+
+class XapianIndexerTest(IndexerTest):
+ def setUp(self):
+ if os.path.exists('test-index'):
+ shutil.rmtree('test-index')
+ os.mkdir('test-index')
+ from roundup.backends.indexer_xapian import Indexer
+ self.dex = Indexer(db)
+ def tearDown(self):
+ shutil.rmtree('test-index')
+
+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
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/test/test_locking.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_locking.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,58 @@
+# 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: test_locking.py,v 1.4 2003/10/25 22:53:26 richard Exp $
+
+import os, unittest, tempfile
+
+from roundup.backends.locking import acquire_lock, release_lock
+
+class LockingTest(unittest.TestCase):
+ def setUp(self):
+ self.path = tempfile.mktemp()
+ open(self.path, 'w').write('hi\n')
+
+ # XXX test disabled because it simply doesn't work on many platforms
+ # (Solaris and Irix are known to fail, but Linux works)
+ def xtest_basics(self):
+ f = acquire_lock(self.path)
+ try:
+ acquire_lock(self.path, block=0)
+ except:
+ pass
+ else:
+ raise AssertionError, 'no exception'
+ release_lock(f)
+ f = acquire_lock(self.path)
+ release_lock(f)
+
+ def tearDown(self):
+ os.remove(self.path)
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(LockingTest))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/test/test_mailgw.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_mailgw.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,1286 @@
+#
+# Copyright (c) 2001 Richard Jones, richard at bofh.asn.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.
+#
+# This module is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+#
+# $Id: test_mailgw.py,v 1.77 2006/03/02 23:45:23 richard Exp $
+
+# TODO: test bcc
+
+import unittest, tempfile, os, shutil, errno, imp, sys, difflib, rfc822, time
+
+from cStringIO import StringIO
+
+if not os.environ.has_key('SENDMAILDEBUG'):
+ os.environ['SENDMAILDEBUG'] = 'mail-test.log'
+SENDMAILDEBUG = os.environ['SENDMAILDEBUG']
+
+from roundup.mailgw import MailGW, Unauthorized, uidFromAddress, \
+ parseContent, IgnoreLoop, IgnoreBulk, MailUsageError
+from roundup import init, instance, password, rfc2822, __version__
+
+import db_test_base
+
+class Message(rfc822.Message):
+ """String-based Message class with equivalence test."""
+ def __init__(self, s):
+ rfc822.Message.__init__(self, StringIO(s.strip()))
+
+ def __eq__(self, other):
+ return (self.dict == other.dict and
+ self.fp.read() == other.fp.read())
+
+class DiffHelper:
+ def compareMessages(self, new, old):
+ """Compare messages for semantic equivalence."""
+ new, old = Message(new), Message(old)
+ del new['date'], old['date']
+
+ if not new == old:
+ res = []
+
+ for key in new.keys():
+ if key.lower() == 'x-roundup-version':
+ # version changes constantly, so handle it specially
+ if new[key] != __version__:
+ res.append(' %s: %s != %s' % (key, __version__,
+ new[key]))
+ elif new[key] != old[key]:
+ res.append(' %s: %s != %s' % (key, old[key], new[key]))
+
+ body_diff = self.compareStrings(new.fp.read(), old.fp.read())
+ if body_diff:
+ res.append('')
+ res.extend(body_diff)
+
+ if res:
+ res.insert(0, 'Generated message not correct (diff follows):')
+ raise AssertionError, '\n'.join(res)
+
+ def compareStrings(self, s2, s1):
+ '''Note the reversal of s2 and s1 - difflib.SequenceMatcher wants
+ the first to be the "original" but in the calls in this file,
+ the second arg is the original. Ho hum.
+ '''
+ l1 = s1.strip().split('\n')
+ l2 = s2.strip().split('\n')
+ if l1 == l2:
+ return
+ s = difflib.SequenceMatcher(None, l1, l2)
+ res = []
+ for value, s1s, s1e, s2s, s2e in s.get_opcodes():
+ if value == 'equal':
+ for i in range(s1s, s1e):
+ res.append(' %s'%l1[i])
+ elif value == 'delete':
+ for i in range(s1s, s1e):
+ res.append('- %s'%l1[i])
+ elif value == 'insert':
+ for i in range(s2s, s2e):
+ res.append('+ %s'%l2[i])
+ elif value == 'replace':
+ for i, j in zip(range(s1s, s1e), range(s2s, s2e)):
+ res.append('- %s'%l1[i])
+ res.append('+ %s'%l2[j])
+
+ return res
+
+class MailgwTestCase(unittest.TestCase, DiffHelper):
+ count = 0
+ schema = 'classic'
+ def setUp(self):
+ MailgwTestCase.count = MailgwTestCase.count + 1
+ self.dirname = '_test_mailgw_%s'%self.count
+ # set up and open a tracker
+ self.instance = db_test_base.setupTracker(self.dirname)
+
+ # and open the database
+ self.db = self.instance.open('admin')
+ self.chef_id = self.db.user.create(username='Chef',
+ address='chef at bork.bork.bork', realname='Bork, Chef', roles='User')
+ self.richard_id = self.db.user.create(username='richard',
+ address='richard at test', roles='User')
+ self.mary_id = self.db.user.create(username='mary', address='mary at test',
+ roles='User', realname='Contrary, Mary')
+ self.john_id = self.db.user.create(username='john', address='john at test',
+ alternate_addresses='jondoe at test\njohn.doe at test', roles='User',
+ realname='John Doe')
+
+ def tearDown(self):
+ if os.path.exists(SENDMAILDEBUG):
+ os.remove(SENDMAILDEBUG)
+ self.db.close()
+ try:
+ shutil.rmtree(self.dirname)
+ except OSError, error:
+ if error.errno not in (errno.ENOENT, errno.ESRCH): raise
+
+ def _handle_mail(self, message):
+ handler = self.instance.MailGW(self.instance, self.db)
+ handler.trapExceptions = 0
+ ret = handler.main(StringIO(message))
+ # handler can close the db on us and open a new one
+ self.db = handler.db
+ return ret
+
+ def _get_mail(self):
+ f = open(SENDMAILDEBUG)
+ try:
+ return f.read()
+ finally:
+ f.close()
+
+ def testEmptyMessage(self):
+ nodeid = self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+Subject: [issue] Testing...
+
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+ self.assertEqual(self.db.issue.get(nodeid, 'title'), 'Testing...')
+
+ def doNewIssue(self):
+ nodeid = self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Cc: richard at test
+Message-Id: <dummy_test_message_id>
+Subject: [issue] Testing...
+
+This is a test submission of a new issue.
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+ l = self.db.issue.get(nodeid, 'nosy')
+ l.sort()
+ self.assertEqual(l, [self.chef_id, self.richard_id])
+ return nodeid
+
+ def testNewIssue(self):
+ self.doNewIssue()
+
+ def testNewIssueNosy(self):
+ self.instance.config.ADD_AUTHOR_TO_NOSY = 'yes'
+ nodeid = self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Cc: richard at test
+Message-Id: <dummy_test_message_id>
+Subject: [issue] Testing...
+
+This is a test submission of a new issue.
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+ l = self.db.issue.get(nodeid, 'nosy')
+ l.sort()
+ self.assertEqual(l, [self.chef_id, self.richard_id])
+
+ def testAlternateAddress(self):
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: John Doe <john.doe at test>
+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.
+''')
+ userlist = self.db.user.list()
+ assert not os.path.exists(SENDMAILDEBUG)
+ self.assertEqual(userlist, self.db.user.list(),
+ "user created when it shouldn't have been")
+
+ def testNewIssueNoClass(self):
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Cc: richard at test
+Message-Id: <dummy_test_message_id>
+Subject: Testing...
+
+This is a test submission of a new issue.
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+
+ def testNewIssueAuthMsg(self):
+ # TODO: fix the damn config - this is apalling
+ self.db.config.MESSAGES_TO_AUTHOR = 'yes'
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <dummy_test_message_id>
+Subject: [issue] Testing... [nosy=mary; assignedto=richard]
+
+This is a test submission of a new issue.
+''')
+ self.compareMessages(self._get_mail(),
+'''FROM: roundup-admin at your.tracker.email.domain.example
+TO: chef at bork.bork.bork, mary at test, richard at test
+Content-Type: text/plain; charset=utf-8
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork, mary at test, richard at test
+From: "Bork, Chef" <issue_tracker at your.tracker.email.domain.example>
+Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+MIME-Version: 1.0
+Message-Id: <dummy_test_message_id>
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+Content-Transfer-Encoding: quoted-printable
+
+
+New submission from Bork, Chef <chef at bork.bork.bork>:
+
+This is a test submission of a new issue.
+
+----------
+assignedto: richard
+messages: 1
+nosy: Chef, mary, richard
+status: unread
+title: Testing...
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+''')
+
+ # BUG
+ # def testMultipart(self):
+ # '''With more than one part'''
+ # see MultipartEnc tests: but if there is more than one part
+ # we return a multipart/mixed and the boundary contains
+ # the ip address of the test machine.
+
+ # BUG should test some binary attamchent too.
+
+ def testSimpleFollowup(self):
+ self.doNewIssue()
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: mary <mary at 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...
+
+This is a second followup
+''')
+ self.compareMessages(self._get_mail(),
+'''FROM: roundup-admin at your.tracker.email.domain.example
+TO: chef at bork.bork.bork, richard at test
+Content-Type: text/plain; charset=utf-8
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork, richard at 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
+Content-Transfer-Encoding: quoted-printable
+
+
+Contrary, Mary <mary at test> added the comment:
+
+This is a second followup
+
+----------
+status: unread -> chatting
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+''')
+
+ def testFollowup(self):
+ self.doNewIssue()
+
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard <richard at 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... [assignedto=mary; nosy=+john]
+
+This is a followup
+''')
+ l = self.db.issue.get('1', 'nosy')
+ l.sort()
+ self.assertEqual(l, [self.chef_id, self.richard_id, self.mary_id,
+ self.john_id])
+
+ self.compareMessages(self._get_mail(),
+'''FROM: roundup-admin at your.tracker.email.domain.example
+TO: chef at bork.bork.bork, john at test, mary at test
+Content-Type: text/plain; charset=utf-8
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork, john at test, mary at test
+From: richard <issue_tracker at your.tracker.email.domain.example>
+Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+MIME-Version: 1.0
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+Content-Transfer-Encoding: quoted-printable
+
+
+richard <richard at test> added the comment:
+
+This is a followup
+
+----------
+assignedto: -> mary
+nosy: +john, mary
+status: unread -> chatting
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+''')
+
+
+ #
+ # FOLLOWUP TITLE MATCH
+ #
+ def testFollowupTitleMatch(self):
+ self.doNewIssue()
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard <richard at test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: Re: Testing... [assignedto=mary; nosy=+john]
+
+This is a followup
+''')
+ self.compareMessages(self._get_mail(),
+'''FROM: roundup-admin at your.tracker.email.domain.example
+TO: chef at bork.bork.bork, john at test, mary at test
+Content-Type: text/plain; charset=utf-8
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork, john at test, mary at test
+From: richard <issue_tracker at your.tracker.email.domain.example>
+Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+MIME-Version: 1.0
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+Content-Transfer-Encoding: quoted-printable
+
+
+richard <richard at test> added the comment:
+
+This is a followup
+
+----------
+assignedto: -> mary
+nosy: +john, mary
+status: unread -> chatting
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+''')
+
+ def testFollowupTitleMatchNever(self):
+ nodeid = self.doNewIssue()
+ self.db.config.MAILGW_SUBJECT_CONTENT_MATCH = 'never'
+ self.assertNotEqual(self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard <richard at test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: Re: Testing...
+
+This is a followup
+'''), nodeid)
+
+ def testFollowupTitleMatchNever(self):
+ nodeid = self.doNewIssue()
+ # force failure of the interval
+ time.sleep(2)
+ self.db.config.MAILGW_SUBJECT_CONTENT_MATCH = 'creation 00:00:01'
+ self.assertNotEqual(self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard <richard at test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: Re: Testing...
+
+This is a followup
+'''), nodeid)
+ # now try a longer interval
+ self.db.config.MAILGW_SUBJECT_CONTENT_MATCH = 'creation +1d'
+ self.assertEqual(self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard <richard at test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: Re: Testing...
+
+This is a followup
+'''), nodeid)
+
+
+ def testFollowupNosyAuthor(self):
+ self.doNewIssue()
+ self.db.config.ADD_AUTHOR_TO_NOSY = 'yes'
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: john at 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...
+
+This is a followup
+''')
+
+ self.compareMessages(self._get_mail(),
+'''FROM: roundup-admin at your.tracker.email.domain.example
+TO: chef at bork.bork.bork, richard at test
+Content-Type: text/plain; charset=utf-8
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork, richard at test
+From: John Doe <issue_tracker at your.tracker.email.domain.example>
+Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+MIME-Version: 1.0
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+Content-Transfer-Encoding: quoted-printable
+
+
+John Doe <john at test> added the comment:
+
+This is a followup
+
+----------
+nosy: +john
+status: unread -> chatting
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+
+''')
+
+ def testFollowupNosyRecipients(self):
+ self.doNewIssue()
+ self.db.config.ADD_RECIPIENTS_TO_NOSY = 'yes'
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard at test
+To: issue_tracker at your.tracker.email.domain.example
+Cc: john at test
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: [issue1] Testing...
+
+This is a followup
+''')
+ 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
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork
+From: richard <issue_tracker at your.tracker.email.domain.example>
+Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+MIME-Version: 1.0
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+Content-Transfer-Encoding: quoted-printable
+
+
+richard <richard at test> added the comment:
+
+This is a followup
+
+----------
+nosy: +john
+status: unread -> chatting
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+
+''')
+
+ def testFollowupNosyAuthorAndCopy(self):
+ self.doNewIssue()
+ self.db.config.ADD_AUTHOR_TO_NOSY = 'yes'
+ self.db.config.MESSAGES_TO_AUTHOR = 'yes'
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: john at 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...
+
+This is a followup
+''')
+ self.compareMessages(self._get_mail(),
+'''FROM: roundup-admin at your.tracker.email.domain.example
+TO: chef at bork.bork.bork, john at test, richard at test
+Content-Type: text/plain; charset=utf-8
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork, john at test, richard at test
+From: John Doe <issue_tracker at your.tracker.email.domain.example>
+Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+MIME-Version: 1.0
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+Content-Transfer-Encoding: quoted-printable
+
+
+John Doe <john at test> added the comment:
+
+This is a followup
+
+----------
+nosy: +john
+status: unread -> chatting
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+
+''')
+
+ def testFollowupNoNosyAuthor(self):
+ self.doNewIssue()
+ self.instance.config.ADD_AUTHOR_TO_NOSY = 'no'
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: john at 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...
+
+This is a followup
+''')
+ self.compareMessages(self._get_mail(),
+'''FROM: roundup-admin at your.tracker.email.domain.example
+TO: chef at bork.bork.bork, richard at test
+Content-Type: text/plain; charset=utf-8
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork, richard at test
+From: John Doe <issue_tracker at your.tracker.email.domain.example>
+Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+MIME-Version: 1.0
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+Content-Transfer-Encoding: quoted-printable
+
+
+John Doe <john at test> added the comment:
+
+This is a followup
+
+----------
+status: unread -> chatting
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+
+''')
+
+ def testFollowupNoNosyRecipients(self):
+ self.doNewIssue()
+ self.instance.config.ADD_RECIPIENTS_TO_NOSY = 'no'
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard at test
+To: issue_tracker at your.tracker.email.domain.example
+Cc: john at test
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: [issue1] Testing...
+
+This is a followup
+''')
+ 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
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork
+From: richard <issue_tracker at your.tracker.email.domain.example>
+Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+MIME-Version: 1.0
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+Content-Transfer-Encoding: quoted-printable
+
+
+richard <richard at test> added the comment:
+
+This is a followup
+
+----------
+status: unread -> chatting
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+
+''')
+
+ def testFollowupEmptyMessage(self):
+ self.doNewIssue()
+
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard <richard at 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... [assignedto=mary; nosy=+john]
+
+''')
+ l = self.db.issue.get('1', 'nosy')
+ l.sort()
+ self.assertEqual(l, [self.chef_id, self.richard_id, self.mary_id,
+ self.john_id])
+
+ # should be no file created (ie. no message)
+ assert not os.path.exists(SENDMAILDEBUG)
+
+ def testFollowupEmptyMessageNoSubject(self):
+ self.doNewIssue()
+
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard <richard at test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: [issue1] [assignedto=mary; nosy=+john]
+
+''')
+ l = self.db.issue.get('1', 'nosy')
+ l.sort()
+ self.assertEqual(l, [self.chef_id, self.richard_id, self.mary_id,
+ self.john_id])
+
+ # should be no file created (ie. no message)
+ assert not os.path.exists(SENDMAILDEBUG)
+
+ def testNosyRemove(self):
+ self.doNewIssue()
+
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard <richard at 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... [nosy=-richard]
+
+''')
+ l = self.db.issue.get('1', 'nosy')
+ l.sort()
+ self.assertEqual(l, [self.chef_id])
+
+ # NO NOSY MESSAGE SHOULD BE SENT!
+ assert not os.path.exists(SENDMAILDEBUG)
+
+ def testNewUserAuthor(self):
+ # first without the permission
+ # heh... just ignore the API for a second ;)
+ self.db.security.role['anonymous'].permissions=[]
+ anonid = self.db.user.lookup('anonymous')
+ self.db.user.set(anonid, roles='Anonymous')
+
+ l = self.db.user.list()
+ l.sort()
+ message = '''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: fubar <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.
+'''
+ self.assertRaises(Unauthorized, self._handle_mail, message)
+ m = self.db.user.list()
+ m.sort()
+ self.assertEqual(l, m)
+
+ # now with the permission
+ p = [
+ self.db.security.getPermission('Create', 'user'),
+ self.db.security.getPermission('Email Access', None),
+ ]
+ self.db.security.role['anonymous'].permissions=p
+ self._handle_mail(message)
+ m = self.db.user.list()
+ m.sort()
+ self.assertNotEqual(l, m)
+
+ def testEnc01(self):
+ self.doNewIssue()
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: mary <mary at 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
+Content-Type: text/plain; charset=utf-8
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork, richard at 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
+Content-Transfer-Encoding: quoted-printable
+
+
+Contrary, Mary <mary at test> added the comment:
+
+A message with encoding (encoded oe =C3=B6)
+
+----------
+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()
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: mary <mary at test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: [issue1] Testing...
+Content-Type: multipart/mixed;
+ boundary="----_=_NextPart_000_01"
+
+This message is in MIME format. Since your mail reader does not understand
+this format, some or all of this message may not be legible.
+
+------_=_NextPart_000_01
+Content-Type: text/plain;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+A message with first part encoded (encoded oe =F6)
+
+''')
+ self.compareMessages(self._get_mail(),
+'''FROM: roundup-admin at your.tracker.email.domain.example
+TO: chef at bork.bork.bork, richard at test
+Content-Type: text/plain; charset=utf-8
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork, richard at 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
+Content-Transfer-Encoding: quoted-printable
+
+
+Contrary, Mary <mary at test> added the comment:
+
+A message with first part encoded (encoded oe =C3=B6)
+
+----------
+status: unread -> chatting
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+''')
+
+ def testContentDisposition(self):
+ self.doNewIssue()
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: mary <mary at test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: [issue1] Testing...
+Content-Type: multipart/mixed; boundary="bCsyhTFzCvuiizWE"
+Content-Disposition: inline
+
+
+--bCsyhTFzCvuiizWE
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+
+test attachment binary
+
+--bCsyhTFzCvuiizWE
+Content-Type: application/octet-stream
+Content-Disposition: attachment; filename="main.dvi"
+
+xxxxxx
+
+--bCsyhTFzCvuiizWE--
+''')
+ messages = self.db.issue.get('1', 'messages')
+ messages.sort()
+ file = self.db.msg.get(messages[-1], 'files')[0]
+ self.assertEqual(self.db.file.get(file, 'name'), 'main.dvi')
+
+ def testFollowupStupidQuoting(self):
+ self.doNewIssue()
+
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard <richard at test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: Re: "[issue1] Testing... "
+
+This is a followup
+''')
+ 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
+Subject: [issue1] Testing...
+To: chef at bork.bork.bork
+From: richard <issue_tracker at your.tracker.email.domain.example>
+Reply-To: Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+MIME-Version: 1.0
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+X-Roundup-Name: Roundup issue tracker
+X-Roundup-Loop: hello
+Content-Transfer-Encoding: quoted-printable
+
+
+richard <richard at test> added the comment:
+
+This is a followup
+
+----------
+status: unread -> chatting
+
+_______________________________________________________________________
+Roundup issue tracker <issue_tracker at your.tracker.email.domain.example>
+<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
+_______________________________________________________________________
+''')
+
+ def testEmailQuoting(self):
+ self.instance.config.EMAIL_KEEP_QUOTED_TEXT = 'no'
+ self.innerTestQuoting('''This is a followup
+''')
+
+ def testEmailQuotingRemove(self):
+ self.instance.config.EMAIL_KEEP_QUOTED_TEXT = 'yes'
+ self.innerTestQuoting('''Blah blah wrote:
+> Blah bklaskdfj sdf asdf jlaskdf skj sdkfjl asdf
+> skdjlkjsdfalsdkfjasdlfkj dlfksdfalksd fj
+>
+
+This is a followup
+''')
+
+ def innerTestQuoting(self, expect):
+ nodeid = self.doNewIssue()
+
+ messages = self.db.issue.get(nodeid, 'messages')
+
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard <richard at test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: Re: [issue1] Testing...
+
+Blah blah wrote:
+> Blah bklaskdfj sdf asdf jlaskdf skj sdkfjl asdf
+> skdjlkjsdfalsdkfjasdlfkj dlfksdfalksd fj
+>
+
+This is a followup
+''')
+ # figure the new message id
+ newmessages = self.db.issue.get(nodeid, 'messages')
+ for msg in messages:
+ newmessages.remove(msg)
+ messageid = newmessages[0]
+
+ self.compareMessages(self.db.msg.get(messageid, 'content'), expect)
+
+ def testUserLookup(self):
+ i = self.db.user.create(username='user1', address='user1 at foo.com')
+ self.assertEqual(uidFromAddress(self.db, ('', 'user1 at foo.com'), 0), i)
+ self.assertEqual(uidFromAddress(self.db, ('', 'USER1 at foo.com'), 0), i)
+ i = self.db.user.create(username='user2', address='USER2 at foo.com')
+ self.assertEqual(uidFromAddress(self.db, ('', 'USER2 at foo.com'), 0), i)
+ self.assertEqual(uidFromAddress(self.db, ('', 'user2 at foo.com'), 0), i)
+
+ def testUserAlternateLookup(self):
+ i = self.db.user.create(username='user1', address='user1 at foo.com',
+ alternate_addresses='user1 at bar.com')
+ self.assertEqual(uidFromAddress(self.db, ('', 'user1 at bar.com'), 0), i)
+ self.assertEqual(uidFromAddress(self.db, ('', 'USER1 at bar.com'), 0), i)
+
+ def testUserCreate(self):
+ i = uidFromAddress(self.db, ('', 'user at foo.com'), 1)
+ self.assertNotEqual(uidFromAddress(self.db, ('', 'user at bar.com'), 1), i)
+
+ def testRFC2822(self):
+ ascii_header = "[issue243] This is a \"test\" - with 'quotation' marks"
+ unicode_header = '[issue244] \xd0\xb0\xd0\xbd\xd0\xb4\xd1\x80\xd0\xb5\xd0\xb9'
+ unicode_encoded = '=?utf-8?q?[issue244]_=D0=B0=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?='
+ self.assertEqual(rfc2822.encode_header(ascii_header), ascii_header)
+ self.assertEqual(rfc2822.encode_header(unicode_header), unicode_encoded)
+
+ def testRegistrationConfirmation(self):
+ otk = "Aj4euk4LZSAdwePohj90SME5SpopLETL"
+ self.db.getOTKManager().set(otk, username='johannes')
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Cc: richard at test
+Message-Id: <dummy_test_message_id>
+Subject: Re: Complete your registration to Roundup issue tracker
+ -- key %s
+
+This is a test confirmation of registration.
+''' % otk)
+ self.db.user.lookup('johannes')
+
+ def testFollowupOnNonIssue(self):
+ self.db.keyword.create(name='Foo')
+ self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: richard <richard at test>
+To: issue_tracker at your.tracker.email.domain.example
+Message-Id: <followup_dummy_id>
+In-Reply-To: <dummy_test_message_id>
+Subject: [keyword1] Testing... [name=Bar]
+
+''')
+ self.assertEqual(self.db.keyword.get('1', 'name'), 'Bar')
+
+ def testResentFrom(self):
+ nodeid = self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+Resent-From: mary <mary at test>
+To: issue_tracker at your.tracker.email.domain.example
+Cc: richard at test
+Message-Id: <dummy_test_message_id>
+Subject: [issue] Testing...
+
+This is a test submission of a new issue.
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+ l = self.db.issue.get(nodeid, 'nosy')
+ l.sort()
+ self.assertEqual(l, [self.richard_id, self.mary_id])
+ return nodeid
+
+ def testDejaVu(self):
+ self.assertRaises(IgnoreLoop, self._handle_mail,
+ '''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+X-Roundup-Loop: hello
+To: issue_tracker at your.tracker.email.domain.example
+Cc: richard at test
+Message-Id: <dummy_test_message_id>
+Subject: Re: [issue] Testing...
+
+Hi, I've been mis-configured to loop messages back to myself.
+''')
+
+ def testItsBulkStupid(self):
+ self.assertRaises(IgnoreBulk, self._handle_mail,
+ '''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+Precedence: bulk
+To: issue_tracker at your.tracker.email.domain.example
+Cc: richard at test
+Message-Id: <dummy_test_message_id>
+Subject: Re: [issue] Testing...
+
+Hi, I'm on holidays, and this is a dumb auto-responder.
+''')
+
+ def testAutoReplyEmailsAreIgnored(self):
+ self.assertRaises(IgnoreBulk, self._handle_mail,
+ '''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Cc: richard at test
+Message-Id: <dummy_test_message_id>
+Subject: Re: [issue] Out of office AutoReply: Back next week
+
+Hi, I'm back in the office next week
+''')
+
+ def testNoSubject(self):
+ self.assertRaises(MailUsageError, self._handle_mail,
+ '''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+
+ #
+ # TEST FOR INVALID DESIGNATOR HANDLING
+ #
+ def testInvalidDesignator(self):
+ self.assertRaises(MailUsageError, self._handle_mail,
+ '''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: [frobulated] testing
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+ self.assertRaises(MailUsageError, self._handle_mail,
+ '''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: [issue12345] testing
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+
+ def testInvalidClassLoose(self):
+ self.instance.config.MAILGW_SUBJECT_PREFIX_PARSING = 'loose'
+ nodeid = self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: [frobulated] testing
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+ self.assertEqual(self.db.issue.get(nodeid, 'title'),
+ '[frobulated] testing')
+
+ def testInvalidClassLoose(self):
+ self.instance.config.MAILGW_SUBJECT_PREFIX_PARSING = 'loose'
+ nodeid = self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: [issue1234] testing
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+ self.assertEqual(self.db.issue.get(nodeid, 'title'),
+ '[issue1234] testing')
+
+ def testClassLooseOK(self):
+ self.instance.config.MAILGW_SUBJECT_PREFIX_PARSING = 'loose'
+ self.db.keyword.create(name='Foo')
+ nodeid = self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: [keyword1] Testing... [name=Bar]
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+ self.assertEqual(self.db.keyword.get('1', 'name'), 'Bar')
+
+ #
+ # TEST FOR INVALID COMMANDS HANDLING
+ #
+ def testInvalidCommands(self):
+ self.assertRaises(MailUsageError, self._handle_mail,
+ '''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: testing [frobulated]
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+
+ def testInvalidCommandPassthrough(self):
+ self.instance.config.MAILGW_SUBJECT_SUFFIX_PARSING = 'none'
+ nodeid = self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: testing [frobulated]
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+ self.assertEqual(self.db.issue.get(nodeid, 'title'),
+ 'testing [frobulated]')
+
+ def testInvalidCommandPassthroughLoose(self):
+ self.instance.config.MAILGW_SUBJECT_SUFFIX_PARSING = 'loose'
+ nodeid = self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: testing [frobulated]
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+ self.assertEqual(self.db.issue.get(nodeid, 'title'),
+ 'testing [frobulated]')
+
+ def testInvalidCommandPassthroughLooseOK(self):
+ self.instance.config.MAILGW_SUBJECT_SUFFIX_PARSING = 'loose'
+ nodeid = self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: testing [assignedto=mary]
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+ self.assertEqual(self.db.issue.get(nodeid, 'title'), 'testing')
+ self.assertEqual(self.db.issue.get(nodeid, 'assignedto'), self.mary_id)
+
+ def testCommandDelimiters(self):
+ self.instance.config.MAILGW_SUBJECT_SUFFIX_DELIMITERS = '{}'
+ nodeid = self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: testing {assignedto=mary}
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+ self.assertEqual(self.db.issue.get(nodeid, 'title'), 'testing')
+ self.assertEqual(self.db.issue.get(nodeid, 'assignedto'), self.mary_id)
+
+ def testCommandDelimitersIgnore(self):
+ self.instance.config.MAILGW_SUBJECT_SUFFIX_DELIMITERS = '{}'
+ nodeid = self._handle_mail('''Content-Type: text/plain;
+ charset="iso-8859-1"
+From: Chef <chef at bork.bork.bork>
+To: issue_tracker at your.tracker.email.domain.example
+Subject: testing [assignedto=mary]
+Cc: richard at test
+Reply-To: chef at bork.bork.bork
+Message-Id: <dummy_test_message_id>
+
+''')
+ assert not os.path.exists(SENDMAILDEBUG)
+ self.assertEqual(self.db.issue.get(nodeid, 'title'),
+ 'testing [assignedto=mary]')
+ self.assertEqual(self.db.issue.get(nodeid, 'assignedto'), None)
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(MailgwTestCase))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/test/test_mailsplit.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_mailsplit.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,239 @@
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: test_mailsplit.py,v 1.15 2003/10/25 22:53:26 richard Exp $
+
+import unittest, cStringIO
+
+from roundup.mailgw import parseContent
+
+class MailsplitTestCase(unittest.TestCase):
+ def testPreComment(self):
+ s = '''
+blah blah blah blah... blah blah? blah blah blah blah blah. blah blah blah
+blah blah blah blah blah blah blah blah blah blah blah!
+
+issue_tracker at foo.com wrote:
+> blah blah blah blahblah blahblah blahblah blah blah blah blah blah blah
+> blah blah blah blah blah blah blah blah blah? blah blah blah blah blah
+> blah blah blah blah blah blah blah... blah blah blah blah. blah blah
+> blah blah blah blah? blah blah blah blah blah blah! blah blah!
+>
+> -------
+> nosy: userfoo, userken
+> _________________________________________________
+> Roundup issue tracker
+> issue_tracker at foo.com
+> http://foo.com/cgi-bin/roundup.cgi/issue_tracker/
+
+--
+blah blah blah signature
+userfoo at foo.com
+'''
+ summary, content = parseContent(s, 0, 0)
+ self.assertEqual(summary, 'blah blah blah blah... blah blah? blah blah blah blah blah. blah blah blah')
+ self.assertEqual(content, 'blah blah blah blah... blah blah? blah blah blah blah blah. blah blah blah\nblah blah blah blah blah blah blah blah blah blah blah!')
+
+
+ def testPostComment(self):
+ s = '''
+issue_tracker at foo.com wrote:
+> blah blah blah blahblah blahblah blahblah blah blah blah blah blah
+> blah
+> blah blah blah blah blah blah blah blah blah? blah blah blah blah
+> blah
+> blah blah blah blah blah blah blah... blah blah blah blah. blah
+> blah
+> blah blah blah blah? blah blah blah blah blah blah! blah blah!
+>
+> -------
+> nosy: userfoo, userken
+> _________________________________________________
+> Roundup issue tracker
+> issue_tracker at foo.com
+> http://foo.com/cgi-bin/roundup.cgi/issue_tracker/
+
+blah blah blah blah... blah blah? blah blah blah blah blah. blah blah blah
+blah blah blah blah blah blah blah blah blah blah blah!
+
+--
+blah blah blah signature
+userfoo at foo.com
+'''
+ summary, content = parseContent(s, 0, 0)
+ self.assertEqual(summary, 'blah blah blah blah... blah blah? blah blah blah blah blah. blah blah blah')
+ self.assertEqual(content, 'blah blah blah blah... blah blah? blah blah blah blah blah. blah blah blah\nblah blah blah blah blah blah blah blah blah blah blah!')
+
+
+ def testKeepCitation(self):
+ s = '''
+blah blah blah blah... blah blah? blah blah blah blah blah. blah blah blah
+blah blah blah blah blah blah blah blah blah blah blah!
+
+issue_tracker at foo.com wrote:
+> blah blah blah blahblah blahblah blahblah blah blah blah blah blah blah
+> blah blah blah blah blah blah blah blah blah? blah blah blah blah blah
+> blah blah blah blah blah blah blah... blah blah blah blah. blah blah
+> blah blah blah blah? blah blah blah blah blah blah! blah blah!
+>
+> -------
+> nosy: userfoo, userken
+> _________________________________________________
+> Roundup issue tracker
+> issue_tracker at foo.com
+> http://foo.com/cgi-bin/roundup.cgi/issue_tracker/
+
+--
+blah blah blah signature
+userfoo at foo.com
+'''
+ summary, content = parseContent(s, 1, 0)
+ self.assertEqual(summary, 'blah blah blah blah... blah blah? blah blah blah blah blah. blah blah blah')
+ self.assertEqual(content, '''\
+blah blah blah blah... blah blah? blah blah blah blah blah. blah blah blah
+blah blah blah blah blah blah blah blah blah blah blah!
+
+issue_tracker at foo.com wrote:
+> blah blah blah blahblah blahblah blahblah blah blah blah blah blah blah
+> blah blah blah blah blah blah blah blah blah? blah blah blah blah blah
+> blah blah blah blah blah blah blah... blah blah blah blah. blah blah
+> blah blah blah blah? blah blah blah blah blah blah! blah blah!
+>
+> -------
+> nosy: userfoo, userken
+> _________________________________________________
+> Roundup issue tracker
+> issue_tracker at foo.com
+> http://foo.com/cgi-bin/roundup.cgi/issue_tracker/''')
+
+
+ def testKeepBody(self):
+ s = '''
+blah blah blah blah... blah blah? blah blah blah blah blah. blah blah blah
+blah blah blah blah blah blah blah blah blah blah blah!
+
+issue_tracker at foo.com wrote:
+> blah blah blah blahblah blahblah blahblah blah blah blah blah blah blah
+> blah blah blah blah blah blah blah blah blah? blah blah blah blah blah
+> blah blah blah blah blah blah blah... blah blah blah blah. blah blah
+> blah blah blah blah? blah blah blah blah blah blah! blah blah!
+>
+> -------
+> nosy: userfoo, userken
+> _________________________________________________
+> Roundup issue tracker
+> issue_tracker at foo.com
+> http://foo.com/cgi-bin/roundup.cgi/issue_tracker/
+
+--
+blah blah blah signature
+userfoo at foo.com
+'''
+ summary, content = parseContent(s, 0, 1)
+ self.assertEqual(summary, 'blah blah blah blah... blah blah? blah blah blah blah blah. blah blah blah')
+ self.assertEqual(content, '''
+blah blah blah blah... blah blah? blah blah blah blah blah. blah blah blah
+blah blah blah blah blah blah blah blah blah blah blah!
+
+issue_tracker at foo.com wrote:
+> blah blah blah blahblah blahblah blahblah blah blah blah blah blah blah
+> blah blah blah blah blah blah blah blah blah? blah blah blah blah blah
+> blah blah blah blah blah blah blah... blah blah blah blah. blah blah
+> blah blah blah blah? blah blah blah blah blah blah! blah blah!
+>
+> -------
+> nosy: userfoo, userken
+> _________________________________________________
+> Roundup issue tracker
+> issue_tracker at foo.com
+> http://foo.com/cgi-bin/roundup.cgi/issue_tracker/
+
+--
+blah blah blah signature
+userfoo at foo.com
+''')
+
+ def testAllQuoted(self):
+ s = '\nissue_tracker at foo.com wrote:\n> testing\n'
+ summary, content = parseContent(s, 0, 1)
+ self.assertEqual(summary, '')
+ self.assertEqual(content, s)
+
+ def testSimple(self):
+ s = '''testing'''
+ summary, content = parseContent(s, 0, 0)
+ self.assertEqual(summary, 'testing')
+ self.assertEqual(content, 'testing')
+
+ def testParagraphs(self):
+ s = '''testing\n\ntesting\n\ntesting'''
+ summary, content = parseContent(s, 0, 0)
+ self.assertEqual(summary, 'testing')
+ self.assertEqual(content, 'testing\n\ntesting\n\ntesting')
+
+ def testSimpleFollowup(self):
+ s = '''>hello\ntesting'''
+ summary, content = parseContent(s, 0, 0)
+ self.assertEqual(summary, 'testing')
+ self.assertEqual(content, 'testing')
+
+ def testSimpleFollowupParas(self):
+ s = '''>hello\ntesting\n\ntesting\n\ntesting'''
+ summary, content = parseContent(s, 0, 0)
+ self.assertEqual(summary, 'testing')
+ self.assertEqual(content, 'testing\n\ntesting\n\ntesting')
+
+ def testEmpty(self):
+ s = ''
+ summary, content = parseContent(s, 0, 0)
+ self.assertEqual(summary, '')
+ self.assertEqual(content, '')
+
+ def testIndentationSummary(self):
+ s = ' Four space indent.\n\n Four space indent.\nNo indent.'
+ summary, content = parseContent(s, 0, 0)
+ self.assertEqual(summary, ' Four space indent.')
+
+ def testIndentationContent(self):
+ s = ' Four space indent.\n\n Four space indent.\nNo indent.'
+ summary, content = parseContent(s, 0, 0)
+ self.assertEqual(content, s)
+
+ def testMultilineSummary(self):
+ s = 'This is a long sentence that would normally\nbe split. More words.'
+ summary, content = parseContent(s, 0, 0)
+ self.assertEqual(summary, 'This is a long sentence that would '
+ 'normally\nbe split.')
+
+ def testKeepMultipleHyphens(self):
+ body = '''Testing, testing.
+
+----
+Testing, testing.'''
+ summary, content = parseContent(body, 1, 0)
+ self.assertEqual(body, content)
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(MailsplitTestCase))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/test/test_metakit.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_metakit.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,83 @@
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: test_metakit.py,v 1.7 2004/11/18 16:33:43 a1s Exp $
+import unittest, os, shutil, time, weakref
+
+from db_test_base import DBTest, ROTest, SchemaTest, ClassicInitTest, config, password
+
+from roundup.backends import get_backend, have_backend
+
+class metakitOpener:
+ if have_backend('metakit'):
+ module = get_backend('metakit')
+ module._instances = weakref.WeakValueDictionary()
+
+ def nuke_database(self):
+ shutil.rmtree(config.DATABASE)
+
+class metakitDBTest(metakitOpener, DBTest):
+ def testBooleanUnset(self):
+ # XXX: metakit can't unset Booleans :(
+ nid = self.db.user.create(username='foo', assignable=1)
+ self.db.user.set(nid, assignable=None)
+ self.assertEqual(self.db.user.get(nid, "assignable"), 0)
+
+ def testNumberUnset(self):
+ # XXX: metakit can't unset Numbers :(
+ nid = self.db.user.create(username='foo', age=1)
+ self.db.user.set(nid, age=None)
+ self.assertEqual(self.db.user.get(nid, "age"), 0)
+
+ def testPasswordUnset(self):
+ # XXX: metakit can't unset Numbers (id's) :(
+ x = password.Password('x')
+ nid = self.db.user.create(username='foo', password=x)
+ self.db.user.set(nid, assignable=None)
+ self.assertEqual(self.db.user.get(nid, "assignable"), 0)
+
+class metakitROTest(metakitOpener, ROTest):
+ pass
+
+class metakitSchemaTest(metakitOpener, SchemaTest):
+ pass
+
+class metakitClassicInitTest(ClassicInitTest):
+ backend = 'metakit'
+
+from session_common import DBMTest
+class metakitSessionTest(metakitOpener, DBMTest):
+ pass
+
+def test_suite():
+ suite = unittest.TestSuite()
+ if not have_backend('metakit'):
+ print 'Skipping metakit tests'
+ return suite
+ print 'Including metakit tests'
+ suite.addTest(unittest.makeSuite(metakitDBTest))
+ suite.addTest(unittest.makeSuite(metakitROTest))
+ suite.addTest(unittest.makeSuite(metakitSchemaTest))
+ suite.addTest(unittest.makeSuite(metakitClassicInitTest))
+ suite.addTest(unittest.makeSuite(metakitSessionTest))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/test/test_multipart.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_multipart.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,230 @@
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: test_multipart.py,v 1.7 2004/01/17 13:49:06 jlgijsbers Exp $
+
+import unittest
+from cStringIO import StringIO
+
+from roundup.mailgw import Message
+
+class TestMessage(Message):
+ table = {'multipart/signed': ' boundary="boundary-%(indent)s";\n',
+ 'multipart/mixed': ' boundary="boundary-%(indent)s";\n',
+ 'multipart/alternative': ' boundary="boundary-%(indent)s";\n',
+ 'text/plain': ' name="foo.txt"\nfoo\n',
+ 'application/pgp-signature': ' name="foo.gpg"\nfoo\n',
+ 'application/pdf': ' name="foo.pdf"\nfoo\n',
+ 'message/rfc822': 'Subject: foo\n\nfoo\n'}
+
+ def __init__(self, spec):
+ """Create a basic MIME message according to 'spec'.
+
+ Each line of a spec has one content-type, which is optionally indented.
+ The indentation signifies how deep in the MIME hierarchy the
+ content-type is.
+
+ """
+ parts = []
+ for line in spec.splitlines():
+ content_type = line.strip()
+ if not content_type:
+ continue
+
+ indent = self.getIndent(line)
+ if indent:
+ parts.append('--boundary-%s\n' % indent)
+ parts.append('Content-type: %s;\n' % content_type)
+ parts.append(self.table[content_type] % {'indent': indent + 1})
+
+ Message.__init__(self, StringIO(''.join(parts)))
+
+ def getIndent(self, line):
+ """Get the current line's indentation, using four-space indents."""
+ count = 0
+ for char in line:
+ if char != ' ':
+ break
+ count += 1
+ return count / 4
+
+class MultipartTestCase(unittest.TestCase):
+ def setUp(self):
+ self.fp = StringIO()
+ w = self.fp.write
+ w('Content-Type: multipart/mixed; boundary="foo"\r\n\r\n')
+ w('This is a multipart message. Ignore this bit.\r\n')
+ w('--foo\r\n')
+
+ w('Content-Type: text/plain\r\n\r\n')
+ w('Hello, world!\r\n')
+ w('\r\n')
+ w('Blah blah\r\n')
+ w('foo\r\n')
+ w('-foo\r\n')
+ w('--foo\r\n')
+
+ w('Content-Type: multipart/alternative; boundary="bar"\r\n\r\n')
+ w('This is a multipart message. Ignore this bit.\r\n')
+ w('--bar\r\n')
+
+ w('Content-Type: text/plain\r\n\r\n')
+ w('Hello, world!\r\n')
+ w('\r\n')
+ w('Blah blah\r\n')
+ w('--bar\r\n')
+
+ w('Content-Type: text/html\r\n\r\n')
+ w('<b>Hello, world!</b>\r\n')
+ w('--bar--\r\n')
+ w('--foo\r\n')
+
+ w('Content-Type: text/plain\r\n\r\n')
+ w('Last bit\n')
+ w('--foo--\r\n')
+ self.fp.seek(0)
+
+ def testMultipart(self):
+ m = Message(self.fp)
+ self.assert_(m is not None)
+
+ # skip the first bit
+ p = m.getpart()
+ self.assert_(p is not None)
+ self.assertEqual(p.fp.read(),
+ 'This is a multipart message. Ignore this bit.\r\n')
+
+ # first text/plain
+ p = m.getpart()
+ self.assert_(p is not None)
+ self.assertEqual(p.gettype(), 'text/plain')
+ self.assertEqual(p.fp.read(),
+ 'Hello, world!\r\n\r\nBlah blah\r\nfoo\r\n-foo\r\n')
+
+ # sub-multipart
+ p = m.getpart()
+ self.assert_(p is not None)
+ self.assertEqual(p.gettype(), 'multipart/alternative')
+
+ # sub-multipart text/plain
+ q = p.getpart()
+ self.assert_(q is not None)
+ q = p.getpart()
+ self.assert_(q is not None)
+ self.assertEqual(q.gettype(), 'text/plain')
+ self.assertEqual(q.fp.read(), 'Hello, world!\r\n\r\nBlah blah\r\n')
+
+ # sub-multipart text/html
+ q = p.getpart()
+ self.assert_(q is not None)
+ self.assertEqual(q.gettype(), 'text/html')
+ self.assertEqual(q.fp.read(), '<b>Hello, world!</b>\r\n')
+
+ # sub-multipart end
+ q = p.getpart()
+ self.assert_(q is None)
+
+ # final text/plain
+ p = m.getpart()
+ self.assert_(p is not None)
+ self.assertEqual(p.gettype(), 'text/plain')
+ self.assertEqual(p.fp.read(),
+ 'Last bit\n')
+
+ # end
+ p = m.getpart()
+ self.assert_(p is None)
+
+ def TestExtraction(self, spec, expected):
+ self.assertEqual(TestMessage(spec).extract_content(), expected)
+
+ def testTextPlain(self):
+ self.TestExtraction('text/plain', ('foo\n', []))
+
+ def testAttachedTextPlain(self):
+ self.TestExtraction("""
+multipart/mixed
+ text/plain
+ text/plain""",
+ ('foo\n',
+ [('foo.txt', 'text/plain', 'foo\n')]))
+
+ def testMultipartMixed(self):
+ self.TestExtraction("""
+multipart/mixed
+ text/plain
+ application/pdf""",
+ ('foo\n',
+ [('foo.pdf', 'application/pdf', 'foo\n')]))
+
+ def testMultipartAlternative(self):
+ self.TestExtraction("""
+multipart/alternative
+ text/plain
+ application/pdf
+""", ('foo\n', [('foo.pdf', 'application/pdf', 'foo\n')]))
+
+ def testDeepMultipartAlternative(self):
+ self.TestExtraction("""
+multipart/mixed
+ multipart/alternative
+ text/plain
+ application/pdf
+""", ('foo\n', [('foo.pdf', 'application/pdf', 'foo\n')]))
+
+ def testSignedText(self):
+ self.TestExtraction("""
+multipart/signed
+ text/plain
+ application/pgp-signature""", ('foo\n', []))
+
+ def testSignedAttachments(self):
+ self.TestExtraction("""
+multipart/signed
+ multipart/mixed
+ text/plain
+ application/pdf
+ application/pgp-signature""",
+ ('foo\n',
+ [('foo.pdf', 'application/pdf', 'foo\n')]))
+
+ def testAttachedSignature(self):
+ self.TestExtraction("""
+multipart/mixed
+ text/plain
+ application/pgp-signature""",
+ ('foo\n',
+ [('foo.gpg', 'application/pgp-signature', 'foo\n')]))
+
+ def testMessageRfc822(self):
+ self.TestExtraction("""
+multipart/mixed
+ message/rfc822""",
+ (None,
+ [('foo', 'message/rfc822', 'foo\n')]))
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(MultipartTestCase))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/test/test_mysql.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_mysql.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,101 @@
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: test_mysql.py,v 1.15 2004/11/10 22:22:59 richard Exp $
+
+import unittest, os, shutil, time, imp
+
+from roundup.hyperdb import DatabaseError
+from roundup.backends import get_backend, have_backend
+
+from db_test_base import DBTest, ROTest, config, SchemaTest, ClassicInitTest
+
+
+class mysqlOpener:
+ if have_backend('mysql'):
+ module = get_backend('mysql')
+
+ def setUp(self):
+ self.module.db_nuke(config)
+
+ def tearDown(self):
+ self.db.close()
+ self.nuke_database()
+
+ def nuke_database(self):
+ self.module.db_nuke(config)
+
+class mysqlDBTest(mysqlOpener, DBTest):
+ def setUp(self):
+ mysqlOpener.setUp(self)
+ DBTest.setUp(self)
+
+class mysqlROTest(mysqlOpener, ROTest):
+ def setUp(self):
+ mysqlOpener.setUp(self)
+ ROTest.setUp(self)
+
+class mysqlSchemaTest(mysqlOpener, SchemaTest):
+ def setUp(self):
+ mysqlOpener.setUp(self)
+ SchemaTest.setUp(self)
+
+class mysqlClassicInitTest(mysqlOpener, ClassicInitTest):
+ backend = 'mysql'
+ def setUp(self):
+ mysqlOpener.setUp(self)
+ ClassicInitTest.setUp(self)
+ def tearDown(self):
+ ClassicInitTest.tearDown(self)
+ self.nuke_database()
+
+from session_common import RDBMSTest
+class mysqlSessionTest(mysqlOpener, RDBMSTest):
+ def setUp(self):
+ mysqlOpener.setUp(self)
+ RDBMSTest.setUp(self)
+ def tearDown(self):
+ RDBMSTest.tearDown(self)
+ mysqlOpener.tearDown(self)
+
+def test_suite():
+ suite = unittest.TestSuite()
+ if not have_backend('mysql'):
+ print "Skipping mysql tests"
+ return suite
+
+ import MySQLdb
+ try:
+ # Check if we can connect to the server.
+ # use db_exists() to make a connection, ignore it's return value
+ mysqlOpener.module.db_exists(config)
+ except (MySQLdb.MySQLError, DatabaseError), msg:
+ print "Skipping mysql tests (%s)"%msg
+ else:
+ print 'Including mysql tests'
+ suite.addTest(unittest.makeSuite(mysqlDBTest))
+ suite.addTest(unittest.makeSuite(mysqlROTest))
+ suite.addTest(unittest.makeSuite(mysqlSchemaTest))
+ suite.addTest(unittest.makeSuite(mysqlClassicInitTest))
+ suite.addTest(unittest.makeSuite(mysqlSessionTest))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/test/test_postgresql.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_postgresql.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,116 @@
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: test_postgresql.py,v 1.12 2004/11/03 01:34:21 richard Exp $
+
+import unittest
+
+from roundup.hyperdb import DatabaseError
+
+from db_test_base import DBTest, ROTest, config, SchemaTest, ClassicInitTest
+
+from roundup.backends import get_backend, have_backend
+
+class postgresqlOpener:
+ if have_backend('postgresql'):
+ module = get_backend('postgresql')
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ self.nuke_database()
+
+ def nuke_database(self):
+ # clear out the database - easiest way is to nuke and re-create it
+ self.module.db_nuke(config)
+
+class postgresqlDBTest(postgresqlOpener, DBTest):
+ def setUp(self):
+ postgresqlOpener.setUp(self)
+ DBTest.setUp(self)
+
+ def tearDown(self):
+ DBTest.tearDown(self)
+ postgresqlOpener.tearDown(self)
+
+ def testFilteringIntervalSort(self):
+ # PostgreSQL sorts NULLs differently to other databases (others
+ # treat it as lower than real values, PG treats it as higher)
+ ae, filt = self.filteringSetup()
+ # ascending should sort None, 1:10, 1d
+ ae(filt(None, {}, ('+','foo'), (None,None)), ['4', '1', '2', '3'])
+ # descending should sort 1d, 1:10, None
+ ae(filt(None, {}, ('-','foo'), (None,None)), ['3', '2', '1', '4'])
+
+class postgresqlROTest(postgresqlOpener, ROTest):
+ def setUp(self):
+ postgresqlOpener.setUp(self)
+ ROTest.setUp(self)
+
+ def tearDown(self):
+ ROTest.tearDown(self)
+ postgresqlOpener.tearDown(self)
+
+class postgresqlSchemaTest(postgresqlOpener, SchemaTest):
+ def setUp(self):
+ postgresqlOpener.setUp(self)
+ SchemaTest.setUp(self)
+
+ def tearDown(self):
+ SchemaTest.tearDown(self)
+ postgresqlOpener.tearDown(self)
+
+class postgresqlClassicInitTest(postgresqlOpener, ClassicInitTest):
+ backend = 'postgresql'
+ def setUp(self):
+ postgresqlOpener.setUp(self)
+ ClassicInitTest.setUp(self)
+
+ def tearDown(self):
+ ClassicInitTest.tearDown(self)
+ postgresqlOpener.tearDown(self)
+
+from session_common import RDBMSTest
+class postgresqlSessionTest(postgresqlOpener, RDBMSTest):
+ def setUp(self):
+ postgresqlOpener.setUp(self)
+ RDBMSTest.setUp(self)
+ def tearDown(self):
+ RDBMSTest.tearDown(self)
+ postgresqlOpener.tearDown(self)
+
+def test_suite():
+ suite = unittest.TestSuite()
+ if not have_backend('postgresql'):
+ print "Skipping postgresql tests"
+ return suite
+
+ # make sure we start with a clean slate
+ if postgresqlOpener.module.db_exists(config):
+ postgresqlOpener.module.db_nuke(config, 1)
+
+ # TODO: Check if we can run postgresql tests
+ print 'Including postgresql tests'
+ suite.addTest(unittest.makeSuite(postgresqlDBTest))
+ suite.addTest(unittest.makeSuite(postgresqlROTest))
+ suite.addTest(unittest.makeSuite(postgresqlSchemaTest))
+ suite.addTest(unittest.makeSuite(postgresqlClassicInitTest))
+ suite.addTest(unittest.makeSuite(postgresqlSessionTest))
+ return suite
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/test/test_rfc2822.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_rfc2822.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,27 @@
+from roundup.rfc2822 import decode_header, encode_header
+
+import unittest, time
+
+class RFC2822TestCase(unittest.TestCase):
+ def testDecode(self):
+ src = 'Re: [it_issue3] '\
+ '=?ISO-8859-1?Q?Ren=E9s_[resp=3Dg=2Cstatus=3D?= '\
+ '=?ISO-8859-1?Q?feedback]?='
+ result = 'Re: [it_issue3] Ren\xc3\xa9s [resp=g,status=feedback]'
+ self.assertEqual(decode_header(src), result)
+
+ src = 'Re: [it_issue3]'\
+ ' =?ISO-8859-1?Q?Ren=E9s_[resp=3Dg=2Cstatus=3D?=' \
+ ' =?ISO-8859-1?Q?feedback]?='
+ result = 'Re: [it_issue3] Ren\xc3\xa9s [resp=g,status=feedback]'
+ self.assertEqual(decode_header(src), result)
+
+ def testEncode(self):
+ src = 'Re: [it_issue3] Ren\xc3\xa9s [status=feedback]'
+ result = '=?utf-8?q?Re:_[it=5Fissue3]_Ren=C3=A9s_[status=3Dfeedback]?='
+ self.assertEqual(encode_header(src), result)
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(RFC2822TestCase))
+ return suite
Added: tracker/vendor/roundup/current/test/test_schema.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_schema.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,88 @@
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: test_schema.py,v 1.15 2004/10/16 12:43:11 a1s Exp $
+
+import unittest, os, shutil
+
+from roundup import configuration
+from roundup.backends import back_anydbm
+from roundup.hyperdb import String, Password, Link, Multilink, Date, \
+ Interval
+
+config = configuration.CoreConfig()
+config.DATABASE = "_test_dir"
+
+class SchemaTestCase(unittest.TestCase):
+ def setUp(self):
+ # remove previous test, ignore errors
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+ os.makedirs(config.DATABASE + '/files')
+ self.db = back_anydbm.Database(config, 'admin')
+ self.db.post_init()
+ self.db.clear()
+
+ def tearDown(self):
+ self.db.close()
+ shutil.rmtree(config.DATABASE)
+
+ def testA_Status(self):
+ status = back_anydbm.Class(self.db, "status", name=String())
+ self.assert_(status, 'no class object generated')
+ status.setkey("name")
+ val = status.create(name="unread")
+ self.assertEqual(val, '1', 'expecting "1"')
+ val = status.create(name="in-progress")
+ self.assertEqual(val, '2', 'expecting "2"')
+ val = status.create(name="testing")
+ self.assertEqual(val, '3', 'expecting "3"')
+ val = status.create(name="resolved")
+ self.assertEqual(val, '4', 'expecting "4"')
+ val = status.count()
+ self.assertEqual(val, 4, 'expecting 4')
+ val = status.list()
+ self.assertEqual(val, ['1', '2', '3', '4'], 'blah')
+ val = status.lookup("in-progress")
+ self.assertEqual(val, '2', 'expecting "2"')
+ status.retire('3')
+ val = status.list()
+ self.assertEqual(val, ['1', '2', '4'], 'blah')
+
+ def testB_Issue(self):
+ issue = back_anydbm.Class(self.db, "issue", title=String(),
+ status=Link("status"))
+ self.assert_(issue, 'no class object returned')
+
+ def testC_User(self):
+ user = back_anydbm.Class(self.db, "user", username=String(),
+ password=Password())
+ self.assert_(user, 'no class object returned')
+ user.setkey("username")
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(SchemaTestCase))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/test/test_security.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_security.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,190 @@
+# 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: test_security.py,v 1.10 2006/02/03 04:04:37 richard Exp $
+
+import os, unittest, shutil
+
+from roundup import backends
+from roundup.password import Password
+from db_test_base import setupSchema, MyTestCase, config
+
+class PermissionTest(MyTestCase):
+ def setUp(self):
+ backend = backends.get_backend('anydbm')
+ # remove previous test, ignore errors
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+ os.makedirs(config.DATABASE + '/files')
+ self.db = backend.Database(config, 'admin')
+ setupSchema(self.db, 1, backend)
+
+ def testInterfaceSecurity(self):
+ ' test that the CGI and mailgw have initialised security OK '
+ # TODO: some asserts
+
+ def testInitialiseSecurity(self):
+ ei = self.db.security.addPermission(name="Edit", klass="issue",
+ description="User is allowed to edit issues")
+ self.db.security.addPermissionToRole('User', ei)
+ ai = self.db.security.addPermission(name="View", klass="issue",
+ description="User is allowed to access issues")
+ self.db.security.addPermissionToRole('User', ai)
+
+ def testAdmin(self):
+ ei = self.db.security.addPermission(name="Edit", klass="issue",
+ description="User is allowed to edit issues")
+ self.db.security.addPermissionToRole('User', ei)
+ ei = self.db.security.addPermission(name="Edit", klass=None,
+ description="User is allowed to edit issues")
+ self.db.security.addPermissionToRole('Admin', ei)
+
+ u1 = self.db.user.create(username='one', roles='Admin')
+ u2 = self.db.user.create(username='two', roles='User')
+
+ self.assert_(self.db.security.hasPermission('Edit', u1, None))
+ self.assert_(not self.db.security.hasPermission('Edit', u2, None))
+
+
+ def testGetPermission(self):
+ self.db.security.getPermission('Edit')
+ self.db.security.getPermission('View')
+ self.assertRaises(ValueError, self.db.security.getPermission, 'x')
+ self.assertRaises(ValueError, self.db.security.getPermission, 'Edit',
+ 'fubar')
+
+ add = self.db.security.addPermission
+ get = self.db.security.getPermission
+
+ # class
+ ei = add(name="Edit", klass="issue")
+ self.assertEquals(get('Edit', 'issue'), ei)
+ ai = add(name="View", klass="issue")
+ self.assertEquals(get('View', 'issue'), ai)
+
+ # property
+ epi = add(name="Edit", klass="issue", properties=['title'])
+ self.assertEquals(get('Edit', 'issue', properties=['title']), epi)
+ api = add(name="View", klass="issue", properties=['title'])
+ self.assertEquals(get('View', 'issue', properties=['title']), api)
+
+ # check function
+ dummy = lambda: 0
+ eci = add(name="Edit", klass="issue", check=dummy)
+ self.assertEquals(get('Edit', 'issue', check=dummy), eci)
+ aci = add(name="View", klass="issue", check=dummy)
+ self.assertEquals(get('View', 'issue', check=dummy), aci)
+
+ # all
+ epci = add(name="Edit", klass="issue", properties=['title'],
+ check=dummy)
+ self.assertEquals(get('Edit', 'issue', properties=['title'],
+ check=dummy), epci)
+ apci = add(name="View", klass="issue", properties=['title'],
+ check=dummy)
+ self.assertEquals(get('View', 'issue', properties=['title'],
+ check=dummy), apci)
+
+ def testDBinit(self):
+ self.db.user.create(username="demo", roles='User')
+ self.db.user.create(username="anonymous", roles='Anonymous')
+
+ def testAccessControls(self):
+ add = self.db.security.addPermission
+ has = self.db.security.hasPermission
+ addRole = self.db.security.addRole
+ addToRole = self.db.security.addPermissionToRole
+
+ none = self.db.user.create(username='none', roles='None')
+
+ # test admin access
+ addRole(name='Super')
+ addToRole('Super', add(name="Test"))
+ super = self.db.user.create(username='super', roles='Super')
+
+ # test class-level access
+ addRole(name='Role1')
+ addToRole('Role1', add(name="Test", klass="test"))
+ user1 = self.db.user.create(username='user1', roles='Role1')
+ self.assertEquals(has('Test', user1, 'test'), 1)
+ self.assertEquals(has('Test', super, 'test'), 1)
+ self.assertEquals(has('Test', none, 'test'), 0)
+
+ # property
+ addRole(name='Role2')
+ addToRole('Role2', add(name="Test", klass="test", properties=['a','b']))
+ user2 = self.db.user.create(username='user2', roles='Role2')
+ # *any* access to class
+ self.assertEquals(has('Test', user1, 'test'), 1)
+ self.assertEquals(has('Test', user2, 'test'), 1)
+
+ # *any* access to item
+ self.assertEquals(has('Test', user1, 'test', itemid='1'), 1)
+ self.assertEquals(has('Test', user2, 'test', itemid='1'), 1)
+ self.assertEquals(has('Test', super, 'test', itemid='1'), 1)
+ self.assertEquals(has('Test', none, 'test', itemid='1'), 0)
+
+ # now property test
+ self.assertEquals(has('Test', user2, 'test', property='a'), 1)
+ self.assertEquals(has('Test', user2, 'test', property='b'), 1)
+ self.assertEquals(has('Test', user2, 'test', property='c'), 0)
+ self.assertEquals(has('Test', user1, 'test', property='a'), 1)
+ self.assertEquals(has('Test', user1, 'test', property='b'), 1)
+ self.assertEquals(has('Test', user1, 'test', property='c'), 1)
+ self.assertEquals(has('Test', super, 'test', property='a'), 1)
+ self.assertEquals(has('Test', super, 'test', property='b'), 1)
+ self.assertEquals(has('Test', super, 'test', property='c'), 1)
+ self.assertEquals(has('Test', none, 'test', property='a'), 0)
+ self.assertEquals(has('Test', none, 'test', property='b'), 0)
+ self.assertEquals(has('Test', none, 'test', property='c'), 0)
+ self.assertEquals(has('Test', none, 'test'), 0)
+
+ # check function
+ check = lambda db, userid, itemid: itemid == '1'
+ addRole(name='Role3')
+ addToRole('Role3', add(name="Test", klass="test", check=check))
+ user3 = self.db.user.create(username='user3', roles='Role3')
+ # *any* access to class
+ self.assertEquals(has('Test', user1, 'test'), 1)
+ self.assertEquals(has('Test', user2, 'test'), 1)
+ self.assertEquals(has('Test', user3, 'test'), 1)
+ self.assertEquals(has('Test', none, 'test'), 0)
+ # now check function
+ self.assertEquals(has('Test', user3, 'test', itemid='1'), 1)
+ self.assertEquals(has('Test', user3, 'test', itemid='2'), 0)
+ self.assertEquals(has('Test', user2, 'test', itemid='1'), 1)
+ self.assertEquals(has('Test', user2, 'test', itemid='2'), 1)
+ self.assertEquals(has('Test', user1, 'test', itemid='2'), 1)
+ self.assertEquals(has('Test', user1, 'test', itemid='2'), 1)
+ self.assertEquals(has('Test', super, 'test', itemid='1'), 1)
+ self.assertEquals(has('Test', super, 'test', itemid='2'), 1)
+ self.assertEquals(has('Test', none, 'test', itemid='1'), 0)
+ self.assertEquals(has('Test', none, 'test', itemid='2'), 0)
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(PermissionTest))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+# vim: set filetype=python sts=4 sw=4 et si :
Added: tracker/vendor/roundup/current/test/test_sqlite.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_sqlite.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,65 @@
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: test_sqlite.py,v 1.5 2004/11/03 01:34:21 richard Exp $
+
+import unittest, os, shutil, time
+from roundup.backends import get_backend, have_backend
+
+from db_test_base import DBTest, ROTest, SchemaTest, ClassicInitTest, config
+
+class sqliteOpener:
+ if have_backend('sqlite'):
+ module = get_backend('sqlite')
+
+ def nuke_database(self):
+ shutil.rmtree(config.DATABASE)
+
+class sqliteDBTest(sqliteOpener, DBTest):
+ pass
+
+class sqliteROTest(sqliteOpener, ROTest):
+ pass
+
+class sqliteSchemaTest(sqliteOpener, SchemaTest):
+ pass
+
+class sqliteClassicInitTest(ClassicInitTest):
+ backend = 'sqlite'
+
+from session_common import RDBMSTest
+class sqliteSessionTest(sqliteOpener, RDBMSTest):
+ pass
+
+def test_suite():
+ suite = unittest.TestSuite()
+ from roundup import backends
+ if not have_backend('sqlite'):
+ print 'Skipping sqlite tests'
+ return suite
+ print 'Including sqlite tests'
+ suite.addTest(unittest.makeSuite(sqliteDBTest))
+ suite.addTest(unittest.makeSuite(sqliteROTest))
+ suite.addTest(unittest.makeSuite(sqliteSchemaTest))
+ suite.addTest(unittest.makeSuite(sqliteClassicInitTest))
+ suite.addTest(unittest.makeSuite(sqliteSessionTest))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
Added: tracker/vendor/roundup/current/test/test_templating.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_templating.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,252 @@
+import unittest
+from cgi import FieldStorage, MiniFieldStorage
+
+from roundup.cgi.templating import *
+from test_actions import MockNull, true
+
+class MockDatabase(MockNull):
+ def getclass(self, name):
+ return self.classes[name]
+
+class TemplatingTestCase(unittest.TestCase):
+ def setUp(self):
+ self.form = FieldStorage()
+ self.client = MockNull()
+ self.client.db = MockDatabase()
+ self.client.form = self.form
+
+class HTMLDatabaseTestCase(TemplatingTestCase):
+ def test_HTMLDatabase___getitem__(self):
+ db = HTMLDatabase(self.client)
+ self.assert_(isinstance(db['issue'], HTMLClass))
+ # following assertions are invalid
+ # since roundup/cgi/templating.py r1.173.
+ # HTMLItem is function, not class,
+ # but HTMLUserClass and HTMLUser are passed on.
+ # these classes are no more. they have ceased to be.
+ #self.assert_(isinstance(db['user'], HTMLUserClass))
+ #self.assert_(isinstance(db['issue1'], HTMLItem))
+ #self.assert_(isinstance(db['user1'], HTMLUser))
+
+ def test_HTMLDatabase___getattr__(self):
+ db = HTMLDatabase(self.client)
+ self.assert_(isinstance(db.issue, HTMLClass))
+ # see comment in test_HTMLDatabase___getitem__
+ #self.assert_(isinstance(db.user, HTMLUserClass))
+ #self.assert_(isinstance(db.issue1, HTMLItem))
+ #self.assert_(isinstance(db.user1, HTMLUser))
+
+ def test_HTMLDatabase_classes(self):
+ db = HTMLDatabase(self.client)
+ db._db.classes = {'issue':MockNull(), 'user': MockNull()}
+ db.classes()
+
+class FunctionsTestCase(TemplatingTestCase):
+ def test_lookupIds(self):
+ db = HTMLDatabase(self.client)
+ def lookup(key):
+ if key == 'ok':
+ return '1'
+ if key == 'fail':
+ raise KeyError, 'fail'
+ return key
+ db._db.classes = {'issue': MockNull(lookup=lookup)}
+ prop = MockNull(classname='issue')
+ self.assertEqual(lookupIds(db._db, prop, ['1','2']), ['1','2'])
+ self.assertEqual(lookupIds(db._db, prop, ['ok','2']), ['1','2'])
+ self.assertEqual(lookupIds(db._db, prop, ['ok', 'fail'], 1),
+ ['1', 'fail'])
+ self.assertEqual(lookupIds(db._db, prop, ['ok', 'fail']), ['1'])
+
+ def test_lookupKeys(self):
+ db = HTMLDatabase(self.client)
+ def get(entry, key):
+ return {'1': 'green', '2': 'eggs'}.get(entry, entry)
+ shrubbery = MockNull(get=get)
+ db._db.classes = {'shrubbery': shrubbery}
+ self.assertEqual(lookupKeys(shrubbery, 'spam', ['1','2']),
+ ['green', 'eggs'])
+ self.assertEqual(lookupKeys(shrubbery, 'spam', ['ok','2']), ['ok',
+ 'eggs'])
+
+'''
+class HTMLPermissions:
+ def is_edit_ok(self):
+ def is_view_ok(self):
+ def is_only_view_ok(self):
+ def view_check(self):
+ def edit_check(self):
+
+def input_html4(**attrs):
+def input_xhtml(**attrs):
+
+class HTMLInputMixin:
+ def __init__(self):
+
+class HTMLClass(HTMLInputMixin, HTMLPermissions):
+ def __init__(self, client, classname, anonymous=0):
+ def __repr__(self):
+ def __getitem__(self, item):
+ def __getattr__(self, attr):
+ def designator(self):
+ def getItem(self, itemid, num_re=re.compile('-?\d+')):
+ def properties(self, sort=1):
+ def list(self, sort_on=None):
+ def csv(self):
+ def propnames(self):
+ def filter(self, request=None, filterspec={}, sort=(None,None),
+ def classhelp(self, properties=None, label='(list)', width='500',
+ def submit(self, label="Submit New Entry"):
+ def history(self):
+ def renderWith(self, name, **kwargs):
+
+class HTMLItem(HTMLInputMixin, HTMLPermissions):
+ def __init__(self, client, classname, nodeid, anonymous=0):
+ def __repr__(self):
+ def __getitem__(self, item):
+ def __getattr__(self, attr):
+ def designator(self):
+ def is_retired(self):
+ def submit(self, label="Submit Changes"):
+ def journal(self, direction='descending'):
+ def history(self, direction='descending', dre=re.compile('\d+')):
+ def renderQueryForm(self):
+
+class HTMLUserPermission:
+ def is_edit_ok(self):
+ def is_view_ok(self):
+ def _user_perm_check(self, type):
+
+class HTMLUserClass(HTMLUserPermission, HTMLClass):
+
+class HTMLUser(HTMLUserPermission, HTMLItem):
+ def __init__(self, client, classname, nodeid, anonymous=0):
+ def hasPermission(self, permission, classname=_marker):
+
+class HTMLProperty(HTMLInputMixin, HTMLPermissions):
+ def __init__(self, client, classname, nodeid, prop, name, value,
+ def __repr__(self):
+ def __str__(self):
+ def __cmp__(self, other):
+ def is_edit_ok(self):
+ def is_view_ok(self):
+
+class StringHTMLProperty(HTMLProperty):
+ def _hyper_repl(self, match):
+ def hyperlinked(self):
+ def plain(self, escape=0, hyperlink=0):
+ def stext(self, escape=0):
+ def field(self, size = 30):
+ def multiline(self, escape=0, rows=5, cols=40):
+ def email(self, escape=1):
+
+class PasswordHTMLProperty(HTMLProperty):
+ def plain(self):
+ def field(self, size = 30):
+ def confirm(self, size = 30):
+
+class NumberHTMLProperty(HTMLProperty):
+ def plain(self):
+ def field(self, size = 30):
+ def __int__(self):
+ def __float__(self):
+
+class BooleanHTMLProperty(HTMLProperty):
+ def plain(self):
+ def field(self):
+
+class DateHTMLProperty(HTMLProperty):
+ def plain(self):
+ def now(self):
+ def field(self, size = 30):
+ def reldate(self, pretty=1):
+ def pretty(self, format=_marker):
+ def local(self, offset):
+
+class IntervalHTMLProperty(HTMLProperty):
+ def plain(self):
+ def pretty(self):
+ def field(self, size = 30):
+
+class LinkHTMLProperty(HTMLProperty):
+ def __init__(self, *args, **kw):
+ def __getattr__(self, attr):
+ def plain(self, escape=0):
+ def field(self, showid=0, size=None):
+ def menu(self, size=None, height=None, showid=0, additional=[],
+
+class MultilinkHTMLProperty(HTMLProperty):
+ def __init__(self, *args, **kwargs):
+ def __len__(self):
+ def __getattr__(self, attr):
+ def __getitem__(self, num):
+ def __contains__(self, value):
+ def reverse(self):
+ def plain(self, escape=0):
+ def field(self, size=30, showid=0):
+ def menu(self, size=None, height=None, showid=0, additional=[],
+
+def make_sort_function(db, classname, sort_on=None):
+ def sortfunc(a, b):
+
+def find_sort_key(linkcl):
+
+def handleListCGIValue(value):
+
+class ShowDict:
+ def __init__(self, columns):
+ def __getitem__(self, name):
+
+class HTMLRequest(HTMLInputMixin):
+ def __init__(self, client):
+ def _post_init(self):
+ def updateFromURL(self, url):
+ def update(self, kwargs):
+ def description(self):
+ def __str__(self):
+ def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
+ def indexargs_url(self, url, args):
+ def base_javascript(self):
+ def batch(self):
+
+class Batch(ZTUtils.Batch):
+ def __init__(self, client, sequence, size, start, end=0, orphan=0,
+ def __getitem__(self, index):
+ def propchanged(self, property):
+ def previous(self):
+ def next(self):
+
+class TemplatingUtils:
+ def __init__(self, client):
+ def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
+
+class NoTemplate(Exception):
+class Unauthorised(Exception):
+ def __init__(self, action, klass):
+ def __str__(self):
+def find_template(dir, name, extension):
+
+class Templates:
+ def __init__(self, dir):
+ def precompileTemplates(self):
+ def get(self, name, extension=None):
+ def __getitem__(self, name):
+
+class RoundupPageTemplate(PageTemplate.PageTemplate):
+ def getContext(self, client, classname, request):
+ def render(self, client, classname, request, **options):
+ def __repr__(self):
+'''
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(HTMLDatabaseTestCase))
+ suite.addTest(unittest.makeSuite(FunctionsTestCase))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/test/test_token.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_token.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,61 @@
+#
+# Copyright (c) 2001 Richard Jones
+# 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.
+#
+# This module is distributed in the hope that it will be useful,
+# 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 $
+
+import unittest, time
+
+from roundup.token import token_split
+
+class TokenTestCase(unittest.TestCase):
+ def testValid(self):
+ l = token_split('hello world')
+ self.assertEqual(l, ['hello', 'world'])
+
+ def testIgnoreExtraSpace(self):
+ l = token_split('hello world ')
+ self.assertEqual(l, ['hello', 'world'])
+
+ def testQuoting(self):
+ l = token_split('"hello world"')
+ self.assertEqual(l, ['hello world'])
+ l = token_split("'hello world'")
+ self.assertEqual(l, ['hello world'])
+
+ def testEmbedQuote(self):
+ l = token_split(r'Roch\'e Compaan')
+ self.assertEqual(l, ["Roch'e", "Compaan"])
+ l = token_split('address="1 2 3"')
+ self.assertEqual(l, ['address=1 2 3'])
+
+ def testEscaping(self):
+ l = token_split('"Roch\'e" Compaan')
+ self.assertEqual(l, ["Roch'e", "Compaan"])
+ l = token_split(r'hello\ world')
+ self.assertEqual(l, ['hello world'])
+ l = token_split(r'\\')
+ self.assertEqual(l, ['\\'])
+ l = token_split(r'\n')
+ self.assertEqual(l, ['\n'])
+
+ def testBadQuote(self):
+ self.assertRaises(ValueError, token_split, '"hello world')
+ self.assertRaises(ValueError, token_split, "Roch'e Compaan")
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(TokenTestCase))
+ return suite
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ unittest.main(testRunner=runner)
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/test/test_tsearch2.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/test/test_tsearch2.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,124 @@
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+#
+# $Id: test_tsearch2.py,v 1.1 2004/12/16 22:22:55 jlgijsbers Exp $
+
+import unittest
+
+from roundup.hyperdb import DatabaseError
+
+from db_test_base import DBTest, ROTest, config, SchemaTest, ClassicInitTest
+
+from roundup.backends import get_backend, have_backend
+
+class tsearch2Opener:
+ if have_backend('tsearch2'):
+ module = get_backend('tsearch2')
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ self.nuke_database()
+
+ def nuke_database(self):
+ # clear out the database - easiest way is to nuke and re-create it
+ self.module.db_nuke(config)
+
+class tsearch2DBTest(tsearch2Opener, DBTest):
+ def setUp(self):
+ tsearch2Opener.setUp(self)
+ DBTest.setUp(self)
+
+ def tearDown(self):
+ DBTest.tearDown(self)
+ tsearch2Opener.tearDown(self)
+
+ def testFilteringIntervalSort(self):
+ # Tsearch2 sorts NULLs differently to other databases (others
+ # treat it as lower than real values, PG treats it as higher)
+ ae, filt = self.filteringSetup()
+ # ascending should sort None, 1:10, 1d
+ ae(filt(None, {}, ('+','foo'), (None,None)), ['4', '1', '2', '3'])
+ # descending should sort 1d, 1:10, None
+ ae(filt(None, {}, ('-','foo'), (None,None)), ['3', '2', '1', '4'])
+
+ def testTransactions(self):
+ # XXX: in its current form, this test doesn't make sense for tsearch2.
+ # It tests the transactions mechanism by counting the number of files
+ # in the FileStorage. As tsearch2 doesn't use the FileStorage, this
+ # fails. The test should probably be rewritten with some other way of
+ # checking rollbacks/commits.
+ pass
+
+class tsearch2ROTest(tsearch2Opener, ROTest):
+ def setUp(self):
+ tsearch2Opener.setUp(self)
+ ROTest.setUp(self)
+
+ def tearDown(self):
+ ROTest.tearDown(self)
+ tsearch2Opener.tearDown(self)
+
+class tsearch2SchemaTest(tsearch2Opener, SchemaTest):
+ def setUp(self):
+ tsearch2Opener.setUp(self)
+ SchemaTest.setUp(self)
+
+ def tearDown(self):
+ SchemaTest.tearDown(self)
+ tsearch2Opener.tearDown(self)
+
+class tsearch2ClassicInitTest(tsearch2Opener, ClassicInitTest):
+ backend = 'tsearch2'
+ def setUp(self):
+ tsearch2Opener.setUp(self)
+ ClassicInitTest.setUp(self)
+
+ def tearDown(self):
+ ClassicInitTest.tearDown(self)
+ tsearch2Opener.tearDown(self)
+
+from session_common import RDBMSTest
+class tsearch2SessionTest(tsearch2Opener, RDBMSTest):
+ def setUp(self):
+ tsearch2Opener.setUp(self)
+ RDBMSTest.setUp(self)
+ def tearDown(self):
+ RDBMSTest.tearDown(self)
+ tsearch2Opener.tearDown(self)
+
+def test_suite():
+ suite = unittest.TestSuite()
+ if not have_backend('tsearch2'):
+ print "Skipping tsearch2 tests"
+ return suite
+
+ # make sure we start with a clean slate
+ if tsearch2Opener.module.db_exists(config):
+ tsearch2Opener.module.db_nuke(config, 1)
+
+ # TODO: Check if we can run postgresql tests
+ print 'Including tsearch2 tests'
+ suite.addTest(unittest.makeSuite(tsearch2DBTest))
+ suite.addTest(unittest.makeSuite(tsearch2ROTest))
+ suite.addTest(unittest.makeSuite(tsearch2SchemaTest))
+ suite.addTest(unittest.makeSuite(tsearch2ClassicInitTest))
+ suite.addTest(unittest.makeSuite(tsearch2SessionTest))
+ return suite
+
+# vim: set et sts=4 sw=4 :
Added: tracker/vendor/roundup/current/tools/.cvsignore
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/tools/.cvsignore Sun Nov 5 21:30:25 2006
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+localconfig.py
+build
Added: tracker/vendor/roundup/current/tools/base64
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/tools/base64 Sun Nov 5 21:30:25 2006
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+import zlib, base64, sys
+print base64.encodestring(zlib.compress(sys.stdin.read()))
Added: tracker/vendor/roundup/current/tools/fixroles.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/tools/fixroles.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,40 @@
+import sys
+
+from roundup import admin
+
+class AdminTool(admin.AdminTool):
+ def __init__(self):
+ self.commands = admin.CommandDict()
+ for k in AdminTool.__dict__.keys():
+ if k[:3] == 'do_':
+ self.commands[k[3:]] = getattr(self, k)
+ self.help = {}
+ for k in AdminTool.__dict__.keys():
+ if k[:5] == 'help_':
+ self.help[k[5:]] = getattr(self, k)
+ self.instance_home = ''
+ self.db = None
+
+ def do_fixroles(self, args):
+ '''Usage: fixroles
+ Set the roles property for all users to reasonable defaults.
+
+ The admin user gets "Admin", the anonymous user gets "Anonymous"
+ and all other users get "User".
+ '''
+ # get the user class
+ cl = self.get_class('user')
+ for userid in cl.list():
+ username = cl.get(userid, 'username')
+ if username == 'admin':
+ roles = 'Admin'
+ elif username == 'anonymous':
+ roles = 'Anonymous'
+ else:
+ roles = 'User'
+ cl.set(userid, roles=roles)
+ return 0
+
+if __name__ == '__main__':
+ tool = AdminTool()
+ sys.exit(tool.main())
Added: tracker/vendor/roundup/current/tools/load_tracker.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/tools/load_tracker.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,96 @@
+#! /usr/bin/env python
+# $Id: load_tracker.py,v 1.6 2005/06/08 02:24:06 anthonybaxter Exp $
+
+'''
+Usage: %s <tracker home> <N>
+
+Load up the indicated tracker with N issues and N/100 users.
+'''
+
+import sys, os, random
+from roundup import instance
+
+# open the instance
+if len(sys.argv) < 2:
+ print "Error: Not enough arguments"
+ print __doc__.strip()%(sys.argv[0])
+ sys.exit(1)
+tracker_home = sys.argv[1]
+N = int(sys.argv[2])
+
+# open the tracker
+tracker = instance.open(tracker_home)
+db = tracker.open('admin')
+
+priorities = db.priority.list()
+statuses = db.status.list()
+resolved_id = db.status.lookup('resolved')
+statuses.remove(resolved_id)
+
+names = ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta',
+'theta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'omicron', 'pi',
+'rho']
+
+titles = '''Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
+Duis nibh purus, bibendum sed, condimentum ut, bibendum ut, risus.
+Fusce pede enim, nonummy sit amet, dapibus a, blandit eget, metus.
+Nulla risus.
+Vivamus tincidunt.
+Donec consequat convallis quam.
+Sed convallis vehicula felis.
+Aliquam laoreet, dui quis pharetra vehicula, magna justo.
+Euismod felis, eu adipiscing eros metus id tortor.
+Suspendisse et turpis.
+Aenean non felis.
+Nam egestas eros.
+Integer tellus quam, mattis ac, vestibulum sed, egestas quis, mauris.
+Nulla tincidunt diam sit amet dui.
+Nam odio mauris, dignissim vitae, eleifend eu, consectetuer id, risus.
+Suspendisse potenti.
+Donec tincidunt.
+Vestibulum gravida.
+Fusce luctus, neque id mattis fringilla, purus pede sodales pede.
+Quis ultricies urna odio sed orci.'''.splitlines()
+
+try:
+ try:
+ db.user.lookup('alpha0')
+ except:
+ # add some users
+ M = N/100
+ for i in range(M):
+ print '\ruser', i, ' ',
+ sys.stdout.flush()
+ if i/17 == 0:
+ db.user.create(username=names[i%17])
+ else:
+ db.user.create(username=names[i%17]+str(i/17))
+
+ # assignable user list
+ users = db.user.list()
+ users.remove(db.user.lookup('anonymous'))
+ print
+
+ # now create the issues
+ for i in range(N):
+ print '\rissue', i, ' ',
+ sys.stdout.flush()
+ # in practise, about 90% of issues are resolved
+ if random.random() > .9:
+ status = random.choice(statuses)
+ else:
+ status = resolved_id
+ db.issue.create(
+ title=random.choice(titles),
+ priority=random.choice(priorities),
+ status=status,
+ assignedto=random.choice(users))
+ if not i%1000:
+ db.commit()
+ print
+
+ db.commit()
+finally:
+ db.close()
+
+# vim: set filetype=python ts=4 sw=4 et si
Added: tracker/vendor/roundup/current/tools/migrate-queries.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/tools/migrate-queries.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,42 @@
+#! /usr/bin/env python
+'''
+migrate-queries <instance-home> [<instance-home> *]
+
+Migrate old queries in the specified instances to Roundup 0.6.0+ by
+removing the leading ? from their URLs. 0.6.0+ queries do not carry a
+leading ?; it is added by the 0.6.0 templating, so old queries lead
+to query URLs with a double leading ?? and a consequent 404 Not Found.
+'''
+__author__ = 'James Kew <jkew at mediabright.co.uk>'
+
+import sys
+import roundup.instance
+
+if len(sys.argv) == 1:
+ print __doc__
+ sys.exit(1)
+
+# Iterate over all instance homes specified in argv.
+for home in sys.argv[1:]:
+ # Do some basic exception handling to catch bad arguments.
+ try:
+ instance = roundup.instance.open(home)
+ except:
+ print 'Cannot open instance home directory %s!' % home
+ continue
+
+ db = instance.open('admin')
+
+ print 'Migrating active queries in %s (%s):'%(
+ instance.config.TRACKER_NAME, home)
+ for query in db.query.list():
+ url = db.query.get(query, 'url')
+ if url[0] == '?':
+ url = url[1:]
+ print ' Migrating query%s (%s)'%(query,
+ db.query.get(query, 'name'))
+ db.query.set(query, url=url)
+
+ db.commit()
+ db.close()
+
Added: tracker/vendor/roundup/current/tools/pygettext.py
==============================================================================
--- (empty file)
+++ tracker/vendor/roundup/current/tools/pygettext.py Sun Nov 5 21:30:25 2006
@@ -0,0 +1,666 @@
+#! /usr/bin/env python
+# Originally written by Barry Warsaw <barry at zope.com>
+#
+# 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>
+# 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
+# directory (including globbing chars, important for Win32).
+# Made docstring fit in 80 chars wide displays using pydoc.
+#
+
+# for selftesting
+try:
+ import fintl
+ _ = fintl.gettext
+except ImportError:
+ _ = lambda s: s
+
+__doc__ = _("""pygettext -- Python equivalent of xgettext(1)
+
+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.
+
+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.
+
+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.
+
+A word about marking Python strings as candidates for translation. GNU
+xgettext recognizes the following keywords: gettext, dgettext, dcgettext,
+and gettext_noop. But those can be a lot of text to include all over your
+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:
+
+ gettext("Translatable String")
+ _("Translatable String")
+
+Python of course has no preprocessor so this doesn't work so well. Thus,
+pygettext searches only for _() by default, but see the -k/--keyword flag
+below for how to augment this.
+
+ [1] http://www.python.org/workshops/1997-10/proceedings/loewis.html
+ [2] http://www.gnu.org/software/gettext/gettext.html
+
+NOTE: pygettext attempts to be option and feature compatible with GNU
+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.
+
+Usage: pygettext [options] inputfile ...
+
+Options:
+
+ -a
+ --extract-all
+ Extract all strings.
+
+ -d name
+ --default-domain=name
+ Rename the default output file from messages.pot to name.pot.
+
+ -E
+ --escape
+ Replace non-ASCII characters with octal escape sequences.
+
+ -D
+ --docstrings
+ Extract module, class, method, and function docstrings. These do
+ not need to be wrapped in _() markers, and in fact cannot be for
+ Python to consider them docstrings. (See also the -X option).
+
+ -h
+ --help
+ Print this help message and exit.
+
+ -k word
+ --keyword=word
+ Keywords to look for in addition to the default set, which are:
+ %(DEFAULTKEYWORDS)s
+
+ You can have multiple -k flags on the command line.
+
+ -K
+ --no-default-keywords
+ Disable the default set of keywords (see above). Any keywords
+ explicitly added with the -k/--keyword option are still recognized.
+
+ --no-location
+ Do not write filename/lineno location comments.
+
+ -n
+ --add-location
+ Write filename/lineno location comments indicating where each
+ extracted string is found in the source. These lines appear before
+ each msgid. The style of comments is controlled by the -S/--style
+ option. This is the default.
+
+ -o filename
+ --output=filename
+ Rename the default output file from messages.pot to filename. If
+ filename is `-' then the output is sent to standard out.
+
+ -p dir
+ --output-dir=dir
+ Output files will be placed in directory dir.
+
+ -S stylename
+ --style stylename
+ Specify which style to use for location comments. Two styles are
+ supported:
+
+ Solaris # File: filename, line: line-number
+ GNU #: filename:line
+
+ The style name is case insensitive. GNU style is the default.
+
+ -v
+ --verbose
+ Print the names of the files being processed.
+
+ -V
+ --version
+ Print the version of pygettext and exit.
+
+ -w columns
+ --width=columns
+ Set width of output to columns.
+
+ -x filename
+ --exclude-file=filename
+ Specify a file that contains a list of strings that are not be
+ extracted from the input files. Each string to be excluded must
+ appear on a line by itself in the file.
+
+ -X filename
+ --no-docstrings=filename
+ Specify a file that contains a list of files (one per line) that
+ should not have their docstrings extracted. This is only useful in
+ conjunction with the -D option above.
+
+If `inputfile' is -, standard input is read.
+""")
+
+import os
+import sys
+import time
+import getopt
+import token
+import tokenize
+import operator
+
+__version__ = '1.5'
+
+default_keywords = ['_']
+DEFAULTKEYWORDS = ', '.join(default_keywords)
+
+EMPTYSTRING = ''
+
+
+
+# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's
+# there.
+pot_header = _('''\
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\\n"
+"POT-Creation-Date: %(time)s\\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"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=CHARSET\\n"
+"Content-Transfer-Encoding: ENCODING\\n"
+"Generated-By: pygettext.py %(version)s\\n"
+
+''')
+
+
+def usage(code, msg=''):
+ print >> sys.stderr, __doc__ % globals()
+ if msg:
+ print >> sys.stderr, msg
+ sys.exit(code)
+
+
+
+escapes = []
+
+def make_escapes(pass_iso8859):
+ global escapes
+ if pass_iso8859:
+ # Allow iso-8859 characters to pass through so that e.g. 'msgid
+ # "Höhe"' would result not result in 'msgid "H\366he"'. Otherwise we
+ # escape any character outside the 32..126 range.
+ mod = 128
+ else:
+ mod = 256
+ for i in range(256):
+ if 32 <= (i % mod) <= 126:
+ escapes.append(chr(i))
+ else:
+ escapes.append("\\%03o" % i)
+ escapes[ord('\\')] = '\\\\'
+ escapes[ord('\t')] = '\\t'
+ escapes[ord('\r')] = '\\r'
+ escapes[ord('\n')] = '\\n'
+ escapes[ord('\"')] = '\\"'
+
+
+def escape(s):
+ global escapes
+ s = list(s)
+ for i in range(len(s)):
+ s[i] = escapes[ord(s[i])]
+ return EMPTYSTRING.join(s)
+
+
+def safe_eval(s):
+ # unwrap quotes, safely
+ return eval(s, {'__builtins__':{}}, {})
+
+
+def normalize(s):
+ # This converts the various Python string types into a format that is
+ # appropriate for .po files, namely much closer to C style.
+ lines = s.split('\n')
+ if len(lines) == 1:
+ s = '"' + escape(s) + '"'
+ else:
+ if not lines[-1]:
+ del lines[-1]
+ lines[-1] = lines[-1] + '\n'
+ for i in range(len(lines)):
+ lines[i] = escape(lines[i])
+ lineterm = '\\n"\n"'
+ s = '""\n"' + lineterm.join(lines) + '"'
+ return s
+
+
+def containsAny(str, 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().
+ """
+ # 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]
+
+ # don't recurse into CVS directories
+ if 'CVS' in names:
+ names.remove('CVS')
+
+ # 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])
+
+
+def _get_modpkg_path(dotted_name, pathlist=None):
+ """ 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.
+ """
+ import imp
+
+ # split off top-most name
+ parts = dotted_name.split('.', 1)
+
+ if len(parts) > 1:
+ # we have a dotted path, import top-level package
+ try:
+ file, pathname, description = imp.find_module(parts[0], pathlist)
+ if file: file.close()
+ except ImportError:
+ return None
+
+ # check if it's indeed a package
+ if description[2] == imp.PKG_DIRECTORY:
+ # recursively handle the remaining name parts
+ pathname = _get_modpkg_path(parts[1], [pathname])
+ else:
+ pathname = None
+ else:
+ # plain name
+ try:
+ 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:
+ pathname = None
+
+ return pathname
+
+
+def getFilesForName(name):
+ """ 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:
+ list.extend(getFilesForName(file))
+ return list
+
+ # try to find module or package
+ name = _get_modpkg_path(name)
+ if not name:
+ return []
+
+ if os.path.isdir(name):
+ # find all python files in directory
+ list = []
+ os.path.walk(name, _visit_pyfiles, list)
+ return list
+ elif os.path.exists(name):
+ # a single file
+ return [name]
+
+ return []
+
+
+class TokenEater:
+ def __init__(self, options):
+ self.__options = options
+ self.__messages = {}
+ self.__state = self.__waiting
+ self.__data = []
+ self.__lineno = -1
+ self.__freshmodule = 1
+ self.__curfile = None
+
+ def __call__(self, ttype, tstring, stup, etup, line):
+ # dispatch
+## import token
+## print >> sys.stderr, 'ttype:', token.tok_name[ttype], \
+## 'tstring:', tstring
+ self.__state(ttype, tstring, stup[0])
+
+ def __waiting(self, ttype, tstring, lineno):
+ opts = self.__options
+ # Do docstring extractions, if enabled
+ if opts.docstrings and not opts.nodocstrings.get(self.__curfile):
+ # module docstring?
+ if self.__freshmodule:
+ if ttype == tokenize.STRING:
+ self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
+ self.__freshmodule = 0
+ elif ttype not in (tokenize.COMMENT, tokenize.NL):
+ self.__freshmodule = 0
+ return
+ # class docstring?
+ if ttype == tokenize.NAME and tstring in ('class', 'def'):
+ self.__state = self.__suiteseen
+ return
+ if ttype == tokenize.NAME and tstring in opts.keywords:
+ self.__state = self.__keywordseen
+
+ def __suiteseen(self, ttype, tstring, lineno):
+ # ignore anything until we see the colon
+ if ttype == tokenize.OP and tstring == ':':
+ self.__state = self.__suitedocstring
+
+ def __suitedocstring(self, ttype, tstring, lineno):
+ # ignore any intervening noise
+ if ttype == tokenize.STRING:
+ self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
+ self.__state = self.__waiting
+ elif ttype not in (tokenize.NEWLINE, tokenize.INDENT,
+ tokenize.COMMENT):
+ # there was no class docstring
+ self.__state = self.__waiting
+
+ def __keywordseen(self, ttype, tstring, lineno):
+ if ttype == tokenize.OP and tstring == '(':
+ self.__data = []
+ self.__lineno = lineno
+ self.__state = self.__openseen
+ else:
+ self.__state = self.__waiting
+
+ 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
+ # of messages seen. Reset state for the next batch. If there
+ # were no strings inside _(), then just ignore this entry.
+ if self.__data:
+ self.__addentry(EMPTYSTRING.join(self.__data))
+ self.__state = self.__waiting
+ elif ttype == tokenize.STRING:
+ self.__data.append(safe_eval(tstring))
+ 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}
+ self.__state = self.__waiting
+
+ def __addentry(self, msg, lineno=None, isdocstring=0):
+ if lineno is None:
+ lineno = self.__lineno
+ if not msg in self.__options.toexclude:
+ entry = (self.__curfile, lineno)
+ self.__messages.setdefault(msg, {})[entry] = isdocstring
+
+ def set_filename(self, filename):
+ self.__curfile = filename
+ self.__freshmodule = 1
+
+ def write(self, fp):
+ options = self.__options
+ timestamp = time.ctime(time.time())
+ # 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__}
+ # Sort the entries. First sort each particular entry's keys, then
+ # sort all the entries by their first item.
+ reverse = {}
+ for k, v in self.__messages.items():
+ keys = v.keys()
+ keys.sort()
+ reverse.setdefault(tuple(keys), []).append((k, v))
+ rkeys = reverse.keys()
+ rkeys.sort()
+ for rkey in rkeys:
+ rentries = reverse[rkey]
+ rentries.sort()
+ for k, v in rentries:
+ isdocstring = 0
+ # If the entry was gleaned out of a docstring, then add a
+ # comment stating so. This is to aid translators who may wish
+ # to skip translating some unimportant docstrings.
+ if reduce(operator.__add__, v.values()):
+ isdocstring = 1
+ # k is the message string, v is a dictionary-set of (filename,
+ # lineno) tuples. We want to sort the entries in v first by
+ # file name and then by line number.
+ v = v.keys()
+ v.sort()
+ if not options.writelocations:
+ pass
+ # location comments are different b/w Solaris and GNU:
+ elif options.locationstyle == options.SOLARIS:
+ for filename, lineno in v:
+ d = {'filename': filename, 'lineno': lineno}
+ print >>fp, _(
+ '# File: %(filename)s, line: %(lineno)d') % d
+ elif options.locationstyle == options.GNU:
+ # fit as many locations on one line, as long as the
+ # resulting line length doesn't exceeds 'options.width'
+ locline = '#:'
+ for filename, lineno in v:
+ d = {'filename': filename, 'lineno': lineno}
+ s = _(' %(filename)s:%(lineno)d') % d
+ if len(locline) + len(s) <= options.width:
+ locline = locline + s
+ else:
+ print >> fp, locline
+ locline = "#:" + s
+ if len(locline) > 2:
+ print >> fp, locline
+ if isdocstring:
+ print >> fp, '#, docstring'
+ print >> fp, 'msgid', normalize(k)
+ print >> fp, 'msgstr ""\n'
+
+
+
+def main():
+ global default_keywords
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:],
+ 'ad:DEhk:Kno:p:S:Vvw:x:X:',
+ ['extract-all', 'default-domain=', 'escape', 'help',
+ 'keyword=', 'no-default-keywords',
+ 'add-location', 'no-location', 'output=', 'output-dir=',
+ 'style=', 'verbose', 'version', 'width=', 'exclude-file=',
+ 'docstrings', 'no-docstrings',
+ ])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ # for holding option values
+ class Options:
+ # constants
+ GNU = 1
+ SOLARIS = 2
+ # defaults
+ extractall = 0 # FIXME: currently this option has no effect at all.
+ escape = 0
+ keywords = []
+ outpath = ''
+ outfile = 'messages.pot'
+ writelocations = 1
+ locationstyle = GNU
+ verbose = 0
+ width = 78
+ excludefilename = ''
+ docstrings = 0
+ nodocstrings = {}
+
+ options = Options()
+ locations = {'gnu' : options.GNU,
+ 'solaris' : options.SOLARIS,
+ }
+
+ # parse options
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-a', '--extract-all'):
+ options.extractall = 1
+ elif opt in ('-d', '--default-domain'):
+ options.outfile = arg + '.pot'
+ elif opt in ('-E', '--escape'):
+ options.escape = 1
+ elif opt in ('-D', '--docstrings'):
+ options.docstrings = 1
+ elif opt in ('-k', '--keyword'):
+ options.keywords.append(arg)
+ elif opt in ('-K', '--no-default-keywords'):
+ default_keywords = []
+ elif opt in ('-n', '--add-location'):
+ options.writelocations = 1
+ elif opt in ('--no-location',):
+ options.writelocations = 0
+ elif opt in ('-S', '--style'):
+ options.locationstyle = locations.get(arg.lower())
+ if options.locationstyle is None:
+ usage(1, _('Invalid value for --style: %s') % arg)
+ elif opt in ('-o', '--output'):
+ options.outfile = arg
+ elif opt in ('-p', '--output-dir'):
+ options.outpath = arg
+ elif opt in ('-v', '--verbose'):
+ options.verbose = 1
+ elif opt in ('-V', '--version'):
+ print _('pygettext.py (xgettext for Python) %s') % __version__
+ sys.exit(0)
+ elif opt in ('-w', '--width'):
+ try:
+ options.width = int(arg)
+ except ValueError:
+ usage(1, _('--width argument must be an integer: %s') % arg)
+ elif opt in ('-x', '--exclude-file'):
+ options.excludefilename = arg
+ elif opt in ('-X', '--no-docstrings'):
+ fp = open(arg)
+ try:
+ while 1:
+ line = fp.readline()
+ if not line:
+ break
+ options.nodocstrings[line[:-1]] = 1
+ finally:
+ fp.close()
+
+ # calculate escapes
+ make_escapes(options.escape)
+
+ # calculate all keywords
+ options.keywords.extend(default_keywords)
+
+ # initialize list of strings to exclude
+ if options.excludefilename:
+ try:
+ fp = open(options.excludefilename)
+ options.toexclude = fp.readlines()
+ fp.close()
+ except IOError:
+ print >> sys.stderr, _(
+ "Can't read --exclude-file: %s") % options.excludefilename
+ sys.exit(1)
+ else:
+ options.toexclude = []
+
+ # resolve args to module lists
+ expanded = []
+ for arg in args:
+ if arg == '-':
+ expanded.append(arg)
+ else:
+ expanded.extend(getFilesForName(arg))
+ args = expanded
+
+ # slurp through all the files
+ eater = TokenEater(options)
+ for filename in args:
+ if filename == '-':
+ if options.verbose:
+ print _('Reading standard input')
+ fp = sys.stdin
+ closep = 0
+ else:
+ if options.verbose:
+ print _('Working on %s') % filename
+ fp = open(filename)
+ closep = 1
+ try:
+ eater.set_filename(filename)
+ try:
+ tokenize.tokenize(fp.readline, eater)
+ except tokenize.TokenError, e:
+ print >> sys.stderr, '%s: %s, line %d, column %d' % (
+ e[0], filename, e[1][0], e[1][1])
+ finally:
+ if closep:
+ fp.close()
+
+ # write the output
+ if options.outfile == '-':
+ fp = sys.stdout
+ closep = 0
+ else:
+ if options.outpath:
+ options.outfile = os.path.join(options.outpath, options.outfile)
+ fp = open(options.outfile, 'w')
+ closep = 1
+ try:
+ eater.write(fp)
+ finally:
+ if closep:
+ fp.close()
+
+
+if __name__ == '__main__':
+ main()
+ # some more test strings
+ _(u'a unicode string')
+ _('*** Seen unexpected token "%(token)s"' % {'token': 'test'}) # this one creates a warning
+ _('more' 'than' 'one' 'string')
+
More information about the Python-checkins
mailing list