Mercurial > p > roundup > code
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 |
