comparison roundup/rest.py @ 5581:30793a435185 REST-rebased

Code convention improved committer: Ralf Schlatterbeck <rsc@runtux.com>
author Chau Nguyen <dangchau1991@yahoo.com>
date Wed, 30 Jan 2019 10:26:35 +0100
parents d5a54b1851aa
children 519b7fd8c8c3
comparison
equal deleted inserted replaced
5580:d5a54b1851aa 5581:30793a435185
15 from roundup import hyperdb 15 from roundup import hyperdb
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 def props_from_args(db, cl, args, itemid=None):
21 class_props = cl.properties.keys()
22 props = {}
23 # props = dict.fromkeys(class_props, None)
24
25 for arg in args:
26 key = arg.name
27 value = arg.value
28 if key not in class_props:
29 continue
30 if isinstance(key, unicode):
31 try:
32 key = key.encode('ascii')
33 except UnicodeEncodeError:
34 raise UsageError('argument %r is no valid ascii keyword' % key)
35 if isinstance(value, unicode):
36 value = value.encode('utf-8')
37 if value:
38 try:
39 props[key] = hyperdb.rawToHyperdb(db, cl, itemid, key, value)
40 except hyperdb.HyperdbValueError, msg:
41 raise UsageError(msg)
42 else:
43 props[key] = None
44
45 return props
46
47
48 def error_obj(status, msg, source=None):
49 result = {
50 'error': {
51 'status': status,
52 'msg': msg
53 }
54 }
55 if source is not None:
56 result['error']['source'] = source
57
58 return result
59
60
61 def data_obj(data):
62 result = {
63 'data': data
64 }
65 return result
66
67
68 class RestfulInstance(object): 20 class RestfulInstance(object):
69 """Dummy Handler for REST 21 """Dummy Handler for REST
70 """ 22 """
71 23
72 def __init__(self, client, db): 24 def __init__(self, client, db):
76 protocol = 'http' 28 protocol = 'http'
77 host = self.client.env['HTTP_HOST'] 29 host = self.client.env['HTTP_HOST']
78 tracker = self.client.env['TRACKER_NAME'] 30 tracker = self.client.env['TRACKER_NAME']
79 self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker) 31 self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker)
80 32
33 def props_from_args(self, cl, args, itemid=None):
34 class_props = cl.properties.keys()
35 props = {}
36 # props = dict.fromkeys(class_props, None)
37
38 for arg in args:
39 key = arg.name
40 value = arg.value
41 if key not in class_props:
42 continue
43 if isinstance(key, unicode):
44 try:
45 key = key.encode('ascii')
46 except UnicodeEncodeError:
47 raise UsageError(
48 'argument %r is no valid ascii keyword' % key
49 )
50 if isinstance(value, unicode):
51 value = value.encode('utf-8')
52 if value:
53 try:
54 props[key] = hyperdb.rawToHyperdb(
55 self.db, cl, itemid, key, value
56 )
57 except hyperdb.HyperdbValueError, msg:
58 raise UsageError(msg)
59 else:
60 props[key] = None
61
62 return props
63
64 @staticmethod
65 def error_obj(status, msg, source=None):
66 result = {
67 'error': {
68 'status': status,
69 'msg': msg
70 }
71 }
72 if source is not None:
73 result['error']['source'] = source
74
75 return result
76
77 @staticmethod
78 def data_obj(data):
79 result = {
80 'data': data
81 }
82 return result
83
81 def get_collection(self, class_name, input): 84 def get_collection(self, class_name, input):
82 if not self.db.security.hasPermission('View', self.db.getuid(), 85 if not self.db.security.hasPermission(
83 class_name): 86 'View', self.db.getuid(), class_name
87 ):
84 raise Unauthorised('Permission to view %s denied' % class_name) 88 raise Unauthorised('Permission to view %s denied' % class_name)
89
85 class_obj = self.db.getclass(class_name) 90 class_obj = self.db.getclass(class_name)
86 class_path = self.base_path + class_name 91 class_path = self.base_path + class_name
87 result = [{'id': item_id, 'link': class_path + item_id} 92 result = [
88 for item_id in class_obj.list() 93 {'id': item_id, 'link': class_path + item_id}
89 if self.db.security.hasPermission('View', self.db.getuid(), 94 for item_id in class_obj.list()
90 class_name, 95 if self.db.security.hasPermission(
91 itemid=item_id)] 96 'View', self.db.getuid(), class_name, itemid=item_id
97 )
98 ]
92 self.client.setHeader("X-Count-Total", str(len(result))) 99 self.client.setHeader("X-Count-Total", str(len(result)))
93 return 200, result 100 return 200, result
94 101
95 def get_element(self, class_name, item_id, input): 102 def get_element(self, class_name, item_id, input):
96 if not self.db.security.hasPermission('View', self.db.getuid(), 103 if not self.db.security.hasPermission(
97 class_name, itemid=item_id): 104 'View', self.db.getuid(), class_name, itemid=item_id
98 raise Unauthorised('Permission to view %s item %d denied' % 105 ):
99 (class_name, item_id)) 106 raise Unauthorised(
107 'Permission to view %s item %d denied' % (class_name, item_id)
108 )
109
100 class_obj = self.db.getclass(class_name) 110 class_obj = self.db.getclass(class_name)
101 props = class_obj.properties.keys() 111 props = class_obj.properties.keys()
102 props.sort() # sort properties 112 props.sort() # sort properties
103 result = [(prop_name, class_obj.get(item_id, prop_name)) 113 result = [
104 for prop_name in props 114 (prop_name, class_obj.get(item_id, prop_name))
105 if self.db.security.hasPermission('View', self.db.getuid(), 115 for prop_name in props
106 class_name, prop_name, 116 if self.db.security.hasPermission(
107 item_id)] 117 'View', self.db.getuid(), class_name, prop_name,
118 )
119 ]
108 result = { 120 result = {
109 'id': item_id, 121 'id': item_id,
110 'type': class_name, 122 'type': class_name,
111 'link': self.base_path + class_name + item_id, 123 'link': self.base_path + class_name + item_id,
112 'attributes': dict(result) 124 'attributes': dict(result)
113 } 125 }
114 126
115 return 200, result 127 return 200, result
116 128
117 def post_collection(self, class_name, input): 129 def post_collection(self, class_name, input):
118 if not self.db.security.hasPermission('Create', self.db.getuid(), 130 if not self.db.security.hasPermission(
119 class_name): 131 'Create', self.db.getuid(), class_name
132 ):
120 raise Unauthorised('Permission to create %s denied' % class_name) 133 raise Unauthorised('Permission to create %s denied' % class_name)
121 134
122 class_obj = self.db.getclass(class_name) 135 class_obj = self.db.getclass(class_name)
123 136
124 # convert types 137 # convert types
125 props = props_from_args(self.db, class_obj, input.value) 138 props = self.props_from_args(class_obj, input.value)
126 139
127 # check for the key property 140 # check for the key property
128 key = class_obj.getkey() 141 key = class_obj.getkey()
129 if key and key not in props: 142 if key and key not in props:
130 raise UsageError("Must provide the '%s' property." % key) 143 raise UsageError("Must provide the '%s' property." % key)
131 144
132 for key in props: 145 for key in props:
133 if not self.db.security.hasPermission('Create', self.db.getuid(), 146 if not self.db.security.hasPermission(
134 class_name, property=key): 147 'Create', self.db.getuid(), class_name, property=key
135 raise Unauthorised('Permission to create %s.%s denied' % 148 ):
136 (class_name, key)) 149 raise Unauthorised(
150 'Permission to create %s.%s denied' % (class_name, key)
151 )
137 152
138 # do the actual create 153 # do the actual create
139 try: 154 try:
140 item_id = class_obj.create(**props) 155 item_id = class_obj.create(**props)
141 self.db.commit() 156 self.db.commit()
162 raise Reject('PUT a class is not allowed') 177 raise Reject('PUT a class is not allowed')
163 178
164 def put_element(self, class_name, item_id, input): 179 def put_element(self, class_name, item_id, input):
165 class_obj = self.db.getclass(class_name) 180 class_obj = self.db.getclass(class_name)
166 181
167 props = props_from_args(self.db, class_obj, input.value, item_id) 182 props = self.props_from_args(class_obj, input.value, item_id)
168 for p in props.iterkeys(): 183 for p in props.iterkeys():
169 if not self.db.security.hasPermission('Edit', self.db.getuid(), 184 if not self.db.security.hasPermission(
170 class_name, p, item_id): 185 'Edit', self.db.getuid(), class_name, p, item_id
171 raise Unauthorised('Permission to edit %s of %s%s denied' % 186 ):
172 (p, class_name, item_id)) 187 raise Unauthorised(
188 'Permission to edit %s of %s%s denied' %
189 (p, class_name, item_id)
190 )
173 try: 191 try:
174 result = class_obj.set(item_id, **props) 192 result = class_obj.set(item_id, **props)
175 self.db.commit() 193 self.db.commit()
176 except (TypeError, IndexError, ValueError), message: 194 except (TypeError, IndexError, ValueError), message:
177 raise ValueError(message) 195 raise ValueError(message)
183 'attribute': result 201 'attribute': result
184 } 202 }
185 return 200, result 203 return 200, result
186 204
187 def delete_collection(self, class_name, input): 205 def delete_collection(self, class_name, input):
188 if not self.db.security.hasPermission('Delete', self.db.getuid(), 206 if not self.db.security.hasPermission(
189 class_name): 207 'Delete', self.db.getuid(), class_name
208 ):
190 raise Unauthorised('Permission to delete %s denied' % class_name) 209 raise Unauthorised('Permission to delete %s denied' % class_name)
191 210
192 class_obj = self.db.getclass(class_name) 211 class_obj = self.db.getclass(class_name)
193 for item_id in class_obj.list(): 212 for item_id in class_obj.list():
194 if not self.db.security.hasPermission('Delete', self.db.getuid(), 213 if not self.db.security.hasPermission(
195 class_name, itemid=item_id): 214 'Delete', self.db.getuid(), class_name, itemid=item_id
196 raise Unauthorised('Permission to delete %s %s denied' % 215 ):
197 (class_name, item_id)) 216 raise Unauthorised(
217 'Permission to delete %s %s denied' % (class_name, item_id)
218 )
198 219
199 count = len(class_obj.list()) 220 count = len(class_obj.list())
200 for item_id in class_obj.list(): 221 for item_id in class_obj.list():
201 self.db.destroynode(class_name, item_id) 222 self.db.destroynode(class_name, item_id)
202 223
207 } 228 }
208 229
209 return 200, result 230 return 200, result
210 231
211 def delete_element(self, class_name, item_id, input): 232 def delete_element(self, class_name, item_id, input):
212 if not self.db.security.hasPermission('Delete', self.db.getuid(), 233 if not self.db.security.hasPermission(
213 class_name, itemid=item_id): 234 'Delete', self.db.getuid(), class_name, itemid=item_id
214 raise Unauthorised('Permission to delete %s %s denied' % 235 ):
215 (class_name, item_id)) 236 raise Unauthorised(
237 'Permission to delete %s %s denied' % (class_name, item_id)
238 )
216 239
217 self.db.destroynode(class_name, item_id) 240 self.db.destroynode(class_name, item_id)
218 self.db.commit() 241 self.db.commit()
219 result = { 242 result = {
220 'status': 'ok' 243 'status': 'ok'
230 op = input['op'].value.lower() 253 op = input['op'].value.lower()
231 except KeyError: 254 except KeyError:
232 op = "replace" 255 op = "replace"
233 class_obj = self.db.getclass(class_name) 256 class_obj = self.db.getclass(class_name)
234 257
235 props = props_from_args(self.db, class_obj, input.value, item_id) 258 props = self.props_from_args(class_obj, input.value, item_id)
236 259
237 for prop, value in props.iteritems(): 260 for prop, value in props.iteritems():
238 if not self.db.security.hasPermission('Edit', self.db.getuid(), 261 if not self.db.security.hasPermission(
239 class_name, prop, item_id): 262 'Edit', self.db.getuid(), class_name, prop, item_id
240 raise Unauthorised('Permission to edit %s of %s%s denied' % 263 ):
241 (prop, class_name, item_id)) 264 raise Unauthorised(
265 'Permission to edit %s of %s%s denied' %
266 (prop, class_name, item_id)
267 )
268
242 if op == 'add': 269 if op == 'add':
243 props[prop] = class_obj.get(item_id, prop) + props[prop] 270 props[prop] = class_obj.get(item_id, prop) + props[prop]
244 elif op == 'replace': 271 elif op == 'replace':
245 pass 272 pass
246 elif op == 'remove': 273 elif op == 'remove':
264 291
265 def options_collection(self, class_name, input): 292 def options_collection(self, class_name, input):
266 return 204, "" 293 return 204, ""
267 294
268 def options_element(self, class_name, item_id, input): 295 def options_element(self, class_name, item_id, input):
269 self.client.setHeader("Accept-Patch", 296 self.client.setHeader(
270 "application/x-www-form-urlencoded, " 297 "Accept-Patch",
271 "multipart/form-data") 298 "application/x-www-form-urlencoded, "
299 "multipart/form-data"
300 )
272 return 204, "" 301 return 204, ""
273 302
274 def dispatch(self, method, uri, input): 303 def dispatch(self, method, uri, input):
275 # PATH is split to multiple pieces 304 # PATH is split to multiple pieces
276 # 0 - rest 305 # 0 - rest
277 # 1 - resource 306 # 1 - resource
278 # 2 - attribute 307 # 2 - attribute
279 resource_uri = uri.split("/")[1] 308 resource_uri = uri.split("/")[1]
280 309
281 # if X-HTTP-Method-Override is set, follow the override method 310 # if X-HTTP-Method-Override is set, follow the override method
282 method = self.client.request.headers.getheader('X-HTTP-Method-Override') or method 311 headers = self.client.request.headers
312 method = headers.getheader('X-HTTP-Method-Override') or method
283 313
284 # get the request format for response 314 # get the request format for response
285 # priority : extension from uri (/rest/issue.json), 315 # priority : extension from uri (/rest/issue.json),
286 # header (Accept: application/json, application/xml) 316 # header (Accept: application/json, application/xml)
287 # default (application/json) 317 # default (application/json)
288 318
289 # format_header need a priority parser 319 # format_header need a priority parser
290 format_ext = os.path.splitext(urlparse.urlparse(uri).path)[1][1:] 320 format_ext = os.path.splitext(urlparse.urlparse(uri).path)[1][1:]
291 format_header = self.client.request.headers.getheader('Accept')[12:] 321 format_header = headers.getheader('Accept')[12:]
292 format_output = format_ext or format_header or "json" 322 format_output = format_ext or format_header or "json"
293 323
294 # check for pretty print 324 # check for pretty print
295 try: 325 try:
296 pretty_output = input['pretty'].value.lower() == "true" 326 pretty_output = input['pretty'].value.lower() == "true"
297 except KeyError: 327 except KeyError:
298 pretty_output = False 328 pretty_output = False
299 329
300 self.client.setHeader("Access-Control-Allow-Origin", "*") 330 self.client.setHeader("Access-Control-Allow-Origin", "*")
301 self.client.setHeader("Access-Control-Allow-Headers", 331 self.client.setHeader(
302 "Content-Type, Authorization, " 332 "Access-Control-Allow-Headers",
303 "X-HTTP-Method-Override") 333 "Content-Type, Authorization, X-HTTP-Method-Override"
334 )
304 335
305 output = None 336 output = None
306 try: 337 try:
307 if resource_uri in self.db.classes: 338 if resource_uri in self.db.classes:
308 self.client.setHeader("Allow", 339 self.client.setHeader(
309 "HEAD, OPTIONS, GET, POST, DELETE") 340 "Allow",
310 self.client.setHeader("Access-Control-Allow-Methods", 341 "HEAD, OPTIONS, GET, POST, DELETE"
311 "HEAD, OPTIONS, GET, POST, DELETE") 342 )
312 response_code, output = getattr(self, "%s_collection" % method.lower())( 343 self.client.setHeader(
313 resource_uri, input) 344 "Access-Control-Allow-Methods",
345 "HEAD, OPTIONS, GET, POST, DELETE"
346 )
347 response_code, output = getattr(
348 self, "%s_collection" % method.lower()
349 )(resource_uri, input)
314 else: 350 else:
315 class_name, item_id = hyperdb.splitDesignator(resource_uri) 351 class_name, item_id = hyperdb.splitDesignator(resource_uri)
316 self.client.setHeader("Allow", 352 self.client.setHeader(
317 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH") 353 "Allow",
318 self.client.setHeader("Access-Control-Allow-Methods", 354 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
319 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH") 355 )
320 response_code, output = getattr(self, "%s_element" % method.lower())( 356 self.client.setHeader(
321 class_name, item_id, input) 357 "Access-Control-Allow-Methods",
322 358 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
323 output = data_obj(output) 359 )
360 response_code, output = getattr(
361 self, "%s_element" % method.lower()
362 )(class_name, item_id, input)
363
364 output = RestfulInstance.data_obj(output)
324 self.client.response_code = response_code 365 self.client.response_code = response_code
325 except IndexError, msg: 366 except IndexError, msg:
326 output = error_obj(404, msg) 367 output = RestfulInstance.error_obj(404, msg)
327 self.client.response_code = 404 368 self.client.response_code = 404
328 except Unauthorised, msg: 369 except Unauthorised, msg:
329 output = error_obj(403, msg) 370 output = RestfulInstance.error_obj(403, msg)
330 self.client.response_code = 403 371 self.client.response_code = 403
331 except (hyperdb.DesignatorError, UsageError), msg: 372 except (hyperdb.DesignatorError, UsageError), msg:
332 output = error_obj(400, msg) 373 output = RestfulInstance.error_obj(400, msg)
333 self.client.response_code = 400 374 self.client.response_code = 400
334 except (AttributeError, Reject), msg: 375 except (AttributeError, Reject), msg:
335 output = error_obj(405, msg) 376 output = RestfulInstance.error_obj(405, msg)
336 self.client.response_code = 405 377 self.client.response_code = 405
337 except ValueError, msg: 378 except ValueError, msg:
338 output = error_obj(409, msg) 379 output = RestfulInstance.error_obj(409, msg)
339 self.client.response_code = 409 380 self.client.response_code = 409
340 except NotImplementedError: 381 except NotImplementedError:
341 output = error_obj(402, 'Method is under development') 382 output = RestfulInstance.error_obj(402, 'Method under development')
342 self.client.response_code = 402 383 self.client.response_code = 402
343 # nothing to pay, just a mark for debugging purpose 384 # nothing to pay, just a mark for debugging purpose
344 except: 385 except:
345 # if self.DEBUG_MODE in roundup_server 386 # if self.DEBUG_MODE in roundup_server
346 # else msg = 'An error occurred. Please check...', 387 # else msg = 'An error occurred. Please check...',
347 exc, val, tb = sys.exc_info() 388 exc, val, tb = sys.exc_info()
348 output = error_obj(400, val) 389 output = RestfulInstance.error_obj(400, val)
349 self.client.response_code = 400 390 self.client.response_code = 400
350 391
351 # out to the logfile, it would be nice if the server do it for me 392 # out to the logfile, it would be nice if the server do it for me
352 print 'EXCEPTION AT', time.ctime() 393 print 'EXCEPTION AT', time.ctime()
353 traceback.print_exc() 394 traceback.print_exc()
359 else: 400 else:
360 indent = None 401 indent = None
361 output = RoundupJSONEncoder(indent=indent).encode(output) 402 output = RoundupJSONEncoder(indent=indent).encode(output)
362 else: 403 else:
363 self.client.response_code = 406 404 self.client.response_code = 406
364 output = "" 405 output = "Content type is not accepted by client"
365 406
366 return output 407 return output
367 408
368 409
369 class RoundupJSONEncoder(json.JSONEncoder): 410 class RoundupJSONEncoder(json.JSONEncoder):

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