Code

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