Mercurial > p > roundup > code
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): |
