Mercurial > p > roundup > code
comparison roundup/rest.py @ 8188:e5362f8e1808
chore(ruff): rename input and id parms/vars - don't shadow builtin
Changed input parm to input_payload and id param to node_id for etag context.
Changed var/param id to item_id. Change var id to working_id.
Trying to clear a lot of ruff warnings as client.py and rest.py will
be getting some work done on them and I want to reduce erros so I can
see errors in newer code more easily.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Wed, 11 Dec 2024 13:04:34 -0500 |
| parents | d02ce1d14acd |
| children | 569aff540a21 |
comparison
equal
deleted
inserted
replaced
| 8187:5c506c778893 | 8188:e5362f8e1808 |
|---|---|
| 171 f.openapi_doc = d | 171 f.openapi_doc = d |
| 172 return f | 172 return f |
| 173 return wrapper | 173 return wrapper |
| 174 | 174 |
| 175 | 175 |
| 176 def calculate_etag(node, key, classname="Missing", id="0", | 176 def calculate_etag(node, key, classname="Missing", node_id="0", |
| 177 repr_format="json"): | 177 repr_format="json"): |
| 178 '''given a hyperdb node generate a hashed representation of it to be | 178 '''given a hyperdb node generate a hashed representation of it to be |
| 179 used as an etag. | 179 used as an etag. |
| 180 | 180 |
| 181 This code needs a __repr__ function in the Password class. This | 181 This code needs a __repr__ function in the Password class. This |
| 192 calculate the etag. | 192 calculate the etag. |
| 193 | 193 |
| 194 Note that repr() is chosen for the node rather than str() since | 194 Note that repr() is chosen for the node rather than str() since |
| 195 repr is meant to be an unambiguous representation. | 195 repr is meant to be an unambiguous representation. |
| 196 | 196 |
| 197 classname and id are used for logging only. | 197 classname and node_id are used for logging only. |
| 198 ''' | 198 ''' |
| 199 | 199 |
| 200 items = node.items(protected=True) # include every item | 200 items = node.items(protected=True) # include every item |
| 201 etag = hmac.new(bs2b(key), bs2b(repr_format + | 201 etag = hmac.new(bs2b(key), bs2b(repr_format + |
| 202 repr(sorted(items))), md5).hexdigest() | 202 repr(sorted(items))), md5).hexdigest() |
| 203 logger.debug("object=%s%s; tag=%s; repr=%s", classname, id, | 203 logger.debug("object=%s%s; tag=%s; repr=%s", classname, node_id, |
| 204 etag, repr(node.items(protected=True))) | 204 etag, repr(node.items(protected=True))) |
| 205 # Quotes are part of ETag spec, normal headers don't have quotes | 205 # Quotes are part of ETag spec, normal headers don't have quotes |
| 206 return '"%s"' % etag | 206 return '"%s"' % etag |
| 207 | 207 |
| 208 | 208 |
| 209 def check_etag(node, key, etags, classname="Missing", id="0", | 209 def check_etag(node, key, etags, classname="Missing", node_id="0", |
| 210 repr_format="json"): | 210 repr_format="json"): |
| 211 '''Take a list of etags and compare to the etag for the given node. | 211 '''Take a list of etags and compare to the etag for the given node. |
| 212 | 212 |
| 213 Iterate over all supplied etags, | 213 Iterate over all supplied etags, |
| 214 If a tag fails to match, return False. | 214 If a tag fails to match, return False. |
| 216 If all etags are None, return False. | 216 If all etags are None, return False. |
| 217 | 217 |
| 218 ''' | 218 ''' |
| 219 have_etag_match = False | 219 have_etag_match = False |
| 220 | 220 |
| 221 node_etag = calculate_etag(node, key, classname, id, | 221 node_etag = calculate_etag(node, key, classname, node_id, |
| 222 repr_format=repr_format) | 222 repr_format=repr_format) |
| 223 | 223 |
| 224 for etag in etags: | 224 for etag in etags: |
| 225 # etag includes doublequotes around tag: | 225 # etag includes doublequotes around tag: |
| 226 # '"a46a5572190e4fad63958c135f3746fa"' | 226 # '"a46a5572190e4fad63958c135f3746fa"' |
| 243 return True | 243 return True |
| 244 else: | 244 else: |
| 245 return False | 245 return False |
| 246 | 246 |
| 247 | 247 |
| 248 def obtain_etags(headers, input): | 248 def obtain_etags(headers, input_payload): |
| 249 '''Get ETags value from headers or payload data''' | 249 '''Get ETags value from headers or payload data |
| 250 Only supports one etag value not list. | |
| 251 ''' | |
| 250 etags = [] | 252 etags = [] |
| 251 if '@etag' in input: | 253 if '@etag' in input_payload: |
| 252 etags.append(input['@etag'].value) | 254 etags.append(input_payload['@etag'].value) |
| 253 etags.append(headers.get("If-Match", None)) | 255 etags.append(headers.get("If-Match", None)) |
| 254 return etags | 256 return etags |
| 255 | 257 |
| 256 | 258 |
| 257 def parse_accept_header(accept): | 259 def parse_accept_header(accept): |
| 388 cls.__route_map[pattern] = rule_route | 390 cls.__route_map[pattern] = rule_route |
| 389 return func | 391 return func |
| 390 return decorator | 392 return decorator |
| 391 | 393 |
| 392 @classmethod | 394 @classmethod |
| 393 def execute(cls, instance, path, method, input): | 395 def execute(cls, instance, path, method, input_payload): |
| 394 # format the input, note that we may not lowercase the path | 396 # format the input_payload, note that we may not lowercase the path |
| 395 # here, URL parameters are case-sensitive | 397 # here, URL parameters are case-sensitive |
| 396 path = path.strip('/') | 398 path = path.strip('/') |
| 397 if path == 'rest': | 399 if path == 'rest': |
| 398 # allow handler to be called for /rest/ | 400 # allow handler to be called for /rest/ |
| 399 path = 'rest/' | 401 path = 'rest/' |
| 420 list_vars = func_obj['vars'] | 422 list_vars = func_obj['vars'] |
| 421 func = func_obj['func'] | 423 func = func_obj['func'] |
| 422 | 424 |
| 423 # zip the varlist into a dictionary, and pass it to the caller | 425 # zip the varlist into a dictionary, and pass it to the caller |
| 424 args = dict(zip(list_vars, match_obj.groups())) | 426 args = dict(zip(list_vars, match_obj.groups())) |
| 425 args['input'] = input | 427 args['input'] = input_payload |
| 426 return func(instance, **args) | 428 return func(instance, **args) |
| 427 raise NotFound('Nothing matches the given URI') | 429 raise NotFound('Nothing matches the given URI') |
| 428 | 430 |
| 429 | 431 |
| 430 class RestfulInstance(object): | 432 class RestfulInstance(object): |
| 666 else: | 668 else: |
| 667 raise UsageError('PATCH Operation %s is not allowed' % op) | 669 raise UsageError('PATCH Operation %s is not allowed' % op) |
| 668 | 670 |
| 669 return result | 671 return result |
| 670 | 672 |
| 671 def raise_if_no_etag(self, class_name, item_id, input, repr_format="json"): | 673 def raise_if_no_etag(self, class_name, item_id, input_payload, |
| 674 repr_format="json"): | |
| 672 class_obj = self.db.getclass(class_name) | 675 class_obj = self.db.getclass(class_name) |
| 673 if not check_etag(class_obj.getnode(item_id), | 676 if not check_etag(class_obj.getnode(item_id), |
| 674 self.db.config.WEB_SECRET_KEY, | 677 self.db.config.WEB_SECRET_KEY, |
| 675 obtain_etags(self.client.request.headers, input), | 678 obtain_etags(self.client.request.headers, |
| 676 class_name, | 679 input_payload), class_name, item_id, |
| 677 item_id, repr_format=repr_format): | 680 repr_format=repr_format): |
| 678 raise PreconditionFailed( | 681 raise PreconditionFailed( |
| 679 "If-Match is missing or does not match." | 682 "If-Match is missing or does not match." |
| 680 " Retrieve asset and retry modification if valid.") | 683 " Retrieve asset and retry modification if valid.") |
| 681 | 684 |
| 682 def format_item(self, node, item_id, props=None, verbose=1): | 685 def format_item(self, node, item_id, props=None, verbose=1): |
| 697 result = {} | 700 result = {} |
| 698 try: | 701 try: |
| 699 # pn = propname | 702 # pn = propname |
| 700 for pn in sorted(props): | 703 for pn in sorted(props): |
| 701 ok = False | 704 ok = False |
| 702 id = item_id | 705 working_id = item_id |
| 703 nd = node | 706 nd = node |
| 704 cn = class_name | 707 cn = class_name |
| 705 for p in pn.split('.'): | 708 for p in pn.split('.'): |
| 706 if not self.db.security.hasPermission( | 709 if not self.db.security.hasPermission( |
| 707 'View', uid, cn, p, id | 710 'View', uid, cn, p, working_id |
| 708 ): | 711 ): |
| 709 break | 712 break |
| 710 cl = self.db.getclass(cn) | 713 cl = self.db.getclass(cn) |
| 711 nd = cl.getnode(id) | 714 nd = cl.getnode(working_id) |
| 712 id = v = getattr(nd, p) | 715 working_id = v = getattr(nd, p) |
| 713 # Handle transitive properties where something on | 716 # Handle transitive properties where something on |
| 714 # the road is None (empty Link property) | 717 # the road is None (empty Link property) |
| 715 if id is None: | 718 if working_id is None: |
| 716 prop = None | 719 prop = None |
| 717 ok = True | 720 ok = True |
| 718 break | 721 break |
| 719 prop = cl.getprops(protected=True)[p] | 722 prop = cl.getprops(protected=True)[p] |
| 720 cn = getattr(prop, 'classname', None) | 723 cn = getattr(prop, 'classname', None) |
| 726 linkcls = self.db.getclass(prop.classname) | 729 linkcls = self.db.getclass(prop.classname) |
| 727 cp = '%s/%s/' % (self.data_path, prop.classname) | 730 cp = '%s/%s/' % (self.data_path, prop.classname) |
| 728 if verbose and v: | 731 if verbose and v: |
| 729 if isinstance(v, type([])): | 732 if isinstance(v, type([])): |
| 730 r = [] | 733 r = [] |
| 731 for id in v: | 734 for working_id in v: |
| 732 d = dict(id=id, link=cp + id) | 735 d = dict(id=working_id, link=cp + working_id) |
| 733 if verbose > 1: | 736 if verbose > 1: |
| 734 label = linkcls.labelprop() | 737 label = linkcls.labelprop() |
| 735 d[label] = linkcls.get(id, label) | 738 d[label] = linkcls.get(working_id, label) |
| 736 r.append(d) | 739 r.append(d) |
| 737 result[pn] = r | 740 result[pn] = r |
| 738 else: | 741 else: |
| 739 result[pn] = dict(id=v, link=cp + v) | 742 result[pn] = dict(id=v, link=cp + v) |
| 740 if verbose > 1: | 743 if verbose > 1: |
| 766 | 769 |
| 767 return result | 770 return result |
| 768 | 771 |
| 769 @Routing.route("/data/<:class_name>", 'GET') | 772 @Routing.route("/data/<:class_name>", 'GET') |
| 770 @_data_decorator | 773 @_data_decorator |
| 771 def get_collection(self, class_name, input): | 774 def get_collection(self, class_name, input_payload): |
| 772 """GET resource from class URI. | 775 """GET resource from class URI. |
| 773 | 776 |
| 774 This function returns only items have View permission | 777 This function returns only items have View permission |
| 775 class_name should be valid already | 778 class_name should be valid already |
| 776 | 779 |
| 777 Args: | 780 Args: |
| 778 class_name (string): class name of the resource (Ex: issue, msg) | 781 class_name (string): class name of the resource (Ex: issue, msg) |
| 779 input (list): the submitted form of the user | 782 input_payload (list): the submitted form of the user |
| 780 | 783 |
| 781 Returns: | 784 Returns: |
| 782 int: http status code 200 (OK) | 785 int: http status code 200 (OK) |
| 783 list: list of reference item in the class | 786 list: list of reference item in the class |
| 784 id: id of the object | 787 id: id of the object |
| 804 } | 807 } |
| 805 verbose = 1 | 808 verbose = 1 |
| 806 display_props = set() | 809 display_props = set() |
| 807 sort = [] | 810 sort = [] |
| 808 group = [] | 811 group = [] |
| 809 for form_field in input.value: | 812 for form_field in input_payload.value: |
| 810 key = form_field.name | 813 key = form_field.name |
| 811 value = form_field.value | 814 value = form_field.value |
| 812 if key.startswith("@page_"): # serve the paging purpose | 815 if key.startswith("@page_"): # serve the paging purpose |
| 813 key = key[6:] | 816 key = key[6:] |
| 814 value = int(value) | 817 value = int(value) |
| 1019 result['@links'][rel].append({ | 1022 result['@links'][rel].append({ |
| 1020 'rel': rel, | 1023 'rel': rel, |
| 1021 'uri': "%s/%s?@page_index=%s&" % (self.data_path, | 1024 'uri': "%s/%s?@page_index=%s&" % (self.data_path, |
| 1022 class_name, index) + | 1025 class_name, index) + |
| 1023 '&'.join(["%s=%s" % (field.name, field.value) | 1026 '&'.join(["%s=%s" % (field.name, field.value) |
| 1024 for field in input.value | 1027 for field in input_payload.value |
| 1025 if field.name != "@page_index"])}) | 1028 if field.name != "@page_index"])}) |
| 1026 | 1029 |
| 1027 result['@total_size'] = total_len | 1030 result['@total_size'] = total_len |
| 1028 self.client.setHeader("X-Count-Total", str(total_len)) | 1031 self.client.setHeader("X-Count-Total", str(total_len)) |
| 1029 self.client.setHeader("Allow", "OPTIONS, GET, POST") | 1032 self.client.setHeader("Allow", "OPTIONS, GET, POST") |
| 1030 return 200, result | 1033 return 200, result |
| 1031 | 1034 |
| 1032 @Routing.route("/data/user/roles", 'GET') | 1035 @Routing.route("/data/user/roles", 'GET') |
| 1033 @_data_decorator | 1036 @_data_decorator |
| 1034 def get_roles(self, input): | 1037 def get_roles(self, input_payload): |
| 1035 """Return all defined roles for users with Admin role. | 1038 """Return all defined roles for users with Admin role. |
| 1036 The User class property roles is a string but simulate | 1039 The User class property roles is a string but simulate |
| 1037 it as a MultiLink to an actual Roles class. | 1040 it as a MultiLink to an actual Roles class. |
| 1038 """ | 1041 """ |
| 1039 if not self.client.db.user.has_role(self.client.db.getuid(), "Admin"): | 1042 if not self.client.db.user.has_role(self.client.db.getuid(), "Admin"): |
| 1049 [{"id": rolename,"name": rolename} | 1052 [{"id": rolename,"name": rolename} |
| 1050 for rolename in list(self.db.security.role.keys())]} | 1053 for rolename in list(self.db.security.role.keys())]} |
| 1051 | 1054 |
| 1052 @Routing.route("/data/<:class_name>/<:item_id>", 'GET') | 1055 @Routing.route("/data/<:class_name>/<:item_id>", 'GET') |
| 1053 @_data_decorator | 1056 @_data_decorator |
| 1054 def get_element(self, class_name, item_id, input): | 1057 def get_element(self, class_name, item_id, input_payload): |
| 1055 """GET resource from object URI. | 1058 """GET resource from object URI. |
| 1056 | 1059 |
| 1057 This function returns only properties have View permission | 1060 This function returns only properties have View permission |
| 1058 class_name and item_id should be valid already | 1061 class_name and item_id should be valid already |
| 1059 | 1062 |
| 1060 Args: | 1063 Args: |
| 1061 class_name (string): class name of the resource (Ex: issue, msg) | 1064 class_name (string): class name of the resource (Ex: issue, msg) |
| 1062 item_id (string): id of the resource (Ex: 12, 15) | 1065 item_id (string): id of the resource (Ex: 12, 15) |
| 1063 or (if the class has a key property) this can also be | 1066 or (if the class has a key property) this can also be |
| 1064 the key name, e.g. class_name = status, item_id = 'open' | 1067 the key name, e.g. class_name = status, item_id = 'open' |
| 1065 input (list): the submitted form of the user | 1068 input_payload (list): the submitted form of the user |
| 1066 | 1069 |
| 1067 Returns: | 1070 Returns: |
| 1068 int: http status code 200 (OK) | 1071 int: http status code 200 (OK) |
| 1069 dict: a dictionary represents the object | 1072 dict: a dictionary represents the object |
| 1070 id: id of the object | 1073 id: id of the object |
| 1110 etag = calculate_etag(node, self.db.config.WEB_SECRET_KEY, | 1113 etag = calculate_etag(node, self.db.config.WEB_SECRET_KEY, |
| 1111 class_name, itemid, repr_format="json") | 1114 class_name, itemid, repr_format="json") |
| 1112 props = None | 1115 props = None |
| 1113 protected = False | 1116 protected = False |
| 1114 verbose = 1 | 1117 verbose = 1 |
| 1115 for form_field in input.value: | 1118 for form_field in input_payload.value: |
| 1116 key = form_field.name | 1119 key = form_field.name |
| 1117 value = form_field.value | 1120 value = form_field.value |
| 1118 if key == "@fields" or key == "@attrs": | 1121 if key == "@fields" or key == "@attrs": |
| 1119 if props is None: | 1122 if props is None: |
| 1120 props = set() | 1123 props = set() |
| 1151 self.client.setHeader("ETag", etag) | 1154 self.client.setHeader("ETag", etag) |
| 1152 return 200, result | 1155 return 200, result |
| 1153 | 1156 |
| 1154 @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'GET') | 1157 @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'GET') |
| 1155 @_data_decorator | 1158 @_data_decorator |
| 1156 def get_attribute(self, class_name, item_id, attr_name, input): | 1159 def get_attribute(self, class_name, item_id, attr_name, input_payload): |
| 1157 """GET resource from attribute URI. | 1160 """GET resource from attribute URI. |
| 1158 | 1161 |
| 1159 This function returns only attribute has View permission | 1162 This function returns only attribute has View permission |
| 1160 class_name should be valid already | 1163 class_name should be valid already |
| 1161 | 1164 |
| 1162 Args: | 1165 Args: |
| 1163 class_name (string): class name of the resource (Ex: issue, msg) | 1166 class_name (string): class name of the resource (Ex: issue, msg) |
| 1164 item_id (string): id of the resource (Ex: 12, 15) | 1167 item_id (string): id of the resource (Ex: 12, 15) |
| 1165 attr_name (string): attribute of the resource (Ex: title, nosy) | 1168 attr_name (string): attribute of the resource (Ex: title, nosy) |
| 1166 input (list): the submitted form of the user | 1169 input_payload (list): the submitted form of the user |
| 1167 | 1170 |
| 1168 Returns: | 1171 Returns: |
| 1169 int: http status code 200 (OK) | 1172 int: http status code 200 (OK) |
| 1170 list: a dictionary represents the attribute | 1173 list: a dictionary represents the attribute |
| 1171 id: id of the object | 1174 id: id of the object |
| 1203 self.client.setHeader("ETag", etag) | 1206 self.client.setHeader("ETag", etag) |
| 1204 return 200, result | 1207 return 200, result |
| 1205 | 1208 |
| 1206 @Routing.route("/data/<:class_name>", 'POST') | 1209 @Routing.route("/data/<:class_name>", 'POST') |
| 1207 @_data_decorator | 1210 @_data_decorator |
| 1208 def post_collection(self, class_name, input): | 1211 def post_collection(self, class_name, input_payload): |
| 1209 """POST a new object to a class | 1212 """POST a new object to a class |
| 1210 | 1213 |
| 1211 If the item is successfully created, the "Location" header will also | 1214 If the item is successfully created, the "Location" header will also |
| 1212 contain the link to the created object | 1215 contain the link to the created object |
| 1213 | 1216 |
| 1214 Args: | 1217 Args: |
| 1215 class_name (string): class name of the resource (Ex: issue, msg) | 1218 class_name (string): class name of the resource (Ex: issue, msg) |
| 1216 input (list): the submitted form of the user | 1219 input_payload (list): the submitted form of the user |
| 1217 | 1220 |
| 1218 Returns: | 1221 Returns: |
| 1219 int: http status code 201 (Created) | 1222 int: http status code 201 (Created) |
| 1220 dict: a reference item to the created object | 1223 dict: a reference item to the created object |
| 1221 id: id of the object | 1224 id: id of the object |
| 1222 link: path to the object | 1225 link: path to the object |
| 1223 """ | 1226 """ |
| 1224 return self.post_collection_inner(class_name, input) | 1227 return self.post_collection_inner(class_name, input_payload) |
| 1225 | 1228 |
| 1226 @Routing.route("/data/<:class_name>/@poe", 'POST') | 1229 @Routing.route("/data/<:class_name>/@poe", 'POST') |
| 1227 @_data_decorator | 1230 @_data_decorator |
| 1228 def get_post_once_exactly(self, class_name, input): | 1231 def get_post_once_exactly(self, class_name, input_payload): |
| 1229 """Get the Post Once Exactly token to create a new instance of class | 1232 """Get the Post Once Exactly token to create a new instance of class |
| 1230 See https://tools.ietf.org/html/draft-nottingham-http-poe-00""" | 1233 See https://tools.ietf.org/html/draft-nottingham-http-poe-00""" |
| 1231 otks = self.db.Otk | 1234 otks = self.db.Otk |
| 1232 poe_key = otks.getUniqueKey() | 1235 poe_key = otks.getUniqueKey() |
| 1233 | 1236 |
| 1234 try: | 1237 try: |
| 1235 lifetime = int(input['lifetime'].value) | 1238 lifetime = int(input_payload['lifetime'].value) |
| 1236 except KeyError: | 1239 except KeyError: |
| 1237 lifetime = 30 * 60 # 30 minutes | 1240 lifetime = 30 * 60 # 30 minutes |
| 1238 except ValueError: | 1241 except ValueError: |
| 1239 raise UsageError("Value 'lifetime' must be an integer specify lifetime in seconds. Got %s." % input['lifetime'].value) | 1242 raise UsageError("Value 'lifetime' must be an integer specify lifetime in seconds. Got %s." % input_payload['lifetime'].value) |
| 1240 | 1243 |
| 1241 if lifetime > 3600 or lifetime < 1: | 1244 if lifetime > 3600 or lifetime < 1: |
| 1242 raise UsageError("Value 'lifetime' must be between 1 second and 1 hour (3600 seconds). Got %s." % input['lifetime'].value) | 1245 raise UsageError("Value 'lifetime' must be between 1 second and 1 hour (3600 seconds). Got %s." % input_payload['lifetime'].value) |
| 1243 | 1246 |
| 1244 try: | 1247 try: |
| 1245 # if generic tag exists, we don't care about the value | 1248 # if generic tag exists, we don't care about the value |
| 1246 is_generic = input['generic'] | 1249 is_generic = input_payload['generic'] |
| 1247 # we generate a generic POE token | 1250 # we generate a generic POE token |
| 1248 is_generic = True | 1251 is_generic = True |
| 1249 except KeyError: | 1252 except KeyError: |
| 1250 is_generic = False | 1253 is_generic = False |
| 1251 | 1254 |
| 1267 (self.data_path, class_name, poe_key), | 1270 (self.data_path, class_name, poe_key), |
| 1268 'expires': ts + (60 * 60 * 24 * 7)} | 1271 'expires': ts + (60 * 60 * 24 * 7)} |
| 1269 | 1272 |
| 1270 @Routing.route("/data/<:class_name>/@poe/<:post_token>", 'POST') | 1273 @Routing.route("/data/<:class_name>/@poe/<:post_token>", 'POST') |
| 1271 @_data_decorator | 1274 @_data_decorator |
| 1272 def post_once_exactly_collection(self, class_name, post_token, input): | 1275 def post_once_exactly_collection(self, class_name, post_token, input_payload): |
| 1273 """Post exactly one to the resource named by class_name""" | 1276 """Post exactly one to the resource named by class_name""" |
| 1274 otks = self.db.Otk | 1277 otks = self.db.Otk |
| 1275 | 1278 |
| 1276 # remove expired keys so we don't use an expired key | 1279 # remove expired keys so we don't use an expired key |
| 1277 otks.clean() | 1280 otks.clean() |
| 1306 | 1309 |
| 1307 if cn != class_name and cn is not None: | 1310 if cn != class_name and cn is not None: |
| 1308 raise UsageError("POE token '%s' not valid for %s, was generated for class %s" % (post_token, class_name, cn)) | 1311 raise UsageError("POE token '%s' not valid for %s, was generated for class %s" % (post_token, class_name, cn)) |
| 1309 | 1312 |
| 1310 # handle this as though they POSTed to /rest/data/class | 1313 # handle this as though they POSTed to /rest/data/class |
| 1311 return self.post_collection_inner(class_name, input) | 1314 return self.post_collection_inner(class_name, input_payload) |
| 1312 | 1315 |
| 1313 def post_collection_inner(self, class_name, input): | 1316 def post_collection_inner(self, class_name, input_payload): |
| 1314 if class_name not in self.db.classes: | 1317 if class_name not in self.db.classes: |
| 1315 raise NotFound('Class %s not found' % class_name) | 1318 raise NotFound('Class %s not found' % class_name) |
| 1316 if not self.db.security.hasPermission( | 1319 if not self.db.security.hasPermission( |
| 1317 'Create', self.db.getuid(), class_name | 1320 'Create', self.db.getuid(), class_name |
| 1318 ): | 1321 ): |
| 1319 raise Unauthorised('Permission to create %s denied' % class_name) | 1322 raise Unauthorised('Permission to create %s denied' % class_name) |
| 1320 | 1323 |
| 1321 class_obj = self.db.getclass(class_name) | 1324 class_obj = self.db.getclass(class_name) |
| 1322 | 1325 |
| 1323 # convert types | 1326 # convert types |
| 1324 props = self.props_from_args(class_obj, input.value) | 1327 props = self.props_from_args(class_obj, input_payload.value) |
| 1325 | 1328 |
| 1326 # check for the key property | 1329 # check for the key property |
| 1327 key = class_obj.getkey() | 1330 key = class_obj.getkey() |
| 1328 if key and key not in props: | 1331 if key and key not in props: |
| 1329 raise UsageError("Must provide the '%s' property." % key) | 1332 raise UsageError("Must provide the '%s' property." % key) |
| 1365 } | 1368 } |
| 1366 return 201, result | 1369 return 201, result |
| 1367 | 1370 |
| 1368 @Routing.route("/data/<:class_name>/<:item_id>", 'PUT') | 1371 @Routing.route("/data/<:class_name>/<:item_id>", 'PUT') |
| 1369 @_data_decorator | 1372 @_data_decorator |
| 1370 def put_element(self, class_name, item_id, input): | 1373 def put_element(self, class_name, item_id, input_payload): |
| 1371 """PUT a new content to an object | 1374 """PUT a new content to an object |
| 1372 | 1375 |
| 1373 Replace the content of the existing object | 1376 Replace the content of the existing object |
| 1374 | 1377 |
| 1375 Args: | 1378 Args: |
| 1376 class_name (string): class name of the resource (Ex: issue, msg) | 1379 class_name (string): class name of the resource (Ex: issue, msg) |
| 1377 item_id (string): id of the resource (Ex: 12, 15) | 1380 item_id (string): id of the resource (Ex: 12, 15) |
| 1378 input (list): the submitted form of the user | 1381 input_payload (list): the submitted form of the user |
| 1379 | 1382 |
| 1380 Returns: | 1383 Returns: |
| 1381 int: http status code 200 (OK) | 1384 int: http status code 200 (OK) |
| 1382 dict: a dictionary represents the modified object | 1385 dict: a dictionary represents the modified object |
| 1383 id: id of the object | 1386 id: id of the object |
| 1388 """ | 1391 """ |
| 1389 if class_name not in self.db.classes: | 1392 if class_name not in self.db.classes: |
| 1390 raise NotFound('Class %s not found' % class_name) | 1393 raise NotFound('Class %s not found' % class_name) |
| 1391 class_obj = self.db.getclass(class_name) | 1394 class_obj = self.db.getclass(class_name) |
| 1392 | 1395 |
| 1393 props = self.props_from_args(class_obj, input.value, item_id) | 1396 props = self.props_from_args(class_obj, input_payload.value, item_id) |
| 1394 for p in props: | 1397 for p in props: |
| 1395 if not self.db.security.hasPermission( | 1398 if not self.db.security.hasPermission( |
| 1396 'Edit', self.db.getuid(), class_name, p, item_id | 1399 'Edit', self.db.getuid(), class_name, p, item_id |
| 1397 ): | 1400 ): |
| 1398 raise Unauthorised( | 1401 raise Unauthorised( |
| 1399 'Permission to edit %s of %s%s denied' % | 1402 'Permission to edit %s of %s%s denied' % |
| 1400 (p, class_name, item_id) | 1403 (p, class_name, item_id) |
| 1401 ) | 1404 ) |
| 1402 try: | 1405 try: |
| 1403 self.raise_if_no_etag(class_name, item_id, input) | 1406 self.raise_if_no_etag(class_name, item_id, input_payload) |
| 1404 result = class_obj.set(item_id, **props) | 1407 result = class_obj.set(item_id, **props) |
| 1405 self.db.commit() | 1408 self.db.commit() |
| 1406 except (TypeError, IndexError, ValueError) as message: | 1409 except (TypeError, IndexError, ValueError) as message: |
| 1407 raise ValueError(message) | 1410 raise ValueError(message) |
| 1408 except KeyError as message: | 1411 except KeyError as message: |
| 1418 } | 1421 } |
| 1419 return 200, result | 1422 return 200, result |
| 1420 | 1423 |
| 1421 @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'PUT') | 1424 @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'PUT') |
| 1422 @_data_decorator | 1425 @_data_decorator |
| 1423 def put_attribute(self, class_name, item_id, attr_name, input): | 1426 def put_attribute(self, class_name, item_id, attr_name, input_payload): |
| 1424 """PUT an attribute to an object | 1427 """PUT an attribute to an object |
| 1425 | 1428 |
| 1426 Args: | 1429 Args: |
| 1427 class_name (string): class name of the resource (Ex: issue, msg) | 1430 class_name (string): class name of the resource (Ex: issue, msg) |
| 1428 item_id (string): id of the resource (Ex: 12, 15) | 1431 item_id (string): id of the resource (Ex: 12, 15) |
| 1429 attr_name (string): attribute of the resource (Ex: title, nosy) | 1432 attr_name (string): attribute of the resource (Ex: title, nosy) |
| 1430 input (list): the submitted form of the user | 1433 input_payload (list): the submitted form of the user |
| 1431 | 1434 |
| 1432 Returns: | 1435 Returns: |
| 1433 int: http status code 200 (OK) | 1436 int: http status code 200 (OK) |
| 1434 dict:a dictionary represents the modified object | 1437 dict:a dictionary represents the modified object |
| 1435 id: id of the object | 1438 id: id of the object |
| 1448 (class_name, item_id, attr_name) | 1451 (class_name, item_id, attr_name) |
| 1449 ) | 1452 ) |
| 1450 class_obj = self.db.getclass(class_name) | 1453 class_obj = self.db.getclass(class_name) |
| 1451 props = { | 1454 props = { |
| 1452 attr_name: self.prop_from_arg( | 1455 attr_name: self.prop_from_arg( |
| 1453 class_obj, attr_name, input['data'].value, item_id | 1456 class_obj, attr_name, input_payload['data'].value, item_id |
| 1454 ) | 1457 ) |
| 1455 } | 1458 } |
| 1456 | 1459 |
| 1457 try: | 1460 try: |
| 1458 self.raise_if_no_etag(class_name, item_id, input) | 1461 self.raise_if_no_etag(class_name, item_id, input_payload) |
| 1459 result = class_obj.set(item_id, **props) | 1462 result = class_obj.set(item_id, **props) |
| 1460 self.db.commit() | 1463 self.db.commit() |
| 1461 except (TypeError, IndexError, ValueError) as message: | 1464 except (TypeError, IndexError, ValueError) as message: |
| 1462 raise ValueError(message) | 1465 raise ValueError(message) |
| 1463 except KeyError as message: | 1466 except KeyError as message: |
| 1474 | 1477 |
| 1475 return 200, result | 1478 return 200, result |
| 1476 | 1479 |
| 1477 @Routing.route("/data/<:class_name>", 'DELETE') | 1480 @Routing.route("/data/<:class_name>", 'DELETE') |
| 1478 @_data_decorator | 1481 @_data_decorator |
| 1479 def delete_collection(self, class_name, input): | 1482 def delete_collection(self, class_name, input_payload): |
| 1480 """DELETE (retire) all objects in a class | 1483 """DELETE (retire) all objects in a class |
| 1481 There is currently no use-case, so this is disabled and | 1484 There is currently no use-case, so this is disabled and |
| 1482 always returns Unauthorised. | 1485 always returns Unauthorised. |
| 1483 | 1486 |
| 1484 Args: | 1487 Args: |
| 1485 class_name (string): class name of the resource (Ex: issue, msg) | 1488 class_name (string): class name of the resource (Ex: issue, msg) |
| 1486 input (list): the submitted form of the user | 1489 input_payload (list): the submitted form of the user |
| 1487 | 1490 |
| 1488 Returns: | 1491 Returns: |
| 1489 int: http status code 200 (OK) | 1492 int: http status code 200 (OK) |
| 1490 dict: | 1493 dict: |
| 1491 status (string): 'ok' | 1494 status (string): 'ok' |
| 1524 return 200, result | 1527 return 200, result |
| 1525 ''' | 1528 ''' |
| 1526 | 1529 |
| 1527 @Routing.route("/data/<:class_name>/<:item_id>", 'DELETE') | 1530 @Routing.route("/data/<:class_name>/<:item_id>", 'DELETE') |
| 1528 @_data_decorator | 1531 @_data_decorator |
| 1529 def delete_element(self, class_name, item_id, input): | 1532 def delete_element(self, class_name, item_id, input_payload): |
| 1530 """DELETE (retire) an object in a class | 1533 """DELETE (retire) an object in a class |
| 1531 | 1534 |
| 1532 Args: | 1535 Args: |
| 1533 class_name (string): class name of the resource (Ex: issue, msg) | 1536 class_name (string): class name of the resource (Ex: issue, msg) |
| 1534 item_id (string): id of the resource (Ex: 12, 15) | 1537 item_id (string): id of the resource (Ex: 12, 15) |
| 1535 input (list): the submitted form of the user | 1538 input_payload (list): the submitted form of the user |
| 1536 | 1539 |
| 1537 Returns: | 1540 Returns: |
| 1538 int: http status code 200 (OK) | 1541 int: http status code 200 (OK) |
| 1539 dict: | 1542 dict: |
| 1540 status (string): 'ok' | 1543 status (string): 'ok' |
| 1547 ): | 1550 ): |
| 1548 raise Unauthorised( | 1551 raise Unauthorised( |
| 1549 'Permission to retire %s %s denied' % (class_name, item_id) | 1552 'Permission to retire %s %s denied' % (class_name, item_id) |
| 1550 ) | 1553 ) |
| 1551 | 1554 |
| 1552 self.raise_if_no_etag(class_name, item_id, input) | 1555 self.raise_if_no_etag(class_name, item_id, input_payload) |
| 1553 class_obj.retire(item_id) | 1556 class_obj.retire(item_id) |
| 1554 self.db.commit() | 1557 self.db.commit() |
| 1555 result = { | 1558 result = { |
| 1556 'status': 'ok' | 1559 'status': 'ok' |
| 1557 } | 1560 } |
| 1558 | 1561 |
| 1559 return 200, result | 1562 return 200, result |
| 1560 | 1563 |
| 1561 @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'DELETE') | 1564 @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'DELETE') |
| 1562 @_data_decorator | 1565 @_data_decorator |
| 1563 def delete_attribute(self, class_name, item_id, attr_name, input): | 1566 def delete_attribute(self, class_name, item_id, attr_name, input_payload): |
| 1564 """DELETE an attribute in a object by setting it to None or empty | 1567 """DELETE an attribute in a object by setting it to None or empty |
| 1565 | 1568 |
| 1566 Args: | 1569 Args: |
| 1567 class_name (string): class name of the resource (Ex: issue, msg) | 1570 class_name (string): class name of the resource (Ex: issue, msg) |
| 1568 item_id (string): id of the resource (Ex: 12, 15) | 1571 item_id (string): id of the resource (Ex: 12, 15) |
| 1569 attr_name (string): attribute of the resource (Ex: title, nosy) | 1572 attr_name (string): attribute of the resource (Ex: title, nosy) |
| 1570 input (list): the submitted form of the user | 1573 input_payload (list): the submitted form of the user |
| 1571 | 1574 |
| 1572 Returns: | 1575 Returns: |
| 1573 int: http status code 200 (OK) | 1576 int: http status code 200 (OK) |
| 1574 dict: | 1577 dict: |
| 1575 status (string): 'ok' | 1578 status (string): 'ok' |
| 1601 props[attr_name] = [] | 1604 props[attr_name] = [] |
| 1602 else: | 1605 else: |
| 1603 props[attr_name] = None | 1606 props[attr_name] = None |
| 1604 | 1607 |
| 1605 try: | 1608 try: |
| 1606 self.raise_if_no_etag(class_name, item_id, input) | 1609 self.raise_if_no_etag(class_name, item_id, input_payload) |
| 1607 class_obj.set(item_id, **props) | 1610 class_obj.set(item_id, **props) |
| 1608 self.db.commit() | 1611 self.db.commit() |
| 1609 except (TypeError, IndexError, ValueError) as message: | 1612 except (TypeError, IndexError, ValueError) as message: |
| 1610 raise ValueError(message) | 1613 raise ValueError(message) |
| 1611 except KeyError as message: | 1614 except KeyError as message: |
| 1619 | 1622 |
| 1620 return 200, result | 1623 return 200, result |
| 1621 | 1624 |
| 1622 @Routing.route("/data/<:class_name>/<:item_id>", 'PATCH') | 1625 @Routing.route("/data/<:class_name>/<:item_id>", 'PATCH') |
| 1623 @_data_decorator | 1626 @_data_decorator |
| 1624 def patch_element(self, class_name, item_id, input): | 1627 def patch_element(self, class_name, item_id, input_payload): |
| 1625 """PATCH an object | 1628 """PATCH an object |
| 1626 | 1629 |
| 1627 Patch an element using 3 operators | 1630 Patch an element using 3 operators |
| 1628 ADD : Append new value to the object's attribute | 1631 ADD : Append new value to the object's attribute |
| 1629 REPLACE: Replace object's attribute | 1632 REPLACE: Replace object's attribute |
| 1630 REMOVE: Clear object's attribute | 1633 REMOVE: Clear object's attribute |
| 1631 | 1634 |
| 1632 Args: | 1635 Args: |
| 1633 class_name (string): class name of the resource (Ex: issue, msg) | 1636 class_name (string): class name of the resource (Ex: issue, msg) |
| 1634 item_id (string): id of the resource (Ex: 12, 15) | 1637 item_id (string): id of the resource (Ex: 12, 15) |
| 1635 input (list): the submitted form of the user | 1638 input_payload (list): the submitted form of the user |
| 1636 | 1639 |
| 1637 Returns: | 1640 Returns: |
| 1638 int: http status code 200 (OK) | 1641 int: http status code 200 (OK) |
| 1639 dict: a dictionary represents the modified object | 1642 dict: a dictionary represents the modified object |
| 1640 id: id of the object | 1643 id: id of the object |
| 1644 the object | 1647 the object |
| 1645 """ | 1648 """ |
| 1646 if class_name not in self.db.classes: | 1649 if class_name not in self.db.classes: |
| 1647 raise NotFound('Class %s not found' % class_name) | 1650 raise NotFound('Class %s not found' % class_name) |
| 1648 try: | 1651 try: |
| 1649 op = input['@op'].value.lower() | 1652 op = input_payload['@op'].value.lower() |
| 1650 except KeyError: | 1653 except KeyError: |
| 1651 op = self.__default_patch_op | 1654 op = self.__default_patch_op |
| 1652 class_obj = self.db.getclass(class_name) | 1655 class_obj = self.db.getclass(class_name) |
| 1653 | 1656 |
| 1654 self.raise_if_no_etag(class_name, item_id, input) | 1657 self.raise_if_no_etag(class_name, item_id, input_payload) |
| 1655 | 1658 |
| 1656 # if patch operation is action, call the action handler | 1659 # if patch operation is action, call the action handler |
| 1657 action_args = [class_name + item_id] | 1660 action_args = [class_name + item_id] |
| 1658 if op == 'action': | 1661 if op == 'action': |
| 1659 # extract action_name and action_args from form fields | 1662 # extract action_name and action_args from form fields |
| 1660 name = None | 1663 name = None |
| 1661 for form_field in input.value: | 1664 for form_field in input_payload.value: |
| 1662 key = form_field.name | 1665 key = form_field.name |
| 1663 value = form_field.value | 1666 value = form_field.value |
| 1664 if key == "@action_name": | 1667 if key == "@action_name": |
| 1665 name = value | 1668 name = value |
| 1666 elif key.startswith('@action_args'): | 1669 elif key.startswith('@action_args'): |
| 1682 'link': '%s/%s/%s' % (self.data_path, class_name, item_id), | 1685 'link': '%s/%s/%s' % (self.data_path, class_name, item_id), |
| 1683 'result': result | 1686 'result': result |
| 1684 } | 1687 } |
| 1685 else: | 1688 else: |
| 1686 # else patch operation is processing data | 1689 # else patch operation is processing data |
| 1687 props = self.props_from_args(class_obj, input.value, item_id, | 1690 props = self.props_from_args(class_obj, input_payload.value, item_id, |
| 1688 skip_protected=False) | 1691 skip_protected=False) |
| 1689 | 1692 |
| 1690 required_props = class_obj.get_required_props() | 1693 required_props = class_obj.get_required_props() |
| 1691 for prop in props: | 1694 for prop in props: |
| 1692 if not self.db.security.hasPermission( | 1695 if not self.db.security.hasPermission( |
| 1720 } | 1723 } |
| 1721 return 200, result | 1724 return 200, result |
| 1722 | 1725 |
| 1723 @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'PATCH') | 1726 @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'PATCH') |
| 1724 @_data_decorator | 1727 @_data_decorator |
| 1725 def patch_attribute(self, class_name, item_id, attr_name, input): | 1728 def patch_attribute(self, class_name, item_id, attr_name, input_payload): |
| 1726 """PATCH an attribute of an object | 1729 """PATCH an attribute of an object |
| 1727 | 1730 |
| 1728 Patch an element using 3 operators | 1731 Patch an element using 3 operators |
| 1729 ADD : Append new value to the attribute | 1732 ADD : Append new value to the attribute |
| 1730 REPLACE: Replace attribute | 1733 REPLACE: Replace attribute |
| 1732 | 1735 |
| 1733 Args: | 1736 Args: |
| 1734 class_name (string): class name of the resource (Ex: issue, msg) | 1737 class_name (string): class name of the resource (Ex: issue, msg) |
| 1735 item_id (string): id of the resource (Ex: 12, 15) | 1738 item_id (string): id of the resource (Ex: 12, 15) |
| 1736 attr_name (string): attribute of the resource (Ex: title, nosy) | 1739 attr_name (string): attribute of the resource (Ex: title, nosy) |
| 1737 input (list): the submitted form of the user | 1740 input_payload (list): the submitted form of the user |
| 1738 | 1741 |
| 1739 Returns: | 1742 Returns: |
| 1740 int: http status code 200 (OK) | 1743 int: http status code 200 (OK) |
| 1741 dict: a dictionary represents the modified object | 1744 dict: a dictionary represents the modified object |
| 1742 id: id of the object | 1745 id: id of the object |
| 1746 the object | 1749 the object |
| 1747 """ | 1750 """ |
| 1748 if class_name not in self.db.classes: | 1751 if class_name not in self.db.classes: |
| 1749 raise NotFound('Class %s not found' % class_name) | 1752 raise NotFound('Class %s not found' % class_name) |
| 1750 try: | 1753 try: |
| 1751 op = input['@op'].value.lower() | 1754 op = input_payload['@op'].value.lower() |
| 1752 except KeyError: | 1755 except KeyError: |
| 1753 op = self.__default_patch_op | 1756 op = self.__default_patch_op |
| 1754 | 1757 |
| 1755 if not self.db.security.hasPermission( | 1758 if not self.db.security.hasPermission( |
| 1756 'Edit', self.db.getuid(), class_name, attr_name, item_id | 1759 'Edit', self.db.getuid(), class_name, attr_name, item_id |
| 1765 if attr_name not in class_obj.getprops(protected=False): | 1768 if attr_name not in class_obj.getprops(protected=False): |
| 1766 if attr_name in class_obj.getprops(protected=True): | 1769 if attr_name in class_obj.getprops(protected=True): |
| 1767 raise AttributeError("Attribute '%s' can not be updated " | 1770 raise AttributeError("Attribute '%s' can not be updated " |
| 1768 "for class %s." % (attr_name, class_name)) | 1771 "for class %s." % (attr_name, class_name)) |
| 1769 | 1772 |
| 1770 self.raise_if_no_etag(class_name, item_id, input) | 1773 self.raise_if_no_etag(class_name, item_id, input_payload) |
| 1771 | 1774 |
| 1772 props = { | 1775 props = { |
| 1773 prop: self.prop_from_arg( | 1776 prop: self.prop_from_arg( |
| 1774 class_obj, prop, input['data'].value, item_id | 1777 class_obj, prop, input_payload['data'].value, item_id |
| 1775 ) | 1778 ) |
| 1776 } | 1779 } |
| 1777 | 1780 |
| 1778 props[prop] = self.patch_data( | 1781 props[prop] = self.patch_data( |
| 1779 op, class_obj.get(item_id, prop), props[prop] | 1782 op, class_obj.get(item_id, prop), props[prop] |
| 1797 } | 1800 } |
| 1798 return 200, result | 1801 return 200, result |
| 1799 | 1802 |
| 1800 @Routing.route("/data/<:class_name>", 'OPTIONS') | 1803 @Routing.route("/data/<:class_name>", 'OPTIONS') |
| 1801 @_data_decorator | 1804 @_data_decorator |
| 1802 def options_collection(self, class_name, input): | 1805 def options_collection(self, class_name, input_payload): |
| 1803 """OPTION return the HTTP Header for the class uri | 1806 """OPTION return the HTTP Header for the class uri |
| 1804 | 1807 |
| 1805 Returns: | 1808 Returns: |
| 1806 int: http status code 204 (No content) | 1809 int: http status code 204 (No content) |
| 1807 body (string): an empty string | 1810 body (string): an empty string |
| 1819 ) | 1822 ) |
| 1820 return 204, "" | 1823 return 204, "" |
| 1821 | 1824 |
| 1822 @Routing.route("/data/<:class_name>/<:item_id>", 'OPTIONS') | 1825 @Routing.route("/data/<:class_name>/<:item_id>", 'OPTIONS') |
| 1823 @_data_decorator | 1826 @_data_decorator |
| 1824 def options_element(self, class_name, item_id, input): | 1827 def options_element(self, class_name, item_id, input_payload): |
| 1825 """OPTION return the HTTP Header for the object uri | 1828 """OPTION return the HTTP Header for the object uri |
| 1826 | 1829 |
| 1827 Returns: | 1830 Returns: |
| 1828 int: http status code 204 (No content) | 1831 int: http status code 204 (No content) |
| 1829 body (string): an empty string | 1832 body (string): an empty string |
| 1844 ) | 1847 ) |
| 1845 return 204, "" | 1848 return 204, "" |
| 1846 | 1849 |
| 1847 @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'OPTIONS') | 1850 @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'OPTIONS') |
| 1848 @_data_decorator | 1851 @_data_decorator |
| 1849 def option_attribute(self, class_name, item_id, attr_name, input): | 1852 def option_attribute(self, class_name, item_id, attr_name, input_payload): |
| 1850 """OPTION return the HTTP Header for the attribute uri | 1853 """OPTION return the HTTP Header for the attribute uri |
| 1851 | 1854 |
| 1852 Returns: | 1855 Returns: |
| 1853 int: http status code 204 (No content) | 1856 int: http status code 204 (No content) |
| 1854 body (string): an empty string | 1857 body (string): an empty string |
| 1960 } | 1963 } |
| 1961 } | 1964 } |
| 1962 ) | 1965 ) |
| 1963 @Routing.route("/") | 1966 @Routing.route("/") |
| 1964 @_data_decorator | 1967 @_data_decorator |
| 1965 def describe(self, input): | 1968 def describe(self, input_payload): |
| 1966 """Describe the rest endpoint. Return direct children in | 1969 """Describe the rest endpoint. Return direct children in |
| 1967 links list. | 1970 links list. |
| 1968 """ | 1971 """ |
| 1969 | 1972 |
| 1970 # paths looks like ['^rest/$', '^rest/summary$', | 1973 # paths looks like ['^rest/$', '^rest/summary$', |
| 1997 | 2000 |
| 1998 return 200, result | 2001 return 200, result |
| 1999 | 2002 |
| 2000 @Routing.route("/", 'OPTIONS') | 2003 @Routing.route("/", 'OPTIONS') |
| 2001 @_data_decorator | 2004 @_data_decorator |
| 2002 def options_describe(self, input): | 2005 def options_describe(self, input_payload): |
| 2003 """OPTION return the HTTP Header for the root | 2006 """OPTION return the HTTP Header for the root |
| 2004 | 2007 |
| 2005 Returns: | 2008 Returns: |
| 2006 int: http status code 204 (No content) | 2009 int: http status code 204 (No content) |
| 2007 body (string): an empty string | 2010 body (string): an empty string |
| 2016 ) | 2019 ) |
| 2017 return 204, "" | 2020 return 204, "" |
| 2018 | 2021 |
| 2019 @Routing.route("/data") | 2022 @Routing.route("/data") |
| 2020 @_data_decorator | 2023 @_data_decorator |
| 2021 def data(self, input): | 2024 def data(self, input_payload): |
| 2022 """Describe the subelements of data | 2025 """Describe the subelements of data |
| 2023 | 2026 |
| 2024 One entry for each class the user may view | 2027 One entry for each class the user may view |
| 2025 """ | 2028 """ |
| 2026 result = {} | 2029 result = {} |
| 2030 result[cls] = dict(link=self.base_path + '/data/' + cls) | 2033 result[cls] = dict(link=self.base_path + '/data/' + cls) |
| 2031 return 200, result | 2034 return 200, result |
| 2032 | 2035 |
| 2033 @Routing.route("/data", 'OPTIONS') | 2036 @Routing.route("/data", 'OPTIONS') |
| 2034 @_data_decorator | 2037 @_data_decorator |
| 2035 def options_data(self, input): | 2038 def options_data(self, input_payload): |
| 2036 """OPTION return the HTTP Header for the /data element | 2039 """OPTION return the HTTP Header for the /data element |
| 2037 | 2040 |
| 2038 Returns: | 2041 Returns: |
| 2039 int: http status code 204 (No content) | 2042 int: http status code 204 (No content) |
| 2040 body (string): an empty string | 2043 body (string): an empty string |
| 2049 ) | 2052 ) |
| 2050 return 204, "" | 2053 return 204, "" |
| 2051 | 2054 |
| 2052 @Routing.route("/summary") | 2055 @Routing.route("/summary") |
| 2053 @_data_decorator | 2056 @_data_decorator |
| 2054 def summary(self, input): | 2057 def summary(self, input_payload): |
| 2055 """Get a summary of resource from class URI. | 2058 """Get a summary of resource from class URI. |
| 2056 | 2059 |
| 2057 This function returns only items have View permission | 2060 This function returns only items have View permission |
| 2058 class_name should be valid already | 2061 class_name should be valid already |
| 2059 | 2062 |
| 2060 Args: | 2063 Args: |
| 2061 class_name (string): class name of the resource (Ex: issue, msg) | 2064 class_name (string): class name of the resource (Ex: issue, msg) |
| 2062 input (list): the submitted form of the user | 2065 input_payload (list): the submitted form of the user |
| 2063 | 2066 |
| 2064 Returns: | 2067 Returns: |
| 2065 int: http status code 200 (OK) | 2068 int: http status code 200 (OK) |
| 2066 list: | 2069 list: |
| 2067 """ | 2070 """ |
| 2401 "Acceptable mime types are: */*, %s") % | 2404 "Acceptable mime types are: */*, %s") % |
| 2402 (self.client.request.headers.get('Accept'), | 2405 (self.client.request.headers.get('Accept'), |
| 2403 ", ".join(sorted( | 2406 ", ".join(sorted( |
| 2404 self.__accepted_content_type.keys()))))) | 2407 self.__accepted_content_type.keys()))))) |
| 2405 | 2408 |
| 2406 def dispatch(self, method, uri, input): | 2409 def dispatch(self, method, uri, input_payload): |
| 2407 """format and process the request""" | 2410 """format and process the request""" |
| 2408 output = None | 2411 output = None |
| 2409 | 2412 |
| 2410 # Before we do anything has the user hit the rate limit. | 2413 # Before we do anything has the user hit the rate limit. |
| 2411 | 2414 |
| 2523 self.client.setHeader( | 2526 self.client.setHeader( |
| 2524 "Allow", | 2527 "Allow", |
| 2525 "OPTIONS, GET, POST, PUT, DELETE, PATCH" | 2528 "OPTIONS, GET, POST, PUT, DELETE, PATCH" |
| 2526 ) | 2529 ) |
| 2527 | 2530 |
| 2528 # Is there an input.value with format json data? | 2531 # Is there an input_payload.value with format json data? |
| 2529 # If so turn it into an object that emulates enough | 2532 # If so turn it into an object that emulates enough |
| 2530 # of the FieldStorge methods/props to allow a response. | 2533 # of the FieldStorge methods/props to allow a response. |
| 2531 content_type_header = headers.get('Content-Type', None) | 2534 content_type_header = headers.get('Content-Type', None) |
| 2532 # python2 is str type, python3 is bytes | 2535 # python2 is str type, python3 is bytes |
| 2533 if type(input.value) in (str, bytes) and content_type_header: | 2536 if type(input_payload.value) in (str, bytes) and content_type_header: |
| 2534 # the structure of a content-type header | 2537 # the structure of a content-type header |
| 2535 # is complex: mime-type; options(charset ...) | 2538 # is complex: mime-type; options(charset ...) |
| 2536 # for now we just accept application/json. | 2539 # for now we just accept application/json. |
| 2537 # FIXME there should be a function: | 2540 # FIXME there should be a function: |
| 2538 # parse_content_type_header(content_type_header) | 2541 # parse_content_type_header(content_type_header) |
| 2542 # That way we could handle stuff like: | 2545 # That way we could handle stuff like: |
| 2543 # application/vnd.roundup-foo+json; charset=UTF8 | 2546 # application/vnd.roundup-foo+json; charset=UTF8 |
| 2544 # for example. | 2547 # for example. |
| 2545 if content_type_header.lower() == "application/json": | 2548 if content_type_header.lower() == "application/json": |
| 2546 try: | 2549 try: |
| 2547 input = SimulateFieldStorageFromJson(b2s(input.value)) | 2550 input_payload = SimulateFieldStorageFromJson(b2s(input_payload.value)) |
| 2548 except ValueError as msg: | 2551 except ValueError as msg: |
| 2549 output = self.error_obj(400, msg) | 2552 output = self.error_obj(400, msg) |
| 2550 else: | 2553 else: |
| 2551 if method.upper() == "PATCH": | 2554 if method.upper() == "PATCH": |
| 2552 self.client.setHeader("Accept-Patch", | 2555 self.client.setHeader("Accept-Patch", |
| 2556 "Unable to process input of type %s" % | 2559 "Unable to process input of type %s" % |
| 2557 content_type_header) | 2560 content_type_header) |
| 2558 | 2561 |
| 2559 # check for pretty print | 2562 # check for pretty print |
| 2560 try: | 2563 try: |
| 2561 pretty_output = not input['@pretty'].value.lower() == "false" | 2564 pretty_output = not input_payload['@pretty'].value.lower() == "false" |
| 2562 # Can also return a TypeError ("not indexable") | 2565 # Can also return a TypeError ("not indexable") |
| 2563 # In case the FieldStorage could not parse the result | 2566 # In case the FieldStorage could not parse the result |
| 2564 except (KeyError, TypeError): | 2567 except (KeyError, TypeError): |
| 2565 pretty_output = True | 2568 pretty_output = True |
| 2566 | 2569 |
| 2567 # check for runtime statistics | 2570 # check for runtime statistics |
| 2568 try: | 2571 try: |
| 2569 # self.report_stats initialized to False | 2572 # self.report_stats initialized to False |
| 2570 self.report_stats = input['@stats'].value.lower() == "true" | 2573 self.report_stats = input_payload['@stats'].value.lower() == "true" |
| 2571 # Can also return a TypeError ("not indexable") | 2574 # Can also return a TypeError ("not indexable") |
| 2572 # In case the FieldStorage could not parse the result | 2575 # In case the FieldStorage could not parse the result |
| 2573 except (KeyError, TypeError): | 2576 except (KeyError, TypeError): |
| 2574 pass | 2577 pass |
| 2575 | 2578 |
| 2580 try: | 2583 try: |
| 2581 # FIXME: the version priority here is different | 2584 # FIXME: the version priority here is different |
| 2582 # from accept header. accept mime type in url | 2585 # from accept header. accept mime type in url |
| 2583 # takes priority over Accept header. Opposite here. | 2586 # takes priority over Accept header. Opposite here. |
| 2584 if not self.api_version: | 2587 if not self.api_version: |
| 2585 self.api_version = int(input['@apiver'].value) | 2588 self.api_version = int(input_payload['@apiver'].value) |
| 2586 # Can also return a TypeError ("not indexable") | 2589 # Can also return a TypeError ("not indexable") |
| 2587 # In case the FieldStorage could not parse the result | 2590 # In case the FieldStorage could not parse the result |
| 2588 except (KeyError, TypeError): | 2591 except (KeyError, TypeError): |
| 2589 self.api_version = None | 2592 self.api_version = None |
| 2590 except ValueError: | 2593 except ValueError: |
| 2591 output = self.error_obj(406, msg % input['@apiver'].value) | 2594 output = self.error_obj(406, msg % input_payload['@apiver'].value) |
| 2592 | 2595 |
| 2593 # by this time the API version is set. Error if we don't | 2596 # by this time the API version is set. Error if we don't |
| 2594 # support it? | 2597 # support it? |
| 2595 if self.api_version is None: | 2598 if self.api_version is None: |
| 2596 # FIXME: do we need to raise an error if client did not specify | 2599 # FIXME: do we need to raise an error if client did not specify |
| 2600 self.api_version = self.__default_api_version | 2603 self.api_version = self.__default_api_version |
| 2601 elif self.api_version not in self.__supported_api_versions: | 2604 elif self.api_version not in self.__supported_api_versions: |
| 2602 output = self.error_obj(406, msg % self.api_version) | 2605 output = self.error_obj(406, msg % self.api_version) |
| 2603 | 2606 |
| 2604 # sadly del doesn't work on FieldStorage which can be the type of | 2607 # sadly del doesn't work on FieldStorage which can be the type of |
| 2605 # input. So we have to ignore keys starting with @ at other | 2608 # input_payload. So we have to ignore keys starting with @ at other |
| 2606 # places in the code. | 2609 # places in the code. |
| 2607 # else: | 2610 # else: |
| 2608 # del(input['@apiver']) | 2611 # del(input_payload['@apiver']) |
| 2609 | 2612 |
| 2610 # Call the appropriate method | 2613 # Call the appropriate method |
| 2611 try: | 2614 try: |
| 2612 # If output was defined by a prior error | 2615 # If output was defined by a prior error |
| 2613 # condition skip call | 2616 # condition skip call |
| 2614 if not output: | 2617 if not output: |
| 2615 output = Routing.execute(self, uri, method, input) | 2618 output = Routing.execute(self, uri, method, input_payload) |
| 2616 except NotFound as msg: | 2619 except NotFound as msg: |
| 2617 output = self.error_obj(404, msg) | 2620 output = self.error_obj(404, msg) |
| 2618 except Reject as msg: | 2621 except Reject as msg: |
| 2619 output = self.error_obj(405, msg.args[0]) | 2622 output = self.error_obj(405, msg.args[0]) |
| 2620 self.client.setHeader("Allow", msg.args[1]) | 2623 self.client.setHeader("Allow", msg.args[1]) |
