comparison examples/trac/trac/scripts/admin.py @ 39:93b4dcbafd7b trunk

Copy Trac to main branch.
author cmlenz
date Mon, 03 Jul 2006 18:53:27 +0000
parents
children
comparison
equal deleted inserted replaced
38:ee669cb9cccc 39:93b4dcbafd7b
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2003-2006 Edgewall Software
4 # All rights reserved.
5 #
6 # This software is licensed as described in the file COPYING, which
7 # you should have received as part of this distribution. The terms
8 # are also available at http://trac.edgewall.com/license.html.
9 #
10 # This software consists of voluntary contributions made by many
11 # individuals. For the exact contribution history, see the revision
12 # history and logs, available at http://projects.edgewall.com/trac/.
13 #
14
15 __copyright__ = 'Copyright (c) 2003-2006 Edgewall Software'
16
17 import cmd
18 import getpass
19 import os
20 import shlex
21 import shutil
22 import StringIO
23 import sys
24 import time
25 import traceback
26 import urllib
27 import locale
28
29 import trac
30 from trac import perm, util, db_default
31 from trac.config import default_dir
32 from trac.core import TracError
33 from trac.env import Environment
34 from trac.perm import PermissionSystem
35 from trac.ticket.model import *
36 from trac.util.markup import html
37 from trac.util.text import to_unicode, wrap
38 from trac.wiki import WikiPage
39 from trac.wiki.macros import WikiMacroBase
40
41 def copytree(src, dst, symlinks=False, skip=[]):
42 """Recursively copy a directory tree using copy2() (from shutil.copytree.)
43
44 Added a `skip` parameter consisting of absolute paths
45 which we don't want to copy.
46 """
47 names = os.listdir(src)
48 os.mkdir(dst)
49 errors = []
50 for name in names:
51 srcname = os.path.join(src, name)
52 if srcname in skip:
53 continue
54 dstname = os.path.join(dst, name)
55 try:
56 if symlinks and os.path.islink(srcname):
57 linkto = os.readlink(srcname)
58 os.symlink(linkto, dstname)
59 elif os.path.isdir(srcname):
60 copytree(srcname, dstname, symlinks, skip)
61 else:
62 shutil.copy2(srcname, dstname)
63 # XXX What about devices, sockets etc.?
64 except (IOError, os.error), why:
65 errors.append((srcname, dstname, why))
66 if errors:
67 raise shutil.Error, errors
68
69
70 class TracAdmin(cmd.Cmd):
71 intro = ''
72 license = trac.__license_long__
73 doc_header = 'Trac Admin Console %(ver)s\n' \
74 'Available Commands:\n' \
75 % {'ver':trac.__version__ }
76 ruler = ''
77 prompt = "Trac> "
78 __env = None
79 _date_format = '%Y-%m-%d'
80 _datetime_format = '%Y-%m-%d %H:%M:%S'
81 _date_format_hint = 'YYYY-MM-DD'
82
83 def __init__(self, envdir=None):
84 cmd.Cmd.__init__(self)
85 self.interactive = False
86 if envdir:
87 self.env_set(os.path.abspath(envdir))
88 self._permsys = None
89
90 def emptyline(self):
91 pass
92
93 def onecmd(self, line):
94 """`line` may be a `str` or an `unicode` object"""
95 try:
96 if isinstance(line, str):
97 line = to_unicode(line, sys.stdin.encoding)
98 rv = cmd.Cmd.onecmd(self, line) or 0
99 except SystemExit:
100 raise
101 except Exception, e:
102 print>>sys.stderr, 'Command failed: %s' % e
103 rv = 2
104 if not self.interactive:
105 return rv
106
107 def run(self):
108 self.interactive = True
109 print 'Welcome to trac-admin %(ver)s\n' \
110 'Interactive Trac administration console.\n' \
111 '%(copy)s\n\n' \
112 "Type: '?' or 'help' for help on commands.\n" % \
113 {'ver':trac.__version__,'copy':__copyright__}
114 self.cmdloop()
115
116 ##
117 ## Environment methods
118 ##
119
120 def env_set(self, envname, env=None):
121 self.envname = envname
122 self.prompt = "Trac [%s]> " % self.envname
123 if env is not None:
124 self.__env = env
125
126 def env_check(self):
127 try:
128 self.__env = Environment(self.envname)
129 except:
130 return 0
131 return 1
132
133 def env_open(self):
134 try:
135 if not self.__env:
136 self.__env = Environment(self.envname)
137 return self.__env
138 except Exception, e:
139 print 'Failed to open environment.', e
140 traceback.print_exc()
141 sys.exit(1)
142
143 def db_open(self):
144 return self.env_open().get_db_cnx()
145
146 def db_query(self, sql, cursor=None, params=None):
147 if not cursor:
148 cnx = self.db_open()
149 cursor = cnx.cursor()
150 if params:
151 cursor.execute(sql, params)
152 else:
153 cursor.execute(sql)
154 for row in cursor:
155 yield row
156
157 def db_update(self, sql, cursor=None, params=None):
158 if not cursor:
159 cnx = self.db_open()
160 cursor = cnx.cursor()
161 else:
162 cnx = None
163 if params:
164 cursor.execute(sql, params)
165 else:
166 cursor.execute(sql)
167 if cnx:
168 cnx.commit()
169
170 ##
171 ## Utility methods
172 ##
173
174 def arg_tokenize (self, argstr):
175 """`argstr` is an `unicode` string
176
177 ... but shlex is not unicode friendly.
178 """
179 return [unicode(token, 'utf-8')
180 for token in shlex.split(argstr.encode('utf-8'))] or ['']
181
182 def word_complete (self, text, words):
183 return [a for a in words if a.startswith (text)]
184
185 def print_listing(self, headers, data, sep=' ', decor=True):
186 cons_charset = sys.stdout.encoding
187 ldata = list(data)
188 if decor:
189 ldata.insert(0, headers)
190 print
191 colw = []
192 ncols = len(ldata[0]) # assumes all rows are of equal length
193 for cnum in xrange(0, ncols):
194 mw = 0
195 for cell in [unicode(d[cnum]) or '' for d in ldata]:
196 if len(cell) > mw:
197 mw = len(cell)
198 colw.append(mw)
199 for rnum in xrange(len(ldata)):
200 for cnum in xrange(ncols):
201 if decor and rnum == 0:
202 sp = ('%%%ds' % len(sep)) % ' ' # No separator in header
203 else:
204 sp = sep
205 if cnum + 1 == ncols:
206 sp = '' # No separator after last column
207 pdata = ((u'%%-%ds%s' % (colw[cnum], sp))
208 % (ldata[rnum][cnum] or ''))
209 if cons_charset and isinstance(pdata, unicode):
210 pdata = pdata.encode(cons_charset, 'replace')
211 print pdata,
212 print
213 if rnum == 0 and decor:
214 print ''.join(['-' for x in
215 xrange(0, (1 + len(sep)) * cnum + sum(colw))])
216 print
217
218 def print_doc(cls, docs, stream=None):
219 if stream is None:
220 stream = sys.stdout
221 if not docs: return
222 for cmd, doc in docs:
223 print>>stream, cmd
224 print>>stream, '\t-- %s\n' % doc
225 print_doc = classmethod(print_doc)
226
227 def get_component_list(self):
228 rows = self.db_query("SELECT name FROM component")
229 return [row[0] for row in rows]
230
231 def get_user_list(self):
232 rows = self.db_query("SELECT DISTINCT username FROM permission")
233 return [row[0] for row in rows]
234
235 def get_wiki_list(self):
236 rows = self.db_query('SELECT DISTINCT name FROM wiki')
237 return [row[0] for row in rows]
238
239 def get_dir_list(self, pathstr, justdirs=False):
240 dname = os.path.dirname(pathstr)
241 d = os.path.join(os.getcwd(), dname)
242 dlist = os.listdir(d)
243 if justdirs:
244 result = []
245 for entry in dlist:
246 try:
247 if os.path.isdir(entry):
248 result.append(entry)
249 except:
250 pass
251 else:
252 result = dlist
253 return result
254
255 def get_enum_list(self, type):
256 rows = self.db_query("SELECT name FROM enum WHERE type=%s",
257 params=[type])
258 return [row[0] for row in rows]
259
260 def get_milestone_list(self):
261 rows = self.db_query("SELECT name FROM milestone")
262 return [row[0] for row in rows]
263
264 def get_version_list(self):
265 rows = self.db_query("SELECT name FROM version")
266 return [row[0] for row in rows]
267
268 def _parse_date(self, t):
269 seconds = None
270 t = t.strip()
271 if t == 'now':
272 seconds = int(time.time())
273 else:
274 for format in [self._date_format, '%x %X', '%x, %X', '%X %x',
275 '%X, %x', '%x', '%c', '%b %d, %Y']:
276 try:
277 pt = time.strptime(t, format)
278 seconds = int(time.mktime(pt))
279 except ValueError:
280 continue
281 break
282 if seconds == None:
283 try:
284 seconds = int(t)
285 except ValueError:
286 pass
287 if seconds == None:
288 print>>sys.stderr, 'Unknown time format %s' % t
289 return seconds
290
291 def _format_date(self, s):
292 return time.strftime(self._date_format, time.localtime(s))
293
294 def _format_datetime(self, s):
295 return time.strftime(self._datetime_format, time.localtime(s))
296
297
298 ##
299 ## Available Commands
300 ##
301
302 ## Help
303 _help_help = [('help', 'Show documentation')]
304
305 def all_docs(cls):
306 return (cls._help_about + cls._help_help +
307 cls._help_initenv + cls._help_hotcopy +
308 cls._help_resync + cls._help_upgrade +
309 cls._help_wiki +
310 # cls._help_config + cls._help_wiki +
311 cls._help_permission + cls._help_component +
312 cls._help_ticket +
313 cls._help_ticket_type + cls._help_priority +
314 cls._help_severity + cls._help_version +
315 cls._help_milestone)
316 all_docs = classmethod(all_docs)
317
318 def do_help(self, line=None):
319 arg = self.arg_tokenize(line)
320 if arg[0]:
321 try:
322 doc = getattr(self, "_help_" + arg[0])
323 self.print_doc(doc)
324 except AttributeError:
325 print "No documentation found for '%s'" % arg[0]
326 else:
327 print 'trac-admin - The Trac Administration Console %s' \
328 % trac.__version__
329 if not self.interactive:
330 print
331 print "Usage: trac-admin </path/to/projenv> [command [subcommand] [option ...]]\n"
332 print "Invoking trac-admin without command starts "\
333 "interactive mode."
334 self.print_doc(self.all_docs())
335
336
337 ## About / Version
338 _help_about = [('about', 'Shows information about trac-admin')]
339
340 def do_about(self, line):
341 print
342 print 'Trac Admin Console %s' % trac.__version__
343 print '================================================================='
344 print self.license
345
346
347 ## Quit / EOF
348 _help_quit = [['quit', 'Exit the program']]
349 _help_exit = _help_quit
350 _help_EOF = _help_quit
351
352 def do_quit(self, line):
353 print
354 sys.exit()
355
356 do_exit = do_quit # Alias
357 do_EOF = do_quit # Alias
358
359
360 # Component
361 _help_component = [('component list', 'Show available components'),
362 ('component add <name> <owner>', 'Add a new component'),
363 ('component rename <name> <newname>',
364 'Rename a component'),
365 ('component remove <name>',
366 'Remove/uninstall component'),
367 ('component chown <name> <owner>',
368 'Change component ownership')]
369
370 def complete_component(self, text, line, begidx, endidx):
371 if begidx in (16, 17):
372 comp = self.get_component_list()
373 elif begidx > 15 and line.startswith('component chown '):
374 comp = self.get_user_list()
375 else:
376 comp = ['list', 'add', 'rename', 'remove', 'chown']
377 return self.word_complete(text, comp)
378
379 def do_component(self, line):
380 arg = self.arg_tokenize(line)
381 if arg[0] == 'list':
382 self._do_component_list()
383 elif arg[0] == 'add' and len(arg)==3:
384 name = arg[1]
385 owner = arg[2]
386 self._do_component_add(name, owner)
387 elif arg[0] == 'rename' and len(arg)==3:
388 name = arg[1]
389 newname = arg[2]
390 self._do_component_rename(name, newname)
391 elif arg[0] == 'remove' and len(arg)==2:
392 name = arg[1]
393 self._do_component_remove(name)
394 elif arg[0] == 'chown' and len(arg)==3:
395 name = arg[1]
396 owner = arg[2]
397 self._do_component_set_owner(name, owner)
398 else:
399 self.do_help ('component')
400
401 def _do_component_list(self):
402 data = []
403 for c in Component.select(self.env_open()):
404 data.append((c.name, c.owner))
405 self.print_listing(['Name', 'Owner'], data)
406
407 def _do_component_add(self, name, owner):
408 component = Component(self.env_open())
409 component.name = name
410 component.owner = owner
411 component.insert()
412
413 def _do_component_rename(self, name, newname):
414 component = Component(self.env_open(), name)
415 component.name = newname
416 component.update()
417
418 def _do_component_remove(self, name):
419 component = Component(self.env_open(), name)
420 component.delete()
421
422 def _do_component_set_owner(self, name, owner):
423 component = Component(self.env_open(), name)
424 component.owner = owner
425 component.update()
426
427
428 ## Permission
429 _help_permission = [('permission list [user]', 'List permission rules'),
430 ('permission add <user> <action> [action] [...]',
431 'Add a new permission rule'),
432 ('permission remove <user> <action> [action] [...]',
433 'Remove permission rule')]
434
435 def complete_permission(self, text, line, begidx, endidx):
436 argv = self.arg_tokenize(line)
437 argc = len(argv)
438 if line[-1] == ' ': # Space starts new argument
439 argc += 1
440 if argc == 2:
441 comp = ['list', 'add', 'remove']
442 elif argc >= 4:
443 comp = perm.permissions + perm.meta_permissions.keys()
444 comp.sort()
445 return self.word_complete(text, comp)
446
447 def do_permission(self, line):
448 arg = self.arg_tokenize(line)
449 if arg[0] == 'list':
450 user = None
451 if len(arg) > 1:
452 user = arg[1]
453 self._do_permission_list(user)
454 elif arg[0] == 'add' and len(arg) >= 3:
455 user = arg[1]
456 for action in arg[2:]:
457 self._do_permission_add(user, action)
458 elif arg[0] == 'remove' and len(arg) >= 3:
459 user = arg[1]
460 for action in arg[2:]:
461 self._do_permission_remove(user, action)
462 else:
463 self.do_help('permission')
464
465 def _do_permission_list(self, user=None):
466 if not self._permsys:
467 self._permsys = PermissionSystem(self.env_open())
468 if user:
469 rows = []
470 perms = self._permsys.get_user_permissions(user)
471 for action in perms:
472 if perms[action]:
473 rows.append((user, action))
474 else:
475 rows = self._permsys.get_all_permissions()
476 rows.sort()
477 self.print_listing(['User', 'Action'], rows)
478 print
479 print 'Available actions:'
480 actions = self._permsys.get_actions()
481 actions.sort()
482 text = ', '.join(actions)
483 print wrap(text, initial_indent=' ', subsequent_indent=' ',
484 linesep='\n')
485 print
486
487 def _do_permission_add(self, user, action):
488 if not self._permsys:
489 self._permsys = PermissionSystem(self.env_open())
490 if not action.islower() and not action.isupper():
491 print 'Group names must be in lower case and actions in upper case'
492 return
493 self._permsys.grant_permission(user, action)
494
495 def _do_permission_remove(self, user, action):
496 if not self._permsys:
497 self._permsys = PermissionSystem(self.env_open())
498 rows = self._permsys.get_all_permissions()
499 if action == '*':
500 for row in rows:
501 if user != '*' and user != row[0]:
502 continue
503 self._permsys.revoke_permission(row[0], row[1])
504 else:
505 for row in rows:
506 if action != row[1]:
507 continue
508 if user != '*' and user != row[0]:
509 continue
510 self._permsys.revoke_permission(row[0], row[1])
511
512 ## Initenv
513 _help_initenv = [('initenv',
514 'Create and initialize a new environment interactively'),
515 ('initenv <projectname> <db> <repostype> <repospath> <templatepath>',
516 'Create and initialize a new environment from arguments')]
517
518 def do_initdb(self, line):
519 self.do_initenv(line)
520
521 def get_initenv_args(self):
522 returnvals = []
523 print 'Creating a new Trac environment at %s' % self.envname
524 print
525 print 'Trac will first ask a few questions about your environment '
526 print 'in order to initalize and prepare the project database.'
527 print
528 print " Please enter the name of your project."
529 print " This name will be used in page titles and descriptions."
530 print
531 dp = 'My Project'
532 returnvals.append(raw_input('Project Name [%s]> ' % dp).strip() or dp)
533 print
534 print ' Please specify the connection string for the database to use.'
535 print ' By default, a local SQLite database is created in the environment '
536 print ' directory. It is also possible to use an already existing '
537 print ' PostgreSQL database (check the Trac documentation for the exact '
538 print ' connection string syntax).'
539 print
540 ddb = 'sqlite:db/trac.db'
541 prompt = 'Database connection string [%s]> ' % ddb
542 returnvals.append(raw_input(prompt).strip() or ddb)
543 print
544 print ' Please specify the type of version control system,'
545 print ' By default, it will be svn.'
546 print
547 print ' If you don\'t want to use Trac with version control integration, '
548 print ' choose the default here and don\'t specify a repository directory. '
549 print ' in the next question.'
550 print
551 drpt = 'svn'
552 prompt = 'Repository type [%s]> ' % drpt
553 returnvals.append(raw_input(prompt).strip() or drpt)
554 print
555 print ' Please specify the absolute path to the version control '
556 print ' repository, or leave it blank to use Trac without a repository.'
557 print ' You can also set the repository location later.'
558 print
559 prompt = 'Path to repository [/path/to/repos]> '
560 returnvals.append(raw_input(prompt).strip())
561 print
562 print ' Please enter location of Trac page templates.'
563 print ' Default is the location of the site-wide templates installed with Trac.'
564 print
565 dt = default_dir('templates')
566 prompt = 'Templates directory [%s]> ' % dt
567 returnvals.append(raw_input(prompt).strip() or dt)
568 print
569 return returnvals
570
571 def do_initenv(self, line):
572 if self.env_check():
573 print "Initenv for '%s' failed." % self.envname
574 print "Does an environment already exist?"
575 return 2
576
577 if os.path.exists(self.envname) and os.listdir(self.envname):
578 print "Initenv for '%s' failed." % self.envname
579 print "Directory exists and is not empty."
580 return 2
581
582 arg = self.arg_tokenize(line)
583 project_name = None
584 db_str = None
585 repository_dir = None
586 templates_dir = None
587 if len(arg) == 1 and not arg[0]:
588 returnvals = self.get_initenv_args()
589 project_name, db_str, repository_type, repository_dir, \
590 templates_dir = returnvals
591 elif len(arg) != 5:
592 print 'Wrong number of arguments to initenv: %d' % len(arg)
593 return 2
594 else:
595 project_name, db_str, repository_type, repository_dir, \
596 templates_dir = arg[:5]
597
598 if not os.access(os.path.join(templates_dir, 'header.cs'), os.F_OK):
599 print templates_dir, "doesn't look like a Trac templates directory"
600 return 2
601
602 try:
603 print 'Creating and Initializing Project'
604 options = [
605 ('trac', 'database', db_str),
606 ('trac', 'repository_type', repository_type),
607 ('trac', 'repository_dir', repository_dir),
608 ('trac', 'templates_dir', templates_dir),
609 ('project', 'name', project_name),
610 ]
611 try:
612 self.__env = Environment(self.envname, create=True,
613 options=options)
614 except Exception, e:
615 print 'Failed to create environment.', e
616 traceback.print_exc()
617 sys.exit(1)
618
619 # Add a few default wiki pages
620 print ' Installing default wiki pages'
621 cnx = self.__env.get_db_cnx()
622 cursor = cnx.cursor()
623 self._do_wiki_load(default_dir('wiki'), cursor)
624 cnx.commit()
625
626 if repository_dir:
627 try:
628 repos = self.__env.get_repository()
629 if repos:
630 print ' Indexing repository'
631 repos.sync()
632 except TracError, e:
633 print>>sys.stderr, "\nWarning:\n"
634 if repository_type == "svn":
635 print>>sys.stderr, "You should install the SVN bindings"
636 else:
637 print>>sys.stderr, "Repository type %s not supported" \
638 % repository_type
639 except Exception, e:
640 print 'Failed to initialize environment.', e
641 traceback.print_exc()
642 return 2
643
644 print """
645 ---------------------------------------------------------------------
646 Project environment for '%(project_name)s' created.
647
648 You may now configure the environment by editing the file:
649
650 %(config_path)s
651
652 If you'd like to take this new project environment for a test drive,
653 try running the Trac standalone web server `tracd`:
654
655 tracd --port 8000 %(project_path)s
656
657 Then point your browser to http://localhost:8000/%(project_dir)s.
658 There you can also browse the documentation for your installed
659 version of Trac, including information on further setup (such as
660 deploying Trac to a real web server).
661
662 The latest documentation can also always be found on the project
663 website:
664
665 http://projects.edgewall.com/trac/
666
667 Congratulations!
668 """ % dict(project_name=project_name, project_path=self.envname,
669 project_dir=os.path.basename(self.envname),
670 config_path=os.path.join(self.envname, 'conf', 'trac.ini'))
671
672 _help_resync = [('resync', 'Re-synchronize trac with the repository')]
673
674 ## Resync
675 def do_resync(self, line):
676 print 'Resyncing repository history...'
677 cnx = self.db_open()
678 cursor = cnx.cursor()
679 cursor.execute("DELETE FROM revision")
680 cursor.execute("DELETE FROM node_change")
681 repos = self.__env.get_repository()
682 cursor.execute("DELETE FROM system WHERE name='repository_dir'")
683 cursor.execute("INSERT INTO system (name,value) "
684 "VALUES ('repository_dir',%s)", (repos.name,))
685 repos.sync()
686 print 'Done.'
687
688 ## Wiki
689 _help_wiki = [('wiki list', 'List wiki pages'),
690 ('wiki remove <name>', 'Remove wiki page'),
691 ('wiki export <page> [file]',
692 'Export wiki page to file or stdout'),
693 ('wiki import <page> [file]',
694 'Import wiki page from file or stdin'),
695 ('wiki dump <directory>',
696 'Export all wiki pages to files named by title'),
697 ('wiki load <directory>',
698 'Import all wiki pages from directory'),
699 ('wiki upgrade',
700 'Upgrade default wiki pages to current version')]
701
702 def complete_wiki(self, text, line, begidx, endidx):
703 argv = self.arg_tokenize(line)
704 argc = len(argv)
705 if line[-1] == ' ': # Space starts new argument
706 argc += 1
707 if argc == 2:
708 comp = ['list', 'remove', 'import', 'export', 'dump', 'load',
709 'upgrade']
710 else:
711 if argv[1] in ('dump', 'load'):
712 comp = self.get_dir_list(argv[-1], 1)
713 elif argv[1] == 'remove':
714 comp = self.get_wiki_list()
715 elif argv[1] in ('export', 'import'):
716 if argc == 3:
717 comp = self.get_wiki_list()
718 elif argc == 4:
719 comp = self.get_dir_list(argv[-1])
720 return self.word_complete(text, comp)
721
722 def do_wiki(self, line):
723 arg = self.arg_tokenize(line)
724 if arg[0] == 'list':
725 self._do_wiki_list()
726 elif arg[0] == 'remove' and len(arg)==2:
727 name = arg[1]
728 self._do_wiki_remove(name)
729 elif arg[0] == 'import' and len(arg) == 3:
730 title = arg[1]
731 file = arg[2]
732 self._do_wiki_import(file, title)
733 elif arg[0] == 'export' and len(arg) in [2,3]:
734 page = arg[1]
735 file = (len(arg) == 3 and arg[2]) or None
736 self._do_wiki_export(page, file)
737 elif arg[0] == 'dump' and len(arg) in [1,2]:
738 dir = (len(arg) == 2 and arg[1]) or ''
739 self._do_wiki_dump(dir)
740 elif arg[0] == 'load' and len(arg) in [1,2]:
741 dir = (len(arg) == 2 and arg[1]) or ''
742 self._do_wiki_load(dir)
743 elif arg[0] == 'upgrade' and len(arg) == 1:
744 self._do_wiki_load(default_dir('wiki'),
745 ignore=['WikiStart', 'checkwiki.py'],
746 create_only=['InterMapTxt'])
747 else:
748 self.do_help ('wiki')
749
750 def _do_wiki_list(self):
751 rows = self.db_query("SELECT name, max(version), max(time) "
752 "FROM wiki GROUP BY name ORDER BY name")
753 self.print_listing(['Title', 'Edits', 'Modified'],
754 [(r[0], r[1], self._format_datetime(r[2])) for r in rows])
755
756 def _do_wiki_remove(self, name):
757 page = WikiPage(self.env_open(), name)
758 page.delete()
759
760 def _do_wiki_import(self, filename, title, cursor=None,
761 create_only=[]):
762 if not os.path.isfile(filename):
763 raise Exception, '%s is not a file' % filename
764
765 f = open(filename,'r')
766 data = to_unicode(f.read(), 'utf-8')
767
768 # Make sure we don't insert the exact same page twice
769 rows = self.db_query("SELECT text FROM wiki WHERE name=%s "
770 "ORDER BY version DESC LIMIT 1", cursor,
771 params=(title,))
772 old = list(rows)
773 if old and title in create_only:
774 print ' %s already exists.' % title
775 return
776 if old and data == old[0][0]:
777 print ' %s already up to date.' % title
778 return
779 f.close()
780
781 self.db_update("INSERT INTO wiki(version,name,time,author,ipnr,text) "
782 " SELECT 1+COALESCE(max(version),0),%s,%s,"
783 " 'trac','127.0.0.1',%s FROM wiki "
784 " WHERE name=%s",
785 cursor, (title, int(time.time()), data, title))
786
787 def _do_wiki_export(self, page, filename=''):
788 data = self.db_query("SELECT text FROM wiki WHERE name=%s "
789 "ORDER BY version DESC LIMIT 1", params=[page])
790 text = data.next()[0]
791 if not filename:
792 print text
793 else:
794 if os.path.isfile(filename):
795 raise Exception("File '%s' exists" % filename)
796 f = open(filename,'w')
797 f.write(text.encode('utf-8'))
798 f.close()
799
800 def _do_wiki_dump(self, dir):
801 pages = self.get_wiki_list()
802 for p in pages:
803 dst = os.path.join(dir, urllib.quote(p, ''))
804 print " %s => %s" % (p, dst)
805 self._do_wiki_export(p, dst)
806
807 def _do_wiki_load(self, dir, cursor=None, ignore=[], create_only=[]):
808 for page in os.listdir(dir):
809 if page in ignore:
810 continue
811 filename = os.path.join(dir, page)
812 page = urllib.unquote(page)
813 if os.path.isfile(filename):
814 print " %s => %s" % (filename, page)
815 self._do_wiki_import(filename, page, cursor, create_only)
816
817 ## Ticket
818 _help_ticket = [('ticket remove <number>', 'Remove ticket')]
819
820 def complete_ticket(self, text, line, begidx, endidx):
821 argv = self.arg_tokenize(line)
822 argc = len(argv)
823 if line[-1] == ' ': # Space starts new argument
824 argc += 1
825 comp = []
826 if argc == 2:
827 comp = ['remove']
828 return self.word_complete(text, comp)
829
830 def do_ticket(self, line):
831 arg = self.arg_tokenize(line)
832 if arg[0] == 'remove' and len(arg)==2:
833 try:
834 number = int(arg[1])
835 except ValueError:
836 print>>sys.stderr, "<number> must be a number"
837 return
838 self._do_ticket_remove(number)
839 else:
840 self.do_help ('ticket')
841
842 def _do_ticket_remove(self, number):
843 ticket = Ticket(self.env_open(), number)
844 ticket.delete()
845 print "Ticket %d and all associated data removed." % number
846
847
848 ## (Ticket) Type
849 _help_ticket_type = [('ticket_type list', 'Show possible ticket types'),
850 ('ticket_type add <value>', 'Add a ticket type'),
851 ('ticket_type change <value> <newvalue>',
852 'Change a ticket type'),
853 ('ticket_type remove <value>', 'Remove a ticket type'),
854 ('ticket_type order <value> up|down',
855 'Move a ticket type up or down in the list')]
856
857 def complete_ticket_type (self, text, line, begidx, endidx):
858 if begidx == 16:
859 comp = self.get_enum_list ('ticket_type')
860 elif begidx < 15:
861 comp = ['list', 'add', 'change', 'remove', 'order']
862 return self.word_complete(text, comp)
863
864 def do_ticket_type(self, line):
865 self._do_enum('ticket_type', line)
866
867 ## (Ticket) Priority
868 _help_priority = [('priority list', 'Show possible ticket priorities'),
869 ('priority add <value>', 'Add a priority value option'),
870 ('priority change <value> <newvalue>',
871 'Change a priority value'),
872 ('priority remove <value>', 'Remove priority value'),
873 ('priority order <value> up|down',
874 'Move a priority value up or down in the list')]
875
876 def complete_priority (self, text, line, begidx, endidx):
877 if begidx == 16:
878 comp = self.get_enum_list ('priority')
879 elif begidx < 15:
880 comp = ['list', 'add', 'change', 'remove', 'order']
881 return self.word_complete(text, comp)
882
883 def do_priority(self, line):
884 self._do_enum('priority', line)
885
886 ## (Ticket) Severity
887 _help_severity = [('severity list', 'Show possible ticket severities'),
888 ('severity add <value>', 'Add a severity value option'),
889 ('severity change <value> <newvalue>',
890 'Change a severity value'),
891 ('severity remove <value>', 'Remove severity value'),
892 ('severity order <value> up|down',
893 'Move a severity value up or down in the list')]
894
895 def complete_severity (self, text, line, begidx, endidx):
896 if begidx == 16:
897 comp = self.get_enum_list ('severity')
898 elif begidx < 15:
899 comp = ['list', 'add', 'change', 'remove', 'order']
900 return self.word_complete(text, comp)
901
902 def do_severity(self, line):
903 self._do_enum('severity', line)
904
905 # Type, priority, severity share the same datastructure and methods:
906
907 _enum_map = {'ticket_type': Type, 'priority': Priority,
908 'severity': Severity}
909
910 def _do_enum(self, type, line):
911 arg = self.arg_tokenize(line)
912 if arg[0] == 'list':
913 self._do_enum_list(type)
914 elif arg[0] == 'add' and len(arg) == 2:
915 name = arg[1]
916 self._do_enum_add(type, name)
917 elif arg[0] == 'change' and len(arg) == 3:
918 name = arg[1]
919 newname = arg[2]
920 self._do_enum_change(type, name, newname)
921 elif arg[0] == 'remove' and len(arg) == 2:
922 name = arg[1]
923 self._do_enum_remove(type, name)
924 elif arg[0] == 'order' and len(arg) == 3 and arg[2] in ('up', 'down'):
925 name = arg[1]
926 if arg[2] == 'up':
927 direction = -1
928 else:
929 direction = 1
930 self._do_enum_order(type, name, direction)
931 else:
932 self.do_help(type)
933
934 def _do_enum_list(self, type):
935 enum_cls = self._enum_map[type]
936 self.print_listing(['Possible Values'],
937 [(e.name,) for e in enum_cls.select(self.env_open())])
938
939 def _do_enum_add(self, type, name):
940 cnx = self.db_open()
941 sql = ("INSERT INTO enum(value,type,name) "
942 " SELECT 1+COALESCE(max(%(cast)s),0),'%(type)s','%(name)s'"
943 " FROM enum WHERE type='%(type)s'"
944 % {'type':type, 'name':name, 'cast': cnx.cast('value', 'int')})
945 cursor = cnx.cursor()
946 self.db_update(sql, cursor)
947 cnx.commit()
948
949 def _do_enum_change(self, type, name, newname):
950 enum_cls = self._enum_map[type]
951 enum = enum_cls(self.env_open(), name)
952 enum.name = newname
953 enum.update()
954
955 def _do_enum_remove(self, type, name):
956 enum_cls = self._enum_map[type]
957 enum = enum_cls(self.env_open(), name)
958 enum.delete()
959
960 def _do_enum_order(self, type, name, direction):
961 env = self.env_open()
962 enum_cls = self._enum_map[type]
963 enum1 = enum_cls(env, name)
964 enum1.value = int(float(enum1.value) + direction)
965 for enum2 in enum_cls.select(env):
966 if int(float(enum2.value)) == enum1.value:
967 enum2.value = int(float(enum2.value) - direction)
968 break
969 else:
970 return
971 enum1.update()
972 enum2.update()
973
974 ## Milestone
975
976 _help_milestone = [('milestone list', 'Show milestones'),
977 ('milestone add <name> [due]', 'Add milestone'),
978 ('milestone rename <name> <newname>',
979 'Rename milestone'),
980 ('milestone due <name> <due>',
981 'Set milestone due date (Format: "%s" or "now")'
982 % _date_format_hint),
983 ('milestone completed <name> <completed>',
984 'Set milestone completed date (Format: "%s" or "now")'
985 % _date_format_hint),
986 ('milestone remove <name>', 'Remove milestone')]
987
988 def complete_milestone (self, text, line, begidx, endidx):
989 if begidx in (15, 17):
990 comp = self.get_milestone_list()
991 elif begidx < 15:
992 comp = ['list', 'add', 'rename', 'time', 'remove']
993 return self.word_complete(text, comp)
994
995 def do_milestone(self, line):
996 arg = self.arg_tokenize(line)
997 if arg[0] == 'list':
998 self._do_milestone_list()
999 elif arg[0] == 'add' and len(arg) in [2,3]:
1000 self._do_milestone_add(arg[1])
1001 if len(arg) == 3:
1002 self._do_milestone_set_due(arg[1], arg[2])
1003 elif arg[0] == 'rename' and len(arg) == 3:
1004 self._do_milestone_rename(arg[1], arg[2])
1005 elif arg[0] == 'remove' and len(arg) == 2:
1006 self._do_milestone_remove(arg[1])
1007 elif arg[0] == 'due' and len(arg) == 3:
1008 self._do_milestone_set_due(arg[1], arg[2])
1009 elif arg[0] == 'completed' and len(arg) == 3:
1010 self._do_milestone_set_completed(arg[1], arg[2])
1011 else:
1012 self.do_help('milestone')
1013
1014 def _do_milestone_list(self):
1015 data = []
1016 for m in Milestone.select(self.env_open()):
1017 data.append((m.name, m.due and self._format_date(m.due),
1018 m.completed and self._format_datetime(m.completed)))
1019
1020 self.print_listing(['Name', 'Due', 'Completed'], data)
1021
1022 def _do_milestone_rename(self, name, newname):
1023 milestone = Milestone(self.env_open(), name)
1024 milestone.name = newname
1025 milestone.update()
1026
1027 def _do_milestone_add(self, name):
1028 milestone = Milestone(self.env_open())
1029 milestone.name = name
1030 milestone.insert()
1031
1032 def _do_milestone_remove(self, name):
1033 milestone = Milestone(self.env_open(), name)
1034 milestone.delete(author=getpass.getuser())
1035
1036 def _do_milestone_set_due(self, name, t):
1037 milestone = Milestone(self.env_open(), name)
1038 milestone.due = self._parse_date(t)
1039 milestone.update()
1040
1041 def _do_milestone_set_completed(self, name, t):
1042 milestone = Milestone(self.env_open(), name)
1043 milestone.completed = self._parse_date(t)
1044 milestone.update()
1045
1046 ## Version
1047 _help_version = [('version list', 'Show versions'),
1048 ('version add <name> [time]', 'Add version'),
1049 ('version rename <name> <newname>',
1050 'Rename version'),
1051 ('version time <name> <time>',
1052 'Set version date (Format: "%s" or "now")'
1053 % _date_format_hint),
1054 ('version remove <name>', 'Remove version')]
1055
1056 def complete_version (self, text, line, begidx, endidx):
1057 if begidx in (13, 15):
1058 comp = self.get_version_list()
1059 elif begidx < 13:
1060 comp = ['list', 'add', 'rename', 'time', 'remove']
1061 return self.word_complete(text, comp)
1062
1063 def do_version(self, line):
1064 arg = self.arg_tokenize(line)
1065 if arg[0] == 'list':
1066 self._do_version_list()
1067 elif arg[0] == 'add' and len(arg) in [2,3]:
1068 self._do_version_add(arg[1])
1069 if len(arg) == 3:
1070 self._do_version_time(arg[1], arg[2])
1071 elif arg[0] == 'rename' and len(arg) == 3:
1072 self._do_version_rename(arg[1], arg[2])
1073 elif arg[0] == 'time' and len(arg) == 3:
1074 self._do_version_time(arg[1], arg[2])
1075 elif arg[0] == 'remove' and len(arg) == 2:
1076 self._do_version_remove(arg[1])
1077 else:
1078 self.do_help('version')
1079
1080 def _do_version_list(self):
1081 data = []
1082 for v in Version.select(self.env_open()):
1083 data.append((v.name, v.time and self._format_date(v.time)))
1084 self.print_listing(['Name', 'Time'], data)
1085
1086 def _do_version_rename(self, name, newname):
1087 version = Version(self.env_open(), name)
1088 version.name = newname
1089 version.update()
1090
1091 def _do_version_add(self, name):
1092 version = Version(self.env_open())
1093 version.name = name
1094 version.insert()
1095
1096 def _do_version_remove(self, name):
1097 version = Version(self.env_open(), name)
1098 version.delete()
1099
1100 def _do_version_time(self, name, t):
1101 version = Version(self.env_open(), name)
1102 version.time = self._parse_date(t)
1103 version.update()
1104
1105 _help_upgrade = [('upgrade', 'Upgrade database to current version')]
1106 def do_upgrade(self, line):
1107 arg = self.arg_tokenize(line)
1108 do_backup = True
1109 if arg[0] in ['-b', '--no-backup']:
1110 do_backup = False
1111 self.db_open()
1112
1113 if not self.__env.needs_upgrade():
1114 print "Database is up to date, no upgrade necessary."
1115 return
1116
1117 self.__env.upgrade(backup=do_backup)
1118 print 'Upgrade done.'
1119
1120 _help_hotcopy = [('hotcopy <backupdir>',
1121 'Make a hot backup copy of an environment')]
1122 def do_hotcopy(self, line):
1123 arg = self.arg_tokenize(line)
1124 if arg[0]:
1125 dest = arg[0]
1126 else:
1127 self.do_help('hotcopy')
1128 return
1129
1130 # Bogus statement to lock the database while copying files
1131 cnx = self.db_open()
1132 cursor = cnx.cursor()
1133 cursor.execute("UPDATE system SET name=NULL WHERE name IS NULL")
1134
1135 try:
1136 print 'Hotcopying %s to %s ...' % (self.__env.path, dest),
1137 db_str = self.__env.config.get('trac', 'database')
1138 prefix, db_path = db_str.split(':', 1)
1139 if prefix == 'sqlite':
1140 # don't copy the journal (also, this would fail on Windows)
1141 db_path = os.path.normpath(db_path)
1142 skip = ['%s-journal' % os.path.join(self.__env.path, db_path)]
1143 else:
1144 skip = []
1145 copytree(self.__env.path, dest, symlinks=1, skip=skip)
1146 finally:
1147 # Unlock database
1148 cnx.rollback()
1149
1150 print 'Hotcopy done.'
1151
1152
1153 class TracAdminHelpMacro(WikiMacroBase):
1154 """Displays help for trac-admin commands.
1155
1156 Examples:
1157 {{{
1158 [[TracAdminHelp]] # all commands
1159 [[TracAdminHelp(wiki)]] # all wiki commands
1160 [[TracAdminHelp(wiki export)]] # the "wiki export" command
1161 [[TracAdminHelp(upgrade)]] # the upgrade command
1162 }}}
1163 """
1164
1165 def render_macro(self, req, name, content):
1166 if content:
1167 try:
1168 arg = content.split(' ', 1)[0]
1169 doc = getattr(TracAdmin, '_help_' + arg)
1170 except AttributeError:
1171 raise TracError('Unknown trac-admin command "%s"' % content)
1172 if arg != content:
1173 for cmd, help in doc:
1174 if cmd.startswith(content):
1175 doc = [(cmd, help)]
1176 break
1177 else:
1178 doc = TracAdmin.all_docs()
1179 buf = StringIO.StringIO()
1180 TracAdmin.print_doc(doc, buf)
1181 return html.PRE(buf.getvalue(), class_='wiki')
1182
1183
1184 def run(args):
1185 """Main entry point."""
1186 admin = TracAdmin()
1187 if len(args) > 0:
1188 if args[0] in ('-h', '--help', 'help'):
1189 return admin.onecmd("help")
1190 elif args[0] in ('-v','--version','about'):
1191 return admin.onecmd("about")
1192 else:
1193 admin.env_set(os.path.abspath(args[0]))
1194 if len(args) > 1:
1195 s_args = ' '.join(["'%s'" % c for c in args[2:]])
1196 command = args[1] + ' ' +s_args
1197 return admin.onecmd(command)
1198 else:
1199 while True:
1200 admin.run()
1201 else:
1202 return admin.onecmd("help")
1203
1204
1205 if __name__ == '__main__':
1206 run(sys.argv[1:])
Copyright (C) 2012-2017 Edgewall Software