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)

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