# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1439400860 -10800
#      Wed Aug 12 20:34:20 2015 +0300
# Branch REST
# Node ID 7f2ad229b72595ff48e821755c50d6f1acacd833
# Parent  a8ae1e986147e68a0807f05b99ae1ee1b970b3d2
Update resource links to match the recently URI changes

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -238,7 +238,8 @@
         protocol = 'http'
         host = self.client.env['HTTP_HOST']
         tracker = self.client.env['TRACKER_NAME']
-        self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker)
+        self.base_path = '%s://%s/%s/rest' % (protocol, host, tracker)
+        self.data_path = self.base_path + '/data'
 
     def props_from_args(self, cl, args, itemid=None):
         """Construct a list of properties from the given arguments,
@@ -393,7 +394,7 @@
             raise Unauthorised('Permission to view %s denied' % class_name)
 
         class_obj = self.db.getclass(class_name)
-        class_path = self.base_path + class_name
+        class_path = '%s/%s/' % (self.data_path, class_name)
 
         # Handle filtering and pagination
         filter_props = {}
@@ -494,7 +495,7 @@
         result = {
             'id': item_id,
             'type': class_name,
-            'link': self.base_path + class_name + item_id,
+            'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
             'attributes': dict(result)
         }
 
@@ -537,8 +538,8 @@
         result = {
             'id': item_id,
             'type': type(data),
-            'link': "%s%s%s/%s" %
-                    (self.base_path, class_name, item_id, attr_name),
+            'link': "%s/%s/%s/%s" %
+                    (self.data_path, class_name, item_id, attr_name),
             'data': data
         }
 
@@ -597,7 +598,7 @@
             raise UsageError("Must provide the %s property." % msg)
 
         # set the header Location
-        link = self.base_path + class_name + item_id
+        link = '%s/%s/%s' % (self.data_path, class_name, item_id)
         self.client.setHeader("Location", link)
 
         # set the response body
@@ -650,7 +651,7 @@
         result = {
             'id': item_id,
             'type': class_name,
-            'link': self.base_path + class_name + item_id,
+            'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
             'attribute': result
         }
         return 200, result
@@ -701,7 +702,7 @@
         result = {
             'id': item_id,
             'type': class_name,
-            'link': self.base_path + class_name + item_id,
+            'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
             'attribute': result
         }
 
@@ -885,7 +886,7 @@
             result = {
                 'id': item_id,
                 'type': class_name,
-                'link': self.base_path + class_name + item_id,
+                'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
                 'result': result
             }
         else:
@@ -914,7 +915,7 @@
             result = {
                 'id': item_id,
                 'type': class_name,
-                'link': self.base_path + class_name + item_id,
+                'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
                 'attribute': result
             }
         return 200, result
@@ -980,7 +981,7 @@
         result = {
             'id': item_id,
             'type': class_name,
-            'link': self.base_path + class_name + item_id,
+            'link': '%s/%s/%s' % (self.data_path, class_name, item_id),
             'attribute': result
         }
         return 200, result
diff --git a/test/test_rest.py b/test/test_rest.py
--- a/test/test_rest.py
+++ b/test/test_rest.py
@@ -146,7 +146,7 @@
             status=self.db.status.lookup('open'),
             priority=self.db.priority.lookup('critical')
         )
-        base_path = self.dummy_client.env['PATH_INFO'] + 'issue'
+        base_path = self.dummy_client.env['PATH_INFO'] + 'data/issue/'
 
         # Retrieve all issue status=open
         form = cgi.FieldStorage()
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1439391426 -10800
#      Wed Aug 12 17:57:06 2015 +0300
# Branch REST
# Node ID a8ae1e986147e68a0807f05b99ae1ee1b970b3d2
# Parent  d6631549bdb62b86d669dc9695809fc4333a9b3c
Added Patch operator 'action' to perform actions such as 'retire'

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -16,6 +16,7 @@
 
 from roundup import hyperdb
 from roundup import date
+from roundup import actions
 from roundup.exceptions import *
 from roundup.cgi.exceptions import *
 
@@ -230,6 +231,9 @@
     def __init__(self, client, db):
         self.client = client
         self.db = db
+        self.translator = client.translator
+        self.actions = client.instance.actions.copy()
+        self.actions.update({'retire': actions.Retire})
 
         protocol = 'http'
         host = self.client.env['HTTP_HOST']
@@ -856,33 +860,63 @@
             op = self.__default_patch_op
         class_obj = self.db.getclass(class_name)
 
-        props = self.props_from_args(class_obj, input.value, item_id)
+        # if patch operation is action, call the action handler
+        action_args = [class_name + item_id]
+        if op == 'action':
+            # extract action_name and action_args from form fields
+            for form_field in input.value:
+                key = form_field.name
+                value = form_field.value
+                if key == "action_name":
+                    name = value
+                elif key.startswith('action_args'):
+                    action_args.append(value)
 
-        for prop, value in props.iteritems():
-            if not self.db.security.hasPermission(
-                'Edit', self.db.getuid(), class_name, prop, item_id
-            ):
-                raise Unauthorised(
-                    'Permission to edit %s of %s%s denied' %
-                    (prop, class_name, item_id)
+            if name in self.actions:
+                action_type = self.actions[name]
+            else:
+                raise UsageError(
+                    'action "%s" is not supported %s' %
+                    (name, ','.join(self.actions.keys()))
+                )
+            action = action_type(self.db, self.translator)
+            result = action.execute(*action_args)
+
+            result = {
+                'id': item_id,
+                'type': class_name,
+                'link': self.base_path + class_name + item_id,
+                'result': result
+            }
+        else:
+            # else patch operation is processing data
+            props = self.props_from_args(class_obj, input.value, item_id)
+
+            for prop, value in props.iteritems():
+                if not self.db.security.hasPermission(
+                    'Edit', self.db.getuid(), class_name, prop, item_id
+                ):
+                    raise Unauthorised(
+                        'Permission to edit %s of %s%s denied' %
+                        (prop, class_name, item_id)
+                    )
+
+                props[prop] = self.patch_data(
+                    op, class_obj.get(item_id, prop), props[prop]
                 )
 
-            props[prop] = self.patch_data(
-                op, class_obj.get(item_id, prop), props[prop]
-            )
+            try:
+                result = class_obj.set(item_id, **props)
+                self.db.commit()
+            except (TypeError, IndexError, ValueError), message:
+                raise ValueError(message)
 
-        try:
-            result = class_obj.set(item_id, **props)
-            self.db.commit()
-        except (TypeError, IndexError, ValueError), message:
-            raise ValueError(message)
-
-        result = {
-            'id': item_id,
-            'type': class_name,
-            'link': self.base_path + class_name + item_id,
-            'attribute': result
-        }
+            result = {
+                'id': item_id,
+                'type': class_name,
+                'link': self.base_path + class_name + item_id,
+                'attribute': result
+            }
         return 200, result
 
     @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'PATCH')
diff --git a/test/test_rest.py b/test/test_rest.py
--- a/test/test_rest.py
+++ b/test/test_rest.py
@@ -461,6 +461,25 @@
         self.assertEqual(len(results['attributes']['nosy']), 0)
         self.assertEqual(results['attributes']['nosy'], [])
 
+    def testPatchAction(self):
+        """
+        Test Patch Action 'Action'
+        """
+        # create a new issue with userid 1 and 2 in the nosy list
+        issue_id = self.db.issue.create(title='foo')
+
+        # execute action retire
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('op', 'action'),
+            cgi.MiniFieldStorage('action_name', 'retire')
+        ]
+        results = self.server.patch_element('issue', issue_id, form)
+        self.assertEqual(self.dummy_client.response_code, 200)
+
+        # verify the result
+        self.assertTrue(self.db.issue.is_retired(issue_id))
+
     def testPatchRemove(self):
         """
         Test Patch Action 'Remove' only some element from a list
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1439385301 -10800
#      Wed Aug 12 16:15:01 2015 +0300
# Branch REST
# Node ID d6631549bdb62b86d669dc9695809fc4333a9b3c
# Parent  c158adb21cb4a182c41b5ea7ff4ee0962ca28a6c
Added the ability to limit returned fields by GET

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -465,15 +465,28 @@
             )
 
         class_obj = self.db.getclass(class_name)
-        props = class_obj.properties.keys()
+        props = None
+        for form_field in input.value:
+            key = form_field.name
+            value = form_field.value
+            if key == "fields":
+                props = value.split(",")
+
+        if props is None:
+            props = class_obj.properties.keys()
+
         props.sort()  # sort properties
-        result = [
-            (prop_name, class_obj.get(item_id, prop_name))
-            for prop_name in props
-            if self.db.security.hasPermission(
-                'View', self.db.getuid(), class_name, prop_name,
-            )
-        ]
+
+        try:
+            result = [
+                (prop_name, class_obj.get(item_id, prop_name))
+                for prop_name in props
+                if self.db.security.hasPermission(
+                    'View', self.db.getuid(), class_name, prop_name,
+                )
+            ]
+        except KeyError, msg:
+            raise UsageError("%s field not valid" % msg)
         result = {
             'id': item_id,
             'type': class_name,
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1437048445 -10800
#      Thu Jul 16 15:07:25 2015 +0300
# Branch REST
# Node ID c158adb21cb4a182c41b5ea7ff4ee0962ca28a6c
# Parent  9bcc917b3bb5798de2f54b79a7a48681734d395d
Added routing decorator

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -12,10 +12,12 @@
 import sys
 import time
 import traceback
-import xml
+import re
+
 from roundup import hyperdb
 from roundup import date
 from roundup.exceptions import *
+from roundup.cgi.exceptions import *
 
 
 def _data_decorator(func):
@@ -24,6 +26,9 @@
         # get the data / error from function
         try:
             code, data = func(self, *args, **kwargs)
+        except NotFound, msg:
+            code = 404
+            data = msg
         except IndexError, msg:
             code = 404
             data = msg
@@ -133,6 +138,84 @@
     return result
 
 
+class Routing(object):
+    __route_map = {}
+    __var_to_regex = re.compile(r"<:(\w+)>")
+    url_to_regex = r"([\w.\-~!$&'()*+,;=:@\%%]+)"
+
+    @classmethod
+    def route(cls, rule, methods='GET'):
+        """A decorator that is used to register a view function for a
+        given URL rule:
+            @self.route('/')
+            def index():
+                return 'Hello World'
+
+        rest/ will be added to the beginning of the url string
+
+        Args:
+            rule (string): the URL rule
+            methods (string or tuple or list): the http method
+        """
+        # strip the '/' character from rule string
+        rule = rule.strip('/')
+
+        # add 'rest/' to the rule string
+        if not rule.startswith('rest/'):
+            rule = '^rest/' + rule + '$'
+
+        if isinstance(methods, basestring):  # convert string to tuple
+            methods = (methods,)
+        methods = set(item.upper() for item in methods)
+
+        # convert a rule to a compiled regex object
+        # so /data/<:class>/<:id> will become
+        #    /data/([charset]+)/([charset]+)
+        # and extract the variable names to a list [(class), (id)]
+        func_vars = cls.__var_to_regex.findall(rule)
+        rule = re.compile(cls.__var_to_regex.sub(cls.url_to_regex, rule))
+
+        # then we decorate it:
+        # route_map[regex][method] = func
+        def decorator(func):
+            rule_route = cls.__route_map.get(rule, {})
+            func_obj = {
+                'func': func,
+                'vars': func_vars
+            }
+            for method in methods:
+                rule_route[method] = func_obj
+            cls.__route_map[rule] = rule_route
+            return func
+        return decorator
+
+    @classmethod
+    def execute(cls, instance, path, method, input):
+        # format the input
+        path = path.strip('/').lower()
+        method = method.upper()
+
+        # find the rule match the path
+        # then get handler match the method
+        for path_regex in cls.__route_map:
+            match_obj = path_regex.match(path)
+            if match_obj:
+                try:
+                    func_obj = cls.__route_map[path_regex][method]
+                except KeyError:
+                    raise Reject('Method %s not allowed' % method)
+
+                # retrieve the vars list and the function caller
+                list_vars = func_obj['vars']
+                func = func_obj['func']
+
+                # zip the varlist into a dictionary, and pass it to the caller
+                args = dict(zip(list_vars, match_obj.groups()))
+                args['input'] = input
+                return func(instance, **args)
+        raise NotFound('Nothing matches the given URI')
+
+
 class RestfulInstance(object):
     """The RestfulInstance performs REST request from the client"""
 
@@ -280,6 +363,7 @@
 
         return result
 
+    @Routing.route("/data/<:class_name>", 'GET')
     @_data_decorator
     def get_collection(self, class_name, input):
         """GET resource from class URI.
@@ -297,6 +381,8 @@
                 id: id of the object
                 link: path to the object
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         if not self.db.security.hasPermission(
             'View', self.db.getuid(), class_name
         ):
@@ -348,6 +434,7 @@
         self.client.setHeader("X-Count-Total", str(len(result)))
         return 200, result
 
+    @Routing.route("/data/<:class_name>/<:item_id>", 'GET')
     @_data_decorator
     def get_element(self, class_name, item_id, input):
         """GET resource from object URI.
@@ -368,6 +455,8 @@
                 link: link to the object
                 attributes: a dictionary represent the attributes of the object
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         if not self.db.security.hasPermission(
             'View', self.db.getuid(), class_name, itemid=item_id
         ):
@@ -394,6 +483,7 @@
 
         return 200, result
 
+    @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'GET')
     @_data_decorator
     def get_attribute(self, class_name, item_id, attr_name, input):
         """GET resource from attribute URI.
@@ -415,6 +505,8 @@
                 link: link to the attribute
                 data: data of the requested attribute
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         if not self.db.security.hasPermission(
             'View', self.db.getuid(), class_name, attr_name, item_id
         ):
@@ -435,6 +527,7 @@
 
         return 200, result
 
+    @Routing.route("/data/<:class_name>", 'POST')
     @_data_decorator
     def post_collection(self, class_name, input):
         """POST a new object to a class
@@ -452,6 +545,8 @@
                 id: id of the object
                 link: path to the object
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         if not self.db.security.hasPermission(
             'Create', self.db.getuid(), class_name
         ):
@@ -495,21 +590,7 @@
         }
         return 201, result
 
-    @_data_decorator
-    def post_element(self, class_name, item_id, input):
-        """POST to an object of a class is not allowed"""
-        raise Reject('POST to an item is not allowed')
-
-    @_data_decorator
-    def post_attribute(self, class_name, item_id, attr_name, input):
-        """POST to an attribute of an object is not allowed"""
-        raise Reject('POST to an attribute is not allowed')
-
-    @_data_decorator
-    def put_collection(self, class_name, input):
-        """PUT a class is not allowed"""
-        raise Reject('PUT a class is not allowed')
-
+    @Routing.route("/data/<:class_name>/<:item_id>", 'PUT')
     @_data_decorator
     def put_element(self, class_name, item_id, input):
         """PUT a new content to an object
@@ -530,6 +611,8 @@
                 attributes: a dictionary represent only changed attributes of
                             the object
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         class_obj = self.db.getclass(class_name)
 
         props = self.props_from_args(class_obj, input.value, item_id)
@@ -555,6 +638,7 @@
         }
         return 200, result
 
+    @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'PUT')
     @_data_decorator
     def put_attribute(self, class_name, item_id, attr_name, input):
         """PUT an attribute to an object
@@ -574,6 +658,8 @@
                 attributes: a dictionary represent only changed attributes of
                             the object
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         if not self.db.security.hasPermission(
             'Edit', self.db.getuid(), class_name, attr_name, item_id
         ):
@@ -604,6 +690,7 @@
 
         return 200, result
 
+    @Routing.route("/data/<:class_name>", 'DELETE')
     @_data_decorator
     def delete_collection(self, class_name, input):
         """DELETE all objects in a class
@@ -618,6 +705,8 @@
                 status (string): 'ok'
                 count (int): number of deleted objects
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         if not self.db.security.hasPermission(
             'Delete', self.db.getuid(), class_name
         ):
@@ -644,6 +733,7 @@
 
         return 200, result
 
+    @Routing.route("/data/<:class_name>/<:item_id>", 'DELETE')
     @_data_decorator
     def delete_element(self, class_name, item_id, input):
         """DELETE an object in a class
@@ -658,6 +748,8 @@
             dict:
                 status (string): 'ok'
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         if not self.db.security.hasPermission(
             'Delete', self.db.getuid(), class_name, itemid=item_id
         ):
@@ -673,6 +765,7 @@
 
         return 200, result
 
+    @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'DELETE')
     @_data_decorator
     def delete_attribute(self, class_name, item_id, attr_name, input):
         """DELETE an attribute in a object by setting it to None or empty
@@ -688,6 +781,8 @@
             dict:
                 status (string): 'ok'
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         if not self.db.security.hasPermission(
             'Edit', self.db.getuid(), class_name, attr_name, item_id
         ):
@@ -716,11 +811,7 @@
 
         return 200, result
 
-    @_data_decorator
-    def patch_collection(self, class_name, input):
-        """PATCH a class is not allowed"""
-        raise Reject('PATCH a class is not allowed')
-
+    @Routing.route("/data/<:class_name>/<:item_id>", 'PATCH')
     @_data_decorator
     def patch_element(self, class_name, item_id, input):
         """PATCH an object
@@ -744,6 +835,8 @@
                 attributes: a dictionary represent only changed attributes of
                             the object
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         try:
             op = input['op'].value.lower()
         except KeyError:
@@ -779,6 +872,7 @@
         }
         return 200, result
 
+    @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'PATCH')
     @_data_decorator
     def patch_attribute(self, class_name, item_id, attr_name, input):
         """PATCH an attribute of an object
@@ -803,6 +897,8 @@
                 attributes: a dictionary represent only changed attributes of
                             the object
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         try:
             op = input['op'].value.lower()
         except KeyError:
@@ -842,6 +938,7 @@
         }
         return 200, result
 
+    @Routing.route("/data/<:class_name>", 'OPTIONS')
     @_data_decorator
     def options_collection(self, class_name, input):
         """OPTION return the HTTP Header for the class uri
@@ -850,8 +947,11 @@
             int: http status code 204 (No content)
             body (string): an empty string
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         return 204, ""
 
+    @Routing.route("/data/<:class_name>/<:item_id>", 'OPTIONS')
     @_data_decorator
     def options_element(self, class_name, item_id, input):
         """OPTION return the HTTP Header for the object uri
@@ -860,12 +960,15 @@
             int: http status code 204 (No content)
             body (string): an empty string
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         self.client.setHeader(
             "Accept-Patch",
             "application/x-www-form-urlencoded, multipart/form-data"
         )
         return 204, ""
 
+    @Routing.route("/data/<:class_name>/<:item_id>/<:attr_name>", 'OPTIONS')
     @_data_decorator
     def option_attribute(self, class_name, item_id, attr_name, input):
         """OPTION return the HTTP Header for the attribute uri
@@ -874,12 +977,15 @@
             int: http status code 204 (No content)
             body (string): an empty string
         """
+        if class_name not in self.db.classes:
+            raise NotFound('Class %s not found' % class_name)
         self.client.setHeader(
             "Accept-Patch",
             "application/x-www-form-urlencoded, multipart/form-data"
         )
         return 204, ""
 
+    @Routing.route("/summary")
     @_data_decorator
     def summary(self, input):
         """Get a summary of resource from class URI.
@@ -983,44 +1089,13 @@
             "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
         )
 
-        # PATH is split to multiple pieces
-        # 0 - rest
-        # 1 - data
-        # 2 - resource
-        # 3 - attribute
-        uri_split = uri.lower().split("/")
-
         # Call the appropriate method
-        if len(uri_split) == 2 and uri_split[1] == 'summary':
-            output = self.summary(input)
-        elif 4 >= len(uri_split) > 2 and uri_split[1] == 'data':
-            resource_uri = uri_split[2]
-            try:
-                class_name, item_id = hyperdb.splitDesignator(resource_uri)
-            except hyperdb.DesignatorError:
-                class_name = resource_uri
-                item_id = None
-
-            if class_name not in self.db.classes:
-                output = self.error_obj(404, "Not found")
-            elif item_id is None:
-                if len(uri_split) == 3:
-                    output = getattr(
-                        self, "%s_collection" % method.lower()
-                    )(class_name, input)
-                else:
-                    output = self.error_obj(404, "Not found")
-            else:
-                if len(uri_split) == 3:
-                    output = getattr(
-                        self, "%s_element" % method.lower()
-                    )(class_name, item_id, input)
-                else:
-                    output = getattr(
-                        self, "%s_attribute" % method.lower()
-                    )(class_name, item_id, uri_split[3], input)
-        else:
-            output = self.error_obj(404, "Not found")
+        try:
+            output = Routing.execute(self, uri, method, input)
+        except NotFound, msg:
+            output = self.error_obj(404, msg)
+        except Reject, msg:
+            output = self.error_obj(405, msg)
 
         # Format the content type
         if data_type.lower() == "json":
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436781974 -10800
#      Mon Jul 13 13:06:14 2015 +0300
# Branch REST
# Node ID 9bcc917b3bb5798de2f54b79a7a48681734d395d
# Parent  0acc1d03c544871964a21908ab20d24d7507ef2e
Added summary page,
Change data uri of class methods from /rest/class to /rest/data/class

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -14,6 +14,7 @@
 import traceback
 import xml
 from roundup import hyperdb
+from roundup import date
 from roundup.exceptions import *
 
 
@@ -70,6 +71,7 @@
         return result
     return format_object
 
+
 def parse_accept_header(accept):
     """
     Parse the Accept header *accept*, returning a list with 3-tuples of
@@ -111,7 +113,7 @@
                             version = media_params.append(('version',
                                                            float(rest)))
                         except ValueError:
-                            version = 1.0 # could not be parsed
+                            version = 1.0  # could not be parsed
                 # add the vendor code as a media param
                 media_params.append(('vendor', vnd))
                 # and re-write media_type to something like application/json so
@@ -130,6 +132,7 @@
     result.sort(lambda x, y: -cmp(x[2], y[2]))
     return result
 
+
 class RestfulInstance(object):
     """The RestfulInstance performs REST request from the client"""
 
@@ -877,6 +880,68 @@
         )
         return 204, ""
 
+    @_data_decorator
+    def summary(self, input):
+        """Get a summary of resource from class URI.
+
+        This function returns only items have View permission
+        class_name should be valid already
+
+        Args:
+            class_name (string): class name of the resource (Ex: issue, msg)
+            input (list): the submitted form of the user
+
+        Returns:
+            int: http status code 200 (OK)
+            list:
+        """
+        if not self.db.security.hasPermission(
+            'View', self.db.getuid(), 'issue'
+        ) and not self.db.security.hasPermission(
+            'View', self.db.getuid(), 'status'
+        ) and not self.db.security.hasPermission(
+            'View', self.db.getuid(), 'issue'
+        ):
+            raise Unauthorised('Permission to view summary denied')
+
+        old = date.Date('-1w')
+
+        created = []
+        summary = {}
+        messages = []
+
+        # loop through all the recently-active issues
+        for issue_id in self.db.issue.filter(None, {'activity': '-1w;'}):
+            num = 0
+            status_name = self.db.status.get(
+                self.db.issue.get(issue_id, 'status'),
+                'name'
+            )
+            issue_object = {
+                'id': issue_id,
+                'link': self.base_path + 'issue' + issue_id,
+                'title': self.db.issue.get(issue_id, 'title')
+            }
+            for x, ts, uid, action, data in self.db.issue.history(issue_id):
+                if ts < old:
+                    continue
+                if action == 'create':
+                    created.append(issue_object)
+                elif action == 'set' and 'messages' in data:
+                    num += 1
+            summary.setdefault(status_name, []).append(issue_object)
+            messages.append((num, issue_object))
+
+        messages.sort(reverse=True)
+
+        result = {
+            'created': created,
+            'summary': summary,
+            'most_discussed': messages[:10]
+        }
+
+        return 200, result
+
     def dispatch(self, method, uri, input):
         """format and process the request"""
         # if X-HTTP-Method-Override is set, follow the override method
@@ -920,34 +985,42 @@
 
         # PATH is split to multiple pieces
         # 0 - rest
-        # 1 - resource
-        # 2 - attribute
-        uri_split = uri.split("/")
-        resource_uri = uri_split[1]
-
-        try:
-            class_name, item_id = hyperdb.splitDesignator(resource_uri)
-        except hyperdb.DesignatorError:
-            class_name = resource_uri
-            item_id = None
+        # 1 - data
+        # 2 - resource
+        # 3 - attribute
+        uri_split = uri.lower().split("/")
 
         # Call the appropriate method
-        if (class_name not in self.db.classes) or (len(uri_split) > 3):
+        if len(uri_split) == 2 and uri_split[1] == 'summary':
+            output = self.summary(input)
+        elif 4 >= len(uri_split) > 2 and uri_split[1] == 'data':
+            resource_uri = uri_split[2]
+            try:
+                class_name, item_id = hyperdb.splitDesignator(resource_uri)
+            except hyperdb.DesignatorError:
+                class_name = resource_uri
+                item_id = None
+
+            if class_name not in self.db.classes:
+                output = self.error_obj(404, "Not found")
+            elif item_id is None:
+                if len(uri_split) == 3:
+                    output = getattr(
+                        self, "%s_collection" % method.lower()
+                    )(class_name, input)
+                else:
+                    output = self.error_obj(404, "Not found")
+            else:
+                if len(uri_split) == 3:
+                    output = getattr(
+                        self, "%s_element" % method.lower()
+                    )(class_name, item_id, input)
+                else:
+                    output = getattr(
+                        self, "%s_attribute" % method.lower()
+                    )(class_name, item_id, uri_split[3], input)
+        else:
             output = self.error_obj(404, "Not found")
-        elif item_id is None:
-            if len(uri_split) == 2:
-                output = getattr(
-                    self, "%s_collection" % method.lower()
-                )(class_name, input)
-        else:
-            if len(uri_split) == 2:
-                output = getattr(
-                    self, "%s_element" % method.lower()
-                )(class_name, item_id, input)
-            else:
-                output = getattr(
-                    self, "%s_attribute" % method.lower()
-                )(class_name, item_id, uri_split[2], input)
 
         # Format the content type
         if data_type.lower() == "json":
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436539349 -10800
#      Fri Jul 10 17:42:29 2015 +0300
# Branch REST
# Node ID 0acc1d03c544871964a21908ab20d24d7507ef2e
# Parent  0b4d67336a900a5f74d9f8320bffc06d435e4ca8
Handle operation for patch separately,
Patch remove operation is now able to remove element from list and dict,
Added more test on new changes

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -12,6 +12,7 @@
 import sys
 import time
 import traceback
+import xml
 from roundup import hyperdb
 from roundup.exceptions import *
 
@@ -223,6 +224,59 @@
 
         return result
 
+    def patch_data(self, op, old_val, new_val):
+        """Perform patch operation based on old_val and new_val
+
+        Args:
+            op (string): PATCH operation: add, replace, remove
+            old_val: old value of the property
+            new_val: new value of the property
+
+        Returns:
+            result (string): value after performed the operation
+        """
+        # add operation: If neither of the value is None, use the other one
+        #                Otherwise, concat those 2 value
+        if op == 'add':
+            if old_val is None:
+                result = new_val
+            elif new_val is None:
+                result = old_val
+            else:
+                result = old_val + new_val
+        # Replace operation: new value is returned
+        elif op == 'replace':
+            result = new_val
+        # Remove operation:
+        #   if old_val is not a list/dict, change it to None
+        #   if old_val is a list/dict, but the parameter is empty,
+        #       change it to none
+        #   if old_val is a list/dict, and parameter is not empty
+        #       proceed to remove the values from parameter from the list/dict
+        elif op == 'remove':
+            if isinstance(old_val, list):
+                if new_val is None:
+                    result = []
+                elif isinstance(new_val, list):
+                    result = [x for x in old_val if x not in new_val]
+                else:
+                    if new_val in old_val:
+                        old_val.remove(new_val)
+            elif isinstance(old_val, dict):
+                if new_val is None:
+                    result = {}
+                elif isinstance(new_val, dict):
+                    for x in new_val:
+                        old_val.pop(x, None)
+                else:
+                    old_val.pop(new_val, None)
+            else:
+                result = None
+        else:
+            raise UsageError('PATCH Operation %s is not allowed' % op)
+
+        return result
+
     @_data_decorator
     def get_collection(self, class_name, input):
         """GET resource from class URI.
@@ -704,18 +758,9 @@
                     (prop, class_name, item_id)
                 )
 
-            if op == 'add':
-                props[prop] = class_obj.get(item_id, prop) + props[prop]
-            elif op == 'replace':
-                pass
-            elif op == 'remove':
-                current_prop = class_obj.get(item_id, prop)
-                if isinstance(current_prop, list):
-                    props[prop] = []
-                else:
-                    props[prop] = None
-            else:
-                raise UsageError('PATCH Operation %s is not allowed' % op)
+            props[prop] = self.patch_data(
+                op, class_obj.get(item_id, prop), props[prop]
+            )
 
         try:
             result = class_obj.set(item_id, **props)
@@ -776,18 +821,9 @@
             )
         }
 
-        if op == 'add':
-            props[prop] = class_obj.get(item_id, prop) + props[prop]
-        elif op == 'replace':
-            pass
-        elif op == 'remove':
-            current_prop = class_obj.get(item_id, prop)
-            if isinstance(current_prop, list):
-                props[prop] = []
-            else:
-                props[prop] = None
-        else:
-            raise UsageError('PATCH Operation %s is not allowed' % op)
+        props[prop] = self.patch_data(
+            op, class_obj.get(item_id, prop), props[prop]
+        )
 
         try:
             result = class_obj.set(item_id, **props)
diff --git a/test/test_rest.py b/test/test_rest.py
--- a/test/test_rest.py
+++ b/test/test_rest.py
@@ -440,7 +440,7 @@
         """
         Test Patch Action 'Remove'
         """
-        # create a new issue with userid 1 in the nosy list
+        # create a new issue with userid 1 and 2 in the nosy list
         issue_id = self.db.issue.create(title='foo', nosy=['1', '2'])
 
         # remove the nosy list and the title
@@ -461,6 +461,29 @@
         self.assertEqual(len(results['attributes']['nosy']), 0)
         self.assertEqual(results['attributes']['nosy'], [])
 
+    def testPatchRemove(self):
+        """
+        Test Patch Action 'Remove' only some element from a list
+        """
+        # create a new issue with userid 1, 2, 3 in the nosy list
+        issue_id = self.db.issue.create(title='foo', nosy=['1', '2', '3'])
+
+        # remove the nosy list and the title
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('op', 'remove'),
+            cgi.MiniFieldStorage('nosy', '1, 2'),
+        ]
+        results = self.server.patch_element('issue', issue_id, form)
+        self.assertEqual(self.dummy_client.response_code, 200)
+
+        # verify the result
+        results = self.server.get_element('issue', issue_id, self.empty_form)
+        results = results['data']
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertEqual(len(results['attributes']['nosy']), 1)
+        self.assertEqual(results['attributes']['nosy'], ['3'])
+
 
 def get_obj(path, id):
     return {
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436457297 -10800
#      Thu Jul 09 18:54:57 2015 +0300
# Branch REST
# Node ID 0b4d67336a900a5f74d9f8320bffc06d435e4ca8
# Parent  0bffcf76f2e10d936e79c27a2f1a42bf9a12cc12
Added ability to parse HTTP accept header to serve the content type correctly

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -69,11 +69,76 @@
         return result
     return format_object
 
+def parse_accept_header(accept):
+    """
+    Parse the Accept header *accept*, returning a list with 3-tuples of
+    [(str(media_type), dict(params), float(q_value)),] ordered by q values.
+
+    If the accept header includes vendor-specific types like::
+        application/vnd.yourcompany.yourproduct-v1.1+json
+
+    It will actually convert the vendor and version into parameters and
+    convert the content type into `application/json` so appropriate content
+    negotiation decisions can be made.
+
+    Default `q` for values that are not specified is 1.0
+
+    # Based on https://gist.github.com/samuraisam/2714195
+    # Also, based on a snipped found in this project:
+    #   https://github.com/martinblech/mimerender
+    """
+    result = []
+    for media_range in accept.split(","):
+        parts = media_range.split(";")
+        media_type = parts.pop(0).strip()
+        media_params = []
+        # convert vendor-specific content types into something useful (see
+        # docstring)
+        typ, subtyp = media_type.split('/')
+        # check for a + in the sub-type
+        if '+' in subtyp:
+            # if it exists, determine if the subtype is a vendor-specific type
+            vnd, sep, extra = subtyp.partition('+')
+            if vnd.startswith('vnd'):
+                # and then... if it ends in something like "-v1.1" parse the
+                # version out
+                if '-v' in vnd:
+                    vnd, sep, rest = vnd.rpartition('-v')
+                    if len(rest):
+                        # add the version as a media param
+                        try:
+                            version = media_params.append(('version',
+                                                           float(rest)))
+                        except ValueError:
+                            version = 1.0 # could not be parsed
+                # add the vendor code as a media param
+                media_params.append(('vendor', vnd))
+                # and re-write media_type to something like application/json so
+                # it can be used usefully when looking up emitters
+                media_type = '{}/{}'.format(typ, extra)
+        q = 1.0
+        for part in parts:
+            (key, value) = part.lstrip().split("=", 1)
+            key = key.strip()
+            value = value.strip()
+            if key == "q":
+                q = float(value)
+            else:
+                media_params.append((key, value))
+        result.append((media_type, dict(media_params), q))
+    result.sort(lambda x, y: -cmp(x[2], y[2]))
+    return result
 
 class RestfulInstance(object):
     """The RestfulInstance performs REST request from the client"""
 
     __default_patch_op = "replace"  # default operator for PATCH method
+    __accepted_content_type = {
+        "application/json": "json",
+        "*/*": "json"
+        # "application/xml": "xml"
+    }
+    __default_accept_type = "json"
 
     def __init__(self, client, db):
         self.client = client
@@ -778,26 +843,23 @@
 
     def dispatch(self, method, uri, input):
         """format and process the request"""
-        # PATH is split to multiple pieces
-        # 0 - rest
-        # 1 - resource
-        # 2 - attribute
-        uri_split = uri.split("/")
-        resource_uri = uri_split[1]
-
         # if X-HTTP-Method-Override is set, follow the override method
         headers = self.client.request.headers
         method = headers.getheader('X-HTTP-Method-Override') or method
 
+        # parse Accept header and get the content type
+        accept_header = parse_accept_header(headers.getheader('Accept'))
+        accept_type = "invalid"
+        for part in accept_header:
+            if part[0] in self.__accepted_content_type:
+                accept_type = self.__accepted_content_type[part[0]]
+
         # get the request format for response
         # priority : extension from uri (/rest/issue.json),
         #            header (Accept: application/json, application/xml)
         #            default (application/json)
-
-        # format_header need a priority parser
         ext_type = os.path.splitext(urlparse.urlparse(uri).path)[1][1:]
-        accept_header = headers.getheader('Accept')[12:]
-        data_type = ext_type or accept_header or "json"
+        data_type = ext_type or accept_type or self.__default_accept_type
 
         # check for pretty print
         try:
@@ -819,6 +881,14 @@
             "Access-Control-Allow-Methods",
             "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
         )
+
+        # PATH is split to multiple pieces
+        # 0 - rest
+        # 1 - resource
+        # 2 - attribute
+        uri_split = uri.split("/")
+        resource_uri = uri_split[1]
+
         try:
             class_name, item_id = hyperdb.splitDesignator(resource_uri)
         except hyperdb.DesignatorError:
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436437290 -10800
#      Thu Jul 09 13:21:30 2015 +0300
# Branch REST
# Node ID 0bffcf76f2e10d936e79c27a2f1a42bf9a12cc12
# Parent  c1bb6dbb6430e1c0868a9bf2bd9e63087efce23a
Added support to print error to output when server DEBUG_MODE is true,
some coding improvements

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -43,12 +43,14 @@
         except:
             exc, val, tb = sys.exc_info()
             code = 400
-            # if self.DEBUG_MODE in roundup_server
-            # else data = 'An error occurred. Please check...',
-            data = val
-
+            ts = time.ctime()
+            if self.client.request.DEBUG_MODE:
+                data = val
+            else:
+                data = '%s: An error occurred. Please check the server log' \
+                       ' for more information.' % ts
             # out to the logfile
-            print 'EXCEPTION AT', time.ctime()
+            print 'EXCEPTION AT', ts
             traceback.print_exc()
 
         # decorate it
@@ -71,6 +73,8 @@
 class RestfulInstance(object):
     """The RestfulInstance performs REST request from the client"""
 
+    __default_patch_op = "replace"  # default operator for PATCH method
+
     def __init__(self, client, db):
         self.client = client
         self.db = db
@@ -621,7 +625,7 @@
         try:
             op = input['op'].value.lower()
         except KeyError:
-            op = "replace"
+            op = self.__default_patch_op
         class_obj = self.db.getclass(class_name)
 
         props = self.props_from_args(class_obj, input.value, item_id)
@@ -689,8 +693,7 @@
         try:
             op = input['op'].value.lower()
         except KeyError:
-            op = "replace"
-        class_obj = self.db.getclass(class_name)
+            op = self.__default_patch_op
 
         if not self.db.security.hasPermission(
             'Edit', self.db.getuid(), class_name, attr_name, item_id
@@ -818,7 +821,7 @@
         )
         try:
             class_name, item_id = hyperdb.splitDesignator(resource_uri)
-        except hyperdb.DesignatorError, msg:
+        except hyperdb.DesignatorError:
             class_name = resource_uri
             item_id = None
 
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436393721 -10800
#      Thu Jul 09 01:15:21 2015 +0300
# Branch REST
# Node ID c1bb6dbb6430e1c0868a9bf2bd9e63087efce23a
# Parent  2f8030bc3227e81677bdc46f84657aa4b29a38f6
Add unittest for pagination and filtering

diff --git a/test/test_rest.py b/test/test_rest.py
--- a/test/test_rest.py
+++ b/test/test_rest.py
@@ -8,6 +8,7 @@
 from roundup.rest import RestfulInstance
 from roundup.backends import list_backends
 from roundup.cgi import client
+import random
 
 import db_test_base
 
@@ -93,6 +94,149 @@
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['data']['data'], 'joe')
 
+    def testFilter(self):
+        """
+        Retrieve all three users
+        obtain data for 'joe'
+        """
+        # create sample data
+        try:
+            self.db.status.create(name='open')
+        except ValueError:
+            pass
+        try:
+            self.db.status.create(name='closed')
+        except ValueError:
+            pass
+        try:
+            self.db.priority.create(name='normal')
+        except ValueError:
+            pass
+        try:
+            self.db.priority.create(name='critical')
+        except ValueError:
+            pass
+        self.db.issue.create(
+            title='foo4',
+            status=self.db.status.lookup('closed'),
+            priority=self.db.priority.lookup('critical')
+        )
+        self.db.issue.create(
+            title='foo1',
+            status=self.db.status.lookup('open'),
+            priority=self.db.priority.lookup('normal')
+        )
+        issue_open_norm = self.db.issue.create(
+            title='foo2',
+            status=self.db.status.lookup('open'),
+            priority=self.db.priority.lookup('normal')
+        )
+        issue_closed_norm = self.db.issue.create(
+            title='foo3',
+            status=self.db.status.lookup('closed'),
+            priority=self.db.priority.lookup('normal')
+        )
+        issue_closed_crit = self.db.issue.create(
+            title='foo4',
+            status=self.db.status.lookup('closed'),
+            priority=self.db.priority.lookup('critical')
+        )
+        issue_open_crit = self.db.issue.create(
+            title='foo5',
+            status=self.db.status.lookup('open'),
+            priority=self.db.priority.lookup('critical')
+        )
+        base_path = self.dummy_client.env['PATH_INFO'] + 'issue'
+
+        # Retrieve all issue status=open
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('where_status', 'open')
+        ]
+        results = self.server.get_collection('issue', form)
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertIn(get_obj(base_path, issue_open_norm), results['data'])
+        self.assertIn(get_obj(base_path, issue_open_crit), results['data'])
+        self.assertNotIn(
+            get_obj(base_path, issue_closed_norm), results['data']
+        )
+
+        # Retrieve all issue status=closed and priority=critical
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('where_status', 'closed'),
+            cgi.MiniFieldStorage('where_priority', 'critical')
+        ]
+        results = self.server.get_collection('issue', form)
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertIn(get_obj(base_path, issue_closed_crit), results['data'])
+        self.assertNotIn(get_obj(base_path, issue_open_crit), results['data'])
+        self.assertNotIn(
+            get_obj(base_path, issue_closed_norm), results['data']
+        )
+
+        # Retrieve all issue status=closed and priority=normal,critical
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('where_status', 'closed'),
+            cgi.MiniFieldStorage('where_priority', 'normal,critical')
+        ]
+        results = self.server.get_collection('issue', form)
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertIn(get_obj(base_path, issue_closed_crit), results['data'])
+        self.assertIn(get_obj(base_path, issue_closed_norm), results['data'])
+        self.assertNotIn(get_obj(base_path, issue_open_crit), results['data'])
+        self.assertNotIn(get_obj(base_path, issue_open_norm), results['data'])
+
+    def testPagination(self):
+        """
+        Retrieve all three users
+        obtain data for 'joe'
+        """
+        # create sample data
+        for i in range(0, random.randint(5, 10)):
+            self.db.issue.create(title='foo' + str(i))
+
+        # Retrieving all the issues
+        results = self.server.get_collection('issue', self.empty_form)
+        self.assertEqual(self.dummy_client.response_code, 200)
+        total_length = len(results['data'])
+
+        # Pagination will be 70% of the total result
+        page_size = total_length * 70 // 100
+        page_zero_expected = page_size
+        page_one_expected = total_length - page_zero_expected
+
+        # Retrieve page 0
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('page_size', page_size),
+            cgi.MiniFieldStorage('page_index', 0)
+        ]
+        results = self.server.get_collection('issue', form)
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertEqual(len(results['data']), page_zero_expected)
+
+        # Retrieve page 1
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('page_size', page_size),
+            cgi.MiniFieldStorage('page_index', 1)
+        ]
+        results = self.server.get_collection('issue', form)
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertEqual(len(results['data']), page_one_expected)
+
+        # Retrieve page 2
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('page_size', page_size),
+            cgi.MiniFieldStorage('page_index', 2)
+        ]
+        results = self.server.get_collection('issue', form)
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertEqual(len(results['data']), 0)
+
     def testPut(self):
         """
         Change joe's 'realname'
@@ -318,6 +462,13 @@
         self.assertEqual(results['attributes']['nosy'], [])
 
 
+def get_obj(path, id):
+    return {
+        'id': id,
+        'link': path + id
+    }
+
+
 def test_suite():
     suite = unittest.TestSuite()
     for l in list_backends():
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436379655 -10800
#      Wed Jul 08 21:20:55 2015 +0300
# Branch REST
# Node ID 2f8030bc3227e81677bdc46f84657aa4b29a38f6
# Parent  3b248d4d85df162296da34335814040631a6e037
Added filtering and pagination,
Adjust unittest to use empty cgi formfield instead of empty dict

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -178,13 +178,47 @@
 
         class_obj = self.db.getclass(class_name)
         class_path = self.base_path + class_name
+
+        # Handle filtering and pagination
+        filter_props = {}
+        page = {
+            'size': None,
+            'index': None
+        }
+        for form_field in input.value:
+            key = form_field.name
+            value = form_field.value
+            if key.startswith("where_"):  # serve the filter purpose
+                key = key[6:]
+                filter_props[key] = [
+                    getattr(self.db, key).lookup(p)
+                    for p in value.split(",")
+                ]
+            elif key.startswith("page_"):  # serve the paging purpose
+                key = key[5:]
+                value = int(value)
+                page[key] = value
+
+        if not filter_props:
+            obj_list = class_obj.list()
+        else:
+            obj_list = class_obj.filter(None, filter_props)
+
+        # extract result from data
         result = [
             {'id': item_id, 'link': class_path + item_id}
-            for item_id in class_obj.list()
+            for item_id in obj_list
             if self.db.security.hasPermission(
                 'View', self.db.getuid(), class_name, itemid=item_id
             )
         ]
+
+        # pagination
+        if page['size'] is not None and page['index'] is not None:
+            page_start = max(page['index'] * page['size'], 0)
+            page_end = min(page_start + page['size'], len(result))
+            result = result[page_start:page_end]
+
         self.client.setHeader("X-Count-Total", str(len(result)))
         return 200, result
 
@@ -807,7 +841,7 @@
                 )(class_name, item_id, uri_split[2], input)
 
         # Format the content type
-        if format_output.lower() == "json":
+        if data_type.lower() == "json":
             self.client.setHeader("Content-Type", "application/json")
             if pretty_output:
                 indent = 4
diff --git a/test/test_rest.py b/test/test_rest.py
--- a/test/test_rest.py
+++ b/test/test_rest.py
@@ -57,6 +57,7 @@
             'TRACKER_NAME': 'rounduptest'
         }
         self.dummy_client = client.Client(self.instance, None, env, [], None)
+        self.empty_form = cgi.FieldStorage()
 
         self.server = RestfulInstance(self.dummy_client, self.db)
 
@@ -74,19 +75,20 @@
         obtain data for 'joe'
         """
         # Retrieve all three users.
-        results = self.server.get_collection('user', {})
+        results = self.server.get_collection('user', self.empty_form)
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(len(results['data']), 3)
 
         # Obtain data for 'joe'.
-        results = self.server.get_element('user', self.joeid, {})['data']
+        results = self.server.get_element('user', self.joeid, self.empty_form)
+        results = results['data']
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['attributes']['username'], 'joe')
         self.assertEqual(results['attributes']['realname'], 'Joe Random')
 
         # Obtain data for 'joe'.
         results = self.server.get_attribute(
-            'user', self.joeid, 'username', {}
+            'user', self.joeid, 'username', self.empty_form
         )
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['data']['data'], 'joe')
@@ -105,7 +107,7 @@
             'user', self.joeid, 'realname', form
         )
         results = self.server.get_attribute(
-            'user', self.joeid, 'realname', {}
+            'user', self.joeid, 'realname', self.empty_form
         )
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['data']['data'], 'Joe Doe Doe')
@@ -116,7 +118,7 @@
             cgi.MiniFieldStorage('realname', 'Joe Doe')
         ]
         results = self.server.put_element('user', self.joeid, form)
-        results = self.server.get_element('user', self.joeid, {})
+        results = self.server.get_element('user', self.joeid, self.empty_form)
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['data']['attributes']['realname'], 'Joe Doe')
 
@@ -137,7 +139,7 @@
         results = self.server.post_collection('issue', form)
         self.assertEqual(self.dummy_client.response_code, 201)
         issueid = results['data']['id']
-        results = self.server.get_element('issue', issueid, {})
+        results = self.server.get_element('issue', issueid, self.empty_form)
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['data']['attributes']['title'], 'foo')
         self.assertEqual(self.db.issue.get(issueid, "tx_Source"), 'web')
@@ -154,7 +156,8 @@
         results = self.server.post_collection('file', form)
         self.assertEqual(self.dummy_client.response_code, 201)
         fileid = results['data']['id']
-        results = self.server.get_element('file', fileid, {})['data']
+        results = self.server.get_element('file', fileid, self.empty_form)
+        results = results['data']
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['attributes']['content'], 'hello\r\nthere')
 
@@ -224,17 +227,18 @@
 
         # remove the title and nosy
         results = self.server.delete_attribute(
-            'issue', issue_id, 'title', {}
+            'issue', issue_id, 'title', self.empty_form
         )
         self.assertEqual(self.dummy_client.response_code, 200)
 
         results = self.server.delete_attribute(
-            'issue', issue_id, 'nosy', {}
+            'issue', issue_id, 'nosy', self.empty_form
         )
         self.assertEqual(self.dummy_client.response_code, 200)
 
         # verify the result
-        results = self.server.get_element('issue', issue_id, {})['data']
+        results = self.server.get_element('issue', issue_id, self.empty_form)
+        results = results['data']
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(len(results['attributes']['nosy']), 0)
         self.assertListEqual(results['attributes']['nosy'], [])
@@ -257,7 +261,8 @@
         self.assertEqual(self.dummy_client.response_code, 200)
 
         # verify the result
-        results = self.server.get_element('issue', issue_id, {})['data']
+        results = self.server.get_element('issue', issue_id, self.empty_form)
+        results = results['data']
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(len(results['attributes']['nosy']), 2)
         self.assertListEqual(results['attributes']['nosy'], ['1', '2'])
@@ -280,7 +285,8 @@
         self.assertEqual(self.dummy_client.response_code, 200)
 
         # verify the result
-        results = self.server.get_element('issue', issue_id, {})['data']
+        results = self.server.get_element('issue', issue_id, self.empty_form)
+        results = results['data']
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['attributes']['status'], '3')
         self.assertEqual(len(results['attributes']['nosy']), 1)
@@ -304,7 +310,8 @@
         self.assertEqual(self.dummy_client.response_code, 200)
 
         # verify the result
-        results = self.server.get_element('issue', issue_id, {})['data']
+        results = self.server.get_element('issue', issue_id, self.empty_form)
+        results = results['data']
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['attributes']['title'], None)
         self.assertEqual(len(results['attributes']['nosy']), 0)
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436261488 -10800
#      Tue Jul 07 12:31:28 2015 +0300
# Branch REST
# Node ID 3b248d4d85df162296da34335814040631a6e037
# Parent  c16988c1ba10458c2c5b619a172ee9af585ac70e
Change the way core function is called,
Change the return header to allow all methods

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -28,7 +28,7 @@
         except Unauthorised, msg:
             code = 403
             data = msg
-        except (hyperdb.DesignatorError, UsageError), msg:
+        except UsageError, msg:
             code = 400
             data = msg
         except (AttributeError, Reject), msg:
@@ -72,7 +72,7 @@
     """The RestfulInstance performs REST request from the client"""
 
     def __init__(self, client, db):
-        self.client = client  # it might be unnecessary to receive the client
+        self.client = client
         self.db = db
 
         protocol = 'http'
@@ -140,6 +140,20 @@
 
         return prop
 
+    def error_obj(self, status, msg, source=None):
+        """Return an error object"""
+        self.client.response_code = status
+        result = {
+            'error': {
+                'status': status,
+                'msg': msg
+            }
+        }
+        if source is not None:
+            result['error']['source'] = source
+
+        return result
+
     @_data_decorator
     def get_collection(self, class_name, input):
         """GET resource from class URI.
@@ -744,9 +758,9 @@
         #            default (application/json)
 
         # format_header need a priority parser
-        format_ext = os.path.splitext(urlparse.urlparse(uri).path)[1][1:]
-        format_header = headers.getheader('Accept')[12:]
-        format_output = format_ext or format_header or "json"
+        ext_type = os.path.splitext(urlparse.urlparse(uri).path)[1][1:]
+        accept_header = headers.getheader('Accept')[12:]
+        data_type = ext_type or accept_header or "json"
 
         # check for pretty print
         try:
@@ -760,42 +774,39 @@
             "Access-Control-Allow-Headers",
             "Content-Type, Authorization, X-HTTP-Method-Override"
         )
-        if resource_uri in self.db.classes:
-            self.client.setHeader(
-                "Allow",
-                "HEAD, OPTIONS, GET, POST, DELETE"
-            )
-            self.client.setHeader(
-                "Access-Control-Allow-Methods",
-                "HEAD, OPTIONS, GET, POST, DELETE"
-            )
-        else:
-            self.client.setHeader(
-                "Allow",
-                "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
-            )
-            self.client.setHeader(
-                "Access-Control-Allow-Methods",
-                "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
-            )
+        self.client.setHeader(
+            "Allow",
+            "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH"
+        )
+        self.client.setHeader(
+            "Access-Control-Allow-Methods",
+            "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
+        )
+        try:
+            class_name, item_id = hyperdb.splitDesignator(resource_uri)
+        except hyperdb.DesignatorError, msg:
+            class_name = resource_uri
+            item_id = None
 
         # Call the appropriate method
-        output = None
-        if resource_uri in self.db.classes:
-            output = getattr(
-                self, "%s_collection" % method.lower()
-                )(resource_uri, input)
+        if (class_name not in self.db.classes) or (len(uri_split) > 3):
+            output = self.error_obj(404, "Not found")
+        elif item_id is None:
+            if len(uri_split) == 2:
+                output = getattr(
+                    self, "%s_collection" % method.lower()
+                )(class_name, input)
         else:
-            class_name, item_id = hyperdb.splitDesignator(resource_uri)
-            if len(uri_split) == 3:
+            if len(uri_split) == 2:
+                output = getattr(
+                    self, "%s_element" % method.lower()
+                )(class_name, item_id, input)
+            else:
                 output = getattr(
                     self, "%s_attribute" % method.lower()
-                    )(class_name, item_id, uri_split[2], input)
-            else:
-                output = getattr(
-                    self, "%s_element" % method.lower()
-                    )(class_name, item_id, input)
+                )(class_name, item_id, uri_split[2], input)
 
+        # Format the content type
         if format_output.lower() == "json":
             self.client.setHeader("Content-Type", "application/json")
             if pretty_output:
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436191555 -10800
#      Mon Jul 06 17:05:55 2015 +0300
# Branch REST
# Node ID c16988c1ba10458c2c5b619a172ee9af585ac70e
# Parent  734f79fbb55e5b2bd1f29baacb8db05d22b0b728
Fix an indentation bug

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -796,16 +796,16 @@
                     self, "%s_element" % method.lower()
                     )(class_name, item_id, input)
 
-            if format_output.lower() == "json":
-                self.client.setHeader("Content-Type", "application/json")
-                if pretty_output:
-                    indent = 4
-                else:
-                    indent = None
-                output = RoundupJSONEncoder(indent=indent).encode(output)
+        if format_output.lower() == "json":
+            self.client.setHeader("Content-Type", "application/json")
+            if pretty_output:
+                indent = 4
             else:
-                self.client.response_code = 406
-                output = "Content type is not accepted by client"
+                indent = None
+            output = RoundupJSONEncoder(indent=indent).encode(output)
+        else:
+            self.client.response_code = 406
+            output = "Content type is not accepted by client"
 
         return output
 
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436191480 -10800
#      Mon Jul 06 17:04:40 2015 +0300
# Branch REST
# Node ID 734f79fbb55e5b2bd1f29baacb8db05d22b0b728
# Parent  5a16780981ad510e410cbc6af6265d5b45f10e37
Move decorator to outside of the class,
Change unittest to test the new format using the decorator

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -14,7 +14,58 @@
 import traceback
 from roundup import hyperdb
 from roundup.exceptions import *
-from roundup import xmlrpc
+
+
+def _data_decorator(func):
+    """Wrap the returned data into an object."""
+    def format_object(self, *args, **kwargs):
+        # get the data / error from function
+        try:
+            code, data = func(self, *args, **kwargs)
+        except IndexError, msg:
+            code = 404
+            data = msg
+        except Unauthorised, msg:
+            code = 403
+            data = msg
+        except (hyperdb.DesignatorError, UsageError), msg:
+            code = 400
+            data = msg
+        except (AttributeError, Reject), msg:
+            code = 405
+            data = msg
+        except ValueError, msg:
+            code = 409
+            data = msg
+        except NotImplementedError:
+            code = 402  # nothing to pay, just a mark for debugging purpose
+            data = 'Method under development'
+        except:
+            exc, val, tb = sys.exc_info()
+            code = 400
+            # if self.DEBUG_MODE in roundup_server
+            # else data = 'An error occurred. Please check...',
+            data = val
+
+            # out to the logfile
+            print 'EXCEPTION AT', time.ctime()
+            traceback.print_exc()
+
+        # decorate it
+        self.client.response_code = code
+        if code >= 400:  # any error require error format
+            result = {
+                'error': {
+                    'status': code,
+                    'msg': data
+                }
+            }
+        else:
+            result = {
+                'data': data
+            }
+        return result
+    return format_object
 
 
 class RestfulInstance(object):
@@ -89,55 +140,6 @@
 
         return prop
 
-    def _data_decorator(func):
-        """Wrap the returned data into an object.."""
-        def format_object(self, *args, **kwargs):
-            try:
-                code, data = func(self, *args, **kwargs)
-            except IndexError, msg:
-                code = 404
-                data = msg
-            except Unauthorised, msg:
-                code = 403
-                data = msg
-            except (hyperdb.DesignatorError, UsageError), msg:
-                code = 400
-                data = msg
-            except (AttributeError, Reject), msg:
-                code = 405
-                data = msg
-            except ValueError, msg:
-                code = 409
-                data = msg
-            except NotImplementedError:
-                code = 402  # nothing to pay, just a mark for debugging purpose
-                data = 'Method under development'
-            except:
-                exc, val, tb = sys.exc_info()
-                code = 400
-                # if self.DEBUG_MODE in roundup_server
-                # else data = 'An error occurred. Please check...',
-                data = val
-
-                # out to the logfile
-                print 'EXCEPTION AT', time.ctime()
-                traceback.print_exc()
-
-            self.client.response_code = code
-            if code >= 400:  # any error require error format
-                result = {
-                    'error': {
-                        'status': code,
-                        'msg': data
-                    }
-                }
-            else:
-                result = {
-                    'data': data
-                }
-            return result
-        return format_object
-
     @_data_decorator
     def get_collection(self, class_name, input):
         """GET resource from class URI.
diff --git a/test/test_rest.py b/test/test_rest.py
--- a/test/test_rest.py
+++ b/test/test_rest.py
@@ -56,9 +56,9 @@
             'HTTP_HOST': 'localhost',
             'TRACKER_NAME': 'rounduptest'
         }
-        dummy_client = client.Client(self.instance, None, env, [], None)
+        self.dummy_client = client.Client(self.instance, None, env, [], None)
 
-        self.server = RestfulInstance(dummy_client, self.db)
+        self.server = RestfulInstance(self.dummy_client, self.db)
 
     def tearDown(self):
         self.db.close()
@@ -74,22 +74,22 @@
         obtain data for 'joe'
         """
         # Retrieve all three users.
-        code, results = self.server.get_collection('user', {})
-        self.assertEqual(code, 200)
-        self.assertEqual(len(results), 3)
+        results = self.server.get_collection('user', {})
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertEqual(len(results['data']), 3)
 
         # Obtain data for 'joe'.
-        code, results = self.server.get_element('user', self.joeid, {})
-        self.assertEqual(code, 200)
+        results = self.server.get_element('user', self.joeid, {})['data']
+        self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['attributes']['username'], 'joe')
         self.assertEqual(results['attributes']['realname'], 'Joe Random')
 
         # Obtain data for 'joe'.
-        code, results = self.server.get_attribute(
+        results = self.server.get_attribute(
             'user', self.joeid, 'username', {}
         )
-        self.assertEqual(code, 200)
-        self.assertEqual(results['data'], 'joe')
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertEqual(results['data']['data'], 'joe')
 
     def testPut(self):
         """
@@ -101,30 +101,29 @@
         form.list = [
             cgi.MiniFieldStorage('data', 'Joe Doe Doe')
         ]
-        code, results = self.server.put_attribute(
+        results = self.server.put_attribute(
             'user', self.joeid, 'realname', form
         )
-        code, results = self.server.get_attribute(
+        results = self.server.get_attribute(
             'user', self.joeid, 'realname', {}
         )
-        self.assertEqual(code, 200)
-        self.assertEqual(results['data'], 'Joe Doe Doe')
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertEqual(results['data']['data'], 'Joe Doe Doe')
 
         # Reset joe's 'realname'.
         form = cgi.FieldStorage()
         form.list = [
             cgi.MiniFieldStorage('realname', 'Joe Doe')
         ]
-        code, results = self.server.put_element('user', self.joeid, form)
-        code, results = self.server.get_element('user', self.joeid, {})
-        self.assertEqual(code, 200)
-        self.assertEqual(results['attributes']['realname'], 'Joe Doe')
+        results = self.server.put_element('user', self.joeid, form)
+        results = self.server.get_element('user', self.joeid, {})
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertEqual(results['data']['attributes']['realname'], 'Joe Doe')
 
         # check we can't change admin's details
-        self.assertRaises(
-            Unauthorised,
-            self.server.put_element, 'user', '1', form
-        )
+        results = self.server.put_element('user', '1', form)
+        self.assertEqual(self.dummy_client.response_code, 403)
+        self.assertEqual(results['error']['status'], 403)
 
     def testPost(self):
         """
@@ -135,12 +134,12 @@
         form.list = [
             cgi.MiniFieldStorage('title', 'foo')
         ]
-        code, results = self.server.post_collection('issue', form)
-        self.assertEqual(code, 201)
-        issueid = results['id']
-        code, results = self.server.get_element('issue', issueid, {})
-        self.assertEqual(code, 200)
-        self.assertEqual(results['attributes']['title'], 'foo')
+        results = self.server.post_collection('issue', form)
+        self.assertEqual(self.dummy_client.response_code, 201)
+        issueid = results['data']['id']
+        results = self.server.get_element('issue', issueid, {})
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertEqual(results['data']['attributes']['title'], 'foo')
         self.assertEqual(self.db.issue.get(issueid, "tx_Source"), 'web')
 
     def testPostFile(self):
@@ -152,11 +151,11 @@
         form.list = [
             cgi.MiniFieldStorage('content', 'hello\r\nthere')
         ]
-        code, results = self.server.post_collection('file', form)
-        self.assertEqual(code, 201)
-        fileid = results['id']
-        code, results = self.server.get_element('file', fileid, {})
-        self.assertEqual(code, 200)
+        results = self.server.post_collection('file', form)
+        self.assertEqual(self.dummy_client.response_code, 201)
+        fileid = results['data']['id']
+        results = self.server.get_element('file', fileid, {})['data']
+        self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['attributes']['content'], 'hello\r\nthere')
 
     def testAuthDeniedPut(self):
@@ -168,10 +167,9 @@
         form.list = [
             cgi.MiniFieldStorage('realname', 'someone')
         ]
-        self.assertRaises(
-            Unauthorised,
-            self.server.put_element, 'user', '1', form
-        )
+        results = self.server.put_element('user', '1', form)
+        self.assertEqual(self.dummy_client.response_code, 403)
+        self.assertEqual(results['error']['status'], 403)
 
     def testAuthDeniedPost(self):
         """
@@ -181,10 +179,9 @@
         form.list = [
             cgi.MiniFieldStorage('username', 'blah')
         ]
-        self.assertRaises(
-            Unauthorised,
-            self.server.post_collection, 'user', form
-        )
+        results = self.server.post_collection('user', form)
+        self.assertEqual(self.dummy_client.response_code, 403)
+        self.assertEqual(results['error']['status'], 403)
 
     def testAuthAllowedPut(self):
         """
@@ -196,10 +193,9 @@
             cgi.MiniFieldStorage('realname', 'someone')
         ]
         try:
-            try:
-                self.server.put_element('user', '2', form)
-            except Unauthorised, err:
-                self.fail('raised %s' % err)
+            self.server.put_element('user', '2', form)
+        except Unauthorised, err:
+            self.fail('raised %s' % err)
         finally:
             self.db.setCurrentUser('joe')
 
@@ -213,10 +209,9 @@
             cgi.MiniFieldStorage('username', 'blah')
         ]
         try:
-            try:
-                self.server.post_collection('user', form)
-            except Unauthorised, err:
-                self.fail('raised %s' % err)
+            self.server.post_collection('user', form)
+        except Unauthorised, err:
+            self.fail('raised %s' % err)
         finally:
             self.db.setCurrentUser('joe')
 
@@ -228,19 +223,19 @@
         issue_id = self.db.issue.create(title='foo', nosy=['1'])
 
         # remove the title and nosy
-        code, results = self.server.delete_attribute(
+        results = self.server.delete_attribute(
             'issue', issue_id, 'title', {}
         )
-        self.assertEqual(code, 200)
+        self.assertEqual(self.dummy_client.response_code, 200)
 
-        code, results = self.server.delete_attribute(
+        results = self.server.delete_attribute(
             'issue', issue_id, 'nosy', {}
         )
-        self.assertEqual(code, 200)
+        self.assertEqual(self.dummy_client.response_code, 200)
 
         # verify the result
-        code, results = self.server.get_element('issue', issue_id, {})
-        self.assertEqual(code, 200)
+        results = self.server.get_element('issue', issue_id, {})['data']
+        self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(len(results['attributes']['nosy']), 0)
         self.assertListEqual(results['attributes']['nosy'], [])
         self.assertEqual(results['attributes']['title'], None)
@@ -258,12 +253,12 @@
             cgi.MiniFieldStorage('op', 'add'),
             cgi.MiniFieldStorage('nosy', '2')
         ]
-        code, results = self.server.patch_element('issue', issue_id, form)
-        self.assertEqual(code, 200)
+        results = self.server.patch_element('issue', issue_id, form)
+        self.assertEqual(self.dummy_client.response_code, 200)
 
         # verify the result
-        code, results = self.server.get_element('issue', issue_id, {})
-        self.assertEqual(code, 200)
+        results = self.server.get_element('issue', issue_id, {})['data']
+        self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(len(results['attributes']['nosy']), 2)
         self.assertListEqual(results['attributes']['nosy'], ['1', '2'])
 
@@ -281,12 +276,12 @@
             cgi.MiniFieldStorage('nosy', '2'),
             cgi.MiniFieldStorage('status', '3')
         ]
-        code, results = self.server.patch_element('issue', issue_id, form)
-        self.assertEqual(code, 200)
+        results = self.server.patch_element('issue', issue_id, form)
+        self.assertEqual(self.dummy_client.response_code, 200)
 
         # verify the result
-        code, results = self.server.get_element('issue', issue_id, {})
-        self.assertEqual(code, 200)
+        results = self.server.get_element('issue', issue_id, {})['data']
+        self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['attributes']['status'], '3')
         self.assertEqual(len(results['attributes']['nosy']), 1)
         self.assertListEqual(results['attributes']['nosy'], ['2'])
@@ -305,12 +300,12 @@
             cgi.MiniFieldStorage('nosy', ''),
             cgi.MiniFieldStorage('title', '')
         ]
-        code, results = self.server.patch_element('issue', issue_id, form)
-        self.assertEqual(code, 200)
+        results = self.server.patch_element('issue', issue_id, form)
+        self.assertEqual(self.dummy_client.response_code, 200)
 
         # verify the result
-        code, results = self.server.get_element('issue', issue_id, {})
-        self.assertEqual(code, 200)
+        results = self.server.get_element('issue', issue_id, {})['data']
+        self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['attributes']['title'], None)
         self.assertEqual(len(results['attributes']['nosy']), 0)
         self.assertEqual(results['attributes']['nosy'], [])
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436183730 -10800
#      Mon Jul 06 14:55:30 2015 +0300
# Branch REST
# Node ID 5a16780981ad510e410cbc6af6265d5b45f10e37
# Parent  88132e1281fa89343bccf9defc431be755fa6138
Added decorator to handle formatting output data

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -89,30 +89,56 @@
 
         return prop
 
-    @staticmethod
-    def error_obj(status, msg, source=None):
-        """Wrap the error data into an object. This function is temporally and
-        will be changed to a decorator later."""
-        result = {
-            'error': {
-                'status': status,
-                'msg': msg
-            }
-        }
-        if source is not None:
-            result['error']['source'] = source
+    def _data_decorator(func):
+        """Wrap the returned data into an object.."""
+        def format_object(self, *args, **kwargs):
+            try:
+                code, data = func(self, *args, **kwargs)
+            except IndexError, msg:
+                code = 404
+                data = msg
+            except Unauthorised, msg:
+                code = 403
+                data = msg
+            except (hyperdb.DesignatorError, UsageError), msg:
+                code = 400
+                data = msg
+            except (AttributeError, Reject), msg:
+                code = 405
+                data = msg
+            except ValueError, msg:
+                code = 409
+                data = msg
+            except NotImplementedError:
+                code = 402  # nothing to pay, just a mark for debugging purpose
+                data = 'Method under development'
+            except:
+                exc, val, tb = sys.exc_info()
+                code = 400
+                # if self.DEBUG_MODE in roundup_server
+                # else data = 'An error occurred. Please check...',
+                data = val
 
-        return result
+                # out to the logfile
+                print 'EXCEPTION AT', time.ctime()
+                traceback.print_exc()
 
-    @staticmethod
-    def data_obj(data):
-        """Wrap the returned data into an object. This function is temporally
-        and will be changed to a decorator later."""
-        result = {
-            'data': data
-        }
-        return result
+            self.client.response_code = code
+            if code >= 400:  # any error require error format
+                result = {
+                    'error': {
+                        'status': code,
+                        'msg': data
+                    }
+                }
+            else:
+                result = {
+                    'data': data
+                }
+            return result
+        return format_object
 
+    @_data_decorator
     def get_collection(self, class_name, input):
         """GET resource from class URI.
 
@@ -146,6 +172,7 @@
         self.client.setHeader("X-Count-Total", str(len(result)))
         return 200, result
 
+    @_data_decorator
     def get_element(self, class_name, item_id, input):
         """GET resource from object URI.
 
@@ -191,6 +218,7 @@
 
         return 200, result
 
+    @_data_decorator
     def get_attribute(self, class_name, item_id, attr_name, input):
         """GET resource from attribute URI.
 
@@ -231,6 +259,7 @@
 
         return 200, result
 
+    @_data_decorator
     def post_collection(self, class_name, input):
         """POST a new object to a class
 
@@ -290,18 +319,22 @@
         }
         return 201, result
 
+    @_data_decorator
     def post_element(self, class_name, item_id, input):
         """POST to an object of a class is not allowed"""
         raise Reject('POST to an item is not allowed')
 
+    @_data_decorator
     def post_attribute(self, class_name, item_id, attr_name, input):
         """POST to an attribute of an object is not allowed"""
         raise Reject('POST to an attribute is not allowed')
 
+    @_data_decorator
     def put_collection(self, class_name, input):
         """PUT a class is not allowed"""
         raise Reject('PUT a class is not allowed')
 
+    @_data_decorator
     def put_element(self, class_name, item_id, input):
         """PUT a new content to an object
 
@@ -346,6 +379,7 @@
         }
         return 200, result
 
+    @_data_decorator
     def put_attribute(self, class_name, item_id, attr_name, input):
         """PUT an attribute to an object
 
@@ -394,6 +428,7 @@
 
         return 200, result
 
+    @_data_decorator
     def delete_collection(self, class_name, input):
         """DELETE all objects in a class
 
@@ -433,6 +468,7 @@
 
         return 200, result
 
+    @_data_decorator
     def delete_element(self, class_name, item_id, input):
         """DELETE an object in a class
 
@@ -461,6 +497,7 @@
 
         return 200, result
 
+    @_data_decorator
     def delete_attribute(self, class_name, item_id, attr_name, input):
         """DELETE an attribute in a object by setting it to None or empty
 
@@ -503,10 +540,12 @@
 
         return 200, result
 
+    @_data_decorator
     def patch_collection(self, class_name, input):
         """PATCH a class is not allowed"""
         raise Reject('PATCH a class is not allowed')
 
+    @_data_decorator
     def patch_element(self, class_name, item_id, input):
         """PATCH an object
 
@@ -573,6 +612,7 @@
         }
         return 200, result
 
+    @_data_decorator
     def patch_attribute(self, class_name, item_id, attr_name, input):
         """PATCH an attribute of an object
 
@@ -645,6 +685,7 @@
         }
         return 200, result
 
+    @_data_decorator
     def options_collection(self, class_name, input):
         """OPTION return the HTTP Header for the class uri
 
@@ -654,6 +695,7 @@
         """
         return 204, ""
 
+    @_data_decorator
     def options_element(self, class_name, item_id, input):
         """OPTION return the HTTP Header for the object uri
 
@@ -667,6 +709,7 @@
         )
         return 204, ""
 
+    @_data_decorator
     def option_attribute(self, class_name, item_id, attr_name, input):
         """OPTION return the HTTP Header for the attribute uri
 
@@ -736,53 +779,21 @@
 
         # Call the appropriate method
         output = None
-        try:
-            if resource_uri in self.db.classes:
-                response_code, output = getattr(
-                    self, "%s_collection" % method.lower()
-                    )(resource_uri, input)
+        if resource_uri in self.db.classes:
+            output = getattr(
+                self, "%s_collection" % method.lower()
+                )(resource_uri, input)
+        else:
+            class_name, item_id = hyperdb.splitDesignator(resource_uri)
+            if len(uri_split) == 3:
+                output = getattr(
+                    self, "%s_attribute" % method.lower()
+                    )(class_name, item_id, uri_split[2], input)
             else:
-                class_name, item_id = hyperdb.splitDesignator(resource_uri)
-                if len(uri_split) == 3:
-                    response_code, output = getattr(
-                        self, "%s_attribute" % method.lower()
-                        )(class_name, item_id, uri_split[2], input)
-                else:
-                    response_code, output = getattr(
-                        self, "%s_element" % method.lower()
-                        )(class_name, item_id, input)
-            output = RestfulInstance.data_obj(output)
-            self.client.response_code = response_code
-        except IndexError, msg:
-            output = RestfulInstance.error_obj(404, msg)
-            self.client.response_code = 404
-        except Unauthorised, msg:
-            output = RestfulInstance.error_obj(403, msg)
-            self.client.response_code = 403
-        except (hyperdb.DesignatorError, UsageError), msg:
-            output = RestfulInstance.error_obj(400, msg)
-            self.client.response_code = 400
-        except (AttributeError, Reject), msg:
-            output = RestfulInstance.error_obj(405, msg)
-            self.client.response_code = 405
-        except ValueError, msg:
-            output = RestfulInstance.error_obj(409, msg)
-            self.client.response_code = 409
-        except NotImplementedError:
-            output = RestfulInstance.error_obj(402, 'Method under development')
-            self.client.response_code = 402
-            # nothing to pay, just a mark for debugging purpose
-        except:
-            # if self.DEBUG_MODE in roundup_server
-            # else msg = 'An error occurred. Please check...',
-            exc, val, tb = sys.exc_info()
-            output = RestfulInstance.error_obj(400, val)
-            self.client.response_code = 400
+                output = getattr(
+                    self, "%s_element" % method.lower()
+                    )(class_name, item_id, input)
 
-            # out to the logfile, it would be nice if the server do it for me
-            print 'EXCEPTION AT', time.ctime()
-            traceback.print_exc()
-        finally:
             if format_output.lower() == "json":
                 self.client.setHeader("Content-Type", "application/json")
                 if pretty_output:
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436049663 -10800
#      Sun Jul 05 01:41:03 2015 +0300
# Branch REST
# Node ID 88132e1281fa89343bccf9defc431be755fa6138
# Parent  6358e8d1d807f8afccffe838a038a1e843ae9a0a
Fixed code convention

diff --git a/test/test_rest.py b/test/test_rest.py
--- a/test/test_rest.py
+++ b/test/test_rest.py
@@ -1,17 +1,19 @@
-import unittest, os, shutil, errno, sys, difflib, cgi, re
+import unittest
+import os
+import shutil
+import errno
 
-from xmlrpclib import MultiCall
 from roundup.cgi.exceptions import *
-from roundup import init, instance, password, hyperdb, date
+from roundup import password, hyperdb
 from roundup.rest import RestfulInstance
 from roundup.backends import list_backends
-from roundup.hyperdb import String
-from roundup.cgi import TranslationService, client
+from roundup.cgi import client
 
 import db_test_base
 
 NEEDS_INSTANCE = 1
 
+
 class TestCase(unittest.TestCase):
 
     backend = None
@@ -313,6 +315,7 @@
         self.assertEqual(len(results['attributes']['nosy']), 0)
         self.assertEqual(results['attributes']['nosy'], [])
 
+
 def test_suite():
     suite = unittest.TestSuite()
     for l in list_backends():
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436027845 -10800
#      Sat Jul 04 19:37:25 2015 +0300
# Branch REST
# Node ID 6358e8d1d807f8afccffe838a038a1e843ae9a0a
# Parent  8becb1b1de45be6d4fe7d0dac6a70ca21334f977
Added test cases for the element URI methods

diff --git a/test/test_rest.py b/test/test_rest.py
--- a/test/test_rest.py
+++ b/test/test_rest.py
@@ -82,11 +82,32 @@
         self.assertEqual(results['attributes']['username'], 'joe')
         self.assertEqual(results['attributes']['realname'], 'Joe Random')
 
+        # Obtain data for 'joe'.
+        code, results = self.server.get_attribute(
+            'user', self.joeid, 'username', {}
+        )
+        self.assertEqual(code, 200)
+        self.assertEqual(results['data'], 'joe')
+
     def testPut(self):
         """
         Change joe's 'realname'
         Check if we can't change admin's detail
         """
+        # change Joe's realname via attribute uri
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('data', 'Joe Doe Doe')
+        ]
+        code, results = self.server.put_attribute(
+            'user', self.joeid, 'realname', form
+        )
+        code, results = self.server.get_attribute(
+            'user', self.joeid, 'realname', {}
+        )
+        self.assertEqual(code, 200)
+        self.assertEqual(results['data'], 'Joe Doe Doe')
+
         # Reset joe's 'realname'.
         form = cgi.FieldStorage()
         form.list = [
@@ -197,6 +218,31 @@
         finally:
             self.db.setCurrentUser('joe')
 
+    def testDeleteAttributeUri(self):
+        """
+        Test Delete an attribute
+        """
+        # create a new issue with userid 1 in the nosy list
+        issue_id = self.db.issue.create(title='foo', nosy=['1'])
+
+        # remove the title and nosy
+        code, results = self.server.delete_attribute(
+            'issue', issue_id, 'title', {}
+        )
+        self.assertEqual(code, 200)
+
+        code, results = self.server.delete_attribute(
+            'issue', issue_id, 'nosy', {}
+        )
+        self.assertEqual(code, 200)
+
+        # verify the result
+        code, results = self.server.get_element('issue', issue_id, {})
+        self.assertEqual(code, 200)
+        self.assertEqual(len(results['attributes']['nosy']), 0)
+        self.assertListEqual(results['attributes']['nosy'], [])
+        self.assertEqual(results['attributes']['title'], None)
+
     def testPatchAdd(self):
         """
         Test Patch op 'Add'
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1436019690 -10800
#      Sat Jul 04 17:21:30 2015 +0300
# Branch REST
# Node ID 8becb1b1de45be6d4fe7d0dac6a70ca21334f977
# Parent  5a68dbfb8477cfad4c72d4a506affeecc6d1e19f
Added attribute URI handling

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -51,27 +51,44 @@
             value = arg.value
             if key not in class_props:
                 continue
-            if isinstance(key, unicode):
-                try:
-                    key = key.encode('ascii')
-                except UnicodeEncodeError:
-                    raise UsageError(
-                        'argument %r is no valid ascii keyword' % key
-                    )
-            if isinstance(value, unicode):
-                value = value.encode('utf-8')
-            if value:
-                try:
-                    props[key] = hyperdb.rawToHyperdb(
-                        self.db, cl, itemid, key, value
-                    )
-                except hyperdb.HyperdbValueError, msg:
-                    raise UsageError(msg)
-            else:
-                props[key] = None
+            props[key] = self.prop_from_arg(cl, key, value, itemid)
 
         return props
 
+    def prop_from_arg(self, cl, key, value, itemid=None):
+        """Construct a property from the given argument,
+        and return them after validation.
+
+        Args:
+            cl (string): class object of the resource
+            key (string): attribute key
+            value (string): attribute value
+            itemid (string, optional): itemid of the object
+
+        Returns:
+            value: value of validated properties
+
+        """
+        prop = None
+        if isinstance(key, unicode):
+            try:
+                key = key.encode('ascii')
+            except UnicodeEncodeError:
+                raise UsageError(
+                    'argument %r is no valid ascii keyword' % key
+                )
+        if isinstance(value, unicode):
+            value = value.encode('utf-8')
+        if value:
+            try:
+                prop = hyperdb.rawToHyperdb(
+                    self.db, cl, itemid, key, value
+                )
+            except hyperdb.HyperdbValueError, msg:
+                raise UsageError(msg)
+
+        return prop
+
     @staticmethod
     def error_obj(status, msg, source=None):
         """Wrap the error data into an object. This function is temporally and
@@ -152,7 +169,7 @@
             'View', self.db.getuid(), class_name, itemid=item_id
         ):
             raise Unauthorised(
-                'Permission to view %s item %s denied' % (class_name, item_id)
+                'Permission to view %s%s denied' % (class_name, item_id)
             )
 
         class_obj = self.db.getclass(class_name)
@@ -174,6 +191,46 @@
 
         return 200, result
 
+    def get_attribute(self, class_name, item_id, attr_name, input):
+        """GET resource from attribute URI.
+
+        This function returns only attribute has View permission
+        class_name should be valid already
+
+        Args:
+            class_name (string): class name of the resource (Ex: issue, msg)
+            item_id (string): id of the resource (Ex: 12, 15)
+            attr_name (string): attribute of the resource (Ex: title, nosy)
+            input (list): the submitted form of the user
+
+        Returns:
+            int: http status code 200 (OK)
+            list: a dictionary represents the attribute
+                id: id of the object
+                type: class name of the attribute
+                link: link to the attribute
+                data: data of the requested attribute
+        """
+        if not self.db.security.hasPermission(
+            'View', self.db.getuid(), class_name, attr_name, item_id
+        ):
+            raise Unauthorised(
+                'Permission to view %s%s %s denied' %
+                (class_name, item_id, attr_name)
+            )
+
+        class_obj = self.db.getclass(class_name)
+        data = class_obj.get(item_id, attr_name)
+        result = {
+            'id': item_id,
+            'type': type(data),
+            'link': "%s%s%s/%s" %
+                    (self.base_path, class_name, item_id, attr_name),
+            'data': data
+        }
+
+        return 200, result
+
     def post_collection(self, class_name, input):
         """POST a new object to a class
 
@@ -237,6 +294,10 @@
         """POST to an object of a class is not allowed"""
         raise Reject('POST to an item is not allowed')
 
+    def post_attribute(self, class_name, item_id, attr_name, input):
+        """POST to an attribute of an object is not allowed"""
+        raise Reject('POST to an attribute is not allowed')
+
     def put_collection(self, class_name, input):
         """PUT a class is not allowed"""
         raise Reject('PUT a class is not allowed')
@@ -285,6 +346,54 @@
         }
         return 200, result
 
+    def put_attribute(self, class_name, item_id, attr_name, input):
+        """PUT an attribute to an object
+
+        Args:
+            class_name (string): class name of the resource (Ex: issue, msg)
+            item_id (string): id of the resource (Ex: 12, 15)
+            attr_name (string): attribute of the resource (Ex: title, nosy)
+            input (list): the submitted form of the user
+
+        Returns:
+            int: http status code 200 (OK)
+            dict:a dictionary represents the modified object
+                id: id of the object
+                type: class name of the object
+                link: link to the object
+                attributes: a dictionary represent only changed attributes of
+                            the object
+        """
+        if not self.db.security.hasPermission(
+            'Edit', self.db.getuid(), class_name, attr_name, item_id
+        ):
+            raise Unauthorised(
+                'Permission to edit %s%s %s denied' %
+                (class_name, item_id, attr_name)
+            )
+
+        class_obj = self.db.getclass(class_name)
+        props = {
+            attr_name: self.prop_from_arg(
+                class_obj, attr_name, input['data'].value, item_id
+            )
+        }
+
+        try:
+            result = class_obj.set(item_id, **props)
+            self.db.commit()
+        except (TypeError, IndexError, ValueError), message:
+            raise ValueError(message)
+
+        result = {
+            'id': item_id,
+            'type': class_name,
+            'link': self.base_path + class_name + item_id,
+            'attribute': result
+        }
+
+        return 200, result
+
     def delete_collection(self, class_name, input):
         """DELETE all objects in a class
 
@@ -352,11 +461,74 @@
 
         return 200, result
 
+    def delete_attribute(self, class_name, item_id, attr_name, input):
+        """DELETE an attribute in a object by setting it to None or empty
+
+        Args:
+            class_name (string): class name of the resource (Ex: issue, msg)
+            item_id (string): id of the resource (Ex: 12, 15)
+            attr_name (string): attribute of the resource (Ex: title, nosy)
+            input (list): the submitted form of the user
+
+        Returns:
+            int: http status code 200 (OK)
+            dict:
+                status (string): 'ok'
+        """
+        if not self.db.security.hasPermission(
+            'Edit', self.db.getuid(), class_name, attr_name, item_id
+        ):
+            raise Unauthorised(
+                'Permission to delete %s%s %s denied' %
+                (class_name, item_id, attr_name)
+            )
+
+        class_obj = self.db.getclass(class_name)
+        props = {}
+        prop_obj = class_obj.get(item_id, attr_name)
+        if isinstance(prop_obj, list):
+            props[attr_name] = []
+        else:
+            props[attr_name] = None
+
+        try:
+            class_obj.set(item_id, **props)
+            self.db.commit()
+        except (TypeError, IndexError, ValueError), message:
+            raise ValueError(message)
+
+        result = {
+            'status': 'ok'
+        }
+
+        return 200, result
+
     def patch_collection(self, class_name, input):
         """PATCH a class is not allowed"""
         raise Reject('PATCH a class is not allowed')
 
     def patch_element(self, class_name, item_id, input):
+        """PATCH an object
+
+        Patch an element using 3 operators
+        ADD : Append new value to the object's attribute
+        REPLACE: Replace object's attribute
+        REMOVE: Clear object's attribute
+
+        Args:
+            class_name (string): class name of the resource (Ex: issue, msg)
+            item_id (string): id of the resource (Ex: 12, 15)
+            input (list): the submitted form of the user
+
+        Returns:
+            int: http status code 200 (OK)
+            dict: a dictionary represents the modified object
+                id: id of the object
+                type: class name of the object
+                link: link to the object
+                attributes: a dictionary represent only changed attributes of
+                            the object
+        """
         try:
             op = input['op'].value.lower()
         except KeyError:
@@ -401,6 +573,78 @@
         }
         return 200, result
 
+    def patch_attribute(self, class_name, item_id, attr_name, input):
+        """PATCH an attribute of an object
+
+        Patch an element using 3 operators
+        ADD : Append new value to the attribute
+        REPLACE: Replace attribute
+        REMOVE: Clear attribute
+
+        Args:
+            class_name (string): class name of the resource (Ex: issue, msg)
+            item_id (string): id of the resource (Ex: 12, 15)
+            attr_name (string): attribute of the resource (Ex: title, nosy)
+            input (list): the submitted form of the user
+
+        Returns:
+            int: http status code 200 (OK)
+            dict: a dictionary represents the modified object
+                id: id of the object
+                type: class name of the object
+                link: link to the object
+                attributes: a dictionary represent only changed attributes of
+                            the object
+        """
+        try:
+            op = input['op'].value.lower()
+        except KeyError:
+            op = "replace"
+        class_obj = self.db.getclass(class_name)
+
+        if not self.db.security.hasPermission(
+            'Edit', self.db.getuid(), class_name, attr_name, item_id
+        ):
+            raise Unauthorised(
+                'Permission to edit %s%s %s denied' %
+                (class_name, item_id, attr_name)
+            )
+
+        prop = attr_name
+        class_obj = self.db.getclass(class_name)
+        props = {
+            prop: self.prop_from_arg(
+                class_obj, prop, input['data'].value, item_id
+            )
+        }
+
+        if op == 'add':
+            props[prop] = class_obj.get(item_id, prop) + props[prop]
+        elif op == 'replace':
+            pass
+        elif op == 'remove':
+            current_prop = class_obj.get(item_id, prop)
+            if isinstance(current_prop, list):
+                props[prop] = []
+            else:
+                props[prop] = None
+        else:
+            raise UsageError('PATCH Operation %s is not allowed' % op)
+
+        try:
+            result = class_obj.set(item_id, **props)
+            self.db.commit()
+        except (TypeError, IndexError, ValueError), message:
+            raise ValueError(message)
+
+        result = {
+            'id': item_id,
+            'type': class_name,
+            'link': self.base_path + class_name + item_id,
+            'attribute': result
+        }
+        return 200, result
+
     def options_collection(self, class_name, input):
         """OPTION return the HTTP Header for the class uri
 
@@ -419,8 +663,20 @@
         """
         self.client.setHeader(
             "Accept-Patch",
-            "application/x-www-form-urlencoded, "
-            "multipart/form-data"
+            "application/x-www-form-urlencoded, multipart/form-data"
+        )
+        return 204, ""
+
+    def option_attribute(self, class_name, item_id, attr_name, input):
+        """OPTION return the HTTP Header for the attribute uri
+
+        Returns:
+            int: http status code 204 (No content)
+            body (string): an empty string
+        """
+        self.client.setHeader(
+            "Accept-Patch",
+            "application/x-www-form-urlencoded, multipart/form-data"
         )
         return 204, ""
 
@@ -430,7 +686,8 @@
         # 0 - rest
         # 1 - resource
         # 2 - attribute
-        resource_uri = uri.split("/")[1]
+        uri_split = uri.split("/")
+        resource_uri = uri_split[1]
 
         # if X-HTTP-Method-Override is set, follow the override method
         headers = self.client.request.headers
@@ -486,9 +743,14 @@
                     )(resource_uri, input)
             else:
                 class_name, item_id = hyperdb.splitDesignator(resource_uri)
-                response_code, output = getattr(
-                    self, "%s_element" % method.lower()
-                    )(class_name, item_id, input)
+                if len(uri_split) == 3:
+                    response_code, output = getattr(
+                        self, "%s_attribute" % method.lower()
+                        )(class_name, item_id, uri_split[2], input)
+                else:
+                    response_code, output = getattr(
+                        self, "%s_element" % method.lower()
+                        )(class_name, item_id, input)
             output = RestfulInstance.data_obj(output)
             self.client.response_code = response_code
         except IndexError, msg:
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1435921733 -10800
#      Fri Jul 03 14:08:53 2015 +0300
# Branch REST
# Node ID 5a68dbfb8477cfad4c72d4a506affeecc6d1e19f
# Parent  c557036a1e23718a250dcf2c08e7752e5a2127ac
Added rest unit test,
Fixed a bug with printing error message,
Patch operation remove now replace empty list instead of None

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -152,7 +152,7 @@
             'View', self.db.getuid(), class_name, itemid=item_id
         ):
             raise Unauthorised(
-                'Permission to view %s item %d denied' % (class_name, item_id)
+                'Permission to view %s item %s denied' % (class_name, item_id)
             )
 
         class_obj = self.db.getclass(class_name)
@@ -379,7 +379,11 @@
             elif op == 'replace':
                 pass
             elif op == 'remove':
-                props[prop] = None
+                current_prop = class_obj.get(item_id, prop)
+                if isinstance(current_prop, list):
+                    props[prop] = []
+                else:
+                    props[prop] = None
             else:
                 raise UsageError('PATCH Operation %s is not allowed' % op)
 
diff --git a/test/test_rest.py b/test/test_rest.py
new file mode 100644
--- /dev/null
+++ b/test/test_rest.py
@@ -0,0 +1,280 @@
+import unittest, os, shutil, errno, sys, difflib, cgi, re
+
+from xmlrpclib import MultiCall
+from roundup.cgi.exceptions import *
+from roundup import init, instance, password, hyperdb, date
+from roundup.rest import RestfulInstance
+from roundup.backends import list_backends
+from roundup.hyperdb import String
+from roundup.cgi import TranslationService, client
+
+import db_test_base
+
+NEEDS_INSTANCE = 1
+
+class TestCase(unittest.TestCase):
+
+    backend = None
+
+    def setUp(self):
+        self.dirname = '_test_rest'
+        # set up and open a tracker
+        self.instance = db_test_base.setupTracker(self.dirname, self.backend)
+
+        # open the database
+        self.db = self.instance.open('admin')
+
+        # Get user id (user4 maybe). Used later to get data from db.
+        self.joeid = self.db.user.create(
+            username='joe',
+            password=password.Password('random'),
+            address='random@home.org',
+            realname='Joe Random',
+            roles='User'
+        )
+
+        self.db.commit()
+        self.db.close()
+        self.db = self.instance.open('joe')
+
+        self.db.tx_Source = 'web'
+
+        self.db.issue.addprop(tx_Source=hyperdb.String())
+        self.db.msg.addprop(tx_Source=hyperdb.String())
+
+        self.db.post_init()
+
+        thisdir = os.path.dirname(__file__)
+        vars = {}
+        execfile(os.path.join(thisdir, "tx_Source_detector.py"), vars)
+        vars['init'](self.db)
+
+        env = {
+            'PATH_INFO': 'http://localhost/rounduptest/rest/',
+            'HTTP_HOST': 'localhost',
+            'TRACKER_NAME': 'rounduptest'
+        }
+        dummy_client = client.Client(self.instance, None, env, [], None)
+
+        self.server = RestfulInstance(dummy_client, self.db)
+
+    def tearDown(self):
+        self.db.close()
+        try:
+            shutil.rmtree(self.dirname)
+        except OSError, error:
+            if error.errno not in (errno.ENOENT, errno.ESRCH):
+                raise
+
+    def testGet(self):
+        """
+        Retrieve all three users
+        obtain data for 'joe'
+        """
+        # Retrieve all three users.
+        code, results = self.server.get_collection('user', {})
+        self.assertEqual(code, 200)
+        self.assertEqual(len(results), 3)
+
+        # Obtain data for 'joe'.
+        code, results = self.server.get_element('user', self.joeid, {})
+        self.assertEqual(code, 200)
+        self.assertEqual(results['attributes']['username'], 'joe')
+        self.assertEqual(results['attributes']['realname'], 'Joe Random')
+
+    def testPut(self):
+        """
+        Change joe's 'realname'
+        Check if we can't change admin's detail
+        """
+        # Reset joe's 'realname'.
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('realname', 'Joe Doe')
+        ]
+        code, results = self.server.put_element('user', self.joeid, form)
+        code, results = self.server.get_element('user', self.joeid, {})
+        self.assertEqual(code, 200)
+        self.assertEqual(results['attributes']['realname'], 'Joe Doe')
+
+        # check we can't change admin's details
+        self.assertRaises(
+            Unauthorised,
+            self.server.put_element, 'user', '1', form
+        )
+
+    def testPost(self):
+        """
+        Post a new issue with title: foo
+        Verify the information of the created issue
+        """
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('title', 'foo')
+        ]
+        code, results = self.server.post_collection('issue', form)
+        self.assertEqual(code, 201)
+        issueid = results['id']
+        code, results = self.server.get_element('issue', issueid, {})
+        self.assertEqual(code, 200)
+        self.assertEqual(results['attributes']['title'], 'foo')
+        self.assertEqual(self.db.issue.get(issueid, "tx_Source"), 'web')
+
+    def testPostFile(self):
+        """
+        Post a new file with content: hello\r\nthere
+        Verify the information of the created file
+        """
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('content', 'hello\r\nthere')
+        ]
+        code, results = self.server.post_collection('file', form)
+        self.assertEqual(code, 201)
+        fileid = results['id']
+        code, results = self.server.get_element('file', fileid, {})
+        self.assertEqual(code, 200)
+        self.assertEqual(results['attributes']['content'], 'hello\r\nthere')
+
+    def testAuthDeniedPut(self):
+        """
+        Test unauthorized PUT request
+        """
+        # Wrong permissions (caught by roundup security module).
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('realname', 'someone')
+        ]
+        self.assertRaises(
+            Unauthorised,
+            self.server.put_element, 'user', '1', form
+        )
+
+    def testAuthDeniedPost(self):
+        """
+        Test unauthorized POST request
+        """
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('username', 'blah')
+        ]
+        self.assertRaises(
+            Unauthorised,
+            self.server.post_collection, 'user', form
+        )
+
+    def testAuthAllowedPut(self):
+        """
+        Test authorized PUT request
+        """
+        self.db.setCurrentUser('admin')
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('realname', 'someone')
+        ]
+        try:
+            try:
+                self.server.put_element('user', '2', form)
+            except Unauthorised, err:
+                self.fail('raised %s' % err)
+        finally:
+            self.db.setCurrentUser('joe')
+
+    def testAuthAllowedPost(self):
+        """
+        Test authorized POST request
+        """
+        self.db.setCurrentUser('admin')
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('username', 'blah')
+        ]
+        try:
+            try:
+                self.server.post_collection('user', form)
+            except Unauthorised, err:
+                self.fail('raised %s' % err)
+        finally:
+            self.db.setCurrentUser('joe')
+
+    def testPatchAdd(self):
+        """
+        Test Patch op 'Add'
+        """
+        # create a new issue with userid 1 in the nosy list
+        issue_id = self.db.issue.create(title='foo', nosy=['1'])
+
+        # add userid 2 to the nosy list
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('op', 'add'),
+            cgi.MiniFieldStorage('nosy', '2')
+        ]
+        code, results = self.server.patch_element('issue', issue_id, form)
+        self.assertEqual(code, 200)
+
+        # verify the result
+        code, results = self.server.get_element('issue', issue_id, {})
+        self.assertEqual(code, 200)
+        self.assertEqual(len(results['attributes']['nosy']), 2)
+        self.assertListEqual(results['attributes']['nosy'], ['1', '2'])
+
+    def testPatchReplace(self):
+        """
+        Test Patch op 'Replace'
+        """
+        # create a new issue with userid 1 in the nosy list and status = 1
+        issue_id = self.db.issue.create(title='foo', nosy=['1'], status='1')
+
+        # replace userid 2 to the nosy list and status = 3
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('op', 'replace'),
+            cgi.MiniFieldStorage('nosy', '2'),
+            cgi.MiniFieldStorage('status', '3')
+        ]
+        code, results = self.server.patch_element('issue', issue_id, form)
+        self.assertEqual(code, 200)
+
+        # verify the result
+        code, results = self.server.get_element('issue', issue_id, {})
+        self.assertEqual(code, 200)
+        self.assertEqual(results['attributes']['status'], '3')
+        self.assertEqual(len(results['attributes']['nosy']), 1)
+        self.assertListEqual(results['attributes']['nosy'], ['2'])
+
+    def testPatchRemoveAll(self):
+        """
+        Test Patch Action 'Remove'
+        """
+        # create a new issue with userid 1 in the nosy list
+        issue_id = self.db.issue.create(title='foo', nosy=['1', '2'])
+
+        # remove the nosy list and the title
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('op', 'remove'),
+            cgi.MiniFieldStorage('nosy', ''),
+            cgi.MiniFieldStorage('title', '')
+        ]
+        code, results = self.server.patch_element('issue', issue_id, form)
+        self.assertEqual(code, 200)
+
+        # verify the result
+        code, results = self.server.get_element('issue', issue_id, {})
+        self.assertEqual(code, 200)
+        self.assertEqual(results['attributes']['title'], None)
+        self.assertEqual(len(results['attributes']['nosy']), 0)
+        self.assertEqual(results['attributes']['nosy'], [])
+
+def test_suite():
+    suite = unittest.TestSuite()
+    for l in list_backends():
+        dct = dict(backend=l)
+        subcls = type(TestCase)('TestCase_%s' % l, (TestCase,), dct)
+        suite.addTest(unittest.makeSuite(subcls))
+    return suite
+
+if __name__ == '__main__':
+    runner = unittest.TextTestRunner()
+    unittest.main(testRunner=runner)
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1435450740 -10800
#      Sun Jun 28 03:19:00 2015 +0300
# Branch REST
# Node ID c557036a1e23718a250dcf2c08e7752e5a2127ac
# Parent  058700d4f473a16b3b2f406171e96ee7b9d07ce5
Added docstring

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -18,8 +18,7 @@
 
 
 class RestfulInstance(object):
-    """Dummy Handler for REST
-    """
+    """The RestfulInstance performs REST request from the client"""
 
     def __init__(self, client, db):
         self.client = client  # it might be unnecessary to receive the client
@@ -31,6 +30,18 @@
         self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker)
 
     def props_from_args(self, cl, args, itemid=None):
+        """Construct a list of properties from the given arguments,
+        and return them after validation.
+
+        Args:
+            cl (string): class object of the resource
+            args (list): the submitted form of the user
+            itemid (string, optional): itemid of the object
+
+        Returns:
+            dict: dictionary of validated properties
+
+        """
         class_props = cl.properties.keys()
         props = {}
         # props = dict.fromkeys(class_props, None)
@@ -63,6 +74,8 @@
 
     @staticmethod
     def error_obj(status, msg, source=None):
+        """Wrap the error data into an object. This function is temporally and
+        will be changed to a decorator later."""
         result = {
             'error': {
                 'status': status,
@@ -76,12 +89,29 @@
 
     @staticmethod
     def data_obj(data):
+        """Wrap the returned data into an object. This function is temporally
+        and will be changed to a decorator later."""
         result = {
             'data': data
         }
         return result
 
     def get_collection(self, class_name, input):
+        """GET resource from class URI.
+
+        This function returns only items have View permission
+        class_name should be valid already
+
+        Args:
+            class_name (string): class name of the resource (Ex: issue, msg)
+            input (list): the submitted form of the user
+
+        Returns:
+            int: http status code 200 (OK)
+            list: list of reference item in the class
+                id: id of the object
+                link: path to the object
+        """
         if not self.db.security.hasPermission(
             'View', self.db.getuid(), class_name
         ):
@@ -100,6 +130,24 @@
         return 200, result
 
     def get_element(self, class_name, item_id, input):
+        """GET resource from object URI.
+
+        This function returns only properties have View permission
+        class_name and item_id should be valid already
+
+        Args:
+            class_name (string): class name of the resource (Ex: issue, msg)
+            item_id (string): id of the resource (Ex: 12, 15)
+            input (list): the submitted form of the user
+
+        Returns:
+            int: http status code 200 (OK)
+            dict: a dictionary represents the object
+                id: id of the object
+                type: class name of the object
+                link: link to the object
+                attributes: a dictionary represent the attributes of the object
+        """
         if not self.db.security.hasPermission(
             'View', self.db.getuid(), class_name, itemid=item_id
         ):
@@ -127,6 +175,21 @@
         return 200, result
 
     def post_collection(self, class_name, input):
+        """POST a new object to a class
+
+        If the item is successfully created, the "Location" header will also
+        contain the link to the created object
+
+        Args:
+            class_name (string): class name of the resource (Ex: issue, msg)
+            input (list): the submitted form of the user
+
+        Returns:
+            int: http status code 201 (Created)
+            dict: a reference item to the created object
+                id: id of the object
+                link: path to the object
+        """
         if not self.db.security.hasPermission(
             'Create', self.db.getuid(), class_name
         ):
@@ -171,12 +234,32 @@
         return 201, result
 
     def post_element(self, class_name, item_id, input):
+        """POST to an object of a class is not allowed"""
         raise Reject('POST to an item is not allowed')
 
     def put_collection(self, class_name, input):
+        """PUT a class is not allowed"""
         raise Reject('PUT a class is not allowed')
 
     def put_element(self, class_name, item_id, input):
+        """PUT a new content to an object
+
+        Replace the content of the existing object
+
+        Args:
+            class_name (string): class name of the resource (Ex: issue, msg)
+            item_id (string): id of the resource (Ex: 12, 15)
+            input (list): the submitted form of the user
+
+        Returns:
+            int: http status code 200 (OK)
+            dict: a dictionary represents the modified object
+                id: id of the object
+                type: class name of the object
+                link: link to the object
+                attributes: a dictionary represent only changed attributes of
+                            the object
+        """
         class_obj = self.db.getclass(class_name)
 
         props = self.props_from_args(class_obj, input.value, item_id)
@@ -203,6 +286,18 @@
         return 200, result
 
     def delete_collection(self, class_name, input):
+        """DELETE all objects in a class
+
+        Args:
+            class_name (string): class name of the resource (Ex: issue, msg)
+            input (list): the submitted form of the user
+
+        Returns:
+            int: http status code 200 (OK)
+            dict:
+                status (string): 'ok'
+                count (int): number of deleted objects
+        """
         if not self.db.security.hasPermission(
             'Delete', self.db.getuid(), class_name
         ):
@@ -230,6 +325,18 @@
         return 200, result
 
     def delete_element(self, class_name, item_id, input):
+        """DELETE an object in a class
+
+        Args:
+            class_name (string): class name of the resource (Ex: issue, msg)
+            item_id (string): id of the resource (Ex: 12, 15)
+            input (list): the submitted form of the user
+
+        Returns:
+            int: http status code 200 (OK)
+            dict:
+                status (string): 'ok'
+        """
         if not self.db.security.hasPermission(
             'Delete', self.db.getuid(), class_name, itemid=item_id
         ):
@@ -246,6 +353,7 @@
         return 200, result
 
     def patch_collection(self, class_name, input):
+        """PATCH a class is not allowed"""
         raise Reject('PATCH a class is not allowed')
 
     def patch_element(self, class_name, item_id, input):
@@ -290,9 +398,21 @@
         return 200, result
 
     def options_collection(self, class_name, input):
+        """OPTION return the HTTP Header for the class uri
+
+        Returns:
+            int: http status code 204 (No content)
+            body (string): an empty string
+        """
         return 204, ""
 
     def options_element(self, class_name, item_id, input):
+        """OPTION return the HTTP Header for the object uri
+
+        Returns:
+            int: http status code 204 (No content)
+            body (string): an empty string
+        """
         self.client.setHeader(
             "Accept-Patch",
             "application/x-www-form-urlencoded, "
@@ -301,6 +421,7 @@
         return 204, ""
 
     def dispatch(self, method, uri, input):
+        """format and process the request"""
         # PATH is split to multiple pieces
         # 0 - rest
         # 1 - resource
@@ -327,40 +448,43 @@
         except KeyError:
             pretty_output = False
 
+        # add access-control-allow-* to support CORS
         self.client.setHeader("Access-Control-Allow-Origin", "*")
         self.client.setHeader(
             "Access-Control-Allow-Headers",
             "Content-Type, Authorization, X-HTTP-Method-Override"
         )
+        if resource_uri in self.db.classes:
+            self.client.setHeader(
+                "Allow",
+                "HEAD, OPTIONS, GET, POST, DELETE"
+            )
+            self.client.setHeader(
+                "Access-Control-Allow-Methods",
+                "HEAD, OPTIONS, GET, POST, DELETE"
+            )
+        else:
+            self.client.setHeader(
+                "Allow",
+                "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
+            )
+            self.client.setHeader(
+                "Access-Control-Allow-Methods",
+                "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
+            )
 
+        # Call the appropriate method
         output = None
         try:
             if resource_uri in self.db.classes:
-                self.client.setHeader(
-                    "Allow",
-                    "HEAD, OPTIONS, GET, POST, DELETE"
-                )
-                self.client.setHeader(
-                    "Access-Control-Allow-Methods",
-                    "HEAD, OPTIONS, GET, POST, DELETE"
-                )
                 response_code, output = getattr(
                     self, "%s_collection" % method.lower()
                     )(resource_uri, input)
             else:
                 class_name, item_id = hyperdb.splitDesignator(resource_uri)
-                self.client.setHeader(
-                    "Allow",
-                    "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
-                )
-                self.client.setHeader(
-                    "Access-Control-Allow-Methods",
-                    "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
-                )
                 response_code, output = getattr(
                     self, "%s_element" % method.lower()
                     )(class_name, item_id, input)
-
             output = RestfulInstance.data_obj(output)
             self.client.response_code = response_code
         except IndexError, msg:
@@ -408,6 +532,8 @@
 
 
 class RoundupJSONEncoder(json.JSONEncoder):
+    """RoundupJSONEncoder overrides the default JSONEncoder to handle all
+    types of the object without returning any error"""
     def default(self, obj):
         try:
             result = json.JSONEncoder.default(self, obj)
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1435446967 -10800
#      Sun Jun 28 02:16:07 2015 +0300
# Branch REST
# Node ID 058700d4f473a16b3b2f406171e96ee7b9d07ce5
# Parent  864fcf43ccea0e3b47cd6bcf698be17a890b0263
Code convention improved

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -17,54 +17,6 @@
 from roundup import xmlrpc
 
 
-def props_from_args(db, cl, args, itemid=None):
-    class_props = cl.properties.keys()
-    props = {}
-    # props = dict.fromkeys(class_props, None)
-
-    for arg in args:
-        key = arg.name
-        value = arg.value
-        if key not in class_props:
-            continue
-        if isinstance(key, unicode):
-            try:
-                key = key.encode('ascii')
-            except UnicodeEncodeError:
-                raise UsageError('argument %r is no valid ascii keyword' % key)
-        if isinstance(value, unicode):
-            value = value.encode('utf-8')
-        if value:
-            try:
-                props[key] = hyperdb.rawToHyperdb(db, cl, itemid, key, value)
-            except hyperdb.HyperdbValueError, msg:
-                raise UsageError(msg)
-        else:
-            props[key] = None
-
-    return props
-
-
-def error_obj(status, msg, source=None):
-    result = {
-        'error': {
-            'status': status,
-            'msg': msg
-        }
-    }
-    if source is not None:
-        result['error']['source'] = source
-
-    return result
-
-
-def data_obj(data):
-    result = {
-        'data': data
-    }
-    return result
-
-
 class RestfulInstance(object):
     """Dummy Handler for REST
     """
@@ -78,33 +30,93 @@
         tracker = self.client.env['TRACKER_NAME']
         self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker)
 
+    def props_from_args(self, cl, args, itemid=None):
+        class_props = cl.properties.keys()
+        props = {}
+        # props = dict.fromkeys(class_props, None)
+
+        for arg in args:
+            key = arg.name
+            value = arg.value
+            if key not in class_props:
+                continue
+            if isinstance(key, unicode):
+                try:
+                    key = key.encode('ascii')
+                except UnicodeEncodeError:
+                    raise UsageError(
+                        'argument %r is no valid ascii keyword' % key
+                    )
+            if isinstance(value, unicode):
+                value = value.encode('utf-8')
+            if value:
+                try:
+                    props[key] = hyperdb.rawToHyperdb(
+                        self.db, cl, itemid, key, value
+                    )
+                except hyperdb.HyperdbValueError, msg:
+                    raise UsageError(msg)
+            else:
+                props[key] = None
+
+        return props
+
+    @staticmethod
+    def error_obj(status, msg, source=None):
+        result = {
+            'error': {
+                'status': status,
+                'msg': msg
+            }
+        }
+        if source is not None:
+            result['error']['source'] = source
+
+        return result
+
+    @staticmethod
+    def data_obj(data):
+        result = {
+            'data': data
+        }
+        return result
+
     def get_collection(self, class_name, input):
-        if not self.db.security.hasPermission('View', self.db.getuid(),
-                                              class_name):
+        if not self.db.security.hasPermission(
+            'View', self.db.getuid(), class_name
+        ):
             raise Unauthorised('Permission to view %s denied' % class_name)
+
         class_obj = self.db.getclass(class_name)
         class_path = self.base_path + class_name
-        result = [{'id': item_id, 'link': class_path + item_id}
-                  for item_id in class_obj.list()
-                  if self.db.security.hasPermission('View', self.db.getuid(),
-                                                    class_name,
-                                                    itemid=item_id)]
+        result = [
+            {'id': item_id, 'link': class_path + item_id}
+            for item_id in class_obj.list()
+            if self.db.security.hasPermission(
+                'View', self.db.getuid(), class_name, itemid=item_id
+            )
+        ]
         self.client.setHeader("X-Count-Total", str(len(result)))
         return 200, result
 
     def get_element(self, class_name, item_id, input):
-        if not self.db.security.hasPermission('View', self.db.getuid(),
-                                              class_name, itemid=item_id):
-            raise Unauthorised('Permission to view %s item %d denied' %
-                               (class_name, item_id))
+        if not self.db.security.hasPermission(
+            'View', self.db.getuid(), class_name, itemid=item_id
+        ):
+            raise Unauthorised(
+                'Permission to view %s item %d denied' % (class_name, item_id)
+            )
+
         class_obj = self.db.getclass(class_name)
         props = class_obj.properties.keys()
         props.sort()  # sort properties
-        result = [(prop_name, class_obj.get(item_id, prop_name))
-                  for prop_name in props
-                  if self.db.security.hasPermission('View', self.db.getuid(),
-                                                    class_name, prop_name,
-                                                    item_id)]
+        result = [
+            (prop_name, class_obj.get(item_id, prop_name))
+            for prop_name in props
+            if self.db.security.hasPermission(
+                'View', self.db.getuid(), class_name, prop_name,
+            )
+        ]
         result = {
             'id': item_id,
             'type': class_name,
@@ -115,14 +127,15 @@
         return 200, result
 
     def post_collection(self, class_name, input):
-        if not self.db.security.hasPermission('Create', self.db.getuid(),
-                                              class_name):
+        if not self.db.security.hasPermission(
+            'Create', self.db.getuid(), class_name
+        ):
             raise Unauthorised('Permission to create %s denied' % class_name)
 
         class_obj = self.db.getclass(class_name)
 
         # convert types
-        props = props_from_args(self.db, class_obj, input.value)
+        props = self.props_from_args(class_obj, input.value)
 
         # check for the key property
         key = class_obj.getkey()
@@ -130,10 +143,12 @@
             raise UsageError("Must provide the '%s' property." % key)
 
         for key in props:
-            if not self.db.security.hasPermission('Create', self.db.getuid(),
-                                                  class_name, property=key):
-                raise Unauthorised('Permission to create %s.%s denied' %
-                                   (class_name, key))
+            if not self.db.security.hasPermission(
+                'Create', self.db.getuid(), class_name, property=key
+            ):
+                raise Unauthorised(
+                    'Permission to create %s.%s denied' % (class_name, key)
+                )
 
         # do the actual create
         try:
@@ -164,12 +179,15 @@
     def put_element(self, class_name, item_id, input):
         class_obj = self.db.getclass(class_name)
 
-        props = props_from_args(self.db, class_obj, input.value, item_id)
+        props = self.props_from_args(class_obj, input.value, item_id)
         for p in props.iterkeys():
-            if not self.db.security.hasPermission('Edit', self.db.getuid(),
-                                                  class_name, p, item_id):
-                raise Unauthorised('Permission to edit %s of %s%s denied' %
-                                   (p, class_name, item_id))
+            if not self.db.security.hasPermission(
+                'Edit', self.db.getuid(), class_name, p, item_id
+            ):
+                raise Unauthorised(
+                    'Permission to edit %s of %s%s denied' %
+                    (p, class_name, item_id)
+                )
         try:
             result = class_obj.set(item_id, **props)
             self.db.commit()
@@ -185,16 +203,19 @@
         return 200, result
 
     def delete_collection(self, class_name, input):
-        if not self.db.security.hasPermission('Delete', self.db.getuid(),
-                                              class_name):
+        if not self.db.security.hasPermission(
+            'Delete', self.db.getuid(), class_name
+        ):
             raise Unauthorised('Permission to delete %s denied' % class_name)
 
         class_obj = self.db.getclass(class_name)
         for item_id in class_obj.list():
-            if not self.db.security.hasPermission('Delete', self.db.getuid(),
-                                                  class_name, itemid=item_id):
-                raise Unauthorised('Permission to delete %s %s denied' %
-                                   (class_name, item_id))
+            if not self.db.security.hasPermission(
+                'Delete', self.db.getuid(), class_name, itemid=item_id
+            ):
+                raise Unauthorised(
+                    'Permission to delete %s %s denied' % (class_name, item_id)
+                )
 
         count = len(class_obj.list())
         for item_id in class_obj.list():
@@ -209,10 +230,12 @@
         return 200, result
 
     def delete_element(self, class_name, item_id, input):
-        if not self.db.security.hasPermission('Delete', self.db.getuid(),
-                                              class_name, itemid=item_id):
-            raise Unauthorised('Permission to delete %s %s denied' %
-                               (class_name, item_id))
+        if not self.db.security.hasPermission(
+            'Delete', self.db.getuid(), class_name, itemid=item_id
+        ):
+            raise Unauthorised(
+                'Permission to delete %s %s denied' % (class_name, item_id)
+            )
 
         self.db.destroynode(class_name, item_id)
         self.db.commit()
@@ -232,13 +255,17 @@
             op = "replace"
         class_obj = self.db.getclass(class_name)
 
-        props = props_from_args(self.db, class_obj, input.value, item_id)
+        props = self.props_from_args(class_obj, input.value, item_id)
 
         for prop, value in props.iteritems():
-            if not self.db.security.hasPermission('Edit', self.db.getuid(),
-                                                  class_name, prop, item_id):
-                raise Unauthorised('Permission to edit %s of %s%s denied' %
-                                   (prop, class_name, item_id))
+            if not self.db.security.hasPermission(
+                'Edit', self.db.getuid(), class_name, prop, item_id
+            ):
+                raise Unauthorised(
+                    'Permission to edit %s of %s%s denied' %
+                    (prop, class_name, item_id)
+                )
+
             if op == 'add':
                 props[prop] = class_obj.get(item_id, prop) + props[prop]
             elif op == 'replace':
@@ -266,9 +293,11 @@
         return 204, ""
 
     def options_element(self, class_name, item_id, input):
-        self.client.setHeader("Accept-Patch",
-                              "application/x-www-form-urlencoded, "
-                              "multipart/form-data")
+        self.client.setHeader(
+            "Accept-Patch",
+            "application/x-www-form-urlencoded, "
+            "multipart/form-data"
+        )
         return 204, ""
 
     def dispatch(self, method, uri, input):
@@ -279,7 +308,8 @@
         resource_uri = uri.split("/")[1]
 
         # if X-HTTP-Method-Override is set, follow the override method
-        method = self.client.request.headers.getheader('X-HTTP-Method-Override') or method
+        headers = self.client.request.headers
+        method = headers.getheader('X-HTTP-Method-Override') or method
 
         # get the request format for response
         # priority : extension from uri (/rest/issue.json),
@@ -288,7 +318,7 @@
 
         # format_header need a priority parser
         format_ext = os.path.splitext(urlparse.urlparse(uri).path)[1][1:]
-        format_header = self.client.request.headers.getheader('Accept')[12:]
+        format_header = headers.getheader('Accept')[12:]
         format_output = format_ext or format_header or "json"
 
         # check for pretty print
@@ -298,54 +328,65 @@
             pretty_output = False
 
         self.client.setHeader("Access-Control-Allow-Origin", "*")
-        self.client.setHeader("Access-Control-Allow-Headers",
-                              "Content-Type, Authorization, "
-                              "X-HTTP-Method-Override")
+        self.client.setHeader(
+            "Access-Control-Allow-Headers",
+            "Content-Type, Authorization, X-HTTP-Method-Override"
+        )
 
         output = None
         try:
             if resource_uri in self.db.classes:
-                self.client.setHeader("Allow",
-                                      "HEAD, OPTIONS, GET, POST, DELETE")
-                self.client.setHeader("Access-Control-Allow-Methods",
-                                      "HEAD, OPTIONS, GET, POST, DELETE")
-                response_code, output = getattr(self, "%s_collection" % method.lower())(
-                    resource_uri, input)
+                self.client.setHeader(
+                    "Allow",
+                    "HEAD, OPTIONS, GET, POST, DELETE"
+                )
+                self.client.setHeader(
+                    "Access-Control-Allow-Methods",
+                    "HEAD, OPTIONS, GET, POST, DELETE"
+                )
+                response_code, output = getattr(
+                    self, "%s_collection" % method.lower()
+                    )(resource_uri, input)
             else:
                 class_name, item_id = hyperdb.splitDesignator(resource_uri)
-                self.client.setHeader("Allow",
-                                      "HEAD, OPTIONS, GET, PUT, DELETE, PATCH")
-                self.client.setHeader("Access-Control-Allow-Methods",
-                                      "HEAD, OPTIONS, GET, PUT, DELETE, PATCH")
-                response_code, output = getattr(self, "%s_element" % method.lower())(
-                    class_name, item_id, input)
+                self.client.setHeader(
+                    "Allow",
+                    "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
+                )
+                self.client.setHeader(
+                    "Access-Control-Allow-Methods",
+                    "HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
+                )
+                response_code, output = getattr(
+                    self, "%s_element" % method.lower()
+                    )(class_name, item_id, input)
 
-            output = data_obj(output)
+            output = RestfulInstance.data_obj(output)
             self.client.response_code = response_code
         except IndexError, msg:
-            output = error_obj(404, msg)
+            output = RestfulInstance.error_obj(404, msg)
             self.client.response_code = 404
         except Unauthorised, msg:
-            output = error_obj(403, msg)
+            output = RestfulInstance.error_obj(403, msg)
             self.client.response_code = 403
         except (hyperdb.DesignatorError, UsageError), msg:
-            output = error_obj(400, msg)
+            output = RestfulInstance.error_obj(400, msg)
             self.client.response_code = 400
         except (AttributeError, Reject), msg:
-            output = error_obj(405, msg)
+            output = RestfulInstance.error_obj(405, msg)
             self.client.response_code = 405
         except ValueError, msg:
-            output = error_obj(409, msg)
+            output = RestfulInstance.error_obj(409, msg)
             self.client.response_code = 409
         except NotImplementedError:
-            output = error_obj(402, 'Method is under development')
+            output = RestfulInstance.error_obj(402, 'Method under development')
             self.client.response_code = 402
             # nothing to pay, just a mark for debugging purpose
         except:
             # if self.DEBUG_MODE in roundup_server
             # else msg = 'An error occurred. Please check...',
             exc, val, tb = sys.exc_info()
-            output = error_obj(400, val)
+            output = RestfulInstance.error_obj(400, val)
             self.client.response_code = 400
 
             # out to the logfile, it would be nice if the server do it for me
@@ -361,7 +402,7 @@
                 output = RoundupJSONEncoder(indent=indent).encode(output)
             else:
                 self.client.response_code = 406
-                output = ""
+                output = "Content type is not accepted by client"
 
         return output
 
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1435412237 -10800
#      Sat Jun 27 16:37:17 2015 +0300
# Branch REST
# Node ID 864fcf43ccea0e3b47cd6bcf698be17a890b0263
# Parent  4d757555035f0c82947813e54d31e887168a2ed8
Add default op action for Patch

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -226,7 +226,10 @@
         raise Reject('PATCH a class is not allowed')
 
     def patch_element(self, class_name, item_id, input):
-        op = input['op'].value.lower()
+        try:
+            op = input['op'].value.lower()
+        except KeyError:
+            op = "replace"
         class_obj = self.db.getclass(class_name)
 
         props = props_from_args(self.db, class_obj, input.value, item_id)
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1435302985 -10800
#      Fri Jun 26 10:16:25 2015 +0300
# Branch REST
# Node ID 4d757555035f0c82947813e54d31e887168a2ed8
# Parent  362340ad2d35bd07e53dbb13cd4b74fafdbedd08
Improve props_from_args to skip initializing invalid property of the class.

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -18,13 +18,15 @@
 
 
 def props_from_args(db, cl, args, itemid=None):
+    class_props = cl.properties.keys()
     props = {}
+    # props = dict.fromkeys(class_props, None)
+
     for arg in args:
-        try:
-            key = arg.name
-            value = arg.value
-        except ValueError:
-            raise UsageError('argument "%s" not propname=value' % arg)
+        key = arg.name
+        value = arg.value
+        if key not in class_props:
+            continue
         if isinstance(key, unicode):
             try:
                 key = key.encode('ascii')
@@ -35,8 +37,8 @@
         if value:
             try:
                 props[key] = hyperdb.rawToHyperdb(db, cl, itemid, key, value)
-            except hyperdb.HyperdbValueError:
-                pass  # pass if a parameter is not a property of the class
+            except hyperdb.HyperdbValueError, msg:
+                raise UsageError(msg)
         else:
             props[key] = None
 
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1435238667 -10800
#      Thu Jun 25 16:24:27 2015 +0300
# Branch REST
# Node ID 362340ad2d35bd07e53dbb13cd4b74fafdbedd08
# Parent  1f374536414e96108b2b6ccc92d051f0fbb5d903
Added PATCH an element

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -224,7 +224,38 @@
         raise Reject('PATCH a class is not allowed')
 
     def patch_element(self, class_name, item_id, input):
-        raise NotImplementedError
+        op = input['op'].value.lower()
+        class_obj = self.db.getclass(class_name)
+
+        props = props_from_args(self.db, class_obj, input.value, item_id)
+
+        for prop, value in props.iteritems():
+            if not self.db.security.hasPermission('Edit', self.db.getuid(),
+                                                  class_name, prop, item_id):
+                raise Unauthorised('Permission to edit %s of %s%s denied' %
+                                   (prop, class_name, item_id))
+            if op == 'add':
+                props[prop] = class_obj.get(item_id, prop) + props[prop]
+            elif op == 'replace':
+                pass
+            elif op == 'remove':
+                props[prop] = None
+            else:
+                raise UsageError('PATCH Operation %s is not allowed' % op)
+
+        try:
+            result = class_obj.set(item_id, **props)
+            self.db.commit()
+        except (TypeError, IndexError, ValueError), message:
+            raise ValueError(message)
+
+        result = {
+            'id': item_id,
+            'type': class_name,
+            'link': self.base_path + class_name + item_id,
+            'attribute': result
+        }
+        return 200, result
 
     def options_collection(self, class_name, input):
         return 204, ""
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1435223805 -10800
#      Thu Jun 25 12:16:45 2015 +0300
# Branch REST
# Node ID 1f374536414e96108b2b6ccc92d051f0fbb5d903
# Parent  1065a2ab9d97e1abfe9d5dfd89746aa61e5e4320
Added pretty print

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -255,6 +255,12 @@
         format_header = self.client.request.headers.getheader('Accept')[12:]
         format_output = format_ext or format_header or "json"
 
+        # check for pretty print
+        try:
+            pretty_output = input['pretty'].value.lower() == "true"
+        except KeyError:
+            pretty_output = False
+
         self.client.setHeader("Access-Control-Allow-Origin", "*")
         self.client.setHeader("Access-Control-Allow-Headers",
                               "Content-Type, Authorization, "
@@ -312,7 +318,11 @@
         finally:
             if format_output.lower() == "json":
                 self.client.setHeader("Content-Type", "application/json")
-                output = RoundupJSONEncoder().encode(output)
+                if pretty_output:
+                    indent = 4
+                else:
+                    indent = None
+                output = RoundupJSONEncoder(indent=indent).encode(output)
             else:
                 self.client.response_code = 406
                 output = ""
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1435220660 -10800
#      Thu Jun 25 11:24:20 2015 +0300
# Branch REST
# Node ID 1065a2ab9d97e1abfe9d5dfd89746aa61e5e4320
# Parent  04b0fed104972fec2c4980c54337267a4db5f2a6
Handle a case where KeyError exception raise uncaught,
Changed some UsageError exception to ValueError exception to handle 409 Conflicted error.

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -125,7 +125,7 @@
         # check for the key property
         key = class_obj.getkey()
         if key and key not in props:
-            raise UsageError('Must provide the "%s" property.' % key)
+            raise UsageError("Must provide the '%s' property." % key)
 
         for key in props:
             if not self.db.security.hasPermission('Create', self.db.getuid(),
@@ -138,7 +138,9 @@
             item_id = class_obj.create(**props)
             self.db.commit()
         except (TypeError, IndexError, ValueError), message:
-            raise UsageError(message)
+            raise ValueError(message)
+        except KeyError, msg:
+            raise UsageError("Must provide the %s property." % msg)
 
         # set the header Location
         link = self.base_path + class_name + item_id
@@ -152,10 +154,10 @@
         return 201, result
 
     def post_element(self, class_name, item_id, input):
-        raise Reject('Invalid request')
+        raise Reject('POST to an item is not allowed')
 
     def put_collection(self, class_name, input):
-        raise Reject('Invalid request')
+        raise Reject('PUT a class is not allowed')
 
     def put_element(self, class_name, item_id, input):
         class_obj = self.db.getclass(class_name)
@@ -170,7 +172,7 @@
             result = class_obj.set(item_id, **props)
             self.db.commit()
         except (TypeError, IndexError, ValueError), message:
-            raise UsageError(message)
+            raise ValueError(message)
 
         result = {
             'id': item_id,
@@ -219,7 +221,7 @@
         return 200, result
 
     def patch_collection(self, class_name, input):
-        raise Reject('Invalid request')
+        raise Reject('PATCH a class is not allowed')
 
     def patch_element(self, class_name, item_id, input):
         raise NotImplementedError
@@ -288,8 +290,11 @@
             output = error_obj(400, msg)
             self.client.response_code = 400
         except (AttributeError, Reject), msg:
-            output = error_obj(405, 'Method Not Allowed. ' + str(msg))
+            output = error_obj(405, msg)
             self.client.response_code = 405
+        except ValueError, msg:
+            output = error_obj(409, msg)
+            self.client.response_code = 409
         except NotImplementedError:
             output = error_obj(402, 'Method is under development')
             self.client.response_code = 402
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1435152613 -10800
#      Wed Jun 24 16:30:13 2015 +0300
# Branch REST
# Node ID 04b0fed104972fec2c4980c54337267a4db5f2a6
# Parent  f51de558ba050a279dedde3000b5cdbc39ba9206
Added OPTIONS method,
Fix a bug with class response header not allowing GET

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -224,6 +224,15 @@
     def patch_element(self, class_name, item_id, input):
         raise NotImplementedError
 
+    def options_collection(self, class_name, input):
+        return 204, ""
+
+    def options_element(self, class_name, item_id, input):
+        self.client.setHeader("Accept-Patch",
+                              "application/x-www-form-urlencoded, "
+                              "multipart/form-data")
+        return 204, ""
+
     def dispatch(self, method, uri, input):
         # PATH is split to multiple pieces
         # 0 - rest
@@ -253,9 +262,9 @@
         try:
             if resource_uri in self.db.classes:
                 self.client.setHeader("Allow",
-                                      "HEAD, OPTIONS, POST, DELETE")
+                                      "HEAD, OPTIONS, GET, POST, DELETE")
                 self.client.setHeader("Access-Control-Allow-Methods",
-                                      "HEAD, OPTIONS, POST, DELETE")
+                                      "HEAD, OPTIONS, GET, POST, DELETE")
                 response_code, output = getattr(self, "%s_collection" % method.lower())(
                     resource_uri, input)
             else:
diff --git a/roundup/scripts/roundup_server.py b/roundup/scripts/roundup_server.py
--- a/roundup/scripts/roundup_server.py
+++ b/roundup/scripts/roundup_server.py
@@ -251,7 +251,7 @@
         else:
             return self.run_cgi()
 
-    do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = do_PATCH = run_cgi_outer
+    do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = do_PATCH = do_OPTIONS = run_cgi_outer
 
     def index(self):
         ''' Print up an index of the available trackers
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1435148811 -10800
#      Wed Jun 24 15:26:51 2015 +0300
# Branch REST
# Node ID f51de558ba050a279dedde3000b5cdbc39ba9206
# Parent  08558a8b5f28f4b71b6dcedfd991e79e032acc24
Added X-Count-Total for pagination of GET method,
Parse X-HTTP-Method-Override from client,
Parse Request Accept content-type from URL and Header

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -5,6 +5,8 @@
 and/or modify under the same terms as Python.
 """
 
+import urlparse
+import os
 import json
 import pprint
 import sys
@@ -74,8 +76,6 @@
         tracker = self.client.env['TRACKER_NAME']
         self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker)
 
-        print self.base_path
-
     def get_collection(self, class_name, input):
         if not self.db.security.hasPermission('View', self.db.getuid(),
                                               class_name):
@@ -87,6 +87,7 @@
                   if self.db.security.hasPermission('View', self.db.getuid(),
                                                     class_name,
                                                     itemid=item_id)]
+        self.client.setHeader("X-Count-Total", str(len(result)))
         return 200, result
 
     def get_element(self, class_name, item_id, input):
@@ -230,21 +231,39 @@
         # 2 - attribute
         resource_uri = uri.split("/")[1]
 
-        self.client.setHeader("Access-Control-Allow-Methods",
-                              "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH")
+        # if X-HTTP-Method-Override is set, follow the override method
+        method = self.client.request.headers.getheader('X-HTTP-Method-Override') or method
+
+        # get the request format for response
+        # priority : extension from uri (/rest/issue.json),
+        #            header (Accept: application/json, application/xml)
+        #            default (application/json)
+
+        # format_header need a priority parser
+        format_ext = os.path.splitext(urlparse.urlparse(uri).path)[1][1:]
+        format_header = self.client.request.headers.getheader('Accept')[12:]
+        format_output = format_ext or format_header or "json"
+
+        self.client.setHeader("Access-Control-Allow-Origin", "*")
         self.client.setHeader("Access-Control-Allow-Headers",
-                              "Content-Type, Authorization,"
+                              "Content-Type, Authorization, "
                               "X-HTTP-Method-Override")
-        self.client.setHeader("Allow",
-                              "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH")
 
         output = None
         try:
             if resource_uri in self.db.classes:
+                self.client.setHeader("Allow",
+                                      "HEAD, OPTIONS, POST, DELETE")
+                self.client.setHeader("Access-Control-Allow-Methods",
+                                      "HEAD, OPTIONS, POST, DELETE")
                 response_code, output = getattr(self, "%s_collection" % method.lower())(
                     resource_uri, input)
             else:
                 class_name, item_id = hyperdb.splitDesignator(resource_uri)
+                self.client.setHeader("Allow",
+                                      "HEAD, OPTIONS, GET, PUT, DELETE, PATCH")
+                self.client.setHeader("Access-Control-Allow-Methods",
+                                      "HEAD, OPTIONS, GET, PUT, DELETE, PATCH")
                 response_code, output = getattr(self, "%s_element" % method.lower())(
                     class_name, item_id, input)
 
@@ -277,8 +296,12 @@
             print 'EXCEPTION AT', time.ctime()
             traceback.print_exc()
         finally:
-            self.client.setHeader("Content-Type", "application/json")
-            output = RoundupJSONEncoder().encode(output)
+            if format_output.lower() == "json":
+                self.client.setHeader("Content-Type", "application/json")
+                output = RoundupJSONEncoder().encode(output)
+            else:
+                self.client.response_code = 406
+                output = ""
 
         return output
 
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1435069725 -10800
#      Tue Jun 23 17:28:45 2015 +0300
# Branch REST
# Node ID 08558a8b5f28f4b71b6dcedfd991e79e032acc24
# Parent  b5c9c775c40fa3a2109f843ef3f277feb4e7bfe4
Handle response header

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -139,9 +139,14 @@
         except (TypeError, IndexError, ValueError), message:
             raise UsageError(message)
 
+        # set the header Location
+        link = self.base_path + class_name + item_id
+        self.client.setHeader("Location", link)
+
+        # set the response body
         result = {
             'id': item_id,
-            'link': self.base_path + class_name + item_id
+            'link': link
         }
         return 201, result
 
@@ -225,6 +230,14 @@
         # 2 - attribute
         resource_uri = uri.split("/")[1]
 
+        self.client.setHeader("Access-Control-Allow-Methods",
+                              "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH")
+        self.client.setHeader("Access-Control-Allow-Headers",
+                              "Content-Type, Authorization,"
+                              "X-HTTP-Method-Override")
+        self.client.setHeader("Allow",
+                              "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH")
+
         output = None
         try:
             if resource_uri in self.db.classes:
@@ -264,9 +277,9 @@
             print 'EXCEPTION AT', time.ctime()
             traceback.print_exc()
         finally:
+            self.client.setHeader("Content-Type", "application/json")
             output = RoundupJSONEncoder().encode(output)
 
-        print "Length: %s - Content(50 char): %s" % (len(output), output[:50])
         return output
 
 
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434902006 -10800
#      Sun Jun 21 18:53:26 2015 +0300
# Branch REST
# Node ID b5c9c775c40fa3a2109f843ef3f277feb4e7bfe4
# Parent  82b4a64bd5b42ad1bc72bc2df20064f81bfe604f
Added successful response status code

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -81,14 +81,13 @@
                                               class_name):
             raise Unauthorised('Permission to view %s denied' % class_name)
         class_obj = self.db.getclass(class_name)
-        prop_name = class_obj.labelprop()
         class_path = self.base_path + class_name
         result = [{'id': item_id, 'link': class_path + item_id}
                   for item_id in class_obj.list()
                   if self.db.security.hasPermission('View', self.db.getuid(),
                                                     class_name,
                                                     itemid=item_id)]
-        return result
+        return 200, result
 
     def get_element(self, class_name, item_id, input):
         if not self.db.security.hasPermission('View', self.db.getuid(),
@@ -110,7 +109,7 @@
             'attributes': dict(result)
         }
 
-        return result
+        return 200, result
 
     def post_collection(self, class_name, input):
         if not self.db.security.hasPermission('Create', self.db.getuid(),
@@ -144,7 +143,7 @@
             'id': item_id,
             'link': self.base_path + class_name + item_id
         }
-        return result
+        return 201, result
 
     def post_element(self, class_name, item_id, input):
         raise Reject('Invalid request')
@@ -173,7 +172,7 @@
             'link': self.base_path + class_name + item_id,
             'attribute': result
         }
-        return result
+        return 200, result
 
     def delete_collection(self, class_name, input):
         if not self.db.security.hasPermission('Delete', self.db.getuid(),
@@ -197,7 +196,7 @@
             'count': count
         }
 
-        return result
+        return 200, result
 
     def delete_element(self, class_name, item_id, input):
         if not self.db.security.hasPermission('Delete', self.db.getuid(),
@@ -211,7 +210,7 @@
             'status': 'ok'
         }
 
-        return result
+        return 200, result
 
     def patch_collection(self, class_name, input):
         raise Reject('Invalid request')
@@ -229,15 +228,15 @@
         output = None
         try:
             if resource_uri in self.db.classes:
-                output = getattr(self, "%s_collection" % method.lower())(
+                response_code, output = getattr(self, "%s_collection" % method.lower())(
                     resource_uri, input)
             else:
                 class_name, item_id = hyperdb.splitDesignator(resource_uri)
-                output = getattr(self, "%s_element" % method.lower())(
+                response_code, output = getattr(self, "%s_element" % method.lower())(
                     class_name, item_id, input)
 
             output = data_obj(output)
-            self.client.response_code = 200
+            self.client.response_code = response_code
         except IndexError, msg:
             output = error_obj(404, msg)
             self.client.response_code = 404
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434698637 -10800
#      Fri Jun 19 10:23:57 2015 +0300
# Branch REST
# Node ID 82b4a64bd5b42ad1bc72bc2df20064f81bfe604f
# Parent  8809907f0b64be97968a0166dc4dda3440c7de83
Added response code

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -237,22 +237,29 @@
                     class_name, item_id, input)
 
             output = data_obj(output)
+            self.client.response_code = 200
         except IndexError, msg:
             output = error_obj(404, msg)
+            self.client.response_code = 404
         except Unauthorised, msg:
             output = error_obj(403, msg)
+            self.client.response_code = 403
         except (hyperdb.DesignatorError, UsageError), msg:
             output = error_obj(400, msg)
+            self.client.response_code = 400
         except (AttributeError, Reject), msg:
             output = error_obj(405, 'Method Not Allowed. ' + str(msg))
+            self.client.response_code = 405
         except NotImplementedError:
             output = error_obj(402, 'Method is under development')
+            self.client.response_code = 402
             # nothing to pay, just a mark for debugging purpose
         except:
             # if self.DEBUG_MODE in roundup_server
             # else msg = 'An error occurred. Please check...',
             exc, val, tb = sys.exc_info()
             output = error_obj(400, val)
+            self.client.response_code = 400
 
             # out to the logfile, it would be nice if the server do it for me
             print 'EXCEPTION AT', time.ctime()
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434638512 -10800
#      Thu Jun 18 17:41:52 2015 +0300
# Branch REST
# Node ID 8809907f0b64be97968a0166dc4dda3440c7de83
# Parent  ac06bf529bb0b11582802533cd99ff7d17ad2670
Successful response is now following the design format
Fix a bug with exception error message

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -82,7 +82,8 @@
             raise Unauthorised('Permission to view %s denied' % class_name)
         class_obj = self.db.getclass(class_name)
         prop_name = class_obj.labelprop()
-        result = [{'id': item_id, prop_name: class_obj.get(item_id, prop_name)}
+        class_path = self.base_path + class_name
+        result = [{'id': item_id, 'link': class_path + item_id}
                   for item_id in class_obj.list()
                   if self.db.security.hasPermission('View', self.db.getuid(),
                                                     class_name,
@@ -102,8 +103,12 @@
                   if self.db.security.hasPermission('View', self.db.getuid(),
                                                     class_name, prop_name,
                                                     item_id)]
-        result = dict(result)
-        result['id'] = item_id
+        result = {
+            'id': item_id,
+            'type': class_name,
+            'link': self.base_path + class_name + item_id,
+            'attributes': dict(result)
+        }
 
         return result
 
@@ -135,7 +140,10 @@
         except (TypeError, IndexError, ValueError), message:
             raise UsageError(message)
 
-        result = {id: item_id}
+        result = {
+            'id': item_id,
+            'link': self.base_path + class_name + item_id
+        }
         return result
 
     def post_element(self, class_name, item_id, input):
@@ -159,7 +167,12 @@
         except (TypeError, IndexError, ValueError), message:
             raise UsageError(message)
 
-        result['id'] = item_id
+        result = {
+            'id': item_id,
+            'type': class_name,
+            'link': self.base_path + class_name + item_id,
+            'attribute': result
+        }
         return result
 
     def delete_collection(self, class_name, input):
@@ -174,11 +187,15 @@
                 raise Unauthorised('Permission to delete %s %s denied' %
                                    (class_name, item_id))
 
+        count = len(class_obj.list())
         for item_id in class_obj.list():
             self.db.destroynode(class_name, item_id)
 
         self.db.commit()
-        result = {"status": "ok"}
+        result = {
+            'status': 'ok',
+            'count': count
+        }
 
         return result
 
@@ -190,7 +207,9 @@
 
         self.db.destroynode(class_name, item_id)
         self.db.commit()
-        result = {"status": "ok"}
+        result = {
+            'status': 'ok'
+        }
 
         return result
 
@@ -224,8 +243,8 @@
             output = error_obj(403, msg)
         except (hyperdb.DesignatorError, UsageError), msg:
             output = error_obj(400, msg)
-        except (AttributeError, Reject):
-            output = error_obj(405, 'Method Not Allowed')
+        except (AttributeError, Reject), msg:
+            output = error_obj(405, 'Method Not Allowed. ' + str(msg))
         except NotImplementedError:
             output = error_obj(402, 'Method is under development')
             # nothing to pay, just a mark for debugging purpose
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434614149 -10800
#      Thu Jun 18 10:55:49 2015 +0300
# Branch REST
# Node ID ac06bf529bb0b11582802533cd99ff7d17ad2670
# Parent  a38ecbc69e633260c4f77e10f27af4481f6e65a8
Add base_path to generate uri
Handle IndexError exception

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -69,6 +69,13 @@
         self.client = client  # it might be unnecessary to receive the client
         self.db = db
 
+        protocol = 'http'
+        host = self.client.env['HTTP_HOST']
+        tracker = self.client.env['TRACKER_NAME']
+        self.base_path = '%s://%s/%s/rest/' % (protocol, host, tracker)
+
+        print self.base_path
+
     def get_collection(self, class_name, input):
         if not self.db.security.hasPermission('View', self.db.getuid(),
                                               class_name):
@@ -197,6 +204,7 @@
         # PATH is split to multiple pieces
         # 0 - rest
         # 1 - resource
+        # 2 - attribute
         resource_uri = uri.split("/")[1]
 
         output = None
@@ -210,6 +218,8 @@
                     class_name, item_id, input)
 
             output = data_obj(output)
+        except IndexError, msg:
+            output = error_obj(404, msg)
         except Unauthorised, msg:
             output = error_obj(403, msg)
         except (hyperdb.DesignatorError, UsageError), msg:
@@ -218,7 +228,7 @@
             output = error_obj(405, 'Method Not Allowed')
         except NotImplementedError:
             output = error_obj(402, 'Method is under development')
-            # nothing to pay, just mark that this is under development
+            # nothing to pay, just a mark for debugging purpose
         except:
             # if self.DEBUG_MODE in roundup_server
             # else msg = 'An error occurred. Please check...',
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434387710 -10800
#      Mon Jun 15 20:01:50 2015 +0300
# Branch REST
# Node ID a38ecbc69e633260c4f77e10f27af4481f6e65a8
# Parent  90d411daacb68ce77603d9cf083e5e22da0e376b
Making objects returned by REST follow the standard (wrapped by a dictionary, in either 'data' or 'error' field)
Temporally added the client to REST so REST can make changes to HTTP Status code and Header

diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
--- a/roundup/cgi/client.py
+++ b/roundup/cgi/client.py
@@ -432,7 +432,7 @@
         self.check_anonymous_access()
 
         # Call rest library to handle the request
-        handler = rest.RestfulInstance(self.db)
+        handler = rest.RestfulInstance(self, self.db)
         output = handler.dispatch(self.env['REQUEST_METHOD'], self.path,
                                   self.form)
 
diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -41,12 +41,32 @@
     return props
 
 
+def error_obj(status, msg, source=None):
+    result = {
+        'error': {
+            'status': status,
+            'msg': msg
+        }
+    }
+    if source is not None:
+        result['error']['source'] = source
+
+    return result
+
+
+def data_obj(data):
+    result = {
+        'data': data
+    }
+    return result
+
+
 class RestfulInstance(object):
     """Dummy Handler for REST
     """
 
-    def __init__(self, db):
-        # TODO: database, translator and instance.actions
+    def __init__(self, client, db):
+        self.client = client  # it might be unnecessary to receive the client
         self.db = db
 
     def get_collection(self, class_name, input):
@@ -188,17 +208,22 @@
                 class_name, item_id = hyperdb.splitDesignator(resource_uri)
                 output = getattr(self, "%s_element" % method.lower())(
                     class_name, item_id, input)
-        except (hyperdb.DesignatorError, UsageError, Unauthorised), msg:
-            output = {'status': 'error', 'msg': msg}
+
+            output = data_obj(output)
+        except Unauthorised, msg:
+            output = error_obj(403, msg)
+        except (hyperdb.DesignatorError, UsageError), msg:
+            output = error_obj(400, msg)
         except (AttributeError, Reject):
-            output = {'status': 'error', 'msg': 'Method is not allowed'}
+            output = error_obj(405, 'Method Not Allowed')
         except NotImplementedError:
-            output = {'status': 'error', 'msg': 'Method is under development'}
+            output = error_obj(402, 'Method is under development')
+            # nothing to pay, just mark that this is under development
         except:
             # if self.DEBUG_MODE in roundup_server
             # else msg = 'An error occurred. Please check...',
             exc, val, tb = sys.exc_info()
-            output = {'status': 'error', 'msg': val}
+            output = error_obj(400, val)
 
             # out to the logfile, it would be nice if the server do it for me
             print 'EXCEPTION AT', time.ctime()
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434321555 -10800
#      Mon Jun 15 01:39:15 2015 +0300
# Branch REST
# Node ID 90d411daacb68ce77603d9cf083e5e22da0e376b
# Parent  c1aeec280158ba2a0d1bf4151f76db3a0f1d47f0
Added exception Handling

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -7,10 +7,14 @@
 
 import json
 import pprint
+import sys
+import time
+import traceback
 from roundup import hyperdb
 from roundup.exceptions import *
 from roundup import xmlrpc
 
+
 def props_from_args(db, cl, args, itemid=None):
     props = {}
     for arg in args:
@@ -36,6 +40,7 @@
 
     return props
 
+
 class RestfulInstance(object):
     """Dummy Handler for REST
     """
@@ -183,16 +188,28 @@
                 class_name, item_id = hyperdb.splitDesignator(resource_uri)
                 output = getattr(self, "%s_element" % method.lower())(
                     class_name, item_id, input)
-        except hyperdb.DesignatorError:
-            raise NotImplementedError('Invalid URI')
-        except AttributeError:
-            raise NotImplementedError('Method is invalid')
+        except (hyperdb.DesignatorError, UsageError, Unauthorised), msg:
+            output = {'status': 'error', 'msg': msg}
+        except (AttributeError, Reject):
+            output = {'status': 'error', 'msg': 'Method is not allowed'}
+        except NotImplementedError:
+            output = {'status': 'error', 'msg': 'Method is under development'}
+        except:
+            # if self.DEBUG_MODE in roundup_server
+            # else msg = 'An error occurred. Please check...',
+            exc, val, tb = sys.exc_info()
+            output = {'status': 'error', 'msg': val}
+
+            # out to the logfile, it would be nice if the server do it for me
+            print 'EXCEPTION AT', time.ctime()
+            traceback.print_exc()
         finally:
             output = RoundupJSONEncoder().encode(output)
 
         print "Length: %s - Content(50 char): %s" % (len(output), output[:50])
         return output
 
+
 class RoundupJSONEncoder(json.JSONEncoder):
     def default(self, obj):
         try:
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434319238 -10800
#      Mon Jun 15 01:00:38 2015 +0300
# Branch REST
# Node ID c1aeec280158ba2a0d1bf4151f76db3a0f1d47f0
# Parent  ba2c4d11a7aa2f98befeec9c914ef79d651c05ee
Added RoundupJSONEncoder to handle classes from roundup

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -50,7 +50,7 @@
             raise Unauthorised('Permission to view %s denied' % class_name)
         class_obj = self.db.getclass(class_name)
         prop_name = class_obj.labelprop()
-        result = [{'id': item_id, 'name': class_obj.get(item_id, prop_name)}
+        result = [{'id': item_id, prop_name: class_obj.get(item_id, prop_name)}
                   for item_id in class_obj.list()
                   if self.db.security.hasPermission('View', self.db.getuid(),
                                                     class_name,
@@ -71,6 +71,7 @@
                                                     class_name, prop_name,
                                                     item_id)]
         result = dict(result)
+        result['id'] = item_id
 
         return result
 
@@ -187,7 +188,15 @@
         except AttributeError:
             raise NotImplementedError('Method is invalid')
         finally:
-            output = json.JSONEncoder().encode(output)
+            output = RoundupJSONEncoder().encode(output)
 
         print "Length: %s - Content(50 char): %s" % (len(output), output[:50])
         return output
+
+class RoundupJSONEncoder(json.JSONEncoder):
+    def default(self, obj):
+        try:
+            result = json.JSONEncoder.default(self, obj)
+        except TypeError:
+            result = str(obj)
+        return result
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434299160 -10800
#      Sun Jun 14 19:26:00 2015 +0300
# Branch REST
# Node ID ba2c4d11a7aa2f98befeec9c914ef79d651c05ee
# Parent  5197e29cf5e74596e48286e719c13bec9178e34a
added custom parsing properties from arguments

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -11,6 +11,30 @@
 from roundup.exceptions import *
 from roundup import xmlrpc
 
+def props_from_args(db, cl, args, itemid=None):
+    props = {}
+    for arg in args:
+        try:
+            key = arg.name
+            value = arg.value
+        except ValueError:
+            raise UsageError('argument "%s" not propname=value' % arg)
+        if isinstance(key, unicode):
+            try:
+                key = key.encode('ascii')
+            except UnicodeEncodeError:
+                raise UsageError('argument %r is no valid ascii keyword' % key)
+        if isinstance(value, unicode):
+            value = value.encode('utf-8')
+        if value:
+            try:
+                props[key] = hyperdb.rawToHyperdb(db, cl, itemid, key, value)
+            except hyperdb.HyperdbValueError:
+                pass  # pass if a parameter is not a property of the class
+        else:
+            props[key] = None
+
+    return props
 
 class RestfulInstance(object):
     """Dummy Handler for REST
@@ -58,8 +82,7 @@
         class_obj = self.db.getclass(class_name)
 
         # convert types
-        input_data = ["%s=%s" % (item.name, item.value) for item in input.value]
-        props = xmlrpc.props_from_args(self.db, class_obj, input_data)
+        props = props_from_args(self.db, class_obj, input.value)
 
         # check for the key property
         key = class_obj.getkey()
@@ -91,8 +114,7 @@
     def put_element(self, class_name, item_id, input):
         class_obj = self.db.getclass(class_name)
 
-        input_data = ["%s=%s" % (item.name, item.value) for item in input.value]
-        props = xmlrpc.props_from_args(self.db, class_obj, input_data, item_id)
+        props = props_from_args(self.db, class_obj, input.value, item_id)
         for p in props.iterkeys():
             if not self.db.security.hasPermission('Edit', self.db.getuid(),
                                                   class_name, p, item_id):
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434254817 -10800
#      Sun Jun 14 07:06:57 2015 +0300
# Branch REST
# Node ID 5197e29cf5e74596e48286e719c13bec9178e34a
# Parent  c13388b3ab514a1902dfde4597f3b81eb4e227b5
Added Partial PUT

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -89,7 +89,23 @@
         raise Reject('Invalid request')
 
     def put_element(self, class_name, item_id, input):
-        raise NotImplementedError
+        class_obj = self.db.getclass(class_name)
+
+        input_data = ["%s=%s" % (item.name, item.value) for item in input.value]
+        props = xmlrpc.props_from_args(self.db, class_obj, input_data, item_id)
+        for p in props.iterkeys():
+            if not self.db.security.hasPermission('Edit', self.db.getuid(),
+                                                  class_name, p, item_id):
+                raise Unauthorised('Permission to edit %s of %s%s denied' %
+                                   (p, class_name, item_id))
+        try:
+            result = class_obj.set(item_id, **props)
+            self.db.commit()
+        except (TypeError, IndexError, ValueError), message:
+            raise UsageError(message)
+
+        result['id'] = item_id
+        return result
 
     def delete_collection(self, class_name, input):
         if not self.db.security.hasPermission('Delete', self.db.getuid(),
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434241919 -10800
#      Sun Jun 14 03:31:59 2015 +0300
# Branch REST
# Node ID c13388b3ab514a1902dfde4597f3b81eb4e227b5
# Parent  e4b78ec60967a683ac78b45f293c654e3cbd036a
Implement delete collection, Added raising exception from post to element, put to collection, and patch to collection

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -83,25 +83,40 @@
         return result
 
     def post_element(self, class_name, item_id, input):
-        raise NotImplementedError
+        raise Reject('Invalid request')
 
     def put_collection(self, class_name, input):
-        raise NotImplementedError
+        raise Reject('Invalid request')
 
     def put_element(self, class_name, item_id, input):
         raise NotImplementedError
 
     def delete_collection(self, class_name, input):
-        # TODO: should I allow user to delete the whole collection ?
-        raise NotImplementedError
+        if not self.db.security.hasPermission('Delete', self.db.getuid(),
+                                              class_name):
+            raise Unauthorised('Permission to delete %s denied' % class_name)
+
+        class_obj = self.db.getclass(class_name)
+        for item_id in class_obj.list():
+            if not self.db.security.hasPermission('Delete', self.db.getuid(),
+                                                  class_name, itemid=item_id):
+                raise Unauthorised('Permission to delete %s %s denied' %
+                                   (class_name, item_id))
+
+        for item_id in class_obj.list():
+            self.db.destroynode(class_name, item_id)
+
+        self.db.commit()
+        result = {"status": "ok"}
+
+        return result
 
     def delete_element(self, class_name, item_id, input):
         if not self.db.security.hasPermission('Delete', self.db.getuid(),
                                               class_name, itemid=item_id):
             raise Unauthorised('Permission to delete %s %s denied' %
                                (class_name, item_id))
-        if item_id != input['id'].value:
-            raise UsageError('Must provide id key as confirmation')
+
         self.db.destroynode(class_name, item_id)
         self.db.commit()
         result = {"status": "ok"}
@@ -109,7 +124,7 @@
         return result
 
     def patch_collection(self, class_name, input):
-        raise NotImplementedError
+        raise Reject('Invalid request')
 
     def patch_element(self, class_name, item_id, input):
         raise NotImplementedError
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434219506 -10800
#      Sat Jun 13 21:18:26 2015 +0300
# Branch REST
# Node ID e4b78ec60967a683ac78b45f293c654e3cbd036a
# Parent  2008b4f7efc08a97c3c5357754f2b5b679b48f3d
Cleanup, fixed a bug with delete action,  change the returned type of every action from JSON to list/object

diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
--- a/roundup/cgi/client.py
+++ b/roundup/cgi/client.py
@@ -423,10 +423,6 @@
         self.write(output)
 
     def handle_rest(self):
-        # Pull the parameters data out of the form.  The "value" attribute
-        # will be the raw content of the request.
-        input = self.form.value
-
         # Set the charset and language
         self.determine_charset()
         self.determine_language()
@@ -437,7 +433,8 @@
 
         # Call rest library to handle the request
         handler = rest.RestfulInstance(self.db)
-        output = handler.dispatch(self.env['REQUEST_METHOD'], self.path, input)
+        output = handler.dispatch(self.env['REQUEST_METHOD'], self.path,
+                                  self.form)
 
         # self.setHeader("Content-Type", "text/xml")
         self.setHeader("Content-Length", str(len(output)))
diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -8,7 +8,7 @@
 import json
 import pprint
 from roundup import hyperdb
-from roundup.cgi.templating import Unauthorised
+from roundup.exceptions import *
 from roundup import xmlrpc
 
 
@@ -21,18 +21,23 @@
         self.db = db
 
     def get_collection(self, class_name, input):
+        if not self.db.security.hasPermission('View', self.db.getuid(),
+                                              class_name):
+            raise Unauthorised('Permission to view %s denied' % class_name)
         class_obj = self.db.getclass(class_name)
         prop_name = class_obj.labelprop()
         result = [{'id': item_id, 'name': class_obj.get(item_id, prop_name)}
                   for item_id in class_obj.list()
                   if self.db.security.hasPermission('View', self.db.getuid(),
-                                                    class_name, None, item_id)
-                  ]
-        result = json.JSONEncoder().encode(result)
-
+                                                    class_name,
+                                                    itemid=item_id)]
         return result
 
     def get_element(self, class_name, item_id, input):
+        if not self.db.security.hasPermission('View', self.db.getuid(),
+                                              class_name, itemid=item_id):
+            raise Unauthorised('Permission to view %s item %d denied' %
+                               (class_name, item_id))
         class_obj = self.db.getclass(class_name)
         props = class_obj.properties.keys()
         props.sort()  # sort properties
@@ -40,9 +45,8 @@
                   for prop_name in props
                   if self.db.security.hasPermission('View', self.db.getuid(),
                                                     class_name, prop_name,
-                                                    item_id)
-                  ]
-        result = json.JSONEncoder().encode(dict(result))
+                                                    item_id)]
+        result = dict(result)
 
         return result
 
@@ -54,12 +58,13 @@
         class_obj = self.db.getclass(class_name)
 
         # convert types
-        props = xmlrpc.props_from_args(self.db, class_obj, input)
+        input_data = ["%s=%s" % (item.name, item.value) for item in input.value]
+        props = xmlrpc.props_from_args(self.db, class_obj, input_data)
 
         # check for the key property
         key = class_obj.getkey()
         if key and key not in props:
-            raise xmlrpc.UsageError, 'Must provide the "%s" property.' % key
+            raise UsageError('Must provide the "%s" property.' % key)
 
         for key in props:
             if not self.db.security.hasPermission('Create', self.db.getuid(),
@@ -69,10 +74,12 @@
 
         # do the actual create
         try:
-            result = class_obj.create(**props)
+            item_id = class_obj.create(**props)
             self.db.commit()
         except (TypeError, IndexError, ValueError), message:
-            raise xmlrpc.UsageError, message
+            raise UsageError(message)
+
+        result = {id: item_id}
         return result
 
     def post_element(self, class_name, item_id, input):
@@ -89,13 +96,15 @@
         raise NotImplementedError
 
     def delete_element(self, class_name, item_id, input):
-        # TODO: BUG with DELETE without form data. Working with random data
-        #       crash at line self.form = cgi.FieldStorage(fp=request.rfile, environ=env)
-        try:
-            self.db.destroynode(class_name, item_id)
-            result = 'OK'
-        except IndexError:
-            result = 'Error'
+        if not self.db.security.hasPermission('Delete', self.db.getuid(),
+                                              class_name, itemid=item_id):
+            raise Unauthorised('Permission to delete %s %s denied' %
+                               (class_name, item_id))
+        if item_id != input['id'].value:
+            raise UsageError('Must provide id key as confirmation')
+        self.db.destroynode(class_name, item_id)
+        self.db.commit()
+        result = {"status": "ok"}
 
         return result
 
@@ -106,33 +115,26 @@
         raise NotImplementedError
 
     def dispatch(self, method, uri, input):
-        print "METHOD: " + method + " URI: " + uri
-        print type(input)
-        pprint.pprint(input)
-        # TODO: process input_form directly instead of making a new array
-        # TODO: rest server
-        # TODO: check roundup/actions.py
-        # TODO: if uri_path has more than 2 child, return 404
-        # TODO: custom JSONEncoder to handle other data type
-        # TODO: catch all error and display error.
-
         # PATH is split to multiple pieces
         # 0 - rest
         # 1 - resource
+        resource_uri = uri.split("/")[1]
 
-        resource_uri = uri.split("/")[1]
-        input_data = ["%s=%s" % (item.name, item.value) for item in input]
-
+        output = None
         try:
             if resource_uri in self.db.classes:
-                output = getattr(self, "%s_collection" % method.lower())(resource_uri, input_data)
+                output = getattr(self, "%s_collection" % method.lower())(
+                    resource_uri, input)
             else:
                 class_name, item_id = hyperdb.splitDesignator(resource_uri)
-                output = getattr(self, "%s_element" % method.lower())(class_name, item_id, input_data)
+                output = getattr(self, "%s_element" % method.lower())(
+                    class_name, item_id, input)
         except hyperdb.DesignatorError:
-            pass  # invalid URI
+            raise NotImplementedError('Invalid URI')
         except AttributeError:
-            raise NotImplementedError  # Error: method is invalid
+            raise NotImplementedError('Method is invalid')
+        finally:
+            output = json.JSONEncoder().encode(output)
 
         print "Length: %s - Content(50 char): %s" % (len(output), output[:50])
         return output
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434131105 -10800
#      Fri Jun 12 20:45:05 2015 +0300
# Branch REST
# Node ID 2008b4f7efc08a97c3c5357754f2b5b679b48f3d
# Parent  c26ec962c982622a8d7c0821a1a55efb814de6d9
Split all rest action into 2 type: element uri and collection uri

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -14,56 +14,39 @@
 
 class RestfulInstance(object):
     """Dummy Handler for REST
-       WARNING: Very ugly !!!!, cleaned & better organized in progress (next commit)
     """
 
     def __init__(self, db):
         # TODO: database, translator and instance.actions
         self.db = db
 
-    def action_get(self, resource_uri, input):
-        # TODO: split this into collection URI and resource URI
-        class_name = resource_uri
-        try:
-            class_obj = self.db.getclass(class_name)
-            """prop_name = class_obj.labelprop()
-            result = [class_obj.get(item_id, prop_name)"""
-            result = [{'id': item_id}
-                      for item_id in class_obj.list()
-                      if self.db.security.hasPermission('View',
-                                                        self.db.getuid(),
-                                                        class_name,
-                                                        None,
-                                                        item_id)
-                      ]
-            result = json.JSONEncoder().encode(result)
-            # result = `len(dict(result))` + ' ' + `len(result)`
-        except KeyError:
-            pass
-
-        try:
-            class_name, item_id = hyperdb.splitDesignator(resource_uri)
-            class_obj = self.db.getclass(class_name)
-            props = class_obj.properties.keys()
-            props.sort()
-            result = [(prop_name, class_obj.get(item_id, prop_name))
-                      for prop_name in props
-                      if self.db.security.hasPermission('View',
-                                                        self.db.getuid(),
-                                                        class_name,
-                                                        prop_name,
-                                                        item_id)
-                      ]
-            # Note: is this a bug by having an extra indent in xmlrpc ?
-            result = json.JSONEncoder().encode(dict(result))
-        except hyperdb.DesignatorError:
-            pass
+    def get_collection(self, class_name, input):
+        class_obj = self.db.getclass(class_name)
+        prop_name = class_obj.labelprop()
+        result = [{'id': item_id, 'name': class_obj.get(item_id, prop_name)}
+                  for item_id in class_obj.list()
+                  if self.db.security.hasPermission('View', self.db.getuid(),
+                                                    class_name, None, item_id)
+                  ]
+        result = json.JSONEncoder().encode(result)
 
         return result
 
-    def action_post(self, resource_uri, input):
-        class_name = resource_uri
+    def get_element(self, class_name, item_id, input):
+        class_obj = self.db.getclass(class_name)
+        props = class_obj.properties.keys()
+        props.sort()  # sort properties
+        result = [(prop_name, class_obj.get(item_id, prop_name))
+                  for prop_name in props
+                  if self.db.security.hasPermission('View', self.db.getuid(),
+                                                    class_name, prop_name,
+                                                    item_id)
+                  ]
+        result = json.JSONEncoder().encode(dict(result))
 
+        return result
+
+    def post_collection(self, class_name, input):
         if not self.db.security.hasPermission('Create', self.db.getuid(),
                                               class_name):
             raise Unauthorised('Permission to create %s denied' % class_name)
@@ -92,60 +75,64 @@
             raise xmlrpc.UsageError, message
         return result
 
-    def action_put(self, resource_uri, input):
+    def post_element(self, class_name, item_id, input):
         raise NotImplementedError
 
-    def action_delete(self, resource_uri, input):
+    def put_collection(self, class_name, input):
+        raise NotImplementedError
+
+    def put_element(self, class_name, item_id, input):
+        raise NotImplementedError
+
+    def delete_collection(self, class_name, input):
         # TODO: should I allow user to delete the whole collection ?
+        raise NotImplementedError
+
+    def delete_element(self, class_name, item_id, input):
         # TODO: BUG with DELETE without form data. Working with random data
         #       crash at line self.form = cgi.FieldStorage(fp=request.rfile, environ=env)
-        class_name = resource_uri
         try:
-            class_obj = self.db.getclass(class_name)
-            raise NotImplementedError
-        except KeyError:
-            pass
-
-        try:
-            class_name, item_id = hyperdb.splitDesignator(resource_uri)
-            print class_name
-            print item_id
             self.db.destroynode(class_name, item_id)
             result = 'OK'
         except IndexError:
             result = 'Error'
-        except hyperdb.DesignatorError:
-            pass
 
         return result
 
-    def action_patch(self, resource_uri, input):
+    def patch_collection(self, class_name, input):
+        raise NotImplementedError
+
+    def patch_element(self, class_name, item_id, input):
         raise NotImplementedError
 
     def dispatch(self, method, uri, input):
         print "METHOD: " + method + " URI: " + uri
         print type(input)
         pprint.pprint(input)
-
-        # PATH is split to multiple pieces
-        # 0 - rest
-        # 1 - resource
-        #
-        # Example: rest/issue - collection uri
-        # Example: rest/issue573 - element uri
-        uri_path = uri.split("/")
-        input_form = ["%s=%s" % (item.name, item.value) for item in input]
         # TODO: process input_form directly instead of making a new array
         # TODO: rest server
         # TODO: check roundup/actions.py
         # TODO: if uri_path has more than 2 child, return 404
         # TODO: custom JSONEncoder to handle other data type
         # TODO: catch all error and display error.
+
+        # PATH is split to multiple pieces
+        # 0 - rest
+        # 1 - resource
+
+        resource_uri = uri.split("/")[1]
+        input_data = ["%s=%s" % (item.name, item.value) for item in input]
+
         try:
-            output = getattr(self, "action_%s" % method.lower())(uri_path[1], input_form)
+            if resource_uri in self.db.classes:
+                output = getattr(self, "%s_collection" % method.lower())(resource_uri, input_data)
+            else:
+                class_name, item_id = hyperdb.splitDesignator(resource_uri)
+                output = getattr(self, "%s_element" % method.lower())(class_name, item_id, input_data)
+        except hyperdb.DesignatorError:
+            pass  # invalid URI
         except AttributeError:
-            raise NotImplementedError
+            raise NotImplementedError  # Error: method is invalid
 
-        print "Response Length: %s - Response Content (First 50 char): %s" %\
-              (len(output), output[:50])
+        print "Length: %s - Content(50 char): %s" % (len(output), output[:50])
         return output
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434126614 -10800
#      Fri Jun 12 19:30:14 2015 +0300
# Branch REST
# Node ID c26ec962c982622a8d7c0821a1a55efb814de6d9
# Parent  ce4f4f21ee661d83de9e42bc2a77d2416ed39fc5
use getattr instead of calling each function

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -98,6 +98,7 @@
     def action_delete(self, resource_uri, input):
         # TODO: should I allow user to delete the whole collection ?
         # TODO: BUG with DELETE without form data. Working with random data
+        #       crash at line self.form = cgi.FieldStorage(fp=request.rfile, environ=env)
         class_name = resource_uri
         try:
             class_obj = self.db.getclass(class_name)
@@ -136,24 +137,14 @@
         input_form = ["%s=%s" % (item.name, item.value) for item in input]
         # TODO: process input_form directly instead of making a new array
         # TODO: rest server
-        # TODO: use named function for this instead
         # TODO: check roundup/actions.py
         # TODO: if uri_path has more than 2 child, return 404
         # TODO: custom JSONEncoder to handle other data type
         # TODO: catch all error and display error.
-        output = "METHOD is not supported"
-        if method == "GET":
-            output = self.action_get(uri_path[1], input_form)
-        elif method == "POST":
-            output = self.action_post(uri_path[1], input_form)
-        elif method == "PUT":
-            output = self.action_put(uri_path[1], input_form)
-        elif method == "DELETE":
-            output = self.action_delete(uri_path[1], input_form)
-        elif method == "PATCH":
-            output = self.action_patch(uri_path[1], input_form)
-        else:
-            pass
+        try:
+            output = getattr(self, "action_%s" % method.lower())(uri_path[1], input_form)
+        except AttributeError:
+            raise NotImplementedError
 
         print "Response Length: %s - Response Content (First 50 char): %s" %\
               (len(output), output[:50])
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434124520 -10800
#      Fri Jun 12 18:55:20 2015 +0300
# Branch REST
# Node ID ce4f4f21ee661d83de9e42bc2a77d2416ed39fc5
# Parent  87bc5b4679c7e4906ba695aa3e19bc42cb07e991
Added POST and DELETE>

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -9,10 +9,12 @@
 import pprint
 from roundup import hyperdb
 from roundup.cgi.templating import Unauthorised
+from roundup import xmlrpc
 
 
 class RestfulInstance(object):
     """Dummy Handler for REST
+       WARNING: Very ugly !!!!, cleaned & better organized in progress (next commit)
     """
 
     def __init__(self, db):
@@ -25,19 +27,17 @@
         try:
             class_obj = self.db.getclass(class_name)
             """prop_name = class_obj.labelprop()
-            result = [class_obj.get(item_id, prop_name)
-                      for item_id in class_obj.list()
-                      if self.db.security.hasPermission('View', self.db.getuid(),
-                                                        class_name, prop_name, item_id)
-                      ]
-            result = json.JSONEncoder().encode(result)"""
+            result = [class_obj.get(item_id, prop_name)"""
             result = [{'id': item_id}
                       for item_id in class_obj.list()
-                      if self.db.security.hasPermission('View', self.db.getuid(),
-                                                        class_name, None, item_id)
+                      if self.db.security.hasPermission('View',
+                                                        self.db.getuid(),
+                                                        class_name,
+                                                        None,
+                                                        item_id)
                       ]
             result = json.JSONEncoder().encode(result)
-            #result = `len(dict(result))` + ' ' + `len(result)`
+            # result = `len(dict(result))` + ' ' + `len(result)`
         except KeyError:
             pass
 
@@ -48,19 +48,78 @@
             props.sort()
             result = [(prop_name, class_obj.get(item_id, prop_name))
                       for prop_name in props
-                      if self.db.security.hasPermission('View', self.db.getuid(),
-                                                        class_name, prop_name, item_id)
+                      if self.db.security.hasPermission('View',
+                                                        self.db.getuid(),
+                                                        class_name,
+                                                        prop_name,
+                                                        item_id)
                       ]
             # Note: is this a bug by having an extra indent in xmlrpc ?
             result = json.JSONEncoder().encode(dict(result))
         except hyperdb.DesignatorError:
             pass
 
-        # print type(result)
-        # print type(dict(result))
         return result
-        # return json.dumps(dict(result))
-        # return dict(result)
+
+    def action_post(self, resource_uri, input):
+        class_name = resource_uri
+
+        if not self.db.security.hasPermission('Create', self.db.getuid(),
+                                              class_name):
+            raise Unauthorised('Permission to create %s denied' % class_name)
+
+        class_obj = self.db.getclass(class_name)
+
+        # convert types
+        props = xmlrpc.props_from_args(self.db, class_obj, input)
+
+        # check for the key property
+        key = class_obj.getkey()
+        if key and key not in props:
+            raise xmlrpc.UsageError, 'Must provide the "%s" property.' % key
+
+        for key in props:
+            if not self.db.security.hasPermission('Create', self.db.getuid(),
+                                                  class_name, property=key):
+                raise Unauthorised('Permission to create %s.%s denied' %
+                                   (class_name, key))
+
+        # do the actual create
+        try:
+            result = class_obj.create(**props)
+            self.db.commit()
+        except (TypeError, IndexError, ValueError), message:
+            raise xmlrpc.UsageError, message
+        return result
+
+    def action_put(self, resource_uri, input):
+        raise NotImplementedError
+
+    def action_delete(self, resource_uri, input):
+        # TODO: should I allow user to delete the whole collection ?
+        # TODO: BUG with DELETE without form data. Working with random data
+        class_name = resource_uri
+        try:
+            class_obj = self.db.getclass(class_name)
+            raise NotImplementedError
+        except KeyError:
+            pass
+
+        try:
+            class_name, item_id = hyperdb.splitDesignator(resource_uri)
+            print class_name
+            print item_id
+            self.db.destroynode(class_name, item_id)
+            result = 'OK'
+        except IndexError:
+            result = 'Error'
+        except hyperdb.DesignatorError:
+            pass
+
+        return result
+
+    def action_patch(self, resource_uri, input):
+        raise NotImplementedError
 
     def dispatch(self, method, uri, input):
         print "METHOD: " + method + " URI: " + uri
@@ -74,22 +133,28 @@
         # Example: rest/issue - collection uri
         # Example: rest/issue573 - element uri
         uri_path = uri.split("/")
+        input_form = ["%s=%s" % (item.name, item.value) for item in input]
+        # TODO: process input_form directly instead of making a new array
+        # TODO: rest server
         # TODO: use named function for this instead
         # TODO: check roundup/actions.py
         # TODO: if uri_path has more than 2 child, return 404
+        # TODO: custom JSONEncoder to handle other data type
+        # TODO: catch all error and display error.
         output = "METHOD is not supported"
         if method == "GET":
-            output = self.action_get(uri_path[1], input)
+            output = self.action_get(uri_path[1], input_form)
         elif method == "POST":
-            pass
+            output = self.action_post(uri_path[1], input_form)
         elif method == "PUT":
-            pass
+            output = self.action_put(uri_path[1], input_form)
         elif method == "DELETE":
-            pass
+            output = self.action_delete(uri_path[1], input_form)
         elif method == "PATCH":
-            pass
+            output = self.action_patch(uri_path[1], input_form)
         else:
             pass
 
-        print "Response Length: " + `len(output)` + " - Response Content (First 50 char): " + output[:50]
+        print "Response Length: %s - Response Content (First 50 char): %s" %\
+              (len(output), output[:50])
         return output
diff --git a/roundup/scripts/roundup_server.py b/roundup/scripts/roundup_server.py
--- a/roundup/scripts/roundup_server.py
+++ b/roundup/scripts/roundup_server.py
@@ -251,7 +251,7 @@
         else:
             return self.run_cgi()
 
-    do_GET = do_POST = do_HEAD = run_cgi_outer
+    do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = do_PATCH = run_cgi_outer
 
     def index(self):
         ''' Print up an index of the available trackers
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1434065314 -10800
#      Fri Jun 12 02:28:34 2015 +0300
# Branch REST
# Node ID 87bc5b4679c7e4906ba695aa3e19bc42cb07e991
# Parent  cff5b11df2488fbcdfe84228c3a9beb0214a6fd3
Recognize both GET element uri and collection uri

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -19,27 +19,51 @@
         # TODO: database, translator and instance.actions
         self.db = db
 
-    def action_get(self, resource, input):
-        classname, itemid = hyperdb.splitDesignator(resource)
-        cl = self.db.getclass(classname)
-        props = cl.properties.keys()
-        props.sort()
-        for p in props:
-            if not self.db.security.hasPermission('View', self.db.getuid(),
-                                                  classname, p, itemid):
-                raise Unauthorised('Permission to view %s of %s denied' %
-                                   (p, resource))
-            result = [(prop, cl.get(itemid, prop)) for prop in props]
+    def action_get(self, resource_uri, input):
+        # TODO: split this into collection URI and resource URI
+        class_name = resource_uri
+        try:
+            class_obj = self.db.getclass(class_name)
+            """prop_name = class_obj.labelprop()
+            result = [class_obj.get(item_id, prop_name)
+                      for item_id in class_obj.list()
+                      if self.db.security.hasPermission('View', self.db.getuid(),
+                                                        class_name, prop_name, item_id)
+                      ]
+            result = json.JSONEncoder().encode(result)"""
+            result = [{'id': item_id}
+                      for item_id in class_obj.list()
+                      if self.db.security.hasPermission('View', self.db.getuid(),
+                                                        class_name, None, item_id)
+                      ]
+            result = json.JSONEncoder().encode(result)
+            #result = `len(dict(result))` + ' ' + `len(result)`
+        except KeyError:
+            pass
+
+        try:
+            class_name, item_id = hyperdb.splitDesignator(resource_uri)
+            class_obj = self.db.getclass(class_name)
+            props = class_obj.properties.keys()
+            props.sort()
+            result = [(prop_name, class_obj.get(item_id, prop_name))
+                      for prop_name in props
+                      if self.db.security.hasPermission('View', self.db.getuid(),
+                                                        class_name, prop_name, item_id)
+                      ]
+            # Note: is this a bug by having an extra indent in xmlrpc ?
+            result = json.JSONEncoder().encode(dict(result))
+        except hyperdb.DesignatorError:
+            pass
 
         # print type(result)
         # print type(dict(result))
-        return json.JSONEncoder().encode(dict(result))
+        return result
         # return json.dumps(dict(result))
         # return dict(result)
 
     def dispatch(self, method, uri, input):
-        print method
-        print uri
+        print "METHOD: " + method + " URI: " + uri
         print type(input)
         pprint.pprint(input)
 
@@ -67,6 +91,5 @@
         else:
             pass
 
-        print output
-        print len(output)
+        print "Response Length: " + `len(output)` + " - Response Content (First 50 char): " + output[:50]
         return output
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1433959927 -10800
#      Wed Jun 10 21:12:07 2015 +0300
# Branch REST
# Node ID cff5b11df2488fbcdfe84228c3a9beb0214a6fd3
# Parent  83209b3047a9ba3138ae0cac13bd561f3134bbc1
Implement getting resource from database

diff --git a/roundup/rest.py b/roundup/rest.py
--- a/roundup/rest.py
+++ b/roundup/rest.py
@@ -1,11 +1,14 @@
-# Restful API for Roundup
-#
-# This module is free software, you may redistribute it
-# and/or modify under the same terms as Python.
-#
+"""
+Restful API for Roundup
+
+This module is free software, you may redistribute it
+and/or modify under the same terms as Python.
+"""
 
 import json
 import pprint
+from roundup import hyperdb
+from roundup.cgi.templating import Unauthorised
 
 
 class RestfulInstance(object):
@@ -16,9 +19,54 @@
         # TODO: database, translator and instance.actions
         self.db = db
 
+    def action_get(self, resource, input):
+        classname, itemid = hyperdb.splitDesignator(resource)
+        cl = self.db.getclass(classname)
+        props = cl.properties.keys()
+        props.sort()
+        for p in props:
+            if not self.db.security.hasPermission('View', self.db.getuid(),
+                                                  classname, p, itemid):
+                raise Unauthorised('Permission to view %s of %s denied' %
+                                   (p, resource))
+            result = [(prop, cl.get(itemid, prop)) for prop in props]
+
+        # print type(result)
+        # print type(dict(result))
+        return json.JSONEncoder().encode(dict(result))
+        # return json.dumps(dict(result))
+        # return dict(result)
+
     def dispatch(self, method, uri, input):
         print method
         print uri
         print type(input)
         pprint.pprint(input)
-        return ' '.join([method, uri, pprint.pformat(input)])
+
+        # PATH is split to multiple pieces
+        # 0 - rest
+        # 1 - resource
+        #
+        # Example: rest/issue - collection uri
+        # Example: rest/issue573 - element uri
+        uri_path = uri.split("/")
+        # TODO: use named function for this instead
+        # TODO: check roundup/actions.py
+        # TODO: if uri_path has more than 2 child, return 404
+        output = "METHOD is not supported"
+        if method == "GET":
+            output = self.action_get(uri_path[1], input)
+        elif method == "POST":
+            pass
+        elif method == "PUT":
+            pass
+        elif method == "DELETE":
+            pass
+        elif method == "PATCH":
+            pass
+        else:
+            pass
+
+        print output
+        print len(output)
+        return output
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1433527399 -10800
#      Fri Jun 05 21:03:19 2015 +0300
# Branch REST
# Node ID 83209b3047a9ba3138ae0cac13bd561f3134bbc1
# Parent  d10821d84c4cc0c0e60bf6e38b170eed1b9a990f
Added RestInstance and calling rest from client.py

diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
--- a/roundup/cgi/client.py
+++ b/roundup/cgi/client.py
@@ -22,6 +22,7 @@
 from roundup.mailer import Mailer, MessageSendError, encode_quopri
 from roundup.cgi import accept_language
 from roundup import xmlrpc
+from roundup import rest
 
 from roundup.anypy.cookie_ import CookieError, BaseCookie, SimpleCookie, \
     get_cookie_date
@@ -378,6 +379,8 @@
         try:
             if self.path == 'xmlrpc':
                 self.handle_xmlrpc()
+            elif self.path == 'rest' or self.path[:5] == 'rest/':
+                self.handle_rest()
             else:
                 self.inner_main()
         finally:
@@ -419,6 +422,27 @@
         self.setHeader("Content-Length", str(len(output)))
         self.write(output)
 
+    def handle_rest(self):
+        # Pull the parameters data out of the form.  The "value" attribute
+        # will be the raw content of the request.
+        input = self.form.value
+
+        # Set the charset and language
+        self.determine_charset()
+        self.determine_language()
+        # Open the database as the correct user.
+        # TODO: add everything to RestfulDispatcher
+        self.determine_user()
+        self.check_anonymous_access()
+
+        # Call rest library to handle the request
+        handler = rest.RestfulInstance(self.db)
+        output = handler.dispatch(self.env['REQUEST_METHOD'], self.path, input)
+
+        # self.setHeader("Content-Type", "text/xml")
+        self.setHeader("Content-Length", str(len(output)))
+        self.write(output)
+
     def add_ok_message(self, msg, escape=True):
         add_message(self._ok_message, msg, escape)
 
diff --git a/roundup/rest.py b/roundup/rest.py
new file mode 100644
--- /dev/null
+++ b/roundup/rest.py
@@ -0,0 +1,24 @@
+# Restful API for Roundup
+#
+# This module is free software, you may redistribute it
+# and/or modify under the same terms as Python.
+#
+
+import json
+import pprint
+
+
+class RestfulInstance(object):
+    """Dummy Handler for REST
+    """
+
+    def __init__(self, db):
+        # TODO: database, translator and instance.actions
+        self.db = db
+
+    def dispatch(self, method, uri, input):
+        print method
+        print uri
+        print type(input)
+        pprint.pprint(input)
+        return ' '.join([method, uri, pprint.pformat(input)])
# HG changeset patch
# User Chau Nguyen <dangchau1991@yahoo.com>
# Date 1433304370 -10800
#      Wed Jun 03 07:06:10 2015 +0300
# Branch REST
# Node ID d10821d84c4cc0c0e60bf6e38b170eed1b9a990f
# Parent  b73fc3e5ee808fd4cda267222eb31997c59b9f1a
Create REST branch for RESTful API

