comparison roundup/rest.py @ 5584:53098db851f2 REST-rebased

Added attribute URI handling committer: Ralf Schlatterbeck <rsc@runtux.com>
author Chau Nguyen <dangchau1991@yahoo.com>
date Wed, 30 Jan 2019 10:26:35 +0100
parents c65d98a16780
children cb2b320fde16
comparison
equal deleted inserted replaced
5583:c65d98a16780 5584:53098db851f2
49 for arg in args: 49 for arg in args:
50 key = arg.name 50 key = arg.name
51 value = arg.value 51 value = arg.value
52 if key not in class_props: 52 if key not in class_props:
53 continue 53 continue
54 if isinstance(key, unicode): 54 props[key] = self.prop_from_arg(cl, key, value, itemid)
55 try:
56 key = key.encode('ascii')
57 except UnicodeEncodeError:
58 raise UsageError(
59 'argument %r is no valid ascii keyword' % key
60 )
61 if isinstance(value, unicode):
62 value = value.encode('utf-8')
63 if value:
64 try:
65 props[key] = hyperdb.rawToHyperdb(
66 self.db, cl, itemid, key, value
67 )
68 except hyperdb.HyperdbValueError, msg:
69 raise UsageError(msg)
70 else:
71 props[key] = None
72 55
73 return props 56 return props
57
58 def prop_from_arg(self, cl, key, value, itemid=None):
59 """Construct a property from the given argument,
60 and return them after validation.
61
62 Args:
63 cl (string): class object of the resource
64 key (string): attribute key
65 value (string): attribute value
66 itemid (string, optional): itemid of the object
67
68 Returns:
69 value: value of validated properties
70
71 """
72 prop = None
73 if isinstance(key, unicode):
74 try:
75 key = key.encode('ascii')
76 except UnicodeEncodeError:
77 raise UsageError(
78 'argument %r is no valid ascii keyword' % key
79 )
80 if isinstance(value, unicode):
81 value = value.encode('utf-8')
82 if value:
83 try:
84 prop = hyperdb.rawToHyperdb(
85 self.db, cl, itemid, key, value
86 )
87 except hyperdb.HyperdbValueError, msg:
88 raise UsageError(msg)
89
90 return prop
74 91
75 @staticmethod 92 @staticmethod
76 def error_obj(status, msg, source=None): 93 def error_obj(status, msg, source=None):
77 """Wrap the error data into an object. This function is temporally and 94 """Wrap the error data into an object. This function is temporally and
78 will be changed to a decorator later.""" 95 will be changed to a decorator later."""
150 """ 167 """
151 if not self.db.security.hasPermission( 168 if not self.db.security.hasPermission(
152 'View', self.db.getuid(), class_name, itemid=item_id 169 'View', self.db.getuid(), class_name, itemid=item_id
153 ): 170 ):
154 raise Unauthorised( 171 raise Unauthorised(
155 'Permission to view %s item %s denied' % (class_name, item_id) 172 'Permission to view %s%s denied' % (class_name, item_id)
156 ) 173 )
157 174
158 class_obj = self.db.getclass(class_name) 175 class_obj = self.db.getclass(class_name)
159 props = class_obj.properties.keys() 176 props = class_obj.properties.keys()
160 props.sort() # sort properties 177 props.sort() # sort properties
172 'attributes': dict(result) 189 'attributes': dict(result)
173 } 190 }
174 191
175 return 200, result 192 return 200, result
176 193
194 def get_attribute(self, class_name, item_id, attr_name, input):
195 """GET resource from attribute URI.
196
197 This function returns only attribute has View permission
198 class_name should be valid already
199
200 Args:
201 class_name (string): class name of the resource (Ex: issue, msg)
202 item_id (string): id of the resource (Ex: 12, 15)
203 attr_name (string): attribute of the resource (Ex: title, nosy)
204 input (list): the submitted form of the user
205
206 Returns:
207 int: http status code 200 (OK)
208 list: a dictionary represents the attribute
209 id: id of the object
210 type: class name of the attribute
211 link: link to the attribute
212 data: data of the requested attribute
213 """
214 if not self.db.security.hasPermission(
215 'View', self.db.getuid(), class_name, attr_name, item_id
216 ):
217 raise Unauthorised(
218 'Permission to view %s%s %s denied' %
219 (class_name, item_id, attr_name)
220 )
221
222 class_obj = self.db.getclass(class_name)
223 data = class_obj.get(item_id, attr_name)
224 result = {
225 'id': item_id,
226 'type': type(data),
227 'link': "%s%s%s/%s" %
228 (self.base_path, class_name, item_id, attr_name),
229 'data': data
230 }
231
232 return 200, result
233
177 def post_collection(self, class_name, input): 234 def post_collection(self, class_name, input):
178 """POST a new object to a class 235 """POST a new object to a class
179 236
180 If the item is successfully created, the "Location" header will also 237 If the item is successfully created, the "Location" header will also
181 contain the link to the created object 238 contain the link to the created object
234 return 201, result 291 return 201, result
235 292
236 def post_element(self, class_name, item_id, input): 293 def post_element(self, class_name, item_id, input):
237 """POST to an object of a class is not allowed""" 294 """POST to an object of a class is not allowed"""
238 raise Reject('POST to an item is not allowed') 295 raise Reject('POST to an item is not allowed')
296
297 def post_attribute(self, class_name, item_id, attr_name, input):
298 """POST to an attribute of an object is not allowed"""
299 raise Reject('POST to an attribute is not allowed')
239 300
240 def put_collection(self, class_name, input): 301 def put_collection(self, class_name, input):
241 """PUT a class is not allowed""" 302 """PUT a class is not allowed"""
242 raise Reject('PUT a class is not allowed') 303 raise Reject('PUT a class is not allowed')
243 304
283 'link': self.base_path + class_name + item_id, 344 'link': self.base_path + class_name + item_id,
284 'attribute': result 345 'attribute': result
285 } 346 }
286 return 200, result 347 return 200, result
287 348
349 def put_attribute(self, class_name, item_id, attr_name, input):
350 """PUT an attribute to an object
351
352 Args:
353 class_name (string): class name of the resource (Ex: issue, msg)
354 item_id (string): id of the resource (Ex: 12, 15)
355 attr_name (string): attribute of the resource (Ex: title, nosy)
356 input (list): the submitted form of the user
357
358 Returns:
359 int: http status code 200 (OK)
360 dict:a dictionary represents the modified object
361 id: id of the object
362 type: class name of the object
363 link: link to the object
364 attributes: a dictionary represent only changed attributes of
365 the object
366 """
367 if not self.db.security.hasPermission(
368 'Edit', self.db.getuid(), class_name, attr_name, item_id
369 ):
370 raise Unauthorised(
371 'Permission to edit %s%s %s denied' %
372 (class_name, item_id, attr_name)
373 )
374
375 class_obj = self.db.getclass(class_name)
376 props = {
377 attr_name: self.prop_from_arg(
378 class_obj, attr_name, input['data'].value, item_id
379 )
380 }
381
382 try:
383 result = class_obj.set(item_id, **props)
384 self.db.commit()
385 except (TypeError, IndexError, ValueError), message:
386 raise ValueError(message)
387
388 result = {
389 'id': item_id,
390 'type': class_name,
391 'link': self.base_path + class_name + item_id,
392 'attribute': result
393 }
394
395 return 200, result
396
288 def delete_collection(self, class_name, input): 397 def delete_collection(self, class_name, input):
289 """DELETE all objects in a class 398 """DELETE all objects in a class
290 399
291 Args: 400 Args:
292 class_name (string): class name of the resource (Ex: issue, msg) 401 class_name (string): class name of the resource (Ex: issue, msg)
350 'status': 'ok' 459 'status': 'ok'
351 } 460 }
352 461
353 return 200, result 462 return 200, result
354 463
464 def delete_attribute(self, class_name, item_id, attr_name, input):
465 """DELETE an attribute in a object by setting it to None or empty
466
467 Args:
468 class_name (string): class name of the resource (Ex: issue, msg)
469 item_id (string): id of the resource (Ex: 12, 15)
470 attr_name (string): attribute of the resource (Ex: title, nosy)
471 input (list): the submitted form of the user
472
473 Returns:
474 int: http status code 200 (OK)
475 dict:
476 status (string): 'ok'
477 """
478 if not self.db.security.hasPermission(
479 'Edit', self.db.getuid(), class_name, attr_name, item_id
480 ):
481 raise Unauthorised(
482 'Permission to delete %s%s %s denied' %
483 (class_name, item_id, attr_name)
484 )
485
486 class_obj = self.db.getclass(class_name)
487 props = {}
488 prop_obj = class_obj.get(item_id, attr_name)
489 if isinstance(prop_obj, list):
490 props[attr_name] = []
491 else:
492 props[attr_name] = None
493
494 try:
495 class_obj.set(item_id, **props)
496 self.db.commit()
497 except (TypeError, IndexError, ValueError), message:
498 raise ValueError(message)
499
500 result = {
501 'status': 'ok'
502 }
503
504 return 200, result
505
355 def patch_collection(self, class_name, input): 506 def patch_collection(self, class_name, input):
356 """PATCH a class is not allowed""" 507 """PATCH a class is not allowed"""
357 raise Reject('PATCH a class is not allowed') 508 raise Reject('PATCH a class is not allowed')
358 509
359 def patch_element(self, class_name, item_id, input): 510 def patch_element(self, class_name, item_id, input):
511 """PATCH an object
512
513 Patch an element using 3 operators
514 ADD : Append new value to the object's attribute
515 REPLACE: Replace object's attribute
516 REMOVE: Clear object's attribute
517
518 Args:
519 class_name (string): class name of the resource (Ex: issue, msg)
520 item_id (string): id of the resource (Ex: 12, 15)
521 input (list): the submitted form of the user
522
523 Returns:
524 int: http status code 200 (OK)
525 dict: a dictionary represents the modified object
526 id: id of the object
527 type: class name of the object
528 link: link to the object
529 attributes: a dictionary represent only changed attributes of
530 the object
531 """
360 try: 532 try:
361 op = input['op'].value.lower() 533 op = input['op'].value.lower()
362 except KeyError: 534 except KeyError:
363 op = "replace" 535 op = "replace"
364 class_obj = self.db.getclass(class_name) 536 class_obj = self.db.getclass(class_name)
399 'link': self.base_path + class_name + item_id, 571 'link': self.base_path + class_name + item_id,
400 'attribute': result 572 'attribute': result
401 } 573 }
402 return 200, result 574 return 200, result
403 575
576 def patch_attribute(self, class_name, item_id, attr_name, input):
577 """PATCH an attribute of an object
578
579 Patch an element using 3 operators
580 ADD : Append new value to the attribute
581 REPLACE: Replace attribute
582 REMOVE: Clear attribute
583
584 Args:
585 class_name (string): class name of the resource (Ex: issue, msg)
586 item_id (string): id of the resource (Ex: 12, 15)
587 attr_name (string): attribute of the resource (Ex: title, nosy)
588 input (list): the submitted form of the user
589
590 Returns:
591 int: http status code 200 (OK)
592 dict: a dictionary represents the modified object
593 id: id of the object
594 type: class name of the object
595 link: link to the object
596 attributes: a dictionary represent only changed attributes of
597 the object
598 """
599 try:
600 op = input['op'].value.lower()
601 except KeyError:
602 op = "replace"
603 class_obj = self.db.getclass(class_name)
604
605 if not self.db.security.hasPermission(
606 'Edit', self.db.getuid(), class_name, attr_name, item_id
607 ):
608 raise Unauthorised(
609 'Permission to edit %s%s %s denied' %
610 (class_name, item_id, attr_name)
611 )
612
613 prop = attr_name
614 class_obj = self.db.getclass(class_name)
615 props = {
616 prop: self.prop_from_arg(
617 class_obj, prop, input['data'].value, item_id
618 )
619 }
620
621 if op == 'add':
622 props[prop] = class_obj.get(item_id, prop) + props[prop]
623 elif op == 'replace':
624 pass
625 elif op == 'remove':
626 current_prop = class_obj.get(item_id, prop)
627 if isinstance(current_prop, list):
628 props[prop] = []
629 else:
630 props[prop] = None
631 else:
632 raise UsageError('PATCH Operation %s is not allowed' % op)
633
634 try:
635 result = class_obj.set(item_id, **props)
636 self.db.commit()
637 except (TypeError, IndexError, ValueError), message:
638 raise ValueError(message)
639
640 result = {
641 'id': item_id,
642 'type': class_name,
643 'link': self.base_path + class_name + item_id,
644 'attribute': result
645 }
646 return 200, result
647
404 def options_collection(self, class_name, input): 648 def options_collection(self, class_name, input):
405 """OPTION return the HTTP Header for the class uri 649 """OPTION return the HTTP Header for the class uri
406 650
407 Returns: 651 Returns:
408 int: http status code 204 (No content) 652 int: http status code 204 (No content)
417 int: http status code 204 (No content) 661 int: http status code 204 (No content)
418 body (string): an empty string 662 body (string): an empty string
419 """ 663 """
420 self.client.setHeader( 664 self.client.setHeader(
421 "Accept-Patch", 665 "Accept-Patch",
422 "application/x-www-form-urlencoded, " 666 "application/x-www-form-urlencoded, multipart/form-data"
423 "multipart/form-data" 667 )
668 return 204, ""
669
670 def option_attribute(self, class_name, item_id, attr_name, input):
671 """OPTION return the HTTP Header for the attribute uri
672
673 Returns:
674 int: http status code 204 (No content)
675 body (string): an empty string
676 """
677 self.client.setHeader(
678 "Accept-Patch",
679 "application/x-www-form-urlencoded, multipart/form-data"
424 ) 680 )
425 return 204, "" 681 return 204, ""
426 682
427 def dispatch(self, method, uri, input): 683 def dispatch(self, method, uri, input):
428 """format and process the request""" 684 """format and process the request"""
429 # PATH is split to multiple pieces 685 # PATH is split to multiple pieces
430 # 0 - rest 686 # 0 - rest
431 # 1 - resource 687 # 1 - resource
432 # 2 - attribute 688 # 2 - attribute
433 resource_uri = uri.split("/")[1] 689 uri_split = uri.split("/")
690 resource_uri = uri_split[1]
434 691
435 # if X-HTTP-Method-Override is set, follow the override method 692 # if X-HTTP-Method-Override is set, follow the override method
436 headers = self.client.request.headers 693 headers = self.client.request.headers
437 method = headers.getheader('X-HTTP-Method-Override') or method 694 method = headers.getheader('X-HTTP-Method-Override') or method
438 695
484 response_code, output = getattr( 741 response_code, output = getattr(
485 self, "%s_collection" % method.lower() 742 self, "%s_collection" % method.lower()
486 )(resource_uri, input) 743 )(resource_uri, input)
487 else: 744 else:
488 class_name, item_id = hyperdb.splitDesignator(resource_uri) 745 class_name, item_id = hyperdb.splitDesignator(resource_uri)
489 response_code, output = getattr( 746 if len(uri_split) == 3:
490 self, "%s_element" % method.lower() 747 response_code, output = getattr(
491 )(class_name, item_id, input) 748 self, "%s_attribute" % method.lower()
749 )(class_name, item_id, uri_split[2], input)
750 else:
751 response_code, output = getattr(
752 self, "%s_element" % method.lower()
753 )(class_name, item_id, input)
492 output = RestfulInstance.data_obj(output) 754 output = RestfulInstance.data_obj(output)
493 self.client.response_code = response_code 755 self.client.response_code = response_code
494 except IndexError, msg: 756 except IndexError, msg:
495 output = RestfulInstance.error_obj(404, msg) 757 output = RestfulInstance.error_obj(404, msg)
496 self.client.response_code = 404 758 self.client.response_code = 404

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