18
18
# Author: Matt Giuca, Will Grant, Nick Chadwick
29
import simplejson as json
31
26
import genshi.template
33
28
from ivle.webapp.base.views import BaseView
34
from ivle.webapp.base.xhtml import GenshiLoaderMixin
35
29
from ivle.webapp.errors import BadRequest, MethodNotAllowed, Unauthorized
37
31
class RESTView(BaseView):
61
55
if not hasattr(op, '_rest_api_permission'):
62
56
raise Unauthorized()
64
if (op._rest_api_permission not in
65
self.get_permissions(req.user, req.config)):
58
if op._rest_api_permission not in self.get_permissions(req.user):
66
59
raise Unauthorized()
68
61
def convert_bool(self, value):
78
71
raise MethodNotAllowed(allowed=self._allowed_methods)
80
73
if req.method == 'GET':
81
qargs = dict(cgi.parse_qsl(
82
urlparse.urlparse(req.unparsed_uri).query,
84
if 'ivle.op' in qargs:
85
outjson = self._named_operation(req, qargs, readonly=True)
87
self.authorize_method(req, self.GET)
88
outjson = self.GET(req)
74
self.authorize_method(req, self.GET)
75
outjson = self.GET(req)
89
76
# Since PATCH isn't yet an official HTTP method, we allow users to
90
77
# turn a PUT into a PATCH by supplying a special header.
91
78
elif req.method == 'PATCH' or (req.method == 'PUT' and
93
80
req.headers_in['X-IVLE-Patch-Semantics'].lower() == 'yes'):
94
81
self.authorize_method(req, self.PATCH)
96
input = json.loads(req.read())
83
input = cjson.decode(req.read())
84
except cjson.DecodeError:
98
85
raise BadRequest('Invalid JSON data')
99
86
outjson = self.PATCH(req, input)
100
87
elif req.method == 'PUT':
101
88
self.authorize_method(req, self.PUT)
103
input = json.loads(req.read())
90
input = cjson.decode(req.read())
91
except cjson.DecodeError:
105
92
raise BadRequest('Invalid JSON data')
106
93
outjson = self.PUT(req, input)
107
94
# POST implies named operation.
109
96
# TODO: Check Content-Type and implement multipart/form-data.
111
98
opargs = dict(cgi.parse_qsl(data, keep_blank_values=1))
112
outjson = self._named_operation(req, opargs)
100
opname = opargs['ivle.op']
101
del opargs['ivle.op']
103
raise BadRequest('No named operation specified.')
106
op = getattr(self, opname)
107
except AttributeError:
108
raise BadRequest('Invalid named operation.')
110
if not hasattr(op, '_rest_api_callable') or \
111
not op._rest_api_callable:
112
raise BadRequest('Invalid named operation.')
114
self.authorize_method(req, op)
116
# Find any missing arguments, except for the first two (self, req)
117
(args, vaargs, varkw, defaults) = inspect.getargspec(op)
120
# To find missing arguments, we eliminate the provided arguments
121
# from the set of remaining function signature arguments. If the
122
# remaining signature arguments are in the args[-len(defaults):],
124
unspec = set(args) - set(opargs.keys())
125
if unspec and not defaults:
126
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
128
unspec = [k for k in unspec if k not in args[-len(defaults):]]
131
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
133
# We have extra arguments if the are no match args in the function
134
# signature, AND there is no **.
135
extra = set(opargs.keys()) - set(args)
136
if extra and not varkw:
137
raise BadRequest('Extra arguments: ' + ', '.join(extra))
139
outjson = op(req, **opargs)
114
141
req.content_type = self.content_type
115
142
self.write_json(req, outjson)
117
144
#This is a separate function to allow additional data to be passed through
118
145
def write_json(self, req, outjson):
119
146
if outjson is not None:
120
req.write(json.dumps(outjson))
147
req.write(cjson.encode(outjson))
123
def _named_operation(self, req, opargs, readonly=False):
125
opname = opargs['ivle.op']
126
del opargs['ivle.op']
128
raise BadRequest('No named operation specified.')
131
op = getattr(self, opname)
132
except AttributeError:
133
raise BadRequest('Invalid named operation.')
135
if not hasattr(op, '_rest_api_callable') or \
136
not op._rest_api_callable:
137
raise BadRequest('Invalid named operation.')
139
if readonly and op._rest_api_write_operation:
140
raise BadRequest('POST required for write operation.')
142
self.authorize_method(req, op)
144
# Find any missing arguments, except for the first two (self, req)
145
(args, vaargs, varkw, defaults) = inspect.getargspec(op)
148
# To find missing arguments, we eliminate the provided arguments
149
# from the set of remaining function signature arguments. If the
150
# remaining signature arguments are in the args[-len(defaults):],
152
unspec = set(args) - set(opargs.keys())
153
if unspec and not defaults:
154
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
156
unspec = [k for k in unspec if k not in args[-len(defaults):]]
159
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
161
# We have extra arguments if the are no match args in the function
162
# signature, AND there is no **.
163
extra = set(opargs.keys()) - set(args)
164
if extra and not varkw:
165
raise BadRequest('Extra arguments: ' + ', '.join(extra))
167
return op(req, **opargs)
170
class XHTMLRESTView(GenshiLoaderMixin, JSONRESTView):
151
class XHTMLRESTView(JSONRESTView):
171
152
"""A special type of RESTView which takes enhances the standard JSON
172
153
with genshi XHTML functions.
183
164
rest_template = os.path.join(os.path.dirname(
184
165
inspect.getmodule(self).__file__), self.template)
185
tmpl = self._loader.load(rest_template)
166
loader = genshi.template.TemplateLoader(".", auto_reload=True)
167
tmpl = loader.load(rest_template)
187
169
return tmpl.generate(self.ctx).render('xhtml', doctype='xhtml')
189
171
# This renders the template and adds it to the json
190
172
def write_json(self, req, outjson):
191
173
outjson["html"] = self.render_fragment()
192
req.write(json.dumps(outjson))
174
req.write(cjson.encode(outjson))
195
class _named_operation(object):
177
class named_operation(object):
196
178
'''Declare a function to be accessible to HTTP users via the REST API.
198
def __init__(self, write_operation, permission):
199
self.write_operation = write_operation
180
def __init__(self, permission):
200
181
self.permission = permission
202
183
def __call__(self, func):
203
184
func._rest_api_callable = True
204
func._rest_api_write_operation = self.write_operation
205
185
func._rest_api_permission = self.permission
208
write_operation = functools.partial(_named_operation, True)
209
read_operation = functools.partial(_named_operation, False)
211
188
class require_permission(object):
212
189
'''Declare the permission required for use of a method via the REST API.