Code

b9cdc9f9a0267eddbd22a7e84ac8e913a910f250
[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.51 2001-12-10 00:57:38 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         else:
607             prop_names = cl.getprops().keys()
609         # now figure column widths
610         props = []
611         for spec in prop_names:
612             if ':' in spec:
613                 try:
614                     name, width = spec.split(':')
615                 except (ValueError, TypeError):
616                     raise UsageError, '"%s" not name:width'%spec
617                 props.append((spec, int(width)))
618             else:
619                 props.append((spec, len(spec)))
621         # now display the heading
622         print ' '.join([string.capitalize(name) for name, width in props])
624         # and the table data
625         for nodeid in cl.list():
626             l = []
627             for name, width in props:
628                 if name != 'id':
629                     try:
630                         value = str(cl.get(nodeid, name))
631                     except KeyError:
632                         raise UsageError, '%s has no property "%s"'%(classname,
633                             name)
634                 else:
635                     value = str(nodeid)
636                 f = '%%-%ds'%width
637                 l.append(f%value[:width])
638             print ' '.join(l)
639         return 0
641     def do_history(self, args):
642         '''Usage: history designator
643         Show the history entries of a designator.
645         Lists the journal entries for the node identified by the designator.
646         '''
647         if len(args) < 1:
648             raise UsageError, 'Not enough arguments supplied'
649         try:
650             classname, nodeid = roundupdb.splitDesignator(args[0])
651         except roundupdb.DesignatorError, message:
652             raise UsageError, message
654         # TODO: handle the -c option?
655         try:
656             print self.db.getclass(classname).history(nodeid)
657         except KeyError:
658             raise UsageError, 'no such class "%s"'%classname
659         except IndexError:
660             raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
661         return 0
663     def do_commit(self, args):
664         '''Usage: commit
665         Commit all changes made to the database.
667         The changes made during an interactive session are not
668         automatically written to the database - they must be committed
669         using this command.
671         One-off commands on the command-line are automatically committed if
672         they are successful.
673         '''
674         self.db.commit()
675         return 0
677     def do_rollback(self, args):
678         '''Usage: rollback
679         Undo all changes that are pending commit to the database.
681         The changes made during an interactive session are not
682         automatically written to the database - they must be committed
683         manually. This command undoes all those changes, so a commit
684         immediately after would make no changes to the database.
685         '''
686         self.db.rollback()
687         return 0
689     def do_retire(self, args):
690         '''Usage: retire designator[,designator]*
691         Retire the node specified by designator.
693         This action indicates that a particular node is not to be retrieved by
694         the list or find commands, and its key value may be re-used.
695         '''
696         if len(args) < 1:
697             raise UsageError, 'Not enough arguments supplied'
698         designators = string.split(args[0], ',')
699         for designator in designators:
700             try:
701                 classname, nodeid = roundupdb.splitDesignator(designator)
702             except roundupdb.DesignatorError, message:
703                 raise UsageError, message
704             try:
705                 self.db.getclass(classname).retire(nodeid)
706             except KeyError:
707                 raise UsageError, 'no such class "%s"'%classname
708             except IndexError:
709                 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
710         return 0
712     def do_export(self, args):
713         '''Usage: export class[,class] destination_dir
714         Export the database to tab-separated-value files.
716         This action exports the current data from the database into
717         tab-separated-value files that are placed in the nominated destination
718         directory. The journals are not exported.
719         '''
720         if len(args) < 2:
721             raise UsageError, 'Not enough arguments supplied'
722         classes = string.split(args[0], ',')
723         dir = args[1]
725         # use the csv parser if we can - it's faster
726         if csv is not None:
727             p = csv.parser(field_sep=':')
729         # do all the classes specified
730         for classname in classes:
731             try:
732                 cl = self.db.getclass(classname)
733             except KeyError:
734                 raise UsageError, 'no such class "%s"'%classname
735             f = open(os.path.join(dir, classname+'.csv'), 'w')
736             f.write(string.join(cl.properties.keys(), ':') + '\n')
738             # all nodes for this class
739             properties = cl.properties.items()
740             for nodeid in cl.list():
741                 l = []
742                 for prop, proptype in properties:
743                     value = cl.get(nodeid, prop)
744                     # convert data where needed
745                     if isinstance(proptype, hyperdb.Date):
746                         value = value.get_tuple()
747                     elif isinstance(proptype, hyperdb.Interval):
748                         value = value.get_tuple()
749                     elif isinstance(proptype, hyperdb.Password):
750                         value = str(value)
751                     l.append(repr(value))
753                 # now write
754                 if csv is not None:
755                    f.write(p.join(l) + '\n')
756                 else:
757                    # escape the individual entries to they're valid CSV
758                    m = []
759                    for entry in l:
760                       if '"' in entry:
761                           entry = '""'.join(entry.split('"'))
762                       if ':' in entry:
763                           entry = '"%s"'%entry
764                       m.append(entry)
765                    f.write(':'.join(m) + '\n')
766         return 0
768     def do_import(self, args):
769         '''Usage: import class file
770         Import the contents of the tab-separated-value file.
772         The file must define the same properties as the class (including having
773         a "header" line with those property names.) The new nodes are added to
774         the existing database - if you want to create a new database using the
775         imported data, then create a new database (or, tediously, retire all
776         the old data.)
777         '''
778         if len(args) < 2:
779             raise UsageError, 'Not enough arguments supplied'
780         if csv is None:
781             raise UsageError, \
782                 'Sorry, you need the csv module to use this function.\n'\
783                 'Get it from: http://www.object-craft.com.au/projects/csv/'
785         from roundup import hyperdb
787         # ensure that the properties and the CSV file headings match
788         classname = args[0]
789         try:
790             cl = self.db.getclass(classname)
791         except KeyError:
792             raise UsageError, 'no such class "%s"'%classname
793         f = open(args[1])
794         p = csv.parser(field_sep=':')
795         file_props = p.parse(f.readline())
796         props = cl.properties.keys()
797         m = file_props[:]
798         m.sort()
799         props.sort()
800         if m != props:
801             raise UsageError, 'Import file doesn\'t define the same '\
802                 'properties as "%s".'%args[0]
804         # loop through the file and create a node for each entry
805         n = range(len(props))
806         while 1:
807             line = f.readline()
808             if not line: break
810             # parse lines until we get a complete entry
811             while 1:
812                 l = p.parse(line)
813                 if l: break
815             # make the new node's property map
816             d = {}
817             for i in n:
818                 # Use eval to reverse the repr() used to output the CSV
819                 value = eval(l[i])
820                 # Figure the property for this column
821                 key = file_props[i]
822                 proptype = cl.properties[key]
823                 # Convert for property type
824                 if isinstance(proptype, hyperdb.Date):
825                     value = date.Date(value)
826                 elif isinstance(proptype, hyperdb.Interval):
827                     value = date.Interval(value)
828                 elif isinstance(proptype, hyperdb.Password):
829                     pwd = password.Password()
830                     pwd.unpack(value)
831                     value = pwd
832                 if value is not None:
833                     d[key] = value
835             # and create the new node
836             apply(cl.create, (), d)
837         return 0
839     def run_command(self, args):
840         '''Run a single command
841         '''
842         command = args[0]
844         # handle help now
845         if command == 'help':
846             if len(args)>1:
847                 self.do_help(args[1:])
848                 return 0
849             self.do_help(['help'])
850             return 0
851         if command == 'morehelp':
852             self.do_help(['help'])
853             self.help_commands()
854             self.help_all()
855             return 0
857         # figure what the command is
858         try:
859             functions = self.commands.get(command)
860         except KeyError:
861             # not a valid command
862             print 'Unknown command "%s" ("help commands" for a list)'%command
863             return 1
865         # check for multiple matches
866         if len(functions) > 1:
867             print 'Multiple commands match "%s": %s'%(command,
868                 ', '.join([i[0] for i in functions]))
869             return 1
870         command, function = functions[0]
872         # make sure we have an instance_home
873         while not self.instance_home:
874             self.instance_home = raw_input('Enter instance home: ').strip()
876         # before we open the db, we may be doing an init
877         if command == 'initialise':
878             return self.do_initialise(self.instance_home, args)
880         # get the instance
881         try:
882             instance = roundup.instance.open(self.instance_home)
883         except ValueError, message:
884             self.instance_home = ''
885             print "Couldn't open instance: %s"%message
886             return 1
888         # only open the database once!
889         if not self.db:
890             self.db = instance.open('admin')
892         # do the command
893         ret = 0
894         try:
895             ret = function(args[1:])
896         except UsageError, message:
897             print 'Error: %s'%message
898             print function.__doc__
899             ret = 1
900         except:
901             import traceback
902             traceback.print_exc()
903             ret = 1
904         return ret
906     def interactive(self, ws_re=re.compile(r'\s+')):
907         '''Run in an interactive mode
908         '''
909         print 'Roundup {version} ready for input.'
910         print 'Type "help" for help.'
911         try:
912             import readline
913         except ImportError:
914             print "Note: command history and editing not available"
916         while 1:
917             try:
918                 command = raw_input('roundup> ')
919             except EOFError:
920                 print 'exit...'
921                 break
922             if not command: continue
923             args = ws_re.split(command)
924             if not args: continue
925             if args[0] in ('quit', 'exit'): break
926             self.run_command(args)
928         # exit.. check for transactions
929         if self.db and self.db.transactions:
930             commit = raw_input("There are unsaved changes. Commit them (y/N)? ")
931             if commit[0].lower() == 'y':
932                 self.db.commit()
933         return 0
935     def main(self):
936         try:
937             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
938         except getopt.GetoptError, e:
939             self.usage(str(e))
940             return 1
942         # handle command-line args
943         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
944         name = password = ''
945         if os.environ.has_key('ROUNDUP_LOGIN'):
946             l = os.environ['ROUNDUP_LOGIN'].split(':')
947             name = l[0]
948             if len(l) > 1:
949                 password = l[1]
950         self.comma_sep = 0
951         for opt, arg in opts:
952             if opt == '-h':
953                 self.usage()
954                 return 0
955             if opt == '-i':
956                 self.instance_home = arg
957             if opt == '-c':
958                 self.comma_sep = 1
960         # if no command - go interactive
961         ret = 0
962         if not args:
963             self.interactive()
964         else:
965             ret = self.run_command(args)
966             if self.db: self.db.commit()
967         return ret
970 if __name__ == '__main__':
971     tool = AdminTool()
972     sys.exit(tool.main())
975 # $Log: not supported by cvs2svn $
976 # Revision 1.50  2001/12/02 05:06:16  richard
977 # . We now use weakrefs in the Classes to keep the database reference, so
978 #   the close() method on the database is no longer needed.
979 #   I bumped the minimum python requirement up to 2.1 accordingly.
980 # . #487480 ] roundup-server
981 # . #487476 ] INSTALL.txt
983 # I also cleaned up the change message / post-edit stuff in the cgi client.
984 # There's now a clearly marked "TODO: append the change note" where I believe
985 # the change note should be added there. The "changes" list will obviously
986 # have to be modified to be a dict of the changes, or somesuch.
988 # More testing needed.
990 # Revision 1.49  2001/12/01 07:17:50  richard
991 # . We now have basic transaction support! Information is only written to
992 #   the database when the commit() method is called. Only the anydbm
993 #   backend is modified in this way - neither of the bsddb backends have been.
994 #   The mail, admin and cgi interfaces all use commit (except the admin tool
995 #   doesn't have a commit command, so interactive users can't commit...)
996 # . Fixed login/registration forwarding the user to the right page (or not,
997 #   on a failure)
999 # Revision 1.48  2001/11/27 22:32:03  richard
1000 # typo
1002 # Revision 1.47  2001/11/26 22:55:56  richard
1003 # Feature:
1004 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
1005 #    the instance.
1006 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1007 #    signature info in e-mails.
1008 #  . Some more flexibility in the mail gateway and more error handling.
1009 #  . Login now takes you to the page you back to the were denied access to.
1011 # Fixed:
1012 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
1014 # Revision 1.46  2001/11/21 03:40:54  richard
1015 # more new property handling
1017 # Revision 1.45  2001/11/12 22:51:59  jhermann
1018 # Fixed option & associated error handling
1020 # Revision 1.44  2001/11/12 22:01:06  richard
1021 # Fixed issues with nosy reaction and author copies.
1023 # Revision 1.43  2001/11/09 22:33:28  richard
1024 # More error handling fixes.
1026 # Revision 1.42  2001/11/09 10:11:08  richard
1027 #  . roundup-admin now handles all hyperdb exceptions
1029 # Revision 1.41  2001/11/09 01:25:40  richard
1030 # Should parse with python 1.5.2 now.
1032 # Revision 1.40  2001/11/08 04:42:00  richard
1033 # Expanded the already-abbreviated "initialise" and "specification" commands,
1034 # and added a comment to the command help about the abbreviation.
1036 # Revision 1.39  2001/11/08 04:29:59  richard
1037 # roundup-admin now accepts abbreviated commands (eg. l = li = lis = list)
1038 # [thanks Engelbert Gruber for the inspiration]
1040 # Revision 1.38  2001/11/05 23:45:40  richard
1041 # Fixed newuser_action so it sets the cookie with the unencrypted password.
1042 # Also made it present nicer error messages (not tracebacks).
1044 # Revision 1.37  2001/10/23 01:00:18  richard
1045 # Re-enabled login and registration access after lopping them off via
1046 # disabling access for anonymous users.
1047 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1048 # a couple of bugs while I was there. Probably introduced a couple, but
1049 # things seem to work OK at the moment.
1051 # Revision 1.36  2001/10/21 00:45:15  richard
1052 # Added author identification to e-mail messages from roundup.
1054 # Revision 1.35  2001/10/20 11:58:48  richard
1055 # Catch errors in login - no username or password supplied.
1056 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
1058 # Revision 1.34  2001/10/18 02:16:42  richard
1059 # Oops, committed the admin script with the wierd #! line.
1060 # Also, made the thing into a class to reduce parameter passing.
1061 # Nuked the leading whitespace from the help __doc__ displays too.
1063 # Revision 1.33  2001/10/17 23:13:19  richard
1064 # Did a fair bit of work on the admin tool. Now has an extra command "table"
1065 # which displays node information in a tabular format. Also fixed import and
1066 # export so they work. Removed freshen.
1067 # Fixed quopri usage in mailgw from bug reports.
1069 # Revision 1.32  2001/10/17 06:57:29  richard
1070 # Interactive startup blurb - need to figure how to get the version in there.
1072 # Revision 1.31  2001/10/17 06:17:26  richard
1073 # Now with readline support :)
1075 # Revision 1.30  2001/10/17 06:04:00  richard
1076 # Beginnings of an interactive mode for roundup-admin
1078 # Revision 1.29  2001/10/16 03:48:01  richard
1079 # admin tool now complains if a "find" is attempted with a non-link property.
1081 # Revision 1.28  2001/10/13 00:07:39  richard
1082 # More help in admin tool.
1084 # Revision 1.27  2001/10/11 23:43:04  richard
1085 # Implemented the comma-separated printing option in the admin tool.
1086 # Fixed a typo (more of a vim-o actually :) in mailgw.
1088 # Revision 1.26  2001/10/11 05:03:51  richard
1089 # Marked the roundup-admin import/export as experimental since they're not fully
1090 # operational.
1092 # Revision 1.25  2001/10/10 04:12:32  richard
1093 # The setup.cfg file is just causing pain. Away it goes.
1095 # Revision 1.24  2001/10/10 03:54:57  richard
1096 # Added database importing and exporting through CSV files.
1097 # Uses the csv module from object-craft for exporting if it's available.
1098 # Requires the csv module for importing.
1100 # Revision 1.23  2001/10/09 23:36:25  richard
1101 # Spit out command help if roundup-admin command doesn't get an argument.
1103 # Revision 1.22  2001/10/09 07:25:59  richard
1104 # Added the Password property type. See "pydoc roundup.password" for
1105 # implementation details. Have updated some of the documentation too.
1107 # Revision 1.21  2001/10/05 02:23:24  richard
1108 #  . roundup-admin create now prompts for property info if none is supplied
1109 #    on the command-line.
1110 #  . hyperdb Class getprops() method may now return only the mutable
1111 #    properties.
1112 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
1113 #    now support anonymous user access (read-only, unless there's an
1114 #    "anonymous" user, in which case write access is permitted). Login
1115 #    handling has been moved into cgi_client.Client.main()
1116 #  . The "extended" schema is now the default in roundup init.
1117 #  . The schemas have had their page headings modified to cope with the new
1118 #    login handling. Existing installations should copy the interfaces.py
1119 #    file from the roundup lib directory to their instance home.
1120 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
1121 #    Ping - has been removed.
1122 #  . Fixed a whole bunch of places in the CGI interface where we should have
1123 #    been returning Not Found instead of throwing an exception.
1124 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
1125 #    an item now throws an exception.
1127 # Revision 1.20  2001/10/04 02:12:42  richard
1128 # Added nicer command-line item adding: passing no arguments will enter an
1129 # interactive more which asks for each property in turn. While I was at it, I
1130 # fixed an implementation problem WRT the spec - I wasn't raising a
1131 # ValueError if the key property was missing from a create(). Also added a
1132 # protected=boolean argument to getprops() so we can list only the mutable
1133 # properties (defaults to yes, which lists the immutables).
1135 # Revision 1.19  2001/10/01 06:40:43  richard
1136 # made do_get have the args in the correct order
1138 # Revision 1.18  2001/09/18 22:58:37  richard
1140 # Added some more help to roundu-admin
1142 # Revision 1.17  2001/08/28 05:58:33  anthonybaxter
1143 # added missing 'import' statements.
1145 # Revision 1.16  2001/08/12 06:32:36  richard
1146 # using isinstance(blah, Foo) now instead of isFooType
1148 # Revision 1.15  2001/08/07 00:24:42  richard
1149 # stupid typo
1151 # Revision 1.14  2001/08/07 00:15:51  richard
1152 # Added the copyright/license notice to (nearly) all files at request of
1153 # Bizar Software.
1155 # Revision 1.13  2001/08/05 07:44:13  richard
1156 # Instances are now opened by a special function that generates a unique
1157 # module name for the instances on import time.
1159 # Revision 1.12  2001/08/03 01:28:33  richard
1160 # Used the much nicer load_package, pointed out by Steve Majewski.
1162 # Revision 1.11  2001/08/03 00:59:34  richard
1163 # Instance import now imports the instance using imp.load_module so that
1164 # we can have instance homes of "roundup" or other existing python package
1165 # names.
1167 # Revision 1.10  2001/07/30 08:12:17  richard
1168 # Added time logging and file uploading to the templates.
1170 # Revision 1.9  2001/07/30 03:52:55  richard
1171 # init help now lists templates and backends
1173 # Revision 1.8  2001/07/30 02:37:07  richard
1174 # Freshen is really broken. Commented out.
1176 # Revision 1.7  2001/07/30 01:28:46  richard
1177 # Bugfixes
1179 # Revision 1.6  2001/07/30 00:57:51  richard
1180 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
1181 # better internal structure. It's just BETTER. :)
1183 # Revision 1.5  2001/07/30 00:04:48  richard
1184 # Made the "init" prompting more friendly.
1186 # Revision 1.4  2001/07/29 07:01:39  richard
1187 # Added vim command to all source so that we don't get no steenkin' tabs :)
1189 # Revision 1.3  2001/07/23 08:45:28  richard
1190 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
1191 # workable instance_home set up :)
1192 # _and_ anydbm has had its first test :)
1194 # Revision 1.2  2001/07/23 08:20:44  richard
1195 # Moved over to using marshal in the bsddb and anydbm backends.
1196 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
1197 #  retired - mod hyperdb.Class.list() so it lists retired nodes)
1199 # Revision 1.1  2001/07/23 03:46:48  richard
1200 # moving the bin files to facilitate out-of-the-boxness
1202 # Revision 1.1  2001/07/22 11:15:45  richard
1203 # More Grande Splite stuff
1206 # vim: set filetype=python ts=4 sw=4 et si