Code

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