"Fossies" - the Fresh Open Source Software Archive

Member "roundup-2.0.0/roundup/admin.py" (29 Jun 2020, 67408 Bytes) of package /linux/www/roundup-2.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "admin.py": 1.6.1_vs_2.0.0.

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