From af313ca07bc607163336e1df98990fbbd12f2941 Mon Sep 17 00:00:00 2001
From: Gunnar Schaefer <gsfr@stanford.edu>
Date: Mon, 10 Aug 2015 17:23:57 -0700
Subject: [PATCH] first pass at proper group schema

delivered at:
 - GET /schema/group => POST schema
 - GET /schema/group?method=put => PUT schema
---
 api.py      | 17 ++++++++++++++-
 groups.json | 51 +++++++++++++++++++++++++++++++++++++++++++++
 users.py    | 60 ++++++++++++++++-------------------------------------
 3 files changed, 85 insertions(+), 43 deletions(-)
 create mode 100644 groups.json

diff --git a/api.py b/api.py
index f931cc8d..191b1b12 100644
--- a/api.py
+++ b/api.py
@@ -1,4 +1,5 @@
 import os
+import copy
 import json
 import webapp2
 import bson.json_util
@@ -23,6 +24,9 @@ routes = [
         webapp2.Route(r'/sites',                                    core.Core, handler_method='sites', methods=['GET']),
         webapp2.Route(r'/search',                                   core.Core, handler_method='search', methods=['GET', 'POST']),
     ]),
+    webapp2_extras.routes.PathPrefixRoute(r'/api/schema', [
+        webapp2.Route(r'/group',                                    users.Group, handler_method='schema', methods=['GET']),
+    ]),
     webapp2.Route(r'/api/users',                                    users.Users),
     webapp2_extras.routes.PathPrefixRoute(r'/api/users', [
         webapp2.Route(r'/count',                                    users.Users, handler_method='count', methods=['GET']),
@@ -36,7 +40,6 @@ routes = [
     webapp2.Route(r'/api/groups',                                   users.Groups),
     webapp2_extras.routes.PathPrefixRoute(r'/api/groups', [
         webapp2.Route(r'/count',                                    users.Groups, handler_method='count', methods=['GET']),
-        webapp2.Route(r'/schema',                                   users.Group, handler_method='schema', methods=['GET']),
         webapp2.Route(r'/<:[^/]+>',                                 users.Group, name='group'),
         webapp2.Route(r'/<gid:[^/]+>/projects',                     projects.Projects, name='g_projects'),
         webapp2.Route(r'/<gid:[^/]+>/sessions',                     sessions.Sessions, name='g_sessions', methods=['GET']),
@@ -93,11 +96,23 @@ routes = [
     ]),
 ]
 
+
+for cls, schema_file in [(cls, os.path.join(os.path.dirname(__file__), schema_file)) for cls, schema_file in [
+        (users.Group, 'groups.json'),
+        ]]:
+    with open(schema_file) as fp:
+        cls.post_schema = json.load(fp)
+    cls.put_schema = copy.deepcopy(cls.post_schema)
+    cls.put_schema['properties'].pop('_id')
+    cls.put_schema.pop('required')
+
+
 def dispatcher(router, request, response):
     rv = router.default_dispatcher(request, response)
     if rv is not None:
         response.write(json.dumps(rv, default=bson.json_util.default))
         response.headers['Content-Type'] = 'application/json; charset=utf-8'
 
+
 app = webapp2.WSGIApplication(routes)
 app.router.set_dispatcher(dispatcher)
diff --git a/groups.json b/groups.json
new file mode 100644
index 00000000..757c2ec9
--- /dev/null
+++ b/groups.json
@@ -0,0 +1,51 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "properties": {
+        "_id": {
+            "minLength": 2,
+            "maxLength": 32,
+            "pattern": "^[0-9a-z_-]*$",
+            "title": "ID",
+            "type": "string"
+        },
+        "name": {
+            "minLength": 2,
+            "maxLength": 32,
+            "pattern": "^[0-9A-Za-z _-]*$",
+            "title": "Name",
+            "type": "string"
+        },
+        "roles": {
+            "default": [],
+            "items": {
+                "additionalProperties": false,
+                "properties": {
+                    "_id": {
+                        "type": "string"
+                    },
+                    "access": {
+                        "enum": [
+                            "ro",
+                            "rw",
+                            "admin"
+                        ],
+                        "type": "string"
+                    }
+                },
+                "required": [
+                    "access",
+                    "_id"
+                ],
+                "type": "object"
+            },
+            "title": "Roles",
+            "type": "array",
+            "uniqueItems": true
+        }
+    },
+    "required": [
+        "_id"
+    ],
+    "title": "Group",
+    "type": "object"
+}
diff --git a/users.py b/users.py
index 4829d4ee..4070a2d9 100644
--- a/users.py
+++ b/users.py
@@ -6,6 +6,7 @@ log = logging.getLogger('scitran.api')
 import copy
 import hashlib
 import pymongo
+import datetime
 import jsonschema
 
 import base
@@ -194,7 +195,10 @@ class Groups(base.RequestHandler):
             self.abort(403, 'must be logged in and superuser to create new group')
         try:
             json_body = self.request.json_body
-            jsonschema.validate(json_body, Group.json_schema)
+            jsonschema.validate(json_body, Group.post_schema)
+            json_body['created'] = datetime.datetime.utcnow()
+            json_body['modified'] = datetime.datetime.utcnow()
+            json_body.setdefault('roles', [])
             self.dbc.insert(json_body)
         except (ValueError, jsonschema.ValidationError) as e:
             self.abort(400, str(e))
@@ -218,6 +222,9 @@ class Groups(base.RequestHandler):
                     query = {'roles._id': self.uid}
                 projection += ['roles.$']
         groups = list(self.app.db.groups.find(query, projection))
+        #for group in groups:
+        #    group['created'], _ = util.format_timestamp(group['created']) # TODO json serializer should do this
+        #    group['modified'], _ = util.format_timestamp(group['modified']) # TODO json serializer should do this
         if self.debug:
             for group in groups:
                 group['debug'] = {}
@@ -231,48 +238,16 @@ class Group(base.RequestHandler):
 
     """/groups/<_id>"""
 
-    json_schema = {
-        '$schema': 'http://json-schema.org/draft-04/schema#',
-        'title': 'Group',
-        'type': 'object',
-        'properties': {
-            '_id': {
-                'title': 'Group ID',
-                'type': 'string',
-            },
-            'name': {
-                'title': 'Name',
-                'type': 'string',
-                'maxLength': 32,
-            },
-            'roles': {
-                'title': 'Roles',
-                'type': 'array',
-                'default': [],
-                'items': {
-                    'type': 'object',
-                    'properties': {
-                        'access': {
-                            'type': 'string',
-                            'enum': [role['rid'] for role in ROLES],
-                        },
-                        '_id': {
-                            'type': 'string',
-                        },
-                    },
-                    'required': ['access', '_id'],
-                    'additionalProperties': False,
-                },
-                'uniqueItems': True,
-            },
-        },
-        'required': ['_id'],
-    }
-
     def __init__(self, request=None, response=None):
         super(Group, self).__init__(request, response)
         self.dbc = self.app.db.groups
 
+    def schema(self):
+        method =self.request.GET.get('method', '').lower()
+        if method == 'put':
+            return self.put_schema
+        return self.post_schema
+
     def get(self, _id):
         """Return Group details."""
         group = self.app.db.groups.find_one({'_id': _id})
@@ -282,6 +257,9 @@ class Group(base.RequestHandler):
             group = self.app.db.groups.find_one({'_id': _id, 'roles': {'$elemMatch': {'_id': self.uid, 'access': 'admin'}}})
             if not group:
                 self.abort(403, 'User ' + self.uid + ' is not an admin of Group ' + _id)
+        if 'created' in group and 'modified' in group:
+            group['created'], _ = util.format_timestamp(group['created']) # TODO json serializer should do this
+            group['modified'], _ = util.format_timestamp(group['modified']) # TODO json serializer should do this
         if self.debug:
             group['debug'] = {}
             group['debug']['projects'] = self.uri_for('g_projects', gid=group['_id'], _full=True) + '?' + self.request.query_string
@@ -296,11 +274,9 @@ class Group(base.RequestHandler):
         user_perm = util.user_perm(group.get('roles', []), self.uid)
         if not self.superuser_request and not user_perm.get('access') == 'admin':
             self.abort(403, 'must be superuser or group admin to update group')
-        schema = copy.deepcopy(self.json_schema)
-        del schema['required']
         try:
             json_body = self.request.json_body
-            jsonschema.validate(json_body, schema)
+            jsonschema.validate(json_body, self.put_schema)
         except (ValueError, jsonschema.ValidationError) as e:
             self.abort(400, str(e))
         self.dbc.update({'_id': _id}, {'$set': util.mongo_dict(json_body)})
-- 
GitLab