From fb8f4ed1528aaa7269bd320ee7198edaa22d5406 Mon Sep 17 00:00:00 2001 From: John Rouillard Date: Sun, 10 Mar 2024 21:59:20 -0400 Subject: [PATCH] Summary: feat: roundup-admin history command has human interpretable output Reformat journal entries and make try to make readable sentences out of them. Set up translation markers and added hints for the tanslators by marking translator comments with; # .Hint text for translator on the line before _() markers. Doc'ed changes in roundup-admin docs and added info to upgrading.txt. If the user wants old format, they can call history designator raw --- CHANGES.txt | 3 + doc/admin_guide.txt | 6 +- doc/upgrading.txt | 68 ++++++++++++ locale/Makefile | 1 + roundup/admin.py | 196 ++++++++++++++++++++++++++++++--- share/man/man1/roundup-admin.1 | 8 +- 6 files changed, 261 insertions(+), 21 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index e795bd49..2a75264c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -127,6 +127,9 @@ Features: - save roundup-admin history between sessions. Load ~/.roundup_admin_rlrc file to set history-size persistently. Add pragma history_length to override for a session. (John Rouillard) +- the roundup-admin history command now dumps the journal entries + in a more human readable format. Use the raw option to get the older + machine parsible output. (John Rouillard) 2023-07-13 2.3.0 diff --git a/doc/admin_guide.txt b/doc/admin_guide.txt index dcea4132..13714d5e 100644 --- a/doc/admin_guide.txt +++ b/doc/admin_guide.txt @@ -1245,7 +1245,9 @@ Examples above show how to use it to: * creating a new user from the command line * list/find users in the tracker -The basic usage is:: +The basic usage is: + +.. code-block:: text Usage: roundup-admin [options] [ ] @@ -1282,7 +1284,7 @@ The basic usage is:: genconfig get property designator[,designator]* help topic - history designator [skipquiet] + history designator [skipquiet] [raw] import import_dir importtables export_dir initialise [adminpw] diff --git a/doc/upgrading.txt b/doc/upgrading.txt index 220c1476..6e608208 100644 --- a/doc/upgrading.txt +++ b/doc/upgrading.txt @@ -270,6 +270,74 @@ before changing your PostgreSQL configuration, try changing the pragma ``10000``. In some cases this may be too high. See the `administration guide`_ for further details. +roundup-admin's History Command Produces Readable Output +-------------------------------------------------------- + +The history command of roundup-admin used to print the raw journal +data. In this release the default is to produce more human readable +data. The original output (not pretty printed as below) was:: + + [('1', , '1', 'create', {}), + ('1', + , + '1', + 'set', + {'messages': (('+', ['3']),)}), + ('1', , '1', 'set', {'priority': '1'}), + ('1', + , + '1', + 'link', + ('issue', '2', 'dependson')), + ('1', , '1', 'link', ('issue', '2', + 'seealso')), + ('1', + , + '1', + 'set', + {'dependson': (('+', ['3']),), 'private': None, 'queue': None}), + ('1', + , + '1', + 'set', + {'dependson': (('+', ['2']),)}), + ('1', + , + '1', + 'unlink', + ('issue', '2', 'seealso')), + ... + +Now it produces (Each entry is on one line, lines wrapped +and indented for display):: + + admin(2013-02-18.20:30:34) create issue + admin(2013-02-19.21:24:20) set modified messages: added: msg3 + admin(2013-02-19.21:24:24) set priority was critical(1) + admin(2013-02-20.03:16:52) link added issue2 to dependson + admin(2013-02-21.20:51:40) link added issue2 to seealso + admin(2013-02-22.05:33:08) set modified dependson: added: issue3; + private was None; queue was None + admin(2013-02-22.05:33:19) set modified dependson: added: issue2 + admin(2013-02-27.03:24:42) unlink removed issue2 from seealso + ... + + +A few things to note: set operations can either assign a property or +report a modification of a multilink property. If an assignment +occurs, the value reported is the **old value** that was there before +the assignment. It is **not** the value that is assigned. In the +example above I don't know what the current value of priority is. All +I know it was set to critical when the issue was created. + +Modifications to multilink properties work differently. I know that +``msg3`` was present in the messages property after 2013-02-19 at +21:24:20 UTC. + +The history command gets a new optional argument ``raw`` that produces +the old style output. The old style is (marginally) more useful for +script automation. + .. index:: Upgrading; 2.2.0 to 2.3.0 Migrating from 2.2.0 to 2.3.0 diff --git a/locale/Makefile b/locale/Makefile index e6b7a19b..e6bde389 100644 --- a/locale/Makefile +++ b/locale/Makefile @@ -86,6 +86,7 @@ roundup.pot: $(SOURCES) $(TEMPLATES) VERSION="`${RUN_PYTHON} -c 'from roundup import __version__; \ print(__version__)';`"; \ ${XGETTEXT} -j -w 80 -F \ + --add-comments=".Hint " \ --package-name=Roundup \ --package-version=$$VERSION \ --msgid-bugs-address=roundup-devel@lists.sourceforge.net \ diff --git a/roundup/admin.py b/roundup/admin.py index da42f094..af07808e 100644 --- a/roundup/admin.py +++ b/roundup/admin.py @@ -953,16 +953,16 @@ def do_help(self, args, nl_re=nl_re, indent_re=indent_re): return 0 def do_history(self, args): - ''"""Usage: history designator [skipquiet] + ''"""Usage: history designator [skipquiet] [raw] Show the history entries of a designator. A designator is a classname and a nodeid concatenated, eg. bug1, user10, ... - Lists the journal entries viewable by the user for the - node identified by the designator. If skipquiet is the - second argument, journal entries for quiet properties - are not shown. + Lists the journal entries viewable by the user for the node + identified by the designator. If skipquiet is added, journal + entries for quiet properties are not shown. If raw is added, + the output is the raw representation of the journal entries. """ if len(args) < 1: @@ -972,15 +972,181 @@ def do_history(self, args): except hyperdb.DesignatorError as message: raise UsageError(message) - skipquiet = False - if len(args) == 2: - if args[1] != 'skipquiet': - raise UsageError("Second argument is not skipquiet") - skipquiet = True + valid_args = ['skipquiet', 'raw'] + + if len(args) >= 2: + check = [a for a in args[1:] if a not in valid_args] + if check: + raise UsageError( + _("Unexpected argument(s): %s. " + "Expected 'skipquiet' or 'raw'.") % ", ".join(check)) + + skipquiet = 'skipquiet' in args[1:] + raw = 'raw' in args[1:] + + getclass = self.db.getclass + def get_prop_name(key, prop_name): + # getclass and classname from enclosing method + klass = getclass(classname) + try: + property_obj = klass.properties[prop_name] + except KeyError: + # the property has been removed from the schema. + return None + if isinstance(property_obj, + (hyperdb.Link, hyperdb.Multilink)): + prop_class = getclass(property_obj.classname) + key_prop_name = prop_class.key + if key_prop_name: + return prop_class.get(key, key_prop_name) + # None indicates that there is no key_prop + return None + return None + + def get_prop_class(prop_name): + # getclass and classname from enclosing method + klass = getclass(classname) + try: + property_obj = klass.properties[prop_name] + except KeyError: + # the property has been removed from the schema. + return None + if isinstance(property_obj, + (hyperdb.Link, hyperdb.Multilink)): + prop_class = getclass(property_obj.classname) + return prop_class.classname + return None # it's not a link + + def _format_tuple_change(data, prop): + ''' ('-', ['2', '4'] -> + "removed fred(2), jim(6)" + ''' + if data[0] == '-': + op = _("removed") + elif data[0] == '+': + op = _("added") + else: + raise ValueError(_("Unknown history set operation '%s'. " + "Expected +/-.") % op) + op_params = data[1] + name = get_prop_name(op_params[0], prop) + if name is not None: + list_items = ["%s(%s)" % + (get_prop_name(o, prop), o) + for o in op_params] + else: + propclass = get_prop_class(prop) + if propclass: # noqa: SIM108 + list_items = ["%s%s" % (propclass, o) + for o in op_params] + else: + list_items = op_params + + return "%s: %s" % (op, ", ".join(list_items)) + + def format_report_class(_data): + """Eat the empty data dictionary or None""" + return classname + + def format_link (data): + '''data = ('issue', '157', 'dependson')''' + # .Hint added issue23 to superseder + f = _("added %(class)s%(item_id)s to %(propname)s") + return f % { + 'class': data[0], 'item_id': data[1], 'propname': data[2]} + + def format_set(data): + '''data = set {'fyi': None, 'priority': '5'} + set {'fyi': '....\ned through cleanly', 'priority': '3'} + ''' + result = [] + + # Note that set data is the old value. So don't use + # current/future tense in sentences. + + for prop, value in data.items(): + if isinstance(value, str): + name = get_prop_name(value, prop) + if name: + result.append( + # .Hint read as: assignedto was admin(1) + # .Hint where assignedto is the property + # .Hint admin is the key name for value 1 + _("%(prop)s was %(name)%(value)s)") % { + "prop": prop, "name": name, "value": value }) + else: + # use repr so strings with embedded \n etc. don't + # generate newlines in output. Try to keep each + # journal entry on 1 line. + result.append(_("%(prop)s was %(value)s") % { + "prop": prop, "value": repr(value)}) + elif isinstance(value, list): + # test to see if there is a key prop. + # Assumption, geting None here means no key + # is defined for the property's class. + name = get_prop_name(value[0], prop) + if name is not None: + list_items = ["%s(%s)" % + (get_prop_name(v, prop), v) + for v in value] + else: + prop_class = get_prop_class(prop) + if prop_class: # noqa: SIM108 + list_items = [ "%s%s" % (prop_class, v) + for v in value ] + else: + list_items = value + + result.append(_("%(prop)s was [%(value_list)s]") % { + "prop": prop, "value_list": ", ".join(list_items)}) + elif isinstance(value, tuple): + # operation data + decorated = [_format_tuple_change(data, prop) + for data in value] + result.append(# .Hint modified nosy: added demo(3) + _("modified %(prop)s: %(how)s") % { + "prop": prop, "how": ", ".join(decorated)}) + else: + result.append(_("%(prop)s was %(value)s") % { + "prop": prop, "value": value}) + + return '; '.join(result) + + def format_unlink (data): + '''data = ('issue', '157', 'dependson')''' + return "removed %s%s from %s" % (data[0], data[1], data[2]) + + formatters = { + "create": format_report_class, + "link": format_link, + "restored": format_report_class, + "retired": format_report_class, + "set": format_set, + "unlink": format_unlink, + } try: - print(self.db.getclass(classname).history(nodeid, - skipquiet=skipquiet)) + # returns a tuple: ( + # [0] = nodeid + # [1] = date + # [2] = userid + # [3] = operation + # [4] = details + raw_history = self.db.getclass(classname).history(nodeid, + skipquiet=skipquiet) + if raw: + print(raw_history) + return 0 + + def make_readable(hist): + return "%s(%s) %s %s" % (self.db.user.get(hist[2], 'username'), + hist[1], + hist[3], + formatters.get(hist[3], lambda x: x)( + hist[4])) + printable_history = [make_readable(hist) for hist in raw_history] + + print("\n".join(printable_history)) except KeyError: raise UsageError(_('no such class "%(classname)s"') % locals()) except IndexError: @@ -2103,7 +2269,7 @@ def interactive(self): ".roundup_admin_history") try: - import readline # noqa: F401 + import readline readline.read_init_file(initfile) try: readline.read_history_file(histfile) @@ -2168,10 +2334,10 @@ def main(self): # noqa: PLR0912, PLR0911 self.print_designator = 0 self.verbose = 0 for opt, arg in opts: - if opt == '-h': # noqa: RET505 - allow elif after returns + if opt == '-h': self.usage() return 0 - elif opt == '-v': + elif opt == '-v': # noqa: RET505 - allow elif after returns print('%s (python %s)' % (roundup_version, sys.version.split()[0])) return 0 diff --git a/share/man/man1/roundup-admin.1 b/share/man/man1/roundup-admin.1 index 1848b687..a35126b0 100644 --- a/share/man/man1/roundup-admin.1 +++ b/share/man/man1/roundup-admin.1 @@ -119,11 +119,11 @@ Retrieves the property value of the nodes specified by the designators. .TP -\fBhistory\fP \fIdesignator [skipquiet]\fP +\fBhistory\fP \fIdesignator [skipquiet] [raw]\fP Lists the journal entries viewable by the user for the -node identified by the designator. If skipquiet is the -second argument, journal entries for quiet properties -are not shown. +node identified by the designator. If skipquiet is added, journal +entries for quiet properties are not shown. Without the raw option +a more human readable output format is used. .TP \fBimport\fP \fIimport_dir\fP Import a database from the directory containing CSV files,