Commit | Line | Data |
---|---|---|
c638d827 CR |
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 | ||
20 | """Administration commands for maintaining Roundup trackers. | |
21 | """ | |
22 | __docformat__ = 'restructuredtext' | |
23 | ||
24 | import csv, getopt, getpass, os, re, shutil, sys, UserDict, operator | |
25 | ||
26 | from roundup import date, hyperdb, roundupdb, init, password, token | |
27 | from roundup import __version__ as roundup_version | |
28 | import roundup.instance | |
29 | from roundup.configuration import CoreConfig | |
30 | from roundup.i18n import _ | |
31 | from roundup.exceptions import UsageError | |
32 | ||
33 | class CommandDict(UserDict.UserDict): | |
34 | """Simple dictionary that lets us do lookups using partial keys. | |
35 | ||
36 | Original code submitted by Engelbert Gruber. | |
37 | """ | |
38 | _marker = [] | |
39 | def get(self, key, default=_marker): | |
40 | if key in self.data: | |
41 | return [(key, self.data[key])] | |
42 | keylist = sorted(self.data) | |
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 | |
50 | ||
51 | class AdminTool: | |
52 | """ A collection of methods used in maintaining Roundup trackers. | |
53 | ||
54 | Typically these methods are accessed through the roundup-admin | |
55 | script. The main() method provided on this class gives the main | |
56 | loop for the roundup-admin script. | |
57 | ||
58 | Actions are defined by do_*() methods, with help for the action | |
59 | given in the method docstring. | |
60 | ||
61 | Additional help may be supplied by help_*() methods. | |
62 | """ | |
63 | def __init__(self): | |
64 | self.commands = CommandDict() | |
65 | for k in AdminTool.__dict__: | |
66 | if k[:3] == 'do_': | |
67 | self.commands[k[3:]] = getattr(self, k) | |
68 | self.help = {} | |
69 | for k in AdminTool.__dict__: | |
70 | if k[:5] == 'help_': | |
71 | self.help[k[5:]] = getattr(self, k) | |
72 | self.tracker_home = '' | |
73 | self.db = None | |
74 | self.db_uncommitted = False | |
75 | ||
76 | def get_class(self, classname): | |
77 | """Get the class - raise an exception if it doesn't exist. | |
78 | """ | |
79 | try: | |
80 | return self.db.getclass(classname) | |
81 | except KeyError: | |
82 | raise UsageError(_('no such class "%(classname)s"')%locals()) | |
83 | ||
84 | def props_from_args(self, args): | |
85 | """ Produce a dictionary of prop: value from the args list. | |
86 | ||
87 | The args list is specified as ``prop=value prop=value ...``. | |
88 | """ | |
89 | props = {} | |
90 | for arg in args: | |
91 | if arg.find('=') == -1: | |
92 | raise UsageError(_('argument "%(arg)s" not propname=value' | |
93 | )%locals()) | |
94 | l = arg.split('=') | |
95 | if len(l) < 2: | |
96 | raise UsageError(_('argument "%(arg)s" not propname=value' | |
97 | )%locals()) | |
98 | key, value = l[0], '='.join(l[1:]) | |
99 | if value: | |
100 | props[key] = value | |
101 | else: | |
102 | props[key] = None | |
103 | return props | |
104 | ||
105 | def usage(self, message=''): | |
106 | """ Display a simple usage message. | |
107 | """ | |
108 | if message: | |
109 | message = _('Problem: %(message)s\n\n')%locals() | |
110 | print _("""%(message)sUsage: roundup-admin [options] [<command> <arguments>] | |
111 | ||
112 | Options: | |
113 | -i instance home -- specify the issue tracker "home directory" to administer | |
114 | -u -- the user[:password] to use for commands | |
115 | -d -- print full designators not just class id numbers | |
116 | -c -- when outputting lists of data, comma-separate them. | |
117 | Same as '-S ","'. | |
118 | -S <string> -- when outputting lists of data, string-separate them | |
119 | -s -- when outputting lists of data, space-separate them. | |
120 | Same as '-S " "'. | |
121 | -V -- be verbose when importing | |
122 | -v -- report Roundup and Python versions (and quit) | |
123 | ||
124 | Only one of -s, -c or -S can be specified. | |
125 | ||
126 | Help: | |
127 | roundup-admin -h | |
128 | roundup-admin help -- this help | |
129 | roundup-admin help <command> -- command-specific help | |
130 | roundup-admin help all -- all available help | |
131 | """)%locals() | |
132 | self.help_commands() | |
133 | ||
134 | def help_commands(self): | |
135 | """List the commands available with their help summary. | |
136 | """ | |
137 | print _('Commands:'), | |
138 | commands = [''] | |
139 | for command in self.commands.itervalues(): | |
140 | h = _(command.__doc__).split('\n')[0] | |
141 | commands.append(' '+h[7:]) | |
142 | commands.sort() | |
143 | commands.append(_( | |
144 | """Commands may be abbreviated as long as the abbreviation | |
145 | matches only one command, e.g. l == li == lis == list.""")) | |
146 | print '\n'.join(commands) | |
147 | ||
148 | ||
149 | def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')): | |
150 | """ Produce an HTML command list. | |
151 | """ | |
152 | commands = sorted(self.commands.itervalues(), | |
153 | operator.attrgetter('__name__')) | |
154 | for command in commands: | |
155 | h = _(command.__doc__).split('\n') | |
156 | name = command.__name__[3:] | |
157 | usage = h[0] | |
158 | print """ | |
159 | <tr><td valign=top><strong>%(name)s</strong></td> | |
160 | <td><tt>%(usage)s</tt><p> | |
161 | <pre>""" % locals() | |
162 | indent = indent_re.match(h[3]) | |
163 | if indent: indent = len(indent.group(1)) | |
164 | for line in h[3:]: | |
165 | if indent: | |
166 | print line[indent:] | |
167 | else: | |
168 | print line | |
169 | print '</pre></td></tr>\n' | |
170 | ||
171 | def help_all(self): | |
172 | print _(""" | |
173 | All commands (except help) require a tracker specifier. This is just | |
174 | the path to the roundup tracker you're working with. A roundup tracker | |
175 | is where roundup keeps the database and configuration file that defines | |
176 | an issue tracker. It may be thought of as the issue tracker's "home | |
177 | directory". It may be specified in the environment variable TRACKER_HOME | |
178 | or on the command line as "-i tracker". | |
179 | ||
180 | A designator is a classname and a nodeid concatenated, eg. bug1, user10, ... | |
181 | ||
182 | Property values are represented as strings in command arguments and in the | |
183 | printed results: | |
184 | . Strings are, well, strings. | |
185 | . Date values are printed in the full date format in the local time zone, | |
186 | and accepted in the full format or any of the partial formats explained | |
187 | below. | |
188 | . Link values are printed as node designators. When given as an argument, | |
189 | node designators and key strings are both accepted. | |
190 | . Multilink values are printed as lists of node designators joined | |
191 | by commas. When given as an argument, node designators and key | |
192 | strings are both accepted; an empty string, a single node, or a list | |
193 | of nodes joined by commas is accepted. | |
194 | ||
195 | When property values must contain spaces, just surround the value with | |
196 | quotes, either ' or ". A single space may also be backslash-quoted. If a | |
197 | value must contain a quote character, it must be backslash-quoted or inside | |
198 | quotes. Examples: | |
199 | hello world (2 tokens: hello, world) | |
200 | "hello world" (1 token: hello world) | |
201 | "Roch'e" Compaan (2 tokens: Roch'e Compaan) | |
202 | Roch\\'e Compaan (2 tokens: Roch'e Compaan) | |
203 | address="1 2 3" (1 token: address=1 2 3) | |
204 | \\\\ (1 token: \\) | |
205 | \\n\\r\\t (1 token: a newline, carriage-return and tab) | |
206 | ||
207 | When multiple nodes are specified to the roundup get or roundup set | |
208 | commands, the specified properties are retrieved or set on all the listed | |
209 | nodes. | |
210 | ||
211 | When multiple results are returned by the roundup get or roundup find | |
212 | commands, they are printed one per line (default) or joined by commas (with | |
213 | the -c) option. | |
214 | ||
215 | Where the command changes data, a login name/password is required. The | |
216 | login may be specified as either "name" or "name:password". | |
217 | . ROUNDUP_LOGIN environment variable | |
218 | . the -u command-line option | |
219 | If either the name or password is not supplied, they are obtained from the | |
220 | command-line. | |
221 | ||
222 | Date format examples: | |
223 | "2000-04-17.03:45" means <Date 2000-04-17.08:45:00> | |
224 | "2000-04-17" means <Date 2000-04-17.00:00:00> | |
225 | "01-25" means <Date yyyy-01-25.00:00:00> | |
226 | "08-13.22:13" means <Date yyyy-08-14.03:13:00> | |
227 | "11-07.09:32:43" means <Date yyyy-11-07.14:32:43> | |
228 | "14:25" means <Date yyyy-mm-dd.19:25:00> | |
229 | "8:47:11" means <Date yyyy-mm-dd.13:47:11> | |
230 | "." means "right now" | |
231 | ||
232 | Command help: | |
233 | """) | |
234 | for name, command in self.commands.items(): | |
235 | print _('%s:')%name | |
236 | print ' ', _(command.__doc__) | |
237 | ||
238 | def do_help(self, args, nl_re=re.compile('[\r\n]'), | |
239 | indent_re=re.compile(r'^(\s+)\S+')): | |
240 | ''"""Usage: help topic | |
241 | Give help about topic. | |
242 | ||
243 | commands -- list commands | |
244 | <command> -- help specific to a command | |
245 | initopts -- init command options | |
246 | all -- all available help | |
247 | """ | |
248 | if len(args)>0: | |
249 | topic = args[0] | |
250 | else: | |
251 | topic = 'help' | |
252 | ||
253 | ||
254 | # try help_ methods | |
255 | if topic in self.help: | |
256 | self.help[topic]() | |
257 | return 0 | |
258 | ||
259 | # try command docstrings | |
260 | try: | |
261 | l = self.commands.get(topic) | |
262 | except KeyError: | |
263 | print _('Sorry, no help for "%(topic)s"')%locals() | |
264 | return 1 | |
265 | ||
266 | # display the help for each match, removing the docsring indent | |
267 | for name, help in l: | |
268 | lines = nl_re.split(_(help.__doc__)) | |
269 | print lines[0] | |
270 | indent = indent_re.match(lines[1]) | |
271 | if indent: indent = len(indent.group(1)) | |
272 | for line in lines[1:]: | |
273 | if indent: | |
274 | print line[indent:] | |
275 | else: | |
276 | print line | |
277 | return 0 | |
278 | ||
279 | def listTemplates(self): | |
280 | """ List all the available templates. | |
281 | ||
282 | Look in the following places, where the later rules take precedence: | |
283 | ||
284 | 1. <roundup.admin.__file__>/../../share/roundup/templates/* | |
285 | this is where they will be if we installed an egg via easy_install | |
286 | 2. <prefix>/share/roundup/templates/* | |
287 | this should be the standard place to find them when Roundup is | |
288 | installed | |
289 | 3. <roundup.admin.__file__>/../templates/* | |
290 | this will be used if Roundup's run in the distro (aka. source) | |
291 | directory | |
292 | 4. <current working dir>/* | |
293 | this is for when someone unpacks a 3rd-party template | |
294 | 5. <current working dir> | |
295 | this is for someone who "cd"s to the 3rd-party template dir | |
296 | """ | |
297 | # OK, try <prefix>/share/roundup/templates | |
298 | # and <egg-directory>/share/roundup/templates | |
299 | # -- this module (roundup.admin) will be installed in something | |
300 | # like: | |
301 | # /usr/lib/python2.5/site-packages/roundup/admin.py (5 dirs up) | |
302 | # c:\python25\lib\site-packages\roundup\admin.py (4 dirs up) | |
303 | # /usr/lib/python2.5/site-packages/roundup-1.3.3-py2.5-egg/roundup/admin.py | |
304 | # (2 dirs up) | |
305 | # | |
306 | # we're interested in where the directory containing "share" is | |
307 | templates = {} | |
308 | for N in 2, 4, 5: | |
309 | path = __file__ | |
310 | # move up N elements in the path | |
311 | for i in range(N): | |
312 | path = os.path.dirname(path) | |
313 | tdir = os.path.join(path, 'share', 'roundup', 'templates') | |
314 | if os.path.isdir(tdir): | |
315 | templates = init.listTemplates(tdir) | |
316 | break | |
317 | ||
318 | # OK, now try as if we're in the roundup source distribution | |
319 | # directory, so this module will be in .../roundup-*/roundup/admin.py | |
320 | # and we're interested in the .../roundup-*/ part. | |
321 | path = __file__ | |
322 | for i in range(2): | |
323 | path = os.path.dirname(path) | |
324 | tdir = os.path.join(path, 'templates') | |
325 | if os.path.isdir(tdir): | |
326 | templates.update(init.listTemplates(tdir)) | |
327 | ||
328 | # Try subdirs of the current dir | |
329 | templates.update(init.listTemplates(os.getcwd())) | |
330 | ||
331 | # Finally, try the current directory as a template | |
332 | template = init.loadTemplateInfo(os.getcwd()) | |
333 | if template: | |
334 | templates[template['name']] = template | |
335 | ||
336 | return templates | |
337 | ||
338 | def help_initopts(self): | |
339 | templates = self.listTemplates() | |
340 | print _('Templates:'), ', '.join(templates) | |
341 | import roundup.backends | |
342 | backends = roundup.backends.list_backends() | |
343 | print _('Back ends:'), ', '.join(backends) | |
344 | ||
345 | def do_install(self, tracker_home, args): | |
346 | ''"""Usage: install [template [backend [key=val[,key=val]]]] | |
347 | Install a new Roundup tracker. | |
348 | ||
349 | The command will prompt for the tracker home directory | |
350 | (if not supplied through TRACKER_HOME or the -i option). | |
351 | The template and backend may be specified on the command-line | |
352 | as arguments, in that order. | |
353 | ||
354 | Command line arguments following the backend allows you to | |
355 | pass initial values for config options. For example, passing | |
356 | "web_http_auth=no,rdbms_user=dinsdale" will override defaults | |
357 | for options http_auth in section [web] and user in section [rdbms]. | |
358 | Please be careful to not use spaces in this argument! (Enclose | |
359 | whole argument in quotes if you need spaces in option value). | |
360 | ||
361 | The initialise command must be called after this command in order | |
362 | to initialise the tracker's database. You may edit the tracker's | |
363 | initial database contents before running that command by editing | |
364 | the tracker's dbinit.py module init() function. | |
365 | ||
366 | See also initopts help. | |
367 | """ | |
368 | if len(args) < 1: | |
369 | raise UsageError(_('Not enough arguments supplied')) | |
370 | ||
371 | # make sure the tracker home can be created | |
372 | tracker_home = os.path.abspath(tracker_home) | |
373 | parent = os.path.split(tracker_home)[0] | |
374 | if not os.path.exists(parent): | |
375 | raise UsageError(_('Instance home parent directory "%(parent)s"' | |
376 | ' does not exist')%locals()) | |
377 | ||
378 | config_ini_file = os.path.join(tracker_home, CoreConfig.INI_FILE) | |
379 | # check for both old- and new-style configs | |
380 | if list(filter(os.path.exists, [config_ini_file, | |
381 | os.path.join(tracker_home, 'config.py')])): | |
382 | ok = raw_input(_( | |
383 | """WARNING: There appears to be a tracker in "%(tracker_home)s"! | |
384 | If you re-install it, you will lose all the data! | |
385 | Erase it? Y/N: """) % locals()) | |
386 | if ok.strip().lower() != 'y': | |
387 | return 0 | |
388 | ||
389 | # clear it out so the install isn't confused | |
390 | shutil.rmtree(tracker_home) | |
391 | ||
392 | # select template | |
393 | templates = self.listTemplates() | |
394 | template = len(args) > 1 and args[1] or '' | |
395 | if template not in templates: | |
396 | print _('Templates:'), ', '.join(templates) | |
397 | while template not in templates: | |
398 | template = raw_input(_('Select template [classic]: ')).strip() | |
399 | if not template: | |
400 | template = 'classic' | |
401 | ||
402 | # select hyperdb backend | |
403 | import roundup.backends | |
404 | backends = roundup.backends.list_backends() | |
405 | backend = len(args) > 2 and args[2] or '' | |
406 | if backend not in backends: | |
407 | print _('Back ends:'), ', '.join(backends) | |
408 | while backend not in backends: | |
409 | backend = raw_input(_('Select backend [anydbm]: ')).strip() | |
410 | if not backend: | |
411 | backend = 'anydbm' | |
412 | # XXX perform a unit test based on the user's selections | |
413 | ||
414 | # Process configuration file definitions | |
415 | if len(args) > 3: | |
416 | try: | |
417 | defns = dict([item.split("=") for item in args[3].split(",")]) | |
418 | except: | |
419 | print _('Error in configuration settings: "%s"') % args[3] | |
420 | raise | |
421 | else: | |
422 | defns = {} | |
423 | ||
424 | # install! | |
425 | init.install(tracker_home, templates[template]['path'], settings=defns) | |
426 | init.write_select_db(tracker_home, backend) | |
427 | ||
428 | print _(""" | |
429 | --------------------------------------------------------------------------- | |
430 | You should now edit the tracker configuration file: | |
431 | %(config_file)s""") % {"config_file": config_ini_file} | |
432 | ||
433 | # find list of options that need manual adjustments | |
434 | # XXX config._get_unset_options() is marked as private | |
435 | # (leading underscore). make it public or don't care? | |
436 | need_set = CoreConfig(tracker_home)._get_unset_options() | |
437 | if need_set: | |
438 | print _(" ... at a minimum, you must set following options:") | |
439 | for section in need_set: | |
440 | print " [%s]: %s" % (section, ", ".join(need_set[section])) | |
441 | ||
442 | # note about schema modifications | |
443 | print _(""" | |
444 | If you wish to modify the database schema, | |
445 | you should also edit the schema file: | |
446 | %(database_config_file)s | |
447 | You may also change the database initialisation file: | |
448 | %(database_init_file)s | |
449 | ... see the documentation on customizing for more information. | |
450 | ||
451 | You MUST run the "roundup-admin initialise" command once you've performed | |
452 | the above steps. | |
453 | --------------------------------------------------------------------------- | |
454 | """) % { | |
455 | 'database_config_file': os.path.join(tracker_home, 'schema.py'), | |
456 | 'database_init_file': os.path.join(tracker_home, 'initial_data.py'), | |
457 | } | |
458 | return 0 | |
459 | ||
460 | def do_genconfig(self, args): | |
461 | ''"""Usage: genconfig <filename> | |
462 | Generate a new tracker config file (ini style) with default values | |
463 | in <filename>. | |
464 | """ | |
465 | if len(args) < 1: | |
466 | raise UsageError(_('Not enough arguments supplied')) | |
467 | config = CoreConfig() | |
468 | config.save(args[0]) | |
469 | ||
470 | def do_initialise(self, tracker_home, args): | |
471 | ''"""Usage: initialise [adminpw] | |
472 | Initialise a new Roundup tracker. | |
473 | ||
474 | The administrator details will be set at this step. | |
475 | ||
476 | Execute the tracker's initialisation function dbinit.init() | |
477 | """ | |
478 | # password | |
479 | if len(args) > 1: | |
480 | adminpw = args[1] | |
481 | else: | |
482 | adminpw = '' | |
483 | confirm = 'x' | |
484 | while adminpw != confirm: | |
485 | adminpw = getpass.getpass(_('Admin Password: ')) | |
486 | confirm = getpass.getpass(_(' Confirm: ')) | |
487 | ||
488 | # make sure the tracker home is installed | |
489 | if not os.path.exists(tracker_home): | |
490 | raise UsageError(_('Instance home does not exist')%locals()) | |
491 | try: | |
492 | tracker = roundup.instance.open(tracker_home) | |
493 | except roundup.instance.TrackerError: | |
494 | raise UsageError(_('Instance has not been installed')%locals()) | |
495 | ||
496 | # is there already a database? | |
497 | if tracker.exists(): | |
498 | ok = raw_input(_( | |
499 | """WARNING: The database is already initialised! | |
500 | If you re-initialise it, you will lose all the data! | |
501 | Erase it? Y/N: """)) | |
502 | if ok.strip().lower() != 'y': | |
503 | return 0 | |
504 | ||
505 | backend = tracker.get_backend_name() | |
506 | ||
507 | # nuke it | |
508 | tracker.nuke() | |
509 | ||
510 | # re-write the backend select file | |
511 | init.write_select_db(tracker_home, backend, tracker.config.DATABASE) | |
512 | ||
513 | # GO | |
514 | tracker.init(password.Password(adminpw)) | |
515 | ||
516 | return 0 | |
517 | ||
518 | ||
519 | def do_get(self, args): | |
520 | ''"""Usage: get property designator[,designator]* | |
521 | Get the given property of one or more designator(s). | |
522 | ||
523 | A designator is a classname and a nodeid concatenated, | |
524 | eg. bug1, user10, ... | |
525 | ||
526 | Retrieves the property value of the nodes specified | |
527 | by the designators. | |
528 | """ | |
529 | if len(args) < 2: | |
530 | raise UsageError(_('Not enough arguments supplied')) | |
531 | propname = args[0] | |
532 | designators = args[1].split(',') | |
533 | l = [] | |
534 | for designator in designators: | |
535 | # decode the node designator | |
536 | try: | |
537 | classname, nodeid = hyperdb.splitDesignator(designator) | |
538 | except hyperdb.DesignatorError, message: | |
539 | raise UsageError(message) | |
540 | ||
541 | # get the class | |
542 | cl = self.get_class(classname) | |
543 | try: | |
544 | id=[] | |
545 | if self.separator: | |
546 | if self.print_designator: | |
547 | # see if property is a link or multilink for | |
548 | # which getting a desginator make sense. | |
549 | # Algorithm: Get the properties of the | |
550 | # current designator's class. (cl.getprops) | |
551 | # get the property object for the property the | |
552 | # user requested (properties[propname]) | |
553 | # verify its type (isinstance...) | |
554 | # raise error if not link/multilink | |
555 | # get class name for link/multilink property | |
556 | # do the get on the designators | |
557 | # append the new designators | |
558 | ||
559 | properties = cl.getprops() | |
560 | property = properties[propname] | |
561 | if not (isinstance(property, hyperdb.Multilink) or | |
562 | isinstance(property, hyperdb.Link)): | |
563 | raise UsageError(_('property %s is not of type' | |
564 | ' Multilink or Link so -d flag does not ' | |
565 | 'apply.')%propname) | |
566 | propclassname = self.db.getclass(property.classname).classname | |
567 | id = cl.get(nodeid, propname) | |
568 | for i in id: | |
569 | l.append(propclassname + i) | |
570 | else: | |
571 | id = cl.get(nodeid, propname) | |
572 | for i in id: | |
573 | l.append(i) | |
574 | else: | |
575 | if self.print_designator: | |
576 | properties = cl.getprops() | |
577 | property = properties[propname] | |
578 | if not (isinstance(property, hyperdb.Multilink) or | |
579 | isinstance(property, hyperdb.Link)): | |
580 | raise UsageError(_('property %s is not of type' | |
581 | ' Multilink or Link so -d flag does not ' | |
582 | 'apply.')%propname) | |
583 | propclassname = self.db.getclass(property.classname).classname | |
584 | id = cl.get(nodeid, propname) | |
585 | for i in id: | |
586 | print propclassname + i | |
587 | else: | |
588 | print cl.get(nodeid, propname) | |
589 | except IndexError: | |
590 | raise UsageError(_('no such %(classname)s node ' | |
591 | '"%(nodeid)s"')%locals()) | |
592 | except KeyError: | |
593 | raise UsageError(_('no such %(classname)s property ' | |
594 | '"%(propname)s"')%locals()) | |
595 | if self.separator: | |
596 | print self.separator.join(l) | |
597 | ||
598 | return 0 | |
599 | ||
600 | ||
601 | def do_set(self, args): | |
602 | ''"""Usage: set items property=value property=value ... | |
603 | Set the given properties of one or more items(s). | |
604 | ||
605 | The items are specified as a class or as a comma-separated | |
606 | list of item designators (ie "designator[,designator,...]"). | |
607 | ||
608 | A designator is a classname and a nodeid concatenated, | |
609 | eg. bug1, user10, ... | |
610 | ||
611 | This command sets the properties to the values for all designators | |
612 | given. If the value is missing (ie. "property=") then the property | |
613 | is un-set. If the property is a multilink, you specify the linked | |
614 | ids for the multilink as comma-separated numbers (ie "1,2,3"). | |
615 | """ | |
616 | if len(args) < 2: | |
617 | raise UsageError(_('Not enough arguments supplied')) | |
618 | from roundup import hyperdb | |
619 | ||
620 | designators = args[0].split(',') | |
621 | if len(designators) == 1: | |
622 | designator = designators[0] | |
623 | try: | |
624 | designator = hyperdb.splitDesignator(designator) | |
625 | designators = [designator] | |
626 | except hyperdb.DesignatorError: | |
627 | cl = self.get_class(designator) | |
628 | designators = [(designator, x) for x in cl.list()] | |
629 | else: | |
630 | try: | |
631 | designators = [hyperdb.splitDesignator(x) for x in designators] | |
632 | except hyperdb.DesignatorError, message: | |
633 | raise UsageError(message) | |
634 | ||
635 | # get the props from the args | |
636 | props = self.props_from_args(args[1:]) | |
637 | ||
638 | # now do the set for all the nodes | |
639 | for classname, itemid in designators: | |
640 | cl = self.get_class(classname) | |
641 | ||
642 | properties = cl.getprops() | |
643 | for key, value in props.items(): | |
644 | try: | |
645 | props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid, | |
646 | key, value) | |
647 | except hyperdb.HyperdbValueError, message: | |
648 | raise UsageError(message) | |
649 | ||
650 | # try the set | |
651 | try: | |
652 | cl.set(itemid, **props) | |
653 | except (TypeError, IndexError, ValueError), message: | |
654 | import traceback; traceback.print_exc() | |
655 | raise UsageError(message) | |
656 | self.db_uncommitted = True | |
657 | return 0 | |
658 | ||
659 | def do_find(self, args): | |
660 | ''"""Usage: find classname propname=value ... | |
661 | Find the nodes of the given class with a given link property value. | |
662 | ||
663 | Find the nodes of the given class with a given link property value. | |
664 | The value may be either the nodeid of the linked node, or its key | |
665 | value. | |
666 | """ | |
667 | if len(args) < 1: | |
668 | raise UsageError(_('Not enough arguments supplied')) | |
669 | classname = args[0] | |
670 | # get the class | |
671 | cl = self.get_class(classname) | |
672 | ||
673 | # handle the propname=value argument | |
674 | props = self.props_from_args(args[1:]) | |
675 | ||
676 | # convert the user-input value to a value used for find() | |
677 | for propname, value in props.iteritems(): | |
678 | if ',' in value: | |
679 | values = value.split(',') | |
680 | else: | |
681 | values = [value] | |
682 | d = props[propname] = {} | |
683 | for value in values: | |
684 | value = hyperdb.rawToHyperdb(self.db, cl, None, propname, value) | |
685 | if isinstance(value, list): | |
686 | for entry in value: | |
687 | d[entry] = 1 | |
688 | else: | |
689 | d[value] = 1 | |
690 | ||
691 | # now do the find | |
692 | try: | |
693 | id = [] | |
694 | designator = [] | |
695 | if self.separator: | |
696 | if self.print_designator: | |
697 | id = cl.find(**props) | |
698 | for i in id: | |
699 | designator.append(classname + i) | |
700 | print self.separator.join(designator) | |
701 | else: | |
702 | print self.separator.join(cl.find(**props)) | |
703 | ||
704 | else: | |
705 | if self.print_designator: | |
706 | id = cl.find(**props) | |
707 | for i in id: | |
708 | designator.append(classname + i) | |
709 | print designator | |
710 | else: | |
711 | print cl.find(**props) | |
712 | except KeyError: | |
713 | raise UsageError(_('%(classname)s has no property ' | |
714 | '"%(propname)s"')%locals()) | |
715 | except (ValueError, TypeError), message: | |
716 | raise UsageError(message) | |
717 | return 0 | |
718 | ||
719 | def do_specification(self, args): | |
720 | ''"""Usage: specification classname | |
721 | Show the properties for a classname. | |
722 | ||
723 | This lists the properties for a given class. | |
724 | """ | |
725 | if len(args) < 1: | |
726 | raise UsageError(_('Not enough arguments supplied')) | |
727 | classname = args[0] | |
728 | # get the class | |
729 | cl = self.get_class(classname) | |
730 | ||
731 | # get the key property | |
732 | keyprop = cl.getkey() | |
733 | for key in cl.properties: | |
734 | value = cl.properties[key] | |
735 | if keyprop == key: | |
736 | print _('%(key)s: %(value)s (key property)')%locals() | |
737 | else: | |
738 | print _('%(key)s: %(value)s')%locals() | |
739 | ||
740 | def do_display(self, args): | |
741 | ''"""Usage: display designator[,designator]* | |
742 | Show the property values for the given node(s). | |
743 | ||
744 | A designator is a classname and a nodeid concatenated, | |
745 | eg. bug1, user10, ... | |
746 | ||
747 | This lists the properties and their associated values for the given | |
748 | node. | |
749 | """ | |
750 | if len(args) < 1: | |
751 | raise UsageError(_('Not enough arguments supplied')) | |
752 | ||
753 | # decode the node designator | |
754 | for designator in args[0].split(','): | |
755 | try: | |
756 | classname, nodeid = hyperdb.splitDesignator(designator) | |
757 | except hyperdb.DesignatorError, message: | |
758 | raise UsageError(message) | |
759 | ||
760 | # get the class | |
761 | cl = self.get_class(classname) | |
762 | ||
763 | # display the values | |
764 | keys = sorted(cl.properties) | |
765 | for key in keys: | |
766 | value = cl.get(nodeid, key) | |
767 | print _('%(key)s: %(value)s')%locals() | |
768 | ||
769 | def do_create(self, args): | |
770 | ''"""Usage: create classname property=value ... | |
771 | Create a new entry of a given class. | |
772 | ||
773 | This creates a new entry of the given class using the property | |
774 | name=value arguments provided on the command line after the "create" | |
775 | command. | |
776 | """ | |
777 | if len(args) < 1: | |
778 | raise UsageError(_('Not enough arguments supplied')) | |
779 | from roundup import hyperdb | |
780 | ||
781 | classname = args[0] | |
782 | ||
783 | # get the class | |
784 | cl = self.get_class(classname) | |
785 | ||
786 | # now do a create | |
787 | props = {} | |
788 | properties = cl.getprops(protected = 0) | |
789 | if len(args) == 1: | |
790 | # ask for the properties | |
791 | for key in properties: | |
792 | if key == 'id': continue | |
793 | value = properties[key] | |
794 | name = value.__class__.__name__ | |
795 | if isinstance(value , hyperdb.Password): | |
796 | again = None | |
797 | while value != again: | |
798 | value = getpass.getpass(_('%(propname)s (Password): ')%{ | |
799 | 'propname': key.capitalize()}) | |
800 | again = getpass.getpass(_(' %(propname)s (Again): ')%{ | |
801 | 'propname': key.capitalize()}) | |
802 | if value != again: print _('Sorry, try again...') | |
803 | if value: | |
804 | props[key] = value | |
805 | else: | |
806 | value = raw_input(_('%(propname)s (%(proptype)s): ')%{ | |
807 | 'propname': key.capitalize(), 'proptype': name}) | |
808 | if value: | |
809 | props[key] = value | |
810 | else: | |
811 | props = self.props_from_args(args[1:]) | |
812 | ||
813 | # convert types | |
814 | for propname in props: | |
815 | try: | |
816 | props[propname] = hyperdb.rawToHyperdb(self.db, cl, None, | |
817 | propname, props[propname]) | |
818 | except hyperdb.HyperdbValueError, message: | |
819 | raise UsageError(message) | |
820 | ||
821 | # check for the key property | |
822 | propname = cl.getkey() | |
823 | if propname and propname not in props: | |
824 | raise UsageError(_('you must provide the "%(propname)s" ' | |
825 | 'property.')%locals()) | |
826 | ||
827 | # do the actual create | |
828 | try: | |
829 | print cl.create(**props) | |
830 | except (TypeError, IndexError, ValueError), message: | |
831 | raise UsageError(message) | |
832 | self.db_uncommitted = True | |
833 | return 0 | |
834 | ||
835 | def do_list(self, args): | |
836 | ''"""Usage: list classname [property] | |
837 | List the instances of a class. | |
838 | ||
839 | Lists all instances of the given class. If the property is not | |
840 | specified, the "label" property is used. The label property is | |
841 | tried in order: the key, "name", "title" and then the first | |
842 | property, alphabetically. | |
843 | ||
844 | With -c, -S or -s print a list of item id's if no property | |
845 | specified. If property specified, print list of that property | |
846 | for every class instance. | |
847 | """ | |
848 | if len(args) > 2: | |
849 | raise UsageError(_('Too many arguments supplied')) | |
850 | if len(args) < 1: | |
851 | raise UsageError(_('Not enough arguments supplied')) | |
852 | classname = args[0] | |
853 | ||
854 | # get the class | |
855 | cl = self.get_class(classname) | |
856 | ||
857 | # figure the property | |
858 | if len(args) > 1: | |
859 | propname = args[1] | |
860 | else: | |
861 | propname = cl.labelprop() | |
862 | ||
863 | if self.separator: | |
864 | if len(args) == 2: | |
865 | # create a list of propnames since user specified propname | |
866 | proplist=[] | |
867 | for nodeid in cl.list(): | |
868 | try: | |
869 | proplist.append(cl.get(nodeid, propname)) | |
870 | except KeyError: | |
871 | raise UsageError(_('%(classname)s has no property ' | |
872 | '"%(propname)s"')%locals()) | |
873 | print self.separator.join(proplist) | |
874 | else: | |
875 | # create a list of index id's since user didn't specify | |
876 | # otherwise | |
877 | print self.separator.join(cl.list()) | |
878 | else: | |
879 | for nodeid in cl.list(): | |
880 | try: | |
881 | value = cl.get(nodeid, propname) | |
882 | except KeyError: | |
883 | raise UsageError(_('%(classname)s has no property ' | |
884 | '"%(propname)s"')%locals()) | |
885 | print _('%(nodeid)4s: %(value)s')%locals() | |
886 | return 0 | |
887 | ||
888 | def do_table(self, args): | |
889 | ''"""Usage: table classname [property[,property]*] | |
890 | List the instances of a class in tabular form. | |
891 | ||
892 | Lists all instances of the given class. If the properties are not | |
893 | specified, all properties are displayed. By default, the column | |
894 | widths are the width of the largest value. The width may be | |
895 | explicitly defined by defining the property as "name:width". | |
896 | For example:: | |
897 | ||
898 | roundup> table priority id,name:10 | |
899 | Id Name | |
900 | 1 fatal-bug | |
901 | 2 bug | |
902 | 3 usability | |
903 | 4 feature | |
904 | ||
905 | Also to make the width of the column the width of the label, | |
906 | leave a trailing : without a width on the property. For example:: | |
907 | ||
908 | roundup> table priority id,name: | |
909 | Id Name | |
910 | 1 fata | |
911 | 2 bug | |
912 | 3 usab | |
913 | 4 feat | |
914 | ||
915 | will result in a the 4 character wide "Name" column. | |
916 | """ | |
917 | if len(args) < 1: | |
918 | raise UsageError(_('Not enough arguments supplied')) | |
919 | classname = args[0] | |
920 | ||
921 | # get the class | |
922 | cl = self.get_class(classname) | |
923 | ||
924 | # figure the property names to display | |
925 | if len(args) > 1: | |
926 | prop_names = args[1].split(',') | |
927 | all_props = cl.getprops() | |
928 | for spec in prop_names: | |
929 | if ':' in spec: | |
930 | try: | |
931 | propname, width = spec.split(':') | |
932 | except (ValueError, TypeError): | |
933 | raise UsageError(_('"%(spec)s" not ' | |
934 | 'name:width')%locals()) | |
935 | else: | |
936 | propname = spec | |
937 | if propname not in all_props: | |
938 | raise UsageError(_('%(classname)s has no property ' | |
939 | '"%(propname)s"')%locals()) | |
940 | else: | |
941 | prop_names = cl.getprops() | |
942 | ||
943 | # now figure column widths | |
944 | props = [] | |
945 | for spec in prop_names: | |
946 | if ':' in spec: | |
947 | name, width = spec.split(':') | |
948 | if width == '': | |
949 | props.append((name, len(spec))) | |
950 | else: | |
951 | props.append((name, int(width))) | |
952 | else: | |
953 | # this is going to be slow | |
954 | maxlen = len(spec) | |
955 | for nodeid in cl.list(): | |
956 | curlen = len(str(cl.get(nodeid, spec))) | |
957 | if curlen > maxlen: | |
958 | maxlen = curlen | |
959 | props.append((spec, maxlen)) | |
960 | ||
961 | # now display the heading | |
962 | print ' '.join([name.capitalize().ljust(width) for name,width in props]) | |
963 | ||
964 | # and the table data | |
965 | for nodeid in cl.list(): | |
966 | l = [] | |
967 | for name, width in props: | |
968 | if name != 'id': | |
969 | try: | |
970 | value = str(cl.get(nodeid, name)) | |
971 | except KeyError: | |
972 | # we already checked if the property is valid - a | |
973 | # KeyError here means the node just doesn't have a | |
974 | # value for it | |
975 | value = '' | |
976 | else: | |
977 | value = str(nodeid) | |
978 | f = '%%-%ds'%width | |
979 | l.append(f%value[:width]) | |
980 | print ' '.join(l) | |
981 | return 0 | |
982 | ||
983 | def do_history(self, args): | |
984 | ''"""Usage: history designator | |
985 | Show the history entries of a designator. | |
986 | ||
987 | A designator is a classname and a nodeid concatenated, | |
988 | eg. bug1, user10, ... | |
989 | ||
990 | Lists the journal entries for the node identified by the designator. | |
991 | """ | |
992 | if len(args) < 1: | |
993 | raise UsageError(_('Not enough arguments supplied')) | |
994 | try: | |
995 | classname, nodeid = hyperdb.splitDesignator(args[0]) | |
996 | except hyperdb.DesignatorError, message: | |
997 | raise UsageError(message) | |
998 | ||
999 | try: | |
1000 | print self.db.getclass(classname).history(nodeid) | |
1001 | except KeyError: | |
1002 | raise UsageError(_('no such class "%(classname)s"')%locals()) | |
1003 | except IndexError: | |
1004 | raise UsageError(_('no such %(classname)s node ' | |
1005 | '"%(nodeid)s"')%locals()) | |
1006 | return 0 | |
1007 | ||
1008 | def do_commit(self, args): | |
1009 | ''"""Usage: commit | |
1010 | Commit changes made to the database during an interactive session. | |
1011 | ||
1012 | The changes made during an interactive session are not | |
1013 | automatically written to the database - they must be committed | |
1014 | using this command. | |
1015 | ||
1016 | One-off commands on the command-line are automatically committed if | |
1017 | they are successful. | |
1018 | """ | |
1019 | self.db.commit() | |
1020 | self.db_uncommitted = False | |
1021 | return 0 | |
1022 | ||
1023 | def do_rollback(self, args): | |
1024 | ''"""Usage: rollback | |
1025 | Undo all changes that are pending commit to the database. | |
1026 | ||
1027 | The changes made during an interactive session are not | |
1028 | automatically written to the database - they must be committed | |
1029 | manually. This command undoes all those changes, so a commit | |
1030 | immediately after would make no changes to the database. | |
1031 | """ | |
1032 | self.db.rollback() | |
1033 | self.db_uncommitted = False | |
1034 | return 0 | |
1035 | ||
1036 | def do_retire(self, args): | |
1037 | ''"""Usage: retire designator[,designator]* | |
1038 | Retire the node specified by designator. | |
1039 | ||
1040 | A designator is a classname and a nodeid concatenated, | |
1041 | eg. bug1, user10, ... | |
1042 | ||
1043 | This action indicates that a particular node is not to be retrieved | |
1044 | by the list or find commands, and its key value may be re-used. | |
1045 | """ | |
1046 | if len(args) < 1: | |
1047 | raise UsageError(_('Not enough arguments supplied')) | |
1048 | designators = args[0].split(',') | |
1049 | for designator in designators: | |
1050 | try: | |
1051 | classname, nodeid = hyperdb.splitDesignator(designator) | |
1052 | except hyperdb.DesignatorError, message: | |
1053 | raise UsageError(message) | |
1054 | try: | |
1055 | self.db.getclass(classname).retire(nodeid) | |
1056 | except KeyError: | |
1057 | raise UsageError(_('no such class "%(classname)s"')%locals()) | |
1058 | except IndexError: | |
1059 | raise UsageError(_('no such %(classname)s node ' | |
1060 | '"%(nodeid)s"')%locals()) | |
1061 | self.db_uncommitted = True | |
1062 | return 0 | |
1063 | ||
1064 | def do_restore(self, args): | |
1065 | ''"""Usage: restore designator[,designator]* | |
1066 | Restore the retired node specified by designator. | |
1067 | ||
1068 | A designator is a classname and a nodeid concatenated, | |
1069 | eg. bug1, user10, ... | |
1070 | ||
1071 | The given nodes will become available for users again. | |
1072 | """ | |
1073 | if len(args) < 1: | |
1074 | raise UsageError(_('Not enough arguments supplied')) | |
1075 | designators = args[0].split(',') | |
1076 | for designator in designators: | |
1077 | try: | |
1078 | classname, nodeid = hyperdb.splitDesignator(designator) | |
1079 | except hyperdb.DesignatorError, message: | |
1080 | raise UsageError(message) | |
1081 | try: | |
1082 | self.db.getclass(classname).restore(nodeid) | |
1083 | except KeyError: | |
1084 | raise UsageError(_('no such class "%(classname)s"')%locals()) | |
1085 | except IndexError: | |
1086 | raise UsageError(_('no such %(classname)s node ' | |
1087 | '"%(nodeid)s"')%locals()) | |
1088 | self.db_uncommitted = True | |
1089 | return 0 | |
1090 | ||
1091 | def do_export(self, args, export_files=True): | |
1092 | ''"""Usage: export [[-]class[,class]] export_dir | |
1093 | Export the database to colon-separated-value files. | |
1094 | To exclude the files (e.g. for the msg or file class), | |
1095 | use the exporttables command. | |
1096 | ||
1097 | Optionally limit the export to just the named classes | |
1098 | or exclude the named classes, if the 1st argument starts with '-'. | |
1099 | ||
1100 | This action exports the current data from the database into | |
1101 | colon-separated-value files that are placed in the nominated | |
1102 | destination directory. | |
1103 | """ | |
1104 | # grab the directory to export to | |
1105 | if len(args) < 1: | |
1106 | raise UsageError(_('Not enough arguments supplied')) | |
1107 | ||
1108 | dir = args[-1] | |
1109 | ||
1110 | # get the list of classes to export | |
1111 | if len(args) == 2: | |
1112 | if args[0].startswith('-'): | |
1113 | classes = [ c for c in self.db.classes | |
1114 | if not c in args[0][1:].split(',') ] | |
1115 | else: | |
1116 | classes = args[0].split(',') | |
1117 | else: | |
1118 | classes = self.db.classes | |
1119 | ||
1120 | class colon_separated(csv.excel): | |
1121 | delimiter = ':' | |
1122 | ||
1123 | # make sure target dir exists | |
1124 | if not os.path.exists(dir): | |
1125 | os.makedirs(dir) | |
1126 | ||
1127 | # maximum csv field length exceeding configured size? | |
1128 | max_len = self.db.config.CSV_FIELD_SIZE | |
1129 | ||
1130 | # do all the classes specified | |
1131 | for classname in classes: | |
1132 | cl = self.get_class(classname) | |
1133 | ||
1134 | if not export_files and hasattr(cl, 'export_files'): | |
1135 | sys.stdout.write('Exporting %s WITHOUT the files\r\n'% | |
1136 | classname) | |
1137 | ||
1138 | f = open(os.path.join(dir, classname+'.csv'), 'wb') | |
1139 | writer = csv.writer(f, colon_separated) | |
1140 | ||
1141 | properties = cl.getprops() | |
1142 | propnames = cl.export_propnames() | |
1143 | fields = propnames[:] | |
1144 | fields.append('is retired') | |
1145 | writer.writerow(fields) | |
1146 | ||
1147 | # all nodes for this class | |
1148 | for nodeid in cl.getnodeids(): | |
1149 | if self.verbose: | |
1150 | sys.stdout.write('\rExporting %s - %s'%(classname, nodeid)) | |
1151 | sys.stdout.flush() | |
1152 | node = cl.getnode(nodeid) | |
1153 | exp = cl.export_list(propnames, nodeid) | |
1154 | lensum = sum ([len (repr(node[p])) for p in propnames]) | |
1155 | # for a safe upper bound of field length we add | |
1156 | # difference between CSV len and sum of all field lengths | |
1157 | d = sum ([len(x) for x in exp]) - lensum | |
1158 | assert (d > 0) | |
1159 | for p in propnames: | |
1160 | ll = len(repr(node[p])) + d | |
1161 | if ll > max_len: | |
1162 | max_len = ll | |
1163 | writer.writerow(exp) | |
1164 | if export_files and hasattr(cl, 'export_files'): | |
1165 | cl.export_files(dir, nodeid) | |
1166 | ||
1167 | # close this file | |
1168 | f.close() | |
1169 | ||
1170 | # export the journals | |
1171 | jf = open(os.path.join(dir, classname+'-journals.csv'), 'wb') | |
1172 | if self.verbose: | |
1173 | sys.stdout.write("\nExporting Journal for %s\n" % classname) | |
1174 | sys.stdout.flush() | |
1175 | journals = csv.writer(jf, colon_separated) | |
1176 | for row in cl.export_journals(): | |
1177 | journals.writerow(row) | |
1178 | jf.close() | |
1179 | if max_len > self.db.config.CSV_FIELD_SIZE: | |
1180 | print >> sys.stderr, \ | |
1181 | "Warning: config csv_field_size should be at least %s"%max_len | |
1182 | return 0 | |
1183 | ||
1184 | def do_exporttables(self, args): | |
1185 | ''"""Usage: exporttables [[-]class[,class]] export_dir | |
1186 | Export the database to colon-separated-value files, excluding the | |
1187 | files below $TRACKER_HOME/db/files/ (which can be archived separately). | |
1188 | To include the files, use the export command. | |
1189 | ||
1190 | Optionally limit the export to just the named classes | |
1191 | or exclude the named classes, if the 1st argument starts with '-'. | |
1192 | ||
1193 | This action exports the current data from the database into | |
1194 | colon-separated-value files that are placed in the nominated | |
1195 | destination directory. | |
1196 | """ | |
1197 | return self.do_export(args, export_files=False) | |
1198 | ||
1199 | def do_import(self, args): | |
1200 | ''"""Usage: import import_dir | |
1201 | Import a database from the directory containing CSV files, | |
1202 | two per class to import. | |
1203 | ||
1204 | The files used in the import are: | |
1205 | ||
1206 | <class>.csv | |
1207 | This must define the same properties as the class (including | |
1208 | having a "header" line with those property names.) | |
1209 | <class>-journals.csv | |
1210 | This defines the journals for the items being imported. | |
1211 | ||
1212 | The imported nodes will have the same nodeid as defined in the | |
1213 | import file, thus replacing any existing content. | |
1214 | ||
1215 | The new nodes are added to the existing database - if you want to | |
1216 | create a new database using the imported data, then create a new | |
1217 | database (or, tediously, retire all the old data.) | |
1218 | """ | |
1219 | if len(args) < 1: | |
1220 | raise UsageError(_('Not enough arguments supplied')) | |
1221 | from roundup import hyperdb | |
1222 | ||
1223 | if hasattr (csv, 'field_size_limit'): | |
1224 | csv.field_size_limit(self.db.config.CSV_FIELD_SIZE) | |
1225 | ||
1226 | # directory to import from | |
1227 | dir = args[0] | |
1228 | ||
1229 | class colon_separated(csv.excel): | |
1230 | delimiter = ':' | |
1231 | ||
1232 | # import all the files | |
1233 | for file in os.listdir(dir): | |
1234 | classname, ext = os.path.splitext(file) | |
1235 | # we only care about CSV files | |
1236 | if ext != '.csv' or classname.endswith('-journals'): | |
1237 | continue | |
1238 | ||
1239 | cl = self.get_class(classname) | |
1240 | ||
1241 | # ensure that the properties and the CSV file headings match | |
1242 | f = open(os.path.join(dir, file), 'r') | |
1243 | reader = csv.reader(f, colon_separated) | |
1244 | file_props = None | |
1245 | maxid = 1 | |
1246 | # loop through the file and create a node for each entry | |
1247 | for n, r in enumerate(reader): | |
1248 | if file_props is None: | |
1249 | file_props = r | |
1250 | continue | |
1251 | ||
1252 | if self.verbose: | |
1253 | sys.stdout.write('\rImporting %s - %s'%(classname, n)) | |
1254 | sys.stdout.flush() | |
1255 | ||
1256 | # do the import and figure the current highest nodeid | |
1257 | nodeid = cl.import_list(file_props, r) | |
1258 | if hasattr(cl, 'import_files'): | |
1259 | cl.import_files(dir, nodeid) | |
1260 | maxid = max(maxid, int(nodeid)) | |
1261 | ||
1262 | # (print to sys.stdout here to allow tests to squash it .. ugh) | |
1263 | print >> sys.stdout | |
1264 | ||
1265 | f.close() | |
1266 | ||
1267 | # import the journals | |
1268 | f = open(os.path.join(args[0], classname + '-journals.csv'), 'r') | |
1269 | reader = csv.reader(f, colon_separated) | |
1270 | cl.import_journals(reader) | |
1271 | f.close() | |
1272 | ||
1273 | # (print to sys.stdout here to allow tests to squash it .. ugh) | |
1274 | print >> sys.stdout, 'setting', classname, maxid+1 | |
1275 | ||
1276 | # set the id counter | |
1277 | self.db.setid(classname, str(maxid+1)) | |
1278 | ||
1279 | self.db_uncommitted = True | |
1280 | return 0 | |
1281 | ||
1282 | def do_pack(self, args): | |
1283 | ''"""Usage: pack period | date | |
1284 | ||
1285 | Remove journal entries older than a period of time specified or | |
1286 | before a certain date. | |
1287 | ||
1288 | A period is specified using the suffixes "y", "m", and "d". The | |
1289 | suffix "w" (for "week") means 7 days. | |
1290 | ||
1291 | "3y" means three years | |
1292 | "2y 1m" means two years and one month | |
1293 | "1m 25d" means one month and 25 days | |
1294 | "2w 3d" means two weeks and three days | |
1295 | ||
1296 | Date format is "YYYY-MM-DD" eg: | |
1297 | 2001-01-01 | |
1298 | ||
1299 | """ | |
1300 | if len(args) != 1: | |
1301 | raise UsageError(_('Not enough arguments supplied')) | |
1302 | ||
1303 | # are we dealing with a period or a date | |
1304 | value = args[0] | |
1305 | date_re = re.compile(r""" | |
1306 | (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd | |
1307 | (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)? | |
1308 | """, re.VERBOSE) | |
1309 | m = date_re.match(value) | |
1310 | if not m: | |
1311 | raise ValueError(_('Invalid format')) | |
1312 | m = m.groupdict() | |
1313 | if m['period']: | |
1314 | pack_before = date.Date(". - %s"%value) | |
1315 | elif m['date']: | |
1316 | pack_before = date.Date(value) | |
1317 | self.db.pack(pack_before) | |
1318 | self.db_uncommitted = True | |
1319 | return 0 | |
1320 | ||
1321 | def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')): | |
1322 | ''"""Usage: reindex [classname|designator]* | |
1323 | Re-generate a tracker's search indexes. | |
1324 | ||
1325 | This will re-generate the search indexes for a tracker. | |
1326 | This will typically happen automatically. | |
1327 | """ | |
1328 | if args: | |
1329 | for arg in args: | |
1330 | m = desre.match(arg) | |
1331 | if m: | |
1332 | cl = self.get_class(m.group(1)) | |
1333 | try: | |
1334 | cl.index(m.group(2)) | |
1335 | except IndexError: | |
1336 | raise UsageError(_('no such item "%(designator)s"')%{ | |
1337 | 'designator': arg}) | |
1338 | else: | |
1339 | cl = self.get_class(arg) | |
1340 | self.db.reindex(arg) | |
1341 | else: | |
1342 | self.db.reindex(show_progress=True) | |
1343 | return 0 | |
1344 | ||
1345 | def do_security(self, args): | |
1346 | ''"""Usage: security [Role name] | |
1347 | Display the Permissions available to one or all Roles. | |
1348 | """ | |
1349 | if len(args) == 1: | |
1350 | role = args[0] | |
1351 | try: | |
1352 | roles = [(args[0], self.db.security.role[args[0]])] | |
1353 | except KeyError: | |
1354 | print _('No such Role "%(role)s"')%locals() | |
1355 | return 1 | |
1356 | else: | |
1357 | roles = list(self.db.security.role.items()) | |
1358 | role = self.db.config.NEW_WEB_USER_ROLES | |
1359 | if ',' in role: | |
1360 | print _('New Web users get the Roles "%(role)s"')%locals() | |
1361 | else: | |
1362 | print _('New Web users get the Role "%(role)s"')%locals() | |
1363 | role = self.db.config.NEW_EMAIL_USER_ROLES | |
1364 | if ',' in role: | |
1365 | print _('New Email users get the Roles "%(role)s"')%locals() | |
1366 | else: | |
1367 | print _('New Email users get the Role "%(role)s"')%locals() | |
1368 | roles.sort() | |
1369 | for rolename, role in roles: | |
1370 | print _('Role "%(name)s":')%role.__dict__ | |
1371 | for permission in role.permissions: | |
1372 | d = permission.__dict__ | |
1373 | if permission.klass: | |
1374 | if permission.properties: | |
1375 | print _(' %(description)s (%(name)s for "%(klass)s"' | |
1376 | ': %(properties)s only)')%d | |
1377 | else: | |
1378 | print _(' %(description)s (%(name)s for "%(klass)s" ' | |
1379 | 'only)')%d | |
1380 | else: | |
1381 | print _(' %(description)s (%(name)s)')%d | |
1382 | return 0 | |
1383 | ||
1384 | ||
1385 | def do_migrate(self, args): | |
1386 | ''"""Usage: migrate | |
1387 | Update a tracker's database to be compatible with the Roundup | |
1388 | codebase. | |
1389 | ||
1390 | You should run the "migrate" command for your tracker once you've | |
1391 | installed the latest codebase. | |
1392 | ||
1393 | Do this before you use the web, command-line or mail interface and | |
1394 | before any users access the tracker. | |
1395 | ||
1396 | This command will respond with either "Tracker updated" (if you've | |
1397 | not previously run it on an RDBMS backend) or "No migration action | |
1398 | required" (if you have run it, or have used another interface to the | |
1399 | tracker, or possibly because you are using anydbm). | |
1400 | ||
1401 | It's safe to run this even if it's not required, so just get into | |
1402 | the habit. | |
1403 | """ | |
1404 | if getattr(self.db, 'db_version_updated'): | |
1405 | print _('Tracker updated') | |
1406 | self.db_uncommitted = True | |
1407 | else: | |
1408 | print _('No migration action required') | |
1409 | return 0 | |
1410 | ||
1411 | def run_command(self, args): | |
1412 | """Run a single command | |
1413 | """ | |
1414 | command = args[0] | |
1415 | ||
1416 | # handle help now | |
1417 | if command == 'help': | |
1418 | if len(args)>1: | |
1419 | self.do_help(args[1:]) | |
1420 | return 0 | |
1421 | self.do_help(['help']) | |
1422 | return 0 | |
1423 | if command == 'morehelp': | |
1424 | self.do_help(['help']) | |
1425 | self.help_commands() | |
1426 | self.help_all() | |
1427 | return 0 | |
1428 | if command == 'config': | |
1429 | self.do_config(args[1:]) | |
1430 | return 0 | |
1431 | ||
1432 | # figure what the command is | |
1433 | try: | |
1434 | functions = self.commands.get(command) | |
1435 | except KeyError: | |
1436 | # not a valid command | |
1437 | print _('Unknown command "%(command)s" ("help commands" for a ' | |
1438 | 'list)')%locals() | |
1439 | return 1 | |
1440 | ||
1441 | # check for multiple matches | |
1442 | if len(functions) > 1: | |
1443 | print _('Multiple commands match "%(command)s": %(list)s')%{'command': | |
1444 | command, 'list': ', '.join([i[0] for i in functions])} | |
1445 | return 1 | |
1446 | command, function = functions[0] | |
1447 | ||
1448 | # make sure we have a tracker_home | |
1449 | while not self.tracker_home: | |
1450 | self.tracker_home = raw_input(_('Enter tracker home: ')).strip() | |
1451 | ||
1452 | # before we open the db, we may be doing an install or init | |
1453 | if command == 'initialise': | |
1454 | try: | |
1455 | return self.do_initialise(self.tracker_home, args) | |
1456 | except UsageError, message: | |
1457 | print _('Error: %(message)s')%locals() | |
1458 | return 1 | |
1459 | elif command == 'install': | |
1460 | try: | |
1461 | return self.do_install(self.tracker_home, args) | |
1462 | except UsageError, message: | |
1463 | print _('Error: %(message)s')%locals() | |
1464 | return 1 | |
1465 | ||
1466 | # get the tracker | |
1467 | try: | |
1468 | tracker = roundup.instance.open(self.tracker_home) | |
1469 | except ValueError, message: | |
1470 | self.tracker_home = '' | |
1471 | print _("Error: Couldn't open tracker: %(message)s")%locals() | |
1472 | return 1 | |
1473 | ||
1474 | # only open the database once! | |
1475 | if not self.db: | |
1476 | self.db = tracker.open('admin') | |
1477 | ||
1478 | # do the command | |
1479 | ret = 0 | |
1480 | try: | |
1481 | ret = function(args[1:]) | |
1482 | except UsageError, message: | |
1483 | print _('Error: %(message)s')%locals() | |
1484 | ||
1485 | print function.__doc__ | |
1486 | ret = 1 | |
1487 | except: | |
1488 | import traceback | |
1489 | traceback.print_exc() | |
1490 | ret = 1 | |
1491 | return ret | |
1492 | ||
1493 | def interactive(self): | |
1494 | """Run in an interactive mode | |
1495 | """ | |
1496 | print _('Roundup %s ready for input.\nType "help" for help.' | |
1497 | % roundup_version) | |
1498 | try: | |
1499 | import readline | |
1500 | except ImportError: | |
1501 | print _('Note: command history and editing not available') | |
1502 | ||
1503 | while 1: | |
1504 | try: | |
1505 | command = raw_input(_('roundup> ')) | |
1506 | except EOFError: | |
1507 | print _('exit...') | |
1508 | break | |
1509 | if not command: continue | |
1510 | args = token.token_split(command) | |
1511 | if not args: continue | |
1512 | if args[0] in ('quit', 'exit'): break | |
1513 | self.run_command(args) | |
1514 | ||
1515 | # exit.. check for transactions | |
1516 | if self.db and self.db_uncommitted: | |
1517 | commit = raw_input(_('There are unsaved changes. Commit them (y/N)? ')) | |
1518 | if commit and commit[0].lower() == 'y': | |
1519 | self.db.commit() | |
1520 | return 0 | |
1521 | ||
1522 | def main(self): | |
1523 | try: | |
1524 | opts, args = getopt.getopt(sys.argv[1:], 'i:u:hcdsS:vV') | |
1525 | except getopt.GetoptError, e: | |
1526 | self.usage(str(e)) | |
1527 | return 1 | |
1528 | ||
1529 | # handle command-line args | |
1530 | self.tracker_home = os.environ.get('TRACKER_HOME', '') | |
1531 | # TODO: reinstate the user/password stuff (-u arg too) | |
1532 | name = password = '' | |
1533 | if 'ROUNDUP_LOGIN' in os.environ: | |
1534 | l = os.environ['ROUNDUP_LOGIN'].split(':') | |
1535 | name = l[0] | |
1536 | if len(l) > 1: | |
1537 | password = l[1] | |
1538 | self.separator = None | |
1539 | self.print_designator = 0 | |
1540 | self.verbose = 0 | |
1541 | for opt, arg in opts: | |
1542 | if opt == '-h': | |
1543 | self.usage() | |
1544 | return 0 | |
1545 | elif opt == '-v': | |
1546 | print '%s (python %s)'%(roundup_version, sys.version.split()[0]) | |
1547 | return 0 | |
1548 | elif opt == '-V': | |
1549 | self.verbose = 1 | |
1550 | elif opt == '-i': | |
1551 | self.tracker_home = arg | |
1552 | elif opt == '-c': | |
1553 | if self.separator != None: | |
1554 | self.usage('Only one of -c, -S and -s may be specified') | |
1555 | return 1 | |
1556 | self.separator = ',' | |
1557 | elif opt == '-S': | |
1558 | if self.separator != None: | |
1559 | self.usage('Only one of -c, -S and -s may be specified') | |
1560 | return 1 | |
1561 | self.separator = arg | |
1562 | elif opt == '-s': | |
1563 | if self.separator != None: | |
1564 | self.usage('Only one of -c, -S and -s may be specified') | |
1565 | return 1 | |
1566 | self.separator = ' ' | |
1567 | elif opt == '-d': | |
1568 | self.print_designator = 1 | |
1569 | ||
1570 | # if no command - go interactive | |
1571 | # wrap in a try/finally so we always close off the db | |
1572 | ret = 0 | |
1573 | try: | |
1574 | if not args: | |
1575 | self.interactive() | |
1576 | else: | |
1577 | ret = self.run_command(args) | |
1578 | if self.db: self.db.commit() | |
1579 | return ret | |
1580 | finally: | |
1581 | if self.db: | |
1582 | self.db.close() | |
1583 | ||
1584 | if __name__ == '__main__': | |
1585 | tool = AdminTool() | |
1586 | sys.exit(tool.main()) | |
1587 | ||
1588 | # vim: set filetype=python sts=4 sw=4 et si : |