1
# IVLE - Informatics Virtual Learning Environment
2
# Copyright (C) 2007-2009 The University of Melbourne
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# Author: Matt Giuca, Will Grant, Nick Chadwick
29
import simplejson as json
31
import genshi.template
33
from ivle.webapp.base.views import BaseView
34
from ivle.webapp.base.xhtml import GenshiLoaderMixin
35
from ivle.webapp.errors import BadRequest, MethodNotAllowed, Unauthorized
37
class RESTView(BaseView):
39
A view which provides a RESTful interface. The content type is
40
unspecified (see JSONRESTView for a specific content type).
42
content_type = "application/octet-stream"
44
def render(self, req):
45
raise NotImplementedError()
47
class JSONRESTView(RESTView):
49
A special case of RESTView which deals entirely in JSON.
51
content_type = "application/json"
53
_allowed_methods = property(
54
lambda self: [m for m in ('GET', 'PUT', 'PATCH')
55
if hasattr(self, m)] + ['POST'])
57
def authorize(self, req):
58
return True # Real authz performed in render().
60
def authorize_method(self, req, op):
61
if not hasattr(op, '_rest_api_permission'):
64
if (op._rest_api_permission not in
65
self.get_permissions(req.user, req.config)):
68
def convert_bool(self, value):
69
if value in ('True', 'true', True):
71
elif value in ('False', 'false', False):
76
def render(self, req):
77
if req.method not in self._allowed_methods:
78
raise MethodNotAllowed(allowed=self._allowed_methods)
80
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)
89
# Since PATCH isn't yet an official HTTP method, we allow users to
90
# turn a PUT into a PATCH by supplying a special header.
91
elif req.method == 'PATCH' or (req.method == 'PUT' and
92
'X-IVLE-Patch-Semantics' in req.headers_in and
93
req.headers_in['X-IVLE-Patch-Semantics'].lower() == 'yes'):
94
self.authorize_method(req, self.PATCH)
96
input = json.loads(req.read())
98
raise BadRequest('Invalid JSON data')
99
outjson = self.PATCH(req, input)
100
elif req.method == 'PUT':
101
self.authorize_method(req, self.PUT)
103
input = json.loads(req.read())
105
raise BadRequest('Invalid JSON data')
106
outjson = self.PUT(req, input)
107
# POST implies named operation.
108
elif req.method == 'POST':
109
# TODO: Check Content-Type and implement multipart/form-data.
111
opargs = dict(cgi.parse_qsl(data, keep_blank_values=1))
112
outjson = self._named_operation(req, opargs)
114
req.content_type = self.content_type
115
self.write_json(req, outjson)
117
#This is a separate function to allow additional data to be passed through
118
def write_json(self, req, outjson):
119
if outjson is not None:
120
req.write(json.dumps(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):
171
"""A special type of RESTView which takes enhances the standard JSON
172
with genshi XHTML functions.
174
XHTMLRESTViews should have a template, which is rendered using their
175
context. This is returned in the JSON as 'html'"""
177
ctx = genshi.template.Context()
179
def render_fragment(self):
180
if self.template is None:
181
raise NotImplementedError()
183
rest_template = os.path.join(os.path.dirname(
184
inspect.getmodule(self).__file__), self.template)
185
tmpl = self._loader.load(rest_template)
187
return tmpl.generate(self.ctx).render('xhtml', doctype='xhtml')
189
# This renders the template and adds it to the json
190
def write_json(self, req, outjson):
191
outjson["html"] = self.render_fragment()
192
req.write(json.dumps(outjson))
195
class _named_operation(object):
196
'''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
200
self.permission = permission
202
def __call__(self, func):
203
func._rest_api_callable = True
204
func._rest_api_write_operation = self.write_operation
205
func._rest_api_permission = self.permission
208
write_operation = functools.partial(_named_operation, True)
209
read_operation = functools.partial(_named_operation, False)
211
class require_permission(object):
212
'''Declare the permission required for use of a method via the REST API.
214
def __init__(self, permission):
215
self.permission = permission
217
def __call__(self, func):
218
func._rest_api_permission = self.permission