Mercurial > p > roundup > code
comparison roundup/rest.py @ 5582:519b7fd8c8c3 REST-rebased
Added docstring
committer: Ralf Schlatterbeck <rsc@runtux.com>
| author | Chau Nguyen <dangchau1991@yahoo.com> |
|---|---|
| date | Wed, 30 Jan 2019 10:26:35 +0100 |
| parents | 30793a435185 |
| children | c65d98a16780 |
comparison
equal
deleted
inserted
replaced
| 5581:30793a435185 | 5582:519b7fd8c8c3 |
|---|---|
| 16 from roundup.exceptions import * | 16 from roundup.exceptions import * |
| 17 from roundup import xmlrpc | 17 from roundup import xmlrpc |
| 18 | 18 |
| 19 | 19 |
| 20 class RestfulInstance(object): | 20 class RestfulInstance(object): |
| 21 """Dummy Handler for REST | 21 """The RestfulInstance performs REST request from the client""" |
| 22 """ | |
| 23 | 22 |
| 24 def __init__(self, client, db): | 23 def __init__(self, client, db): |
| 25 self.client = client # it might be unnecessary to receive the client | 24 self.client = client # it might be unnecessary to receive the client |
| 26 self.db = db | 25 self.db = db |
| 27 | 26 |
| 29 host = self.client.env['HTTP_HOST'] | 28 host = self.client.env['HTTP_HOST'] |
| 30 tracker = self.client.env['TRACKER_NAME'] | 29 tracker = self.client.env['TRACKER_NAME'] |
| 31 self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker) | 30 self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker) |
| 32 | 31 |
| 33 def props_from_args(self, cl, args, itemid=None): | 32 def props_from_args(self, cl, args, itemid=None): |
| 33 """Construct a list of properties from the given arguments, | |
| 34 and return them after validation. | |
| 35 | |
| 36 Args: | |
| 37 cl (string): class object of the resource | |
| 38 args (list): the submitted form of the user | |
| 39 itemid (string, optional): itemid of the object | |
| 40 | |
| 41 Returns: | |
| 42 dict: dictionary of validated properties | |
| 43 | |
| 44 """ | |
| 34 class_props = cl.properties.keys() | 45 class_props = cl.properties.keys() |
| 35 props = {} | 46 props = {} |
| 36 # props = dict.fromkeys(class_props, None) | 47 # props = dict.fromkeys(class_props, None) |
| 37 | 48 |
| 38 for arg in args: | 49 for arg in args: |
| 61 | 72 |
| 62 return props | 73 return props |
| 63 | 74 |
| 64 @staticmethod | 75 @staticmethod |
| 65 def error_obj(status, msg, source=None): | 76 def error_obj(status, msg, source=None): |
| 77 """Wrap the error data into an object. This function is temporally and | |
| 78 will be changed to a decorator later.""" | |
| 66 result = { | 79 result = { |
| 67 'error': { | 80 'error': { |
| 68 'status': status, | 81 'status': status, |
| 69 'msg': msg | 82 'msg': msg |
| 70 } | 83 } |
| 74 | 87 |
| 75 return result | 88 return result |
| 76 | 89 |
| 77 @staticmethod | 90 @staticmethod |
| 78 def data_obj(data): | 91 def data_obj(data): |
| 92 """Wrap the returned data into an object. This function is temporally | |
| 93 and will be changed to a decorator later.""" | |
| 79 result = { | 94 result = { |
| 80 'data': data | 95 'data': data |
| 81 } | 96 } |
| 82 return result | 97 return result |
| 83 | 98 |
| 84 def get_collection(self, class_name, input): | 99 def get_collection(self, class_name, input): |
| 100 """GET resource from class URI. | |
| 101 | |
| 102 This function returns only items have View permission | |
| 103 class_name should be valid already | |
| 104 | |
| 105 Args: | |
| 106 class_name (string): class name of the resource (Ex: issue, msg) | |
| 107 input (list): the submitted form of the user | |
| 108 | |
| 109 Returns: | |
| 110 int: http status code 200 (OK) | |
| 111 list: list of reference item in the class | |
| 112 id: id of the object | |
| 113 link: path to the object | |
| 114 """ | |
| 85 if not self.db.security.hasPermission( | 115 if not self.db.security.hasPermission( |
| 86 'View', self.db.getuid(), class_name | 116 'View', self.db.getuid(), class_name |
| 87 ): | 117 ): |
| 88 raise Unauthorised('Permission to view %s denied' % class_name) | 118 raise Unauthorised('Permission to view %s denied' % class_name) |
| 89 | 119 |
| 98 ] | 128 ] |
| 99 self.client.setHeader("X-Count-Total", str(len(result))) | 129 self.client.setHeader("X-Count-Total", str(len(result))) |
| 100 return 200, result | 130 return 200, result |
| 101 | 131 |
| 102 def get_element(self, class_name, item_id, input): | 132 def get_element(self, class_name, item_id, input): |
| 133 """GET resource from object URI. | |
| 134 | |
| 135 This function returns only properties have View permission | |
| 136 class_name and item_id should be valid already | |
| 137 | |
| 138 Args: | |
| 139 class_name (string): class name of the resource (Ex: issue, msg) | |
| 140 item_id (string): id of the resource (Ex: 12, 15) | |
| 141 input (list): the submitted form of the user | |
| 142 | |
| 143 Returns: | |
| 144 int: http status code 200 (OK) | |
| 145 dict: a dictionary represents the object | |
| 146 id: id of the object | |
| 147 type: class name of the object | |
| 148 link: link to the object | |
| 149 attributes: a dictionary represent the attributes of the object | |
| 150 """ | |
| 103 if not self.db.security.hasPermission( | 151 if not self.db.security.hasPermission( |
| 104 'View', self.db.getuid(), class_name, itemid=item_id | 152 'View', self.db.getuid(), class_name, itemid=item_id |
| 105 ): | 153 ): |
| 106 raise Unauthorised( | 154 raise Unauthorised( |
| 107 'Permission to view %s item %d denied' % (class_name, item_id) | 155 'Permission to view %s item %d denied' % (class_name, item_id) |
| 125 } | 173 } |
| 126 | 174 |
| 127 return 200, result | 175 return 200, result |
| 128 | 176 |
| 129 def post_collection(self, class_name, input): | 177 def post_collection(self, class_name, input): |
| 178 """POST a new object to a class | |
| 179 | |
| 180 If the item is successfully created, the "Location" header will also | |
| 181 contain the link to the created object | |
| 182 | |
| 183 Args: | |
| 184 class_name (string): class name of the resource (Ex: issue, msg) | |
| 185 input (list): the submitted form of the user | |
| 186 | |
| 187 Returns: | |
| 188 int: http status code 201 (Created) | |
| 189 dict: a reference item to the created object | |
| 190 id: id of the object | |
| 191 link: path to the object | |
| 192 """ | |
| 130 if not self.db.security.hasPermission( | 193 if not self.db.security.hasPermission( |
| 131 'Create', self.db.getuid(), class_name | 194 'Create', self.db.getuid(), class_name |
| 132 ): | 195 ): |
| 133 raise Unauthorised('Permission to create %s denied' % class_name) | 196 raise Unauthorised('Permission to create %s denied' % class_name) |
| 134 | 197 |
| 169 'link': link | 232 'link': link |
| 170 } | 233 } |
| 171 return 201, result | 234 return 201, result |
| 172 | 235 |
| 173 def post_element(self, class_name, item_id, input): | 236 def post_element(self, class_name, item_id, input): |
| 237 """POST to an object of a class is not allowed""" | |
| 174 raise Reject('POST to an item is not allowed') | 238 raise Reject('POST to an item is not allowed') |
| 175 | 239 |
| 176 def put_collection(self, class_name, input): | 240 def put_collection(self, class_name, input): |
| 241 """PUT a class is not allowed""" | |
| 177 raise Reject('PUT a class is not allowed') | 242 raise Reject('PUT a class is not allowed') |
| 178 | 243 |
| 179 def put_element(self, class_name, item_id, input): | 244 def put_element(self, class_name, item_id, input): |
| 245 """PUT a new content to an object | |
| 246 | |
| 247 Replace the content of the existing object | |
| 248 | |
| 249 Args: | |
| 250 class_name (string): class name of the resource (Ex: issue, msg) | |
| 251 item_id (string): id of the resource (Ex: 12, 15) | |
| 252 input (list): the submitted form of the user | |
| 253 | |
| 254 Returns: | |
| 255 int: http status code 200 (OK) | |
| 256 dict: a dictionary represents the modified object | |
| 257 id: id of the object | |
| 258 type: class name of the object | |
| 259 link: link to the object | |
| 260 attributes: a dictionary represent only changed attributes of | |
| 261 the object | |
| 262 """ | |
| 180 class_obj = self.db.getclass(class_name) | 263 class_obj = self.db.getclass(class_name) |
| 181 | 264 |
| 182 props = self.props_from_args(class_obj, input.value, item_id) | 265 props = self.props_from_args(class_obj, input.value, item_id) |
| 183 for p in props.iterkeys(): | 266 for p in props.iterkeys(): |
| 184 if not self.db.security.hasPermission( | 267 if not self.db.security.hasPermission( |
| 201 'attribute': result | 284 'attribute': result |
| 202 } | 285 } |
| 203 return 200, result | 286 return 200, result |
| 204 | 287 |
| 205 def delete_collection(self, class_name, input): | 288 def delete_collection(self, class_name, input): |
| 289 """DELETE all objects in a class | |
| 290 | |
| 291 Args: | |
| 292 class_name (string): class name of the resource (Ex: issue, msg) | |
| 293 input (list): the submitted form of the user | |
| 294 | |
| 295 Returns: | |
| 296 int: http status code 200 (OK) | |
| 297 dict: | |
| 298 status (string): 'ok' | |
| 299 count (int): number of deleted objects | |
| 300 """ | |
| 206 if not self.db.security.hasPermission( | 301 if not self.db.security.hasPermission( |
| 207 'Delete', self.db.getuid(), class_name | 302 'Delete', self.db.getuid(), class_name |
| 208 ): | 303 ): |
| 209 raise Unauthorised('Permission to delete %s denied' % class_name) | 304 raise Unauthorised('Permission to delete %s denied' % class_name) |
| 210 | 305 |
| 228 } | 323 } |
| 229 | 324 |
| 230 return 200, result | 325 return 200, result |
| 231 | 326 |
| 232 def delete_element(self, class_name, item_id, input): | 327 def delete_element(self, class_name, item_id, input): |
| 328 """DELETE an object in a class | |
| 329 | |
| 330 Args: | |
| 331 class_name (string): class name of the resource (Ex: issue, msg) | |
| 332 item_id (string): id of the resource (Ex: 12, 15) | |
| 333 input (list): the submitted form of the user | |
| 334 | |
| 335 Returns: | |
| 336 int: http status code 200 (OK) | |
| 337 dict: | |
| 338 status (string): 'ok' | |
| 339 """ | |
| 233 if not self.db.security.hasPermission( | 340 if not self.db.security.hasPermission( |
| 234 'Delete', self.db.getuid(), class_name, itemid=item_id | 341 'Delete', self.db.getuid(), class_name, itemid=item_id |
| 235 ): | 342 ): |
| 236 raise Unauthorised( | 343 raise Unauthorised( |
| 237 'Permission to delete %s %s denied' % (class_name, item_id) | 344 'Permission to delete %s %s denied' % (class_name, item_id) |
| 244 } | 351 } |
| 245 | 352 |
| 246 return 200, result | 353 return 200, result |
| 247 | 354 |
| 248 def patch_collection(self, class_name, input): | 355 def patch_collection(self, class_name, input): |
| 356 """PATCH a class is not allowed""" | |
| 249 raise Reject('PATCH a class is not allowed') | 357 raise Reject('PATCH a class is not allowed') |
| 250 | 358 |
| 251 def patch_element(self, class_name, item_id, input): | 359 def patch_element(self, class_name, item_id, input): |
| 252 try: | 360 try: |
| 253 op = input['op'].value.lower() | 361 op = input['op'].value.lower() |
| 288 'attribute': result | 396 'attribute': result |
| 289 } | 397 } |
| 290 return 200, result | 398 return 200, result |
| 291 | 399 |
| 292 def options_collection(self, class_name, input): | 400 def options_collection(self, class_name, input): |
| 401 """OPTION return the HTTP Header for the class uri | |
| 402 | |
| 403 Returns: | |
| 404 int: http status code 204 (No content) | |
| 405 body (string): an empty string | |
| 406 """ | |
| 293 return 204, "" | 407 return 204, "" |
| 294 | 408 |
| 295 def options_element(self, class_name, item_id, input): | 409 def options_element(self, class_name, item_id, input): |
| 410 """OPTION return the HTTP Header for the object uri | |
| 411 | |
| 412 Returns: | |
| 413 int: http status code 204 (No content) | |
| 414 body (string): an empty string | |
| 415 """ | |
| 296 self.client.setHeader( | 416 self.client.setHeader( |
| 297 "Accept-Patch", | 417 "Accept-Patch", |
| 298 "application/x-www-form-urlencoded, " | 418 "application/x-www-form-urlencoded, " |
| 299 "multipart/form-data" | 419 "multipart/form-data" |
| 300 ) | 420 ) |
| 301 return 204, "" | 421 return 204, "" |
| 302 | 422 |
| 303 def dispatch(self, method, uri, input): | 423 def dispatch(self, method, uri, input): |
| 424 """format and process the request""" | |
| 304 # PATH is split to multiple pieces | 425 # PATH is split to multiple pieces |
| 305 # 0 - rest | 426 # 0 - rest |
| 306 # 1 - resource | 427 # 1 - resource |
| 307 # 2 - attribute | 428 # 2 - attribute |
| 308 resource_uri = uri.split("/")[1] | 429 resource_uri = uri.split("/")[1] |
| 325 try: | 446 try: |
| 326 pretty_output = input['pretty'].value.lower() == "true" | 447 pretty_output = input['pretty'].value.lower() == "true" |
| 327 except KeyError: | 448 except KeyError: |
| 328 pretty_output = False | 449 pretty_output = False |
| 329 | 450 |
| 451 # add access-control-allow-* to support CORS | |
| 330 self.client.setHeader("Access-Control-Allow-Origin", "*") | 452 self.client.setHeader("Access-Control-Allow-Origin", "*") |
| 331 self.client.setHeader( | 453 self.client.setHeader( |
| 332 "Access-Control-Allow-Headers", | 454 "Access-Control-Allow-Headers", |
| 333 "Content-Type, Authorization, X-HTTP-Method-Override" | 455 "Content-Type, Authorization, X-HTTP-Method-Override" |
| 334 ) | 456 ) |
| 335 | 457 if resource_uri in self.db.classes: |
| 458 self.client.setHeader( | |
| 459 "Allow", | |
| 460 "HEAD, OPTIONS, GET, POST, DELETE" | |
| 461 ) | |
| 462 self.client.setHeader( | |
| 463 "Access-Control-Allow-Methods", | |
| 464 "HEAD, OPTIONS, GET, POST, DELETE" | |
| 465 ) | |
| 466 else: | |
| 467 self.client.setHeader( | |
| 468 "Allow", | |
| 469 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH" | |
| 470 ) | |
| 471 self.client.setHeader( | |
| 472 "Access-Control-Allow-Methods", | |
| 473 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH" | |
| 474 ) | |
| 475 | |
| 476 # Call the appropriate method | |
| 336 output = None | 477 output = None |
| 337 try: | 478 try: |
| 338 if resource_uri in self.db.classes: | 479 if resource_uri in self.db.classes: |
| 339 self.client.setHeader( | |
| 340 "Allow", | |
| 341 "HEAD, OPTIONS, GET, POST, DELETE" | |
| 342 ) | |
| 343 self.client.setHeader( | |
| 344 "Access-Control-Allow-Methods", | |
| 345 "HEAD, OPTIONS, GET, POST, DELETE" | |
| 346 ) | |
| 347 response_code, output = getattr( | 480 response_code, output = getattr( |
| 348 self, "%s_collection" % method.lower() | 481 self, "%s_collection" % method.lower() |
| 349 )(resource_uri, input) | 482 )(resource_uri, input) |
| 350 else: | 483 else: |
| 351 class_name, item_id = hyperdb.splitDesignator(resource_uri) | 484 class_name, item_id = hyperdb.splitDesignator(resource_uri) |
| 352 self.client.setHeader( | |
| 353 "Allow", | |
| 354 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH" | |
| 355 ) | |
| 356 self.client.setHeader( | |
| 357 "Access-Control-Allow-Methods", | |
| 358 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH" | |
| 359 ) | |
| 360 response_code, output = getattr( | 485 response_code, output = getattr( |
| 361 self, "%s_element" % method.lower() | 486 self, "%s_element" % method.lower() |
| 362 )(class_name, item_id, input) | 487 )(class_name, item_id, input) |
| 363 | |
| 364 output = RestfulInstance.data_obj(output) | 488 output = RestfulInstance.data_obj(output) |
| 365 self.client.response_code = response_code | 489 self.client.response_code = response_code |
| 366 except IndexError, msg: | 490 except IndexError, msg: |
| 367 output = RestfulInstance.error_obj(404, msg) | 491 output = RestfulInstance.error_obj(404, msg) |
| 368 self.client.response_code = 404 | 492 self.client.response_code = 404 |
| 406 | 530 |
| 407 return output | 531 return output |
| 408 | 532 |
| 409 | 533 |
| 410 class RoundupJSONEncoder(json.JSONEncoder): | 534 class RoundupJSONEncoder(json.JSONEncoder): |
| 535 """RoundupJSONEncoder overrides the default JSONEncoder to handle all | |
| 536 types of the object without returning any error""" | |
| 411 def default(self, obj): | 537 def default(self, obj): |
| 412 try: | 538 try: |
| 413 result = json.JSONEncoder.default(self, obj) | 539 result = json.JSONEncoder.default(self, obj) |
| 414 except TypeError: | 540 except TypeError: |
| 415 result = str(obj) | 541 result = str(obj) |
