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
29
import simplejson as json
25
31
import genshi.template
27
33
from ivle.webapp.base.views import BaseView
34
from ivle.webapp.base.xhtml import GenshiLoaderMixin
28
35
from ivle.webapp.errors import BadRequest, MethodNotAllowed, Unauthorized
30
37
class RESTView(BaseView):
35
42
content_type = "application/octet-stream"
37
def __init__(self, req, *args, **kwargs):
39
setattr(self, key, kwargs[key])
41
44
def render(self, req):
42
45
raise NotImplementedError()
58
61
if not hasattr(op, '_rest_api_permission'):
59
62
raise Unauthorized()
61
if op._rest_api_permission not in self.get_permissions(req.user):
64
if (op._rest_api_permission not in
65
self.get_permissions(req.user, req.config)):
62
66
raise Unauthorized()
64
68
def convert_bool(self, value):
74
78
raise MethodNotAllowed(allowed=self._allowed_methods)
76
80
if req.method == 'GET':
77
self.authorize_method(req, self.GET)
78
outjson = self.GET(req)
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)
79
89
# Since PATCH isn't yet an official HTTP method, we allow users to
80
90
# turn a PUT into a PATCH by supplying a special header.
81
91
elif req.method == 'PATCH' or (req.method == 'PUT' and
83
93
req.headers_in['X-IVLE-Patch-Semantics'].lower() == 'yes'):
84
94
self.authorize_method(req, self.PATCH)
86
input = cjson.decode(req.read())
87
except cjson.DecodeError:
96
input = json.loads(req.read())
88
98
raise BadRequest('Invalid JSON data')
89
99
outjson = self.PATCH(req, input)
90
100
elif req.method == 'PUT':
91
101
self.authorize_method(req, self.PUT)
93
input = cjson.decode(req.read())
94
except cjson.DecodeError:
103
input = json.loads(req.read())
95
105
raise BadRequest('Invalid JSON data')
96
106
outjson = self.PUT(req, input)
97
107
# POST implies named operation.
99
109
# TODO: Check Content-Type and implement multipart/form-data.
100
110
data = req.read()
101
111
opargs = dict(cgi.parse_qsl(data, keep_blank_values=1))
103
opname = opargs['ivle.op']
104
del opargs['ivle.op']
106
raise BadRequest('No named operation specified.')
109
op = getattr(self, opname)
110
except AttributeError:
111
raise BadRequest('Invalid named operation.')
113
if not hasattr(op, '_rest_api_callable') or \
114
not op._rest_api_callable:
115
raise BadRequest('Invalid named operation.')
117
self.authorize_method(req, op)
119
# Find any missing arguments, except for the first two (self, req)
120
(args, vaargs, varkw, defaults) = inspect.getargspec(op)
123
# To find missing arguments, we eliminate the provided arguments
124
# from the set of remaining function signature arguments. If the
125
# remaining signature arguments are in the args[-len(defaults):],
127
unspec = set(args) - set(opargs.keys())
128
if unspec and not defaults:
129
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
131
unspec = [k for k in unspec if k not in args[-len(defaults):]]
134
raise BadRequest('Missing arguments: ' + ', '.join(unspec))
136
# We have extra arguments if the are no match args in the function
137
# signature, AND there is no **.
138
extra = set(opargs.keys()) - set(args)
139
if extra and not varkw:
140
raise BadRequest('Extra arguments: ' + ', '.join(extra))
142
outjson = op(req, **opargs)
112
outjson = self._named_operation(req, opargs)
144
114
req.content_type = self.content_type
145
115
self.write_json(req, outjson)
147
117
#This is a separate function to allow additional data to be passed through
148
118
def write_json(self, req, outjson):
149
119
if outjson is not None:
150
req.write(cjson.encode(outjson))
120
req.write(json.dumps(outjson))
154
class XHTMLRESTView(JSONRESTView):
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):
155
171
"""A special type of RESTView which takes enhances the standard JSON
156
172
with genshi XHTML functions.
161
177
ctx = genshi.template.Context()
163
def __init__(self, req, *args, **kwargs):
165
setattr(self, key, kwargs[key])
167
179
def render_fragment(self):
168
180
if self.template is None:
169
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)
171
187
return tmpl.generate(self.ctx).render('xhtml', doctype='xhtml')
173
189
# This renders the template and adds it to the json
174
190
def write_json(self, req, outjson):
175
191
outjson["html"] = self.render_fragment()
176
req.write(cjson.encode(outjson))
192
req.write(json.dumps(outjson))
179
class named_operation(object):
195
class _named_operation(object):
180
196
'''Declare a function to be accessible to HTTP users via the REST API.
182
def __init__(self, permission):
198
def __init__(self, write_operation, permission):
199
self.write_operation = write_operation
183
200
self.permission = permission
185
202
def __call__(self, func):
186
203
func._rest_api_callable = True
204
func._rest_api_write_operation = self.write_operation
187
205
func._rest_api_permission = self.permission
208
write_operation = functools.partial(_named_operation, True)
209
read_operation = functools.partial(_named_operation, False)
190
211
class require_permission(object):
191
212
'''Declare the permission required for use of a method via the REST API.