comparison roundup/admin.py @ 7797:8bdf0484215c

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
author John Rouillard <rouilj@ieee.org>
date Sun, 10 Mar 2024 21:59:20 -0400
parents 5f3b49bb7742
children f84f7879c768
comparison
equal deleted inserted replaced
7796:5f3b49bb7742 7797:8bdf0484215c
951 else: 951 else:
952 print(line) 952 print(line)
953 return 0 953 return 0
954 954
955 def do_history(self, args): 955 def do_history(self, args):
956 ''"""Usage: history designator [skipquiet] 956 ''"""Usage: history designator [skipquiet] [raw]
957 Show the history entries of a designator. 957 Show the history entries of a designator.
958 958
959 A designator is a classname and a nodeid concatenated, 959 A designator is a classname and a nodeid concatenated,
960 eg. bug1, user10, ... 960 eg. bug1, user10, ...
961 961
962 Lists the journal entries viewable by the user for the 962 Lists the journal entries viewable by the user for the node
963 node identified by the designator. If skipquiet is the 963 identified by the designator. If skipquiet is added, journal
964 second argument, journal entries for quiet properties 964 entries for quiet properties are not shown. If raw is added,
965 are not shown. 965 the output is the raw representation of the journal entries.
966 """ 966 """
967 967
968 if len(args) < 1: 968 if len(args) < 1:
969 raise UsageError(_('Not enough arguments supplied')) 969 raise UsageError(_('Not enough arguments supplied'))
970 try: 970 try:
971 classname, nodeid = hyperdb.splitDesignator(args[0]) 971 classname, nodeid = hyperdb.splitDesignator(args[0])
972 except hyperdb.DesignatorError as message: 972 except hyperdb.DesignatorError as message:
973 raise UsageError(message) 973 raise UsageError(message)
974 974
975 skipquiet = False 975 valid_args = ['skipquiet', 'raw']
976 if len(args) == 2: 976
977 if args[1] != 'skipquiet': 977 if len(args) >= 2:
978 raise UsageError("Second argument is not skipquiet") 978 check = [a for a in args[1:] if a not in valid_args]
979 skipquiet = True 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 }
980 1127
981 try: 1128 try:
982 print(self.db.getclass(classname).history(nodeid, 1129 # returns a tuple: (
983 skipquiet=skipquiet)) 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))
984 except KeyError: 1150 except KeyError:
985 raise UsageError(_('no such class "%(classname)s"') % locals()) 1151 raise UsageError(_('no such class "%(classname)s"') % locals())
986 except IndexError: 1152 except IndexError:
987 raise UsageError(_('no such %(classname)s node ' 1153 raise UsageError(_('no such %(classname)s node '
988 '"%(nodeid)s"') % locals()) 1154 '"%(nodeid)s"') % locals())
2101 ".roundup_admin_rlrc") 2267 ".roundup_admin_rlrc")
2102 histfile = os.path.join(os.path.expanduser("~"), 2268 histfile = os.path.join(os.path.expanduser("~"),
2103 ".roundup_admin_history") 2269 ".roundup_admin_history")
2104 2270
2105 try: 2271 try:
2106 import readline # noqa: F401 2272 import readline
2107 readline.read_init_file(initfile) 2273 readline.read_init_file(initfile)
2108 try: 2274 try:
2109 readline.read_history_file(histfile) 2275 readline.read_history_file(histfile)
2110 except FileNotFoundError: 2276 except FileNotFoundError:
2111 # no history file yet 2277 # no history file yet
2166 self.password = login_env[1] 2332 self.password = login_env[1]
2167 self.separator = None 2333 self.separator = None
2168 self.print_designator = 0 2334 self.print_designator = 0
2169 self.verbose = 0 2335 self.verbose = 0
2170 for opt, arg in opts: 2336 for opt, arg in opts:
2171 if opt == '-h': # noqa: RET505 - allow elif after returns 2337 if opt == '-h':
2172 self.usage() 2338 self.usage()
2173 return 0 2339 return 0
2174 elif opt == '-v': 2340 elif opt == '-v': # noqa: RET505 - allow elif after returns
2175 print('%s (python %s)' % (roundup_version, 2341 print('%s (python %s)' % (roundup_version,
2176 sys.version.split()[0])) 2342 sys.version.split()[0]))
2177 return 0 2343 return 0
2178 elif opt == '-V': 2344 elif opt == '-V':
2179 self.verbose = 1 2345 self.verbose = 1

Roundup Issue Tracker: http://roundup-tracker.org/