- attachment:roundupRestfulAPI.patch of RestImplementationStatus
Attachment 'roundupRestfulAPI.patch'
Download 1 # HG changeset patch
2 # User Chau Nguyen <dangchau1991@yahoo.com>
3 # Date 1439400860 -10800
4 # Wed Aug 12 20:34:20 2015 +0300
5 # Branch REST
6 # Node ID 7f2ad229b72595ff48e821755c50d6f1acacd833
7 # Parent a8ae1e986147e68a0807f05b99ae1ee1b970b3d2
8 Update resource links to match the recently URI changes
9
10 diff --git a/roundup/rest.py b/roundup/rest.py
11 --- a/roundup/rest.py
12 +++ b/roundup/rest.py
13 @@ -238,7 +238,8 @@
14 protocol = 'http'
15 host = self.client.env['HTTP_HOST']
16 tracker = self.client.env['TRACKER_NAME']
17 - self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker)
18 + self.base_path = '%s://%s/%s/rest' % (protocol, host, tracker)
19 + self.data_path = self.base_path + '/data'
20
21 def props_from_args(self, cl, args, itemid=None):
22 """Construct a list of properties from the given arguments,
23 @@ -393,7 +394,7 @@
24 raise Unauthorised('Permission to view %s denied' % class_name)
25
26 class_obj = self.db.getclass(class_name)
27 - class_path = self.base_path + class_name
28 + class_path = '%s/%s/' % (self.data_path, class_name)
29
30 # Handle filtering and pagination
31 filter_props = {}
32 @@ -494,7 +495,7 @@
33 result = {
34 'id': item_id,
35 'type': class_name,
36 - 'link': self.base_path + class_name + item_id,
37 + 'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
38 'attributes': dict(result)
39 }
40
41 @@ -537,8 +538,8 @@
42 result = {
43 'id': item_id,
44 'type': type(data),
45 - 'link': "%s%s%s/%s" %
46 - (self.base_path, class_name, item_id, attr_name),
47 + 'link': "%s/%s/%s/%s" %
48 + (self.data_path, class_name, item_id, attr_name),
49 'data': data
50 }
51
52 @@ -597,7 +598,7 @@
53 raise UsageError("Must provide the %s property." % msg)
54
55 # set the header Location
56 - link = self.base_path + class_name + item_id
57 + link = '%s/%s/%s' % (self.data_path, class_name, item_id)
58 self.client.setHeader("Location", link)
59
60 # set the response body
61 @@ -650,7 +651,7 @@
62 result = {
63 'id': item_id,
64 'type': class_name,
65 - 'link': self.base_path + class_name + item_id,
66 + 'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
67 'attribute': result
68 }
69 return 200, result
70 @@ -701,7 +702,7 @@
71 result = {
72 'id': item_id,
73 'type': class_name,
74 - 'link': self.base_path + class_name + item_id,
75 + 'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
76 'attribute': result
77 }
78
79 @@ -885,7 +886,7 @@
80 result = {
81 'id': item_id,
82 'type': class_name,
83 - 'link': self.base_path + class_name + item_id,
84 + 'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
85 'result': result
86 }
87 else:
88 @@ -914,7 +915,7 @@
89 result = {
90 'id': item_id,
91 'type': class_name,
92 - 'link': self.base_path + class_name + item_id,
93 + 'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
94 'attribute': result
95 }
96 return 200, result
97 @@ -980,7 +981,7 @@
98 result = {
99 'id': item_id,
100 'type': class_name,
101 - 'link': self.base_path + class_name + item_id,
102 + 'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
103 'attribute': result
104 }
105 return 200, result
106 diff --git a/test/test_rest.py b/test/test_rest.py
107 --- a/test/test_rest.py
108 +++ b/test/test_rest.py
109 @@ -146,7 +146,7 @@
110 status=self.db.status.lookup('open'),
111 priority=self.db.priority.lookup('critical')
112 )
113 - base_path = self.dummy_client.env['PATH_INFO'] + 'issue'
114 + base_path = self.dummy_client.env['PATH_INFO'] + 'data/issue/'
115
116 # Retrieve all issue status=open
117 form = cgi.FieldStorage()
118 # HG changeset patch
119 # User Chau Nguyen <dangchau1991@yahoo.com>
120 # Date 1439391426 -10800
121 # Wed Aug 12 17:57:06 2015 +0300
122 # Branch REST
123 # Node ID a8ae1e986147e68a0807f05b99ae1ee1b970b3d2
124 # Parent d6631549bdb62b86d669dc9695809fc4333a9b3c
125 Added Patch operator 'action' to perform actions such as 'retire'
126
127 diff --git a/roundup/rest.py b/roundup/rest.py
128 --- a/roundup/rest.py
129 +++ b/roundup/rest.py
130 @@ -16,6 +16,7 @@
131
132 from roundup import hyperdb
133 from roundup import date
134 +from roundup import actions
135 from roundup.exceptions import *
136 from roundup.cgi.exceptions import *
137
138 @@ -230,6 +231,9 @@
139 def __init__(self, client, db):
140 self.client = client
141 self.db = db
142 + self.translator = client.translator
143 + self.actions = client.instance.actions.copy()
144 + self.actions.update({'retire': actions.Retire})
145
146 protocol = 'http'
147 host = self.client.env['HTTP_HOST']
148 @@ -856,33 +860,63 @@
149 op = self.__default_patch_op
150 class_obj = self.db.getclass(class_name)
151
152 - props = self.props_from_args(class_obj, input.value, item_id)
153 + # if patch operation is action, call the action handler
154 + action_args = [class_name + item_id]
155 + if op == 'action':
156 + # extract action_name and action_args from form fields
157 + for form_field in input.value:
158 + key = form_field.name
159 + value = form_field.value
160 + if key == "action_name":
161 + name = value
162 + elif key.startswith('action_args'):
163 + action_args.append(value)
164
165 - for prop, value in props.iteritems():
166 - if not self.db.security.hasPermission(
167 - 'Edit', self.db.getuid(), class_name, prop, item_id
168 - ):
169 - raise Unauthorised(
170 - 'Permission to edit %s of %s%s denied' %
171 - (prop, class_name, item_id)
172 + if name in self.actions:
173 + action_type = self.actions[name]
174 + else:
175 + raise UsageError(
176 + 'action "%s" is not supported %s' %
177 + (name, ','.join(self.actions.keys()))
178 + )
179 + action = action_type(self.db, self.translator)
180 + result = action.execute(*action_args)
181 +
182 + result = {
183 + 'id': item_id,
184 + 'type': class_name,
185 + 'link': self.base_path + class_name + item_id,
186 + 'result': result
187 + }
188 + else:
189 + # else patch operation is processing data
190 + props = self.props_from_args(class_obj, input.value, item_id)
191 +
192 + for prop, value in props.iteritems():
193 + if not self.db.security.hasPermission(
194 + 'Edit', self.db.getuid(), class_name, prop, item_id
195 + ):
196 + raise Unauthorised(
197 + 'Permission to edit %s of %s%s denied' %
198 + (prop, class_name, item_id)
199 + )
200 +
201 + props[prop] = self.patch_data(
202 + op, class_obj.get(item_id, prop), props[prop]
203 )
204
205 - props[prop] = self.patch_data(
206 - op, class_obj.get(item_id, prop), props[prop]
207 - )
208 + try:
209 + result = class_obj.set(item_id, **props)
210 + self.db.commit()
211 + except (TypeError, IndexError, ValueError), message:
212 + raise ValueError(message)
213
214 - try:
215 - result = class_obj.set(item_id, **props)
216 - self.db.commit()
217 - except (TypeError, IndexError, ValueError), message:
218 - raise ValueError(message)
219 -
220 - result = {
221 - 'id': item_id,
222 - 'type': class_name,
223 - 'link': self.base_path + class_name + item_id,
224 - 'attribute': result
225 - }
226 + result = {
227 + 'id': item_id,
228 + 'type': class_name,
229 + 'link': self.base_path + class_name + item_id,
230 + 'attribute': result
231 + }
232 return 200, result
233
234 @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'PATCH')
235 diff --git a/test/test_rest.py b/test/test_rest.py
236 --- a/test/test_rest.py
237 +++ b/test/test_rest.py
238 @@ -461,6 +461,25 @@
239 self.assertEqual(len(results['attributes']['nosy']), 0)
240 self.assertEqual(results['attributes']['nosy'], [])
241
242 + def testPatchAction(self):
243 + """
244 + Test Patch Action 'Action'
245 + """
246 + # create a new issue with userid 1 and 2 in the nosy list
247 + issue_id = self.db.issue.create(title='foo')
248 +
249 + # execute action retire
250 + form = cgi.FieldStorage()
251 + form.list = [
252 + cgi.MiniFieldStorage('op', 'action'),
253 + cgi.MiniFieldStorage('action_name', 'retire')
254 + ]
255 + results = self.server.patch_element('issue', issue_id, form)
256 + self.assertEqual(self.dummy_client.response_code, 200)
257 +
258 + # verify the result
259 + self.assertTrue(self.db.issue.is_retired(issue_id))
260 +
261 def testPatchRemove(self):
262 """
263 Test Patch Action 'Remove' only some element from a list
264 # HG changeset patch
265 # User Chau Nguyen <dangchau1991@yahoo.com>
266 # Date 1439385301 -10800
267 # Wed Aug 12 16:15:01 2015 +0300
268 # Branch REST
269 # Node ID d6631549bdb62b86d669dc9695809fc4333a9b3c
270 # Parent c158adb21cb4a182c41b5ea7ff4ee0962ca28a6c
271 Added the ability to limit returned fields by GET
272
273 diff --git a/roundup/rest.py b/roundup/rest.py
274 --- a/roundup/rest.py
275 +++ b/roundup/rest.py
276 @@ -465,15 +465,28 @@
277 )
278
279 class_obj = self.db.getclass(class_name)
280 - props = class_obj.properties.keys()
281 + props = None
282 + for form_field in input.value:
283 + key = form_field.name
284 + value = form_field.value
285 + if key == "fields":
286 + props = value.split(",")
287 +
288 + if props is None:
289 + props = class_obj.properties.keys()
290 +
291 props.sort() # sort properties
292 - result = [
293 - (prop_name, class_obj.get(item_id, prop_name))
294 - for prop_name in props
295 - if self.db.security.hasPermission(
296 - 'View', self.db.getuid(), class_name, prop_name,
297 - )
298 - ]
299 +
300 + try:
301 + result = [
302 + (prop_name, class_obj.get(item_id, prop_name))
303 + for prop_name in props
304 + if self.db.security.hasPermission(
305 + 'View', self.db.getuid(), class_name, prop_name,
306 + )
307 + ]
308 + except KeyError, msg:
309 + raise UsageError("%s field not valid" % msg)
310 result = {
311 'id': item_id,
312 'type': class_name,
313 # HG changeset patch
314 # User Chau Nguyen <dangchau1991@yahoo.com>
315 # Date 1437048445 -10800
316 # Thu Jul 16 15:07:25 2015 +0300
317 # Branch REST
318 # Node ID c158adb21cb4a182c41b5ea7ff4ee0962ca28a6c
319 # Parent 9bcc917b3bb5798de2f54b79a7a48681734d395d
320 Added routing decorator
321
322 diff --git a/roundup/rest.py b/roundup/rest.py
323 --- a/roundup/rest.py
324 +++ b/roundup/rest.py
325 @@ -12,10 +12,12 @@
326 import sys
327 import time
328 import traceback
329 -import xml
330 +import re
331 +
332 from roundup import hyperdb
333 from roundup import date
334 from roundup.exceptions import *
335 +from roundup.cgi.exceptions import *
336
337
338 def _data_decorator(func):
339 @@ -24,6 +26,9 @@
340 # get the data / error from function
341 try:
342 code, data = func(self, *args, **kwargs)
343 + except NotFound, msg:
344 + code = 404
345 + data = msg
346 except IndexError, msg:
347 code = 404
348 data = msg
349 @@ -133,6 +138,84 @@
350 return result
351
352
353 +class Routing(object):
354 + __route_map = {}
355 + __var_to_regex = re.compile(r"<:(\w+)>")
356 + url_to_regex = r"([\w.\-~!$&'()*+,;=:@\%%]+)"
357 +
358 + @classmethod
359 + def route(cls, rule, methods='GET'):
360 + """A decorator that is used to register a view function for a
361 + given URL rule:
362 + @self.route('/')
363 + def index():
364 + return 'Hello World'
365 +
366 + rest/ will be added to the beginning of the url string
367 +
368 + Args:
369 + rule (string): the URL rule
370 + methods (string or tuple or list): the http method
371 + """
372 + # strip the '/' character from rule string
373 + rule = rule.strip('/')
374 +
375 + # add 'rest/' to the rule string
376 + if not rule.startswith('rest/'):
377 + rule = '^rest/' + rule + '$'
378 +
379 + if isinstance(methods, basestring): # convert string to tuple
380 + methods = (methods,)
381 + methods = set(item.upper() for item in methods)
382 +
383 + # convert a rule to a compiled regex object
384 + # so /data/<:class>/<:id> will become
385 + # /data/([charset]+)/([charset]+)
386 + # and extract the variable names to a list [(class), (id)]
387 + func_vars = cls.__var_to_regex.findall(rule)
388 + rule = re.compile(cls.__var_to_regex.sub(cls.url_to_regex, rule))
389 +
390 + # then we decorate it:
391 + # route_map[regex][method] = func
392 + def decorator(func):
393 + rule_route = cls.__route_map.get(rule, {})
394 + func_obj = {
395 + 'func': func,
396 + 'vars': func_vars
397 + }
398 + for method in methods:
399 + rule_route[method] = func_obj
400 + cls.__route_map[rule] = rule_route
401 + return func
402 + return decorator
403 +
404 + @classmethod
405 + def execute(cls, instance, path, method, input):
406 + # format the input
407 + path = path.strip('/').lower()
408 + method = method.upper()
409 +
410 + # find the rule match the path
411 + # then get handler match the method
412 + for path_regex in cls.__route_map:
413 + match_obj = path_regex.match(path)
414 + if match_obj:
415 + try:
416 + func_obj = cls.__route_map[path_regex][method]
417 + except KeyError:
418 + raise Reject('Method %s not allowed' % method)
419 +
420 + # retrieve the vars list and the function caller
421 + list_vars = func_obj['vars']
422 + func = func_obj['func']
423 +
424 + # zip the varlist into a dictionary, and pass it to the caller
425 + args = dict(zip(list_vars, match_obj.groups()))
426 + args['input'] = input
427 + return func(instance, **args)
428 + raise NotFound('Nothing matches the given URI')
429 +
430 +
431 class RestfulInstance(object):
432 """The RestfulInstance performs REST request from the client"""
433
434 @@ -280,6 +363,7 @@
435
436 return result
437
438 + @Routing.route("/data/<:class_name>", 'GET')
439 @_data_decorator
440 def get_collection(self, class_name, input):
441 """GET resource from class URI.
442 @@ -297,6 +381,8 @@
443 id: id of the object
444 link: path to the object
445 """
446 + if class_name not in self.db.classes:
447 + raise NotFound('Class %s not found' % class_name)
448 if not self.db.security.hasPermission(
449 'View', self.db.getuid(), class_name
450 ):
451 @@ -348,6 +434,7 @@
452 self.client.setHeader("X-Count-Total", str(len(result)))
453 return 200, result
454
455 + @Routing.route("/data/<:class_name>/<:item_id>", 'GET')
456 @_data_decorator
457 def get_element(self, class_name, item_id, input):
458 """GET resource from object URI.
459 @@ -368,6 +455,8 @@
460 link: link to the object
461 attributes: a dictionary represent the attributes of the object
462 """
463 + if class_name not in self.db.classes:
464 + raise NotFound('Class %s not found' % class_name)
465 if not self.db.security.hasPermission(
466 'View', self.db.getuid(), class_name, itemid=item_id
467 ):
468 @@ -394,6 +483,7 @@
469
470 return 200, result
471
472 + @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'GET')
473 @_data_decorator
474 def get_attribute(self, class_name, item_id, attr_name, input):
475 """GET resource from attribute URI.
476 @@ -415,6 +505,8 @@
477 link: link to the attribute
478 data: data of the requested attribute
479 """
480 + if class_name not in self.db.classes:
481 + raise NotFound('Class %s not found' % class_name)
482 if not self.db.security.hasPermission(
483 'View', self.db.getuid(), class_name, attr_name, item_id
484 ):
485 @@ -435,6 +527,7 @@
486
487 return 200, result
488
489 + @Routing.route("/data/<:class_name>", 'POST')
490 @_data_decorator
491 def post_collection(self, class_name, input):
492 """POST a new object to a class
493 @@ -452,6 +545,8 @@
494 id: id of the object
495 link: path to the object
496 """
497 + if class_name not in self.db.classes:
498 + raise NotFound('Class %s not found' % class_name)
499 if not self.db.security.hasPermission(
500 'Create', self.db.getuid(), class_name
501 ):
502 @@ -495,21 +590,7 @@
503 }
504 return 201, result
505
506 - @_data_decorator
507 - def post_element(self, class_name, item_id, input):
508 - """POST to an object of a class is not allowed"""
509 - raise Reject('POST to an item is not allowed')
510 -
511 - @_data_decorator
512 - def post_attribute(self, class_name, item_id, attr_name, input):
513 - """POST to an attribute of an object is not allowed"""
514 - raise Reject('POST to an attribute is not allowed')
515 -
516 - @_data_decorator
517 - def put_collection(self, class_name, input):
518 - """PUT a class is not allowed"""
519 - raise Reject('PUT a class is not allowed')
520 -
521 + @Routing.route("/data/<:class_name>/<:item_id>", 'PUT')
522 @_data_decorator
523 def put_element(self, class_name, item_id, input):
524 """PUT a new content to an object
525 @@ -530,6 +611,8 @@
526 attributes: a dictionary represent only changed attributes of
527 the object
528 """
529 + if class_name not in self.db.classes:
530 + raise NotFound('Class %s not found' % class_name)
531 class_obj = self.db.getclass(class_name)
532
533 props = self.props_from_args(class_obj, input.value, item_id)
534 @@ -555,6 +638,7 @@
535 }
536 return 200, result
537
538 + @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'PUT')
539 @_data_decorator
540 def put_attribute(self, class_name, item_id, attr_name, input):
541 """PUT an attribute to an object
542 @@ -574,6 +658,8 @@
543 attributes: a dictionary represent only changed attributes of
544 the object
545 """
546 + if class_name not in self.db.classes:
547 + raise NotFound('Class %s not found' % class_name)
548 if not self.db.security.hasPermission(
549 'Edit', self.db.getuid(), class_name, attr_name, item_id
550 ):
551 @@ -604,6 +690,7 @@
552
553 return 200, result
554
555 + @Routing.route("/data/<:class_name>", 'DELETE')
556 @_data_decorator
557 def delete_collection(self, class_name, input):
558 """DELETE all objects in a class
559 @@ -618,6 +705,8 @@
560 status (string): 'ok'
561 count (int): number of deleted objects
562 """
563 + if class_name not in self.db.classes:
564 + raise NotFound('Class %s not found' % class_name)
565 if not self.db.security.hasPermission(
566 'Delete', self.db.getuid(), class_name
567 ):
568 @@ -644,6 +733,7 @@
569
570 return 200, result
571
572 + @Routing.route("/data/<:class_name>/<:item_id>", 'DELETE')
573 @_data_decorator
574 def delete_element(self, class_name, item_id, input):
575 """DELETE an object in a class
576 @@ -658,6 +748,8 @@
577 dict:
578 status (string): 'ok'
579 """
580 + if class_name not in self.db.classes:
581 + raise NotFound('Class %s not found' % class_name)
582 if not self.db.security.hasPermission(
583 'Delete', self.db.getuid(), class_name, itemid=item_id
584 ):
585 @@ -673,6 +765,7 @@
586
587 return 200, result
588
589 + @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'DELETE')
590 @_data_decorator
591 def delete_attribute(self, class_name, item_id, attr_name, input):
592 """DELETE an attribute in a object by setting it to None or empty
593 @@ -688,6 +781,8 @@
594 dict:
595 status (string): 'ok'
596 """
597 + if class_name not in self.db.classes:
598 + raise NotFound('Class %s not found' % class_name)
599 if not self.db.security.hasPermission(
600 'Edit', self.db.getuid(), class_name, attr_name, item_id
601 ):
602 @@ -716,11 +811,7 @@
603
604 return 200, result
605
606 - @_data_decorator
607 - def patch_collection(self, class_name, input):
608 - """PATCH a class is not allowed"""
609 - raise Reject('PATCH a class is not allowed')
610 -
611 + @Routing.route("/data/<:class_name>/<:item_id>", 'PATCH')
612 @_data_decorator
613 def patch_element(self, class_name, item_id, input):
614 """PATCH an object
615 @@ -744,6 +835,8 @@
616 attributes: a dictionary represent only changed attributes of
617 the object
618 """
619 + if class_name not in self.db.classes:
620 + raise NotFound('Class %s not found' % class_name)
621 try:
622 op = input['op'].value.lower()
623 except KeyError:
624 @@ -779,6 +872,7 @@
625 }
626 return 200, result
627
628 + @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'PATCH')
629 @_data_decorator
630 def patch_attribute(self, class_name, item_id, attr_name, input):
631 """PATCH an attribute of an object
632 @@ -803,6 +897,8 @@
633 attributes: a dictionary represent only changed attributes of
634 the object
635 """
636 + if class_name not in self.db.classes:
637 + raise NotFound('Class %s not found' % class_name)
638 try:
639 op = input['op'].value.lower()
640 except KeyError:
641 @@ -842,6 +938,7 @@
642 }
643 return 200, result
644
645 + @Routing.route("/data/<:class_name>", 'OPTIONS')
646 @_data_decorator
647 def options_collection(self, class_name, input):
648 """OPTION return the HTTP Header for the class uri
649 @@ -850,8 +947,11 @@
650 int: http status code 204 (No content)
651 body (string): an empty string
652 """
653 + if class_name not in self.db.classes:
654 + raise NotFound('Class %s not found' % class_name)
655 return 204, ""
656
657 + @Routing.route("/data/<:class_name>/<:item_id>", 'OPTIONS')
658 @_data_decorator
659 def options_element(self, class_name, item_id, input):
660 """OPTION return the HTTP Header for the object uri
661 @@ -860,12 +960,15 @@
662 int: http status code 204 (No content)
663 body (string): an empty string
664 """
665 + if class_name not in self.db.classes:
666 + raise NotFound('Class %s not found' % class_name)
667 self.client.setHeader(
668 "Accept-Patch",
669 "application/x-www-form-urlencoded, multipart/form-data"
670 )
671 return 204, ""
672
673 + @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'OPTIONS')
674 @_data_decorator
675 def option_attribute(self, class_name, item_id, attr_name, input):
676 """OPTION return the HTTP Header for the attribute uri
677 @@ -874,12 +977,15 @@
678 int: http status code 204 (No content)
679 body (string): an empty string
680 """
681 + if class_name not in self.db.classes:
682 + raise NotFound('Class %s not found' % class_name)
683 self.client.setHeader(
684 "Accept-Patch",
685 "application/x-www-form-urlencoded, multipart/form-data"
686 )
687 return 204, ""
688
689 + @Routing.route("/summary")
690 @_data_decorator
691 def summary(self, input):
692 """Get a summary of resource from class URI.
693 @@ -983,44 +1089,13 @@
694 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
695 )
696
697 - # PATH is split to multiple pieces
698 - # 0 - rest
699 - # 1 - data
700 - # 2 - resource
701 - # 3 - attribute
702 - uri_split = uri.lower().split("/")
703 -
704 # Call the appropriate method
705 - if len(uri_split) == 2 and uri_split[1] == 'summary':
706 - output = self.summary(input)
707 - elif 4 >= len(uri_split) > 2 and uri_split[1] == 'data':
708 - resource_uri = uri_split[2]
709 - try:
710 - class_name, item_id = hyperdb.splitDesignator(resource_uri)
711 - except hyperdb.DesignatorError:
712 - class_name = resource_uri
713 - item_id = None
714 -
715 - if class_name not in self.db.classes:
716 - output = self.error_obj(404, "Not found")
717 - elif item_id is None:
718 - if len(uri_split) == 3:
719 - output = getattr(
720 - self, "%s_collection" % method.lower()
721 - )(class_name, input)
722 - else:
723 - output = self.error_obj(404, "Not found")
724 - else:
725 - if len(uri_split) == 3:
726 - output = getattr(
727 - self, "%s_element" % method.lower()
728 - )(class_name, item_id, input)
729 - else:
730 - output = getattr(
731 - self, "%s_attribute" % method.lower()
732 - )(class_name, item_id, uri_split[3], input)
733 - else:
734 - output = self.error_obj(404, "Not found")
735 + try:
736 + output = Routing.execute(self, uri, method, input)
737 + except NotFound, msg:
738 + output = self.error_obj(404, msg)
739 + except Reject, msg:
740 + output = self.error_obj(405, msg)
741
742 # Format the content type
743 if data_type.lower() == "json":
744 # HG changeset patch
745 # User Chau Nguyen <dangchau1991@yahoo.com>
746 # Date 1436781974 -10800
747 # Mon Jul 13 13:06:14 2015 +0300
748 # Branch REST
749 # Node ID 9bcc917b3bb5798de2f54b79a7a48681734d395d
750 # Parent 0acc1d03c544871964a21908ab20d24d7507ef2e
751 Added summary page,
752 Change data uri of class methods from /rest/class to /rest/data/class
753
754 diff --git a/roundup/rest.py b/roundup/rest.py
755 --- a/roundup/rest.py
756 +++ b/roundup/rest.py
757 @@ -14,6 +14,7 @@
758 import traceback
759 import xml
760 from roundup import hyperdb
761 +from roundup import date
762 from roundup.exceptions import *
763
764
765 @@ -70,6 +71,7 @@
766 return result
767 return format_object
768
769 +
770 def parse_accept_header(accept):
771 """
772 Parse the Accept header *accept*, returning a list with 3-tuples of
773 @@ -111,7 +113,7 @@
774 version = media_params.append(('version',
775 float(rest)))
776 except ValueError:
777 - version = 1.0 # could not be parsed
778 + version = 1.0 # could not be parsed
779 # add the vendor code as a media param
780 media_params.append(('vendor', vnd))
781 # and re-write media_type to something like application/json so
782 @@ -130,6 +132,7 @@
783 result.sort(lambda x, y: -cmp(x[2], y[2]))
784 return result
785
786 +
787 class RestfulInstance(object):
788 """The RestfulInstance performs REST request from the client"""
789
790 @@ -877,6 +880,68 @@
791 )
792 return 204, ""
793
794 + @_data_decorator
795 + def summary(self, input):
796 + """Get a summary of resource from class URI.
797 +
798 + This function returns only items have View permission
799 + class_name should be valid already
800 +
801 + Args:
802 + class_name (string): class name of the resource (Ex: issue, msg)
803 + input (list): the submitted form of the user
804 +
805 + Returns:
806 + int: http status code 200 (OK)
807 + list:
808 + """
809 + if not self.db.security.hasPermission(
810 + 'View', self.db.getuid(), 'issue'
811 + ) and not self.db.security.hasPermission(
812 + 'View', self.db.getuid(), 'status'
813 + ) and not self.db.security.hasPermission(
814 + 'View', self.db.getuid(), 'issue'
815 + ):
816 + raise Unauthorised('Permission to view summary denied')
817 +
818 + old = date.Date('-1w')
819 +
820 + created = []
821 + summary = {}
822 + messages = []
823 +
824 + # loop through all the recently-active issues
825 + for issue_id in self.db.issue.filter(None, {'activity': '-1w;'}):
826 + num = 0
827 + status_name = self.db.status.get(
828 + self.db.issue.get(issue_id, 'status'),
829 + 'name'
830 + )
831 + issue_object = {
832 + 'id': issue_id,
833 + 'link': self.base_path + 'issue' + issue_id,
834 + 'title': self.db.issue.get(issue_id, 'title')
835 + }
836 + for x, ts, uid, action, data in self.db.issue.history(issue_id):
837 + if ts < old:
838 + continue
839 + if action == 'create':
840 + created.append(issue_object)
841 + elif action == 'set' and 'messages' in data:
842 + num += 1
843 + summary.setdefault(status_name, []).append(issue_object)
844 + messages.append((num, issue_object))
845 +
846 + messages.sort(reverse=True)
847 +
848 + result = {
849 + 'created': created,
850 + 'summary': summary,
851 + 'most_discussed': messages[:10]
852 + }
853 +
854 + return 200, result
855 +
856 def dispatch(self, method, uri, input):
857 """format and process the request"""
858 # if X-HTTP-Method-Override is set, follow the override method
859 @@ -920,34 +985,42 @@
860
861 # PATH is split to multiple pieces
862 # 0 - rest
863 - # 1 - resource
864 - # 2 - attribute
865 - uri_split = uri.split("/")
866 - resource_uri = uri_split[1]
867 -
868 - try:
869 - class_name, item_id = hyperdb.splitDesignator(resource_uri)
870 - except hyperdb.DesignatorError:
871 - class_name = resource_uri
872 - item_id = None
873 + # 1 - data
874 + # 2 - resource
875 + # 3 - attribute
876 + uri_split = uri.lower().split("/")
877
878 # Call the appropriate method
879 - if (class_name not in self.db.classes) or (len(uri_split) > 3):
880 + if len(uri_split) == 2 and uri_split[1] == 'summary':
881 + output = self.summary(input)
882 + elif 4 >= len(uri_split) > 2 and uri_split[1] == 'data':
883 + resource_uri = uri_split[2]
884 + try:
885 + class_name, item_id = hyperdb.splitDesignator(resource_uri)
886 + except hyperdb.DesignatorError:
887 + class_name = resource_uri
888 + item_id = None
889 +
890 + if class_name not in self.db.classes:
891 + output = self.error_obj(404, "Not found")
892 + elif item_id is None:
893 + if len(uri_split) == 3:
894 + output = getattr(
895 + self, "%s_collection" % method.lower()
896 + )(class_name, input)
897 + else:
898 + output = self.error_obj(404, "Not found")
899 + else:
900 + if len(uri_split) == 3:
901 + output = getattr(
902 + self, "%s_element" % method.lower()
903 + )(class_name, item_id, input)
904 + else:
905 + output = getattr(
906 + self, "%s_attribute" % method.lower()
907 + )(class_name, item_id, uri_split[3], input)
908 + else:
909 output = self.error_obj(404, "Not found")
910 - elif item_id is None:
911 - if len(uri_split) == 2:
912 - output = getattr(
913 - self, "%s_collection" % method.lower()
914 - )(class_name, input)
915 - else:
916 - if len(uri_split) == 2:
917 - output = getattr(
918 - self, "%s_element" % method.lower()
919 - )(class_name, item_id, input)
920 - else:
921 - output = getattr(
922 - self, "%s_attribute" % method.lower()
923 - )(class_name, item_id, uri_split[2], input)
924
925 # Format the content type
926 if data_type.lower() == "json":
927 # HG changeset patch
928 # User Chau Nguyen <dangchau1991@yahoo.com>
929 # Date 1436539349 -10800
930 # Fri Jul 10 17:42:29 2015 +0300
931 # Branch REST
932 # Node ID 0acc1d03c544871964a21908ab20d24d7507ef2e
933 # Parent 0b4d67336a900a5f74d9f8320bffc06d435e4ca8
934 Handle operation for patch separately,
935 Patch remove operation is now able to remove element from list and dict,
936 Added more test on new changes
937
938 diff --git a/roundup/rest.py b/roundup/rest.py
939 --- a/roundup/rest.py
940 +++ b/roundup/rest.py
941 @@ -12,6 +12,7 @@
942 import sys
943 import time
944 import traceback
945 +import xml
946 from roundup import hyperdb
947 from roundup.exceptions import *
948
949 @@ -223,6 +224,59 @@
950
951 return result
952
953 + def patch_data(self, op, old_val, new_val):
954 + """Perform patch operation based on old_val and new_val
955 +
956 + Args:
957 + op (string): PATCH operation: add, replace, remove
958 + old_val: old value of the property
959 + new_val: new value of the property
960 +
961 + Returns:
962 + result (string): value after performed the operation
963 + """
964 + # add operation: If neither of the value is None, use the other one
965 + # Otherwise, concat those 2 value
966 + if op == 'add':
967 + if old_val is None:
968 + result = new_val
969 + elif new_val is None:
970 + result = old_val
971 + else:
972 + result = old_val + new_val
973 + # Replace operation: new value is returned
974 + elif op == 'replace':
975 + result = new_val
976 + # Remove operation:
977 + # if old_val is not a list/dict, change it to None
978 + # if old_val is a list/dict, but the parameter is empty,
979 + # change it to none
980 + # if old_val is a list/dict, and parameter is not empty
981 + # proceed to remove the values from parameter from the list/dict
982 + elif op == 'remove':
983 + if isinstance(old_val, list):
984 + if new_val is None:
985 + result = []
986 + elif isinstance(new_val, list):
987 + result = [x for x in old_val if x not in new_val]
988 + else:
989 + if new_val in old_val:
990 + old_val.remove(new_val)
991 + elif isinstance(old_val, dict):
992 + if new_val is None:
993 + result = {}
994 + elif isinstance(new_val, dict):
995 + for x in new_val:
996 + old_val.pop(x, None)
997 + else:
998 + old_val.pop(new_val, None)
999 + else:
1000 + result = None
1001 + else:
1002 + raise UsageError('PATCH Operation %s is not allowed' % op)
1003 +
1004 + return result
1005 +
1006 @_data_decorator
1007 def get_collection(self, class_name, input):
1008 """GET resource from class URI.
1009 @@ -704,18 +758,9 @@
1010 (prop, class_name, item_id)
1011 )
1012
1013 - if op == 'add':
1014 - props[prop] = class_obj.get(item_id, prop) + props[prop]
1015 - elif op == 'replace':
1016 - pass
1017 - elif op == 'remove':
1018 - current_prop = class_obj.get(item_id, prop)
1019 - if isinstance(current_prop, list):
1020 - props[prop] = []
1021 - else:
1022 - props[prop] = None
1023 - else:
1024 - raise UsageError('PATCH Operation %s is not allowed' % op)
1025 + props[prop] = self.patch_data(
1026 + op, class_obj.get(item_id, prop), props[prop]
1027 + )
1028
1029 try:
1030 result = class_obj.set(item_id, **props)
1031 @@ -776,18 +821,9 @@
1032 )
1033 }
1034
1035 - if op == 'add':
1036 - props[prop] = class_obj.get(item_id, prop) + props[prop]
1037 - elif op == 'replace':
1038 - pass
1039 - elif op == 'remove':
1040 - current_prop = class_obj.get(item_id, prop)
1041 - if isinstance(current_prop, list):
1042 - props[prop] = []
1043 - else:
1044 - props[prop] = None
1045 - else:
1046 - raise UsageError('PATCH Operation %s is not allowed' % op)
1047 + props[prop] = self.patch_data(
1048 + op, class_obj.get(item_id, prop), props[prop]
1049 + )
1050
1051 try:
1052 result = class_obj.set(item_id, **props)
1053 diff --git a/test/test_rest.py b/test/test_rest.py
1054 --- a/test/test_rest.py
1055 +++ b/test/test_rest.py
1056 @@ -440,7 +440,7 @@
1057 """
1058 Test Patch Action 'Remove'
1059 """
1060 - # create a new issue with userid 1 in the nosy list
1061 + # create a new issue with userid 1 and 2 in the nosy list
1062 issue_id = self.db.issue.create(title='foo', nosy=['1', '2'])
1063
1064 # remove the nosy list and the title
1065 @@ -461,6 +461,29 @@
1066 self.assertEqual(len(results['attributes']['nosy']), 0)
1067 self.assertEqual(results['attributes']['nosy'], [])
1068
1069 + def testPatchRemove(self):
1070 + """
1071 + Test Patch Action 'Remove' only some element from a list
1072 + """
1073 + # create a new issue with userid 1, 2, 3 in the nosy list
1074 + issue_id = self.db.issue.create(title='foo', nosy=['1', '2', '3'])
1075 +
1076 + # remove the nosy list and the title
1077 + form = cgi.FieldStorage()
1078 + form.list = [
1079 + cgi.MiniFieldStorage('op', 'remove'),
1080 + cgi.MiniFieldStorage('nosy', '1, 2'),
1081 + ]
1082 + results = self.server.patch_element('issue', issue_id, form)
1083 + self.assertEqual(self.dummy_client.response_code, 200)
1084 +
1085 + # verify the result
1086 + results = self.server.get_element('issue', issue_id, self.empty_form)
1087 + results = results['data']
1088 + self.assertEqual(self.dummy_client.response_code, 200)
1089 + self.assertEqual(len(results['attributes']['nosy']), 1)
1090 + self.assertEqual(results['attributes']['nosy'], ['3'])
1091 +
1092
1093 def get_obj(path, id):
1094 return {
1095 # HG changeset patch
1096 # User Chau Nguyen <dangchau1991@yahoo.com>
1097 # Date 1436457297 -10800
1098 # Thu Jul 09 18:54:57 2015 +0300
1099 # Branch REST
1100 # Node ID 0b4d67336a900a5f74d9f8320bffc06d435e4ca8
1101 # Parent 0bffcf76f2e10d936e79c27a2f1a42bf9a12cc12
1102 Added ability to parse HTTP accept header to serve the content type correctly
1103
1104 diff --git a/roundup/rest.py b/roundup/rest.py
1105 --- a/roundup/rest.py
1106 +++ b/roundup/rest.py
1107 @@ -69,11 +69,76 @@
1108 return result
1109 return format_object
1110
1111 +def parse_accept_header(accept):
1112 + """
1113 + Parse the Accept header *accept*, returning a list with 3-tuples of
1114 + [(str(media_type), dict(params), float(q_value)),] ordered by q values.
1115 +
1116 + If the accept header includes vendor-specific types like::
1117 + application/vnd.yourcompany.yourproduct-v1.1+json
1118 +
1119 + It will actually convert the vendor and version into parameters and
1120 + convert the content type into `application/json` so appropriate content
1121 + negotiation decisions can be made.
1122 +
1123 + Default `q` for values that are not specified is 1.0
1124 +
1125 + # Based on https://gist.github.com/samuraisam/2714195
1126 + # Also, based on a snipped found in this project:
1127 + # https://github.com/martinblech/mimerender
1128 + """
1129 + result = []
1130 + for media_range in accept.split(","):
1131 + parts = media_range.split(";")
1132 + media_type = parts.pop(0).strip()
1133 + media_params = []
1134 + # convert vendor-specific content types into something useful (see
1135 + # docstring)
1136 + typ, subtyp = media_type.split('/')
1137 + # check for a + in the sub-type
1138 + if '+' in subtyp:
1139 + # if it exists, determine if the subtype is a vendor-specific type
1140 + vnd, sep, extra = subtyp.partition('+')
1141 + if vnd.startswith('vnd'):
1142 + # and then... if it ends in something like "-v1.1" parse the
1143 + # version out
1144 + if '-v' in vnd:
1145 + vnd, sep, rest = vnd.rpartition('-v')
1146 + if len(rest):
1147 + # add the version as a media param
1148 + try:
1149 + version = media_params.append(('version',
1150 + float(rest)))
1151 + except ValueError:
1152 + version = 1.0 # could not be parsed
1153 + # add the vendor code as a media param
1154 + media_params.append(('vendor', vnd))
1155 + # and re-write media_type to something like application/json so
1156 + # it can be used usefully when looking up emitters
1157 + media_type = '{}/{}'.format(typ, extra)
1158 + q = 1.0
1159 + for part in parts:
1160 + (key, value) = part.lstrip().split("=", 1)
1161 + key = key.strip()
1162 + value = value.strip()
1163 + if key == "q":
1164 + q = float(value)
1165 + else:
1166 + media_params.append((key, value))
1167 + result.append((media_type, dict(media_params), q))
1168 + result.sort(lambda x, y: -cmp(x[2], y[2]))
1169 + return result
1170
1171 class RestfulInstance(object):
1172 """The RestfulInstance performs REST request from the client"""
1173
1174 __default_patch_op = "replace" # default operator for PATCH method
1175 + __accepted_content_type = {
1176 + "application/json": "json",
1177 + "*/*": "json"
1178 + # "application/xml": "xml"
1179 + }
1180 + __default_accept_type = "json"
1181
1182 def __init__(self, client, db):
1183 self.client = client
1184 @@ -778,26 +843,23 @@
1185
1186 def dispatch(self, method, uri, input):
1187 """format and process the request"""
1188 - # PATH is split to multiple pieces
1189 - # 0 - rest
1190 - # 1 - resource
1191 - # 2 - attribute
1192 - uri_split = uri.split("/")
1193 - resource_uri = uri_split[1]
1194 -
1195 # if X-HTTP-Method-Override is set, follow the override method
1196 headers = self.client.request.headers
1197 method = headers.getheader('X-HTTP-Method-Override') or method
1198
1199 + # parse Accept header and get the content type
1200 + accept_header = parse_accept_header(headers.getheader('Accept'))
1201 + accept_type = "invalid"
1202 + for part in accept_header:
1203 + if part[0] in self.__accepted_content_type:
1204 + accept_type = self.__accepted_content_type[part[0]]
1205 +
1206 # get the request format for response
1207 # priority : extension from uri (/rest/issue.json),
1208 # header (Accept: application/json, application/xml)
1209 # default (application/json)
1210 -
1211 - # format_header need a priority parser
1212 ext_type = os.path.splitext(urlparse.urlparse(uri).path)[1][1:]
1213 - accept_header = headers.getheader('Accept')[12:]
1214 - data_type = ext_type or accept_header or "json"
1215 + data_type = ext_type or accept_type or self.__default_accept_type
1216
1217 # check for pretty print
1218 try:
1219 @@ -819,6 +881,14 @@
1220 "Access-Control-Allow-Methods",
1221 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
1222 )
1223 +
1224 + # PATH is split to multiple pieces
1225 + # 0 - rest
1226 + # 1 - resource
1227 + # 2 - attribute
1228 + uri_split = uri.split("/")
1229 + resource_uri = uri_split[1]
1230 +
1231 try:
1232 class_name, item_id = hyperdb.splitDesignator(resource_uri)
1233 except hyperdb.DesignatorError:
1234 # HG changeset patch
1235 # User Chau Nguyen <dangchau1991@yahoo.com>
1236 # Date 1436437290 -10800
1237 # Thu Jul 09 13:21:30 2015 +0300
1238 # Branch REST
1239 # Node ID 0bffcf76f2e10d936e79c27a2f1a42bf9a12cc12
1240 # Parent c1bb6dbb6430e1c0868a9bf2bd9e63087efce23a
1241 Added support to print error to output when server DEBUG_MODE is true,
1242 some coding improvements
1243
1244 diff --git a/roundup/rest.py b/roundup/rest.py
1245 --- a/roundup/rest.py
1246 +++ b/roundup/rest.py
1247 @@ -43,12 +43,14 @@
1248 except:
1249 exc, val, tb = sys.exc_info()
1250 code = 400
1251 - # if self.DEBUG_MODE in roundup_server
1252 - # else data = 'An error occurred. Please check...',
1253 - data = val
1254 -
1255 + ts = time.ctime()
1256 + if self.client.request.DEBUG_MODE:
1257 + data = val
1258 + else:
1259 + data = '%s: An error occurred. Please check the server log' \
1260 + ' for more information.' % ts
1261 # out to the logfile
1262 - print 'EXCEPTION AT', time.ctime()
1263 + print 'EXCEPTION AT', ts
1264 traceback.print_exc()
1265
1266 # decorate it
1267 @@ -71,6 +73,8 @@
1268 class RestfulInstance(object):
1269 """The RestfulInstance performs REST request from the client"""
1270
1271 + __default_patch_op = "replace" # default operator for PATCH method
1272 +
1273 def __init__(self, client, db):
1274 self.client = client
1275 self.db = db
1276 @@ -621,7 +625,7 @@
1277 try:
1278 op = input['op'].value.lower()
1279 except KeyError:
1280 - op = "replace"
1281 + op = self.__default_patch_op
1282 class_obj = self.db.getclass(class_name)
1283
1284 props = self.props_from_args(class_obj, input.value, item_id)
1285 @@ -689,8 +693,7 @@
1286 try:
1287 op = input['op'].value.lower()
1288 except KeyError:
1289 - op = "replace"
1290 - class_obj = self.db.getclass(class_name)
1291 + op = self.__default_patch_op
1292
1293 if not self.db.security.hasPermission(
1294 'Edit', self.db.getuid(), class_name, attr_name, item_id
1295 @@ -818,7 +821,7 @@
1296 )
1297 try:
1298 class_name, item_id = hyperdb.splitDesignator(resource_uri)
1299 - except hyperdb.DesignatorError, msg:
1300 + except hyperdb.DesignatorError:
1301 class_name = resource_uri
1302 item_id = None
1303
1304 # HG changeset patch
1305 # User Chau Nguyen <dangchau1991@yahoo.com>
1306 # Date 1436393721 -10800
1307 # Thu Jul 09 01:15:21 2015 +0300
1308 # Branch REST
1309 # Node ID c1bb6dbb6430e1c0868a9bf2bd9e63087efce23a
1310 # Parent 2f8030bc3227e81677bdc46f84657aa4b29a38f6
1311 Add unittest for pagination and filtering
1312
1313 diff --git a/test/test_rest.py b/test/test_rest.py
1314 --- a/test/test_rest.py
1315 +++ b/test/test_rest.py
1316 @@ -8,6 +8,7 @@
1317 from roundup.rest import RestfulInstance
1318 from roundup.backends import list_backends
1319 from roundup.cgi import client
1320 +import random
1321
1322 import db_test_base
1323
1324 @@ -93,6 +94,149 @@
1325 self.assertEqual(self.dummy_client.response_code, 200)
1326 self.assertEqual(results['data']['data'], 'joe')
1327
1328 + def testFilter(self):
1329 + """
1330 + Retrieve all three users
1331 + obtain data for 'joe'
1332 + """
1333 + # create sample data
1334 + try:
1335 + self.db.status.create(name='open')
1336 + except ValueError:
1337 + pass
1338 + try:
1339 + self.db.status.create(name='closed')
1340 + except ValueError:
1341 + pass
1342 + try:
1343 + self.db.priority.create(name='normal')
1344 + except ValueError:
1345 + pass
1346 + try:
1347 + self.db.priority.create(name='critical')
1348 + except ValueError:
1349 + pass
1350 + self.db.issue.create(
1351 + title='foo4',
1352 + status=self.db.status.lookup('closed'),
1353 + priority=self.db.priority.lookup('critical')
1354 + )
1355 + self.db.issue.create(
1356 + title='foo1',
1357 + status=self.db.status.lookup('open'),
1358 + priority=self.db.priority.lookup('normal')
1359 + )
1360 + issue_open_norm = self.db.issue.create(
1361 + title='foo2',
1362 + status=self.db.status.lookup('open'),
1363 + priority=self.db.priority.lookup('normal')
1364 + )
1365 + issue_closed_norm = self.db.issue.create(
1366 + title='foo3',
1367 + status=self.db.status.lookup('closed'),
1368 + priority=self.db.priority.lookup('normal')
1369 + )
1370 + issue_closed_crit = self.db.issue.create(
1371 + title='foo4',
1372 + status=self.db.status.lookup('closed'),
1373 + priority=self.db.priority.lookup('critical')
1374 + )
1375 + issue_open_crit = self.db.issue.create(
1376 + title='foo5',
1377 + status=self.db.status.lookup('open'),
1378 + priority=self.db.priority.lookup('critical')
1379 + )
1380 + base_path = self.dummy_client.env['PATH_INFO'] + 'issue'
1381 +
1382 + # Retrieve all issue status=open
1383 + form = cgi.FieldStorage()
1384 + form.list = [
1385 + cgi.MiniFieldStorage('where_status', 'open')
1386 + ]
1387 + results = self.server.get_collection('issue', form)
1388 + self.assertEqual(self.dummy_client.response_code, 200)
1389 + self.assertIn(get_obj(base_path, issue_open_norm), results['data'])
1390 + self.assertIn(get_obj(base_path, issue_open_crit), results['data'])
1391 + self.assertNotIn(
1392 + get_obj(base_path, issue_closed_norm), results['data']
1393 + )
1394 +
1395 + # Retrieve all issue status=closed and priority=critical
1396 + form = cgi.FieldStorage()
1397 + form.list = [
1398 + cgi.MiniFieldStorage('where_status', 'closed'),
1399 + cgi.MiniFieldStorage('where_priority', 'critical')
1400 + ]
1401 + results = self.server.get_collection('issue', form)
1402 + self.assertEqual(self.dummy_client.response_code, 200)
1403 + self.assertIn(get_obj(base_path, issue_closed_crit), results['data'])
1404 + self.assertNotIn(get_obj(base_path, issue_open_crit), results['data'])
1405 + self.assertNotIn(
1406 + get_obj(base_path, issue_closed_norm), results['data']
1407 + )
1408 +
1409 + # Retrieve all issue status=closed and priority=normal,critical
1410 + form = cgi.FieldStorage()
1411 + form.list = [
1412 + cgi.MiniFieldStorage('where_status', 'closed'),
1413 + cgi.MiniFieldStorage('where_priority', 'normal,critical')
1414 + ]
1415 + results = self.server.get_collection('issue', form)
1416 + self.assertEqual(self.dummy_client.response_code, 200)
1417 + self.assertIn(get_obj(base_path, issue_closed_crit), results['data'])
1418 + self.assertIn(get_obj(base_path, issue_closed_norm), results['data'])
1419 + self.assertNotIn(get_obj(base_path, issue_open_crit), results['data'])
1420 + self.assertNotIn(get_obj(base_path, issue_open_norm), results['data'])
1421 +
1422 + def testPagination(self):
1423 + """
1424 + Retrieve all three users
1425 + obtain data for 'joe'
1426 + """
1427 + # create sample data
1428 + for i in range(0, random.randint(5, 10)):
1429 + self.db.issue.create(title='foo' + str(i))
1430 +
1431 + # Retrieving all the issues
1432 + results = self.server.get_collection('issue', self.empty_form)
1433 + self.assertEqual(self.dummy_client.response_code, 200)
1434 + total_length = len(results['data'])
1435 +
1436 + # Pagination will be 70% of the total result
1437 + page_size = total_length * 70 // 100
1438 + page_zero_expected = page_size
1439 + page_one_expected = total_length - page_zero_expected
1440 +
1441 + # Retrieve page 0
1442 + form = cgi.FieldStorage()
1443 + form.list = [
1444 + cgi.MiniFieldStorage('page_size', page_size),
1445 + cgi.MiniFieldStorage('page_index', 0)
1446 + ]
1447 + results = self.server.get_collection('issue', form)
1448 + self.assertEqual(self.dummy_client.response_code, 200)
1449 + self.assertEqual(len(results['data']), page_zero_expected)
1450 +
1451 + # Retrieve page 1
1452 + form = cgi.FieldStorage()
1453 + form.list = [
1454 + cgi.MiniFieldStorage('page_size', page_size),
1455 + cgi.MiniFieldStorage('page_index', 1)
1456 + ]
1457 + results = self.server.get_collection('issue', form)
1458 + self.assertEqual(self.dummy_client.response_code, 200)
1459 + self.assertEqual(len(results['data']), page_one_expected)
1460 +
1461 + # Retrieve page 2
1462 + form = cgi.FieldStorage()
1463 + form.list = [
1464 + cgi.MiniFieldStorage('page_size', page_size),
1465 + cgi.MiniFieldStorage('page_index', 2)
1466 + ]
1467 + results = self.server.get_collection('issue', form)
1468 + self.assertEqual(self.dummy_client.response_code, 200)
1469 + self.assertEqual(len(results['data']), 0)
1470 +
1471 def testPut(self):
1472 """
1473 Change joe's 'realname'
1474 @@ -318,6 +462,13 @@
1475 self.assertEqual(results['attributes']['nosy'], [])
1476
1477
1478 +def get_obj(path, id):
1479 + return {
1480 + 'id': id,
1481 + 'link': path + id
1482 + }
1483 +
1484 +
1485 def test_suite():
1486 suite = unittest.TestSuite()
1487 for l in list_backends():
1488 # HG changeset patch
1489 # User Chau Nguyen <dangchau1991@yahoo.com>
1490 # Date 1436379655 -10800
1491 # Wed Jul 08 21:20:55 2015 +0300
1492 # Branch REST
1493 # Node ID 2f8030bc3227e81677bdc46f84657aa4b29a38f6
1494 # Parent 3b248d4d85df162296da34335814040631a6e037
1495 Added filtering and pagination,
1496 Adjust unittest to use empty cgi formfield instead of empty dict
1497
1498 diff --git a/roundup/rest.py b/roundup/rest.py
1499 --- a/roundup/rest.py
1500 +++ b/roundup/rest.py
1501 @@ -178,13 +178,47 @@
1502
1503 class_obj = self.db.getclass(class_name)
1504 class_path = self.base_path + class_name
1505 +
1506 + # Handle filtering and pagination
1507 + filter_props = {}
1508 + page = {
1509 + 'size': None,
1510 + 'index': None
1511 + }
1512 + for form_field in input.value:
1513 + key = form_field.name
1514 + value = form_field.value
1515 + if key.startswith("where_"): # serve the filter purpose
1516 + key = key[6:]
1517 + filter_props[key] = [
1518 + getattr(self.db, key).lookup(p)
1519 + for p in value.split(",")
1520 + ]
1521 + elif key.startswith("page_"): # serve the paging purpose
1522 + key = key[5:]
1523 + value = int(value)
1524 + page[key] = value
1525 +
1526 + if not filter_props:
1527 + obj_list = class_obj.list()
1528 + else:
1529 + obj_list = class_obj.filter(None, filter_props)
1530 +
1531 + # extract result from data
1532 result = [
1533 {'id': item_id, 'link': class_path + item_id}
1534 - for item_id in class_obj.list()
1535 + for item_id in obj_list
1536 if self.db.security.hasPermission(
1537 'View', self.db.getuid(), class_name, itemid=item_id
1538 )
1539 ]
1540 +
1541 + # pagination
1542 + if page['size'] is not None and page['index'] is not None:
1543 + page_start = max(page['index'] * page['size'], 0)
1544 + page_end = min(page_start + page['size'], len(result))
1545 + result = result[page_start:page_end]
1546 +
1547 self.client.setHeader("X-Count-Total", str(len(result)))
1548 return 200, result
1549
1550 @@ -807,7 +841,7 @@
1551 )(class_name, item_id, uri_split[2], input)
1552
1553 # Format the content type
1554 - if format_output.lower() == "json":
1555 + if data_type.lower() == "json":
1556 self.client.setHeader("Content-Type", "application/json")
1557 if pretty_output:
1558 indent = 4
1559 diff --git a/test/test_rest.py b/test/test_rest.py
1560 --- a/test/test_rest.py
1561 +++ b/test/test_rest.py
1562 @@ -57,6 +57,7 @@
1563 'TRACKER_NAME': 'rounduptest'
1564 }
1565 self.dummy_client = client.Client(self.instance, None, env, [], None)
1566 + self.empty_form = cgi.FieldStorage()
1567
1568 self.server = RestfulInstance(self.dummy_client, self.db)
1569
1570 @@ -74,19 +75,20 @@
1571 obtain data for 'joe'
1572 """
1573 # Retrieve all three users.
1574 - results = self.server.get_collection('user', {})
1575 + results = self.server.get_collection('user', self.empty_form)
1576 self.assertEqual(self.dummy_client.response_code, 200)
1577 self.assertEqual(len(results['data']), 3)
1578
1579 # Obtain data for 'joe'.
1580 - results = self.server.get_element('user', self.joeid, {})['data']
1581 + results = self.server.get_element('user', self.joeid, self.empty_form)
1582 + results = results['data']
1583 self.assertEqual(self.dummy_client.response_code, 200)
1584 self.assertEqual(results['attributes']['username'], 'joe')
1585 self.assertEqual(results['attributes']['realname'], 'Joe Random')
1586
1587 # Obtain data for 'joe'.
1588 results = self.server.get_attribute(
1589 - 'user', self.joeid, 'username', {}
1590 + 'user', self.joeid, 'username', self.empty_form
1591 )
1592 self.assertEqual(self.dummy_client.response_code, 200)
1593 self.assertEqual(results['data']['data'], 'joe')
1594 @@ -105,7 +107,7 @@
1595 'user', self.joeid, 'realname', form
1596 )
1597 results = self.server.get_attribute(
1598 - 'user', self.joeid, 'realname', {}
1599 + 'user', self.joeid, 'realname', self.empty_form
1600 )
1601 self.assertEqual(self.dummy_client.response_code, 200)
1602 self.assertEqual(results['data']['data'], 'Joe Doe Doe')
1603 @@ -116,7 +118,7 @@
1604 cgi.MiniFieldStorage('realname', 'Joe Doe')
1605 ]
1606 results = self.server.put_element('user', self.joeid, form)
1607 - results = self.server.get_element('user', self.joeid, {})
1608 + results = self.server.get_element('user', self.joeid, self.empty_form)
1609 self.assertEqual(self.dummy_client.response_code, 200)
1610 self.assertEqual(results['data']['attributes']['realname'], 'Joe Doe')
1611
1612 @@ -137,7 +139,7 @@
1613 results = self.server.post_collection('issue', form)
1614 self.assertEqual(self.dummy_client.response_code, 201)
1615 issueid = results['data']['id']
1616 - results = self.server.get_element('issue', issueid, {})
1617 + results = self.server.get_element('issue', issueid, self.empty_form)
1618 self.assertEqual(self.dummy_client.response_code, 200)
1619 self.assertEqual(results['data']['attributes']['title'], 'foo')
1620 self.assertEqual(self.db.issue.get(issueid, "tx_Source"), 'web')
1621 @@ -154,7 +156,8 @@
1622 results = self.server.post_collection('file', form)
1623 self.assertEqual(self.dummy_client.response_code, 201)
1624 fileid = results['data']['id']
1625 - results = self.server.get_element('file', fileid, {})['data']
1626 + results = self.server.get_element('file', fileid, self.empty_form)
1627 + results = results['data']
1628 self.assertEqual(self.dummy_client.response_code, 200)
1629 self.assertEqual(results['attributes']['content'], 'hello\r\nthere')
1630
1631 @@ -224,17 +227,18 @@
1632
1633 # remove the title and nosy
1634 results = self.server.delete_attribute(
1635 - 'issue', issue_id, 'title', {}
1636 + 'issue', issue_id, 'title', self.empty_form
1637 )
1638 self.assertEqual(self.dummy_client.response_code, 200)
1639
1640 results = self.server.delete_attribute(
1641 - 'issue', issue_id, 'nosy', {}
1642 + 'issue', issue_id, 'nosy', self.empty_form
1643 )
1644 self.assertEqual(self.dummy_client.response_code, 200)
1645
1646 # verify the result
1647 - results = self.server.get_element('issue', issue_id, {})['data']
1648 + results = self.server.get_element('issue', issue_id, self.empty_form)
1649 + results = results['data']
1650 self.assertEqual(self.dummy_client.response_code, 200)
1651 self.assertEqual(len(results['attributes']['nosy']), 0)
1652 self.assertListEqual(results['attributes']['nosy'], [])
1653 @@ -257,7 +261,8 @@
1654 self.assertEqual(self.dummy_client.response_code, 200)
1655
1656 # verify the result
1657 - results = self.server.get_element('issue', issue_id, {})['data']
1658 + results = self.server.get_element('issue', issue_id, self.empty_form)
1659 + results = results['data']
1660 self.assertEqual(self.dummy_client.response_code, 200)
1661 self.assertEqual(len(results['attributes']['nosy']), 2)
1662 self.assertListEqual(results['attributes']['nosy'], ['1', '2'])
1663 @@ -280,7 +285,8 @@
1664 self.assertEqual(self.dummy_client.response_code, 200)
1665
1666 # verify the result
1667 - results = self.server.get_element('issue', issue_id, {})['data']
1668 + results = self.server.get_element('issue', issue_id, self.empty_form)
1669 + results = results['data']
1670 self.assertEqual(self.dummy_client.response_code, 200)
1671 self.assertEqual(results['attributes']['status'], '3')
1672 self.assertEqual(len(results['attributes']['nosy']), 1)
1673 @@ -304,7 +310,8 @@
1674 self.assertEqual(self.dummy_client.response_code, 200)
1675
1676 # verify the result
1677 - results = self.server.get_element('issue', issue_id, {})['data']
1678 + results = self.server.get_element('issue', issue_id, self.empty_form)
1679 + results = results['data']
1680 self.assertEqual(self.dummy_client.response_code, 200)
1681 self.assertEqual(results['attributes']['title'], None)
1682 self.assertEqual(len(results['attributes']['nosy']), 0)
1683 # HG changeset patch
1684 # User Chau Nguyen <dangchau1991@yahoo.com>
1685 # Date 1436261488 -10800
1686 # Tue Jul 07 12:31:28 2015 +0300
1687 # Branch REST
1688 # Node ID 3b248d4d85df162296da34335814040631a6e037
1689 # Parent c16988c1ba10458c2c5b619a172ee9af585ac70e
1690 Change the way core function is called,
1691 Change the return header to allow all methods
1692
1693 diff --git a/roundup/rest.py b/roundup/rest.py
1694 --- a/roundup/rest.py
1695 +++ b/roundup/rest.py
1696 @@ -28,7 +28,7 @@
1697 except Unauthorised, msg:
1698 code = 403
1699 data = msg
1700 - except (hyperdb.DesignatorError, UsageError), msg:
1701 + except UsageError, msg:
1702 code = 400
1703 data = msg
1704 except (AttributeError, Reject), msg:
1705 @@ -72,7 +72,7 @@
1706 """The RestfulInstance performs REST request from the client"""
1707
1708 def __init__(self, client, db):
1709 - self.client = client # it might be unnecessary to receive the client
1710 + self.client = client
1711 self.db = db
1712
1713 protocol = 'http'
1714 @@ -140,6 +140,20 @@
1715
1716 return prop
1717
1718 + def error_obj(self, status, msg, source=None):
1719 + """Return an error object"""
1720 + self.client.response_code = status
1721 + result = {
1722 + 'error': {
1723 + 'status': status,
1724 + 'msg': msg
1725 + }
1726 + }
1727 + if source is not None:
1728 + result['error']['source'] = source
1729 +
1730 + return result
1731 +
1732 @_data_decorator
1733 def get_collection(self, class_name, input):
1734 """GET resource from class URI.
1735 @@ -744,9 +758,9 @@
1736 # default (application/json)
1737
1738 # format_header need a priority parser
1739 - format_ext = os.path.splitext(urlparse.urlparse(uri).path)[1][1:]
1740 - format_header = headers.getheader('Accept')[12:]
1741 - format_output = format_ext or format_header or "json"
1742 + ext_type = os.path.splitext(urlparse.urlparse(uri).path)[1][1:]
1743 + accept_header = headers.getheader('Accept')[12:]
1744 + data_type = ext_type or accept_header or "json"
1745
1746 # check for pretty print
1747 try:
1748 @@ -760,42 +774,39 @@
1749 "Access-Control-Allow-Headers",
1750 "Content-Type, Authorization, X-HTTP-Method-Override"
1751 )
1752 - if resource_uri in self.db.classes:
1753 - self.client.setHeader(
1754 - "Allow",
1755 - "HEAD, OPTIONS, GET, POST, DELETE"
1756 - )
1757 - self.client.setHeader(
1758 - "Access-Control-Allow-Methods",
1759 - "HEAD, OPTIONS, GET, POST, DELETE"
1760 - )
1761 - else:
1762 - self.client.setHeader(
1763 - "Allow",
1764 - "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
1765 - )
1766 - self.client.setHeader(
1767 - "Access-Control-Allow-Methods",
1768 - "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
1769 - )
1770 + self.client.setHeader(
1771 + "Allow",
1772 + "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH"
1773 + )
1774 + self.client.setHeader(
1775 + "Access-Control-Allow-Methods",
1776 + "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
1777 + )
1778 + try:
1779 + class_name, item_id = hyperdb.splitDesignator(resource_uri)
1780 + except hyperdb.DesignatorError, msg:
1781 + class_name = resource_uri
1782 + item_id = None
1783
1784 # Call the appropriate method
1785 - output = None
1786 - if resource_uri in self.db.classes:
1787 - output = getattr(
1788 - self, "%s_collection" % method.lower()
1789 - )(resource_uri, input)
1790 + if (class_name not in self.db.classes) or (len(uri_split) > 3):
1791 + output = self.error_obj(404, "Not found")
1792 + elif item_id is None:
1793 + if len(uri_split) == 2:
1794 + output = getattr(
1795 + self, "%s_collection" % method.lower()
1796 + )(class_name, input)
1797 else:
1798 - class_name, item_id = hyperdb.splitDesignator(resource_uri)
1799 - if len(uri_split) == 3:
1800 + if len(uri_split) == 2:
1801 + output = getattr(
1802 + self, "%s_element" % method.lower()
1803 + )(class_name, item_id, input)
1804 + else:
1805 output = getattr(
1806 self, "%s_attribute" % method.lower()
1807 - )(class_name, item_id, uri_split[2], input)
1808 - else:
1809 - output = getattr(
1810 - self, "%s_element" % method.lower()
1811 - )(class_name, item_id, input)
1812 + )(class_name, item_id, uri_split[2], input)
1813
1814 + # Format the content type
1815 if format_output.lower() == "json":
1816 self.client.setHeader("Content-Type", "application/json")
1817 if pretty_output:
1818 # HG changeset patch
1819 # User Chau Nguyen <dangchau1991@yahoo.com>
1820 # Date 1436191555 -10800
1821 # Mon Jul 06 17:05:55 2015 +0300
1822 # Branch REST
1823 # Node ID c16988c1ba10458c2c5b619a172ee9af585ac70e
1824 # Parent 734f79fbb55e5b2bd1f29baacb8db05d22b0b728
1825 Fix an indentation bug
1826
1827 diff --git a/roundup/rest.py b/roundup/rest.py
1828 --- a/roundup/rest.py
1829 +++ b/roundup/rest.py
1830 @@ -796,16 +796,16 @@
1831 self, "%s_element" % method.lower()
1832 )(class_name, item_id, input)
1833
1834 - if format_output.lower() == "json":
1835 - self.client.setHeader("Content-Type", "application/json")
1836 - if pretty_output:
1837 - indent = 4
1838 - else:
1839 - indent = None
1840 - output = RoundupJSONEncoder(indent=indent).encode(output)
1841 + if format_output.lower() == "json":
1842 + self.client.setHeader("Content-Type", "application/json")
1843 + if pretty_output:
1844 + indent = 4
1845 else:
1846 - self.client.response_code = 406
1847 - output = "Content type is not accepted by client"
1848 + indent = None
1849 + output = RoundupJSONEncoder(indent=indent).encode(output)
1850 + else:
1851 + self.client.response_code = 406
1852 + output = "Content type is not accepted by client"
1853
1854 return output
1855
1856 # HG changeset patch
1857 # User Chau Nguyen <dangchau1991@yahoo.com>
1858 # Date 1436191480 -10800
1859 # Mon Jul 06 17:04:40 2015 +0300
1860 # Branch REST
1861 # Node ID 734f79fbb55e5b2bd1f29baacb8db05d22b0b728
1862 # Parent 5a16780981ad510e410cbc6af6265d5b45f10e37
1863 Move decorator to outside of the class,
1864 Change unittest to test the new format using the decorator
1865
1866 diff --git a/roundup/rest.py b/roundup/rest.py
1867 --- a/roundup/rest.py
1868 +++ b/roundup/rest.py
1869 @@ -14,7 +14,58 @@
1870 import traceback
1871 from roundup import hyperdb
1872 from roundup.exceptions import *
1873 -from roundup import xmlrpc
1874 +
1875 +
1876 +def _data_decorator(func):
1877 + """Wrap the returned data into an object."""
1878 + def format_object(self, *args, **kwargs):
1879 + # get the data / error from function
1880 + try:
1881 + code, data = func(self, *args, **kwargs)
1882 + except IndexError, msg:
1883 + code = 404
1884 + data = msg
1885 + except Unauthorised, msg:
1886 + code = 403
1887 + data = msg
1888 + except (hyperdb.DesignatorError, UsageError), msg:
1889 + code = 400
1890 + data = msg
1891 + except (AttributeError, Reject), msg:
1892 + code = 405
1893 + data = msg
1894 + except ValueError, msg:
1895 + code = 409
1896 + data = msg
1897 + except NotImplementedError:
1898 + code = 402 # nothing to pay, just a mark for debugging purpose
1899 + data = 'Method under development'
1900 + except:
1901 + exc, val, tb = sys.exc_info()
1902 + code = 400
1903 + # if self.DEBUG_MODE in roundup_server
1904 + # else data = 'An error occurred. Please check...',
1905 + data = val
1906 +
1907 + # out to the logfile
1908 + print 'EXCEPTION AT', time.ctime()
1909 + traceback.print_exc()
1910 +
1911 + # decorate it
1912 + self.client.response_code = code
1913 + if code >= 400: # any error require error format
1914 + result = {
1915 + 'error': {
1916 + 'status': code,
1917 + 'msg': data
1918 + }
1919 + }
1920 + else:
1921 + result = {
1922 + 'data': data
1923 + }
1924 + return result
1925 + return format_object
1926
1927
1928 class RestfulInstance(object):
1929 @@ -89,55 +140,6 @@
1930
1931 return prop
1932
1933 - def _data_decorator(func):
1934 - """Wrap the returned data into an object.."""
1935 - def format_object(self, *args, **kwargs):
1936 - try:
1937 - code, data = func(self, *args, **kwargs)
1938 - except IndexError, msg:
1939 - code = 404
1940 - data = msg
1941 - except Unauthorised, msg:
1942 - code = 403
1943 - data = msg
1944 - except (hyperdb.DesignatorError, UsageError), msg:
1945 - code = 400
1946 - data = msg
1947 - except (AttributeError, Reject), msg:
1948 - code = 405
1949 - data = msg
1950 - except ValueError, msg:
1951 - code = 409
1952 - data = msg
1953 - except NotImplementedError:
1954 - code = 402 # nothing to pay, just a mark for debugging purpose
1955 - data = 'Method under development'
1956 - except:
1957 - exc, val, tb = sys.exc_info()
1958 - code = 400
1959 - # if self.DEBUG_MODE in roundup_server
1960 - # else data = 'An error occurred. Please check...',
1961 - data = val
1962 -
1963 - # out to the logfile
1964 - print 'EXCEPTION AT', time.ctime()
1965 - traceback.print_exc()
1966 -
1967 - self.client.response_code = code
1968 - if code >= 400: # any error require error format
1969 - result = {
1970 - 'error': {
1971 - 'status': code,
1972 - 'msg': data
1973 - }
1974 - }
1975 - else:
1976 - result = {
1977 - 'data': data
1978 - }
1979 - return result
1980 - return format_object
1981 -
1982 @_data_decorator
1983 def get_collection(self, class_name, input):
1984 """GET resource from class URI.
1985 diff --git a/test/test_rest.py b/test/test_rest.py
1986 --- a/test/test_rest.py
1987 +++ b/test/test_rest.py
1988 @@ -56,9 +56,9 @@
1989 'HTTP_HOST': 'localhost',
1990 'TRACKER_NAME': 'rounduptest'
1991 }
1992 - dummy_client = client.Client(self.instance, None, env, [], None)
1993 + self.dummy_client = client.Client(self.instance, None, env, [], None)
1994
1995 - self.server = RestfulInstance(dummy_client, self.db)
1996 + self.server = RestfulInstance(self.dummy_client, self.db)
1997
1998 def tearDown(self):
1999 self.db.close()
2000 @@ -74,22 +74,22 @@
2001 obtain data for 'joe'
2002 """
2003 # Retrieve all three users.
2004 - code, results = self.server.get_collection('user', {})
2005 - self.assertEqual(code, 200)
2006 - self.assertEqual(len(results), 3)
2007 + results = self.server.get_collection('user', {})
2008 + self.assertEqual(self.dummy_client.response_code, 200)
2009 + self.assertEqual(len(results['data']), 3)
2010
2011 # Obtain data for 'joe'.
2012 - code, results = self.server.get_element('user', self.joeid, {})
2013 - self.assertEqual(code, 200)
2014 + results = self.server.get_element('user', self.joeid, {})['data']
2015 + self.assertEqual(self.dummy_client.response_code, 200)
2016 self.assertEqual(results['attributes']['username'], 'joe')
2017 self.assertEqual(results['attributes']['realname'], 'Joe Random')
2018
2019 # Obtain data for 'joe'.
2020 - code, results = self.server.get_attribute(
2021 + results = self.server.get_attribute(
2022 'user', self.joeid, 'username', {}
2023 )
2024 - self.assertEqual(code, 200)
2025 - self.assertEqual(results['data'], 'joe')
2026 + self.assertEqual(self.dummy_client.response_code, 200)
2027 + self.assertEqual(results['data']['data'], 'joe')
2028
2029 def testPut(self):
2030 """
2031 @@ -101,30 +101,29 @@
2032 form.list = [
2033 cgi.MiniFieldStorage('data', 'Joe Doe Doe')
2034 ]
2035 - code, results = self.server.put_attribute(
2036 + results = self.server.put_attribute(
2037 'user', self.joeid, 'realname', form
2038 )
2039 - code, results = self.server.get_attribute(
2040 + results = self.server.get_attribute(
2041 'user', self.joeid, 'realname', {}
2042 )
2043 - self.assertEqual(code, 200)
2044 - self.assertEqual(results['data'], 'Joe Doe Doe')
2045 + self.assertEqual(self.dummy_client.response_code, 200)
2046 + self.assertEqual(results['data']['data'], 'Joe Doe Doe')
2047
2048 # Reset joe's 'realname'.
2049 form = cgi.FieldStorage()
2050 form.list = [
2051 cgi.MiniFieldStorage('realname', 'Joe Doe')
2052 ]
2053 - code, results = self.server.put_element('user', self.joeid, form)
2054 - code, results = self.server.get_element('user', self.joeid, {})
2055 - self.assertEqual(code, 200)
2056 - self.assertEqual(results['attributes']['realname'], 'Joe Doe')
2057 + results = self.server.put_element('user', self.joeid, form)
2058 + results = self.server.get_element('user', self.joeid, {})
2059 + self.assertEqual(self.dummy_client.response_code, 200)
2060 + self.assertEqual(results['data']['attributes']['realname'], 'Joe Doe')
2061
2062 # check we can't change admin's details
2063 - self.assertRaises(
2064 - Unauthorised,
2065 - self.server.put_element, 'user', '1', form
2066 - )
2067 + results = self.server.put_element('user', '1', form)
2068 + self.assertEqual(self.dummy_client.response_code, 403)
2069 + self.assertEqual(results['error']['status'], 403)
2070
2071 def testPost(self):
2072 """
2073 @@ -135,12 +134,12 @@
2074 form.list = [
2075 cgi.MiniFieldStorage('title', 'foo')
2076 ]
2077 - code, results = self.server.post_collection('issue', form)
2078 - self.assertEqual(code, 201)
2079 - issueid = results['id']
2080 - code, results = self.server.get_element('issue', issueid, {})
2081 - self.assertEqual(code, 200)
2082 - self.assertEqual(results['attributes']['title'], 'foo')
2083 + results = self.server.post_collection('issue', form)
2084 + self.assertEqual(self.dummy_client.response_code, 201)
2085 + issueid = results['data']['id']
2086 + results = self.server.get_element('issue', issueid, {})
2087 + self.assertEqual(self.dummy_client.response_code, 200)
2088 + self.assertEqual(results['data']['attributes']['title'], 'foo')
2089 self.assertEqual(self.db.issue.get(issueid, "tx_Source"), 'web')
2090
2091 def testPostFile(self):
2092 @@ -152,11 +151,11 @@
2093 form.list = [
2094 cgi.MiniFieldStorage('content', 'hello\r\nthere')
2095 ]
2096 - code, results = self.server.post_collection('file', form)
2097 - self.assertEqual(code, 201)
2098 - fileid = results['id']
2099 - code, results = self.server.get_element('file', fileid, {})
2100 - self.assertEqual(code, 200)
2101 + results = self.server.post_collection('file', form)
2102 + self.assertEqual(self.dummy_client.response_code, 201)
2103 + fileid = results['data']['id']
2104 + results = self.server.get_element('file', fileid, {})['data']
2105 + self.assertEqual(self.dummy_client.response_code, 200)
2106 self.assertEqual(results['attributes']['content'], 'hello\r\nthere')
2107
2108 def testAuthDeniedPut(self):
2109 @@ -168,10 +167,9 @@
2110 form.list = [
2111 cgi.MiniFieldStorage('realname', 'someone')
2112 ]
2113 - self.assertRaises(
2114 - Unauthorised,
2115 - self.server.put_element, 'user', '1', form
2116 - )
2117 + results = self.server.put_element('user', '1', form)
2118 + self.assertEqual(self.dummy_client.response_code, 403)
2119 + self.assertEqual(results['error']['status'], 403)
2120
2121 def testAuthDeniedPost(self):
2122 """
2123 @@ -181,10 +179,9 @@
2124 form.list = [
2125 cgi.MiniFieldStorage('username', 'blah')
2126 ]
2127 - self.assertRaises(
2128 - Unauthorised,
2129 - self.server.post_collection, 'user', form
2130 - )
2131 + results = self.server.post_collection('user', form)
2132 + self.assertEqual(self.dummy_client.response_code, 403)
2133 + self.assertEqual(results['error']['status'], 403)
2134
2135 def testAuthAllowedPut(self):
2136 """
2137 @@ -196,10 +193,9 @@
2138 cgi.MiniFieldStorage('realname', 'someone')
2139 ]
2140 try:
2141 - try:
2142 - self.server.put_element('user', '2', form)
2143 - except Unauthorised, err:
2144 - self.fail('raised %s' % err)
2145 + self.server.put_element('user', '2', form)
2146 + except Unauthorised, err:
2147 + self.fail('raised %s' % err)
2148 finally:
2149 self.db.setCurrentUser('joe')
2150
2151 @@ -213,10 +209,9 @@
2152 cgi.MiniFieldStorage('username', 'blah')
2153 ]
2154 try:
2155 - try:
2156 - self.server.post_collection('user', form)
2157 - except Unauthorised, err:
2158 - self.fail('raised %s' % err)
2159 + self.server.post_collection('user', form)
2160 + except Unauthorised, err:
2161 + self.fail('raised %s' % err)
2162 finally:
2163 self.db.setCurrentUser('joe')
2164
2165 @@ -228,19 +223,19 @@
2166 issue_id = self.db.issue.create(title='foo', nosy=['1'])
2167
2168 # remove the title and nosy
2169 - code, results = self.server.delete_attribute(
2170 + results = self.server.delete_attribute(
2171 'issue', issue_id, 'title', {}
2172 )
2173 - self.assertEqual(code, 200)
2174 + self.assertEqual(self.dummy_client.response_code, 200)
2175
2176 - code, results = self.server.delete_attribute(
2177 + results = self.server.delete_attribute(
2178 'issue', issue_id, 'nosy', {}
2179 )
2180 - self.assertEqual(code, 200)
2181 + self.assertEqual(self.dummy_client.response_code, 200)
2182
2183 # verify the result
2184 - code, results = self.server.get_element('issue', issue_id, {})
2185 - self.assertEqual(code, 200)
2186 + results = self.server.get_element('issue', issue_id, {})['data']
2187 + self.assertEqual(self.dummy_client.response_code, 200)
2188 self.assertEqual(len(results['attributes']['nosy']), 0)
2189 self.assertListEqual(results['attributes']['nosy'], [])
2190 self.assertEqual(results['attributes']['title'], None)
2191 @@ -258,12 +253,12 @@
2192 cgi.MiniFieldStorage('op', 'add'),
2193 cgi.MiniFieldStorage('nosy', '2')
2194 ]
2195 - code, results = self.server.patch_element('issue', issue_id, form)
2196 - self.assertEqual(code, 200)
2197 + results = self.server.patch_element('issue', issue_id, form)
2198 + self.assertEqual(self.dummy_client.response_code, 200)
2199
2200 # verify the result
2201 - code, results = self.server.get_element('issue', issue_id, {})
2202 - self.assertEqual(code, 200)
2203 + results = self.server.get_element('issue', issue_id, {})['data']
2204 + self.assertEqual(self.dummy_client.response_code, 200)
2205 self.assertEqual(len(results['attributes']['nosy']), 2)
2206 self.assertListEqual(results['attributes']['nosy'], ['1', '2'])
2207
2208 @@ -281,12 +276,12 @@
2209 cgi.MiniFieldStorage('nosy', '2'),
2210 cgi.MiniFieldStorage('status', '3')
2211 ]
2212 - code, results = self.server.patch_element('issue', issue_id, form)
2213 - self.assertEqual(code, 200)
2214 + results = self.server.patch_element('issue', issue_id, form)
2215 + self.assertEqual(self.dummy_client.response_code, 200)
2216
2217 # verify the result
2218 - code, results = self.server.get_element('issue', issue_id, {})
2219 - self.assertEqual(code, 200)
2220 + results = self.server.get_element('issue', issue_id, {})['data']
2221 + self.assertEqual(self.dummy_client.response_code, 200)
2222 self.assertEqual(results['attributes']['status'], '3')
2223 self.assertEqual(len(results['attributes']['nosy']), 1)
2224 self.assertListEqual(results['attributes']['nosy'], ['2'])
2225 @@ -305,12 +300,12 @@
2226 cgi.MiniFieldStorage('nosy', ''),
2227 cgi.MiniFieldStorage('title', '')
2228 ]
2229 - code, results = self.server.patch_element('issue', issue_id, form)
2230 - self.assertEqual(code, 200)
2231 + results = self.server.patch_element('issue', issue_id, form)
2232 + self.assertEqual(self.dummy_client.response_code, 200)
2233
2234 # verify the result
2235 - code, results = self.server.get_element('issue', issue_id, {})
2236 - self.assertEqual(code, 200)
2237 + results = self.server.get_element('issue', issue_id, {})['data']
2238 + self.assertEqual(self.dummy_client.response_code, 200)
2239 self.assertEqual(results['attributes']['title'], None)
2240 self.assertEqual(len(results['attributes']['nosy']), 0)
2241 self.assertEqual(results['attributes']['nosy'], [])
2242 # HG changeset patch
2243 # User Chau Nguyen <dangchau1991@yahoo.com>
2244 # Date 1436183730 -10800
2245 # Mon Jul 06 14:55:30 2015 +0300
2246 # Branch REST
2247 # Node ID 5a16780981ad510e410cbc6af6265d5b45f10e37
2248 # Parent 88132e1281fa89343bccf9defc431be755fa6138
2249 Added decorator to handle formatting output data
2250
2251 diff --git a/roundup/rest.py b/roundup/rest.py
2252 --- a/roundup/rest.py
2253 +++ b/roundup/rest.py
2254 @@ -89,30 +89,56 @@
2255
2256 return prop
2257
2258 - @staticmethod
2259 - def error_obj(status, msg, source=None):
2260 - """Wrap the error data into an object. This function is temporally and
2261 - will be changed to a decorator later."""
2262 - result = {
2263 - 'error': {
2264 - 'status': status,
2265 - 'msg': msg
2266 - }
2267 - }
2268 - if source is not None:
2269 - result['error']['source'] = source
2270 + def _data_decorator(func):
2271 + """Wrap the returned data into an object.."""
2272 + def format_object(self, *args, **kwargs):
2273 + try:
2274 + code, data = func(self, *args, **kwargs)
2275 + except IndexError, msg:
2276 + code = 404
2277 + data = msg
2278 + except Unauthorised, msg:
2279 + code = 403
2280 + data = msg
2281 + except (hyperdb.DesignatorError, UsageError), msg:
2282 + code = 400
2283 + data = msg
2284 + except (AttributeError, Reject), msg:
2285 + code = 405
2286 + data = msg
2287 + except ValueError, msg:
2288 + code = 409
2289 + data = msg
2290 + except NotImplementedError:
2291 + code = 402 # nothing to pay, just a mark for debugging purpose
2292 + data = 'Method under development'
2293 + except:
2294 + exc, val, tb = sys.exc_info()
2295 + code = 400
2296 + # if self.DEBUG_MODE in roundup_server
2297 + # else data = 'An error occurred. Please check...',
2298 + data = val
2299
2300 - return result
2301 + # out to the logfile
2302 + print 'EXCEPTION AT', time.ctime()
2303 + traceback.print_exc()
2304
2305 - @staticmethod
2306 - def data_obj(data):
2307 - """Wrap the returned data into an object. This function is temporally
2308 - and will be changed to a decorator later."""
2309 - result = {
2310 - 'data': data
2311 - }
2312 - return result
2313 + self.client.response_code = code
2314 + if code >= 400: # any error require error format
2315 + result = {
2316 + 'error': {
2317 + 'status': code,
2318 + 'msg': data
2319 + }
2320 + }
2321 + else:
2322 + result = {
2323 + 'data': data
2324 + }
2325 + return result
2326 + return format_object
2327
2328 + @_data_decorator
2329 def get_collection(self, class_name, input):
2330 """GET resource from class URI.
2331
2332 @@ -146,6 +172,7 @@
2333 self.client.setHeader("X-Count-Total", str(len(result)))
2334 return 200, result
2335
2336 + @_data_decorator
2337 def get_element(self, class_name, item_id, input):
2338 """GET resource from object URI.
2339
2340 @@ -191,6 +218,7 @@
2341
2342 return 200, result
2343
2344 + @_data_decorator
2345 def get_attribute(self, class_name, item_id, attr_name, input):
2346 """GET resource from attribute URI.
2347
2348 @@ -231,6 +259,7 @@
2349
2350 return 200, result
2351
2352 + @_data_decorator
2353 def post_collection(self, class_name, input):
2354 """POST a new object to a class
2355
2356 @@ -290,18 +319,22 @@
2357 }
2358 return 201, result
2359
2360 + @_data_decorator
2361 def post_element(self, class_name, item_id, input):
2362 """POST to an object of a class is not allowed"""
2363 raise Reject('POST to an item is not allowed')
2364
2365 + @_data_decorator
2366 def post_attribute(self, class_name, item_id, attr_name, input):
2367 """POST to an attribute of an object is not allowed"""
2368 raise Reject('POST to an attribute is not allowed')
2369
2370 + @_data_decorator
2371 def put_collection(self, class_name, input):
2372 """PUT a class is not allowed"""
2373 raise Reject('PUT a class is not allowed')
2374
2375 + @_data_decorator
2376 def put_element(self, class_name, item_id, input):
2377 """PUT a new content to an object
2378
2379 @@ -346,6 +379,7 @@
2380 }
2381 return 200, result
2382
2383 + @_data_decorator
2384 def put_attribute(self, class_name, item_id, attr_name, input):
2385 """PUT an attribute to an object
2386
2387 @@ -394,6 +428,7 @@
2388
2389 return 200, result
2390
2391 + @_data_decorator
2392 def delete_collection(self, class_name, input):
2393 """DELETE all objects in a class
2394
2395 @@ -433,6 +468,7 @@
2396
2397 return 200, result
2398
2399 + @_data_decorator
2400 def delete_element(self, class_name, item_id, input):
2401 """DELETE an object in a class
2402
2403 @@ -461,6 +497,7 @@
2404
2405 return 200, result
2406
2407 + @_data_decorator
2408 def delete_attribute(self, class_name, item_id, attr_name, input):
2409 """DELETE an attribute in a object by setting it to None or empty
2410
2411 @@ -503,10 +540,12 @@
2412
2413 return 200, result
2414
2415 + @_data_decorator
2416 def patch_collection(self, class_name, input):
2417 """PATCH a class is not allowed"""
2418 raise Reject('PATCH a class is not allowed')
2419
2420 + @_data_decorator
2421 def patch_element(self, class_name, item_id, input):
2422 """PATCH an object
2423
2424 @@ -573,6 +612,7 @@
2425 }
2426 return 200, result
2427
2428 + @_data_decorator
2429 def patch_attribute(self, class_name, item_id, attr_name, input):
2430 """PATCH an attribute of an object
2431
2432 @@ -645,6 +685,7 @@
2433 }
2434 return 200, result
2435
2436 + @_data_decorator
2437 def options_collection(self, class_name, input):
2438 """OPTION return the HTTP Header for the class uri
2439
2440 @@ -654,6 +695,7 @@
2441 """
2442 return 204, ""
2443
2444 + @_data_decorator
2445 def options_element(self, class_name, item_id, input):
2446 """OPTION return the HTTP Header for the object uri
2447
2448 @@ -667,6 +709,7 @@
2449 )
2450 return 204, ""
2451
2452 + @_data_decorator
2453 def option_attribute(self, class_name, item_id, attr_name, input):
2454 """OPTION return the HTTP Header for the attribute uri
2455
2456 @@ -736,53 +779,21 @@
2457
2458 # Call the appropriate method
2459 output = None
2460 - try:
2461 - if resource_uri in self.db.classes:
2462 - response_code, output = getattr(
2463 - self, "%s_collection" % method.lower()
2464 - )(resource_uri, input)
2465 + if resource_uri in self.db.classes:
2466 + output = getattr(
2467 + self, "%s_collection" % method.lower()
2468 + )(resource_uri, input)
2469 + else:
2470 + class_name, item_id = hyperdb.splitDesignator(resource_uri)
2471 + if len(uri_split) == 3:
2472 + output = getattr(
2473 + self, "%s_attribute" % method.lower()
2474 + )(class_name, item_id, uri_split[2], input)
2475 else:
2476 - class_name, item_id = hyperdb.splitDesignator(resource_uri)
2477 - if len(uri_split) == 3:
2478 - response_code, output = getattr(
2479 - self, "%s_attribute" % method.lower()
2480 - )(class_name, item_id, uri_split[2], input)
2481 - else:
2482 - response_code, output = getattr(
2483 - self, "%s_element" % method.lower()
2484 - )(class_name, item_id, input)
2485 - output = RestfulInstance.data_obj(output)
2486 - self.client.response_code = response_code
2487 - except IndexError, msg:
2488 - output = RestfulInstance.error_obj(404, msg)
2489 - self.client.response_code = 404
2490 - except Unauthorised, msg:
2491 - output = RestfulInstance.error_obj(403, msg)
2492 - self.client.response_code = 403
2493 - except (hyperdb.DesignatorError, UsageError), msg:
2494 - output = RestfulInstance.error_obj(400, msg)
2495 - self.client.response_code = 400
2496 - except (AttributeError, Reject), msg:
2497 - output = RestfulInstance.error_obj(405, msg)
2498 - self.client.response_code = 405
2499 - except ValueError, msg:
2500 - output = RestfulInstance.error_obj(409, msg)
2501 - self.client.response_code = 409
2502 - except NotImplementedError:
2503 - output = RestfulInstance.error_obj(402, 'Method under development')
2504 - self.client.response_code = 402
2505 - # nothing to pay, just a mark for debugging purpose
2506 - except:
2507 - # if self.DEBUG_MODE in roundup_server
2508 - # else msg = 'An error occurred. Please check...',
2509 - exc, val, tb = sys.exc_info()
2510 - output = RestfulInstance.error_obj(400, val)
2511 - self.client.response_code = 400
2512 + output = getattr(
2513 + self, "%s_element" % method.lower()
2514 + )(class_name, item_id, input)
2515
2516 - # out to the logfile, it would be nice if the server do it for me
2517 - print 'EXCEPTION AT', time.ctime()
2518 - traceback.print_exc()
2519 - finally:
2520 if format_output.lower() == "json":
2521 self.client.setHeader("Content-Type", "application/json")
2522 if pretty_output:
2523 # HG changeset patch
2524 # User Chau Nguyen <dangchau1991@yahoo.com>
2525 # Date 1436049663 -10800
2526 # Sun Jul 05 01:41:03 2015 +0300
2527 # Branch REST
2528 # Node ID 88132e1281fa89343bccf9defc431be755fa6138
2529 # Parent 6358e8d1d807f8afccffe838a038a1e843ae9a0a
2530 Fixed code convention
2531
2532 diff --git a/test/test_rest.py b/test/test_rest.py
2533 --- a/test/test_rest.py
2534 +++ b/test/test_rest.py
2535 @@ -1,17 +1,19 @@
2536 -import unittest, os, shutil, errno, sys, difflib, cgi, re
2537 +import unittest
2538 +import os
2539 +import shutil
2540 +import errno
2541
2542 -from xmlrpclib import MultiCall
2543 from roundup.cgi.exceptions import *
2544 -from roundup import init, instance, password, hyperdb, date
2545 +from roundup import password, hyperdb
2546 from roundup.rest import RestfulInstance
2547 from roundup.backends import list_backends
2548 -from roundup.hyperdb import String
2549 -from roundup.cgi import TranslationService, client
2550 +from roundup.cgi import client
2551
2552 import db_test_base
2553
2554 NEEDS_INSTANCE = 1
2555
2556 +
2557 class TestCase(unittest.TestCase):
2558
2559 backend = None
2560 @@ -313,6 +315,7 @@
2561 self.assertEqual(len(results['attributes']['nosy']), 0)
2562 self.assertEqual(results['attributes']['nosy'], [])
2563
2564 +
2565 def test_suite():
2566 suite = unittest.TestSuite()
2567 for l in list_backends():
2568 # HG changeset patch
2569 # User Chau Nguyen <dangchau1991@yahoo.com>
2570 # Date 1436027845 -10800
2571 # Sat Jul 04 19:37:25 2015 +0300
2572 # Branch REST
2573 # Node ID 6358e8d1d807f8afccffe838a038a1e843ae9a0a
2574 # Parent 8becb1b1de45be6d4fe7d0dac6a70ca21334f977
2575 Added test cases for the element URI methods
2576
2577 diff --git a/test/test_rest.py b/test/test_rest.py
2578 --- a/test/test_rest.py
2579 +++ b/test/test_rest.py
2580 @@ -82,11 +82,32 @@
2581 self.assertEqual(results['attributes']['username'], 'joe')
2582 self.assertEqual(results['attributes']['realname'], 'Joe Random')
2583
2584 + # Obtain data for 'joe'.
2585 + code, results = self.server.get_attribute(
2586 + 'user', self.joeid, 'username', {}
2587 + )
2588 + self.assertEqual(code, 200)
2589 + self.assertEqual(results['data'], 'joe')
2590 +
2591 def testPut(self):
2592 """
2593 Change joe's 'realname'
2594 Check if we can't change admin's detail
2595 """
2596 + # change Joe's realname via attribute uri
2597 + form = cgi.FieldStorage()
2598 + form.list = [
2599 + cgi.MiniFieldStorage('data', 'Joe Doe Doe')
2600 + ]
2601 + code, results = self.server.put_attribute(
2602 + 'user', self.joeid, 'realname', form
2603 + )
2604 + code, results = self.server.get_attribute(
2605 + 'user', self.joeid, 'realname', {}
2606 + )
2607 + self.assertEqual(code, 200)
2608 + self.assertEqual(results['data'], 'Joe Doe Doe')
2609 +
2610 # Reset joe's 'realname'.
2611 form = cgi.FieldStorage()
2612 form.list = [
2613 @@ -197,6 +218,31 @@
2614 finally:
2615 self.db.setCurrentUser('joe')
2616
2617 + def testDeleteAttributeUri(self):
2618 + """
2619 + Test Delete an attribute
2620 + """
2621 + # create a new issue with userid 1 in the nosy list
2622 + issue_id = self.db.issue.create(title='foo', nosy=['1'])
2623 +
2624 + # remove the title and nosy
2625 + code, results = self.server.delete_attribute(
2626 + 'issue', issue_id, 'title', {}
2627 + )
2628 + self.assertEqual(code, 200)
2629 +
2630 + code, results = self.server.delete_attribute(
2631 + 'issue', issue_id, 'nosy', {}
2632 + )
2633 + self.assertEqual(code, 200)
2634 +
2635 + # verify the result
2636 + code, results = self.server.get_element('issue', issue_id, {})
2637 + self.assertEqual(code, 200)
2638 + self.assertEqual(len(results['attributes']['nosy']), 0)
2639 + self.assertListEqual(results['attributes']['nosy'], [])
2640 + self.assertEqual(results['attributes']['title'], None)
2641 +
2642 def testPatchAdd(self):
2643 """
2644 Test Patch op 'Add'
2645 # HG changeset patch
2646 # User Chau Nguyen <dangchau1991@yahoo.com>
2647 # Date 1436019690 -10800
2648 # Sat Jul 04 17:21:30 2015 +0300
2649 # Branch REST
2650 # Node ID 8becb1b1de45be6d4fe7d0dac6a70ca21334f977
2651 # Parent 5a68dbfb8477cfad4c72d4a506affeecc6d1e19f
2652 Added attribute URI handling
2653
2654 diff --git a/roundup/rest.py b/roundup/rest.py
2655 --- a/roundup/rest.py
2656 +++ b/roundup/rest.py
2657 @@ -51,27 +51,44 @@
2658 value = arg.value
2659 if key not in class_props:
2660 continue
2661 - if isinstance(key, unicode):
2662 - try:
2663 - key = key.encode('ascii')
2664 - except UnicodeEncodeError:
2665 - raise UsageError(
2666 - 'argument %r is no valid ascii keyword' % key
2667 - )
2668 - if isinstance(value, unicode):
2669 - value = value.encode('utf-8')
2670 - if value:
2671 - try:
2672 - props[key] = hyperdb.rawToHyperdb(
2673 - self.db, cl, itemid, key, value
2674 - )
2675 - except hyperdb.HyperdbValueError, msg:
2676 - raise UsageError(msg)
2677 - else:
2678 - props[key] = None
2679 + props[key] = self.prop_from_arg(cl, key, value, itemid)
2680
2681 return props
2682
2683 + def prop_from_arg(self, cl, key, value, itemid=None):
2684 + """Construct a property from the given argument,
2685 + and return them after validation.
2686 +
2687 + Args:
2688 + cl (string): class object of the resource
2689 + key (string): attribute key
2690 + value (string): attribute value
2691 + itemid (string, optional): itemid of the object
2692 +
2693 + Returns:
2694 + value: value of validated properties
2695 +
2696 + """
2697 + prop = None
2698 + if isinstance(key, unicode):
2699 + try:
2700 + key = key.encode('ascii')
2701 + except UnicodeEncodeError:
2702 + raise UsageError(
2703 + 'argument %r is no valid ascii keyword' % key
2704 + )
2705 + if isinstance(value, unicode):
2706 + value = value.encode('utf-8')
2707 + if value:
2708 + try:
2709 + prop = hyperdb.rawToHyperdb(
2710 + self.db, cl, itemid, key, value
2711 + )
2712 + except hyperdb.HyperdbValueError, msg:
2713 + raise UsageError(msg)
2714 +
2715 + return prop
2716 +
2717 @staticmethod
2718 def error_obj(status, msg, source=None):
2719 """Wrap the error data into an object. This function is temporally and
2720 @@ -152,7 +169,7 @@
2721 'View', self.db.getuid(), class_name, itemid=item_id
2722 ):
2723 raise Unauthorised(
2724 - 'Permission to view %s item %s denied' % (class_name, item_id)
2725 + 'Permission to view %s%s denied' % (class_name, item_id)
2726 )
2727
2728 class_obj = self.db.getclass(class_name)
2729 @@ -174,6 +191,46 @@
2730
2731 return 200, result
2732
2733 + def get_attribute(self, class_name, item_id, attr_name, input):
2734 + """GET resource from attribute URI.
2735 +
2736 + This function returns only attribute has View permission
2737 + class_name should be valid already
2738 +
2739 + Args:
2740 + class_name (string): class name of the resource (Ex: issue, msg)
2741 + item_id (string): id of the resource (Ex: 12, 15)
2742 + attr_name (string): attribute of the resource (Ex: title, nosy)
2743 + input (list): the submitted form of the user
2744 +
2745 + Returns:
2746 + int: http status code 200 (OK)
2747 + list: a dictionary represents the attribute
2748 + id: id of the object
2749 + type: class name of the attribute
2750 + link: link to the attribute
2751 + data: data of the requested attribute
2752 + """
2753 + if not self.db.security.hasPermission(
2754 + 'View', self.db.getuid(), class_name, attr_name, item_id
2755 + ):
2756 + raise Unauthorised(
2757 + 'Permission to view %s%s %s denied' %
2758 + (class_name, item_id, attr_name)
2759 + )
2760 +
2761 + class_obj = self.db.getclass(class_name)
2762 + data = class_obj.get(item_id, attr_name)
2763 + result = {
2764 + 'id': item_id,
2765 + 'type': type(data),
2766 + 'link': "%s%s%s/%s" %
2767 + (self.base_path, class_name, item_id, attr_name),
2768 + 'data': data
2769 + }
2770 +
2771 + return 200, result
2772 +
2773 def post_collection(self, class_name, input):
2774 """POST a new object to a class
2775
2776 @@ -237,6 +294,10 @@
2777 """POST to an object of a class is not allowed"""
2778 raise Reject('POST to an item is not allowed')
2779
2780 + def post_attribute(self, class_name, item_id, attr_name, input):
2781 + """POST to an attribute of an object is not allowed"""
2782 + raise Reject('POST to an attribute is not allowed')
2783 +
2784 def put_collection(self, class_name, input):
2785 """PUT a class is not allowed"""
2786 raise Reject('PUT a class is not allowed')
2787 @@ -285,6 +346,54 @@
2788 }
2789 return 200, result
2790
2791 + def put_attribute(self, class_name, item_id, attr_name, input):
2792 + """PUT an attribute to an object
2793 +
2794 + Args:
2795 + class_name (string): class name of the resource (Ex: issue, msg)
2796 + item_id (string): id of the resource (Ex: 12, 15)
2797 + attr_name (string): attribute of the resource (Ex: title, nosy)
2798 + input (list): the submitted form of the user
2799 +
2800 + Returns:
2801 + int: http status code 200 (OK)
2802 + dict:a dictionary represents the modified object
2803 + id: id of the object
2804 + type: class name of the object
2805 + link: link to the object
2806 + attributes: a dictionary represent only changed attributes of
2807 + the object
2808 + """
2809 + if not self.db.security.hasPermission(
2810 + 'Edit', self.db.getuid(), class_name, attr_name, item_id
2811 + ):
2812 + raise Unauthorised(
2813 + 'Permission to edit %s%s %s denied' %
2814 + (class_name, item_id, attr_name)
2815 + )
2816 +
2817 + class_obj = self.db.getclass(class_name)
2818 + props = {
2819 + attr_name: self.prop_from_arg(
2820 + class_obj, attr_name, input['data'].value, item_id
2821 + )
2822 + }
2823 +
2824 + try:
2825 + result = class_obj.set(item_id, **props)
2826 + self.db.commit()
2827 + except (TypeError, IndexError, ValueError), message:
2828 + raise ValueError(message)
2829 +
2830 + result = {
2831 + 'id': item_id,
2832 + 'type': class_name,
2833 + 'link': self.base_path + class_name + item_id,
2834 + 'attribute': result
2835 + }
2836 +
2837 + return 200, result
2838 +
2839 def delete_collection(self, class_name, input):
2840 """DELETE all objects in a class
2841
2842 @@ -352,11 +461,74 @@
2843
2844 return 200, result
2845
2846 + def delete_attribute(self, class_name, item_id, attr_name, input):
2847 + """DELETE an attribute in a object by setting it to None or empty
2848 +
2849 + Args:
2850 + class_name (string): class name of the resource (Ex: issue, msg)
2851 + item_id (string): id of the resource (Ex: 12, 15)
2852 + attr_name (string): attribute of the resource (Ex: title, nosy)
2853 + input (list): the submitted form of the user
2854 +
2855 + Returns:
2856 + int: http status code 200 (OK)
2857 + dict:
2858 + status (string): 'ok'
2859 + """
2860 + if not self.db.security.hasPermission(
2861 + 'Edit', self.db.getuid(), class_name, attr_name, item_id
2862 + ):
2863 + raise Unauthorised(
2864 + 'Permission to delete %s%s %s denied' %
2865 + (class_name, item_id, attr_name)
2866 + )
2867 +
2868 + class_obj = self.db.getclass(class_name)
2869 + props = {}
2870 + prop_obj = class_obj.get(item_id, attr_name)
2871 + if isinstance(prop_obj, list):
2872 + props[attr_name] = []
2873 + else:
2874 + props[attr_name] = None
2875 +
2876 + try:
2877 + class_obj.set(item_id, **props)
2878 + self.db.commit()
2879 + except (TypeError, IndexError, ValueError), message:
2880 + raise ValueError(message)
2881 +
2882 + result = {
2883 + 'status': 'ok'
2884 + }
2885 +
2886 + return 200, result
2887 +
2888 def patch_collection(self, class_name, input):
2889 """PATCH a class is not allowed"""
2890 raise Reject('PATCH a class is not allowed')
2891
2892 def patch_element(self, class_name, item_id, input):
2893 + """PATCH an object
2894 +
2895 + Patch an element using 3 operators
2896 + ADD : Append new value to the object's attribute
2897 + REPLACE: Replace object's attribute
2898 + REMOVE: Clear object's attribute
2899 +
2900 + Args:
2901 + class_name (string): class name of the resource (Ex: issue, msg)
2902 + item_id (string): id of the resource (Ex: 12, 15)
2903 + input (list): the submitted form of the user
2904 +
2905 + Returns:
2906 + int: http status code 200 (OK)
2907 + dict: a dictionary represents the modified object
2908 + id: id of the object
2909 + type: class name of the object
2910 + link: link to the object
2911 + attributes: a dictionary represent only changed attributes of
2912 + the object
2913 + """
2914 try:
2915 op = input['op'].value.lower()
2916 except KeyError:
2917 @@ -401,6 +573,78 @@
2918 }
2919 return 200, result
2920
2921 + def patch_attribute(self, class_name, item_id, attr_name, input):
2922 + """PATCH an attribute of an object
2923 +
2924 + Patch an element using 3 operators
2925 + ADD : Append new value to the attribute
2926 + REPLACE: Replace attribute
2927 + REMOVE: Clear attribute
2928 +
2929 + Args:
2930 + class_name (string): class name of the resource (Ex: issue, msg)
2931 + item_id (string): id of the resource (Ex: 12, 15)
2932 + attr_name (string): attribute of the resource (Ex: title, nosy)
2933 + input (list): the submitted form of the user
2934 +
2935 + Returns:
2936 + int: http status code 200 (OK)
2937 + dict: a dictionary represents the modified object
2938 + id: id of the object
2939 + type: class name of the object
2940 + link: link to the object
2941 + attributes: a dictionary represent only changed attributes of
2942 + the object
2943 + """
2944 + try:
2945 + op = input['op'].value.lower()
2946 + except KeyError:
2947 + op = "replace"
2948 + class_obj = self.db.getclass(class_name)
2949 +
2950 + if not self.db.security.hasPermission(
2951 + 'Edit', self.db.getuid(), class_name, attr_name, item_id
2952 + ):
2953 + raise Unauthorised(
2954 + 'Permission to edit %s%s %s denied' %
2955 + (class_name, item_id, attr_name)
2956 + )
2957 +
2958 + prop = attr_name
2959 + class_obj = self.db.getclass(class_name)
2960 + props = {
2961 + prop: self.prop_from_arg(
2962 + class_obj, prop, input['data'].value, item_id
2963 + )
2964 + }
2965 +
2966 + if op == 'add':
2967 + props[prop] = class_obj.get(item_id, prop) + props[prop]
2968 + elif op == 'replace':
2969 + pass
2970 + elif op == 'remove':
2971 + current_prop = class_obj.get(item_id, prop)
2972 + if isinstance(current_prop, list):
2973 + props[prop] = []
2974 + else:
2975 + props[prop] = None
2976 + else:
2977 + raise UsageError('PATCH Operation %s is not allowed' % op)
2978 +
2979 + try:
2980 + result = class_obj.set(item_id, **props)
2981 + self.db.commit()
2982 + except (TypeError, IndexError, ValueError), message:
2983 + raise ValueError(message)
2984 +
2985 + result = {
2986 + 'id': item_id,
2987 + 'type': class_name,
2988 + 'link': self.base_path + class_name + item_id,
2989 + 'attribute': result
2990 + }
2991 + return 200, result
2992 +
2993 def options_collection(self, class_name, input):
2994 """OPTION return the HTTP Header for the class uri
2995
2996 @@ -419,8 +663,20 @@
2997 """
2998 self.client.setHeader(
2999 "Accept-Patch",
3000 - "application/x-www-form-urlencoded, "
3001 - "multipart/form-data"
3002 + "application/x-www-form-urlencoded, multipart/form-data"
3003 + )
3004 + return 204, ""
3005 +
3006 + def option_attribute(self, class_name, item_id, attr_name, input):
3007 + """OPTION return the HTTP Header for the attribute uri
3008 +
3009 + Returns:
3010 + int: http status code 204 (No content)
3011 + body (string): an empty string
3012 + """
3013 + self.client.setHeader(
3014 + "Accept-Patch",
3015 + "application/x-www-form-urlencoded, multipart/form-data"
3016 )
3017 return 204, ""
3018
3019 @@ -430,7 +686,8 @@
3020 # 0 - rest
3021 # 1 - resource
3022 # 2 - attribute
3023 - resource_uri = uri.split("/")[1]
3024 + uri_split = uri.split("/")
3025 + resource_uri = uri_split[1]
3026
3027 # if X-HTTP-Method-Override is set, follow the override method
3028 headers = self.client.request.headers
3029 @@ -486,9 +743,14 @@
3030 )(resource_uri, input)
3031 else:
3032 class_name, item_id = hyperdb.splitDesignator(resource_uri)
3033 - response_code, output = getattr(
3034 - self, "%s_element" % method.lower()
3035 - )(class_name, item_id, input)
3036 + if len(uri_split) == 3:
3037 + response_code, output = getattr(
3038 + self, "%s_attribute" % method.lower()
3039 + )(class_name, item_id, uri_split[2], input)
3040 + else:
3041 + response_code, output = getattr(
3042 + self, "%s_element" % method.lower()
3043 + )(class_name, item_id, input)
3044 output = RestfulInstance.data_obj(output)
3045 self.client.response_code = response_code
3046 except IndexError, msg:
3047 # HG changeset patch
3048 # User Chau Nguyen <dangchau1991@yahoo.com>
3049 # Date 1435921733 -10800
3050 # Fri Jul 03 14:08:53 2015 +0300
3051 # Branch REST
3052 # Node ID 5a68dbfb8477cfad4c72d4a506affeecc6d1e19f
3053 # Parent c557036a1e23718a250dcf2c08e7752e5a2127ac
3054 Added rest unit test,
3055 Fixed a bug with printing error message,
3056 Patch operation remove now replace empty list instead of None
3057
3058 diff --git a/roundup/rest.py b/roundup/rest.py
3059 --- a/roundup/rest.py
3060 +++ b/roundup/rest.py
3061 @@ -152,7 +152,7 @@
3062 'View', self.db.getuid(), class_name, itemid=item_id
3063 ):
3064 raise Unauthorised(
3065 - 'Permission to view %s item %d denied' % (class_name, item_id)
3066 + 'Permission to view %s item %s denied' % (class_name, item_id)
3067 )
3068
3069 class_obj = self.db.getclass(class_name)
3070 @@ -379,7 +379,11 @@
3071 elif op == 'replace':
3072 pass
3073 elif op == 'remove':
3074 - props[prop] = None
3075 + current_prop = class_obj.get(item_id, prop)
3076 + if isinstance(current_prop, list):
3077 + props[prop] = []
3078 + else:
3079 + props[prop] = None
3080 else:
3081 raise UsageError('PATCH Operation %s is not allowed' % op)
3082
3083 diff --git a/test/test_rest.py b/test/test_rest.py
3084 new file mode 100644
3085 --- /dev/null
3086 +++ b/test/test_rest.py
3087 @@ -0,0 +1,280 @@
3088 +import unittest, os, shutil, errno, sys, difflib, cgi, re
3089 +
3090 +from xmlrpclib import MultiCall
3091 +from roundup.cgi.exceptions import *
3092 +from roundup import init, instance, password, hyperdb, date
3093 +from roundup.rest import RestfulInstance
3094 +from roundup.backends import list_backends
3095 +from roundup.hyperdb import String
3096 +from roundup.cgi import TranslationService, client
3097 +
3098 +import db_test_base
3099 +
3100 +NEEDS_INSTANCE = 1
3101 +
3102 +class TestCase(unittest.TestCase):
3103 +
3104 + backend = None
3105 +
3106 + def setUp(self):
3107 + self.dirname = '_test_rest'
3108 + # set up and open a tracker
3109 + self.instance = db_test_base.setupTracker(self.dirname, self.backend)
3110 +
3111 + # open the database
3112 + self.db = self.instance.open('admin')
3113 +
3114 + # Get user id (user4 maybe). Used later to get data from db.
3115 + self.joeid = self.db.user.create(
3116 + username='joe',
3117 + password=password.Password('random'),
3118 + address='random@home.org',
3119 + realname='Joe Random',
3120 + roles='User'
3121 + )
3122 +
3123 + self.db.commit()
3124 + self.db.close()
3125 + self.db = self.instance.open('joe')
3126 +
3127 + self.db.tx_Source = 'web'
3128 +
3129 + self.db.issue.addprop(tx_Source=hyperdb.String())
3130 + self.db.msg.addprop(tx_Source=hyperdb.String())
3131 +
3132 + self.db.post_init()
3133 +
3134 + thisdir = os.path.dirname(__file__)
3135 + vars = {}
3136 + execfile(os.path.join(thisdir, "tx_Source_detector.py"), vars)
3137 + vars['init'](self.db)
3138 +
3139 + env = {
3140 + 'PATH_INFO': 'http://localhost/rounduptest/rest/',
3141 + 'HTTP_HOST': 'localhost',
3142 + 'TRACKER_NAME': 'rounduptest'
3143 + }
3144 + dummy_client = client.Client(self.instance, None, env, [], None)
3145 +
3146 + self.server = RestfulInstance(dummy_client, self.db)
3147 +
3148 + def tearDown(self):
3149 + self.db.close()
3150 + try:
3151 + shutil.rmtree(self.dirname)
3152 + except OSError, error:
3153 + if error.errno not in (errno.ENOENT, errno.ESRCH):
3154 + raise
3155 +
3156 + def testGet(self):
3157 + """
3158 + Retrieve all three users
3159 + obtain data for 'joe'
3160 + """
3161 + # Retrieve all three users.
3162 + code, results = self.server.get_collection('user', {})
3163 + self.assertEqual(code, 200)
3164 + self.assertEqual(len(results), 3)
3165 +
3166 + # Obtain data for 'joe'.
3167 + code, results = self.server.get_element('user', self.joeid, {})
3168 + self.assertEqual(code, 200)
3169 + self.assertEqual(results['attributes']['username'], 'joe')
3170 + self.assertEqual(results['attributes']['realname'], 'Joe Random')
3171 +
3172 + def testPut(self):
3173 + """
3174 + Change joe's 'realname'
3175 + Check if we can't change admin's detail
3176 + """
3177 + # Reset joe's 'realname'.
3178 + form = cgi.FieldStorage()
3179 + form.list = [
3180 + cgi.MiniFieldStorage('realname', 'Joe Doe')
3181 + ]
3182 + code, results = self.server.put_element('user', self.joeid, form)
3183 + code, results = self.server.get_element('user', self.joeid, {})
3184 + self.assertEqual(code, 200)
3185 + self.assertEqual(results['attributes']['realname'], 'Joe Doe')
3186 +
3187 + # check we can't change admin's details
3188 + self.assertRaises(
3189 + Unauthorised,
3190 + self.server.put_element, 'user', '1', form
3191 + )
3192 +
3193 + def testPost(self):
3194 + """
3195 + Post a new issue with title: foo
3196 + Verify the information of the created issue
3197 + """
3198 + form = cgi.FieldStorage()
3199 + form.list = [
3200 + cgi.MiniFieldStorage('title', 'foo')
3201 + ]
3202 + code, results = self.server.post_collection('issue', form)
3203 + self.assertEqual(code, 201)
3204 + issueid = results['id']
3205 + code, results = self.server.get_element('issue', issueid, {})
3206 + self.assertEqual(code, 200)
3207 + self.assertEqual(results['attributes']['title'], 'foo')
3208 + self.assertEqual(self.db.issue.get(issueid, "tx_Source"), 'web')
3209 +
3210 + def testPostFile(self):
3211 + """
3212 + Post a new file with content: hello\r\nthere
3213 + Verify the information of the created file
3214 + """
3215 + form = cgi.FieldStorage()
3216 + form.list = [
3217 + cgi.MiniFieldStorage('content', 'hello\r\nthere')
3218 + ]
3219 + code, results = self.server.post_collection('file', form)
3220 + self.assertEqual(code, 201)
3221 + fileid = results['id']
3222 + code, results = self.server.get_element('file', fileid, {})
3223 + self.assertEqual(code, 200)
3224 + self.assertEqual(results['attributes']['content'], 'hello\r\nthere')
3225 +
3226 + def testAuthDeniedPut(self):
3227 + """
3228 + Test unauthorized PUT request
3229 + """
3230 + # Wrong permissions (caught by roundup security module).
3231 + form = cgi.FieldStorage()
3232 + form.list = [
3233 + cgi.MiniFieldStorage('realname', 'someone')
3234 + ]
3235 + self.assertRaises(
3236 + Unauthorised,
3237 + self.server.put_element, 'user', '1', form
3238 + )
3239 +
3240 + def testAuthDeniedPost(self):
3241 + """
3242 + Test unauthorized POST request
3243 + """
3244 + form = cgi.FieldStorage()
3245 + form.list = [
3246 + cgi.MiniFieldStorage('username', 'blah')
3247 + ]
3248 + self.assertRaises(
3249 + Unauthorised,
3250 + self.server.post_collection, 'user', form
3251 + )
3252 +
3253 + def testAuthAllowedPut(self):
3254 + """
3255 + Test authorized PUT request
3256 + """
3257 + self.db.setCurrentUser('admin')
3258 + form = cgi.FieldStorage()
3259 + form.list = [
3260 + cgi.MiniFieldStorage('realname', 'someone')
3261 + ]
3262 + try:
3263 + try:
3264 + self.server.put_element('user', '2', form)
3265 + except Unauthorised, err:
3266 + self.fail('raised %s' % err)
3267 + finally:
3268 + self.db.setCurrentUser('joe')
3269 +
3270 + def testAuthAllowedPost(self):
3271 + """
3272 + Test authorized POST request
3273 + """
3274 + self.db.setCurrentUser('admin')
3275 + form = cgi.FieldStorage()
3276 + form.list = [
3277 + cgi.MiniFieldStorage('username', 'blah')
3278 + ]
3279 + try:
3280 + try:
3281 + self.server.post_collection('user', form)
3282 + except Unauthorised, err:
3283 + self.fail('raised %s' % err)
3284 + finally:
3285 + self.db.setCurrentUser('joe')
3286 +
3287 + def testPatchAdd(self):
3288 + """
3289 + Test Patch op 'Add'
3290 + """
3291 + # create a new issue with userid 1 in the nosy list
3292 + issue_id = self.db.issue.create(title='foo', nosy=['1'])
3293 +
3294 + # add userid 2 to the nosy list
3295 + form = cgi.FieldStorage()
3296 + form.list = [
3297 + cgi.MiniFieldStorage('op', 'add'),
3298 + cgi.MiniFieldStorage('nosy', '2')
3299 + ]
3300 + code, results = self.server.patch_element('issue', issue_id, form)
3301 + self.assertEqual(code, 200)
3302 +
3303 + # verify the result
3304 + code, results = self.server.get_element('issue', issue_id, {})
3305 + self.assertEqual(code, 200)
3306 + self.assertEqual(len(results['attributes']['nosy']), 2)
3307 + self.assertListEqual(results['attributes']['nosy'], ['1', '2'])
3308 +
3309 + def testPatchReplace(self):
3310 + """
3311 + Test Patch op 'Replace'
3312 + """
3313 + # create a new issue with userid 1 in the nosy list and status = 1
3314 + issue_id = self.db.issue.create(title='foo', nosy=['1'], status='1')
3315 +
3316 + # replace userid 2 to the nosy list and status = 3
3317 + form = cgi.FieldStorage()
3318 + form.list = [
3319 + cgi.MiniFieldStorage('op', 'replace'),
3320 + cgi.MiniFieldStorage('nosy', '2'),
3321 + cgi.MiniFieldStorage('status', '3')
3322 + ]
3323 + code, results = self.server.patch_element('issue', issue_id, form)
3324 + self.assertEqual(code, 200)
3325 +
3326 + # verify the result
3327 + code, results = self.server.get_element('issue', issue_id, {})
3328 + self.assertEqual(code, 200)
3329 + self.assertEqual(results['attributes']['status'], '3')
3330 + self.assertEqual(len(results['attributes']['nosy']), 1)
3331 + self.assertListEqual(results['attributes']['nosy'], ['2'])
3332 +
3333 + def testPatchRemoveAll(self):
3334 + """
3335 + Test Patch Action 'Remove'
3336 + """
3337 + # create a new issue with userid 1 in the nosy list
3338 + issue_id = self.db.issue.create(title='foo', nosy=['1', '2'])
3339 +
3340 + # remove the nosy list and the title
3341 + form = cgi.FieldStorage()
3342 + form.list = [
3343 + cgi.MiniFieldStorage('op', 'remove'),
3344 + cgi.MiniFieldStorage('nosy', ''),
3345 + cgi.MiniFieldStorage('title', '')
3346 + ]
3347 + code, results = self.server.patch_element('issue', issue_id, form)
3348 + self.assertEqual(code, 200)
3349 +
3350 + # verify the result
3351 + code, results = self.server.get_element('issue', issue_id, {})
3352 + self.assertEqual(code, 200)
3353 + self.assertEqual(results['attributes']['title'], None)
3354 + self.assertEqual(len(results['attributes']['nosy']), 0)
3355 + self.assertEqual(results['attributes']['nosy'], [])
3356 +
3357 +def test_suite():
3358 + suite = unittest.TestSuite()
3359 + for l in list_backends():
3360 + dct = dict(backend=l)
3361 + subcls = type(TestCase)('TestCase_%s' % l, (TestCase,), dct)
3362 + suite.addTest(unittest.makeSuite(subcls))
3363 + return suite
3364 +
3365 +if __name__ == '__main__':
3366 + runner = unittest.TextTestRunner()
3367 + unittest.main(testRunner=runner)
3368 # HG changeset patch
3369 # User Chau Nguyen <dangchau1991@yahoo.com>
3370 # Date 1435450740 -10800
3371 # Sun Jun 28 03:19:00 2015 +0300
3372 # Branch REST
3373 # Node ID c557036a1e23718a250dcf2c08e7752e5a2127ac
3374 # Parent 058700d4f473a16b3b2f406171e96ee7b9d07ce5
3375 Added docstring
3376
3377 diff --git a/roundup/rest.py b/roundup/rest.py
3378 --- a/roundup/rest.py
3379 +++ b/roundup/rest.py
3380 @@ -18,8 +18,7 @@
3381
3382
3383 class RestfulInstance(object):
3384 - """Dummy Handler for REST
3385 - """
3386 + """The RestfulInstance performs REST request from the client"""
3387
3388 def __init__(self, client, db):
3389 self.client = client # it might be unnecessary to receive the client
3390 @@ -31,6 +30,18 @@
3391 self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker)
3392
3393 def props_from_args(self, cl, args, itemid=None):
3394 + """Construct a list of properties from the given arguments,
3395 + and return them after validation.
3396 +
3397 + Args:
3398 + cl (string): class object of the resource
3399 + args (list): the submitted form of the user
3400 + itemid (string, optional): itemid of the object
3401 +
3402 + Returns:
3403 + dict: dictionary of validated properties
3404 +
3405 + """
3406 class_props = cl.properties.keys()
3407 props = {}
3408 # props = dict.fromkeys(class_props, None)
3409 @@ -63,6 +74,8 @@
3410
3411 @staticmethod
3412 def error_obj(status, msg, source=None):
3413 + """Wrap the error data into an object. This function is temporally and
3414 + will be changed to a decorator later."""
3415 result = {
3416 'error': {
3417 'status': status,
3418 @@ -76,12 +89,29 @@
3419
3420 @staticmethod
3421 def data_obj(data):
3422 + """Wrap the returned data into an object. This function is temporally
3423 + and will be changed to a decorator later."""
3424 result = {
3425 'data': data
3426 }
3427 return result
3428
3429 def get_collection(self, class_name, input):
3430 + """GET resource from class URI.
3431 +
3432 + This function returns only items have View permission
3433 + class_name should be valid already
3434 +
3435 + Args:
3436 + class_name (string): class name of the resource (Ex: issue, msg)
3437 + input (list): the submitted form of the user
3438 +
3439 + Returns:
3440 + int: http status code 200 (OK)
3441 + list: list of reference item in the class
3442 + id: id of the object
3443 + link: path to the object
3444 + """
3445 if not self.db.security.hasPermission(
3446 'View', self.db.getuid(), class_name
3447 ):
3448 @@ -100,6 +130,24 @@
3449 return 200, result
3450
3451 def get_element(self, class_name, item_id, input):
3452 + """GET resource from object URI.
3453 +
3454 + This function returns only properties have View permission
3455 + class_name and item_id should be valid already
3456 +
3457 + Args:
3458 + class_name (string): class name of the resource (Ex: issue, msg)
3459 + item_id (string): id of the resource (Ex: 12, 15)
3460 + input (list): the submitted form of the user
3461 +
3462 + Returns:
3463 + int: http status code 200 (OK)
3464 + dict: a dictionary represents the object
3465 + id: id of the object
3466 + type: class name of the object
3467 + link: link to the object
3468 + attributes: a dictionary represent the attributes of the object
3469 + """
3470 if not self.db.security.hasPermission(
3471 'View', self.db.getuid(), class_name, itemid=item_id
3472 ):
3473 @@ -127,6 +175,21 @@
3474 return 200, result
3475
3476 def post_collection(self, class_name, input):
3477 + """POST a new object to a class
3478 +
3479 + If the item is successfully created, the "Location" header will also
3480 + contain the link to the created object
3481 +
3482 + Args:
3483 + class_name (string): class name of the resource (Ex: issue, msg)
3484 + input (list): the submitted form of the user
3485 +
3486 + Returns:
3487 + int: http status code 201 (Created)
3488 + dict: a reference item to the created object
3489 + id: id of the object
3490 + link: path to the object
3491 + """
3492 if not self.db.security.hasPermission(
3493 'Create', self.db.getuid(), class_name
3494 ):
3495 @@ -171,12 +234,32 @@
3496 return 201, result
3497
3498 def post_element(self, class_name, item_id, input):
3499 + """POST to an object of a class is not allowed"""
3500 raise Reject('POST to an item is not allowed')
3501
3502 def put_collection(self, class_name, input):
3503 + """PUT a class is not allowed"""
3504 raise Reject('PUT a class is not allowed')
3505
3506 def put_element(self, class_name, item_id, input):
3507 + """PUT a new content to an object
3508 +
3509 + Replace the content of the existing object
3510 +
3511 + Args:
3512 + class_name (string): class name of the resource (Ex: issue, msg)
3513 + item_id (string): id of the resource (Ex: 12, 15)
3514 + input (list): the submitted form of the user
3515 +
3516 + Returns:
3517 + int: http status code 200 (OK)
3518 + dict: a dictionary represents the modified object
3519 + id: id of the object
3520 + type: class name of the object
3521 + link: link to the object
3522 + attributes: a dictionary represent only changed attributes of
3523 + the object
3524 + """
3525 class_obj = self.db.getclass(class_name)
3526
3527 props = self.props_from_args(class_obj, input.value, item_id)
3528 @@ -203,6 +286,18 @@
3529 return 200, result
3530
3531 def delete_collection(self, class_name, input):
3532 + """DELETE all objects in a class
3533 +
3534 + Args:
3535 + class_name (string): class name of the resource (Ex: issue, msg)
3536 + input (list): the submitted form of the user
3537 +
3538 + Returns:
3539 + int: http status code 200 (OK)
3540 + dict:
3541 + status (string): 'ok'
3542 + count (int): number of deleted objects
3543 + """
3544 if not self.db.security.hasPermission(
3545 'Delete', self.db.getuid(), class_name
3546 ):
3547 @@ -230,6 +325,18 @@
3548 return 200, result
3549
3550 def delete_element(self, class_name, item_id, input):
3551 + """DELETE an object in a class
3552 +
3553 + Args:
3554 + class_name (string): class name of the resource (Ex: issue, msg)
3555 + item_id (string): id of the resource (Ex: 12, 15)
3556 + input (list): the submitted form of the user
3557 +
3558 + Returns:
3559 + int: http status code 200 (OK)
3560 + dict:
3561 + status (string): 'ok'
3562 + """
3563 if not self.db.security.hasPermission(
3564 'Delete', self.db.getuid(), class_name, itemid=item_id
3565 ):
3566 @@ -246,6 +353,7 @@
3567 return 200, result
3568
3569 def patch_collection(self, class_name, input):
3570 + """PATCH a class is not allowed"""
3571 raise Reject('PATCH a class is not allowed')
3572
3573 def patch_element(self, class_name, item_id, input):
3574 @@ -290,9 +398,21 @@
3575 return 200, result
3576
3577 def options_collection(self, class_name, input):
3578 + """OPTION return the HTTP Header for the class uri
3579 +
3580 + Returns:
3581 + int: http status code 204 (No content)
3582 + body (string): an empty string
3583 + """
3584 return 204, ""
3585
3586 def options_element(self, class_name, item_id, input):
3587 + """OPTION return the HTTP Header for the object uri
3588 +
3589 + Returns:
3590 + int: http status code 204 (No content)
3591 + body (string): an empty string
3592 + """
3593 self.client.setHeader(
3594 "Accept-Patch",
3595 "application/x-www-form-urlencoded, "
3596 @@ -301,6 +421,7 @@
3597 return 204, ""
3598
3599 def dispatch(self, method, uri, input):
3600 + """format and process the request"""
3601 # PATH is split to multiple pieces
3602 # 0 - rest
3603 # 1 - resource
3604 @@ -327,40 +448,43 @@
3605 except KeyError:
3606 pretty_output = False
3607
3608 + # add access-control-allow-* to support CORS
3609 self.client.setHeader("Access-Control-Allow-Origin", "*")
3610 self.client.setHeader(
3611 "Access-Control-Allow-Headers",
3612 "Content-Type, Authorization, X-HTTP-Method-Override"
3613 )
3614 + if resource_uri in self.db.classes:
3615 + self.client.setHeader(
3616 + "Allow",
3617 + "HEAD, OPTIONS, GET, POST, DELETE"
3618 + )
3619 + self.client.setHeader(
3620 + "Access-Control-Allow-Methods",
3621 + "HEAD, OPTIONS, GET, POST, DELETE"
3622 + )
3623 + else:
3624 + self.client.setHeader(
3625 + "Allow",
3626 + "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
3627 + )
3628 + self.client.setHeader(
3629 + "Access-Control-Allow-Methods",
3630 + "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
3631 + )
3632
3633 + # Call the appropriate method
3634 output = None
3635 try:
3636 if resource_uri in self.db.classes:
3637 - self.client.setHeader(
3638 - "Allow",
3639 - "HEAD, OPTIONS, GET, POST, DELETE"
3640 - )
3641 - self.client.setHeader(
3642 - "Access-Control-Allow-Methods",
3643 - "HEAD, OPTIONS, GET, POST, DELETE"
3644 - )
3645 response_code, output = getattr(
3646 self, "%s_collection" % method.lower()
3647 )(resource_uri, input)
3648 else:
3649 class_name, item_id = hyperdb.splitDesignator(resource_uri)
3650 - self.client.setHeader(
3651 - "Allow",
3652 - "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
3653 - )
3654 - self.client.setHeader(
3655 - "Access-Control-Allow-Methods",
3656 - "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
3657 - )
3658 response_code, output = getattr(
3659 self, "%s_element" % method.lower()
3660 )(class_name, item_id, input)
3661 -
3662 output = RestfulInstance.data_obj(output)
3663 self.client.response_code = response_code
3664 except IndexError, msg:
3665 @@ -408,6 +532,8 @@
3666
3667
3668 class RoundupJSONEncoder(json.JSONEncoder):
3669 + """RoundupJSONEncoder overrides the default JSONEncoder to handle all
3670 + types of the object without returning any error"""
3671 def default(self, obj):
3672 try:
3673 result = json.JSONEncoder.default(self, obj)
3674 # HG changeset patch
3675 # User Chau Nguyen <dangchau1991@yahoo.com>
3676 # Date 1435446967 -10800
3677 # Sun Jun 28 02:16:07 2015 +0300
3678 # Branch REST
3679 # Node ID 058700d4f473a16b3b2f406171e96ee7b9d07ce5
3680 # Parent 864fcf43ccea0e3b47cd6bcf698be17a890b0263
3681 Code convention improved
3682
3683 diff --git a/roundup/rest.py b/roundup/rest.py
3684 --- a/roundup/rest.py
3685 +++ b/roundup/rest.py
3686 @@ -17,54 +17,6 @@
3687 from roundup import xmlrpc
3688
3689
3690 -def props_from_args(db, cl, args, itemid=None):
3691 - class_props = cl.properties.keys()
3692 - props = {}
3693 - # props = dict.fromkeys(class_props, None)
3694 -
3695 - for arg in args:
3696 - key = arg.name
3697 - value = arg.value
3698 - if key not in class_props:
3699 - continue
3700 - if isinstance(key, unicode):
3701 - try:
3702 - key = key.encode('ascii')
3703 - except UnicodeEncodeError:
3704 - raise UsageError('argument %r is no valid ascii keyword' % key)
3705 - if isinstance(value, unicode):
3706 - value = value.encode('utf-8')
3707 - if value:
3708 - try:
3709 - props[key] = hyperdb.rawToHyperdb(db, cl, itemid, key, value)
3710 - except hyperdb.HyperdbValueError, msg:
3711 - raise UsageError(msg)
3712 - else:
3713 - props[key] = None
3714 -
3715 - return props
3716 -
3717 -
3718 -def error_obj(status, msg, source=None):
3719 - result = {
3720 - 'error': {
3721 - 'status': status,
3722 - 'msg': msg
3723 - }
3724 - }
3725 - if source is not None:
3726 - result['error']['source'] = source
3727 -
3728 - return result
3729 -
3730 -
3731 -def data_obj(data):
3732 - result = {
3733 - 'data': data
3734 - }
3735 - return result
3736 -
3737 -
3738 class RestfulInstance(object):
3739 """Dummy Handler for REST
3740 """
3741 @@ -78,33 +30,93 @@
3742 tracker = self.client.env['TRACKER_NAME']
3743 self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker)
3744
3745 + def props_from_args(self, cl, args, itemid=None):
3746 + class_props = cl.properties.keys()
3747 + props = {}
3748 + # props = dict.fromkeys(class_props, None)
3749 +
3750 + for arg in args:
3751 + key = arg.name
3752 + value = arg.value
3753 + if key not in class_props:
3754 + continue
3755 + if isinstance(key, unicode):
3756 + try:
3757 + key = key.encode('ascii')
3758 + except UnicodeEncodeError:
3759 + raise UsageError(
3760 + 'argument %r is no valid ascii keyword' % key
3761 + )
3762 + if isinstance(value, unicode):
3763 + value = value.encode('utf-8')
3764 + if value:
3765 + try:
3766 + props[key] = hyperdb.rawToHyperdb(
3767 + self.db, cl, itemid, key, value
3768 + )
3769 + except hyperdb.HyperdbValueError, msg:
3770 + raise UsageError(msg)
3771 + else:
3772 + props[key] = None
3773 +
3774 + return props
3775 +
3776 + @staticmethod
3777 + def error_obj(status, msg, source=None):
3778 + result = {
3779 + 'error': {
3780 + 'status': status,
3781 + 'msg': msg
3782 + }
3783 + }
3784 + if source is not None:
3785 + result['error']['source'] = source
3786 +
3787 + return result
3788 +
3789 + @staticmethod
3790 + def data_obj(data):
3791 + result = {
3792 + 'data': data
3793 + }
3794 + return result
3795 +
3796 def get_collection(self, class_name, input):
3797 - if not self.db.security.hasPermission('View', self.db.getuid(),
3798 - class_name):
3799 + if not self.db.security.hasPermission(
3800 + 'View', self.db.getuid(), class_name
3801 + ):
3802 raise Unauthorised('Permission to view %s denied' % class_name)
3803 +
3804 class_obj = self.db.getclass(class_name)
3805 class_path = self.base_path + class_name
3806 - result = [{'id': item_id, 'link': class_path + item_id}
3807 - for item_id in class_obj.list()
3808 - if self.db.security.hasPermission('View', self.db.getuid(),
3809 - class_name,
3810 - itemid=item_id)]
3811 + result = [
3812 + {'id': item_id, 'link': class_path + item_id}
3813 + for item_id in class_obj.list()
3814 + if self.db.security.hasPermission(
3815 + 'View', self.db.getuid(), class_name, itemid=item_id
3816 + )
3817 + ]
3818 self.client.setHeader("X-Count-Total", str(len(result)))
3819 return 200, result
3820
3821 def get_element(self, class_name, item_id, input):
3822 - if not self.db.security.hasPermission('View', self.db.getuid(),
3823 - class_name, itemid=item_id):
3824 - raise Unauthorised('Permission to view %s item %d denied' %
3825 - (class_name, item_id))
3826 + if not self.db.security.hasPermission(
3827 + 'View', self.db.getuid(), class_name, itemid=item_id
3828 + ):
3829 + raise Unauthorised(
3830 + 'Permission to view %s item %d denied' % (class_name, item_id)
3831 + )
3832 +
3833 class_obj = self.db.getclass(class_name)
3834 props = class_obj.properties.keys()
3835 props.sort() # sort properties
3836 - result = [(prop_name, class_obj.get(item_id, prop_name))
3837 - for prop_name in props
3838 - if self.db.security.hasPermission('View', self.db.getuid(),
3839 - class_name, prop_name,
3840 - item_id)]
3841 + result = [
3842 + (prop_name, class_obj.get(item_id, prop_name))
3843 + for prop_name in props
3844 + if self.db.security.hasPermission(
3845 + 'View', self.db.getuid(), class_name, prop_name,
3846 + )
3847 + ]
3848 result = {
3849 'id': item_id,
3850 'type': class_name,
3851 @@ -115,14 +127,15 @@
3852 return 200, result
3853
3854 def post_collection(self, class_name, input):
3855 - if not self.db.security.hasPermission('Create', self.db.getuid(),
3856 - class_name):
3857 + if not self.db.security.hasPermission(
3858 + 'Create', self.db.getuid(), class_name
3859 + ):
3860 raise Unauthorised('Permission to create %s denied' % class_name)
3861
3862 class_obj = self.db.getclass(class_name)
3863
3864 # convert types
3865 - props = props_from_args(self.db, class_obj, input.value)
3866 + props = self.props_from_args(class_obj, input.value)
3867
3868 # check for the key property
3869 key = class_obj.getkey()
3870 @@ -130,10 +143,12 @@
3871 raise UsageError("Must provide the '%s' property." % key)
3872
3873 for key in props:
3874 - if not self.db.security.hasPermission('Create', self.db.getuid(),
3875 - class_name, property=key):
3876 - raise Unauthorised('Permission to create %s.%s denied' %
3877 - (class_name, key))
3878 + if not self.db.security.hasPermission(
3879 + 'Create', self.db.getuid(), class_name, property=key
3880 + ):
3881 + raise Unauthorised(
3882 + 'Permission to create %s.%s denied' % (class_name, key)
3883 + )
3884
3885 # do the actual create
3886 try:
3887 @@ -164,12 +179,15 @@
3888 def put_element(self, class_name, item_id, input):
3889 class_obj = self.db.getclass(class_name)
3890
3891 - props = props_from_args(self.db, class_obj, input.value, item_id)
3892 + props = self.props_from_args(class_obj, input.value, item_id)
3893 for p in props.iterkeys():
3894 - if not self.db.security.hasPermission('Edit', self.db.getuid(),
3895 - class_name, p, item_id):
3896 - raise Unauthorised('Permission to edit %s of %s%s denied' %
3897 - (p, class_name, item_id))
3898 + if not self.db.security.hasPermission(
3899 + 'Edit', self.db.getuid(), class_name, p, item_id
3900 + ):
3901 + raise Unauthorised(
3902 + 'Permission to edit %s of %s%s denied' %
3903 + (p, class_name, item_id)
3904 + )
3905 try:
3906 result = class_obj.set(item_id, **props)
3907 self.db.commit()
3908 @@ -185,16 +203,19 @@
3909 return 200, result
3910
3911 def delete_collection(self, class_name, input):
3912 - if not self.db.security.hasPermission('Delete', self.db.getuid(),
3913 - class_name):
3914 + if not self.db.security.hasPermission(
3915 + 'Delete', self.db.getuid(), class_name
3916 + ):
3917 raise Unauthorised('Permission to delete %s denied' % class_name)
3918
3919 class_obj = self.db.getclass(class_name)
3920 for item_id in class_obj.list():
3921 - if not self.db.security.hasPermission('Delete', self.db.getuid(),
3922 - class_name, itemid=item_id):
3923 - raise Unauthorised('Permission to delete %s %s denied' %
3924 - (class_name, item_id))
3925 + if not self.db.security.hasPermission(
3926 + 'Delete', self.db.getuid(), class_name, itemid=item_id
3927 + ):
3928 + raise Unauthorised(
3929 + 'Permission to delete %s %s denied' % (class_name, item_id)
3930 + )
3931
3932 count = len(class_obj.list())
3933 for item_id in class_obj.list():
3934 @@ -209,10 +230,12 @@
3935 return 200, result
3936
3937 def delete_element(self, class_name, item_id, input):
3938 - if not self.db.security.hasPermission('Delete', self.db.getuid(),
3939 - class_name, itemid=item_id):
3940 - raise Unauthorised('Permission to delete %s %s denied' %
3941 - (class_name, item_id))
3942 + if not self.db.security.hasPermission(
3943 + 'Delete', self.db.getuid(), class_name, itemid=item_id
3944 + ):
3945 + raise Unauthorised(
3946 + 'Permission to delete %s %s denied' % (class_name, item_id)
3947 + )
3948
3949 self.db.destroynode(class_name, item_id)
3950 self.db.commit()
3951 @@ -232,13 +255,17 @@
3952 op = "replace"
3953 class_obj = self.db.getclass(class_name)
3954
3955 - props = props_from_args(self.db, class_obj, input.value, item_id)
3956 + props = self.props_from_args(class_obj, input.value, item_id)
3957
3958 for prop, value in props.iteritems():
3959 - if not self.db.security.hasPermission('Edit', self.db.getuid(),
3960 - class_name, prop, item_id):
3961 - raise Unauthorised('Permission to edit %s of %s%s denied' %
3962 - (prop, class_name, item_id))
3963 + if not self.db.security.hasPermission(
3964 + 'Edit', self.db.getuid(), class_name, prop, item_id
3965 + ):
3966 + raise Unauthorised(
3967 + 'Permission to edit %s of %s%s denied' %
3968 + (prop, class_name, item_id)
3969 + )
3970 +
3971 if op == 'add':
3972 props[prop] = class_obj.get(item_id, prop) + props[prop]
3973 elif op == 'replace':
3974 @@ -266,9 +293,11 @@
3975 return 204, ""
3976
3977 def options_element(self, class_name, item_id, input):
3978 - self.client.setHeader("Accept-Patch",
3979 - "application/x-www-form-urlencoded, "
3980 - "multipart/form-data")
3981 + self.client.setHeader(
3982 + "Accept-Patch",
3983 + "application/x-www-form-urlencoded, "
3984 + "multipart/form-data"
3985 + )
3986 return 204, ""
3987
3988 def dispatch(self, method, uri, input):
3989 @@ -279,7 +308,8 @@
3990 resource_uri = uri.split("/")[1]
3991
3992 # if X-HTTP-Method-Override is set, follow the override method
3993 - method = self.client.request.headers.getheader('X-HTTP-Method-Override') or method
3994 + headers = self.client.request.headers
3995 + method = headers.getheader('X-HTTP-Method-Override') or method
3996
3997 # get the request format for response
3998 # priority : extension from uri (/rest/issue.json),
3999 @@ -288,7 +318,7 @@
4000
4001 # format_header need a priority parser
4002 format_ext = os.path.splitext(urlparse.urlparse(uri).path)[1][1:]
4003 - format_header = self.client.request.headers.getheader('Accept')[12:]
4004 + format_header = headers.getheader('Accept')[12:]
4005 format_output = format_ext or format_header or "json"
4006
4007 # check for pretty print
4008 @@ -298,54 +328,65 @@
4009 pretty_output = False
4010
4011 self.client.setHeader("Access-Control-Allow-Origin", "*")
4012 - self.client.setHeader("Access-Control-Allow-Headers",
4013 - "Content-Type, Authorization, "
4014 - "X-HTTP-Method-Override")
4015 + self.client.setHeader(
4016 + "Access-Control-Allow-Headers",
4017 + "Content-Type, Authorization, X-HTTP-Method-Override"
4018 + )
4019
4020 output = None
4021 try:
4022 if resource_uri in self.db.classes:
4023 - self.client.setHeader("Allow",
4024 - "HEAD, OPTIONS, GET, POST, DELETE")
4025 - self.client.setHeader("Access-Control-Allow-Methods",
4026 - "HEAD, OPTIONS, GET, POST, DELETE")
4027 - response_code, output = getattr(self, "%s_collection" % method.lower())(
4028 - resource_uri, input)
4029 + self.client.setHeader(
4030 + "Allow",
4031 + "HEAD, OPTIONS, GET, POST, DELETE"
4032 + )
4033 + self.client.setHeader(
4034 + "Access-Control-Allow-Methods",
4035 + "HEAD, OPTIONS, GET, POST, DELETE"
4036 + )
4037 + response_code, output = getattr(
4038 + self, "%s_collection" % method.lower()
4039 + )(resource_uri, input)
4040 else:
4041 class_name, item_id = hyperdb.splitDesignator(resource_uri)
4042 - self.client.setHeader("Allow",
4043 - "HEAD, OPTIONS, GET, PUT, DELETE, PATCH")
4044 - self.client.setHeader("Access-Control-Allow-Methods",
4045 - "HEAD, OPTIONS, GET, PUT, DELETE, PATCH")
4046 - response_code, output = getattr(self, "%s_element" % method.lower())(
4047 - class_name, item_id, input)
4048 + self.client.setHeader(
4049 + "Allow",
4050 + "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
4051 + )
4052 + self.client.setHeader(
4053 + "Access-Control-Allow-Methods",
4054 + "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
4055 + )
4056 + response_code, output = getattr(
4057 + self, "%s_element" % method.lower()
4058 + )(class_name, item_id, input)
4059
4060 - output = data_obj(output)
4061 + output = RestfulInstance.data_obj(output)
4062 self.client.response_code = response_code
4063 except IndexError, msg:
4064 - output = error_obj(404, msg)
4065 + output = RestfulInstance.error_obj(404, msg)
4066 self.client.response_code = 404
4067 except Unauthorised, msg:
4068 - output = error_obj(403, msg)
4069 + output = RestfulInstance.error_obj(403, msg)
4070 self.client.response_code = 403
4071 except (hyperdb.DesignatorError, UsageError), msg:
4072 - output = error_obj(400, msg)
4073 + output = RestfulInstance.error_obj(400, msg)
4074 self.client.response_code = 400
4075 except (AttributeError, Reject), msg:
4076 - output = error_obj(405, msg)
4077 + output = RestfulInstance.error_obj(405, msg)
4078 self.client.response_code = 405
4079 except ValueError, msg:
4080 - output = error_obj(409, msg)
4081 + output = RestfulInstance.error_obj(409, msg)
4082 self.client.response_code = 409
4083 except NotImplementedError:
4084 - output = error_obj(402, 'Method is under development')
4085 + output = RestfulInstance.error_obj(402, 'Method under development')
4086 self.client.response_code = 402
4087 # nothing to pay, just a mark for debugging purpose
4088 except:
4089 # if self.DEBUG_MODE in roundup_server
4090 # else msg = 'An error occurred. Please check...',
4091 exc, val, tb = sys.exc_info()
4092 - output = error_obj(400, val)
4093 + output = RestfulInstance.error_obj(400, val)
4094 self.client.response_code = 400
4095
4096 # out to the logfile, it would be nice if the server do it for me
4097 @@ -361,7 +402,7 @@
4098 output = RoundupJSONEncoder(indent=indent).encode(output)
4099 else:
4100 self.client.response_code = 406
4101 - output = ""
4102 + output = "Content type is not accepted by client"
4103
4104 return output
4105
4106 # HG changeset patch
4107 # User Chau Nguyen <dangchau1991@yahoo.com>
4108 # Date 1435412237 -10800
4109 # Sat Jun 27 16:37:17 2015 +0300
4110 # Branch REST
4111 # Node ID 864fcf43ccea0e3b47cd6bcf698be17a890b0263
4112 # Parent 4d757555035f0c82947813e54d31e887168a2ed8
4113 Add default op action for Patch
4114
4115 diff --git a/roundup/rest.py b/roundup/rest.py
4116 --- a/roundup/rest.py
4117 +++ b/roundup/rest.py
4118 @@ -226,7 +226,10 @@
4119 raise Reject('PATCH a class is not allowed')
4120
4121 def patch_element(self, class_name, item_id, input):
4122 - op = input['op'].value.lower()
4123 + try:
4124 + op = input['op'].value.lower()
4125 + except KeyError:
4126 + op = "replace"
4127 class_obj = self.db.getclass(class_name)
4128
4129 props = props_from_args(self.db, class_obj, input.value, item_id)
4130 # HG changeset patch
4131 # User Chau Nguyen <dangchau1991@yahoo.com>
4132 # Date 1435302985 -10800
4133 # Fri Jun 26 10:16:25 2015 +0300
4134 # Branch REST
4135 # Node ID 4d757555035f0c82947813e54d31e887168a2ed8
4136 # Parent 362340ad2d35bd07e53dbb13cd4b74fafdbedd08
4137 Improve props_from_args to skip initializing invalid property of the class.
4138
4139 diff --git a/roundup/rest.py b/roundup/rest.py
4140 --- a/roundup/rest.py
4141 +++ b/roundup/rest.py
4142 @@ -18,13 +18,15 @@
4143
4144
4145 def props_from_args(db, cl, args, itemid=None):
4146 + class_props = cl.properties.keys()
4147 props = {}
4148 + # props = dict.fromkeys(class_props, None)
4149 +
4150 for arg in args:
4151 - try:
4152 - key = arg.name
4153 - value = arg.value
4154 - except ValueError:
4155 - raise UsageError('argument "%s" not propname=value' % arg)
4156 + key = arg.name
4157 + value = arg.value
4158 + if key not in class_props:
4159 + continue
4160 if isinstance(key, unicode):
4161 try:
4162 key = key.encode('ascii')
4163 @@ -35,8 +37,8 @@
4164 if value:
4165 try:
4166 props[key] = hyperdb.rawToHyperdb(db, cl, itemid, key, value)
4167 - except hyperdb.HyperdbValueError:
4168 - pass # pass if a parameter is not a property of the class
4169 + except hyperdb.HyperdbValueError, msg:
4170 + raise UsageError(msg)
4171 else:
4172 props[key] = None
4173
4174 # HG changeset patch
4175 # User Chau Nguyen <dangchau1991@yahoo.com>
4176 # Date 1435238667 -10800
4177 # Thu Jun 25 16:24:27 2015 +0300
4178 # Branch REST
4179 # Node ID 362340ad2d35bd07e53dbb13cd4b74fafdbedd08
4180 # Parent 1f374536414e96108b2b6ccc92d051f0fbb5d903
4181 Added PATCH an element
4182
4183 diff --git a/roundup/rest.py b/roundup/rest.py
4184 --- a/roundup/rest.py
4185 +++ b/roundup/rest.py
4186 @@ -224,7 +224,38 @@
4187 raise Reject('PATCH a class is not allowed')
4188
4189 def patch_element(self, class_name, item_id, input):
4190 - raise NotImplementedError
4191 + op = input['op'].value.lower()
4192 + class_obj = self.db.getclass(class_name)
4193 +
4194 + props = props_from_args(self.db, class_obj, input.value, item_id)
4195 +
4196 + for prop, value in props.iteritems():
4197 + if not self.db.security.hasPermission('Edit', self.db.getuid(),
4198 + class_name, prop, item_id):
4199 + raise Unauthorised('Permission to edit %s of %s%s denied' %
4200 + (prop, class_name, item_id))
4201 + if op == 'add':
4202 + props[prop] = class_obj.get(item_id, prop) + props[prop]
4203 + elif op == 'replace':
4204 + pass
4205 + elif op == 'remove':
4206 + props[prop] = None
4207 + else:
4208 + raise UsageError('PATCH Operation %s is not allowed' % op)
4209 +
4210 + try:
4211 + result = class_obj.set(item_id, **props)
4212 + self.db.commit()
4213 + except (TypeError, IndexError, ValueError), message:
4214 + raise ValueError(message)
4215 +
4216 + result = {
4217 + 'id': item_id,
4218 + 'type': class_name,
4219 + 'link': self.base_path + class_name + item_id,
4220 + 'attribute': result
4221 + }
4222 + return 200, result
4223
4224 def options_collection(self, class_name, input):
4225 return 204, ""
4226 # HG changeset patch
4227 # User Chau Nguyen <dangchau1991@yahoo.com>
4228 # Date 1435223805 -10800
4229 # Thu Jun 25 12:16:45 2015 +0300
4230 # Branch REST
4231 # Node ID 1f374536414e96108b2b6ccc92d051f0fbb5d903
4232 # Parent 1065a2ab9d97e1abfe9d5dfd89746aa61e5e4320
4233 Added pretty print
4234
4235 diff --git a/roundup/rest.py b/roundup/rest.py
4236 --- a/roundup/rest.py
4237 +++ b/roundup/rest.py
4238 @@ -255,6 +255,12 @@
4239 format_header = self.client.request.headers.getheader('Accept')[12:]
4240 format_output = format_ext or format_header or "json"
4241
4242 + # check for pretty print
4243 + try:
4244 + pretty_output = input['pretty'].value.lower() == "true"
4245 + except KeyError:
4246 + pretty_output = False
4247 +
4248 self.client.setHeader("Access-Control-Allow-Origin", "*")
4249 self.client.setHeader("Access-Control-Allow-Headers",
4250 "Content-Type, Authorization, "
4251 @@ -312,7 +318,11 @@
4252 finally:
4253 if format_output.lower() == "json":
4254 self.client.setHeader("Content-Type", "application/json")
4255 - output = RoundupJSONEncoder().encode(output)
4256 + if pretty_output:
4257 + indent = 4
4258 + else:
4259 + indent = None
4260 + output = RoundupJSONEncoder(indent=indent).encode(output)
4261 else:
4262 self.client.response_code = 406
4263 output = ""
4264 # HG changeset patch
4265 # User Chau Nguyen <dangchau1991@yahoo.com>
4266 # Date 1435220660 -10800
4267 # Thu Jun 25 11:24:20 2015 +0300
4268 # Branch REST
4269 # Node ID 1065a2ab9d97e1abfe9d5dfd89746aa61e5e4320
4270 # Parent 04b0fed104972fec2c4980c54337267a4db5f2a6
4271 Handle a case where KeyError exception raise uncaught,
4272 Changed some UsageError exception to ValueError exception to handle 409 Conflicted error.
4273
4274 diff --git a/roundup/rest.py b/roundup/rest.py
4275 --- a/roundup/rest.py
4276 +++ b/roundup/rest.py
4277 @@ -125,7 +125,7 @@
4278 # check for the key property
4279 key = class_obj.getkey()
4280 if key and key not in props:
4281 - raise UsageError('Must provide the "%s" property.' % key)
4282 + raise UsageError("Must provide the '%s' property." % key)
4283
4284 for key in props:
4285 if not self.db.security.hasPermission('Create', self.db.getuid(),
4286 @@ -138,7 +138,9 @@
4287 item_id = class_obj.create(**props)
4288 self.db.commit()
4289 except (TypeError, IndexError, ValueError), message:
4290 - raise UsageError(message)
4291 + raise ValueError(message)
4292 + except KeyError, msg:
4293 + raise UsageError("Must provide the %s property." % msg)
4294
4295 # set the header Location
4296 link = self.base_path + class_name + item_id
4297 @@ -152,10 +154,10 @@
4298 return 201, result
4299
4300 def post_element(self, class_name, item_id, input):
4301 - raise Reject('Invalid request')
4302 + raise Reject('POST to an item is not allowed')
4303
4304 def put_collection(self, class_name, input):
4305 - raise Reject('Invalid request')
4306 + raise Reject('PUT a class is not allowed')
4307
4308 def put_element(self, class_name, item_id, input):
4309 class_obj = self.db.getclass(class_name)
4310 @@ -170,7 +172,7 @@
4311 result = class_obj.set(item_id, **props)
4312 self.db.commit()
4313 except (TypeError, IndexError, ValueError), message:
4314 - raise UsageError(message)
4315 + raise ValueError(message)
4316
4317 result = {
4318 'id': item_id,
4319 @@ -219,7 +221,7 @@
4320 return 200, result
4321
4322 def patch_collection(self, class_name, input):
4323 - raise Reject('Invalid request')
4324 + raise Reject('PATCH a class is not allowed')
4325
4326 def patch_element(self, class_name, item_id, input):
4327 raise NotImplementedError
4328 @@ -288,8 +290,11 @@
4329 output = error_obj(400, msg)
4330 self.client.response_code = 400
4331 except (AttributeError, Reject), msg:
4332 - output = error_obj(405, 'Method Not Allowed. ' + str(msg))
4333 + output = error_obj(405, msg)
4334 self.client.response_code = 405
4335 + except ValueError, msg:
4336 + output = error_obj(409, msg)
4337 + self.client.response_code = 409
4338 except NotImplementedError:
4339 output = error_obj(402, 'Method is under development')
4340 self.client.response_code = 402
4341 # HG changeset patch
4342 # User Chau Nguyen <dangchau1991@yahoo.com>
4343 # Date 1435152613 -10800
4344 # Wed Jun 24 16:30:13 2015 +0300
4345 # Branch REST
4346 # Node ID 04b0fed104972fec2c4980c54337267a4db5f2a6
4347 # Parent f51de558ba050a279dedde3000b5cdbc39ba9206
4348 Added OPTIONS method,
4349 Fix a bug with class response header not allowing GET
4350
4351 diff --git a/roundup/rest.py b/roundup/rest.py
4352 --- a/roundup/rest.py
4353 +++ b/roundup/rest.py
4354 @@ -224,6 +224,15 @@
4355 def patch_element(self, class_name, item_id, input):
4356 raise NotImplementedError
4357
4358 + def options_collection(self, class_name, input):
4359 + return 204, ""
4360 +
4361 + def options_element(self, class_name, item_id, input):
4362 + self.client.setHeader("Accept-Patch",
4363 + "application/x-www-form-urlencoded, "
4364 + "multipart/form-data")
4365 + return 204, ""
4366 +
4367 def dispatch(self, method, uri, input):
4368 # PATH is split to multiple pieces
4369 # 0 - rest
4370 @@ -253,9 +262,9 @@
4371 try:
4372 if resource_uri in self.db.classes:
4373 self.client.setHeader("Allow",
4374 - "HEAD, OPTIONS, POST, DELETE")
4375 + "HEAD, OPTIONS, GET, POST, DELETE")
4376 self.client.setHeader("Access-Control-Allow-Methods",
4377 - "HEAD, OPTIONS, POST, DELETE")
4378 + "HEAD, OPTIONS, GET, POST, DELETE")
4379 response_code, output = getattr(self, "%s_collection" % method.lower())(
4380 resource_uri, input)
4381 else:
4382 diff --git a/roundup/scripts/roundup_server.py b/roundup/scripts/roundup_server.py
4383 --- a/roundup/scripts/roundup_server.py
4384 +++ b/roundup/scripts/roundup_server.py
4385 @@ -251,7 +251,7 @@
4386 else:
4387 return self.run_cgi()
4388
4389 - do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = do_PATCH = run_cgi_outer
4390 + do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = do_PATCH = do_OPTIONS = run_cgi_outer
4391
4392 def index(self):
4393 ''' Print up an index of the available trackers
4394 # HG changeset patch
4395 # User Chau Nguyen <dangchau1991@yahoo.com>
4396 # Date 1435148811 -10800
4397 # Wed Jun 24 15:26:51 2015 +0300
4398 # Branch REST
4399 # Node ID f51de558ba050a279dedde3000b5cdbc39ba9206
4400 # Parent 08558a8b5f28f4b71b6dcedfd991e79e032acc24
4401 Added X-Count-Total for pagination of GET method,
4402 Parse X-HTTP-Method-Override from client,
4403 Parse Request Accept content-type from URL and Header
4404
4405 diff --git a/roundup/rest.py b/roundup/rest.py
4406 --- a/roundup/rest.py
4407 +++ b/roundup/rest.py
4408 @@ -5,6 +5,8 @@
4409 and/or modify under the same terms as Python.
4410 """
4411
4412 +import urlparse
4413 +import os
4414 import json
4415 import pprint
4416 import sys
4417 @@ -74,8 +76,6 @@
4418 tracker = self.client.env['TRACKER_NAME']
4419 self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker)
4420
4421 - print self.base_path
4422 -
4423 def get_collection(self, class_name, input):
4424 if not self.db.security.hasPermission('View', self.db.getuid(),
4425 class_name):
4426 @@ -87,6 +87,7 @@
4427 if self.db.security.hasPermission('View', self.db.getuid(),
4428 class_name,
4429 itemid=item_id)]
4430 + self.client.setHeader("X-Count-Total", str(len(result)))
4431 return 200, result
4432
4433 def get_element(self, class_name, item_id, input):
4434 @@ -230,21 +231,39 @@
4435 # 2 - attribute
4436 resource_uri = uri.split("/")[1]
4437
4438 - self.client.setHeader("Access-Control-Allow-Methods",
4439 - "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH")
4440 + # if X-HTTP-Method-Override is set, follow the override method
4441 + method = self.client.request.headers.getheader('X-HTTP-Method-Override') or method
4442 +
4443 + # get the request format for response
4444 + # priority : extension from uri (/rest/issue.json),
4445 + # header (Accept: application/json, application/xml)
4446 + # default (application/json)
4447 +
4448 + # format_header need a priority parser
4449 + format_ext = os.path.splitext(urlparse.urlparse(uri).path)[1][1:]
4450 + format_header = self.client.request.headers.getheader('Accept')[12:]
4451 + format_output = format_ext or format_header or "json"
4452 +
4453 + self.client.setHeader("Access-Control-Allow-Origin", "*")
4454 self.client.setHeader("Access-Control-Allow-Headers",
4455 - "Content-Type, Authorization,"
4456 + "Content-Type, Authorization, "
4457 "X-HTTP-Method-Override")
4458 - self.client.setHeader("Allow",
4459 - "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH")
4460
4461 output = None
4462 try:
4463 if resource_uri in self.db.classes:
4464 + self.client.setHeader("Allow",
4465 + "HEAD, OPTIONS, POST, DELETE")
4466 + self.client.setHeader("Access-Control-Allow-Methods",
4467 + "HEAD, OPTIONS, POST, DELETE")
4468 response_code, output = getattr(self, "%s_collection" % method.lower())(
4469 resource_uri, input)
4470 else:
4471 class_name, item_id = hyperdb.splitDesignator(resource_uri)
4472 + self.client.setHeader("Allow",
4473 + "HEAD, OPTIONS, GET, PUT, DELETE, PATCH")
4474 + self.client.setHeader("Access-Control-Allow-Methods",
4475 + "HEAD, OPTIONS, GET, PUT, DELETE, PATCH")
4476 response_code, output = getattr(self, "%s_element" % method.lower())(
4477 class_name, item_id, input)
4478
4479 @@ -277,8 +296,12 @@
4480 print 'EXCEPTION AT', time.ctime()
4481 traceback.print_exc()
4482 finally:
4483 - self.client.setHeader("Content-Type", "application/json")
4484 - output = RoundupJSONEncoder().encode(output)
4485 + if format_output.lower() == "json":
4486 + self.client.setHeader("Content-Type", "application/json")
4487 + output = RoundupJSONEncoder().encode(output)
4488 + else:
4489 + self.client.response_code = 406
4490 + output = ""
4491
4492 return output
4493
4494 # HG changeset patch
4495 # User Chau Nguyen <dangchau1991@yahoo.com>
4496 # Date 1435069725 -10800
4497 # Tue Jun 23 17:28:45 2015 +0300
4498 # Branch REST
4499 # Node ID 08558a8b5f28f4b71b6dcedfd991e79e032acc24
4500 # Parent b5c9c775c40fa3a2109f843ef3f277feb4e7bfe4
4501 Handle response header
4502
4503 diff --git a/roundup/rest.py b/roundup/rest.py
4504 --- a/roundup/rest.py
4505 +++ b/roundup/rest.py
4506 @@ -139,9 +139,14 @@
4507 except (TypeError, IndexError, ValueError), message:
4508 raise UsageError(message)
4509
4510 + # set the header Location
4511 + link = self.base_path + class_name + item_id
4512 + self.client.setHeader("Location", link)
4513 +
4514 + # set the response body
4515 result = {
4516 'id': item_id,
4517 - 'link': self.base_path + class_name + item_id
4518 + 'link': link
4519 }
4520 return 201, result
4521
4522 @@ -225,6 +230,14 @@
4523 # 2 - attribute
4524 resource_uri = uri.split("/")[1]
4525
4526 + self.client.setHeader("Access-Control-Allow-Methods",
4527 + "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH")
4528 + self.client.setHeader("Access-Control-Allow-Headers",
4529 + "Content-Type, Authorization,"
4530 + "X-HTTP-Method-Override")
4531 + self.client.setHeader("Allow",
4532 + "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH")
4533 +
4534 output = None
4535 try:
4536 if resource_uri in self.db.classes:
4537 @@ -264,9 +277,9 @@
4538 print 'EXCEPTION AT', time.ctime()
4539 traceback.print_exc()
4540 finally:
4541 + self.client.setHeader("Content-Type", "application/json")
4542 output = RoundupJSONEncoder().encode(output)
4543
4544 - print "Length: %s - Content(50 char): %s" % (len(output), output[:50])
4545 return output
4546
4547
4548 # HG changeset patch
4549 # User Chau Nguyen <dangchau1991@yahoo.com>
4550 # Date 1434902006 -10800
4551 # Sun Jun 21 18:53:26 2015 +0300
4552 # Branch REST
4553 # Node ID b5c9c775c40fa3a2109f843ef3f277feb4e7bfe4
4554 # Parent 82b4a64bd5b42ad1bc72bc2df20064f81bfe604f
4555 Added successful response status code
4556
4557 diff --git a/roundup/rest.py b/roundup/rest.py
4558 --- a/roundup/rest.py
4559 +++ b/roundup/rest.py
4560 @@ -81,14 +81,13 @@
4561 class_name):
4562 raise Unauthorised('Permission to view %s denied' % class_name)
4563 class_obj = self.db.getclass(class_name)
4564 - prop_name = class_obj.labelprop()
4565 class_path = self.base_path + class_name
4566 result = [{'id': item_id, 'link': class_path + item_id}
4567 for item_id in class_obj.list()
4568 if self.db.security.hasPermission('View', self.db.getuid(),
4569 class_name,
4570 itemid=item_id)]
4571 - return result
4572 + return 200, result
4573
4574 def get_element(self, class_name, item_id, input):
4575 if not self.db.security.hasPermission('View', self.db.getuid(),
4576 @@ -110,7 +109,7 @@
4577 'attributes': dict(result)
4578 }
4579
4580 - return result
4581 + return 200, result
4582
4583 def post_collection(self, class_name, input):
4584 if not self.db.security.hasPermission('Create', self.db.getuid(),
4585 @@ -144,7 +143,7 @@
4586 'id': item_id,
4587 'link': self.base_path + class_name + item_id
4588 }
4589 - return result
4590 + return 201, result
4591
4592 def post_element(self, class_name, item_id, input):
4593 raise Reject('Invalid request')
4594 @@ -173,7 +172,7 @@
4595 'link': self.base_path + class_name + item_id,
4596 'attribute': result
4597 }
4598 - return result
4599 + return 200, result
4600
4601 def delete_collection(self, class_name, input):
4602 if not self.db.security.hasPermission('Delete', self.db.getuid(),
4603 @@ -197,7 +196,7 @@
4604 'count': count
4605 }
4606
4607 - return result
4608 + return 200, result
4609
4610 def delete_element(self, class_name, item_id, input):
4611 if not self.db.security.hasPermission('Delete', self.db.getuid(),
4612 @@ -211,7 +210,7 @@
4613 'status': 'ok'
4614 }
4615
4616 - return result
4617 + return 200, result
4618
4619 def patch_collection(self, class_name, input):
4620 raise Reject('Invalid request')
4621 @@ -229,15 +228,15 @@
4622 output = None
4623 try:
4624 if resource_uri in self.db.classes:
4625 - output = getattr(self, "%s_collection" % method.lower())(
4626 + response_code, output = getattr(self, "%s_collection" % method.lower())(
4627 resource_uri, input)
4628 else:
4629 class_name, item_id = hyperdb.splitDesignator(resource_uri)
4630 - output = getattr(self, "%s_element" % method.lower())(
4631 + response_code, output = getattr(self, "%s_element" % method.lower())(
4632 class_name, item_id, input)
4633
4634 output = data_obj(output)
4635 - self.client.response_code = 200
4636 + self.client.response_code = response_code
4637 except IndexError, msg:
4638 output = error_obj(404, msg)
4639 self.client.response_code = 404
4640 # HG changeset patch
4641 # User Chau Nguyen <dangchau1991@yahoo.com>
4642 # Date 1434698637 -10800
4643 # Fri Jun 19 10:23:57 2015 +0300
4644 # Branch REST
4645 # Node ID 82b4a64bd5b42ad1bc72bc2df20064f81bfe604f
4646 # Parent 8809907f0b64be97968a0166dc4dda3440c7de83
4647 Added response code
4648
4649 diff --git a/roundup/rest.py b/roundup/rest.py
4650 --- a/roundup/rest.py
4651 +++ b/roundup/rest.py
4652 @@ -237,22 +237,29 @@
4653 class_name, item_id, input)
4654
4655 output = data_obj(output)
4656 + self.client.response_code = 200
4657 except IndexError, msg:
4658 output = error_obj(404, msg)
4659 + self.client.response_code = 404
4660 except Unauthorised, msg:
4661 output = error_obj(403, msg)
4662 + self.client.response_code = 403
4663 except (hyperdb.DesignatorError, UsageError), msg:
4664 output = error_obj(400, msg)
4665 + self.client.response_code = 400
4666 except (AttributeError, Reject), msg:
4667 output = error_obj(405, 'Method Not Allowed. ' + str(msg))
4668 + self.client.response_code = 405
4669 except NotImplementedError:
4670 output = error_obj(402, 'Method is under development')
4671 + self.client.response_code = 402
4672 # nothing to pay, just a mark for debugging purpose
4673 except:
4674 # if self.DEBUG_MODE in roundup_server
4675 # else msg = 'An error occurred. Please check...',
4676 exc, val, tb = sys.exc_info()
4677 output = error_obj(400, val)
4678 + self.client.response_code = 400
4679
4680 # out to the logfile, it would be nice if the server do it for me
4681 print 'EXCEPTION AT', time.ctime()
4682 # HG changeset patch
4683 # User Chau Nguyen <dangchau1991@yahoo.com>
4684 # Date 1434638512 -10800
4685 # Thu Jun 18 17:41:52 2015 +0300
4686 # Branch REST
4687 # Node ID 8809907f0b64be97968a0166dc4dda3440c7de83
4688 # Parent ac06bf529bb0b11582802533cd99ff7d17ad2670
4689 Successful response is now following the design format
4690 Fix a bug with exception error message
4691
4692 diff --git a/roundup/rest.py b/roundup/rest.py
4693 --- a/roundup/rest.py
4694 +++ b/roundup/rest.py
4695 @@ -82,7 +82,8 @@
4696 raise Unauthorised('Permission to view %s denied' % class_name)
4697 class_obj = self.db.getclass(class_name)
4698 prop_name = class_obj.labelprop()
4699 - result = [{'id': item_id, prop_name: class_obj.get(item_id, prop_name)}
4700 + class_path = self.base_path + class_name
4701 + result = [{'id': item_id, 'link': class_path + item_id}
4702 for item_id in class_obj.list()
4703 if self.db.security.hasPermission('View', self.db.getuid(),
4704 class_name,
4705 @@ -102,8 +103,12 @@
4706 if self.db.security.hasPermission('View', self.db.getuid(),
4707 class_name, prop_name,
4708 item_id)]
4709 - result = dict(result)
4710 - result['id'] = item_id
4711 + result = {
4712 + 'id': item_id,
4713 + 'type': class_name,
4714 + 'link': self.base_path + class_name + item_id,
4715 + 'attributes': dict(result)
4716 + }
4717
4718 return result
4719
4720 @@ -135,7 +140,10 @@
4721 except (TypeError, IndexError, ValueError), message:
4722 raise UsageError(message)
4723
4724 - result = {id: item_id}
4725 + result = {
4726 + 'id': item_id,
4727 + 'link': self.base_path + class_name + item_id
4728 + }
4729 return result
4730
4731 def post_element(self, class_name, item_id, input):
4732 @@ -159,7 +167,12 @@
4733 except (TypeError, IndexError, ValueError), message:
4734 raise UsageError(message)
4735
4736 - result['id'] = item_id
4737 + result = {
4738 + 'id': item_id,
4739 + 'type': class_name,
4740 + 'link': self.base_path + class_name + item_id,
4741 + 'attribute': result
4742 + }
4743 return result
4744
4745 def delete_collection(self, class_name, input):
4746 @@ -174,11 +187,15 @@
4747 raise Unauthorised('Permission to delete %s %s denied' %
4748 (class_name, item_id))
4749
4750 + count = len(class_obj.list())
4751 for item_id in class_obj.list():
4752 self.db.destroynode(class_name, item_id)
4753
4754 self.db.commit()
4755 - result = {"status": "ok"}
4756 + result = {
4757 + 'status': 'ok',
4758 + 'count': count
4759 + }
4760
4761 return result
4762
4763 @@ -190,7 +207,9 @@
4764
4765 self.db.destroynode(class_name, item_id)
4766 self.db.commit()
4767 - result = {"status": "ok"}
4768 + result = {
4769 + 'status': 'ok'
4770 + }
4771
4772 return result
4773
4774 @@ -224,8 +243,8 @@
4775 output = error_obj(403, msg)
4776 except (hyperdb.DesignatorError, UsageError), msg:
4777 output = error_obj(400, msg)
4778 - except (AttributeError, Reject):
4779 - output = error_obj(405, 'Method Not Allowed')
4780 + except (AttributeError, Reject), msg:
4781 + output = error_obj(405, 'Method Not Allowed. ' + str(msg))
4782 except NotImplementedError:
4783 output = error_obj(402, 'Method is under development')
4784 # nothing to pay, just a mark for debugging purpose
4785 # HG changeset patch
4786 # User Chau Nguyen <dangchau1991@yahoo.com>
4787 # Date 1434614149 -10800
4788 # Thu Jun 18 10:55:49 2015 +0300
4789 # Branch REST
4790 # Node ID ac06bf529bb0b11582802533cd99ff7d17ad2670
4791 # Parent a38ecbc69e633260c4f77e10f27af4481f6e65a8
4792 Add base_path to generate uri
4793 Handle IndexError exception
4794
4795 diff --git a/roundup/rest.py b/roundup/rest.py
4796 --- a/roundup/rest.py
4797 +++ b/roundup/rest.py
4798 @@ -69,6 +69,13 @@
4799 self.client = client # it might be unnecessary to receive the client
4800 self.db = db
4801
4802 + protocol = 'http'
4803 + host = self.client.env['HTTP_HOST']
4804 + tracker = self.client.env['TRACKER_NAME']
4805 + self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker)
4806 +
4807 + print self.base_path
4808 +
4809 def get_collection(self, class_name, input):
4810 if not self.db.security.hasPermission('View', self.db.getuid(),
4811 class_name):
4812 @@ -197,6 +204,7 @@
4813 # PATH is split to multiple pieces
4814 # 0 - rest
4815 # 1 - resource
4816 + # 2 - attribute
4817 resource_uri = uri.split("/")[1]
4818
4819 output = None
4820 @@ -210,6 +218,8 @@
4821 class_name, item_id, input)
4822
4823 output = data_obj(output)
4824 + except IndexError, msg:
4825 + output = error_obj(404, msg)
4826 except Unauthorised, msg:
4827 output = error_obj(403, msg)
4828 except (hyperdb.DesignatorError, UsageError), msg:
4829 @@ -218,7 +228,7 @@
4830 output = error_obj(405, 'Method Not Allowed')
4831 except NotImplementedError:
4832 output = error_obj(402, 'Method is under development')
4833 - # nothing to pay, just mark that this is under development
4834 + # nothing to pay, just a mark for debugging purpose
4835 except:
4836 # if self.DEBUG_MODE in roundup_server
4837 # else msg = 'An error occurred. Please check...',
4838 # HG changeset patch
4839 # User Chau Nguyen <dangchau1991@yahoo.com>
4840 # Date 1434387710 -10800
4841 # Mon Jun 15 20:01:50 2015 +0300
4842 # Branch REST
4843 # Node ID a38ecbc69e633260c4f77e10f27af4481f6e65a8
4844 # Parent 90d411daacb68ce77603d9cf083e5e22da0e376b
4845 Making objects returned by REST follow the standard (wrapped by a dictionary, in either 'data' or 'error' field)
4846 Temporally added the client to REST so REST can make changes to HTTP Status code and Header
4847
4848 diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
4849 --- a/roundup/cgi/client.py
4850 +++ b/roundup/cgi/client.py
4851 @@ -432,7 +432,7 @@
4852 self.check_anonymous_access()
4853
4854 # Call rest library to handle the request
4855 - handler = rest.RestfulInstance(self.db)
4856 + handler = rest.RestfulInstance(self, self.db)
4857 output = handler.dispatch(self.env['REQUEST_METHOD'], self.path,
4858 self.form)
4859
4860 diff --git a/roundup/rest.py b/roundup/rest.py
4861 --- a/roundup/rest.py
4862 +++ b/roundup/rest.py
4863 @@ -41,12 +41,32 @@
4864 return props
4865
4866
4867 +def error_obj(status, msg, source=None):
4868 + result = {
4869 + 'error': {
4870 + 'status': status,
4871 + 'msg': msg
4872 + }
4873 + }
4874 + if source is not None:
4875 + result['error']['source'] = source
4876 +
4877 + return result
4878 +
4879 +
4880 +def data_obj(data):
4881 + result = {
4882 + 'data': data
4883 + }
4884 + return result
4885 +
4886 +
4887 class RestfulInstance(object):
4888 """Dummy Handler for REST
4889 """
4890
4891 - def __init__(self, db):
4892 - # TODO: database, translator and instance.actions
4893 + def __init__(self, client, db):
4894 + self.client = client # it might be unnecessary to receive the client
4895 self.db = db
4896
4897 def get_collection(self, class_name, input):
4898 @@ -188,17 +208,22 @@
4899 class_name, item_id = hyperdb.splitDesignator(resource_uri)
4900 output = getattr(self, "%s_element" % method.lower())(
4901 class_name, item_id, input)
4902 - except (hyperdb.DesignatorError, UsageError, Unauthorised), msg:
4903 - output = {'status': 'error', 'msg': msg}
4904 +
4905 + output = data_obj(output)
4906 + except Unauthorised, msg:
4907 + output = error_obj(403, msg)
4908 + except (hyperdb.DesignatorError, UsageError), msg:
4909 + output = error_obj(400, msg)
4910 except (AttributeError, Reject):
4911 - output = {'status': 'error', 'msg': 'Method is not allowed'}
4912 + output = error_obj(405, 'Method Not Allowed')
4913 except NotImplementedError:
4914 - output = {'status': 'error', 'msg': 'Method is under development'}
4915 + output = error_obj(402, 'Method is under development')
4916 + # nothing to pay, just mark that this is under development
4917 except:
4918 # if self.DEBUG_MODE in roundup_server
4919 # else msg = 'An error occurred. Please check...',
4920 exc, val, tb = sys.exc_info()
4921 - output = {'status': 'error', 'msg': val}
4922 + output = error_obj(400, val)
4923
4924 # out to the logfile, it would be nice if the server do it for me
4925 print 'EXCEPTION AT', time.ctime()
4926 # HG changeset patch
4927 # User Chau Nguyen <dangchau1991@yahoo.com>
4928 # Date 1434321555 -10800
4929 # Mon Jun 15 01:39:15 2015 +0300
4930 # Branch REST
4931 # Node ID 90d411daacb68ce77603d9cf083e5e22da0e376b
4932 # Parent c1aeec280158ba2a0d1bf4151f76db3a0f1d47f0
4933 Added exception Handling
4934
4935 diff --git a/roundup/rest.py b/roundup/rest.py
4936 --- a/roundup/rest.py
4937 +++ b/roundup/rest.py
4938 @@ -7,10 +7,14 @@
4939
4940 import json
4941 import pprint
4942 +import sys
4943 +import time
4944 +import traceback
4945 from roundup import hyperdb
4946 from roundup.exceptions import *
4947 from roundup import xmlrpc
4948
4949 +
4950 def props_from_args(db, cl, args, itemid=None):
4951 props = {}
4952 for arg in args:
4953 @@ -36,6 +40,7 @@
4954
4955 return props
4956
4957 +
4958 class RestfulInstance(object):
4959 """Dummy Handler for REST
4960 """
4961 @@ -183,16 +188,28 @@
4962 class_name, item_id = hyperdb.splitDesignator(resource_uri)
4963 output = getattr(self, "%s_element" % method.lower())(
4964 class_name, item_id, input)
4965 - except hyperdb.DesignatorError:
4966 - raise NotImplementedError('Invalid URI')
4967 - except AttributeError:
4968 - raise NotImplementedError('Method is invalid')
4969 + except (hyperdb.DesignatorError, UsageError, Unauthorised), msg:
4970 + output = {'status': 'error', 'msg': msg}
4971 + except (AttributeError, Reject):
4972 + output = {'status': 'error', 'msg': 'Method is not allowed'}
4973 + except NotImplementedError:
4974 + output = {'status': 'error', 'msg': 'Method is under development'}
4975 + except:
4976 + # if self.DEBUG_MODE in roundup_server
4977 + # else msg = 'An error occurred. Please check...',
4978 + exc, val, tb = sys.exc_info()
4979 + output = {'status': 'error', 'msg': val}
4980 +
4981 + # out to the logfile, it would be nice if the server do it for me
4982 + print 'EXCEPTION AT', time.ctime()
4983 + traceback.print_exc()
4984 finally:
4985 output = RoundupJSONEncoder().encode(output)
4986
4987 print "Length: %s - Content(50 char): %s" % (len(output), output[:50])
4988 return output
4989
4990 +
4991 class RoundupJSONEncoder(json.JSONEncoder):
4992 def default(self, obj):
4993 try:
4994 # HG changeset patch
4995 # User Chau Nguyen <dangchau1991@yahoo.com>
4996 # Date 1434319238 -10800
4997 # Mon Jun 15 01:00:38 2015 +0300
4998 # Branch REST
4999 # Node ID c1aeec280158ba2a0d1bf4151f76db3a0f1d47f0
5000 # Parent ba2c4d11a7aa2f98befeec9c914ef79d651c05ee
5001 Added RoundupJSONEncoder to handle classes from roundup
5002
5003 diff --git a/roundup/rest.py b/roundup/rest.py
5004 --- a/roundup/rest.py
5005 +++ b/roundup/rest.py
5006 @@ -50,7 +50,7 @@
5007 raise Unauthorised('Permission to view %s denied' % class_name)
5008 class_obj = self.db.getclass(class_name)
5009 prop_name = class_obj.labelprop()
5010 - result = [{'id': item_id, 'name': class_obj.get(item_id, prop_name)}
5011 + result = [{'id': item_id, prop_name: class_obj.get(item_id, prop_name)}
5012 for item_id in class_obj.list()
5013 if self.db.security.hasPermission('View', self.db.getuid(),
5014 class_name,
5015 @@ -71,6 +71,7 @@
5016 class_name, prop_name,
5017 item_id)]
5018 result = dict(result)
5019 + result['id'] = item_id
5020
5021 return result
5022
5023 @@ -187,7 +188,15 @@
5024 except AttributeError:
5025 raise NotImplementedError('Method is invalid')
5026 finally:
5027 - output = json.JSONEncoder().encode(output)
5028 + output = RoundupJSONEncoder().encode(output)
5029
5030 print "Length: %s - Content(50 char): %s" % (len(output), output[:50])
5031 return output
5032 +
5033 +class RoundupJSONEncoder(json.JSONEncoder):
5034 + def default(self, obj):
5035 + try:
5036 + result = json.JSONEncoder.default(self, obj)
5037 + except TypeError:
5038 + result = str(obj)
5039 + return result
5040 # HG changeset patch
5041 # User Chau Nguyen <dangchau1991@yahoo.com>
5042 # Date 1434299160 -10800
5043 # Sun Jun 14 19:26:00 2015 +0300
5044 # Branch REST
5045 # Node ID ba2c4d11a7aa2f98befeec9c914ef79d651c05ee
5046 # Parent 5197e29cf5e74596e48286e719c13bec9178e34a
5047 added custom parsing properties from arguments
5048
5049 diff --git a/roundup/rest.py b/roundup/rest.py
5050 --- a/roundup/rest.py
5051 +++ b/roundup/rest.py
5052 @@ -11,6 +11,30 @@
5053 from roundup.exceptions import *
5054 from roundup import xmlrpc
5055
5056 +def props_from_args(db, cl, args, itemid=None):
5057 + props = {}
5058 + for arg in args:
5059 + try:
5060 + key = arg.name
5061 + value = arg.value
5062 + except ValueError:
5063 + raise UsageError('argument "%s" not propname=value' % arg)
5064 + if isinstance(key, unicode):
5065 + try:
5066 + key = key.encode('ascii')
5067 + except UnicodeEncodeError:
5068 + raise UsageError('argument %r is no valid ascii keyword' % key)
5069 + if isinstance(value, unicode):
5070 + value = value.encode('utf-8')
5071 + if value:
5072 + try:
5073 + props[key] = hyperdb.rawToHyperdb(db, cl, itemid, key, value)
5074 + except hyperdb.HyperdbValueError:
5075 + pass # pass if a parameter is not a property of the class
5076 + else:
5077 + props[key] = None
5078 +
5079 + return props
5080
5081 class RestfulInstance(object):
5082 """Dummy Handler for REST
5083 @@ -58,8 +82,7 @@
5084 class_obj = self.db.getclass(class_name)
5085
5086 # convert types
5087 - input_data = ["%s=%s" % (item.name, item.value) for item in input.value]
5088 - props = xmlrpc.props_from_args(self.db, class_obj, input_data)
5089 + props = props_from_args(self.db, class_obj, input.value)
5090
5091 # check for the key property
5092 key = class_obj.getkey()
5093 @@ -91,8 +114,7 @@
5094 def put_element(self, class_name, item_id, input):
5095 class_obj = self.db.getclass(class_name)
5096
5097 - input_data = ["%s=%s" % (item.name, item.value) for item in input.value]
5098 - props = xmlrpc.props_from_args(self.db, class_obj, input_data, item_id)
5099 + props = props_from_args(self.db, class_obj, input.value, item_id)
5100 for p in props.iterkeys():
5101 if not self.db.security.hasPermission('Edit', self.db.getuid(),
5102 class_name, p, item_id):
5103 # HG changeset patch
5104 # User Chau Nguyen <dangchau1991@yahoo.com>
5105 # Date 1434254817 -10800
5106 # Sun Jun 14 07:06:57 2015 +0300
5107 # Branch REST
5108 # Node ID 5197e29cf5e74596e48286e719c13bec9178e34a
5109 # Parent c13388b3ab514a1902dfde4597f3b81eb4e227b5
5110 Added Partial PUT
5111
5112 diff --git a/roundup/rest.py b/roundup/rest.py
5113 --- a/roundup/rest.py
5114 +++ b/roundup/rest.py
5115 @@ -89,7 +89,23 @@
5116 raise Reject('Invalid request')
5117
5118 def put_element(self, class_name, item_id, input):
5119 - raise NotImplementedError
5120 + class_obj = self.db.getclass(class_name)
5121 +
5122 + input_data = ["%s=%s" % (item.name, item.value) for item in input.value]
5123 + props = xmlrpc.props_from_args(self.db, class_obj, input_data, item_id)
5124 + for p in props.iterkeys():
5125 + if not self.db.security.hasPermission('Edit', self.db.getuid(),
5126 + class_name, p, item_id):
5127 + raise Unauthorised('Permission to edit %s of %s%s denied' %
5128 + (p, class_name, item_id))
5129 + try:
5130 + result = class_obj.set(item_id, **props)
5131 + self.db.commit()
5132 + except (TypeError, IndexError, ValueError), message:
5133 + raise UsageError(message)
5134 +
5135 + result['id'] = item_id
5136 + return result
5137
5138 def delete_collection(self, class_name, input):
5139 if not self.db.security.hasPermission('Delete', self.db.getuid(),
5140 # HG changeset patch
5141 # User Chau Nguyen <dangchau1991@yahoo.com>
5142 # Date 1434241919 -10800
5143 # Sun Jun 14 03:31:59 2015 +0300
5144 # Branch REST
5145 # Node ID c13388b3ab514a1902dfde4597f3b81eb4e227b5
5146 # Parent e4b78ec60967a683ac78b45f293c654e3cbd036a
5147 Implement delete collection, Added raising exception from post to element, put to collection, and patch to collection
5148
5149 diff --git a/roundup/rest.py b/roundup/rest.py
5150 --- a/roundup/rest.py
5151 +++ b/roundup/rest.py
5152 @@ -83,25 +83,40 @@
5153 return result
5154
5155 def post_element(self, class_name, item_id, input):
5156 - raise NotImplementedError
5157 + raise Reject('Invalid request')
5158
5159 def put_collection(self, class_name, input):
5160 - raise NotImplementedError
5161 + raise Reject('Invalid request')
5162
5163 def put_element(self, class_name, item_id, input):
5164 raise NotImplementedError
5165
5166 def delete_collection(self, class_name, input):
5167 - # TODO: should I allow user to delete the whole collection ?
5168 - raise NotImplementedError
5169 + if not self.db.security.hasPermission('Delete', self.db.getuid(),
5170 + class_name):
5171 + raise Unauthorised('Permission to delete %s denied' % class_name)
5172 +
5173 + class_obj = self.db.getclass(class_name)
5174 + for item_id in class_obj.list():
5175 + if not self.db.security.hasPermission('Delete', self.db.getuid(),
5176 + class_name, itemid=item_id):
5177 + raise Unauthorised('Permission to delete %s %s denied' %
5178 + (class_name, item_id))
5179 +
5180 + for item_id in class_obj.list():
5181 + self.db.destroynode(class_name, item_id)
5182 +
5183 + self.db.commit()
5184 + result = {"status": "ok"}
5185 +
5186 + return result
5187
5188 def delete_element(self, class_name, item_id, input):
5189 if not self.db.security.hasPermission('Delete', self.db.getuid(),
5190 class_name, itemid=item_id):
5191 raise Unauthorised('Permission to delete %s %s denied' %
5192 (class_name, item_id))
5193 - if item_id != input['id'].value:
5194 - raise UsageError('Must provide id key as confirmation')
5195 +
5196 self.db.destroynode(class_name, item_id)
5197 self.db.commit()
5198 result = {"status": "ok"}
5199 @@ -109,7 +124,7 @@
5200 return result
5201
5202 def patch_collection(self, class_name, input):
5203 - raise NotImplementedError
5204 + raise Reject('Invalid request')
5205
5206 def patch_element(self, class_name, item_id, input):
5207 raise NotImplementedError
5208 # HG changeset patch
5209 # User Chau Nguyen <dangchau1991@yahoo.com>
5210 # Date 1434219506 -10800
5211 # Sat Jun 13 21:18:26 2015 +0300
5212 # Branch REST
5213 # Node ID e4b78ec60967a683ac78b45f293c654e3cbd036a
5214 # Parent 2008b4f7efc08a97c3c5357754f2b5b679b48f3d
5215 Cleanup, fixed a bug with delete action, change the returned type of every action from JSON to list/object
5216
5217 diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
5218 --- a/roundup/cgi/client.py
5219 +++ b/roundup/cgi/client.py
5220 @@ -423,10 +423,6 @@
5221 self.write(output)
5222
5223 def handle_rest(self):
5224 - # Pull the parameters data out of the form. The "value" attribute
5225 - # will be the raw content of the request.
5226 - input = self.form.value
5227 -
5228 # Set the charset and language
5229 self.determine_charset()
5230 self.determine_language()
5231 @@ -437,7 +433,8 @@
5232
5233 # Call rest library to handle the request
5234 handler = rest.RestfulInstance(self.db)
5235 - output = handler.dispatch(self.env['REQUEST_METHOD'], self.path, input)
5236 + output = handler.dispatch(self.env['REQUEST_METHOD'], self.path,
5237 + self.form)
5238
5239 # self.setHeader("Content-Type", "text/xml")
5240 self.setHeader("Content-Length", str(len(output)))
5241 diff --git a/roundup/rest.py b/roundup/rest.py
5242 --- a/roundup/rest.py
5243 +++ b/roundup/rest.py
5244 @@ -8,7 +8,7 @@
5245 import json
5246 import pprint
5247 from roundup import hyperdb
5248 -from roundup.cgi.templating import Unauthorised
5249 +from roundup.exceptions import *
5250 from roundup import xmlrpc
5251
5252
5253 @@ -21,18 +21,23 @@
5254 self.db = db
5255
5256 def get_collection(self, class_name, input):
5257 + if not self.db.security.hasPermission('View', self.db.getuid(),
5258 + class_name):
5259 + raise Unauthorised('Permission to view %s denied' % class_name)
5260 class_obj = self.db.getclass(class_name)
5261 prop_name = class_obj.labelprop()
5262 result = [{'id': item_id, 'name': class_obj.get(item_id, prop_name)}
5263 for item_id in class_obj.list()
5264 if self.db.security.hasPermission('View', self.db.getuid(),
5265 - class_name, None, item_id)
5266 - ]
5267 - result = json.JSONEncoder().encode(result)
5268 -
5269 + class_name,
5270 + itemid=item_id)]
5271 return result
5272
5273 def get_element(self, class_name, item_id, input):
5274 + if not self.db.security.hasPermission('View', self.db.getuid(),
5275 + class_name, itemid=item_id):
5276 + raise Unauthorised('Permission to view %s item %d denied' %
5277 + (class_name, item_id))
5278 class_obj = self.db.getclass(class_name)
5279 props = class_obj.properties.keys()
5280 props.sort() # sort properties
5281 @@ -40,9 +45,8 @@
5282 for prop_name in props
5283 if self.db.security.hasPermission('View', self.db.getuid(),
5284 class_name, prop_name,
5285 - item_id)
5286 - ]
5287 - result = json.JSONEncoder().encode(dict(result))
5288 + item_id)]
5289 + result = dict(result)
5290
5291 return result
5292
5293 @@ -54,12 +58,13 @@
5294 class_obj = self.db.getclass(class_name)
5295
5296 # convert types
5297 - props = xmlrpc.props_from_args(self.db, class_obj, input)
5298 + input_data = ["%s=%s" % (item.name, item.value) for item in input.value]
5299 + props = xmlrpc.props_from_args(self.db, class_obj, input_data)
5300
5301 # check for the key property
5302 key = class_obj.getkey()
5303 if key and key not in props:
5304 - raise xmlrpc.UsageError, 'Must provide the "%s" property.' % key
5305 + raise UsageError('Must provide the "%s" property.' % key)
5306
5307 for key in props:
5308 if not self.db.security.hasPermission('Create', self.db.getuid(),
5309 @@ -69,10 +74,12 @@
5310
5311 # do the actual create
5312 try:
5313 - result = class_obj.create(**props)
5314 + item_id = class_obj.create(**props)
5315 self.db.commit()
5316 except (TypeError, IndexError, ValueError), message:
5317 - raise xmlrpc.UsageError, message
5318 + raise UsageError(message)
5319 +
5320 + result = {id: item_id}
5321 return result
5322
5323 def post_element(self, class_name, item_id, input):
5324 @@ -89,13 +96,15 @@
5325 raise NotImplementedError
5326
5327 def delete_element(self, class_name, item_id, input):
5328 - # TODO: BUG with DELETE without form data. Working with random data
5329 - # crash at line self.form = cgi.FieldStorage(fp=request.rfile, environ=env)
5330 - try:
5331 - self.db.destroynode(class_name, item_id)
5332 - result = 'OK'
5333 - except IndexError:
5334 - result = 'Error'
5335 + if not self.db.security.hasPermission('Delete', self.db.getuid(),
5336 + class_name, itemid=item_id):
5337 + raise Unauthorised('Permission to delete %s %s denied' %
5338 + (class_name, item_id))
5339 + if item_id != input['id'].value:
5340 + raise UsageError('Must provide id key as confirmation')
5341 + self.db.destroynode(class_name, item_id)
5342 + self.db.commit()
5343 + result = {"status": "ok"}
5344
5345 return result
5346
5347 @@ -106,33 +115,26 @@
5348 raise NotImplementedError
5349
5350 def dispatch(self, method, uri, input):
5351 - print "METHOD: " + method + " URI: " + uri
5352 - print type(input)
5353 - pprint.pprint(input)
5354 - # TODO: process input_form directly instead of making a new array
5355 - # TODO: rest server
5356 - # TODO: check roundup/actions.py
5357 - # TODO: if uri_path has more than 2 child, return 404
5358 - # TODO: custom JSONEncoder to handle other data type
5359 - # TODO: catch all error and display error.
5360 -
5361 # PATH is split to multiple pieces
5362 # 0 - rest
5363 # 1 - resource
5364 + resource_uri = uri.split("/")[1]
5365
5366 - resource_uri = uri.split("/")[1]
5367 - input_data = ["%s=%s" % (item.name, item.value) for item in input]
5368 -
5369 + output = None
5370 try:
5371 if resource_uri in self.db.classes:
5372 - output = getattr(self, "%s_collection" % method.lower())(resource_uri, input_data)
5373 + output = getattr(self, "%s_collection" % method.lower())(
5374 + resource_uri, input)
5375 else:
5376 class_name, item_id = hyperdb.splitDesignator(resource_uri)
5377 - output = getattr(self, "%s_element" % method.lower())(class_name, item_id, input_data)
5378 + output = getattr(self, "%s_element" % method.lower())(
5379 + class_name, item_id, input)
5380 except hyperdb.DesignatorError:
5381 - pass # invalid URI
5382 + raise NotImplementedError('Invalid URI')
5383 except AttributeError:
5384 - raise NotImplementedError # Error: method is invalid
5385 + raise NotImplementedError('Method is invalid')
5386 + finally:
5387 + output = json.JSONEncoder().encode(output)
5388
5389 print "Length: %s - Content(50 char): %s" % (len(output), output[:50])
5390 return output
5391 # HG changeset patch
5392 # User Chau Nguyen <dangchau1991@yahoo.com>
5393 # Date 1434131105 -10800
5394 # Fri Jun 12 20:45:05 2015 +0300
5395 # Branch REST
5396 # Node ID 2008b4f7efc08a97c3c5357754f2b5b679b48f3d
5397 # Parent c26ec962c982622a8d7c0821a1a55efb814de6d9
5398 Split all rest action into 2 type: element uri and collection uri
5399
5400 diff --git a/roundup/rest.py b/roundup/rest.py
5401 --- a/roundup/rest.py
5402 +++ b/roundup/rest.py
5403 @@ -14,56 +14,39 @@
5404
5405 class RestfulInstance(object):
5406 """Dummy Handler for REST
5407 - WARNING: Very ugly !!!!, cleaned & better organized in progress (next commit)
5408 """
5409
5410 def __init__(self, db):
5411 # TODO: database, translator and instance.actions
5412 self.db = db
5413
5414 - def action_get(self, resource_uri, input):
5415 - # TODO: split this into collection URI and resource URI
5416 - class_name = resource_uri
5417 - try:
5418 - class_obj = self.db.getclass(class_name)
5419 - """prop_name = class_obj.labelprop()
5420 - result = [class_obj.get(item_id, prop_name)"""
5421 - result = [{'id': item_id}
5422 - for item_id in class_obj.list()
5423 - if self.db.security.hasPermission('View',
5424 - self.db.getuid(),
5425 - class_name,
5426 - None,
5427 - item_id)
5428 - ]
5429 - result = json.JSONEncoder().encode(result)
5430 - # result = `len(dict(result))` + ' ' + `len(result)`
5431 - except KeyError:
5432 - pass
5433 -
5434 - try:
5435 - class_name, item_id = hyperdb.splitDesignator(resource_uri)
5436 - class_obj = self.db.getclass(class_name)
5437 - props = class_obj.properties.keys()
5438 - props.sort()
5439 - result = [(prop_name, class_obj.get(item_id, prop_name))
5440 - for prop_name in props
5441 - if self.db.security.hasPermission('View',
5442 - self.db.getuid(),
5443 - class_name,
5444 - prop_name,
5445 - item_id)
5446 - ]
5447 - # Note: is this a bug by having an extra indent in xmlrpc ?
5448 - result = json.JSONEncoder().encode(dict(result))
5449 - except hyperdb.DesignatorError:
5450 - pass
5451 + def get_collection(self, class_name, input):
5452 + class_obj = self.db.getclass(class_name)
5453 + prop_name = class_obj.labelprop()
5454 + result = [{'id': item_id, 'name': class_obj.get(item_id, prop_name)}
5455 + for item_id in class_obj.list()
5456 + if self.db.security.hasPermission('View', self.db.getuid(),
5457 + class_name, None, item_id)
5458 + ]
5459 + result = json.JSONEncoder().encode(result)
5460
5461 return result
5462
5463 - def action_post(self, resource_uri, input):
5464 - class_name = resource_uri
5465 + def get_element(self, class_name, item_id, input):
5466 + class_obj = self.db.getclass(class_name)
5467 + props = class_obj.properties.keys()
5468 + props.sort() # sort properties
5469 + result = [(prop_name, class_obj.get(item_id, prop_name))
5470 + for prop_name in props
5471 + if self.db.security.hasPermission('View', self.db.getuid(),
5472 + class_name, prop_name,
5473 + item_id)
5474 + ]
5475 + result = json.JSONEncoder().encode(dict(result))
5476
5477 + return result
5478 +
5479 + def post_collection(self, class_name, input):
5480 if not self.db.security.hasPermission('Create', self.db.getuid(),
5481 class_name):
5482 raise Unauthorised('Permission to create %s denied' % class_name)
5483 @@ -92,60 +75,64 @@
5484 raise xmlrpc.UsageError, message
5485 return result
5486
5487 - def action_put(self, resource_uri, input):
5488 + def post_element(self, class_name, item_id, input):
5489 raise NotImplementedError
5490
5491 - def action_delete(self, resource_uri, input):
5492 + def put_collection(self, class_name, input):
5493 + raise NotImplementedError
5494 +
5495 + def put_element(self, class_name, item_id, input):
5496 + raise NotImplementedError
5497 +
5498 + def delete_collection(self, class_name, input):
5499 # TODO: should I allow user to delete the whole collection ?
5500 + raise NotImplementedError
5501 +
5502 + def delete_element(self, class_name, item_id, input):
5503 # TODO: BUG with DELETE without form data. Working with random data
5504 # crash at line self.form = cgi.FieldStorage(fp=request.rfile, environ=env)
5505 - class_name = resource_uri
5506 try:
5507 - class_obj = self.db.getclass(class_name)
5508 - raise NotImplementedError
5509 - except KeyError:
5510 - pass
5511 -
5512 - try:
5513 - class_name, item_id = hyperdb.splitDesignator(resource_uri)
5514 - print class_name
5515 - print item_id
5516 self.db.destroynode(class_name, item_id)
5517 result = 'OK'
5518 except IndexError:
5519 result = 'Error'
5520 - except hyperdb.DesignatorError:
5521 - pass
5522
5523 return result
5524
5525 - def action_patch(self, resource_uri, input):
5526 + def patch_collection(self, class_name, input):
5527 + raise NotImplementedError
5528 +
5529 + def patch_element(self, class_name, item_id, input):
5530 raise NotImplementedError
5531
5532 def dispatch(self, method, uri, input):
5533 print "METHOD: " + method + " URI: " + uri
5534 print type(input)
5535 pprint.pprint(input)
5536 -
5537 - # PATH is split to multiple pieces
5538 - # 0 - rest
5539 - # 1 - resource
5540 - #
5541 - # Example: rest/issue - collection uri
5542 - # Example: rest/issue573 - element uri
5543 - uri_path = uri.split("/")
5544 - input_form = ["%s=%s" % (item.name, item.value) for item in input]
5545 # TODO: process input_form directly instead of making a new array
5546 # TODO: rest server
5547 # TODO: check roundup/actions.py
5548 # TODO: if uri_path has more than 2 child, return 404
5549 # TODO: custom JSONEncoder to handle other data type
5550 # TODO: catch all error and display error.
5551 +
5552 + # PATH is split to multiple pieces
5553 + # 0 - rest
5554 + # 1 - resource
5555 +
5556 + resource_uri = uri.split("/")[1]
5557 + input_data = ["%s=%s" % (item.name, item.value) for item in input]
5558 +
5559 try:
5560 - output = getattr(self, "action_%s" % method.lower())(uri_path[1], input_form)
5561 + if resource_uri in self.db.classes:
5562 + output = getattr(self, "%s_collection" % method.lower())(resource_uri, input_data)
5563 + else:
5564 + class_name, item_id = hyperdb.splitDesignator(resource_uri)
5565 + output = getattr(self, "%s_element" % method.lower())(class_name, item_id, input_data)
5566 + except hyperdb.DesignatorError:
5567 + pass # invalid URI
5568 except AttributeError:
5569 - raise NotImplementedError
5570 + raise NotImplementedError # Error: method is invalid
5571
5572 - print "Response Length: %s - Response Content (First 50 char): %s" %\
5573 - (len(output), output[:50])
5574 + print "Length: %s - Content(50 char): %s" % (len(output), output[:50])
5575 return output
5576 # HG changeset patch
5577 # User Chau Nguyen <dangchau1991@yahoo.com>
5578 # Date 1434126614 -10800
5579 # Fri Jun 12 19:30:14 2015 +0300
5580 # Branch REST
5581 # Node ID c26ec962c982622a8d7c0821a1a55efb814de6d9
5582 # Parent ce4f4f21ee661d83de9e42bc2a77d2416ed39fc5
5583 use getattr instead of calling each function
5584
5585 diff --git a/roundup/rest.py b/roundup/rest.py
5586 --- a/roundup/rest.py
5587 +++ b/roundup/rest.py
5588 @@ -98,6 +98,7 @@
5589 def action_delete(self, resource_uri, input):
5590 # TODO: should I allow user to delete the whole collection ?
5591 # TODO: BUG with DELETE without form data. Working with random data
5592 + # crash at line self.form = cgi.FieldStorage(fp=request.rfile, environ=env)
5593 class_name = resource_uri
5594 try:
5595 class_obj = self.db.getclass(class_name)
5596 @@ -136,24 +137,14 @@
5597 input_form = ["%s=%s" % (item.name, item.value) for item in input]
5598 # TODO: process input_form directly instead of making a new array
5599 # TODO: rest server
5600 - # TODO: use named function for this instead
5601 # TODO: check roundup/actions.py
5602 # TODO: if uri_path has more than 2 child, return 404
5603 # TODO: custom JSONEncoder to handle other data type
5604 # TODO: catch all error and display error.
5605 - output = "METHOD is not supported"
5606 - if method == "GET":
5607 - output = self.action_get(uri_path[1], input_form)
5608 - elif method == "POST":
5609 - output = self.action_post(uri_path[1], input_form)
5610 - elif method == "PUT":
5611 - output = self.action_put(uri_path[1], input_form)
5612 - elif method == "DELETE":
5613 - output = self.action_delete(uri_path[1], input_form)
5614 - elif method == "PATCH":
5615 - output = self.action_patch(uri_path[1], input_form)
5616 - else:
5617 - pass
5618 + try:
5619 + output = getattr(self, "action_%s" % method.lower())(uri_path[1], input_form)
5620 + except AttributeError:
5621 + raise NotImplementedError
5622
5623 print "Response Length: %s - Response Content (First 50 char): %s" %\
5624 (len(output), output[:50])
5625 # HG changeset patch
5626 # User Chau Nguyen <dangchau1991@yahoo.com>
5627 # Date 1434124520 -10800
5628 # Fri Jun 12 18:55:20 2015 +0300
5629 # Branch REST
5630 # Node ID ce4f4f21ee661d83de9e42bc2a77d2416ed39fc5
5631 # Parent 87bc5b4679c7e4906ba695aa3e19bc42cb07e991
5632 Added POST and DELETE>
5633
5634 diff --git a/roundup/rest.py b/roundup/rest.py
5635 --- a/roundup/rest.py
5636 +++ b/roundup/rest.py
5637 @@ -9,10 +9,12 @@
5638 import pprint
5639 from roundup import hyperdb
5640 from roundup.cgi.templating import Unauthorised
5641 +from roundup import xmlrpc
5642
5643
5644 class RestfulInstance(object):
5645 """Dummy Handler for REST
5646 + WARNING: Very ugly !!!!, cleaned & better organized in progress (next commit)
5647 """
5648
5649 def __init__(self, db):
5650 @@ -25,19 +27,17 @@
5651 try:
5652 class_obj = self.db.getclass(class_name)
5653 """prop_name = class_obj.labelprop()
5654 - result = [class_obj.get(item_id, prop_name)
5655 - for item_id in class_obj.list()
5656 - if self.db.security.hasPermission('View', self.db.getuid(),
5657 - class_name, prop_name, item_id)
5658 - ]
5659 - result = json.JSONEncoder().encode(result)"""
5660 + result = [class_obj.get(item_id, prop_name)"""
5661 result = [{'id': item_id}
5662 for item_id in class_obj.list()
5663 - if self.db.security.hasPermission('View', self.db.getuid(),
5664 - class_name, None, item_id)
5665 + if self.db.security.hasPermission('View',
5666 + self.db.getuid(),
5667 + class_name,
5668 + None,
5669 + item_id)
5670 ]
5671 result = json.JSONEncoder().encode(result)
5672 - #result = `len(dict(result))` + ' ' + `len(result)`
5673 + # result = `len(dict(result))` + ' ' + `len(result)`
5674 except KeyError:
5675 pass
5676
5677 @@ -48,19 +48,78 @@
5678 props.sort()
5679 result = [(prop_name, class_obj.get(item_id, prop_name))
5680 for prop_name in props
5681 - if self.db.security.hasPermission('View', self.db.getuid(),
5682 - class_name, prop_name, item_id)
5683 + if self.db.security.hasPermission('View',
5684 + self.db.getuid(),
5685 + class_name,
5686 + prop_name,
5687 + item_id)
5688 ]
5689 # Note: is this a bug by having an extra indent in xmlrpc ?
5690 result = json.JSONEncoder().encode(dict(result))
5691 except hyperdb.DesignatorError:
5692 pass
5693
5694 - # print type(result)
5695 - # print type(dict(result))
5696 return result
5697 - # return json.dumps(dict(result))
5698 - # return dict(result)
5699 +
5700 + def action_post(self, resource_uri, input):
5701 + class_name = resource_uri
5702 +
5703 + if not self.db.security.hasPermission('Create', self.db.getuid(),
5704 + class_name):
5705 + raise Unauthorised('Permission to create %s denied' % class_name)
5706 +
5707 + class_obj = self.db.getclass(class_name)
5708 +
5709 + # convert types
5710 + props = xmlrpc.props_from_args(self.db, class_obj, input)
5711 +
5712 + # check for the key property
5713 + key = class_obj.getkey()
5714 + if key and key not in props:
5715 + raise xmlrpc.UsageError, 'Must provide the "%s" property.' % key
5716 +
5717 + for key in props:
5718 + if not self.db.security.hasPermission('Create', self.db.getuid(),
5719 + class_name, property=key):
5720 + raise Unauthorised('Permission to create %s.%s denied' %
5721 + (class_name, key))
5722 +
5723 + # do the actual create
5724 + try:
5725 + result = class_obj.create(**props)
5726 + self.db.commit()
5727 + except (TypeError, IndexError, ValueError), message:
5728 + raise xmlrpc.UsageError, message
5729 + return result
5730 +
5731 + def action_put(self, resource_uri, input):
5732 + raise NotImplementedError
5733 +
5734 + def action_delete(self, resource_uri, input):
5735 + # TODO: should I allow user to delete the whole collection ?
5736 + # TODO: BUG with DELETE without form data. Working with random data
5737 + class_name = resource_uri
5738 + try:
5739 + class_obj = self.db.getclass(class_name)
5740 + raise NotImplementedError
5741 + except KeyError:
5742 + pass
5743 +
5744 + try:
5745 + class_name, item_id = hyperdb.splitDesignator(resource_uri)
5746 + print class_name
5747 + print item_id
5748 + self.db.destroynode(class_name, item_id)
5749 + result = 'OK'
5750 + except IndexError:
5751 + result = 'Error'
5752 + except hyperdb.DesignatorError:
5753 + pass
5754 +
5755 + return result
5756 +
5757 + def action_patch(self, resource_uri, input):
5758 + raise NotImplementedError
5759
5760 def dispatch(self, method, uri, input):
5761 print "METHOD: " + method + " URI: " + uri
5762 @@ -74,22 +133,28 @@
5763 # Example: rest/issue - collection uri
5764 # Example: rest/issue573 - element uri
5765 uri_path = uri.split("/")
5766 + input_form = ["%s=%s" % (item.name, item.value) for item in input]
5767 + # TODO: process input_form directly instead of making a new array
5768 + # TODO: rest server
5769 # TODO: use named function for this instead
5770 # TODO: check roundup/actions.py
5771 # TODO: if uri_path has more than 2 child, return 404
5772 + # TODO: custom JSONEncoder to handle other data type
5773 + # TODO: catch all error and display error.
5774 output = "METHOD is not supported"
5775 if method == "GET":
5776 - output = self.action_get(uri_path[1], input)
5777 + output = self.action_get(uri_path[1], input_form)
5778 elif method == "POST":
5779 - pass
5780 + output = self.action_post(uri_path[1], input_form)
5781 elif method == "PUT":
5782 - pass
5783 + output = self.action_put(uri_path[1], input_form)
5784 elif method == "DELETE":
5785 - pass
5786 + output = self.action_delete(uri_path[1], input_form)
5787 elif method == "PATCH":
5788 - pass
5789 + output = self.action_patch(uri_path[1], input_form)
5790 else:
5791 pass
5792
5793 - print "Response Length: " + `len(output)` + " - Response Content (First 50 char): " + output[:50]
5794 + print "Response Length: %s - Response Content (First 50 char): %s" %\
5795 + (len(output), output[:50])
5796 return output
5797 diff --git a/roundup/scripts/roundup_server.py b/roundup/scripts/roundup_server.py
5798 --- a/roundup/scripts/roundup_server.py
5799 +++ b/roundup/scripts/roundup_server.py
5800 @@ -251,7 +251,7 @@
5801 else:
5802 return self.run_cgi()
5803
5804 - do_GET = do_POST = do_HEAD = run_cgi_outer
5805 + do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = do_PATCH = run_cgi_outer
5806
5807 def index(self):
5808 ''' Print up an index of the available trackers
5809 # HG changeset patch
5810 # User Chau Nguyen <dangchau1991@yahoo.com>
5811 # Date 1434065314 -10800
5812 # Fri Jun 12 02:28:34 2015 +0300
5813 # Branch REST
5814 # Node ID 87bc5b4679c7e4906ba695aa3e19bc42cb07e991
5815 # Parent cff5b11df2488fbcdfe84228c3a9beb0214a6fd3
5816 Recognize both GET element uri and collection uri
5817
5818 diff --git a/roundup/rest.py b/roundup/rest.py
5819 --- a/roundup/rest.py
5820 +++ b/roundup/rest.py
5821 @@ -19,27 +19,51 @@
5822 # TODO: database, translator and instance.actions
5823 self.db = db
5824
5825 - def action_get(self, resource, input):
5826 - classname, itemid = hyperdb.splitDesignator(resource)
5827 - cl = self.db.getclass(classname)
5828 - props = cl.properties.keys()
5829 - props.sort()
5830 - for p in props:
5831 - if not self.db.security.hasPermission('View', self.db.getuid(),
5832 - classname, p, itemid):
5833 - raise Unauthorised('Permission to view %s of %s denied' %
5834 - (p, resource))
5835 - result = [(prop, cl.get(itemid, prop)) for prop in props]
5836 + def action_get(self, resource_uri, input):
5837 + # TODO: split this into collection URI and resource URI
5838 + class_name = resource_uri
5839 + try:
5840 + class_obj = self.db.getclass(class_name)
5841 + """prop_name = class_obj.labelprop()
5842 + result = [class_obj.get(item_id, prop_name)
5843 + for item_id in class_obj.list()
5844 + if self.db.security.hasPermission('View', self.db.getuid(),
5845 + class_name, prop_name, item_id)
5846 + ]
5847 + result = json.JSONEncoder().encode(result)"""
5848 + result = [{'id': item_id}
5849 + for item_id in class_obj.list()
5850 + if self.db.security.hasPermission('View', self.db.getuid(),
5851 + class_name, None, item_id)
5852 + ]
5853 + result = json.JSONEncoder().encode(result)
5854 + #result = `len(dict(result))` + ' ' + `len(result)`
5855 + except KeyError:
5856 + pass
5857 +
5858 + try:
5859 + class_name, item_id = hyperdb.splitDesignator(resource_uri)
5860 + class_obj = self.db.getclass(class_name)
5861 + props = class_obj.properties.keys()
5862 + props.sort()
5863 + result = [(prop_name, class_obj.get(item_id, prop_name))
5864 + for prop_name in props
5865 + if self.db.security.hasPermission('View', self.db.getuid(),
5866 + class_name, prop_name, item_id)
5867 + ]
5868 + # Note: is this a bug by having an extra indent in xmlrpc ?
5869 + result = json.JSONEncoder().encode(dict(result))
5870 + except hyperdb.DesignatorError:
5871 + pass
5872
5873 # print type(result)
5874 # print type(dict(result))
5875 - return json.JSONEncoder().encode(dict(result))
5876 + return result
5877 # return json.dumps(dict(result))
5878 # return dict(result)
5879
5880 def dispatch(self, method, uri, input):
5881 - print method
5882 - print uri
5883 + print "METHOD: " + method + " URI: " + uri
5884 print type(input)
5885 pprint.pprint(input)
5886
5887 @@ -67,6 +91,5 @@
5888 else:
5889 pass
5890
5891 - print output
5892 - print len(output)
5893 + print "Response Length: " + `len(output)` + " - Response Content (First 50 char): " + output[:50]
5894 return output
5895 # HG changeset patch
5896 # User Chau Nguyen <dangchau1991@yahoo.com>
5897 # Date 1433959927 -10800
5898 # Wed Jun 10 21:12:07 2015 +0300
5899 # Branch REST
5900 # Node ID cff5b11df2488fbcdfe84228c3a9beb0214a6fd3
5901 # Parent 83209b3047a9ba3138ae0cac13bd561f3134bbc1
5902 Implement getting resource from database
5903
5904 diff --git a/roundup/rest.py b/roundup/rest.py
5905 --- a/roundup/rest.py
5906 +++ b/roundup/rest.py
5907 @@ -1,11 +1,14 @@
5908 -# Restful API for Roundup
5909 -#
5910 -# This module is free software, you may redistribute it
5911 -# and/or modify under the same terms as Python.
5912 -#
5913 +"""
5914 +Restful API for Roundup
5915 +
5916 +This module is free software, you may redistribute it
5917 +and/or modify under the same terms as Python.
5918 +"""
5919
5920 import json
5921 import pprint
5922 +from roundup import hyperdb
5923 +from roundup.cgi.templating import Unauthorised
5924
5925
5926 class RestfulInstance(object):
5927 @@ -16,9 +19,54 @@
5928 # TODO: database, translator and instance.actions
5929 self.db = db
5930
5931 + def action_get(self, resource, input):
5932 + classname, itemid = hyperdb.splitDesignator(resource)
5933 + cl = self.db.getclass(classname)
5934 + props = cl.properties.keys()
5935 + props.sort()
5936 + for p in props:
5937 + if not self.db.security.hasPermission('View', self.db.getuid(),
5938 + classname, p, itemid):
5939 + raise Unauthorised('Permission to view %s of %s denied' %
5940 + (p, resource))
5941 + result = [(prop, cl.get(itemid, prop)) for prop in props]
5942 +
5943 + # print type(result)
5944 + # print type(dict(result))
5945 + return json.JSONEncoder().encode(dict(result))
5946 + # return json.dumps(dict(result))
5947 + # return dict(result)
5948 +
5949 def dispatch(self, method, uri, input):
5950 print method
5951 print uri
5952 print type(input)
5953 pprint.pprint(input)
5954 - return ' '.join([method, uri, pprint.pformat(input)])
5955 +
5956 + # PATH is split to multiple pieces
5957 + # 0 - rest
5958 + # 1 - resource
5959 + #
5960 + # Example: rest/issue - collection uri
5961 + # Example: rest/issue573 - element uri
5962 + uri_path = uri.split("/")
5963 + # TODO: use named function for this instead
5964 + # TODO: check roundup/actions.py
5965 + # TODO: if uri_path has more than 2 child, return 404
5966 + output = "METHOD is not supported"
5967 + if method == "GET":
5968 + output = self.action_get(uri_path[1], input)
5969 + elif method == "POST":
5970 + pass
5971 + elif method == "PUT":
5972 + pass
5973 + elif method == "DELETE":
5974 + pass
5975 + elif method == "PATCH":
5976 + pass
5977 + else:
5978 + pass
5979 +
5980 + print output
5981 + print len(output)
5982 + return output
5983 # HG changeset patch
5984 # User Chau Nguyen <dangchau1991@yahoo.com>
5985 # Date 1433527399 -10800
5986 # Fri Jun 05 21:03:19 2015 +0300
5987 # Branch REST
5988 # Node ID 83209b3047a9ba3138ae0cac13bd561f3134bbc1
5989 # Parent d10821d84c4cc0c0e60bf6e38b170eed1b9a990f
5990 Added RestInstance and calling rest from client.py
5991
5992 diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
5993 --- a/roundup/cgi/client.py
5994 +++ b/roundup/cgi/client.py
5995 @@ -22,6 +22,7 @@
5996 from roundup.mailer import Mailer, MessageSendError, encode_quopri
5997 from roundup.cgi import accept_language
5998 from roundup import xmlrpc
5999 +from roundup import rest
6000
6001 from roundup.anypy.cookie_ import CookieError, BaseCookie, SimpleCookie, \
6002 get_cookie_date
6003 @@ -378,6 +379,8 @@
6004 try:
6005 if self.path == 'xmlrpc':
6006 self.handle_xmlrpc()
6007 + elif self.path == 'rest' or self.path[:5] == 'rest/':
6008 + self.handle_rest()
6009 else:
6010 self.inner_main()
6011 finally:
6012 @@ -419,6 +422,27 @@
6013 self.setHeader("Content-Length", str(len(output)))
6014 self.write(output)
6015
6016 + def handle_rest(self):
6017 + # Pull the parameters data out of the form. The "value" attribute
6018 + # will be the raw content of the request.
6019 + input = self.form.value
6020 +
6021 + # Set the charset and language
6022 + self.determine_charset()
6023 + self.determine_language()
6024 + # Open the database as the correct user.
6025 + # TODO: add everything to RestfulDispatcher
6026 + self.determine_user()
6027 + self.check_anonymous_access()
6028 +
6029 + # Call rest library to handle the request
6030 + handler = rest.RestfulInstance(self.db)
6031 + output = handler.dispatch(self.env['REQUEST_METHOD'], self.path, input)
6032 +
6033 + # self.setHeader("Content-Type", "text/xml")
6034 + self.setHeader("Content-Length", str(len(output)))
6035 + self.write(output)
6036 +
6037 def add_ok_message(self, msg, escape=True):
6038 add_message(self._ok_message, msg, escape)
6039
6040 diff --git a/roundup/rest.py b/roundup/rest.py
6041 new file mode 100644
6042 --- /dev/null
6043 +++ b/roundup/rest.py
6044 @@ -0,0 +1,24 @@
6045 +# Restful API for Roundup
6046 +#
6047 +# This module is free software, you may redistribute it
6048 +# and/or modify under the same terms as Python.
6049 +#
6050 +
6051 +import json
6052 +import pprint
6053 +
6054 +
6055 +class RestfulInstance(object):
6056 + """Dummy Handler for REST
6057 + """
6058 +
6059 + def __init__(self, db):
6060 + # TODO: database, translator and instance.actions
6061 + self.db = db
6062 +
6063 + def dispatch(self, method, uri, input):
6064 + print method
6065 + print uri
6066 + print type(input)
6067 + pprint.pprint(input)
6068 + return ' '.join([method, uri, pprint.pformat(input)])
6069 # HG changeset patch
6070 # User Chau Nguyen <dangchau1991@yahoo.com>
6071 # Date 1433304370 -10800
6072 # Wed Jun 03 07:06:10 2015 +0300
6073 # Branch REST
6074 # Node ID d10821d84c4cc0c0e60bf6e38b170eed1b9a990f
6075 # Parent b73fc3e5ee808fd4cda267222eb31997c59b9f1a
6076 Create REST branch for RESTful API
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.