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