15
15
# along with this program; if not, write to the Free Software
16
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# Author: Matt Giuca, Will Grant
18
# Author: Matt Giuca, Will Grant, Nick Chadwick
26
import genshi.template
25
28
from ivle.webapp.base.views import BaseView
26
from ivle.webapp.errors import BadRequest, MethodNotAllowed
29
from ivle.webapp.errors import BadRequest, MethodNotAllowed, Unauthorized
28
31
class RESTView(BaseView):
33
36
content_type = "application/octet-stream"
35
def __init__(self, req, *args, **kwargs):
38
38
def render(self, req):
39
if req.method == 'GET':
40
outstr = self.GET(req)
42
if req.method == 'PUT':
43
outstr = self.PATCH(req, req.read())
44
req.content_type = self.content_type
39
raise NotImplementedError()
47
41
class JSONRESTView(RESTView):
54
48
lambda self: [m for m in ('GET', 'PUT', 'PATCH')
55
49
if hasattr(self, m)] + ['POST'])
51
def authorize(self, req):
52
return True # Real authz performed in render().
54
def authorize_method(self, req, op):
55
if not hasattr(op, '_rest_api_permission'):
58
if (op._rest_api_permission not in
59
self.get_permissions(req.user, req.config)):
62
def convert_bool(self, value):
63
if value in ('True', 'true', True):
65
elif value in ('False', 'false', False):
57
70
def render(self, req):
58
71
if req.method not in self._allowed_methods:
59
72
raise MethodNotAllowed(allowed=self._allowed_methods)
61
74
if req.method == 'GET':
75
self.authorize_method(req, self.GET)
62
76
outjson = self.GET(req)
63
77
# Since PATCH isn't yet an official HTTP method, we allow users to
64
78
# turn a PUT into a PATCH by supplying a special header.
65
79
elif req.method == 'PATCH' or (req.method == 'PUT' and
66
80
'X-IVLE-Patch-Semantics' in req.headers_in and
67
81
req.headers_in['X-IVLE-Patch-Semantics'].lower() == 'yes'):
68
outjson = self.PATCH(req, cjson.decode(req.read()))
82
self.authorize_method(req, self.PATCH)
84
input = cjson.decode(req.read())
85
except cjson.DecodeError:
86
raise BadRequest('Invalid JSON data')
87
outjson = self.PATCH(req, input)
69
88
elif req.method == 'PUT':
70
outjson = self.PUT(req, cjson.decode(req.read()))
89
self.authorize_method(req, self.PUT)
91
input = cjson.decode(req.read())
92
except cjson.DecodeError:
93
raise BadRequest('Invalid JSON data')
94
outjson = self.PUT(req, input)
71
95
# POST implies named operation.
72
96
elif req.method == 'POST':
73
97
# TODO: Check Content-Type and implement multipart/form-data.
74
opargs = dict(cgi.parse_qsl(req.read()))
99
opargs = dict(cgi.parse_qsl(data, keep_blank_values=1))
76
101
opname = opargs['ivle.op']
77
102
del opargs['ivle.op']
87
112
not op._rest_api_callable:
88
113
raise BadRequest('Invalid named operation.')
115
self.authorize_method(req, op)
90
117
# Find any missing arguments, except for the first two (self, req)
91
118
(args, vaargs, varkw, defaults) = inspect.getargspec(op)
98
125
unspec = set(args) - set(opargs.keys())
99
126
if unspec and not defaults:
100
raise BadRequest('Missing arguments: ' + ','.join(unspec))
127
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
102
129
unspec = [k for k in unspec if k not in args[-len(defaults):]]
105
raise BadRequest('Missing arguments: ' + ','.join(unspec))
132
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
107
134
# We have extra arguments if the are no match args in the function
108
135
# signature, AND there is no **.
111
138
raise BadRequest('Extra arguments: ' + ', '.join(extra))
113
140
outjson = op(req, **opargs)
115
raise AssertionError('Unknown method somehow got through.')
117
142
req.content_type = self.content_type
143
self.write_json(req, outjson)
145
#This is a separate function to allow additional data to be passed through
146
def write_json(self, req, outjson):
118
147
if outjson is not None:
119
148
req.write(cjson.encode(outjson))
122
def named_operation(meth):
152
class XHTMLRESTView(JSONRESTView):
153
"""A special type of RESTView which takes enhances the standard JSON
154
with genshi XHTML functions.
156
XHTMLRESTViews should have a template, which is rendered using their
157
context. This is returned in the JSON as 'html'"""
159
ctx = genshi.template.Context()
161
def render_fragment(self):
162
if self.template is None:
163
raise NotImplementedError()
165
rest_template = os.path.join(os.path.dirname(
166
inspect.getmodule(self).__file__), self.template)
167
loader = genshi.template.TemplateLoader(".", auto_reload=True)
168
tmpl = loader.load(rest_template)
170
return tmpl.generate(self.ctx).render('xhtml', doctype='xhtml')
172
# This renders the template and adds it to the json
173
def write_json(self, req, outjson):
174
outjson["html"] = self.render_fragment()
175
req.write(cjson.encode(outjson))
178
class named_operation(object):
123
179
'''Declare a function to be accessible to HTTP users via the REST API.
125
meth._rest_api_callable = True
181
def __init__(self, permission):
182
self.permission = permission
184
def __call__(self, func):
185
func._rest_api_callable = True
186
func._rest_api_permission = self.permission
189
class require_permission(object):
190
'''Declare the permission required for use of a method via the REST API.
192
def __init__(self, permission):
193
self.permission = permission
195
def __call__(self, func):
196
func._rest_api_permission = self.permission