Code

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