Skip to content

Commit 897a844

Browse files
committed
Added attribute URI handling
committer: Ralf Schlatterbeck <[email protected]>
1 parent bbc55b8 commit 897a844

File tree

1 file changed

+287
-25
lines changed

1 file changed

+287
-25
lines changed

roundup/rest.py

Lines changed: 287 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,27 +51,44 @@ def props_from_args(self, cl, args, itemid=None):
5151
value = arg.value
5252
if key not in class_props:
5353
continue
54-
if isinstance(key, unicode):
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
54+
props[key] = self.prop_from_arg(cl, key, value, itemid)
7255

7356
return props
7457

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
91+
7592
@staticmethod
7693
def error_obj(status, msg, source=None):
7794
"""Wrap the error data into an object. This function is temporally and
@@ -152,7 +169,7 @@ def get_element(self, class_name, item_id, input):
152169
'View', self.db.getuid(), class_name, itemid=item_id
153170
):
154171
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)
156173
)
157174

158175
class_obj = self.db.getclass(class_name)
@@ -174,6 +191,46 @@ def get_element(self, class_name, item_id, input):
174191

175192
return 200, result
176193

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+
177234
def post_collection(self, class_name, input):
178235
"""POST a new object to a class
179236
@@ -237,6 +294,10 @@ def post_element(self, class_name, item_id, input):
237294
"""POST to an object of a class is not allowed"""
238295
raise Reject('POST to an item is not allowed')
239296

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')
300+
240301
def put_collection(self, class_name, input):
241302
"""PUT a class is not allowed"""
242303
raise Reject('PUT a class is not allowed')
@@ -285,6 +346,54 @@ def put_element(self, class_name, item_id, input):
285346
}
286347
return 200, result
287348

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+
288397
def delete_collection(self, class_name, input):
289398
"""DELETE all objects in a class
290399
@@ -352,11 +461,74 @@ def delete_element(self, class_name, item_id, input):
352461

353462
return 200, result
354463

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+
355506
def patch_collection(self, class_name, input):
356507
"""PATCH a class is not allowed"""
357508
raise Reject('PATCH a class is not allowed')
358509

359510
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+
"""
360532
try:
361533
op = input['op'].value.lower()
362534
except KeyError:
@@ -401,6 +573,78 @@ def patch_element(self, class_name, item_id, input):
401573
}
402574
return 200, result
403575

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+
404648
def options_collection(self, class_name, input):
405649
"""OPTION return the HTTP Header for the class uri
406650
@@ -419,8 +663,20 @@ def options_element(self, class_name, item_id, input):
419663
"""
420664
self.client.setHeader(
421665
"Accept-Patch",
422-
"application/x-www-form-urlencoded, "
423-
"multipart/form-data"
666+
"application/x-www-form-urlencoded, 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"
424680
)
425681
return 204, ""
426682

@@ -430,7 +686,8 @@ def dispatch(self, method, uri, input):
430686
# 0 - rest
431687
# 1 - resource
432688
# 2 - attribute
433-
resource_uri = uri.split("/")[1]
689+
uri_split = uri.split("/")
690+
resource_uri = uri_split[1]
434691

435692
# if X-HTTP-Method-Override is set, follow the override method
436693
headers = self.client.request.headers
@@ -486,9 +743,14 @@ def dispatch(self, method, uri, input):
486743
)(resource_uri, input)
487744
else:
488745
class_name, item_id = hyperdb.splitDesignator(resource_uri)
489-
response_code, output = getattr(
490-
self, "%s_element" % method.lower()
491-
)(class_name, item_id, input)
746+
if len(uri_split) == 3:
747+
response_code, output = getattr(
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)
492754
output = RestfulInstance.data_obj(output)
493755
self.client.response_code = response_code
494756
except IndexError, msg:

0 commit comments

Comments
 (0)