showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 1 | import cgi, datetime, re, time, urllib |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 2 | from django import http |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 3 | import django.core.exceptions |
| 4 | from django.core import urlresolvers |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 5 | from django.utils import datastructures |
| 6 | import simplejson |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 7 | from autotest_lib.frontend.shared import exceptions, query_lib |
| 8 | from autotest_lib.frontend.afe import model_logic |
| 9 | |
| 10 | |
| 11 | _JSON_CONTENT_TYPE = 'application/json' |
| 12 | |
| 13 | |
| 14 | def _resolve_class_path(class_path): |
| 15 | module_path, class_name = class_path.rsplit('.', 1) |
| 16 | module = __import__(module_path, {}, {}, ['']) |
| 17 | return getattr(module, class_name) |
| 18 | |
| 19 | |
| 20 | _NO_VALUE_SPECIFIED = object() |
| 21 | |
| 22 | class _InputDict(dict): |
| 23 | def get(self, key, default=_NO_VALUE_SPECIFIED): |
| 24 | return super(_InputDict, self).get(key, default) |
| 25 | |
| 26 | |
| 27 | @classmethod |
| 28 | def remove_unspecified_fields(cls, field_dict): |
| 29 | return dict((key, value) for key, value in field_dict.iteritems() |
| 30 | if value is not _NO_VALUE_SPECIFIED) |
| 31 | |
| 32 | |
| 33 | class Resource(object): |
| 34 | _permitted_methods = None # subclasses must override this |
| 35 | |
| 36 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 37 | def __init__(self, request): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 38 | assert self._permitted_methods |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 39 | # this request should be used for global environment info, like |
| 40 | # constructing absolute URIs. it should not be used for query |
| 41 | # parameters, because the request may not have been for this particular |
| 42 | # resource. |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 43 | self._request = request |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 44 | # this dict will contain the applicable query parameters |
| 45 | self._query_params = datastructures.MultiValueDict() |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 46 | |
| 47 | |
| 48 | @classmethod |
| 49 | def dispatch_request(cls, request, *args, **kwargs): |
| 50 | # handle a request directly |
| 51 | try: |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 52 | try: |
| 53 | instance = cls.from_uri_args(request, **kwargs) |
| 54 | except django.core.exceptions.ObjectDoesNotExist, exc: |
| 55 | raise http.Http404(exc) |
| 56 | |
| 57 | instance.read_query_parameters(request.GET) |
| 58 | return instance.handle_request() |
| 59 | except exceptions.RequestError, exc: |
| 60 | return exc.response |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 61 | |
| 62 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 63 | def handle_request(self): |
| 64 | if self._request.method.upper() not in self._permitted_methods: |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 65 | return http.HttpResponseNotAllowed(self._permitted_methods) |
| 66 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 67 | handler = getattr(self, self._request.method.lower()) |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 68 | return handler() |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 69 | |
| 70 | |
| 71 | # the handler methods below only need to be overridden if the resource |
| 72 | # supports the method |
| 73 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 74 | def get(self): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 75 | """Handle a GET request. |
| 76 | |
| 77 | @returns an HttpResponse |
| 78 | """ |
| 79 | raise NotImplementedError |
| 80 | |
| 81 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 82 | def post(self): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 83 | """Handle a POST request. |
| 84 | |
| 85 | @returns an HttpResponse |
| 86 | """ |
| 87 | raise NotImplementedError |
| 88 | |
| 89 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 90 | def put(self): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 91 | """Handle a PUT request. |
| 92 | |
| 93 | @returns an HttpResponse |
| 94 | """ |
| 95 | raise NotImplementedError |
| 96 | |
| 97 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 98 | def delete(self): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 99 | """Handle a DELETE request. |
| 100 | |
| 101 | @returns an HttpResponse |
| 102 | """ |
| 103 | raise NotImplementedError |
| 104 | |
| 105 | |
| 106 | @classmethod |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 107 | def from_uri_args(cls, request, **kwargs): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 108 | """Construct an instance from URI args. |
| 109 | |
| 110 | Default implementation for resources with no URI args. |
| 111 | """ |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 112 | return cls(request) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 113 | |
| 114 | |
| 115 | def _uri_args(self): |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 116 | """Return kwargs for a URI reference to this resource. |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 117 | |
| 118 | Default implementation for resources with no URI args. |
| 119 | """ |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 120 | return {} |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 121 | |
| 122 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 123 | def _query_parameters_accepted(self): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 124 | """Return sequence of tuples (name, description) for query parameters. |
| 125 | |
| 126 | Documents the available query parameters for GETting this resource. |
| 127 | Default implementation for resources with no parameters. |
| 128 | """ |
| 129 | return () |
| 130 | |
| 131 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 132 | def read_query_parameters(self, parameters): |
| 133 | """Read relevant query parameters from a Django MultiValueDict.""" |
| 134 | for param_name, _ in self._query_parameters_accepted(): |
| 135 | if param_name in parameters: |
| 136 | self._query_params.setlist(param_name, |
| 137 | parameters.getlist(param_name)) |
| 138 | |
| 139 | |
| 140 | def set_query_parameters(self, **parameters): |
| 141 | """Set query parameters programmatically.""" |
| 142 | self._query_params.update(parameters) |
| 143 | |
| 144 | |
| 145 | def href(self, query_params=None): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 146 | """Return URI to this resource.""" |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 147 | kwargs = self._uri_args() |
| 148 | path = urlresolvers.reverse(self.dispatch_request, kwargs=kwargs) |
| 149 | full_query_params = datastructures.MultiValueDict(self._query_params) |
| 150 | if query_params: |
| 151 | full_query_params.update(query_params) |
| 152 | if full_query_params: |
| 153 | path += '?' + urllib.urlencode(full_query_params.lists(), |
| 154 | doseq=True) |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 155 | return self._request.build_absolute_uri(path) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 156 | |
| 157 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 158 | def resolve_uri(self, uri): |
| 159 | # check for absolute URIs |
| 160 | match = re.match(r'(?P<root>https?://[^/]+)(?P<path>/.*)', uri) |
| 161 | if match: |
| 162 | # is this URI for a different host? |
| 163 | my_root = self._request.build_absolute_uri('/') |
| 164 | request_root = match.group('root') + '/' |
| 165 | if my_root != request_root: |
| 166 | # might support this in the future, but not now |
| 167 | raise exceptions.BadRequest('Unable to resolve remote URI %s' |
| 168 | % uri) |
| 169 | uri = match.group('path') |
| 170 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 171 | try: |
| 172 | view_method, args, kwargs = urlresolvers.resolve(uri) |
| 173 | except http.Http404: |
| 174 | raise exceptions.BadRequest('Unable to resolve URI %s' % uri) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 175 | resource_class = view_method.im_self # class owning this classmethod |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 176 | return resource_class.from_uri_args(self._request, **kwargs) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 177 | |
| 178 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 179 | def resolve_link(self, link): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 180 | if isinstance(link, dict): |
| 181 | uri = link['href'] |
| 182 | elif isinstance(link, basestring): |
| 183 | uri = link |
| 184 | else: |
| 185 | raise exceptions.BadRequest('Unable to understand link %s' % link) |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 186 | return self.resolve_uri(uri) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 187 | |
| 188 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 189 | def link(self, query_params=None): |
| 190 | return {'href': self.href(query_params=query_params)} |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 191 | |
| 192 | |
| 193 | def _query_parameters_response(self): |
| 194 | return dict((name, description) |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 195 | for name, description in self._query_parameters_accepted()) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 196 | |
| 197 | |
| 198 | def _basic_response(self, content): |
| 199 | """Construct and return a simple 200 response.""" |
| 200 | assert isinstance(content, dict) |
| 201 | query_parameters = self._query_parameters_response() |
| 202 | if query_parameters: |
| 203 | content['query_parameters'] = query_parameters |
| 204 | encoded_content = simplejson.dumps(content) |
| 205 | return http.HttpResponse(encoded_content, |
| 206 | content_type=_JSON_CONTENT_TYPE) |
| 207 | |
| 208 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 209 | def _decoded_input(self): |
| 210 | content_type = self._request.META.get('CONTENT_TYPE', |
| 211 | _JSON_CONTENT_TYPE) |
| 212 | raw_data = self._request.raw_post_data |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 213 | if content_type == _JSON_CONTENT_TYPE: |
| 214 | try: |
| 215 | raw_dict = simplejson.loads(raw_data) |
| 216 | except ValueError, exc: |
| 217 | raise exceptions.BadRequest('Error decoding request body: ' |
| 218 | '%s\n%r' % (exc, raw_data)) |
| 219 | elif content_type == 'application/x-www-form-urlencoded': |
| 220 | cgi_dict = cgi.parse_qs(raw_data) # django won't do this for PUT |
| 221 | raw_dict = {} |
| 222 | for key, values in cgi_dict.items(): |
| 223 | value = values[-1] # take last value if multiple were given |
| 224 | try: |
| 225 | # attempt to parse numbers, booleans and nulls |
| 226 | raw_dict[key] = simplejson.loads(value) |
| 227 | except ValueError: |
| 228 | # otherwise, leave it as a string |
| 229 | raw_dict[key] = value |
| 230 | else: |
| 231 | raise exceptions.RequestError(415, 'Unsupported media type: %s' |
| 232 | % content_type) |
| 233 | |
| 234 | return _InputDict(raw_dict) |
| 235 | |
| 236 | |
| 237 | def _format_datetime(self, date_time): |
| 238 | """Return ISO 8601 string for the given datetime""" |
| 239 | if date_time is None: |
| 240 | return None |
| 241 | timezone_hrs = time.timezone / 60 / 60 # convert seconds to hours |
| 242 | if timezone_hrs >= 0: |
| 243 | timezone_join = '+' |
| 244 | else: |
| 245 | timezone_join = '' # minus sign comes from number itself |
| 246 | timezone_spec = '%s%s:00' % (timezone_join, timezone_hrs) |
| 247 | return date_time.strftime('%Y-%m-%dT%H:%M:%S') + timezone_spec |
| 248 | |
| 249 | |
| 250 | @classmethod |
| 251 | def _check_for_required_fields(cls, input_dict, fields): |
| 252 | assert isinstance(fields, (list, tuple)), fields |
| 253 | missing_fields = ', '.join(field for field in fields |
| 254 | if field not in input_dict) |
| 255 | if missing_fields: |
| 256 | raise exceptions.BadRequest('Missing input: ' + missing_fields) |
| 257 | |
| 258 | |
| 259 | class Entry(Resource): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 260 | @classmethod |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 261 | def add_query_selectors(cls, query_processor): |
| 262 | """Sbuclasses may override this to support querying.""" |
| 263 | pass |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 264 | |
| 265 | |
| 266 | def short_representation(self): |
| 267 | return self.link() |
| 268 | |
| 269 | |
| 270 | def full_representation(self): |
| 271 | return self.short_representation() |
| 272 | |
| 273 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 274 | def get(self): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 275 | return self._basic_response(self.full_representation()) |
| 276 | |
| 277 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 278 | def put(self): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 279 | try: |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 280 | self.update(self._decoded_input()) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 281 | except model_logic.ValidationError, exc: |
| 282 | raise exceptions.BadRequest('Invalid input: %s' % exc) |
| 283 | return self._basic_response(self.full_representation()) |
| 284 | |
| 285 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 286 | def _delete_entry(self): |
| 287 | raise NotImplementedError |
| 288 | |
| 289 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 290 | def delete(self): |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 291 | self._delete_entry() |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 292 | return http.HttpResponse(status=204) # No content |
| 293 | |
| 294 | |
| 295 | def create_instance(self, input_dict, containing_collection): |
| 296 | raise NotImplementedError |
| 297 | |
| 298 | |
| 299 | def update(self, input_dict): |
| 300 | raise NotImplementedError |
| 301 | |
| 302 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 303 | class InstanceEntry(Entry): |
| 304 | class NullEntry(object): |
| 305 | def link(self): |
| 306 | return None |
| 307 | |
| 308 | |
| 309 | def short_representation(self): |
| 310 | return None |
| 311 | |
| 312 | |
| 313 | _null_entry = NullEntry() |
| 314 | _permitted_methods = ('GET', 'PUT', 'DELETE') |
| 315 | model = None # subclasses must override this with a Django model class |
| 316 | |
| 317 | |
| 318 | def __init__(self, request, instance): |
| 319 | assert self.model is not None |
| 320 | super(Entry, self).__init__(request) |
| 321 | self.instance = instance |
| 322 | |
| 323 | |
| 324 | @classmethod |
| 325 | def from_optional_instance(cls, request, instance): |
| 326 | if instance is None: |
| 327 | return cls._null_entry |
| 328 | return cls(request, instance) |
| 329 | |
| 330 | |
| 331 | def _delete_entry(self): |
| 332 | self.instance.delete() |
| 333 | |
| 334 | |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 335 | class Collection(Resource): |
| 336 | _DEFAULT_ITEMS_PER_PAGE = 50 |
| 337 | |
| 338 | _permitted_methods=('GET', 'POST') |
| 339 | |
| 340 | # subclasses must override these |
| 341 | queryset = None # or override _fresh_queryset() directly |
| 342 | entry_class = None |
| 343 | |
| 344 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 345 | def __init__(self, request): |
| 346 | super(Collection, self).__init__(request) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 347 | assert self.entry_class is not None |
| 348 | if isinstance(self.entry_class, basestring): |
| 349 | type(self).entry_class = _resolve_class_path(self.entry_class) |
| 350 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 351 | self._query_processor = query_lib.QueryProcessor() |
| 352 | self.entry_class.add_query_selectors(self._query_processor) |
| 353 | |
| 354 | |
| 355 | def _query_parameters_accepted(self): |
| 356 | params = [('start_index', 'Index of first member to include'), |
| 357 | ('items_per_page', 'Number of members to include')] |
| 358 | for selector in self._query_processor.selectors(): |
| 359 | params.append((selector.name, selector.doc)) |
| 360 | return params |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 361 | |
| 362 | |
| 363 | def _fresh_queryset(self): |
| 364 | assert self.queryset is not None |
| 365 | # always copy the queryset before using it to avoid caching |
| 366 | return self.queryset.all() |
| 367 | |
| 368 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 369 | def _entry_from_instance(self, instance): |
| 370 | return self.entry_class(self._request, instance) |
| 371 | |
| 372 | |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 373 | def _representation(self, entry_instances): |
| 374 | members = [] |
| 375 | for instance in entry_instances: |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 376 | entry = self._entry_from_instance(instance) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 377 | members.append(entry.short_representation()) |
| 378 | |
| 379 | rep = self.link() |
| 380 | rep.update({'members': members}) |
| 381 | return rep |
| 382 | |
| 383 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 384 | def _read_int_parameter(self, name, default): |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 385 | if name not in self._query_params: |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 386 | return default |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 387 | input_value = self._query_params[name] |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 388 | try: |
| 389 | return int(input_value) |
| 390 | except ValueError: |
| 391 | raise exceptions.BadRequest('Invalid non-numeric value for %s: %r' |
| 392 | % (name, input_value)) |
| 393 | |
| 394 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 395 | def _apply_form_query(self, queryset): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 396 | """Apply any query selectors passed as form variables.""" |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 397 | for parameter, values in self._query_params.lists(): |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 398 | if not self._query_processor.has_selector(parameter): |
| 399 | continue |
| 400 | for value in values: # forms keys can have multiple values |
| 401 | queryset = self._query_processor.apply_selector(queryset, |
| 402 | parameter, |
| 403 | value) |
| 404 | return queryset |
| 405 | |
| 406 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 407 | def _filtered_queryset(self): |
| 408 | return self._apply_form_query(self._fresh_queryset()) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 409 | |
| 410 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 411 | def get(self): |
| 412 | queryset = self._filtered_queryset() |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 413 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 414 | items_per_page = self._read_int_parameter('items_per_page', |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 415 | self._DEFAULT_ITEMS_PER_PAGE) |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 416 | start_index = self._read_int_parameter('start_index', 0) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 417 | page = queryset[start_index:(start_index + items_per_page)] |
| 418 | |
| 419 | rep = self._representation(page) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 420 | rep.update({'total_results': len(queryset), |
| 421 | 'start_index': start_index, |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 422 | 'items_per_page': items_per_page}) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 423 | return self._basic_response(rep) |
| 424 | |
| 425 | |
| 426 | def full_representation(self): |
| 427 | # careful, this rep can be huge for large collections |
| 428 | return self._representation(self._fresh_queryset()) |
| 429 | |
| 430 | |
showard | f46ad4c | 2010-02-03 20:28:59 +0000 | [diff] [blame] | 431 | def post(self): |
| 432 | input_dict = self._decoded_input() |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 433 | try: |
| 434 | instance = self.entry_class.create_instance(input_dict, self) |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 435 | entry = self._entry_from_instance(instance) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 436 | entry.update(input_dict) |
| 437 | except model_logic.ValidationError, exc: |
| 438 | raise exceptions.BadRequest('Invalid input: %s' % exc) |
| 439 | # RFC 2616 specifies that we provide the new URI in both the Location |
| 440 | # header and the body |
| 441 | response = http.HttpResponse(status=201, # Created |
| 442 | content=entry.href()) |
| 443 | response['Location'] = entry.href() |
| 444 | return response |
| 445 | |
| 446 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 447 | class Relationship(Entry): |
| 448 | _permitted_methods = ('GET', 'DELETE') |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 449 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 450 | # subclasses must override this with a dict mapping name to entry class |
| 451 | related_classes = None |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 452 | |
| 453 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 454 | def __init__(self, **kwargs): |
| 455 | assert len(self.related_classes) == 2 |
| 456 | self.entries = dict((name, kwargs[name]) |
| 457 | for name in self.related_classes) |
| 458 | for name in self.related_classes: # sanity check |
| 459 | assert isinstance(self.entries[name], self.related_classes[name]) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 460 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 461 | # just grab the request from one of the entries |
| 462 | some_entry = self.entries.itervalues().next() |
| 463 | super(Relationship, self).__init__(some_entry._request) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 464 | |
| 465 | |
| 466 | @classmethod |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 467 | def from_uri_args(cls, request, **kwargs): |
| 468 | # kwargs contains URI args for each entry |
| 469 | entries = {} |
| 470 | for name, entry_class in cls.related_classes.iteritems(): |
| 471 | entries[name] = entry_class.from_uri_args(request, **kwargs) |
| 472 | return cls(**entries) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 473 | |
| 474 | |
| 475 | def _uri_args(self): |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 476 | kwargs = {} |
| 477 | for name, entry in self.entries.iteritems(): |
| 478 | kwargs.update(entry._uri_args()) |
| 479 | return kwargs |
| 480 | |
| 481 | |
| 482 | def short_representation(self): |
| 483 | rep = self.link() |
| 484 | for name, entry in self.entries.iteritems(): |
| 485 | rep[name] = entry.short_representation() |
| 486 | return rep |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 487 | |
| 488 | |
| 489 | @classmethod |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 490 | def _get_related_manager(cls, instance): |
| 491 | """Get the related objects manager for the given instance. |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 492 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 493 | The instance must be one of the related classes. This method will |
| 494 | return the related manager from that instance to instances of the other |
| 495 | related class. |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 496 | """ |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 497 | this_model = type(instance) |
| 498 | models = [entry_class.model for entry_class |
| 499 | in cls.related_classes.values()] |
| 500 | if isinstance(instance, models[0]): |
| 501 | this_model, other_model = models |
| 502 | else: |
| 503 | other_model, this_model = models |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 504 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 505 | _, field = this_model.objects.determine_relationship(other_model) |
| 506 | this_models_fields = (this_model._meta.fields |
| 507 | + this_model._meta.many_to_many) |
| 508 | if field in this_models_fields: |
| 509 | manager_name = field.attname |
| 510 | else: |
| 511 | # related manager is on other_model, get name of reverse related |
| 512 | # manager on this_model |
| 513 | manager_name = field.related.get_accessor_name() |
| 514 | |
| 515 | return getattr(instance, manager_name) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 516 | |
| 517 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 518 | def _delete_entry(self): |
| 519 | # choose order arbitrarily |
| 520 | entry, other_entry = self.entries.itervalues() |
| 521 | related_manager = self._get_related_manager(entry.instance) |
| 522 | related_manager.remove(other_entry.instance) |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 523 | |
| 524 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 525 | @classmethod |
| 526 | def create_instance(cls, input_dict, containing_collection): |
| 527 | other_name = containing_collection.unfixed_name |
| 528 | cls._check_for_required_fields(input_dict, (other_name,)) |
| 529 | entry = containing_collection.fixed_entry |
| 530 | other_entry = containing_collection.resolve_link(input_dict[other_name]) |
| 531 | related_manager = cls._get_related_manager(entry.instance) |
| 532 | related_manager.add(other_entry.instance) |
| 533 | return other_entry.instance |
showard | f828c77 | 2010-01-25 21:49:42 +0000 | [diff] [blame] | 534 | |
| 535 | |
jamesren | 3981f44 | 2010-02-16 19:27:59 +0000 | [diff] [blame] | 536 | def update(self, input_dict): |
| 537 | pass |
| 538 | |
| 539 | |
| 540 | class RelationshipCollection(Collection): |
| 541 | def __init__(self, request=None, fixed_entry=None): |
| 542 | if request is None: |
| 543 | request = fixed_entry._request |
| 544 | super(RelationshipCollection, self).__init__(request) |
| 545 | |
| 546 | assert issubclass(self.entry_class, Relationship) |
| 547 | self.related_classes = self.entry_class.related_classes |
| 548 | self.fixed_name = None |
| 549 | self.fixed_entry = None |
| 550 | self.unfixed_name = None |
| 551 | self.related_manager = None |
| 552 | |
| 553 | if fixed_entry is not None: |
| 554 | self._set_fixed_entry(fixed_entry) |
| 555 | entry_uri_arg = self.fixed_entry._uri_args().values()[0] |
| 556 | self._query_params[self.fixed_name] = entry_uri_arg |
| 557 | |
| 558 | |
| 559 | def _set_fixed_entry(self, entry): |
| 560 | """Set the fixed entry for this collection. |
| 561 | |
| 562 | The entry must be an instance of one of the related entry classes. This |
| 563 | method must be called before a relationship is used. It gets called |
| 564 | either from the constructor (when collections are instantiated from |
| 565 | other resource handling code) or from read_query_parameters() (when a |
| 566 | request is made directly for the collection. |
| 567 | """ |
| 568 | names = self.related_classes.keys() |
| 569 | if isinstance(entry, self.related_classes[names[0]]): |
| 570 | self.fixed_name, self.unfixed_name = names |
| 571 | else: |
| 572 | assert isinstance(entry, self.related_classes[names[1]]) |
| 573 | self.unfixed_name, self.fixed_name = names |
| 574 | self.fixed_entry = entry |
| 575 | self.unfixed_class = self.related_classes[self.unfixed_name] |
| 576 | self.related_manager = self.entry_class._get_related_manager( |
| 577 | entry.instance) |
| 578 | |
| 579 | |
| 580 | def _query_parameters_accepted(self): |
| 581 | return [(name, 'Show relationships for this %s' % entry_class.__name__) |
| 582 | for name, entry_class |
| 583 | in self.related_classes.iteritems()] |
| 584 | |
| 585 | |
| 586 | def _resolve_query_param(self, name, uri_arg): |
| 587 | entry_class = self.related_classes[name] |
| 588 | return entry_class.from_uri_args(self._request, uri_arg) |
| 589 | |
| 590 | |
| 591 | def read_query_parameters(self, query_params): |
| 592 | super(RelationshipCollection, self).read_query_parameters(query_params) |
| 593 | if not self._query_params: |
| 594 | raise exceptions.BadRequest( |
| 595 | 'You must specify one of the parameters %s and %s' |
| 596 | % tuple(self.related_classes.keys())) |
| 597 | query_items = self._query_params.items() |
| 598 | fixed_entry = self._resolve_query_param(*query_items[0]) |
| 599 | self._set_fixed_entry(fixed_entry) |
| 600 | |
| 601 | if len(query_items) > 1: |
| 602 | other_fixed_entry = self._resolve_query_param(*query_items[1]) |
| 603 | self.related_manager = self.related_manager.filter( |
| 604 | pk=other_fixed_entry.instance.id) |
| 605 | |
| 606 | |
| 607 | def _entry_from_instance(self, instance): |
| 608 | unfixed_entry = self.unfixed_class(self._request, instance) |
| 609 | entries = {self.fixed_name: self.fixed_entry, |
| 610 | self.unfixed_name: unfixed_entry} |
| 611 | return self.entry_class(**entries) |
| 612 | |
| 613 | |
| 614 | def _fresh_queryset(self): |
| 615 | return self.related_manager.all() |