Skip to content

Commit fb8f4ed

Browse files
committed
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
1 parent d37721b commit fb8f4ed

File tree

6 files changed

+261
-21
lines changed

6 files changed

+261
-21
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ Features:
127127
- save roundup-admin history between sessions. Load
128128
~/.roundup_admin_rlrc file to set history-size persistently. Add
129129
pragma history_length to override for a session. (John Rouillard)
130+
- the roundup-admin history command now dumps the journal entries
131+
in a more human readable format. Use the raw option to get the older
132+
machine parsible output. (John Rouillard)
130133

131134

132135
2023-07-13 2.3.0

doc/admin_guide.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,7 +1245,9 @@ Examples above show how to use it to:
12451245
* creating a new user from the command line
12461246
* list/find users in the tracker
12471247

1248-
The basic usage is::
1248+
The basic usage is:
1249+
1250+
.. code-block:: text
12491251

12501252
Usage: roundup-admin [options] [<command> <arguments>]
12511253

@@ -1282,7 +1284,7 @@ The basic usage is::
12821284
genconfig <filename>
12831285
get property designator[,designator]*
12841286
help topic
1285-
history designator [skipquiet]
1287+
history designator [skipquiet] [raw]
12861288
import import_dir
12871289
importtables export_dir
12881290
initialise [adminpw]

doc/upgrading.txt

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,74 @@ before changing your PostgreSQL configuration, try changing the pragma
270270
``10000``. In some cases this may be too high. See the `administration
271271
guide`_ for further details.
272272

273+
roundup-admin's History Command Produces Readable Output
274+
--------------------------------------------------------
275+
276+
The history command of roundup-admin used to print the raw journal
277+
data. In this release the default is to produce more human readable
278+
data. The original output (not pretty printed as below) was::
279+
280+
[('1', <Date 2013-02-18.20:30:34.125>, '1', 'create', {}),
281+
('1',
282+
<Date 2013-02-19.21:24:20.391>,
283+
'1',
284+
'set',
285+
{'messages': (('+', ['3']),)}),
286+
('1', <Date 2013-02-19.21:24:24.797>, '1', 'set', {'priority': '1'}),
287+
('1',
288+
<Date 2013-02-20.03:16:52.000>,
289+
'1',
290+
'link',
291+
('issue', '2', 'dependson')),
292+
('1', <Date 2013-02-21.20:51:40.750>, '1', 'link', ('issue', '2',
293+
'seealso')),
294+
('1',
295+
<Date 2013-02-22.05:33:08.875>,
296+
'1',
297+
'set',
298+
{'dependson': (('+', ['3']),), 'private': None, 'queue': None}),
299+
('1',
300+
<Date 2013-02-22.05:33:19.406>,
301+
'1',
302+
'set',
303+
{'dependson': (('+', ['2']),)}),
304+
('1',
305+
<Date 2013-02-27.03:24:42.844>,
306+
'1',
307+
'unlink',
308+
('issue', '2', 'seealso')),
309+
...
310+
311+
Now it produces (Each entry is on one line, lines wrapped
312+
and indented for display)::
313+
314+
admin(2013-02-18.20:30:34) create issue
315+
admin(2013-02-19.21:24:20) set modified messages: added: msg3
316+
admin(2013-02-19.21:24:24) set priority was critical(1)
317+
admin(2013-02-20.03:16:52) link added issue2 to dependson
318+
admin(2013-02-21.20:51:40) link added issue2 to seealso
319+
admin(2013-02-22.05:33:08) set modified dependson: added: issue3;
320+
private was None; queue was None
321+
admin(2013-02-22.05:33:19) set modified dependson: added: issue2
322+
admin(2013-02-27.03:24:42) unlink removed issue2 from seealso
323+
...
324+
325+
326+
A few things to note: set operations can either assign a property or
327+
report a modification of a multilink property. If an assignment
328+
occurs, the value reported is the **old value** that was there before
329+
the assignment. It is **not** the value that is assigned. In the
330+
example above I don't know what the current value of priority is. All
331+
I know it was set to critical when the issue was created.
332+
333+
Modifications to multilink properties work differently. I know that
334+
``msg3`` was present in the messages property after 2013-02-19 at
335+
21:24:20 UTC.
336+
337+
The history command gets a new optional argument ``raw`` that produces
338+
the old style output. The old style is (marginally) more useful for
339+
script automation.
340+
273341
.. index:: Upgrading; 2.2.0 to 2.3.0
274342

275343
Migrating from 2.2.0 to 2.3.0

locale/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ roundup.pot: $(SOURCES) $(TEMPLATES)
8686
VERSION="`${RUN_PYTHON} -c 'from roundup import __version__; \
8787
print(__version__)';`"; \
8888
${XGETTEXT} -j -w 80 -F \
89+
--add-comments=".Hint " \
8990
--package-name=Roundup \
9091
--package-version=$$VERSION \
9192

roundup/admin.py

Lines changed: 181 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -953,16 +953,16 @@ def do_help(self, args, nl_re=nl_re, indent_re=indent_re):
953953
return 0
954954

955955
def do_history(self, args):
956-
''"""Usage: history designator [skipquiet]
956+
''"""Usage: history designator [skipquiet] [raw]
957957
Show the history entries of a designator.
958958
959959
A designator is a classname and a nodeid concatenated,
960960
eg. bug1, user10, ...
961961
962-
Lists the journal entries viewable by the user for the
963-
node identified by the designator. If skipquiet is the
964-
second argument, journal entries for quiet properties
965-
are not shown.
962+
Lists the journal entries viewable by the user for the node
963+
identified by the designator. If skipquiet is added, journal
964+
entries for quiet properties are not shown. If raw is added,
965+
the output is the raw representation of the journal entries.
966966
"""
967967

968968
if len(args) < 1:
@@ -972,15 +972,181 @@ def do_history(self, args):
972972
except hyperdb.DesignatorError as message:
973973
raise UsageError(message)
974974

975-
skipquiet = False
976-
if len(args) == 2:
977-
if args[1] != 'skipquiet':
978-
raise UsageError("Second argument is not skipquiet")
979-
skipquiet = True
975+
valid_args = ['skipquiet', 'raw']
976+
977+
if len(args) >= 2:
978+
check = [a for a in args[1:] if a not in valid_args]
979+
if check:
980+
raise UsageError(
981+
_("Unexpected argument(s): %s. "
982+
"Expected 'skipquiet' or 'raw'.") % ", ".join(check))
983+
984+
skipquiet = 'skipquiet' in args[1:]
985+
raw = 'raw' in args[1:]
986+
987+
getclass = self.db.getclass
988+
def get_prop_name(key, prop_name):
989+
# getclass and classname from enclosing method
990+
klass = getclass(classname)
991+
try:
992+
property_obj = klass.properties[prop_name]
993+
except KeyError:
994+
# the property has been removed from the schema.
995+
return None
996+
if isinstance(property_obj,
997+
(hyperdb.Link, hyperdb.Multilink)):
998+
prop_class = getclass(property_obj.classname)
999+
key_prop_name = prop_class.key
1000+
if key_prop_name:
1001+
return prop_class.get(key, key_prop_name)
1002+
# None indicates that there is no key_prop
1003+
return None
1004+
return None
1005+
1006+
def get_prop_class(prop_name):
1007+
# getclass and classname from enclosing method
1008+
klass = getclass(classname)
1009+
try:
1010+
property_obj = klass.properties[prop_name]
1011+
except KeyError:
1012+
# the property has been removed from the schema.
1013+
return None
1014+
if isinstance(property_obj,
1015+
(hyperdb.Link, hyperdb.Multilink)):
1016+
prop_class = getclass(property_obj.classname)
1017+
return prop_class.classname
1018+
return None # it's not a link
1019+
1020+
def _format_tuple_change(data, prop):
1021+
''' ('-', ['2', '4'] ->
1022+
"removed fred(2), jim(6)"
1023+
'''
1024+
if data[0] == '-':
1025+
op = _("removed")
1026+
elif data[0] == '+':
1027+
op = _("added")
1028+
else:
1029+
raise ValueError(_("Unknown history set operation '%s'. "
1030+
"Expected +/-.") % op)
1031+
op_params = data[1]
1032+
name = get_prop_name(op_params[0], prop)
1033+
if name is not None:
1034+
list_items = ["%s(%s)" %
1035+
(get_prop_name(o, prop), o)
1036+
for o in op_params]
1037+
else:
1038+
propclass = get_prop_class(prop)
1039+
if propclass: # noqa: SIM108
1040+
list_items = ["%s%s" % (propclass, o)
1041+
for o in op_params]
1042+
else:
1043+
list_items = op_params
1044+
1045+
return "%s: %s" % (op, ", ".join(list_items))
1046+
1047+
def format_report_class(_data):
1048+
"""Eat the empty data dictionary or None"""
1049+
return classname
1050+
1051+
def format_link (data):
1052+
'''data = ('issue', '157', 'dependson')'''
1053+
# .Hint added issue23 to superseder
1054+
f = _("added %(class)s%(item_id)s to %(propname)s")
1055+
return f % {
1056+
'class': data[0], 'item_id': data[1], 'propname': data[2]}
1057+
1058+
def format_set(data):
1059+
'''data = set {'fyi': None, 'priority': '5'}
1060+
set {'fyi': '....\ned through cleanly', 'priority': '3'}
1061+
'''
1062+
result = []
1063+
1064+
# Note that set data is the old value. So don't use
1065+
# current/future tense in sentences.
1066+
1067+
for prop, value in data.items():
1068+
if isinstance(value, str):
1069+
name = get_prop_name(value, prop)
1070+
if name:
1071+
result.append(
1072+
# .Hint read as: assignedto was admin(1)
1073+
# .Hint where assignedto is the property
1074+
# .Hint admin is the key name for value 1
1075+
_("%(prop)s was %(name)%(value)s)") % {
1076+
"prop": prop, "name": name, "value": value })
1077+
else:
1078+
# use repr so strings with embedded \n etc. don't
1079+
# generate newlines in output. Try to keep each
1080+
# journal entry on 1 line.
1081+
result.append(_("%(prop)s was %(value)s") % {
1082+
"prop": prop, "value": repr(value)})
1083+
elif isinstance(value, list):
1084+
# test to see if there is a key prop.
1085+
# Assumption, geting None here means no key
1086+
# is defined for the property's class.
1087+
name = get_prop_name(value[0], prop)
1088+
if name is not None:
1089+
list_items = ["%s(%s)" %
1090+
(get_prop_name(v, prop), v)
1091+
for v in value]
1092+
else:
1093+
prop_class = get_prop_class(prop)
1094+
if prop_class: # noqa: SIM108
1095+
list_items = [ "%s%s" % (prop_class, v)
1096+
for v in value ]
1097+
else:
1098+
list_items = value
1099+
1100+
result.append(_("%(prop)s was [%(value_list)s]") % {
1101+
"prop": prop, "value_list": ", ".join(list_items)})
1102+
elif isinstance(value, tuple):
1103+
# operation data
1104+
decorated = [_format_tuple_change(data, prop)
1105+
for data in value]
1106+
result.append(# .Hint modified nosy: added demo(3)
1107+
_("modified %(prop)s: %(how)s") % {
1108+
"prop": prop, "how": ", ".join(decorated)})
1109+
else:
1110+
result.append(_("%(prop)s was %(value)s") % {
1111+
"prop": prop, "value": value})
1112+
1113+
return '; '.join(result)
1114+
1115+
def format_unlink (data):
1116+
'''data = ('issue', '157', 'dependson')'''
1117+
return "removed %s%s from %s" % (data[0], data[1], data[2])
1118+
1119+
formatters = {
1120+
"create": format_report_class,
1121+
"link": format_link,
1122+
"restored": format_report_class,
1123+
"retired": format_report_class,
1124+
"set": format_set,
1125+
"unlink": format_unlink,
1126+
}
9801127

9811128
try:
982-
print(self.db.getclass(classname).history(nodeid,
983-
skipquiet=skipquiet))
1129+
# returns a tuple: (
1130+
# [0] = nodeid
1131+
# [1] = date
1132+
# [2] = userid
1133+
# [3] = operation
1134+
# [4] = details
1135+
raw_history = self.db.getclass(classname).history(nodeid,
1136+
skipquiet=skipquiet)
1137+
if raw:
1138+
print(raw_history)
1139+
return 0
1140+
1141+
def make_readable(hist):
1142+
return "%s(%s) %s %s" % (self.db.user.get(hist[2], 'username'),
1143+
hist[1],
1144+
hist[3],
1145+
formatters.get(hist[3], lambda x: x)(
1146+
hist[4]))
1147+
printable_history = [make_readable(hist) for hist in raw_history]
1148+
1149+
print("\n".join(printable_history))
9841150
except KeyError:
9851151
raise UsageError(_('no such class "%(classname)s"') % locals())
9861152
except IndexError:
@@ -2103,7 +2269,7 @@ def interactive(self):
21032269
".roundup_admin_history")
21042270

21052271
try:
2106-
import readline # noqa: F401
2272+
import readline
21072273
readline.read_init_file(initfile)
21082274
try:
21092275
readline.read_history_file(histfile)
@@ -2168,10 +2334,10 @@ def main(self): # noqa: PLR0912, PLR0911
21682334
self.print_designator = 0
21692335
self.verbose = 0
21702336
for opt, arg in opts:
2171-
if opt == '-h': # noqa: RET505 - allow elif after returns
2337+
if opt == '-h':
21722338
self.usage()
21732339
return 0
2174-
elif opt == '-v':
2340+
elif opt == '-v': # noqa: RET505 - allow elif after returns
21752341
print('%s (python %s)' % (roundup_version,
21762342
sys.version.split()[0]))
21772343
return 0

share/man/man1/roundup-admin.1

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,11 @@ Retrieves the property value of the nodes specified
119119
by the designators.
120120

121121
.TP
122-
\fBhistory\fP \fIdesignator [skipquiet]\fP
122+
\fBhistory\fP \fIdesignator [skipquiet] [raw]\fP
123123
Lists the journal entries viewable by the user for the
124-
node identified by the designator. If skipquiet is the
125-
second argument, journal entries for quiet properties
126-
are not shown.
124+
node identified by the designator. If skipquiet is added, journal
125+
entries for quiet properties are not shown. Without the raw option
126+
a more human readable output format is used.
127127
.TP
128128
\fBimport\fP \fIimport_dir\fP
129129
Import a database from the directory containing CSV files,

0 commit comments

Comments
 (0)