diff --git a/roundup/admin.py b/roundup/admin.py
index 4521c812578f6c69cc7538f0a4f65ed0f5227578..1a4e241df277116f00424244eefa5658264f3a4c 100644 (file)
--- a/roundup/admin.py
+++ b/roundup/admin.py
# 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.
# 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.45 2003-03-21 04:02:12 richard Exp $
+#
+
+"""Administration commands for maintaining Roundup trackers.
+"""
+__docformat__ = 'restructuredtext'
-'''Administration commands for maintaining Roundup trackers.
-'''
+import csv, getopt, getpass, os, re, shutil, sys, UserDict, operator
-import sys, os, getpass, getopt, re, UserDict, shlex, shutil
-try:
- import csv
-except ImportError:
- csv = None
from roundup import date, hyperdb, roundupdb, init, password, token
from roundup import __version__ as roundup_version
import roundup.instance
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 _
from roundup.i18n import _
+from roundup.exceptions import UsageError
class CommandDict(UserDict.UserDict):
class CommandDict(UserDict.UserDict):
- '''Simple dictionary that lets us do lookups using partial keys.
+ """Simple dictionary that lets us do lookups using partial keys.
Original code submitted by Engelbert Gruber.
Original code submitted by Engelbert Gruber.
- '''
+ """
_marker = []
def get(self, key, default=_marker):
_marker = []
def get(self, key, default=_marker):
- if self.data.has_key(key):
+ if key in self.data:
return [(key, self.data[key])]
return [(key, self.data[key])]
- keylist = self.data.keys()
- keylist.sort()
+ keylist = sorted(self.data)
l = []
for ki in keylist:
if ki.startswith(key):
l.append((ki, self.data[ki]))
if not l and default is self._marker:
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
+ raise KeyError(key)
return l
return l
-class UsageError(ValueError):
- pass
-
class AdminTool:
class AdminTool:
- ''' A collection of methods used in maintaining Roundup trackers.
+ """ A collection of methods used in maintaining Roundup trackers.
Typically these methods are accessed through the roundup-admin
script. The main() method provided on this class gives the main
Typically these methods are accessed through the roundup-admin
script. The main() method provided on this class gives the main
given in the method docstring.
Additional help may be supplied by help_*() methods.
given in the method docstring.
Additional help may be supplied by help_*() methods.
- '''
+ """
def __init__(self):
self.commands = CommandDict()
def __init__(self):
self.commands = CommandDict()
- for k in AdminTool.__dict__.keys():
+ for k in AdminTool.__dict__:
if k[:3] == 'do_':
self.commands[k[3:]] = getattr(self, k)
self.help = {}
if k[:3] == 'do_':
self.commands[k[3:]] = getattr(self, k)
self.help = {}
- for k in AdminTool.__dict__.keys():
+ for k in AdminTool.__dict__:
if k[:5] == 'help_':
self.help[k[5:]] = getattr(self, k)
self.tracker_home = ''
self.db = None
if k[:5] == 'help_':
self.help[k[5:]] = getattr(self, k)
self.tracker_home = ''
self.db = None
+ self.db_uncommitted = False
def get_class(self, classname):
def get_class(self, classname):
- '''Get the class - raise an exception if it doesn't exist.
- '''
+ """Get the class - raise an exception if it doesn't exist.
+ """
try:
return self.db.getclass(classname)
except KeyError:
try:
return self.db.getclass(classname)
except KeyError:
- raise UsageError, _('no such class "%(classname)s"')%locals()
+ raise UsageError(_('no such class "%(classname)s"')%locals())
def props_from_args(self, args):
def props_from_args(self, args):
- ''' Produce a dictionary of prop: value from the args list.
+ """ Produce a dictionary of prop: value from the args list.
The args list is specified as ``prop=value prop=value ...``.
The args list is specified as ``prop=value prop=value ...``.
- '''
+ """
props = {}
for arg in args:
if arg.find('=') == -1:
props = {}
for arg in args:
if arg.find('=') == -1:
- raise UsageError, _('argument "%(arg)s" not propname=value'
- )%locals()
- try:
- key, value = arg.split('=')
- except ValueError:
- raise UsageError, _('argument "%(arg)s" not propname=value'
- )%locals()
+ 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:
if value:
props[key] = value
else:
return props
def usage(self, message=''):
return props
def usage(self, message=''):
- ''' Display a simple usage message.
- '''
+ """ Display a simple usage message.
+ """
if message:
if message:
- message = _('Problem: %(message)s)\n\n')%locals()
- print _('''%(message)sUsage: roundup-admin [options] <command> <arguments>
+ 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
Options:
-i instance home -- specify the issue tracker "home directory" to administer
-u -- the user[:password] to use for commands
- -c -- when outputting lists of data, just comma-separate them
+ -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
Help:
roundup-admin -h
roundup-admin help -- this help
roundup-admin help <command> -- command-specific help
roundup-admin help all -- all available help
-''')%locals()
+""")%locals()
self.help_commands()
def help_commands(self):
self.help_commands()
def help_commands(self):
- ''' List the commands available with their precis help.
- '''
+ """List the commands available with their help summary.
+ """
print _('Commands:'),
commands = ['']
print _('Commands:'),
commands = ['']
- for command in self.commands.values():
- h = command.__doc__.split('\n')[0]
+ for command in self.commands.itervalues():
+ h = _(command.__doc__).split('\n')[0]
commands.append(' '+h[7:])
commands.sort()
commands.append(' '+h[7:])
commands.sort()
- commands.append(_('Commands may be abbreviated as long as the abbreviation matches only one'))
- commands.append(_('command, e.g. l == li == lis == list.'))
+ 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+')):
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)
+ """ Produce an HTML command list.
+ """
+ commands = sorted(self.commands.itervalues(),
+ operator.attrgetter('__name__'))
for command in commands:
for command in commands:
- h = command.__doc__.split('\n')
+ h = _(command.__doc__).split('\n')
name = command.__name__[3:]
usage = h[0]
name = command.__name__[3:]
usage = h[0]
- print _('''
+ print """
<tr><td valign=top><strong>%(name)s</strong></td>
<td><tt>%(usage)s</tt><p>
<tr><td valign=top><strong>%(name)s</strong></td>
<td><tt>%(usage)s</tt><p>
-<pre>''')%locals()
+<pre>""" % locals()
indent = indent_re.match(h[3])
if indent: indent = len(indent.group(1))
for line in h[3:]:
indent = indent_re.match(h[3])
if indent: indent = len(indent.group(1))
for line in h[3:]:
print line[indent:]
else:
print line
print line[indent:]
else:
print line
- print _('</pre></td></tr>\n')
+ print '</pre></td></tr>\n'
def help_all(self):
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".
+ 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.
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.
+ . 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.
. 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.
+ . 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
When property values must contain spaces, just surround the value with
quotes, either ' or ". A single space may also be backslash-quoted. If a
-valuu must contain a quote character, it must be backslash-quoted or inside
+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)
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)
+ Roch\\'e Compaan (2 tokens: Roch'e Compaan)
address="1 2 3" (1 token: address=1 2 3)
address="1 2 3" (1 token: address=1 2 3)
- \\ (1 token: \)
- \n\r\t (1 token: a newline, carriage-return and tab)
+ \\\\ (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
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.
+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
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.
+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
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.
+command-line.
Date format examples:
"2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
Date format examples:
"2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
"." means "right now"
Command help:
"." means "right now"
Command help:
-''')
+""")
for name, command in self.commands.items():
print _('%s:')%name
for name, command in self.commands.items():
print _('%s:')%name
- print _(' '), command.__doc__
+ print ' ', _(command.__doc__)
def do_help(self, args, nl_re=re.compile('[\r\n]'),
indent_re=re.compile(r'^(\s+)\S+')):
def do_help(self, args, nl_re=re.compile('[\r\n]'),
indent_re=re.compile(r'^(\s+)\S+')):
- '''Usage: help topic
+ ''"""Usage: help topic
Give help about topic.
commands -- list commands
<command> -- help specific to a command
initopts -- init command options
all -- all available help
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'
if len(args)>0:
topic = args[0]
else:
topic = 'help'
-
+
# try help_ methods
# try help_ methods
- if self.help.has_key(topic):
+ if topic in self.help:
self.help[topic]()
return 0
self.help[topic]()
return 0
# display the help for each match, removing the docsring indent
for name, help in l:
# display the help for each match, removing the docsring indent
for name, help in l:
- lines = nl_re.split(help.__doc__)
+ lines = nl_re.split(_(help.__doc__))
print lines[0]
indent = indent_re.match(lines[1])
if indent: indent = len(indent.group(1))
print lines[0]
indent = indent_re.match(lines[1])
if indent: indent = len(indent.group(1))
print line
return 0
print line
return 0
+ def listTemplates(self):
+ """ List all the available templates.
+
+ Look in the following places, where the later rules take precedence:
+
+ 1. <roundup.admin.__file__>/../../share/roundup/templates/*
+ this is where they will be if we installed an egg via easy_install
+ 2. <prefix>/share/roundup/templates/*
+ this should be the standard place to find them when Roundup is
+ installed
+ 3. <roundup.admin.__file__>/../templates/*
+ this will be used if Roundup's run in the distro (aka. source)
+ directory
+ 4. <current working dir>/*
+ this is for when someone unpacks a 3rd-party template
+ 5. <current working dir>
+ this is for someone who "cd"s to the 3rd-party template dir
+ """
+ # OK, try <prefix>/share/roundup/templates
+ # and <egg-directory>/share/roundup/templates
+ # -- this module (roundup.admin) will be installed in something
+ # like:
+ # /usr/lib/python2.5/site-packages/roundup/admin.py (5 dirs up)
+ # c:\python25\lib\site-packages\roundup\admin.py (4 dirs up)
+ # /usr/lib/python2.5/site-packages/roundup-1.3.3-py2.5-egg/roundup/admin.py
+ # (2 dirs up)
+ #
+ # we're interested in where the directory containing "share" is
+ templates = {}
+ for N in 2, 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):
def help_initopts(self):
- import roundup.templates
- templates = roundup.templates.listTemplates()
+ templates = self.listTemplates()
print _('Templates:'), ', '.join(templates)
import roundup.backends
print _('Templates:'), ', '.join(templates)
import roundup.backends
- backends = roundup.backends.__all__
+ backends = roundup.backends.list_backends()
print _('Back ends:'), ', '.join(backends)
def do_install(self, tracker_home, args):
print _('Back ends:'), ', '.join(backends)
def do_install(self, tracker_home, args):
- '''Usage: install [template [backend [admin password]]]
+ ''"""Usage: install [template [backend [key=val[,key=val]]]]
Install a new Roundup tracker.
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 command will prompt for the tracker home directory
+ (if not supplied through TRACKER_HOME or the -i option).
+ The template and backend may be specified on the command-line
+ as arguments, in that order.
+
+ Command line arguments following the backend allows you 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
The initialise command must be called after this command in order
to initialise the tracker's database. You may edit the tracker's
the tracker's dbinit.py module init() function.
See also initopts help.
the tracker's dbinit.py module init() function.
See also initopts help.
- '''
+ """
if len(args) < 1:
if len(args) < 1:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
# make sure the tracker home can be created
# 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):
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()
+ 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 list(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
# select template
- import roundup.templates
- templates = roundup.templates.listTemplates()
+ templates = self.listTemplates()
template = len(args) > 1 and args[1] or ''
if template not in templates:
print _('Templates:'), ', '.join(templates)
template = len(args) > 1 and args[1] or ''
if template not in templates:
print _('Templates:'), ', '.join(templates)
# select hyperdb backend
import roundup.backends
# select hyperdb backend
import roundup.backends
- backends = roundup.backends.__all__
+ backends = roundup.backends.list_backends()
backend = len(args) > 2 and args[2] or ''
if backend not in backends:
print _('Back ends:'), ', '.join(backends)
backend = len(args) > 2 and args[2] or ''
if backend not in backends:
print _('Back ends:'), ', '.join(backends)
backend = 'anydbm'
# XXX perform a unit test based on the user's selections
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!
# install!
- init.install(tracker_home, template)
+ init.install(tracker_home, templates[template]['path'], settings=defns)
init.write_select_db(tracker_home, backend)
init.write_select_db(tracker_home, backend)
- print _('''
+ print _("""
+---------------------------------------------------------------------------
You should now edit the tracker configuration file:
You should now edit the tracker configuration file:
- %(config_file)s
- ... at a minimum, you must set MAILHOST, TRACKER_WEB, MAIL_DOMAIN and
- ADMIN_EMAIL.
-
- If you wish to modify the default schema, you should also edit the database
- initialisation 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 in need_set:
+ print " [%s]: %s" % (section, ", ".join(need_set[section]))
+
+ # note about schema modifications
+ print _("""
+ If you wish to modify the database schema,
+ you should also edit the schema file:
%(database_config_file)s
%(database_config_file)s
+ You may also change the database initialisation file:
+ %(database_init_file)s
... see the documentation on customizing for more information.
... see the documentation on customizing for more information.
-''')%{
- 'config_file': os.path.join(tracker_home, 'config.py'),
- 'database_config_file': os.path.join(tracker_home, 'dbinit.py')
+
+ 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
}
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):
def do_initialise(self, tracker_home, args):
- '''Usage: initialise [adminpw]
+ ''"""Usage: initialise [adminpw]
Initialise a new Roundup tracker.
The administrator details will be set at this step.
Execute the tracker's initialisation function dbinit.init()
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]
# password
if len(args) > 1:
adminpw = args[1]
# make sure the tracker home is installed
if not os.path.exists(tracker_home):
# make sure the tracker home is installed
if not os.path.exists(tracker_home):
- raise UsageError, _('Instance home does not exist')%locals()
+ raise UsageError(_('Instance home does not exist')%locals())
try:
tracker = roundup.instance.open(tracker_home)
except roundup.instance.TrackerError:
try:
tracker = roundup.instance.open(tracker_home)
except roundup.instance.TrackerError:
- raise UsageError, _('Instance has not been installed')%locals()
+ raise UsageError(_('Instance has not been installed')%locals())
# is there already a database?
# is there already a database?
- try:
- db_exists = tracker.select_db.Database.exists(tracker.config)
- except AttributeError:
- # TODO: move this code to exists() static method in every backend
- db_exists = os.path.exists(os.path.join(tracker_home, 'db'))
- if db_exists:
- print _('WARNING: The database is already initialised!')
- print _('If you re-initialise it, you will lose all the data!')
- ok = raw_input(_('Erase it? Y/[N]: ')).strip()
- if ok.lower() != 'y':
+ 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
return 0
- # Get a database backend in use by tracker
- try:
- # nuke it
- tracker.select_db.Database.nuke(tracker.config)
- except AttributeError:
- # TODO: move this code to nuke() static method in every backend
- shutil.rmtree(os.path.join(tracker_home, 'db'))
+ backend = tracker.get_backend_name()
+
+ # nuke it
+ tracker.nuke()
+
+ # re-write the backend select file
+ init.write_select_db(tracker_home, backend, tracker.config.DATABASE)
# GO
# GO
- init.initialise(tracker_home, adminpw)
+ tracker.init(password.Password(adminpw, config=tracker.config))
return 0
def do_get(self, args):
return 0
def do_get(self, args):
- '''Usage: get property designator[,designator]*
+ ''"""Usage: get property designator[,designator]*
Get the given property of one or more designator(s).
Get the given property of one or more designator(s).
- Retrieves the property value of the nodes specified by the designators.
- '''
+ A designator is a classname and a nodeid concatenated,
+ eg. bug1, user10, ...
+
+ Retrieves the property value of the nodes specified
+ by the designators.
+ """
if len(args) < 2:
if len(args) < 2:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
propname = args[0]
designators = args[1].split(',')
l = []
propname = args[0]
designators = args[1].split(',')
l = []
try:
classname, nodeid = hyperdb.splitDesignator(designator)
except hyperdb.DesignatorError, message:
try:
classname, nodeid = hyperdb.splitDesignator(designator)
except hyperdb.DesignatorError, message:
- raise UsageError, message
+ raise UsageError(message)
# get the class
cl = self.get_class(classname)
try:
# get the class
cl = self.get_class(classname)
try:
- if self.comma_sep:
- l.append(cl.get(nodeid, propname))
+ 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:
else:
- print cl.get(nodeid, propname)
+ 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:
except IndexError:
- raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+ raise UsageError(_('no such %(classname)s node '
+ '"%(nodeid)s"')%locals())
except KeyError:
except KeyError:
- raise UsageError, _('no such %(classname)s property '
- '"%(propname)s"')%locals()
- if self.comma_sep:
- print ','.join(l)
+ raise UsageError(_('no such %(classname)s property '
+ '"%(propname)s"')%locals())
+ if self.separator:
+ print self.separator.join(l)
+
return 0
return 0
- def do_set(self, args, pwre = re.compile(r'{(\w+)}(.+)')):
- '''Usage: set [items] property=value property=value ...
+ def do_set(self, args):
+ ''"""Usage: set items property=value property=value ...
Set the given properties of one or more items(s).
Set the given properties of one or more items(s).
- The items may be specified as a class or as a comma-separeted
+ The items are specified as a class or as a comma-separated
list of item designators (ie "designator[,designator,...]").
list of item designators (ie "designator[,designator,...]").
+ A designator is a classname and a nodeid concatenated,
+ eg. bug1, user10, ...
+
This command sets the properties to the values for all designators
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").
- '''
+ 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:
if len(args) < 2:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
from roundup import hyperdb
designators = args[0].split(',')
from roundup import hyperdb
designators = args[0].split(',')
try:
designators = [hyperdb.splitDesignator(x) for x in designators]
except hyperdb.DesignatorError, message:
try:
designators = [hyperdb.splitDesignator(x) for x in designators]
except hyperdb.DesignatorError, message:
- raise UsageError, message
+ raise UsageError(message)
# get the props from the args
props = self.props_from_args(args[1:])
# get the props from the args
props = self.props_from_args(args[1:])
properties = cl.getprops()
for key, value in props.items():
properties = cl.getprops()
for key, value in props.items():
- proptype = properties[key]
- if isinstance(proptype, hyperdb.Multilink):
- if value is None:
- props[key] = []
- else:
- props[key] = value.split(',')
- elif value is None:
- continue
- elif isinstance(proptype, hyperdb.String):
- continue
- elif isinstance(proptype, hyperdb.Password):
- m = pwre.match(value)
- if m:
- # password is being given to us encrypted
- p = password.Password()
- p.scheme = m.group(1)
- p.password = m.group(2)
- props[key] = p
- else:
- props[key] = password.Password(value)
- elif isinstance(proptype, hyperdb.Date):
- try:
- props[key] = date.Date(value)
- except ValueError, message:
- raise UsageError, '"%s": %s'%(value, message)
- elif isinstance(proptype, hyperdb.Interval):
- try:
- props[key] = date.Interval(value)
- except ValueError, message:
- raise UsageError, '"%s": %s'%(value, message)
- elif isinstance(proptype, hyperdb.Link):
- props[key] = value
- elif isinstance(proptype, hyperdb.Boolean):
- props[key] = value.lower() in ('yes', 'true', 'on', '1')
- elif isinstance(proptype, hyperdb.Number):
- props[key] = float(value)
+ try:
+ props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid,
+ key, value)
+ except hyperdb.HyperdbValueError, message:
+ raise UsageError(message)
# try the set
try:
# try the set
try:
- apply(cl.set, (itemid, ), props)
+ cl.set(itemid, **props)
except (TypeError, IndexError, ValueError), message:
import traceback; traceback.print_exc()
except (TypeError, IndexError, ValueError), message:
import traceback; traceback.print_exc()
- raise UsageError, message
+ raise UsageError(message)
+ self.db_uncommitted = True
return 0
def do_find(self, args):
return 0
def do_find(self, args):
- '''Usage: find classname propname=value ...
+ ''"""Usage: find classname propname=value ...
Find the nodes of the given class with a given link property value.
Find the nodes of the given class with a given link property value.
- 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.
- '''
+ 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:
if len(args) < 1:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
classname = args[0]
# get the class
cl = self.get_class(classname)
classname = args[0]
# get the class
cl = self.get_class(classname)
# handle the propname=value argument
props = self.props_from_args(args[1:])
# handle the propname=value argument
props = self.props_from_args(args[1:])
- # if the value isn't a number, look up the linked class to get the
- # number
- for propname, value in props.items():
- num_re = re.compile('^\d+$')
- if not num_re.match(value):
- # get the property
- try:
- property = cl.properties[propname]
- except KeyError:
- raise UsageError, _('%(classname)s has no property '
- '"%(propname)s"')%locals()
-
- # make sure it's a link
- if (not isinstance(property, hyperdb.Link) and not
- isinstance(property, hyperdb.Multilink)):
- raise UsageError, _('You may only "find" link properties')
-
- # get the linked-to class and look up the key property
- link_class = self.db.getclass(property.classname)
- try:
- props[propname] = link_class.lookup(value)
- except TypeError:
- raise UsageError, _('%(classname)s has no key property"')%{
- 'classname': link_class.classname}
+ # convert the user-input value to a value used for find()
+ for propname, value in props.iteritems():
+ 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
+ # now do the find
try:
try:
- if self.comma_sep:
- print ','.join(apply(cl.find, (), props))
+ id = []
+ designator = []
+ if self.separator:
+ if self.print_designator:
+ id = cl.find(**props)
+ for i in id:
+ designator.append(classname + i)
+ print self.separator.join(designator)
+ else:
+ print self.separator.join(cl.find(**props))
+
else:
else:
- print apply(cl.find, (), props)
+ if self.print_designator:
+ id = cl.find(**props)
+ for i in id:
+ designator.append(classname + i)
+ print designator
+ else:
+ print cl.find(**props)
except KeyError:
except KeyError:
- raise UsageError, _('%(classname)s has no property '
- '"%(propname)s"')%locals()
+ raise UsageError(_('%(classname)s has no property '
+ '"%(propname)s"')%locals())
except (ValueError, TypeError), message:
except (ValueError, TypeError), message:
- raise UsageError, message
+ raise UsageError(message)
return 0
def do_specification(self, args):
return 0
def do_specification(self, args):
- '''Usage: specification classname
+ ''"""Usage: specification classname
Show the properties for a classname.
This lists the properties for a given class.
Show the properties for a classname.
This lists the properties for a given class.
- '''
+ """
if len(args) < 1:
if len(args) < 1:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
classname = args[0]
# get the class
cl = self.get_class(classname)
# get the key property
keyprop = cl.getkey()
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():
+ for key in cl.properties:
+ value = cl.properties[key]
if keyprop == key:
print _('%(key)s: %(value)s (key property)')%locals()
else:
print _('%(key)s: %(value)s')%locals()
def do_display(self, args):
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
- Show the property values for the given node.
+ ''"""Usage: display designator[,designator]*
+ Show the property values for the given node(s).
+
+ A designator is a classname and a nodeid concatenated,
+ eg. bug1, user10, ...
This lists the properties and their associated values for the given
node.
This lists the properties and their associated values for the given
node.
- '''
+ """
if len(args) < 1:
if len(args) < 1:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
# decode the node designator
# decode the node designator
- try:
- classname, nodeid = hyperdb.splitDesignator(args[0])
- except hyperdb.DesignatorError, message:
- raise UsageError, message
+ 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)
+ # get the class
+ cl = self.get_class(classname)
- # display the values
- for key in cl.properties.keys():
- value = cl.get(nodeid, key)
- print _('%(key)s: %(value)s')%locals()
+ # display the values
+ keys = sorted(cl.properties)
+ for key in keys:
+ value = cl.get(nodeid, key)
+ print _('%(key)s: %(value)s')%locals()
- def do_create(self, args, pwre = re.compile(r'{(\w+)}(.+)')):
- '''Usage: create classname property=value ...
+ def do_create(self, args):
+ ''"""Usage: create classname property=value ...
Create a new entry of a given class.
This creates a new entry of the given class using the property
name=value arguments provided on the command line after the "create"
command.
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:
if len(args) < 1:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
from roundup import hyperdb
classname = args[0]
from roundup import hyperdb
classname = args[0]
properties = cl.getprops(protected = 0)
if len(args) == 1:
# ask for the properties
properties = cl.getprops(protected = 0)
if len(args) == 1:
# ask for the properties
- for key, value in properties.items():
+ for key in properties:
if key == 'id': continue
if key == 'id': continue
+ value = properties[key]
name = value.__class__.__name__
if isinstance(value , hyperdb.Password):
again = None
name = value.__class__.__name__
if isinstance(value , hyperdb.Password):
again = None
props = self.props_from_args(args[1:])
# convert types
props = self.props_from_args(args[1:])
# convert types
- for propname, value in props.items():
- # get the property
+ for propname in props:
try:
try:
- proptype = properties[propname]
- except KeyError:
- raise UsageError, _('%(classname)s has no property '
- '"%(propname)s"')%locals()
-
- if isinstance(proptype, hyperdb.Date):
- try:
- props[propname] = date.Date(value)
- except ValueError, message:
- raise UsageError, _('"%(value)s": %(message)s')%locals()
- elif isinstance(proptype, hyperdb.Interval):
- try:
- props[propname] = date.Interval(value)
- except ValueError, message:
- raise UsageError, _('"%(value)s": %(message)s')%locals()
- elif isinstance(proptype, hyperdb.Password):
- m = pwre.match(value)
- if m:
- # password is being given to us encrypted
- p = password.Password()
- p.scheme = m.group(1)
- p.password = m.group(2)
- props[propname] = p
- else:
- props[propname] = password.Password(value)
- elif isinstance(proptype, hyperdb.Multilink):
- props[propname] = value.split(',')
- elif isinstance(proptype, hyperdb.Boolean):
- props[propname] = value.lower() in ('yes', 'true', 'on', '1')
- elif isinstance(proptype, hyperdb.Number):
- props[propname] = float(value)
+ props[propname] = hyperdb.rawToHyperdb(self.db, cl, None,
+ propname, props[propname])
+ except hyperdb.HyperdbValueError, message:
+ raise UsageError(message)
# check for the key property
propname = cl.getkey()
# 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()
+ if propname and propname not in props:
+ raise UsageError(_('you must provide the "%(propname)s" '
+ 'property.')%locals())
# do the actual create
try:
# do the actual create
try:
- print apply(cl.create, (), props)
+ print cl.create(**props)
except (TypeError, IndexError, ValueError), message:
except (TypeError, IndexError, ValueError), message:
- raise UsageError, message
+ raise UsageError(message)
+ self.db_uncommitted = True
return 0
def do_list(self, args):
return 0
def do_list(self, args):
- '''Usage: list classname [property]
+ ''"""Usage: list classname [property]
List the instances of a class.
Lists all instances of the given class. If the property is not
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.
- '''
+ 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:
if len(args) < 1:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
classname = args[0]
classname = args[0]
-
+
# get the class
cl = self.get_class(classname)
# get the class
cl = self.get_class(classname)
else:
propname = cl.labelprop()
else:
propname = cl.labelprop()
- if self.comma_sep:
- print ','.join(cl.list())
+ 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:
else:
for nodeid in cl.list():
try:
value = cl.get(nodeid, propname)
except KeyError:
- raise UsageError, _('%(classname)s has no property '
- '"%(propname)s"')%locals()
+ raise UsageError(_('%(classname)s has no property '
+ '"%(propname)s"')%locals())
print _('%(nodeid)4s: %(value)s')%locals()
return 0
def do_table(self, args):
print _('%(nodeid)4s: %(value)s')%locals()
return 0
def do_table(self, args):
- '''Usage: table classname [property[,property]*]
+ ''"""Usage: table classname [property[,property]*]
List the instances of a class in tabular form.
Lists all instances of the given class. If the properties are not
List the instances of a class in tabular form.
Lists all instances of the given class. If the properties are not
- specified, all properties are displayed. By default, the column widths
- are the width of the property names. The width may be explicitly defined
- by defining the property as "name:width". For example::
+ 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
roundup> table priority id,name:10
Id Name
- 1 fatal-bug
- 2 bug
- 3 usability
- 4 feature
- '''
+ 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:
if len(args) < 1:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
classname = args[0]
# get the class
classname = args[0]
# get the class
try:
propname, width = spec.split(':')
except (ValueError, TypeError):
try:
propname, width = spec.split(':')
except (ValueError, TypeError):
- raise UsageError, _('"%(spec)s" not name:width')%locals()
+ raise UsageError(_('"%(spec)s" not '
+ 'name:width')%locals())
else:
propname = spec
else:
propname = spec
- if not all_props.has_key(propname):
- raise UsageError, _('%(classname)s has no property '
- '"%(propname)s"')%locals()
+ if propname not in all_props:
+ raise UsageError(_('%(classname)s has no property '
+ '"%(propname)s"')%locals())
else:
else:
- prop_names = cl.getprops().keys()
+ prop_names = cl.getprops()
# now figure column widths
props = []
for spec in prop_names:
if ':' in spec:
name, width = spec.split(':')
# now figure column widths
props = []
for spec in prop_names:
if ':' in spec:
name, width = spec.split(':')
- props.append((name, int(width)))
+ if width == '':
+ props.append((name, len(spec)))
+ else:
+ props.append((name, int(width)))
else:
else:
- props.append((spec, len(spec)))
+ # 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])
# now display the heading
print ' '.join([name.capitalize().ljust(width) for name,width in props])
return 0
def do_history(self, args):
return 0
def do_history(self, args):
- '''Usage: history designator
+ ''"""Usage: history designator
Show the history entries of a designator.
Show the history entries of a designator.
+ A designator is a classname and a nodeid concatenated,
+ eg. bug1, user10, ...
+
Lists the journal entries for the node identified by the designator.
Lists the journal entries for the node identified by the designator.
- '''
+ """
if len(args) < 1:
if len(args) < 1:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
try:
classname, nodeid = hyperdb.splitDesignator(args[0])
except hyperdb.DesignatorError, message:
try:
classname, nodeid = hyperdb.splitDesignator(args[0])
except hyperdb.DesignatorError, message:
- raise UsageError, message
+ raise UsageError(message)
try:
print self.db.getclass(classname).history(nodeid)
except KeyError:
try:
print self.db.getclass(classname).history(nodeid)
except KeyError:
- raise UsageError, _('no such class "%(classname)s"')%locals()
+ raise UsageError(_('no such class "%(classname)s"')%locals())
except IndexError:
except IndexError:
- raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+ raise UsageError(_('no such %(classname)s node '
+ '"%(nodeid)s"')%locals())
return 0
def do_commit(self, args):
return 0
def do_commit(self, args):
- '''Usage: commit
- Commit all changes made to the database.
+ ''"""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
The changes made during an interactive session are not
automatically written to the database - they must be committed
One-off commands on the command-line are automatically committed if
they are successful.
One-off commands on the command-line are automatically committed if
they are successful.
- '''
+ """
self.db.commit()
self.db.commit()
+ self.db_uncommitted = False
return 0
def do_rollback(self, args):
return 0
def do_rollback(self, args):
- '''Usage: rollback
+ ''"""Usage: rollback
Undo all changes that are pending commit to the database.
The changes made during an interactive session are not
automatically written to the database - they must be committed
manually. This command undoes all those changes, so a commit
immediately after would make no changes to the database.
Undo all changes that are pending commit to the database.
The changes made during an interactive session are not
automatically written to the database - they must be committed
manually. This command undoes all those changes, so a commit
immediately after would make no changes to the database.
- '''
+ """
self.db.rollback()
self.db.rollback()
+ self.db_uncommitted = False
return 0
def do_retire(self, args):
return 0
def do_retire(self, args):
- '''Usage: retire designator[,designator]*
+ ''"""Usage: retire designator[,designator]*
Retire the node specified by 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.
- '''
+ A designator is a classname and a nodeid concatenated,
+ eg. bug1, user10, ...
+
+ 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:
if len(args) < 1:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
designators = args[0].split(',')
for designator in designators:
try:
classname, nodeid = hyperdb.splitDesignator(designator)
except hyperdb.DesignatorError, message:
designators = args[0].split(',')
for designator in designators:
try:
classname, nodeid = hyperdb.splitDesignator(designator)
except hyperdb.DesignatorError, message:
- raise UsageError, message
+ raise UsageError(message)
try:
self.db.getclass(classname).retire(nodeid)
except KeyError:
try:
self.db.getclass(classname).retire(nodeid)
except KeyError:
- raise UsageError, _('no such class "%(classname)s"')%locals()
+ raise UsageError(_('no such class "%(classname)s"')%locals())
except IndexError:
except IndexError:
- raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+ raise UsageError(_('no such %(classname)s node '
+ '"%(nodeid)s"')%locals())
+ self.db_uncommitted = True
return 0
def do_restore(self, args):
return 0
def do_restore(self, args):
- '''Usage: restore designator[,designator]*
+ ''"""Usage: restore designator[,designator]*
Restore the retired node specified by designator.
Restore the retired node specified by designator.
+ A designator is a classname and a nodeid concatenated,
+ eg. bug1, user10, ...
+
The given nodes will become available for users again.
The given nodes will become available for users again.
- '''
+ """
if len(args) < 1:
if len(args) < 1:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
designators = args[0].split(',')
for designator in designators:
try:
classname, nodeid = hyperdb.splitDesignator(designator)
except hyperdb.DesignatorError, message:
designators = args[0].split(',')
for designator in designators:
try:
classname, nodeid = hyperdb.splitDesignator(designator)
except hyperdb.DesignatorError, message:
- raise UsageError, message
+ raise UsageError(message)
try:
self.db.getclass(classname).restore(nodeid)
except KeyError:
try:
self.db.getclass(classname).restore(nodeid)
except KeyError:
- raise UsageError, _('no such class "%(classname)s"')%locals()
+ raise UsageError(_('no such class "%(classname)s"')%locals())
except IndexError:
except IndexError:
- raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+ raise UsageError(_('no such %(classname)s node '
+ '"%(nodeid)s"')%locals())
+ self.db_uncommitted = True
return 0
return 0
- def do_export(self, args):
- '''Usage: export [class[,class]] export_dir
+ def do_export(self, args, export_files=True):
+ ''"""Usage: export [[-]class[,class]] export_dir
Export the database to colon-separated-value files.
Export the database to colon-separated-value files.
+ To exclude the files (e.g. for the msg or file class),
+ use the exporttables command.
+
+ Optionally limit the export to just the named classes
+ or exclude the named classes, if the 1st argument starts with '-'.
This action exports the current data from the database into
colon-separated-value files that are placed in the nominated
This action exports the current data from the database into
colon-separated-value files that are placed in the nominated
- destination directory. The journals are not exported.
- '''
- # we need the CSV module
- if csv is None:
- raise UsageError, \
- _('Sorry, you need the csv module to use this function.\n'
- 'Get it from: http://www.object-craft.com.au/projects/csv/')
-
+ destination directory.
+ """
# grab the directory to export to
if len(args) < 1:
# grab the directory to export to
if len(args) < 1:
- raise UsageError, _('Not enough arguments supplied')
+ raise UsageError(_('Not enough arguments supplied'))
+
dir = args[-1]
# get the list of classes to export
if len(args) == 2:
dir = args[-1]
# get the list of classes to export
if len(args) == 2:
- classes = args[0].split(',')
+ if args[0].startswith('-'):
+ classes = [ c for c in self.db.classes
+ if not c in args[0][1:].split(',') ]
+ else:
+ classes = args[0].split(',')
else:
else:
- classes = self.db.classes.keys()
+ classes = self.db.classes
- # use the csv parser if we can - it's faster
- p = csv.parser(field_sep=':')
+ class colon_separated(csv.excel):
+ delimiter = ':'
+
+ # make sure target dir exists
+ if not os.path.exists(dir):
+ os.makedirs(dir)
+
+ # maximum csv field length exceeding configured size?
+ max_len = self.db.config.CSV_FIELD_SIZE
# do all the classes specified
for classname in classes:
cl = self.get_class(classname)
# do all the classes specified
for classname in classes:
cl = self.get_class(classname)
- f = open(os.path.join(dir, classname+'.csv'), 'w')
- properties = cl.getprops()
- propnames = properties.keys()
- propnames.sort()
- l = propnames[:]
- l.append('is retired')
- print >> f, p.join(l)
- # all nodes for this class (not using list() 'cos it doesn't
- # include retired nodes)
+ if not export_files and hasattr(cl, 'export_files'):
+ sys.stdout.write('Exporting %s WITHOUT the files\r\n'%
+ classname)
+
+ f = open(os.path.join(dir, classname+'.csv'), 'wb')
+ writer = csv.writer(f, colon_separated)
- for nodeid in self.db.getclass(classname).getnodeids():
- # get the regular props
- print >>f, p.join(cl.export_list(propnames, nodeid))
+ 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():
+ if self.verbose:
+ sys.stdout.write('\rExporting %s - %s'%(classname, nodeid))
+ sys.stdout.flush()
+ node = cl.getnode(nodeid)
+ exp = cl.export_list(propnames, nodeid)
+ lensum = sum ([len (repr(node[p])) for p in propnames])
+ # for a safe upper bound of field length we add
+ # difference between CSV len and sum of all field lengths
+ d = sum ([len(x) for x in exp]) - lensum
+ assert (d > 0)
+ for p in propnames:
+ ll = len(repr(node[p])) + d
+ if ll > max_len:
+ max_len = ll
+ writer.writerow(exp)
+ if export_files and hasattr(cl, 'export_files'):
+ cl.export_files(dir, nodeid)
# close this file
f.close()
# close this file
f.close()
+
+ # export the journals
+ jf = open(os.path.join(dir, classname+'-journals.csv'), 'wb')
+ if self.verbose:
+ sys.stdout.write("\nExporting Journal for %s\n" % classname)
+ sys.stdout.flush()
+ journals = csv.writer(jf, colon_separated)
+ for row in cl.export_journals():
+ journals.writerow(row)
+ jf.close()
+ if max_len > self.db.config.CSV_FIELD_SIZE:
+ print >> sys.stderr, \
+ "Warning: config csv_field_size should be at least %s"%max_len
return 0
return 0
+ def do_exporttables(self, args):
+ ''"""Usage: exporttables [[-]class[,class]] export_dir
+ Export the database to colon-separated-value files, excluding the
+ files below $TRACKER_HOME/db/files/ (which can be archived separately).
+ To include the files, use the export command.
+
+ Optionally limit the export to just the named classes
+ or exclude the named classes, if the 1st argument starts with '-'.
+
+ This action exports the current data from the database into
+ colon-separated-value files that are placed in the nominated
+ destination directory.
+ """
+ return self.do_export(args, export_files=False)
+
def do_import(self, args):
def do_import(self, args):
- '''Usage: import import_dir
- Import a database from the directory containing CSV files, one per
- class to import.
+ ''"""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:
- The files must define the same properties as the class (including having
- a "header" line with those property names.)
+ <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 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.)
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:
if len(args) < 1:
- raise UsageError, _('Not enough arguments supplied')
- if csv is None:
- raise UsageError, \
- _('Sorry, you need the csv module to use this function.\n'
- 'Get it from: http://www.object-craft.com.au/projects/csv/')
-
+ raise UsageError(_('Not enough arguments supplied'))
from roundup import hyperdb
from roundup import hyperdb
- for file in os.listdir(args[0]):
- # we only care about CSV files
- if not file.endswith('.csv'):
- continue
+ if hasattr (csv, 'field_size_limit'):
+ csv.field_size_limit(self.db.config.CSV_FIELD_SIZE)
- f = open(os.path.join(args[0], file))
+ # directory to import from
+ dir = args[0]
- # get the classname
- classname = os.path.splitext(file)[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
- # ensure that the properties and the CSV file headings match
cl = self.get_class(classname)
cl = self.get_class(classname)
- p = csv.parser(field_sep=':')
- file_props = p.parse(f.readline())
-
-# XXX we don't _really_ need to do this...
-# properties = cl.getprops()
-# propnames = properties.keys()
-# propnames.sort()
-# m = file_props[:]
-# m.sort()
-# if m != propnames:
-# raise UsageError, _('Import file doesn\'t define the same '
-# 'properties as "%(arg0)s".')%{'arg0': args[0]}
- # loop through the file and create a node for each entry
+ # 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
maxid = 1
- while 1:
- line = f.readline()
- if not line: break
-
- # parse lines until we get a complete entry
- while 1:
- l = p.parse(line)
- if l: break
- line = f.readline()
- if not line:
- raise ValueError, "Unexpected EOF during CSV parse"
+ # 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
+
+ if self.verbose:
+ sys.stdout.write('\rImporting %s - %s'%(classname, n))
+ sys.stdout.flush()
# do the import and figure the current highest nodeid
# do the import and figure the current highest nodeid
- maxid = max(maxid, int(cl.import_list(file_props, l)))
+ nodeid = cl.import_list(file_props, r)
+ if hasattr(cl, 'import_files'):
+ cl.import_files(dir, nodeid)
+ maxid = max(maxid, int(nodeid))
+
+ # (print to sys.stdout here to allow tests to squash it .. ugh)
+ print >> sys.stdout
- print 'setting', classname, maxid+1
+ 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()
+
+ # (print to sys.stdout here to allow tests to squash it .. ugh)
+ print >> sys.stdout, 'setting', classname, maxid+1
+
+ # set the id counter
self.db.setid(classname, str(maxid+1))
self.db.setid(classname, str(maxid+1))
+
+ self.db_uncommitted = True
return 0
def do_pack(self, args):
return 0
def do_pack(self, args):
- '''Usage: pack period | date
+ ''"""Usage: pack period | date
-Remove journal entries older than a period of time specified or
-before a certain date.
+ 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.
+ 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
+ "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'))
-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]
# are we dealing with a period or a date
value = args[0]
- date_re = re.compile(r'''
+ date_re = re.compile(r"""
(?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
(?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
(?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
(?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
- ''', re.VERBOSE)
+ """, re.VERBOSE)
m = date_re.match(value)
if not m:
m = date_re.match(value)
if not m:
- raise ValueError, _('Invalid format')
+ 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)
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)
+ self.db_uncommitted = True
return 0
return 0
- def do_reindex(self, args):
- '''Usage: reindex
+ def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')):
+ ''"""Usage: reindex [classname|designator]*
Re-generate a tracker's search indexes.
Re-generate a tracker's search indexes.
- This will re-generate the search indexes for a tracker. This will
- typically happen automatically.
- '''
- self.db.indexer.force_reindex()
- self.db.reindex()
+ 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):
return 0
def do_security(self, args):
- '''Usage: security [Role name]
+ ''"""Usage: security [Role name]
Display the Permissions available to one or all Roles.
Display the Permissions available to one or all Roles.
- '''
+ """
if len(args) == 1:
role = args[0]
try:
if len(args) == 1:
role = args[0]
try:
print _('No such Role "%(role)s"')%locals()
return 1
else:
print _('No such Role "%(role)s"')%locals()
return 1
else:
- roles = self.db.security.role.items()
+ roles = list(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()
role = self.db.config.NEW_WEB_USER_ROLES
if ',' in role:
print _('New Web users get the Roles "%(role)s"')%locals()
for rolename, role in roles:
print _('Role "%(name)s":')%role.__dict__
for permission in role.permissions:
for rolename, role in roles:
print _('Role "%(name)s":')%role.__dict__
for permission in role.permissions:
+ d = permission.__dict__
if permission.klass:
if permission.klass:
- print _(' %(description)s (%(name)s for "%(klass)s" '
- 'only)')%permission.__dict__
+ 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:
else:
- print _(' %(description)s (%(name)s)')%permission.__dict__
+ print _(' %(description)s (%(name)s)')%d
+ return 0
+
+
+ def do_migrate(self, args):
+ ''"""Usage: migrate
+ Update a tracker's database to be compatible with the Roundup
+ codebase.
+
+ You should run the "migrate" command for your tracker once you've
+ installed the latest codebase.
+
+ Do this before you use the web, command-line or mail interface and
+ before any users access the tracker.
+
+ This command will respond with either "Tracker updated" (if you've
+ not previously run it on an RDBMS backend) or "No migration action
+ required" (if you have run it, or have used another interface to the
+ tracker, or possibly because you are using anydbm).
+
+ It's safe to run this even if it's not required, so just get into
+ the habit.
+ """
+ if getattr(self.db, 'db_version_updated'):
+ print _('Tracker updated')
+ self.db_uncommitted = True
+ else:
+ print _('No migration action required')
return 0
def run_command(self, args):
return 0
def run_command(self, args):
- '''Run a single command
- '''
+ """Run a single command
+ """
command = args[0]
# handle help now
command = args[0]
# handle help now
self.help_commands()
self.help_all()
return 0
self.help_commands()
self.help_all()
return 0
+ if command == 'config':
+ self.do_config(args[1:])
+ return 0
# figure what the command is
try:
# figure what the command is
try:
return ret
def interactive(self):
return ret
def interactive(self):
- '''Run in an interactive mode
- '''
- print _('Roundup %s ready for input.'%roundup_version)
- print _('Type "help" for help.')
+ """Run in an interactive mode
+ """
+ print _('Roundup %s ready for input.\nType "help" for help.'
+ % roundup_version)
try:
import readline
except ImportError:
try:
import readline
except ImportError:
self.run_command(args)
# exit.. check for transactions
self.run_command(args)
# exit.. check for transactions
- if self.db and self.db.transactions:
+ if self.db and self.db_uncommitted:
commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
if commit and commit[0].lower() == 'y':
self.db.commit()
commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
if commit and commit[0].lower() == 'y':
self.db.commit()
def main(self):
try:
def main(self):
try:
- opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
+ opts, args = getopt.getopt(sys.argv[1:], 'i:u:hcdsS:vV')
except getopt.GetoptError, e:
self.usage(str(e))
return 1
except getopt.GetoptError, e:
self.usage(str(e))
return 1
self.tracker_home = os.environ.get('TRACKER_HOME', '')
# TODO: reinstate the user/password stuff (-u arg too)
name = password = ''
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'):
+ if 'ROUNDUP_LOGIN' in os.environ:
l = os.environ['ROUNDUP_LOGIN'].split(':')
name = l[0]
if len(l) > 1:
password = l[1]
l = os.environ['ROUNDUP_LOGIN'].split(':')
name = l[0]
if len(l) > 1:
password = l[1]
- self.comma_sep = 0
+ self.separator = None
+ self.print_designator = 0
+ self.verbose = 0
for opt, arg in opts:
if opt == '-h':
self.usage()
return 0
for opt, arg in opts:
if opt == '-h':
self.usage()
return 0
- if opt == '-i':
+ 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
self.tracker_home = arg
- if opt == '-c':
- self.comma_sep = 1
+ 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
# if no command - go interactive
# wrap in a try/finally so we always close off the db
tool = AdminTool()
sys.exit(tool.main())
tool = AdminTool()
sys.exit(tool.main())
-# vim: set filetype=python ts=4 sw=4 et si
+# vim: set filetype=python sts=4 sw=4 et si :