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 self.get_permissions(req.user):
61
def convert_bool(self, value):
62
if value in ('True', 'true', True):
64
elif value in ('False', 'false', False):
57
69
def render(self, req):
58
70
if req.method not in self._allowed_methods:
59
71
raise MethodNotAllowed(allowed=self._allowed_methods)
61
73
if req.method == 'GET':
74
self.authorize_method(req, self.GET)
62
75
outjson = self.GET(req)
63
76
# Since PATCH isn't yet an official HTTP method, we allow users to
64
77
# turn a PUT into a PATCH by supplying a special header.
65
78
elif req.method == 'PATCH' or (req.method == 'PUT' and
66
79
'X-IVLE-Patch-Semantics' in req.headers_in and
67
80
req.headers_in['X-IVLE-Patch-Semantics'].lower() == 'yes'):
68
outjson = self.PATCH(req, cjson.decode(req.read()))
81
self.authorize_method(req, self.PATCH)
83
input = cjson.decode(req.read())
84
except cjson.DecodeError:
85
raise BadRequest('Invalid JSON data')
86
outjson = self.PATCH(req, input)
69
87
elif req.method == 'PUT':
70
outjson = self.PUT(req, cjson.decode(req.read()))
88
self.authorize_method(req, self.PUT)
90
input = cjson.decode(req.read())
91
except cjson.DecodeError:
92
raise BadRequest('Invalid JSON data')
93
outjson = self.PUT(req, input)
71
94
# POST implies named operation.
72
95
elif req.method == 'POST':
73
96
# TODO: Check Content-Type and implement multipart/form-data.
74
opargs = dict(cgi.parse_qsl(req.read()))
98
opargs = dict(cgi.parse_qsl(data, keep_blank_values=1))
76
100
opname = opargs['ivle.op']
77
101
del opargs['ivle.op']
87
111
not op._rest_api_callable:
88
112
raise BadRequest('Invalid named operation.')
114
self.authorize_method(req, op)
90
116
# Find any missing arguments, except for the first two (self, req)
91
117
(args, vaargs, varkw, defaults) = inspect.getargspec(op)
98
124
unspec = set(args) - set(opargs.keys())
99
125
if unspec and not defaults:
100
raise BadRequest('Missing arguments: ' + ','.join(unspec))
126
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
102
128
unspec = [k for k in unspec if k not in args[-len(defaults):]]
105
raise BadRequest('Missing arguments: ' + ','.join(unspec))
131
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
107
133
# We have extra arguments if the are no match args in the function
108
134
# signature, AND there is no **.
111
137
raise BadRequest('Extra arguments: ' + ', '.join(extra))
113
139
outjson = op(req, **opargs)
115
raise AssertionError('Unknown method somehow got through.')
117
141
req.content_type = self.content_type
142
self.write_json(req, outjson)
144
#This is a separate function to allow additional data to be passed through
145
def write_json(self, req, outjson):
118
146
if outjson is not None:
119
147
req.write(cjson.encode(outjson))
122
def named_operation(meth):
151
class XHTMLRESTView(JSONRESTView):
152
"""A special type of RESTView which takes enhances the standard JSON
153
with genshi XHTML functions.
155
XHTMLRESTViews should have a template, which is rendered using their
156
context. This is returned in the JSON as 'html'"""
158
ctx = genshi.template.Context()
160
def render_fragment(self):
161
if self.template is None:
162
raise NotImplementedError()
164
rest_template = os.path.join(os.path.dirname(
165
inspect.getmodule(self).__file__), self.template)
166
loader = genshi.template.TemplateLoader(".", auto_reload=True)
167
tmpl = loader.load(rest_template)
169
return tmpl.generate(self.ctx).render('xhtml', doctype='xhtml')
171
# This renders the template and adds it to the json
172
def write_json(self, req, outjson):
173
outjson["html"] = self.render_fragment()
174
req.write(cjson.encode(outjson))
177
class named_operation(object):
123
178
'''Declare a function to be accessible to HTTP users via the REST API.
125
meth._rest_api_callable = True
180
def __init__(self, permission):
181
self.permission = permission
183
def __call__(self, func):
184
func._rest_api_callable = True
185
func._rest_api_permission = self.permission
188
class require_permission(object):
189
'''Declare the permission required for use of a method via the REST API.
191
def __init__(self, permission):
192
self.permission = permission
194
def __call__(self, func):
195
func._rest_api_permission = self.permission