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):
42
36
content_type = "application/octet-stream"
38
def __init__(self, req, *args, **kwargs):
40
setattr(self, key, kwargs[key])
44
42
def render(self, req):
45
43
raise NotImplementedError()
61
59
if not hasattr(op, '_rest_api_permission'):
62
60
raise Unauthorized()
64
if (op._rest_api_permission not in
65
self.get_permissions(req.user, req.config)):
62
if op._rest_api_permission not in self.get_permissions(req.user):
66
63
raise Unauthorized()
68
65
def convert_bool(self, value):
78
75
raise MethodNotAllowed(allowed=self._allowed_methods)
80
77
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)
78
self.authorize_method(req, self.GET)
79
outjson = self.GET(req)
89
80
# Since PATCH isn't yet an official HTTP method, we allow users to
90
81
# turn a PUT into a PATCH by supplying a special header.
91
82
elif req.method == 'PATCH' or (req.method == 'PUT' and
93
84
req.headers_in['X-IVLE-Patch-Semantics'].lower() == 'yes'):
94
85
self.authorize_method(req, self.PATCH)
96
input = json.loads(req.read())
87
input = cjson.decode(req.read())
88
except cjson.DecodeError:
98
89
raise BadRequest('Invalid JSON data')
99
90
outjson = self.PATCH(req, input)
100
91
elif req.method == 'PUT':
101
92
self.authorize_method(req, self.PUT)
103
input = json.loads(req.read())
94
input = cjson.decode(req.read())
95
except cjson.DecodeError:
105
96
raise BadRequest('Invalid JSON data')
106
97
outjson = self.PUT(req, input)
107
98
# POST implies named operation.
109
100
# TODO: Check Content-Type and implement multipart/form-data.
110
101
data = req.read()
111
102
opargs = dict(cgi.parse_qsl(data, keep_blank_values=1))
112
outjson = self._named_operation(req, opargs)
104
opname = opargs['ivle.op']
105
del opargs['ivle.op']
107
raise BadRequest('No named operation specified.')
110
op = getattr(self, opname)
111
except AttributeError:
112
raise BadRequest('Invalid named operation.')
114
if not hasattr(op, '_rest_api_callable') or \
115
not op._rest_api_callable:
116
raise BadRequest('Invalid named operation.')
118
self.authorize_method(req, op)
120
# Find any missing arguments, except for the first two (self, req)
121
(args, vaargs, varkw, defaults) = inspect.getargspec(op)
124
# To find missing arguments, we eliminate the provided arguments
125
# from the set of remaining function signature arguments. If the
126
# remaining signature arguments are in the args[-len(defaults):],
128
unspec = set(args) - set(opargs.keys())
129
if unspec and not defaults:
130
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
132
unspec = [k for k in unspec if k not in args[-len(defaults):]]
135
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
137
# We have extra arguments if the are no match args in the function
138
# signature, AND there is no **.
139
extra = set(opargs.keys()) - set(args)
140
if extra and not varkw:
141
raise BadRequest('Extra arguments: ' + ', '.join(extra))
143
outjson = op(req, **opargs)
114
145
req.content_type = self.content_type
115
146
self.write_json(req, outjson)
117
148
#This is a separate function to allow additional data to be passed through
118
149
def write_json(self, req, outjson):
119
150
if outjson is not None:
120
req.write(json.dumps(outjson))
151
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):
155
class XHTMLRESTView(JSONRESTView):
171
156
"""A special type of RESTView which takes enhances the standard JSON
172
157
with genshi XHTML functions.
177
162
ctx = genshi.template.Context()
164
def __init__(self, req, *args, **kwargs):
166
setattr(self, key, kwargs[key])
179
168
def render_fragment(self):
180
169
if self.template is None:
181
170
raise NotImplementedError()
183
172
rest_template = os.path.join(os.path.dirname(
184
173
inspect.getmodule(self).__file__), self.template)
185
tmpl = self._loader.load(rest_template)
174
loader = genshi.template.TemplateLoader(".", auto_reload=True)
175
tmpl = loader.load(rest_template)
187
177
return tmpl.generate(self.ctx).render('xhtml', doctype='xhtml')
189
179
# This renders the template and adds it to the json
190
180
def write_json(self, req, outjson):
191
181
outjson["html"] = self.render_fragment()
192
req.write(json.dumps(outjson))
182
req.write(cjson.encode(outjson))
195
class _named_operation(object):
185
class named_operation(object):
196
186
'''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
188
def __init__(self, permission):
200
189
self.permission = permission
202
191
def __call__(self, func):
203
192
func._rest_api_callable = True
204
func._rest_api_write_operation = self.write_operation
205
193
func._rest_api_permission = self.permission
208
write_operation = functools.partial(_named_operation, True)
209
read_operation = functools.partial(_named_operation, False)
211
196
class require_permission(object):
212
197
'''Declare the permission required for use of a method via the REST API.