blob: a26acd43ee8a4643baed6c707a8b0b1eed3aa5fc [file] [log] [blame]
showardf46ad4c2010-02-03 20:28:59 +00001import cgi, datetime, re, time, urllib
showardf828c772010-01-25 21:49:42 +00002from django import http
showardf46ad4c2010-02-03 20:28:59 +00003import django.core.exceptions
4from django.core import urlresolvers
showardf828c772010-01-25 21:49:42 +00005from django.utils import simplejson
6from autotest_lib.frontend.shared import exceptions, query_lib
7from autotest_lib.frontend.afe import model_logic
8
9
10_JSON_CONTENT_TYPE = 'application/json'
11
12
13def _resolve_class_path(class_path):
14 module_path, class_name = class_path.rsplit('.', 1)
15 module = __import__(module_path, {}, {}, [''])
16 return getattr(module, class_name)
17
18
19_NO_VALUE_SPECIFIED = object()
20
21class _InputDict(dict):
22 def get(self, key, default=_NO_VALUE_SPECIFIED):
23 return super(_InputDict, self).get(key, default)
24
25
26 @classmethod
27 def remove_unspecified_fields(cls, field_dict):
28 return dict((key, value) for key, value in field_dict.iteritems()
29 if value is not _NO_VALUE_SPECIFIED)
30
31
32class Resource(object):
33 _permitted_methods = None # subclasses must override this
34
35
showardf46ad4c2010-02-03 20:28:59 +000036 def __init__(self, request):
showardf828c772010-01-25 21:49:42 +000037 assert self._permitted_methods
showardf46ad4c2010-02-03 20:28:59 +000038 self._request = request
showardf828c772010-01-25 21:49:42 +000039
40
41 @classmethod
42 def dispatch_request(cls, request, *args, **kwargs):
43 # handle a request directly
44 try:
showardf46ad4c2010-02-03 20:28:59 +000045 instance = cls.from_uri_args(request, *args, **kwargs)
46 except django.core.exceptions.ObjectDoesNotExist, exc:
showardf828c772010-01-25 21:49:42 +000047 raise http.Http404(exc)
showardf46ad4c2010-02-03 20:28:59 +000048 return instance.handle_request()
showardf828c772010-01-25 21:49:42 +000049
50
showardf46ad4c2010-02-03 20:28:59 +000051 def handle_request(self):
52 if self._request.method.upper() not in self._permitted_methods:
showardf828c772010-01-25 21:49:42 +000053 return http.HttpResponseNotAllowed(self._permitted_methods)
54
showardf46ad4c2010-02-03 20:28:59 +000055 handler = getattr(self, self._request.method.lower())
showardf828c772010-01-25 21:49:42 +000056 try:
showardf46ad4c2010-02-03 20:28:59 +000057 return handler()
showardf828c772010-01-25 21:49:42 +000058 except exceptions.RequestError, exc:
59 return exc.response
60
61
62 # the handler methods below only need to be overridden if the resource
63 # supports the method
64
showardf46ad4c2010-02-03 20:28:59 +000065 def get(self):
showardf828c772010-01-25 21:49:42 +000066 """Handle a GET request.
67
68 @returns an HttpResponse
69 """
70 raise NotImplementedError
71
72
showardf46ad4c2010-02-03 20:28:59 +000073 def post(self):
showardf828c772010-01-25 21:49:42 +000074 """Handle a POST request.
75
76 @returns an HttpResponse
77 """
78 raise NotImplementedError
79
80
showardf46ad4c2010-02-03 20:28:59 +000081 def put(self):
showardf828c772010-01-25 21:49:42 +000082 """Handle a PUT request.
83
84 @returns an HttpResponse
85 """
86 raise NotImplementedError
87
88
showardf46ad4c2010-02-03 20:28:59 +000089 def delete(self):
showardf828c772010-01-25 21:49:42 +000090 """Handle a DELETE request.
91
92 @returns an HttpResponse
93 """
94 raise NotImplementedError
95
96
97 @classmethod
showardf46ad4c2010-02-03 20:28:59 +000098 def from_uri_args(cls, request):
showardf828c772010-01-25 21:49:42 +000099 """Construct an instance from URI args.
100
101 Default implementation for resources with no URI args.
102 """
showardf46ad4c2010-02-03 20:28:59 +0000103 return cls(request)
showardf828c772010-01-25 21:49:42 +0000104
105
106 def _uri_args(self):
107 """Return (args, kwargs) for a URI reference to this resource.
108
109 Default implementation for resources with no URI args.
110 """
111 return (), {}
112
113
114 def _query_parameters(self):
115 """Return sequence of tuples (name, description) for query parameters.
116
117 Documents the available query parameters for GETting this resource.
118 Default implementation for resources with no parameters.
119 """
120 return ()
121
122
123 def href(self):
124 """Return URI to this resource."""
125 args, kwargs = self._uri_args()
showardf46ad4c2010-02-03 20:28:59 +0000126 path = urlresolvers.reverse(self.dispatch_request, args=args,
showardf828c772010-01-25 21:49:42 +0000127 kwargs=kwargs)
showardf46ad4c2010-02-03 20:28:59 +0000128 return self._request.build_absolute_uri(path)
showardf828c772010-01-25 21:49:42 +0000129
130
showardf46ad4c2010-02-03 20:28:59 +0000131 def resolve_uri(self, uri):
132 # check for absolute URIs
133 match = re.match(r'(?P<root>https?://[^/]+)(?P<path>/.*)', uri)
134 if match:
135 # is this URI for a different host?
136 my_root = self._request.build_absolute_uri('/')
137 request_root = match.group('root') + '/'
138 if my_root != request_root:
139 # might support this in the future, but not now
140 raise exceptions.BadRequest('Unable to resolve remote URI %s'
141 % uri)
142 uri = match.group('path')
143
showardf828c772010-01-25 21:49:42 +0000144 view_method, args, kwargs = urlresolvers.resolve(uri)
145 resource_class = view_method.im_self # class owning this classmethod
showardf46ad4c2010-02-03 20:28:59 +0000146 return resource_class.from_uri_args(self._request, *args, **kwargs)
showardf828c772010-01-25 21:49:42 +0000147
148
showardf46ad4c2010-02-03 20:28:59 +0000149 def resolve_link(self, link):
showardf828c772010-01-25 21:49:42 +0000150 if isinstance(link, dict):
151 uri = link['href']
152 elif isinstance(link, basestring):
153 uri = link
154 else:
155 raise exceptions.BadRequest('Unable to understand link %s' % link)
showardf46ad4c2010-02-03 20:28:59 +0000156 return self.resolve_uri(uri)
showardf828c772010-01-25 21:49:42 +0000157
158
159 def link(self):
160 return {'href': self.href()}
161
162
163 def _query_parameters_response(self):
164 return dict((name, description)
165 for name, description in self._query_parameters())
166
167
168 def _basic_response(self, content):
169 """Construct and return a simple 200 response."""
170 assert isinstance(content, dict)
171 query_parameters = self._query_parameters_response()
172 if query_parameters:
173 content['query_parameters'] = query_parameters
174 encoded_content = simplejson.dumps(content)
175 return http.HttpResponse(encoded_content,
176 content_type=_JSON_CONTENT_TYPE)
177
178
showardf46ad4c2010-02-03 20:28:59 +0000179 def _decoded_input(self):
180 content_type = self._request.META.get('CONTENT_TYPE',
181 _JSON_CONTENT_TYPE)
182 raw_data = self._request.raw_post_data
showardf828c772010-01-25 21:49:42 +0000183 if content_type == _JSON_CONTENT_TYPE:
184 try:
185 raw_dict = simplejson.loads(raw_data)
186 except ValueError, exc:
187 raise exceptions.BadRequest('Error decoding request body: '
188 '%s\n%r' % (exc, raw_data))
189 elif content_type == 'application/x-www-form-urlencoded':
190 cgi_dict = cgi.parse_qs(raw_data) # django won't do this for PUT
191 raw_dict = {}
192 for key, values in cgi_dict.items():
193 value = values[-1] # take last value if multiple were given
194 try:
195 # attempt to parse numbers, booleans and nulls
196 raw_dict[key] = simplejson.loads(value)
197 except ValueError:
198 # otherwise, leave it as a string
199 raw_dict[key] = value
200 else:
201 raise exceptions.RequestError(415, 'Unsupported media type: %s'
202 % content_type)
203
204 return _InputDict(raw_dict)
205
206
207 def _format_datetime(self, date_time):
208 """Return ISO 8601 string for the given datetime"""
209 if date_time is None:
210 return None
211 timezone_hrs = time.timezone / 60 / 60 # convert seconds to hours
212 if timezone_hrs >= 0:
213 timezone_join = '+'
214 else:
215 timezone_join = '' # minus sign comes from number itself
216 timezone_spec = '%s%s:00' % (timezone_join, timezone_hrs)
217 return date_time.strftime('%Y-%m-%dT%H:%M:%S') + timezone_spec
218
219
220 @classmethod
221 def _check_for_required_fields(cls, input_dict, fields):
222 assert isinstance(fields, (list, tuple)), fields
223 missing_fields = ', '.join(field for field in fields
224 if field not in input_dict)
225 if missing_fields:
226 raise exceptions.BadRequest('Missing input: ' + missing_fields)
227
228
229class Entry(Resource):
230 class NullEntry(object):
231 def link(self):
232 return None
233
234
235 def short_representation(self):
236 return None
237
238 _null_entry = NullEntry()
239
240
241 _permitted_methods = ('GET', 'PUT', 'DELETE')
242
243
244 # sublcasses must define this class to support querying
245 QueryProcessor = query_lib.BaseQueryProcessor
246
247
showardf46ad4c2010-02-03 20:28:59 +0000248 def __init__(self, request, instance):
249 super(Entry, self).__init__(request)
showardf828c772010-01-25 21:49:42 +0000250 self.instance = instance
251
252
253 @classmethod
showardf46ad4c2010-02-03 20:28:59 +0000254 def from_optional_instance(cls, request, instance):
showardf828c772010-01-25 21:49:42 +0000255 if instance is None:
256 return cls._null_entry
showardf46ad4c2010-02-03 20:28:59 +0000257 return cls(request, instance)
showardf828c772010-01-25 21:49:42 +0000258
259
260 def short_representation(self):
261 return self.link()
262
263
264 def full_representation(self):
265 return self.short_representation()
266
267
showardf46ad4c2010-02-03 20:28:59 +0000268 def get(self):
showardf828c772010-01-25 21:49:42 +0000269 return self._basic_response(self.full_representation())
270
271
showardf46ad4c2010-02-03 20:28:59 +0000272 def put(self):
showardf828c772010-01-25 21:49:42 +0000273 try:
showardf46ad4c2010-02-03 20:28:59 +0000274 self.update(self._decoded_input())
showardf828c772010-01-25 21:49:42 +0000275 except model_logic.ValidationError, exc:
276 raise exceptions.BadRequest('Invalid input: %s' % exc)
277 return self._basic_response(self.full_representation())
278
279
showardf46ad4c2010-02-03 20:28:59 +0000280 def delete(self):
showardf828c772010-01-25 21:49:42 +0000281 self.instance.delete()
282 return http.HttpResponse(status=204) # No content
283
284
285 def create_instance(self, input_dict, containing_collection):
286 raise NotImplementedError
287
288
289 def update(self, input_dict):
290 raise NotImplementedError
291
292
293class Collection(Resource):
294 _DEFAULT_ITEMS_PER_PAGE = 50
295
296 _permitted_methods=('GET', 'POST')
297
298 # subclasses must override these
299 queryset = None # or override _fresh_queryset() directly
300 entry_class = None
301
302
showardf46ad4c2010-02-03 20:28:59 +0000303 def __init__(self, request):
304 super(Collection, self).__init__(request)
showardf828c772010-01-25 21:49:42 +0000305 assert self.entry_class is not None
306 if isinstance(self.entry_class, basestring):
307 type(self).entry_class = _resolve_class_path(self.entry_class)
308
309 self._query_processor = self.entry_class.QueryProcessor()
310
311
312 def _fresh_queryset(self):
313 assert self.queryset is not None
314 # always copy the queryset before using it to avoid caching
315 return self.queryset.all()
316
317
318 def _representation(self, entry_instances):
319 members = []
320 for instance in entry_instances:
showardf46ad4c2010-02-03 20:28:59 +0000321 entry = self.entry_class(self._request, instance)
showardf828c772010-01-25 21:49:42 +0000322 members.append(entry.short_representation())
323
324 rep = self.link()
325 rep.update({'members': members})
326 return rep
327
328
showardf46ad4c2010-02-03 20:28:59 +0000329 def _read_int_parameter(self, name, default):
330 query_dict = self._request.GET
showardf828c772010-01-25 21:49:42 +0000331 if name not in query_dict:
332 return default
333 input_value = query_dict[name]
334 try:
335 return int(input_value)
336 except ValueError:
337 raise exceptions.BadRequest('Invalid non-numeric value for %s: %r'
338 % (name, input_value))
339
340
showardf46ad4c2010-02-03 20:28:59 +0000341 def _apply_form_query(self, queryset):
showardf828c772010-01-25 21:49:42 +0000342 """Apply any query selectors passed as form variables."""
showardf46ad4c2010-02-03 20:28:59 +0000343 for parameter, values in self._request.GET.lists():
showardf828c772010-01-25 21:49:42 +0000344 if not self._query_processor.has_selector(parameter):
345 continue
346 for value in values: # forms keys can have multiple values
347 queryset = self._query_processor.apply_selector(queryset,
348 parameter,
349 value)
350 return queryset
351
352
showardf46ad4c2010-02-03 20:28:59 +0000353 def _filtered_queryset(self):
354 return self._apply_form_query(self._fresh_queryset())
showardf828c772010-01-25 21:49:42 +0000355
356
showardf46ad4c2010-02-03 20:28:59 +0000357 def get(self):
358 queryset = self._filtered_queryset()
showardf828c772010-01-25 21:49:42 +0000359
showardf46ad4c2010-02-03 20:28:59 +0000360 items_per_page = self._read_int_parameter('items_per_page',
showardf828c772010-01-25 21:49:42 +0000361 self._DEFAULT_ITEMS_PER_PAGE)
showardf46ad4c2010-02-03 20:28:59 +0000362 start_index = self._read_int_parameter('start_index', 0)
showardf828c772010-01-25 21:49:42 +0000363 page = queryset[start_index:(start_index + items_per_page)]
364
365 rep = self._representation(page)
366 selector_dict = dict((selector.name, selector.doc)
367 for selector
368 in self.entry_class.QueryProcessor.selectors())
369 rep.update({'total_results': len(queryset),
370 'start_index': start_index,
371 'items_per_page': items_per_page,
372 'filtering_selectors': selector_dict})
373 return self._basic_response(rep)
374
375
376 def full_representation(self):
377 # careful, this rep can be huge for large collections
378 return self._representation(self._fresh_queryset())
379
380
showardf46ad4c2010-02-03 20:28:59 +0000381 def post(self):
382 input_dict = self._decoded_input()
showardf828c772010-01-25 21:49:42 +0000383 try:
384 instance = self.entry_class.create_instance(input_dict, self)
showardf46ad4c2010-02-03 20:28:59 +0000385 entry = self.entry_class(self._request, instance)
showardf828c772010-01-25 21:49:42 +0000386 entry.update(input_dict)
387 except model_logic.ValidationError, exc:
388 raise exceptions.BadRequest('Invalid input: %s' % exc)
389 # RFC 2616 specifies that we provide the new URI in both the Location
390 # header and the body
391 response = http.HttpResponse(status=201, # Created
392 content=entry.href())
393 response['Location'] = entry.href()
394 return response
395
396
397class Relationship(Collection):
398 _permitted_methods=('GET', 'PUT')
399
400 base_entry_class = None # subclasses must override this
401
402
403 def __init__(self, base_entry):
404 assert self.base_entry_class
405 if isinstance(self.base_entry_class, basestring):
406 type(self).base_entry_class = _resolve_class_path(
407 self.base_entry_class)
408 assert isinstance(base_entry, self.base_entry_class)
409 self.base_entry = base_entry
showardf46ad4c2010-02-03 20:28:59 +0000410 super(Relationship, self).__init__(base_entry._request)
showardf828c772010-01-25 21:49:42 +0000411
412
413 def _fresh_queryset(self):
414 """Return a QuerySet for this relationship using self.base_entry."""
415 raise NotImplementedError
416
417
418 @classmethod
showardf46ad4c2010-02-03 20:28:59 +0000419 def from_uri_args(cls, request, *args, **kwargs):
420 base_entry = cls.base_entry_class.from_uri_args(request, *args,
421 **kwargs)
showardf828c772010-01-25 21:49:42 +0000422 return cls(base_entry)
423
424
425 def _uri_args(self):
426 return self.base_entry._uri_args()
427
428
429 @classmethod
showardf46ad4c2010-02-03 20:28:59 +0000430 def _input_collection_links(cls, input_data):
showardf828c772010-01-25 21:49:42 +0000431 """Get the members of a user-provided collection.
432
433 Tries to be flexible about formats accepted from the user.
showardf46ad4c2010-02-03 20:28:59 +0000434 @returns a list of links, possibly only href strings (use
435 resolve_link())
showardf828c772010-01-25 21:49:42 +0000436 """
437 if isinstance(input_data, dict) and 'members' in input_data:
438 # this mirrors the output representation for collections
439 # guard against accidental truncation of the relationship due to
440 # paging
441 is_partial_collection = ('total_results' in input_data
442 and 'items_per_page' in input_data
443 and input_data['total_results'] >
444 input_data['items_per_page'])
445 if is_partial_collection:
446 raise exceptions.BadRequest('You must retreive the full '
447 'collection to perform updates')
448
showardf46ad4c2010-02-03 20:28:59 +0000449 return input_data['members']
showardf828c772010-01-25 21:49:42 +0000450 if isinstance(input_data, list):
showardf46ad4c2010-02-03 20:28:59 +0000451 return input_data
showardf828c772010-01-25 21:49:42 +0000452 raise exceptions.BadRequest('Cannot understand collection in input: %r'
453 % input_data)
454
455
showardf46ad4c2010-02-03 20:28:59 +0000456 def put(self):
457 input_data = self._decoded_input()
showardf828c772010-01-25 21:49:42 +0000458 self.update(input_data)
showardf46ad4c2010-02-03 20:28:59 +0000459 return self.get()
showardf828c772010-01-25 21:49:42 +0000460
461
462 def update(self, input_data):
showardf46ad4c2010-02-03 20:28:59 +0000463 links = self._input_collection_links(input_data)
464 instances = [self.resolve_link(link).instance for link in links]
showardf828c772010-01-25 21:49:42 +0000465 self._update_relationship(instances)
466
467
468 def _update_relationship(self, related_instances):
469 raise NotImplementedError