Code

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