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, Nick Chadwick
18
# Author: Matt Giuca, Will Grant
26
import genshi.template
28
25
from ivle.webapp.base.views import BaseView
29
from ivle.webapp.base.xhtml import GenshiLoaderMixin
30
from ivle.webapp.errors import BadRequest, MethodNotAllowed, Unauthorized
26
from ivle.webapp.errors import BadRequest, MethodNotAllowed
32
28
class RESTView(BaseView):
37
33
content_type = "application/octet-stream"
35
def __init__(self, req, *args, **kwargs):
37
setattr(self, key, kwargs[key])
39
39
def render(self, req):
40
raise NotImplementedError()
40
if req.method == 'GET':
41
outstr = self.GET(req)
43
if req.method == 'PUT':
44
outstr = self.PATCH(req, req.read())
45
req.content_type = self.content_type
42
48
class JSONRESTView(RESTView):
49
55
lambda self: [m for m in ('GET', 'PUT', 'PATCH')
50
56
if hasattr(self, m)] + ['POST'])
52
def authorize(self, req):
53
return True # Real authz performed in render().
55
def authorize_method(self, req, op):
56
if not hasattr(op, '_rest_api_permission'):
59
if (op._rest_api_permission not in
60
self.get_permissions(req.user, req.config)):
63
def convert_bool(self, value):
64
if value in ('True', 'true', True):
66
elif value in ('False', 'false', False):
71
58
def render(self, req):
72
59
if req.method not in self._allowed_methods:
73
60
raise MethodNotAllowed(allowed=self._allowed_methods)
75
62
if req.method == 'GET':
76
self.authorize_method(req, self.GET)
77
63
outjson = self.GET(req)
78
64
# Since PATCH isn't yet an official HTTP method, we allow users to
79
65
# turn a PUT into a PATCH by supplying a special header.
80
66
elif req.method == 'PATCH' or (req.method == 'PUT' and
81
67
'X-IVLE-Patch-Semantics' in req.headers_in and
82
68
req.headers_in['X-IVLE-Patch-Semantics'].lower() == 'yes'):
83
self.authorize_method(req, self.PATCH)
85
input = cjson.decode(req.read())
86
except cjson.DecodeError:
87
raise BadRequest('Invalid JSON data')
88
outjson = self.PATCH(req, input)
69
outjson = self.PATCH(req, cjson.decode(req.read()))
89
70
elif req.method == 'PUT':
90
self.authorize_method(req, self.PUT)
92
input = cjson.decode(req.read())
93
except cjson.DecodeError:
94
raise BadRequest('Invalid JSON data')
95
outjson = self.PUT(req, input)
71
outjson = self.PUT(req, cjson.decode(req.read()))
96
72
# POST implies named operation.
97
73
elif req.method == 'POST':
98
74
# TODO: Check Content-Type and implement multipart/form-data.
100
opargs = dict(cgi.parse_qsl(data, keep_blank_values=1))
75
opargs = dict(cgi.parse_qsl(req.read()))
102
77
opname = opargs['ivle.op']
103
78
del opargs['ivle.op']
113
88
not op._rest_api_callable:
114
89
raise BadRequest('Invalid named operation.')
116
self.authorize_method(req, op)
118
91
# Find any missing arguments, except for the first two (self, req)
119
92
(args, vaargs, varkw, defaults) = inspect.getargspec(op)
126
99
unspec = set(args) - set(opargs.keys())
127
100
if unspec and not defaults:
128
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
101
raise BadRequest('Missing arguments: ' + ','.join(unspec))
130
103
unspec = [k for k in unspec if k not in args[-len(defaults):]]
133
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
106
raise BadRequest('Missing arguments: ' + ','.join(unspec))
135
108
# We have extra arguments if the are no match args in the function
136
109
# signature, AND there is no **.
139
112
raise BadRequest('Extra arguments: ' + ', '.join(extra))
141
114
outjson = op(req, **opargs)
116
raise AssertionError('Unknown method somehow got through.')
143
118
req.content_type = self.content_type
144
self.write_json(req, outjson)
146
#This is a separate function to allow additional data to be passed through
147
def write_json(self, req, outjson):
148
119
if outjson is not None:
149
120
req.write(cjson.encode(outjson))
153
class XHTMLRESTView(GenshiLoaderMixin, JSONRESTView):
154
"""A special type of RESTView which takes enhances the standard JSON
155
with genshi XHTML functions.
157
XHTMLRESTViews should have a template, which is rendered using their
158
context. This is returned in the JSON as 'html'"""
160
ctx = genshi.template.Context()
162
def render_fragment(self):
163
if self.template is None:
164
raise NotImplementedError()
166
rest_template = os.path.join(os.path.dirname(
167
inspect.getmodule(self).__file__), self.template)
168
tmpl = self._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
def named_operation(meth):
179
124
'''Declare a function to be accessible to HTTP users via the REST API.
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
126
meth._rest_api_callable = True