Code

added some implementation notes
[roundup.git] / roundup-admin
1 #! /usr/bin/env python
2 #
3 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
4 # This module is free software, and you may redistribute it and/or modify
5 # under the same terms as Python, so long as this copyright message and
6 # disclaimer are retained in their original form.
7 #
8 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
9 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
10 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
11 # POSSIBILITY OF SUCH DAMAGE.
12 #
13 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
14 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
15 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
16 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
17 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
18
19 # $Id: roundup-admin,v 1.52 2001-12-12 21:47:45 richard Exp $
21 import sys
22 if not hasattr(sys, 'version_info') or sys.version_info[:2] < (2,1):
23     print 'Roundup requires python 2.1 or later.'
24     sys.exit(1)
26 import string, os, getpass, getopt, re, UserDict
27 try:
28     import csv
29 except ImportError:
30     csv = None
31 from roundup import date, hyperdb, roundupdb, init, password
32 import roundup.instance
34 class CommandDict(UserDict.UserDict):
35     '''Simple dictionary that lets us do lookups using partial keys.
37     Original code submitted by Engelbert Gruber.
38     '''
39     _marker = []
40     def get(self, key, default=_marker):
41         if self.data.has_key(key):
42             return [(key, self.data[key])]
43         keylist = self.data.keys()
44         keylist.sort()
45         l = []
46         for ki in keylist:
47             if ki.startswith(key):
48                 l.append((ki, self.data[ki]))
49         if not l and default is self._marker:
50             raise KeyError, key
51         return l
53 class UsageError(ValueError):
54     pass
56 class AdminTool:
58     def __init__(self):
59         self.commands = CommandDict()
60         for k in AdminTool.__dict__.keys():
61             if k[:3] == 'do_':
62                 self.commands[k[3:]] = getattr(self, k)
63         self.help = {}
64         for k in AdminTool.__dict__.keys():
65             if k[:5] == 'help_':
66                 self.help[k[5:]] = getattr(self, k)
67         self.instance_home = ''
68         self.db = None
70     def usage(self, message=''):
71         if message: message = 'Problem: '+message+'\n\n'
72         print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments>
74 Help:
75  roundup-admin -h
76  roundup-admin help                       -- this help
77  roundup-admin help <command>             -- command-specific help
78  roundup-admin help all                   -- all available help
79 Options:
80  -i instance home  -- specify the issue tracker "home directory" to administer
81  -u                -- the user[:password] to use for commands
82  -c                -- when outputting lists of data, just comma-separate them'''%message
83         self.help_commands()
85     def help_commands(self):
86         print 'Commands:',
87         commands = ['']
88         for command in self.commands.values():
89             h = command.__doc__.split('\n')[0]
90             commands.append(' '+h[7:])
91         commands.sort()
92         commands.append(
93 'Commands may be abbreviated as long as the abbreviation matches only one')
94         commands.append('command, e.g. l == li == lis == list.')
95         print '\n'.join(commands)
96         print
98     def help_all(self):
99         print '''
100 All commands (except help) require an instance specifier. This is just the path
101 to the roundup instance you're working with. A roundup instance is where 
102 roundup keeps the database and configuration file that defines an issue
103 tracker. It may be thought of as the issue tracker's "home directory". It may
104 be specified in the environment variable ROUNDUP_INSTANCE or on the command
105 line as "-i instance".
107 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
109 Property values are represented as strings in command arguments and in the
110 printed results:
111  . Strings are, well, strings.
112  . Date values are printed in the full date format in the local time zone, and
113    accepted in the full format or any of the partial formats explained below.
114  . Link values are printed as node designators. When given as an argument,
115    node designators and key strings are both accepted.
116  . Multilink values are printed as lists of node designators joined by commas.
117    When given as an argument, node designators and key strings are both
118    accepted; an empty string, a single node, or a list of nodes joined by
119    commas is accepted.
121 When multiple nodes are specified to the roundup get or roundup set
122 commands, the specified properties are retrieved or set on all the listed
123 nodes. 
125 When multiple results are returned by the roundup get or roundup find
126 commands, they are printed one per line (default) or joined by commas (with
127 the -c) option. 
129 Where the command changes data, a login name/password is required. The
130 login may be specified as either "name" or "name:password".
131  . ROUNDUP_LOGIN environment variable
132  . the -u command-line option
133 If either the name or password is not supplied, they are obtained from the
134 command-line. 
136 Date format examples:
137   "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
138   "2000-04-17" means <Date 2000-04-17.00:00:00>
139   "01-25" means <Date yyyy-01-25.00:00:00>
140   "08-13.22:13" means <Date yyyy-08-14.03:13:00>
141   "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
142   "14:25" means <Date yyyy-mm-dd.19:25:00>
143   "8:47:11" means <Date yyyy-mm-dd.13:47:11>
144   "." means "right now"
146 Command help:
147 '''
148         for name, command in self.commands.items():
149             print '%s:'%name
150             print '   ',command.__doc__
152     def do_help(self, args, nl_re=re.compile('[\r\n]'),
153             indent_re=re.compile(r'^(\s+)\S+')):
154         '''Usage: help topic
155         Give help about topic.
157         commands  -- list commands
158         <command> -- help specific to a command
159         initopts  -- init command options
160         all       -- all available help
161         '''
162         topic = args[0]
164         # try help_ methods
165         if self.help.has_key(topic):
166             self.help[topic]()
167             return 0
169         # try command docstrings
170         try:
171             l = self.commands.get(topic)
172         except KeyError:
173             print 'Sorry, no help for "%s"'%topic
174             return 1
176         # display the help for each match, removing the docsring indent
177         for name, help in l:
178             lines = nl_re.split(help.__doc__)
179             print lines[0]
180             indent = indent_re.match(lines[1])
181             if indent: indent = len(indent.group(1))
182             for line in lines[1:]:
183                 if indent:
184                     print line[indent:]
185                 else:
186                     print line
187         return 0
189     def help_initopts(self):
190         import roundup.templates
191         templates = roundup.templates.listTemplates()
192         print 'Templates:', ', '.join(templates)
193         import roundup.backends
194         backends = roundup.backends.__all__
195         print 'Back ends:', ', '.join(backends)
198     def do_initialise(self, instance_home, args):
199         '''Usage: initialise [template [backend [admin password]]]
200         Initialise a new Roundup instance.
202         The command will prompt for the instance home directory (if not supplied
203         through INSTANCE_HOME or the -i option. The template, backend and admin
204         password may be specified on the command-line as arguments, in that
205         order.
207         See also initopts help.
208         '''
209         if len(args) < 1:
210             raise UsageError, 'Not enough arguments supplied'
211         # select template
212         import roundup.templates
213         templates = roundup.templates.listTemplates()
214         template = len(args) > 1 and args[1] or ''
215         if template not in templates:
216             print 'Templates:', ', '.join(templates)
217         while template not in templates:
218             template = raw_input('Select template [classic]: ').strip()
219             if not template:
220                 template = 'classic'
222         import roundup.backends
223         backends = roundup.backends.__all__
224         backend = len(args) > 2 and args[2] or ''
225         if backend not in backends:
226             print 'Back ends:', ', '.join(backends)
227         while backend not in backends:
228             backend = raw_input('Select backend [anydbm]: ').strip()
229             if not backend:
230                 backend = 'anydbm'
231         if len(args) > 3:
232             adminpw = confirm = args[3]
233         else:
234             adminpw = ''
235             confirm = 'x'
236         while adminpw != confirm:
237             adminpw = getpass.getpass('Admin Password: ')
238             confirm = getpass.getpass('       Confirm: ')
239         init.init(instance_home, template, backend, adminpw)
240         return 0
243     def do_get(self, args):
244         '''Usage: get property designator[,designator]*
245         Get the given property of one or more designator(s).
247         Retrieves the property value of the nodes specified by the designators.
248         '''
249         if len(args) < 2:
250             raise UsageError, 'Not enough arguments supplied'
251         propname = args[0]
252         designators = string.split(args[1], ',')
253         l = []
254         for designator in designators:
255             # decode the node designator
256             try:
257                 classname, nodeid = roundupdb.splitDesignator(designator)
258             except roundupdb.DesignatorError, message:
259                 raise UsageError, message
261             # get the class
262             try:
263                 cl = self.db.getclass(classname)
264             except KeyError:
265                 raise UsageError, 'invalid class "%s"'%classname
266             try:
267                 if self.comma_sep:
268                     l.append(cl.get(nodeid, propname))
269                 else:
270                     print cl.get(nodeid, propname)
271             except IndexError:
272                 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
273             except KeyError:
274                 raise UsageError, 'no such %s property "%s"'%(classname,
275                     propname)
276         if self.comma_sep:
277             print ','.join(l)
278         return 0
281     def do_set(self, args):
282         '''Usage: set designator[,designator]* propname=value ...
283         Set the given property of one or more designator(s).
285         Sets the property to the value for all designators given.
286         '''
287         if len(args) < 2:
288             raise UsageError, 'Not enough arguments supplied'
289         from roundup import hyperdb
291         designators = string.split(args[0], ',')
292         props = {}
293         for prop in args[1:]:
294             if prop.find('=') == -1:
295                 raise UsageError, 'argument "%s" not propname=value'%prop
296             try:
297                 key, value = prop.split('=')
298             except ValueError:
299                 raise UsageError, 'argument "%s" not propname=value'%prop
300             props[key] = value
301         for designator in designators:
302             # decode the node designator
303             try:
304                 classname, nodeid = roundupdb.splitDesignator(designator)
305             except roundupdb.DesignatorError, message:
306                 raise UsageError, message
308             # get the class
309             try:
310                 cl = self.db.getclass(classname)
311             except KeyError:
312                 raise UsageError, 'invalid class "%s"'%classname
314             properties = cl.getprops()
315             for key, value in props.items():
316                 proptype =  properties[key]
317                 if isinstance(proptype, hyperdb.String):
318                     continue
319                 elif isinstance(proptype, hyperdb.Password):
320                     props[key] = password.Password(value)
321                 elif isinstance(proptype, hyperdb.Date):
322                     try:
323                         props[key] = date.Date(value)
324                     except ValueError, message:
325                         raise UsageError, '"%s": %s'%(value, message)
326                 elif isinstance(proptype, hyperdb.Interval):
327                     try:
328                         props[key] = date.Interval(value)
329                     except ValueError, message:
330                         raise UsageError, '"%s": %s'%(value, message)
331                 elif isinstance(proptype, hyperdb.Link):
332                     props[key] = value
333                 elif isinstance(proptype, hyperdb.Multilink):
334                     props[key] = value.split(',')
336             # try the set
337             try:
338                 apply(cl.set, (nodeid, ), props)
339             except (TypeError, IndexError, ValueError), message:
340                 raise UsageError, message
341         return 0
343     def do_find(self, args):
344         '''Usage: find classname propname=value ...
345         Find the nodes of the given class with a given link property value.
347         Find the nodes of the given class with a given link property value. The
348         value may be either the nodeid of the linked node, or its key value.
349         '''
350         if len(args) < 1:
351             raise UsageError, 'Not enough arguments supplied'
352         classname = args[0]
353         # get the class
354         try:
355             cl = self.db.getclass(classname)
356         except KeyError:
357             raise UsageError, 'invalid class "%s"'%classname
359         # TODO: handle > 1 argument
360         # handle the propname=value argument
361         if args[1].find('=') == -1:
362             raise UsageError, 'argument "%s" not propname=value'%prop
363         try:
364             propname, value = args[1].split('=')
365         except ValueError:
366             raise UsageError, 'argument "%s" not propname=value'%prop
368         # if the value isn't a number, look up the linked class to get the
369         # number
370         num_re = re.compile('^\d+$')
371         if not num_re.match(value):
372             # get the property
373             try:
374                 property = cl.properties[propname]
375             except KeyError:
376                 raise UsageError, '%s has no property "%s"'%(classname,
377                     propname)
379             # make sure it's a link
380             if (not isinstance(property, hyperdb.Link) and not
381                     isinstance(property, hyperdb.Multilink)):
382                 raise UsageError, 'You may only "find" link properties'
384             # get the linked-to class and look up the key property
385             link_class = self.db.getclass(property.classname)
386             try:
387                 value = link_class.lookup(value)
388             except TypeError:
389                 raise UsageError, '%s has no key property"'%link_class.classname
390             except KeyError:
391                 raise UsageError, '%s has no entry "%s"'%(link_class.classname,
392                     propname)
394         # now do the find 
395         try:
396             if self.comma_sep:
397                 print ','.join(apply(cl.find, (), {propname: value}))
398             else:
399                 print apply(cl.find, (), {propname: value})
400         except KeyError:
401             raise UsageError, '%s has no property "%s"'%(classname,
402                 propname)
403         except (ValueError, TypeError), message:
404             raise UsageError, message
405         return 0
407     def do_specification(self, args):
408         '''Usage: specification classname
409         Show the properties for a classname.
411         This lists the properties for a given class.
412         '''
413         if len(args) < 1:
414             raise UsageError, 'Not enough arguments supplied'
415         classname = args[0]
416         # get the class
417         try:
418             cl = self.db.getclass(classname)
419         except KeyError:
420             raise UsageError, 'invalid class "%s"'%classname
422         # get the key property
423         keyprop = cl.getkey()
424         for key, value in cl.properties.items():
425             if keyprop == key:
426                 print '%s: %s (key property)'%(key, value)
427             else:
428                 print '%s: %s'%(key, value)
430     def do_display(self, args):
431         '''Usage: display designator
432         Show the property values for the given node.
434         This lists the properties and their associated values for the given
435         node.
436         '''
437         if len(args) < 1:
438             raise UsageError, 'Not enough arguments supplied'
440         # decode the node designator
441         try:
442             classname, nodeid = roundupdb.splitDesignator(args[0])
443         except roundupdb.DesignatorError, message:
444             raise UsageError, message
446         # get the class
447         try:
448             cl = self.db.getclass(classname)
449         except KeyError:
450             raise UsageError, 'invalid class "%s"'%classname
452         # display the values
453         for key in cl.properties.keys():
454             value = cl.get(nodeid, key)
455             print '%s: %s'%(key, value)
457     def do_create(self, args):
458         '''Usage: create classname property=value ...
459         Create a new entry of a given class.
461         This creates a new entry of the given class using the property
462         name=value arguments provided on the command line after the "create"
463         command.
464         '''
465         if len(args) < 1:
466             raise UsageError, 'Not enough arguments supplied'
467         from roundup import hyperdb
469         classname = args[0]
471         # get the class
472         try:
473             cl = self.db.getclass(classname)
474         except KeyError:
475             raise UsageError, 'invalid class "%s"'%classname
477         # now do a create
478         props = {}
479         properties = cl.getprops(protected = 0)
480         if len(args) == 1:
481             # ask for the properties
482             for key, value in properties.items():
483                 if key == 'id': continue
484                 name = value.__class__.__name__
485                 if isinstance(value , hyperdb.Password):
486                     again = None
487                     while value != again:
488                         value = getpass.getpass('%s (Password): '%key.capitalize())
489                         again = getpass.getpass('   %s (Again): '%key.capitalize())
490                         if value != again: print 'Sorry, try again...'
491                     if value:
492                         props[key] = value
493                 else:
494                     value = raw_input('%s (%s): '%(key.capitalize(), name))
495                     if value:
496                         props[key] = value
497         else:
498             # use the args
499             for prop in args[1:]:
500                 if prop.find('=') == -1:
501                     raise UsageError, 'argument "%s" not propname=value'%prop
502                 try:
503                     key, value = prop.split('=')
504                 except ValueError:
505                     raise UsageError, 'argument "%s" not propname=value'%prop
506                 props[key] = value 
508         # convert types
509         for key in props.keys():
510             # get the property
511             try:
512                 proptype = properties[key]
513             except KeyError:
514                 raise UsageError, '%s has no property "%s"'%(classname, key)
516             if isinstance(proptype, hyperdb.Date):
517                 try:
518                     props[key] = date.Date(value)
519                 except ValueError, message:
520                     raise UsageError, '"%s": %s'%(value, message)
521             elif isinstance(proptype, hyperdb.Interval):
522                 try:
523                     props[key] = date.Interval(value)
524                 except ValueError, message:
525                     raise UsageError, '"%s": %s'%(value, message)
526             elif isinstance(proptype, hyperdb.Password):
527                 props[key] = password.Password(value)
528             elif isinstance(proptype, hyperdb.Multilink):
529                 props[key] = value.split(',')
531         # check for the key property
532         if cl.getkey() and not props.has_key(cl.getkey()):
533             raise UsageError, "you must provide the '%s' property."%cl.getkey()
535         # do the actual create
536         try:
537             print apply(cl.create, (), props)
538         except (TypeError, IndexError, ValueError), message:
539             raise UsageError, message
540         return 0
542     def do_list(self, args):
543         '''Usage: list classname [property]
544         List the instances of a class.
546         Lists all instances of the given class. If the property is not
547         specified, the  "label" property is used. The label property is tried
548         in order: the key, "name", "title" and then the first property,
549         alphabetically.
550         '''
551         if len(args) < 1:
552             raise UsageError, 'Not enough arguments supplied'
553         classname = args[0]
555         # get the class
556         try:
557             cl = self.db.getclass(classname)
558         except KeyError:
559             raise UsageError, 'invalid class "%s"'%classname
561         # figure the property
562         if len(args) > 1:
563             key = args[1]
564         else:
565             key = cl.labelprop()
567         if self.comma_sep:
568             print ','.join(cl.list())
569         else:
570             for nodeid in cl.list():
571                 try:
572                     value = cl.get(nodeid, key)
573                 except KeyError:
574                     raise UsageError, '%s has no property "%s"'%(classname, key)
575                 print "%4s: %s"%(nodeid, value)
576         return 0
578     def do_table(self, args):
579         '''Usage: table classname [property[,property]*]
580         List the instances of a class in tabular form.
582         Lists all instances of the given class. If the properties are not
583         specified, all properties are displayed. By default, the column widths
584         are the width of the property names. The width may be explicitly defined
585         by defining the property as "name:width". For example::
586           roundup> table priority id,name:10
587           Id Name
588           1  fatal-bug 
589           2  bug       
590           3  usability 
591           4  feature   
592         '''
593         if len(args) < 1:
594             raise UsageError, 'Not enough arguments supplied'
595         classname = args[0]
597         # get the class
598         try:
599             cl = self.db.getclass(classname)
600         except KeyError:
601             raise UsageError, 'invalid class "%s"'%classname
603         # figure the property names to display
604         if len(args) > 1:
605             prop_names = args[1].split(',')
606             all_props = cl.getprops()
607             for prop_name in prop_names:
608                 if not all_props.has_key(prop_name):
609                     raise UsageError, '%s has no property "%s"'%(classname,
610                         prop_name)
611         else:
612             prop_names = cl.getprops().keys()
614         # now figure column widths
615         props = []
616         for spec in prop_names:
617             if ':' in spec:
618                 try:
619                     name, width = spec.split(':')
620                 except (ValueError, TypeError):
621                     raise UsageError, '"%s" not name:width'%spec
622                 props.append((spec, int(width)))
623             else:
624                 props.append((spec, len(spec)))
626         # now display the heading
627         print ' '.join([string.capitalize(name) for name, width in props])
629         # and the table data
630         for nodeid in cl.list():
631             l = []
632             for name, width in props:
633                 if name != 'id':
634                     try:
635                         value = str(cl.get(nodeid, name))
636                     except KeyError:
637                         # we already checked if the property is valid - a
638                         # KeyError here means the node just doesn't have a
639                         # value for it
640                         value = ''
641                 else:
642                     value = str(nodeid)
643                 f = '%%-%ds'%width
644                 l.append(f%value[:width])
645             print ' '.join(l)
646         return 0
648     def do_history(self, args):
649         '''Usage: history designator
650         Show the history entries of a designator.
652         Lists the journal entries for the node identified by the designator.
653         '''
654         if len(args) < 1:
655             raise UsageError, 'Not enough arguments supplied'
656         try:
657             classname, nodeid = roundupdb.splitDesignator(args[0])
658         except roundupdb.DesignatorError, message:
659             raise UsageError, message
661         # TODO: handle the -c option?
662         try:
663             print self.db.getclass(classname).history(nodeid)
664         except KeyError:
665             raise UsageError, 'no such class "%s"'%classname
666         except IndexError:
667             raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
668         return 0
670     def do_commit(self, args):
671         '''Usage: commit
672         Commit all changes made to the database.
674         The changes made during an interactive session are not
675         automatically written to the database - they must be committed
676         using this command.
678         One-off commands on the command-line are automatically committed if
679         they are successful.
680         '''
681         self.db.commit()
682         return 0
684     def do_rollback(self, args):
685         '''Usage: rollback
686         Undo all changes that are pending commit to the database.
688         The changes made during an interactive session are not
689         automatically written to the database - they must be committed
690         manually. This command undoes all those changes, so a commit
691         immediately after would make no changes to the database.
692         '''
693         self.db.rollback()
694         return 0
696     def do_retire(self, args):
697         '''Usage: retire designator[,designator]*
698         Retire the node specified by designator.
700         This action indicates that a particular node is not to be retrieved by
701         the list or find commands, and its key value may be re-used.
702         '''
703         if len(args) < 1:
704             raise UsageError, 'Not enough arguments supplied'
705         designators = string.split(args[0], ',')
706         for designator in designators:
707             try:
708                 classname, nodeid = roundupdb.splitDesignator(designator)
709             except roundupdb.DesignatorError, message:
710                 raise UsageError, message
711             try:
712                 self.db.getclass(classname).retire(nodeid)
713             except KeyError:
714                 raise UsageError, 'no such class "%s"'%classname
715             except IndexError:
716                 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
717         return 0
719     def do_export(self, args):
720         '''Usage: export class[,class] destination_dir
721         Export the database to tab-separated-value files.
723         This action exports the current data from the database into
724         tab-separated-value files that are placed in the nominated destination
725         directory. The journals are not exported.
726         '''
727         if len(args) < 2:
728             raise UsageError, 'Not enough arguments supplied'
729         classes = string.split(args[0], ',')
730         dir = args[1]
732         # use the csv parser if we can - it's faster
733         if csv is not None:
734             p = csv.parser(field_sep=':')
736         # do all the classes specified
737         for classname in classes:
738             try:
739                 cl = self.db.getclass(classname)
740             except KeyError:
741                 raise UsageError, 'no such class "%s"'%classname
742             f = open(os.path.join(dir, classname+'.csv'), 'w')
743             f.write(string.join(cl.properties.keys(), ':') + '\n')
745             # all nodes for this class
746             properties = cl.properties.items()
747             for nodeid in cl.list():
748                 l = []
749                 for prop, proptype in properties:
750                     value = cl.get(nodeid, prop)
751                     # convert data where needed
752                     if isinstance(proptype, hyperdb.Date):
753                         value = value.get_tuple()
754                     elif isinstance(proptype, hyperdb.Interval):
755                         value = value.get_tuple()
756                     elif isinstance(proptype, hyperdb.Password):
757                         value = str(value)
758                     l.append(repr(value))
760                 # now write
761                 if csv is not None:
762                    f.write(p.join(l) + '\n')
763                 else:
764                    # escape the individual entries to they're valid CSV
765                    m = []
766                    for entry in l:
767                       if '"' in entry:
768                           entry = '""'.join(entry.split('"'))
769                       if ':' in entry:
770                           entry = '"%s"'%entry
771                       m.append(entry)
772                    f.write(':'.join(m) + '\n')
773         return 0
775     def do_import(self, args):
776         '''Usage: import class file
777         Import the contents of the tab-separated-value file.
779         The file must define the same properties as the class (including having
780         a "header" line with those property names.) The new nodes are added to
781         the existing database - if you want to create a new database using the
782         imported data, then create a new database (or, tediously, retire all
783         the old data.)
784         '''
785         if len(args) < 2:
786             raise UsageError, 'Not enough arguments supplied'
787         if csv is None:
788             raise UsageError, \
789                 'Sorry, you need the csv module to use this function.\n'\
790                 'Get it from: http://www.object-craft.com.au/projects/csv/'
792         from roundup import hyperdb
794         # ensure that the properties and the CSV file headings match
795         classname = args[0]
796         try:
797             cl = self.db.getclass(classname)
798         except KeyError:
799             raise UsageError, 'no such class "%s"'%classname
800         f = open(args[1])
801         p = csv.parser(field_sep=':')
802         file_props = p.parse(f.readline())
803         props = cl.properties.keys()
804         m = file_props[:]
805         m.sort()
806         props.sort()
807         if m != props:
808             raise UsageError, 'Import file doesn\'t define the same '\
809                 'properties as "%s".'%args[0]
811         # loop through the file and create a node for each entry
812         n = range(len(props))
813         while 1:
814             line = f.readline()
815             if not line: break
817             # parse lines until we get a complete entry
818             while 1:
819                 l = p.parse(line)
820                 if l: break
822             # make the new node's property map
823             d = {}
824             for i in n:
825                 # Use eval to reverse the repr() used to output the CSV
826                 value = eval(l[i])
827                 # Figure the property for this column
828                 key = file_props[i]
829                 proptype = cl.properties[key]
830                 # Convert for property type
831                 if isinstance(proptype, hyperdb.Date):
832                     value = date.Date(value)
833                 elif isinstance(proptype, hyperdb.Interval):
834                     value = date.Interval(value)
835                 elif isinstance(proptype, hyperdb.Password):
836                     pwd = password.Password()
837                     pwd.unpack(value)
838                     value = pwd
839                 if value is not None:
840                     d[key] = value
842             # and create the new node
843             apply(cl.create, (), d)
844         return 0
846     def run_command(self, args):
847         '''Run a single command
848         '''
849         command = args[0]
851         # handle help now
852         if command == 'help':
853             if len(args)>1:
854                 self.do_help(args[1:])
855                 return 0
856             self.do_help(['help'])
857             return 0
858         if command == 'morehelp':
859             self.do_help(['help'])
860             self.help_commands()
861             self.help_all()
862             return 0
864         # figure what the command is
865         try:
866             functions = self.commands.get(command)
867         except KeyError:
868             # not a valid command
869             print 'Unknown command "%s" ("help commands" for a list)'%command
870             return 1
872         # check for multiple matches
873         if len(functions) > 1:
874             print 'Multiple commands match "%s": %s'%(command,
875                 ', '.join([i[0] for i in functions]))
876             return 1
877         command, function = functions[0]
879         # make sure we have an instance_home
880         while not self.instance_home:
881             self.instance_home = raw_input('Enter instance home: ').strip()
883         # before we open the db, we may be doing an init
884         if command == 'initialise':
885             return self.do_initialise(self.instance_home, args)
887         # get the instance
888         try:
889             instance = roundup.instance.open(self.instance_home)
890         except ValueError, message:
891             self.instance_home = ''
892             print "Couldn't open instance: %s"%message
893             return 1
895         # only open the database once!
896         if not self.db:
897             self.db = instance.open('admin')
899         # do the command
900         ret = 0
901         try:
902             ret = function(args[1:])
903         except UsageError, message:
904             print 'Error: %s'%message
905             print function.__doc__
906             ret = 1
907         except:
908             import traceback
909             traceback.print_exc()
910             ret = 1
911         return ret
913     def interactive(self, ws_re=re.compile(r'\s+')):
914         '''Run in an interactive mode
915         '''
916         print 'Roundup {version} ready for input.'
917         print 'Type "help" for help.'
918         try:
919             import readline
920         except ImportError:
921             print "Note: command history and editing not available"
923         while 1:
924             try:
925                 command = raw_input('roundup> ')
926             except EOFError:
927                 print 'exit...'
928                 break
929             if not command: continue
930             args = ws_re.split(command)
931             if not args: continue
932             if args[0] in ('quit', 'exit'): break
933             self.run_command(args)
935         # exit.. check for transactions
936         if self.db and self.db.transactions:
937             commit = raw_input("There are unsaved changes. Commit them (y/N)? ")
938             if commit[0].lower() == 'y':
939                 self.db.commit()
940         return 0
942     def main(self):
943         try:
944             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
945         except getopt.GetoptError, e:
946             self.usage(str(e))
947             return 1
949         # handle command-line args
950         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
951         name = password = ''
952         if os.environ.has_key('ROUNDUP_LOGIN'):
953             l = os.environ['ROUNDUP_LOGIN'].split(':')
954             name = l[0]
955             if len(l) > 1:
956                 password = l[1]
957         self.comma_sep = 0
958         for opt, arg in opts:
959             if opt == '-h':
960                 self.usage()
961                 return 0
962             if opt == '-i':
963                 self.instance_home = arg
964             if opt == '-c':
965                 self.comma_sep = 1
967         # if no command - go interactive
968         ret = 0
969         if not args:
970             self.interactive()
971         else:
972             ret = self.run_command(args)
973             if self.db: self.db.commit()
974         return ret
977 if __name__ == '__main__':
978     tool = AdminTool()
979     sys.exit(tool.main())
982 # $Log: not supported by cvs2svn $
983 # Revision 1.51  2001/12/10 00:57:38  richard
984 # From CHANGES:
985 #  . Added the "display" command to the admin tool - displays a node's values
986 #  . #489760 ] [issue] only subject
987 #  . fixed the doc/index.html to include the quoting in the mail alias.
989 # Also:
990 #  . fixed roundup-admin so it works with transactions
991 #  . disabled the back_anydbm module if anydbm tries to use dumbdbm
993 # Revision 1.50  2001/12/02 05:06:16  richard
994 # . We now use weakrefs in the Classes to keep the database reference, so
995 #   the close() method on the database is no longer needed.
996 #   I bumped the minimum python requirement up to 2.1 accordingly.
997 # . #487480 ] roundup-server
998 # . #487476 ] INSTALL.txt
1000 # I also cleaned up the change message / post-edit stuff in the cgi client.
1001 # There's now a clearly marked "TODO: append the change note" where I believe
1002 # the change note should be added there. The "changes" list will obviously
1003 # have to be modified to be a dict of the changes, or somesuch.
1005 # More testing needed.
1007 # Revision 1.49  2001/12/01 07:17:50  richard
1008 # . We now have basic transaction support! Information is only written to
1009 #   the database when the commit() method is called. Only the anydbm
1010 #   backend is modified in this way - neither of the bsddb backends have been.
1011 #   The mail, admin and cgi interfaces all use commit (except the admin tool
1012 #   doesn't have a commit command, so interactive users can't commit...)
1013 # . Fixed login/registration forwarding the user to the right page (or not,
1014 #   on a failure)
1016 # Revision 1.48  2001/11/27 22:32:03  richard
1017 # typo
1019 # Revision 1.47  2001/11/26 22:55:56  richard
1020 # Feature:
1021 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
1022 #    the instance.
1023 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1024 #    signature info in e-mails.
1025 #  . Some more flexibility in the mail gateway and more error handling.
1026 #  . Login now takes you to the page you back to the were denied access to.
1028 # Fixed:
1029 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
1031 # Revision 1.46  2001/11/21 03:40:54  richard
1032 # more new property handling
1034 # Revision 1.45  2001/11/12 22:51:59  jhermann
1035 # Fixed option & associated error handling
1037 # Revision 1.44  2001/11/12 22:01:06  richard
1038 # Fixed issues with nosy reaction and author copies.
1040 # Revision 1.43  2001/11/09 22:33:28  richard
1041 # More error handling fixes.
1043 # Revision 1.42  2001/11/09 10:11:08  richard
1044 #  . roundup-admin now handles all hyperdb exceptions
1046 # Revision 1.41  2001/11/09 01:25:40  richard
1047 # Should parse with python 1.5.2 now.
1049 # Revision 1.40  2001/11/08 04:42:00  richard
1050 # Expanded the already-abbreviated "initialise" and "specification" commands,
1051 # and added a comment to the command help about the abbreviation.
1053 # Revision 1.39  2001/11/08 04:29:59  richard
1054 # roundup-admin now accepts abbreviated commands (eg. l = li = lis = list)
1055 # [thanks Engelbert Gruber for the inspiration]
1057 # Revision 1.38  2001/11/05 23:45:40  richard
1058 # Fixed newuser_action so it sets the cookie with the unencrypted password.
1059 # Also made it present nicer error messages (not tracebacks).
1061 # Revision 1.37  2001/10/23 01:00:18  richard
1062 # Re-enabled login and registration access after lopping them off via
1063 # disabling access for anonymous users.
1064 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1065 # a couple of bugs while I was there. Probably introduced a couple, but
1066 # things seem to work OK at the moment.
1068 # Revision 1.36  2001/10/21 00:45:15  richard
1069 # Added author identification to e-mail messages from roundup.
1071 # Revision 1.35  2001/10/20 11:58:48  richard
1072 # Catch errors in login - no username or password supplied.
1073 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
1075 # Revision 1.34  2001/10/18 02:16:42  richard
1076 # Oops, committed the admin script with the wierd #! line.
1077 # Also, made the thing into a class to reduce parameter passing.
1078 # Nuked the leading whitespace from the help __doc__ displays too.
1080 # Revision 1.33  2001/10/17 23:13:19  richard
1081 # Did a fair bit of work on the admin tool. Now has an extra command "table"
1082 # which displays node information in a tabular format. Also fixed import and
1083 # export so they work. Removed freshen.
1084 # Fixed quopri usage in mailgw from bug reports.
1086 # Revision 1.32  2001/10/17 06:57:29  richard
1087 # Interactive startup blurb - need to figure how to get the version in there.
1089 # Revision 1.31  2001/10/17 06:17:26  richard
1090 # Now with readline support :)
1092 # Revision 1.30  2001/10/17 06:04:00  richard
1093 # Beginnings of an interactive mode for roundup-admin
1095 # Revision 1.29  2001/10/16 03:48:01  richard
1096 # admin tool now complains if a "find" is attempted with a non-link property.
1098 # Revision 1.28  2001/10/13 00:07:39  richard
1099 # More help in admin tool.
1101 # Revision 1.27  2001/10/11 23:43:04  richard
1102 # Implemented the comma-separated printing option in the admin tool.
1103 # Fixed a typo (more of a vim-o actually :) in mailgw.
1105 # Revision 1.26  2001/10/11 05:03:51  richard
1106 # Marked the roundup-admin import/export as experimental since they're not fully
1107 # operational.
1109 # Revision 1.25  2001/10/10 04:12:32  richard
1110 # The setup.cfg file is just causing pain. Away it goes.
1112 # Revision 1.24  2001/10/10 03:54:57  richard
1113 # Added database importing and exporting through CSV files.
1114 # Uses the csv module from object-craft for exporting if it's available.
1115 # Requires the csv module for importing.
1117 # Revision 1.23  2001/10/09 23:36:25  richard
1118 # Spit out command help if roundup-admin command doesn't get an argument.
1120 # Revision 1.22  2001/10/09 07:25:59  richard
1121 # Added the Password property type. See "pydoc roundup.password" for
1122 # implementation details. Have updated some of the documentation too.
1124 # Revision 1.21  2001/10/05 02:23:24  richard
1125 #  . roundup-admin create now prompts for property info if none is supplied
1126 #    on the command-line.
1127 #  . hyperdb Class getprops() method may now return only the mutable
1128 #    properties.
1129 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
1130 #    now support anonymous user access (read-only, unless there's an
1131 #    "anonymous" user, in which case write access is permitted). Login
1132 #    handling has been moved into cgi_client.Client.main()
1133 #  . The "extended" schema is now the default in roundup init.
1134 #  . The schemas have had their page headings modified to cope with the new
1135 #    login handling. Existing installations should copy the interfaces.py
1136 #    file from the roundup lib directory to their instance home.
1137 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
1138 #    Ping - has been removed.
1139 #  . Fixed a whole bunch of places in the CGI interface where we should have
1140 #    been returning Not Found instead of throwing an exception.
1141 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
1142 #    an item now throws an exception.
1144 # Revision 1.20  2001/10/04 02:12:42  richard
1145 # Added nicer command-line item adding: passing no arguments will enter an
1146 # interactive more which asks for each property in turn. While I was at it, I
1147 # fixed an implementation problem WRT the spec - I wasn't raising a
1148 # ValueError if the key property was missing from a create(). Also added a
1149 # protected=boolean argument to getprops() so we can list only the mutable
1150 # properties (defaults to yes, which lists the immutables).
1152 # Revision 1.19  2001/10/01 06:40:43  richard
1153 # made do_get have the args in the correct order
1155 # Revision 1.18  2001/09/18 22:58:37  richard
1157 # Added some more help to roundu-admin
1159 # Revision 1.17  2001/08/28 05:58:33  anthonybaxter
1160 # added missing 'import' statements.
1162 # Revision 1.16  2001/08/12 06:32:36  richard
1163 # using isinstance(blah, Foo) now instead of isFooType
1165 # Revision 1.15  2001/08/07 00:24:42  richard
1166 # stupid typo
1168 # Revision 1.14  2001/08/07 00:15:51  richard
1169 # Added the copyright/license notice to (nearly) all files at request of
1170 # Bizar Software.
1172 # Revision 1.13  2001/08/05 07:44:13  richard
1173 # Instances are now opened by a special function that generates a unique
1174 # module name for the instances on import time.
1176 # Revision 1.12  2001/08/03 01:28:33  richard
1177 # Used the much nicer load_package, pointed out by Steve Majewski.
1179 # Revision 1.11  2001/08/03 00:59:34  richard
1180 # Instance import now imports the instance using imp.load_module so that
1181 # we can have instance homes of "roundup" or other existing python package
1182 # names.
1184 # Revision 1.10  2001/07/30 08:12:17  richard
1185 # Added time logging and file uploading to the templates.
1187 # Revision 1.9  2001/07/30 03:52:55  richard
1188 # init help now lists templates and backends
1190 # Revision 1.8  2001/07/30 02:37:07  richard
1191 # Freshen is really broken. Commented out.
1193 # Revision 1.7  2001/07/30 01:28:46  richard
1194 # Bugfixes
1196 # Revision 1.6  2001/07/30 00:57:51  richard
1197 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
1198 # better internal structure. It's just BETTER. :)
1200 # Revision 1.5  2001/07/30 00:04:48  richard
1201 # Made the "init" prompting more friendly.
1203 # Revision 1.4  2001/07/29 07:01:39  richard
1204 # Added vim command to all source so that we don't get no steenkin' tabs :)
1206 # Revision 1.3  2001/07/23 08:45:28  richard
1207 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
1208 # workable instance_home set up :)
1209 # _and_ anydbm has had its first test :)
1211 # Revision 1.2  2001/07/23 08:20:44  richard
1212 # Moved over to using marshal in the bsddb and anydbm backends.
1213 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
1214 #  retired - mod hyperdb.Class.list() so it lists retired nodes)
1216 # Revision 1.1  2001/07/23 03:46:48  richard
1217 # moving the bin files to facilitate out-of-the-boxness
1219 # Revision 1.1  2001/07/22 11:15:45  richard
1220 # More Grande Splite stuff
1223 # vim: set filetype=python ts=4 sw=4 et si