Roundup Tracker

Attachment 'roundupRestfulAPI.patch'

Download

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

Attached Files

To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.
  • [get | view] (2018-05-26 02:54:27, 217.6 KB) [[attachment:roundupRestfulAPI.patch]]
 All files | Selected Files: delete move to page copy to page

You are not allowed to attach a file to this page.