1from __future__ import unicode_literals 2 3from copy import copy, deepcopy 4from datetime import datetime 5import logging 6import sys 7from time import mktime 8import traceback 9import warnings 10from wsgiref.handlers import format_date_time 11 12from django.conf import settings 13from django.core.exceptions import ( 14 ObjectDoesNotExist, MultipleObjectsReturned, ValidationError, FieldDoesNotExist 15) 16from django.core.signals import got_request_exception 17from django.core.exceptions import ImproperlyConfigured 18from django.db.models.fields.related import ForeignKey 19from django.urls.conf import re_path 20from tastypie.utils.timezone import make_naive_utc 21try: 22 from django.contrib.gis.db.models.fields import GeometryField 23except (ImproperlyConfigured, ImportError): 24 GeometryField = None 25from django.db.models.constants import LOOKUP_SEP 26try: 27 from django.db.models.fields.related import\ 28 SingleRelatedObjectDescriptor as ReverseOneToOneDescriptor 29except ImportError: 30 from django.db.models.fields.related_descriptors import\ 31 ReverseOneToOneDescriptor 32 33from django.http import HttpResponse, HttpResponseNotFound, Http404 34from django.utils.cache import patch_cache_control, patch_vary_headers 35from django.utils.html import escape 36from django.views.decorators.csrf import csrf_exempt 37 38import six 39 40from tastypie.authentication import Authentication 41from tastypie.authorization import ReadOnlyAuthorization 42from tastypie.bundle import Bundle 43from tastypie.cache import NoCache 44from tastypie.compat import NoReverseMatch, reverse, Resolver404, get_script_prefix, is_ajax 45from tastypie.constants import ALL, ALL_WITH_RELATIONS 46from tastypie.exceptions import ( 47 NotFound, BadRequest, InvalidFilterError, HydrationError, InvalidSortError, 48 ImmediateHttpResponse, Unauthorized, UnsupportedFormat, 49 UnsupportedSerializationFormat, UnsupportedDeserializationFormat, 50) 51from tastypie import fields 52from tastypie import http 53from tastypie.paginator import Paginator 54from tastypie.serializers import Serializer 55from tastypie.throttle import BaseThrottle 56from tastypie.utils import ( 57 dict_strip_unicode_keys, is_valid_jsonp_callback_value, string_to_python, 58 trailing_slash, 59) 60from tastypie.utils.mime import determine_format, build_content_type 61from tastypie.validation import Validation 62from tastypie.compat import get_module_name, atomic_decorator 63 64 65def sanitize(text): 66 # We put the single quotes back, due to their frequent usage in exception 67 # messages. 68 return escape(text).replace(''', "'").replace('"', '"').replace(''', "'") 69 70 71class ResourceOptions(object): 72 """ 73 A configuration class for ``Resource``. 74 75 Provides sane defaults and the logic needed to augment these settings with 76 the internal ``class Meta`` used on ``Resource`` subclasses. 77 """ 78 serializer = Serializer() 79 authentication = Authentication() 80 authorization = ReadOnlyAuthorization() 81 cache = NoCache() 82 throttle = BaseThrottle() 83 validation = Validation() 84 paginator_class = Paginator 85 allowed_methods = ['get', 'post', 'put', 'delete', 'patch'] 86 list_allowed_methods = None 87 detail_allowed_methods = None 88 limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20) 89 max_limit = 1000 90 api_name = None 91 resource_name = None 92 urlconf_namespace = None 93 default_format = 'application/json' 94 filtering = {} 95 ordering = [] 96 object_class = None 97 queryset = None 98 fields = None 99 excludes = [] 100 include_resource_uri = True 101 include_absolute_url = False 102 always_return_data = False 103 collection_name = 'objects' 104 detail_uri_name = 'pk' 105 106 def __new__(cls, meta=None): 107 overrides = {} 108 109 # Handle overrides. 110 if meta: 111 for override_name in dir(meta): 112 # No internals please. 113 if not override_name.startswith('_'): 114 overrides[override_name] = getattr(meta, override_name) 115 116 allowed_methods = overrides.get('allowed_methods', ['get', 'post', 'put', 'delete', 'patch']) 117 118 if overrides.get('list_allowed_methods', None) is None: 119 overrides['list_allowed_methods'] = allowed_methods 120 121 if overrides.get('detail_allowed_methods', None) is None: 122 overrides['detail_allowed_methods'] = allowed_methods 123 124 if six.PY3: 125 return object.__new__(type('ResourceOptions', (cls,), overrides)) 126 else: 127 return object.__new__(type(b'ResourceOptions', (cls,), overrides)) 128 129 130class DeclarativeMetaclass(type): 131 def __new__(cls, name, bases, attrs): 132 attrs['base_fields'] = {} 133 declared_fields = {} 134 135 # Inherit any fields from parent(s). 136 parents = [b for b in bases if issubclass(b, Resource)] 137 # Simulate the MRO. 138 parents.reverse() 139 140 for p in parents: 141 parent_fields = getattr(p, 'base_fields', {}) 142 143 for field_name, field_object in parent_fields.items(): 144 attrs['base_fields'][field_name] = deepcopy(field_object) 145 146 for field_name, obj in attrs.copy().items(): 147 # Look for ``dehydrated_type`` instead of doing ``isinstance``, 148 # which can break down if Tastypie is re-namespaced as something 149 # else. 150 if hasattr(obj, 'dehydrated_type'): 151 field = attrs.pop(field_name) 152 declared_fields[field_name] = field 153 154 attrs['base_fields'].update(declared_fields) 155 attrs['declared_fields'] = declared_fields 156 new_class = super(DeclarativeMetaclass, cls).__new__(cls, name, bases, attrs) 157 opts = getattr(new_class, 'Meta', None) 158 new_class._meta = ResourceOptions(opts) 159 abstract = getattr(new_class._meta, 'abstract', False) 160 161 if not getattr(new_class._meta, 'resource_name', None): 162 # No ``resource_name`` provided. Attempt to auto-name the resource. 163 class_name = new_class.__name__ 164 name_bits = [bit for bit in class_name.split('Resource') if bit] 165 resource_name = ''.join(name_bits).lower() 166 new_class._meta.resource_name = resource_name 167 168 if getattr(new_class._meta, 'include_resource_uri', True): 169 if 'resource_uri' not in new_class.base_fields: 170 new_class.base_fields['resource_uri'] = fields.CharField(readonly=True, verbose_name="resource uri") 171 elif 'resource_uri' in new_class.base_fields and 'resource_uri' not in attrs: 172 del(new_class.base_fields['resource_uri']) 173 174 if abstract and 'resource_uri' not in attrs: 175 # abstract classes don't have resource_uris unless explicitly provided 176 if 'resource_uri' in new_class.base_fields: 177 del(new_class.base_fields['resource_uri']) 178 179 for field_name, field_object in new_class.base_fields.items(): 180 if hasattr(field_object, 'contribute_to_class'): 181 field_object.contribute_to_class(new_class, field_name) 182 183 return new_class 184 185 186class Resource(six.with_metaclass(DeclarativeMetaclass)): 187 """ 188 Handles the data, request dispatch and responding to requests. 189 190 Serialization/deserialization is handled "at the edges" (i.e. at the 191 beginning/end of the request/response cycle) so that everything internally 192 is Python data structures. 193 194 This class tries to be non-model specific, so it can be hooked up to other 195 data sources, such as search results, files, other data, etc. 196 """ 197 def __init__(self, api_name=None): 198 # this can cause: 199 # TypeError: object.__new__(method-wrapper) is not safe, use method-wrapper.__new__() 200 # when trying to copy a generator used as a default. Wrap call to 201 # generator in lambda to get around this error. 202 self.fields = {k: copy(v) for k, v in self.base_fields.items()} 203 204 if api_name is not None: 205 self._meta.api_name = api_name 206 207 def __getattr__(self, name): 208 if name == '__setstate__': 209 raise AttributeError(name) 210 try: 211 return self.fields[name] 212 except KeyError: 213 raise AttributeError(name) 214 215 def wrap_view(self, view): 216 """ 217 Wraps methods so they can be called in a more functional way as well 218 as handling exceptions better. 219 220 Note that if ``BadRequest`` or an exception with a ``response`` attr 221 are seen, there is special handling to either present a message back 222 to the user or return the response traveling with the exception. 223 """ 224 @csrf_exempt 225 def wrapper(request, *args, **kwargs): 226 try: 227 callback = getattr(self, view) 228 response = callback(request, *args, **kwargs) 229 230 # Our response can vary based on a number of factors, use 231 # the cache class to determine what we should ``Vary`` on so 232 # caches won't return the wrong (cached) version. 233 varies = getattr(self._meta.cache, "varies", []) 234 235 if varies: 236 patch_vary_headers(response, varies) 237 238 if self._meta.cache.cacheable(request, response): 239 if self._meta.cache.cache_control(): 240 # If the request is cacheable and we have a 241 # ``Cache-Control`` available then patch the header. 242 patch_cache_control(response, **self._meta.cache.cache_control()) 243 244 if is_ajax(request) and not response.has_header("Cache-Control"): 245 # IE excessively caches XMLHttpRequests, so we're disabling 246 # the browser cache here. 247 # See http://www.enhanceie.com/ie/bugs.asp for details. 248 patch_cache_control(response, no_cache=True) 249 250 return response 251 except (BadRequest, fields.ApiFieldError) as e: 252 data = {"error": sanitize(e.args[0]) if getattr(e, 'args') else ''} 253 return self.error_response(request, data, response_class=http.HttpBadRequest) 254 except ValidationError as e: 255 data = {"error": sanitize(e.messages)} 256 return self.error_response(request, data, response_class=http.HttpBadRequest) 257 except Exception as e: 258 # Prevent muting non-django's exceptions 259 # i.e. RequestException from 'requests' library 260 if hasattr(e, 'response') and isinstance(e.response, HttpResponse): 261 return e.response 262 263 # A real, non-expected exception. 264 # Handle the case where the full traceback is more helpful 265 # than the serialized error. 266 if settings.DEBUG and getattr(settings, 'TASTYPIE_FULL_DEBUG', False): 267 raise 268 269 # Re-raise the error to get a proper traceback when the error 270 # happend during a test case 271 if request.META.get('SERVER_NAME') == 'testserver': 272 raise 273 274 # Rather than re-raising, we're going to things similar to 275 # what Django does. The difference is returning a serialized 276 # error message. 277 return self._handle_500(request, e) 278 279 return wrapper 280 281 def get_response_class_for_exception(self, request, exception): 282 """ 283 Can be overridden to customize response classes used for uncaught 284 exceptions. Should always return a subclass of 285 ``django.http.HttpResponse``. 286 """ 287 if isinstance(exception, (NotFound, ObjectDoesNotExist, Http404)): 288 return HttpResponseNotFound 289 elif isinstance(exception, UnsupportedSerializationFormat): 290 return http.HttpNotAcceptable 291 elif isinstance(exception, UnsupportedDeserializationFormat): 292 return http.HttpUnsupportedMediaType 293 elif isinstance(exception, UnsupportedFormat): 294 return http.HttpBadRequest 295 296 return http.HttpApplicationError 297 298 def _handle_500(self, request, exception): 299 the_trace = traceback.format_exception(*sys.exc_info()) 300 if six.PY2: 301 the_trace = [ 302 six.text_type(line, 'utf-8') 303 for line in the_trace 304 ] 305 the_trace = u'\n'.join(the_trace) 306 307 response_class = self.get_response_class_for_exception(request, exception) 308 309 if settings.DEBUG: 310 data = { 311 "error_message": sanitize(six.text_type(exception)), 312 "traceback": the_trace, 313 } 314 else: 315 data = { 316 "error_message": getattr(settings, 'TASTYPIE_CANNED_ERROR', "Sorry, this request could not be processed. Please try again later."), 317 } 318 319 if response_class.status_code >= 500: 320 log = logging.getLogger('django.request.tastypie') 321 log.error('Internal Server Error: %s' % request.path, exc_info=True, 322 extra={'status_code': response_class.status_code, 'request': request}) 323 324 # Send the signal so other apps are aware of the exception. 325 got_request_exception.send(self.__class__, request=request) 326 327 return self.error_response(request, data, response_class=response_class) 328 329 def _build_reverse_url(self, name, args=None, kwargs=None): 330 """ 331 A convenience hook for overriding how URLs are built. 332 333 See ``NamespacedModelResource._build_reverse_url`` for an example. 334 """ 335 return reverse(name, args=args, kwargs=kwargs) 336 337 def base_urls(self): 338 """ 339 The standard URLs this ``Resource`` should respond to. 340 """ 341 return [ 342 re_path(r"^(?P<resource_name>%s)%s$" % (self._meta.resource_name, trailing_slash), self.wrap_view('dispatch_list'), name="api_dispatch_list"), 343 re_path(r"^(?P<resource_name>%s)/schema%s$" % (self._meta.resource_name, trailing_slash), self.wrap_view('get_schema'), name="api_get_schema"), 344 re_path(r"^(?P<resource_name>%s)/set/(?P<%s_list>.*?)%s$" % (self._meta.resource_name, self._meta.detail_uri_name, trailing_slash), self.wrap_view('get_multiple'), name="api_get_multiple"), 345 re_path(r"^(?P<resource_name>%s)/(?P<%s>.*?)%s$" % (self._meta.resource_name, self._meta.detail_uri_name, trailing_slash), self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), 346 ] 347 348 def override_urls(self): 349 """ 350 Deprecated. Will be removed by v1.0.0. Please use ``prepend_urls`` instead. 351 """ 352 return [] 353 354 def prepend_urls(self): 355 """ 356 A hook for adding your own URLs or matching before the default URLs. 357 """ 358 return [] 359 360 @property 361 def urls(self): 362 """ 363 The endpoints this ``Resource`` responds to. 364 365 Mostly a standard URLconf, this is suitable for either automatic use 366 when registered with an ``Api`` class or for including directly in 367 a URLconf should you choose to. 368 """ 369 urls = self.prepend_urls() 370 371 overridden_urls = self.override_urls() 372 if overridden_urls: 373 warnings.warn("'override_urls' is a deprecated method & will be removed by v1.0.0. Please rename your method to ``prepend_urls``.") 374 urls += overridden_urls 375 376 urls += self.base_urls() 377 return urls 378 379 def determine_format(self, request): 380 """ 381 Used to determine the desired format. 382 383 Largely relies on ``tastypie.utils.mime.determine_format`` but here 384 as a point of extension. 385 """ 386 return determine_format(request, self._meta.serializer, default_format=self._meta.default_format) 387 388 def serialize(self, request, data, format, options=None): 389 """ 390 Given a request, data and a desired format, produces a serialized 391 version suitable for transfer over the wire. 392 393 Mostly a hook, this uses the ``Serializer`` from ``Resource._meta``. 394 """ 395 options = options or {} 396 397 if 'text/javascript' in format: 398 # get JSONP callback name. default to "callback" 399 callback = request.GET.get('callback', 'callback') 400 401 if not is_valid_jsonp_callback_value(callback): 402 raise BadRequest('JSONP callback name is invalid.') 403 404 options['callback'] = callback 405 406 return self._meta.serializer.serialize(data, format, options) 407 408 def deserialize(self, request, data, format='application/json'): 409 """ 410 Given a request, data and a format, deserializes the given data. 411 412 It relies on the request properly sending a ``CONTENT_TYPE`` header, 413 falling back to ``application/json`` if not provided. 414 415 Mostly a hook, this uses the ``Serializer`` from ``Resource._meta``. 416 """ 417 deserialized = self._meta.serializer.deserialize(data, format=request.META.get('CONTENT_TYPE', format)) 418 return deserialized 419 420 def alter_list_data_to_serialize(self, request, data): 421 """ 422 A hook to alter list data just before it gets serialized & sent to the user. 423 424 Useful for restructuring/renaming aspects of the what's going to be 425 sent. 426 427 Should accommodate for a list of objects, generally also including 428 meta data. 429 """ 430 return data 431 432 def alter_detail_data_to_serialize(self, request, data): 433 """ 434 A hook to alter detail data just before it gets serialized & sent to the user. 435 436 Useful for restructuring/renaming aspects of the what's going to be 437 sent. 438 439 Should accommodate for receiving a single bundle of data. 440 """ 441 return data 442 443 def alter_deserialized_list_data(self, request, data): 444 """ 445 A hook to alter list data just after it has been received from the user & 446 gets deserialized. 447 448 Useful for altering the user data before any hydration is applied. 449 """ 450 return data 451 452 def alter_deserialized_detail_data(self, request, data): 453 """ 454 A hook to alter detail data just after it has been received from the user & 455 gets deserialized. 456 457 Useful for altering the user data before any hydration is applied. 458 """ 459 return data 460 461 def dispatch_list(self, request, **kwargs): 462 """ 463 A view for handling the various HTTP methods (GET/POST/PUT/DELETE) over 464 the entire list of resources. 465 466 Relies on ``Resource.dispatch`` for the heavy-lifting. 467 """ 468 return self.dispatch('list', request, **kwargs) 469 470 def dispatch_detail(self, request, **kwargs): 471 """ 472 A view for handling the various HTTP methods (GET/POST/PUT/DELETE) on 473 a single resource. 474 475 Relies on ``Resource.dispatch`` for the heavy-lifting. 476 """ 477 return self.dispatch('detail', request, **kwargs) 478 479 def dispatch(self, request_type, request, **kwargs): 480 """ 481 Handles the common operations (allowed HTTP method, authentication, 482 throttling, method lookup) surrounding most CRUD interactions. 483 """ 484 allowed_methods = getattr(self._meta, "%s_allowed_methods" % request_type, None) 485 486 if 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META: 487 request.method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE'] 488 489 request_method = self.method_check(request, allowed=allowed_methods) 490 method = getattr(self, "%s_%s" % (request_method, request_type), None) 491 492 if method is None: 493 raise ImmediateHttpResponse(response=http.HttpNotImplemented()) 494 495 self.is_authenticated(request) 496 self.throttle_check(request) 497 498 # All clear. Process the request. 499 request = convert_post_to_put(request) 500 response = method(request, **kwargs) 501 502 # Add the throttled request. 503 self.log_throttled_access(request) 504 505 # If what comes back isn't a ``HttpResponse``, assume that the 506 # request was accepted and that some action occurred. This also 507 # prevents Django from freaking out. 508 if not isinstance(response, HttpResponse): 509 return http.HttpNoContent() 510 511 return response 512 513 def remove_api_resource_names(self, url_dict): 514 """ 515 Given a dictionary of regex matches from a URLconf, removes 516 ``api_name`` and/or ``resource_name`` if found. 517 518 This is useful for converting URLconf matches into something suitable 519 for data lookup. For example:: 520 521 Model.objects.filter(**self.remove_api_resource_names(matches)) 522 """ 523 kwargs_subset = url_dict.copy() 524 525 for key in ['api_name', 'resource_name']: 526 try: 527 del(kwargs_subset[key]) 528 except KeyError: 529 pass 530 531 return kwargs_subset 532 533 def method_check(self, request, allowed=None): 534 """ 535 Ensures that the HTTP method used on the request is allowed to be 536 handled by the resource. 537 538 Takes an ``allowed`` parameter, which should be a list of lowercase 539 HTTP methods to check against. Usually, this looks like:: 540 541 # The most generic lookup. 542 self.method_check(request, self._meta.allowed_methods) 543 544 # A lookup against what's allowed for list-type methods. 545 self.method_check(request, self._meta.list_allowed_methods) 546 547 # A useful check when creating a new endpoint that only handles 548 # GET. 549 self.method_check(request, ['get']) 550 """ 551 if allowed is None: 552 allowed = [] 553 554 request_method = request.method.lower() 555 allows = ','.join([meth.upper() for meth in allowed]) 556 557 if request_method == "options": 558 response = HttpResponse(allows) 559 response['Allow'] = allows 560 raise ImmediateHttpResponse(response=response) 561 562 if request_method not in allowed: 563 response = http.HttpMethodNotAllowed(allows) 564 response['Allow'] = allows 565 raise ImmediateHttpResponse(response=response) 566 567 return request_method 568 569 def is_authenticated(self, request): 570 """ 571 Handles checking if the user is authenticated and dealing with 572 unauthenticated users. 573 574 Mostly a hook, this uses class assigned to ``authentication`` from 575 ``Resource._meta``. 576 """ 577 # Authenticate the request as needed. 578 auth_result = self._meta.authentication.is_authenticated(request) 579 580 if isinstance(auth_result, HttpResponse): 581 raise ImmediateHttpResponse(response=auth_result) 582 583 if auth_result is not True: 584 raise ImmediateHttpResponse(response=http.HttpUnauthorized()) 585 586 def throttle_check(self, request): 587 """ 588 Handles checking if the user should be throttled. 589 590 Mostly a hook, this uses class assigned to ``throttle`` from 591 ``Resource._meta``. 592 """ 593 identifier = self._meta.authentication.get_identifier(request) 594 595 # Check to see if they should be throttled. 596 throttle = self._meta.throttle.should_be_throttled(identifier) 597 598 if throttle: 599 # Throttle limit exceeded. 600 601 response = http.HttpTooManyRequests() 602 603 if isinstance(throttle, int) and not isinstance(throttle, bool): 604 response['Retry-After'] = throttle 605 elif isinstance(throttle, datetime): 606 # change to UTC (GMT) and make naive, to avoid wsgiref also doing an implicit TZ conversion 607 throttle_utc = make_naive_utc(throttle) 608 response['Retry-After'] = format_date_time(mktime(throttle_utc.timetuple())) 609 610 raise ImmediateHttpResponse(response=response) 611 612 def log_throttled_access(self, request): 613 """ 614 Handles the recording of the user's access for throttling purposes. 615 616 Mostly a hook, this uses class assigned to ``throttle`` from 617 ``Resource._meta``. 618 """ 619 request_method = request.method.lower() 620 self._meta.throttle.accessed(self._meta.authentication.get_identifier(request), url=request.get_full_path(), request_method=request_method) 621 622 def unauthorized_result(self, exception): 623 raise ImmediateHttpResponse(response=http.HttpUnauthorized()) 624 625 def authorized_read_list(self, object_list, bundle): 626 """ 627 Handles checking of permissions to see if the user has authorization 628 to GET this resource. 629 """ 630 try: 631 auth_result = self._meta.authorization.read_list(object_list, bundle) 632 except Unauthorized as e: 633 self.unauthorized_result(e) 634 635 return auth_result 636 637 def authorized_read_detail(self, object_list, bundle): 638 """ 639 Handles checking of permissions to see if the user has authorization 640 to GET this resource. 641 """ 642 try: 643 auth_result = self._meta.authorization.read_detail(object_list, bundle) 644 if auth_result is not True: 645 raise Unauthorized() 646 except Unauthorized as e: 647 self.unauthorized_result(e) 648 649 return auth_result 650 651 def authorized_create_list(self, object_list, bundle): 652 """ 653 Handles checking of permissions to see if the user has authorization 654 to POST this resource. 655 """ 656 try: 657 auth_result = self._meta.authorization.create_list(object_list, bundle) 658 except Unauthorized as e: 659 self.unauthorized_result(e) 660 661 return auth_result 662 663 def authorized_create_detail(self, object_list, bundle): 664 """ 665 Handles checking of permissions to see if the user has authorization 666 to POST this resource. 667 """ 668 try: 669 auth_result = self._meta.authorization.create_detail(object_list, bundle) 670 if auth_result is not True: 671 raise Unauthorized() 672 except Unauthorized as e: 673 self.unauthorized_result(e) 674 675 return auth_result 676 677 def authorized_update_list(self, object_list, bundle): 678 """ 679 Handles checking of permissions to see if the user has authorization 680 to PUT this resource. 681 """ 682 try: 683 auth_result = self._meta.authorization.update_list(object_list, bundle) 684 except Unauthorized as e: 685 self.unauthorized_result(e) 686 687 return auth_result 688 689 def authorized_update_detail(self, object_list, bundle): 690 """ 691 Handles checking of permissions to see if the user has authorization 692 to PUT this resource. 693 """ 694 try: 695 auth_result = self._meta.authorization.update_detail(object_list, bundle) 696 if auth_result is not True: 697 raise Unauthorized() 698 except Unauthorized as e: 699 self.unauthorized_result(e) 700 701 return auth_result 702 703 def authorized_delete_list(self, object_list, bundle): 704 """ 705 Handles checking of permissions to see if the user has authorization 706 to DELETE this resource. 707 """ 708 try: 709 auth_result = self._meta.authorization.delete_list(object_list, bundle) 710 except Unauthorized as e: 711 self.unauthorized_result(e) 712 713 return auth_result 714 715 def authorized_delete_detail(self, object_list, bundle): 716 """ 717 Handles checking of permissions to see if the user has authorization 718 to DELETE this resource. 719 """ 720 try: 721 auth_result = self._meta.authorization.delete_detail(object_list, bundle) 722 if not auth_result: 723 raise Unauthorized() 724 except Unauthorized as e: 725 self.unauthorized_result(e) 726 727 return auth_result 728 729 def build_bundle(self, obj=None, data=None, request=None, objects_saved=None, via_uri=None): 730 """ 731 Given either an object, a data dictionary or both, builds a ``Bundle`` 732 for use throughout the ``dehydrate/hydrate`` cycle. 733 734 If no object is provided, an empty object from 735 ``Resource._meta.object_class`` is created so that attempts to access 736 ``bundle.obj`` do not fail. 737 """ 738 if obj is None and self._meta.object_class: 739 obj = self._meta.object_class() 740 741 return Bundle( 742 obj=obj, 743 data=data, 744 request=request, 745 objects_saved=objects_saved, 746 via_uri=via_uri 747 ) 748 749 def build_filters(self, filters=None, ignore_bad_filters=False): 750 """ 751 Allows for the filtering of applicable objects. 752 753 This needs to be implemented at the user level.' 754 755 ``ModelResource`` includes a full working version specific to Django's 756 ``Models``. 757 """ 758 return filters 759 760 def apply_sorting(self, obj_list, options=None): 761 """ 762 Allows for the sorting of objects being returned. 763 764 This needs to be implemented at the user level. 765 766 ``ModelResource`` includes a full working version specific to Django's 767 ``Models``. 768 """ 769 return obj_list 770 771 def get_bundle_detail_data(self, bundle): 772 """ 773 Convenience method to return the ``detail_uri_name`` attribute off 774 ``bundle.obj``. 775 776 Usually just accesses ``bundle.obj.pk`` by default. 777 """ 778 return getattr(bundle.obj, self._meta.detail_uri_name, None) 779 780 # URL-related methods. 781 782 def detail_uri_kwargs(self, bundle_or_obj): 783 """ 784 Given a ``Bundle`` or an object (typically a ``Model`` instance), 785 it returns the extra kwargs needed to generate a detail URI. 786 787 By default, it uses this resource's ``detail_uri_name`` in order to 788 create the URI. 789 """ 790 kwargs = {} 791 792 if isinstance(bundle_or_obj, Bundle): 793 bundle_or_obj = bundle_or_obj.obj 794 795 kwargs[self._meta.detail_uri_name] = getattr(bundle_or_obj, self._meta.detail_uri_name) 796 797 return kwargs 798 799 def resource_uri_kwargs(self, bundle_or_obj=None): 800 """ 801 Builds a dictionary of kwargs to help generate URIs. 802 803 Automatically provides the ``Resource.Meta.resource_name`` (and 804 optionally the ``Resource.Meta.api_name`` if populated by an ``Api`` 805 object). 806 807 If the ``bundle_or_obj`` argument is provided, it calls 808 ``Resource.detail_uri_kwargs`` for additional bits to create 809 """ 810 kwargs = { 811 'resource_name': self._meta.resource_name, 812 } 813 814 if self._meta.api_name is not None: 815 kwargs['api_name'] = self._meta.api_name 816 817 if bundle_or_obj is not None: 818 kwargs.update(self.detail_uri_kwargs(bundle_or_obj)) 819 820 return kwargs 821 822 def get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list'): 823 """ 824 Handles generating a resource URI. 825 826 If the ``bundle_or_obj`` argument is not provided, it builds the URI 827 for the list endpoint. 828 829 If the ``bundle_or_obj`` argument is provided, it builds the URI for 830 the detail endpoint. 831 832 Return the generated URI. If that URI can not be reversed (not found 833 in the URLconf), it will return an empty string. 834 """ 835 if bundle_or_obj is not None: 836 url_name = 'api_dispatch_detail' 837 838 try: 839 return self._build_reverse_url(url_name, kwargs=self.resource_uri_kwargs(bundle_or_obj)) 840 except NoReverseMatch: 841 return '' 842 843 def get_via_uri(self, uri, request=None): 844 """ 845 This pulls apart the salient bits of the URI and populates the 846 resource via a ``obj_get``. 847 848 Optionally accepts a ``request``. 849 850 If you need custom behavior based on other portions of the URI, 851 simply override this method. 852 """ 853 prefix = get_script_prefix() 854 chomped_uri = uri 855 856 if prefix and chomped_uri.startswith(prefix): 857 chomped_uri = chomped_uri[len(prefix) - 1:] 858 859 # We know that we are dealing with a "detail" URI 860 # Look for the beginning of object key (last meaningful part of the URI) 861 end_of_resource_name = chomped_uri.rstrip('/').rfind('/') 862 if end_of_resource_name == -1: 863 raise NotFound("An incorrect URL was provided '%s' for the '%s' resource." % (uri, self.__class__.__name__)) 864 # We mangle the path a bit further & run URL resolution against *only* 865 # the current class (but up to detail key). This ought to prevent bad 866 # URLs from resolving to incorrect data. 867 split_url = chomped_uri.rstrip('/').rsplit('/', 1)[0] 868 if not split_url.endswith('/' + self._meta.resource_name): 869 raise NotFound("An incorrect URL was provided '%s' for the '%s' resource." % (uri, self.__class__.__name__)) 870 found_at = chomped_uri.rfind(self._meta.resource_name, 0, end_of_resource_name) 871 chomped_uri = chomped_uri[found_at:] 872 try: 873 for url_resolver in getattr(self, 'urls', []): 874 result = url_resolver.resolve(chomped_uri) 875 876 if result is not None: 877 view, args, kwargs = result 878 break 879 else: 880 raise Resolver404("URI not found in 'self.urls'.") 881 except Resolver404: 882 raise NotFound("The URL provided '%s' was not a link to a valid resource." % uri) 883 884 bundle = self.build_bundle(request=request) 885 return self.obj_get(bundle=bundle, **self.remove_api_resource_names(kwargs)) 886 887 # Data preparation. 888 889 def full_dehydrate(self, bundle, for_list=False): 890 """ 891 Given a bundle with an object instance, extract the information from it 892 to populate the resource. 893 """ 894 data = bundle.data 895 896 api_name = self._meta.api_name 897 resource_name = self._meta.resource_name 898 899 # Dehydrate each field. 900 for field_name, field_object in self.fields.items(): 901 # If it's not for use in this mode, skip 902 field_use_in = field_object.use_in 903 if callable(field_use_in): 904 if not field_use_in(bundle): 905 continue 906 else: 907 if field_use_in not in ['all', 'list' if for_list else 'detail']: 908 continue 909 910 # A touch leaky but it makes URI resolution work. 911 if field_object.dehydrated_type == 'related': 912 field_object.api_name = api_name 913 field_object.resource_name = resource_name 914 915 data[field_name] = field_object.dehydrate(bundle, for_list=for_list) 916 917 # Check for an optional method to do further dehydration. 918 method = getattr(self, "dehydrate_%s" % field_name, None) 919 920 if method: 921 data[field_name] = method(bundle) 922 923 bundle = self.dehydrate(bundle) 924 return bundle 925 926 def dehydrate(self, bundle): 927 """ 928 A hook to allow a final manipulation of data once all fields/methods 929 have built out the dehydrated data. 930 931 Useful if you need to access more than one dehydrated field or want 932 to annotate on additional data. 933 934 Must return the modified bundle. 935 """ 936 return bundle 937 938 def full_hydrate(self, bundle): 939 """ 940 Given a populated bundle, distill it and turn it back into 941 a full-fledged object instance. 942 """ 943 if bundle.obj is None: 944 bundle.obj = self._meta.object_class() 945 946 bundle = self.hydrate(bundle) 947 948 for field_name, field_object in self.fields.items(): 949 if field_object.readonly is True: 950 continue 951 952 # Check for an optional method to do further hydration. 953 method = getattr(self, "hydrate_%s" % field_name, None) 954 955 if method: 956 bundle = method(bundle) 957 958 if field_object.attribute: 959 value = field_object.hydrate(bundle) 960 961 # NOTE: We only get back a bundle when it is related field. 962 if isinstance(value, Bundle) and value.errors.get(field_name): 963 bundle.errors[field_name] = value.errors[field_name] 964 965 if value is not None or field_object.null: 966 # We need to avoid populating M2M data here as that will 967 # cause things to blow up. 968 if not field_object.is_related: 969 setattr(bundle.obj, field_object.attribute, value) 970 elif not field_object.is_m2m: 971 if value is not None: 972 # NOTE: A bug fix in Django (ticket #18153) fixes incorrect behavior 973 # which Tastypie was relying on. To fix this, we store value.obj to 974 # be saved later in save_related. 975 try: 976 setattr(bundle.obj, field_object.attribute, value.obj) 977 except (ValueError, ObjectDoesNotExist): 978 bundle.related_objects_to_save[field_object.attribute] = value.obj 979 # not required, not sent 980 elif field_object.blank and field_name not in bundle.data: 981 continue 982 elif field_object.null: 983 if not isinstance(getattr(bundle.obj.__class__, field_object.attribute, None), ReverseOneToOneDescriptor): 984 # only update if not a reverse one to one field 985 setattr(bundle.obj, field_object.attribute, value) 986 987 return bundle 988 989 def hydrate(self, bundle): 990 """ 991 A hook to allow an initial manipulation of data before all methods/fields 992 have built out the hydrated data. 993 994 Useful if you need to access more than one hydrated field or want 995 to annotate on additional data. 996 997 Must return the modified bundle. 998 """ 999 return bundle 1000 1001 def hydrate_m2m(self, bundle): 1002 """ 1003 Populate the ManyToMany data on the instance. 1004 """ 1005 if bundle.obj is None: 1006 raise HydrationError("You must call 'full_hydrate' before attempting to run 'hydrate_m2m' on %r." % self) 1007 1008 for field_name, field_object in self.fields.items(): 1009 if not field_object.is_m2m: 1010 continue 1011 1012 if field_object.attribute: 1013 # Note that we only hydrate the data, leaving the instance 1014 # unmodified. It's up to the user's code to handle this. 1015 # The ``ModelResource`` provides a working baseline 1016 # in this regard. 1017 bundle.data[field_name] = field_object.hydrate_m2m(bundle) 1018 1019 for field_name, field_object in self.fields.items(): 1020 if not field_object.is_m2m: 1021 continue 1022 1023 method = getattr(self, "hydrate_%s" % field_name, None) 1024 1025 if method: 1026 method(bundle) 1027 1028 return bundle 1029 1030 def build_schema(self): 1031 """ 1032 Returns a dictionary of all the fields on the resource and some 1033 properties about those fields. 1034 1035 Used by the ``schema/`` endpoint to describe what will be available. 1036 """ 1037 data = { 1038 'fields': {}, 1039 'default_format': self._meta.default_format, 1040 'allowed_list_http_methods': self._meta.list_allowed_methods, 1041 'allowed_detail_http_methods': self._meta.detail_allowed_methods, 1042 'default_limit': self._meta.limit, 1043 } 1044 1045 if self._meta.ordering: 1046 data['ordering'] = self._meta.ordering 1047 1048 if self._meta.filtering: 1049 data['filtering'] = self._meta.filtering 1050 1051 # Skip assigning pk_field_name for non-model resources 1052 try: 1053 pk_field_name = self._meta.queryset.model._meta.pk.name 1054 except AttributeError: 1055 pk_field_name = None 1056 1057 for field_name, field_object in self.fields.items(): 1058 data['fields'][field_name] = { 1059 'default': field_object.default, 1060 'type': field_object.dehydrated_type, 1061 'nullable': field_object.null, 1062 'blank': field_object.blank, 1063 'readonly': field_object.readonly, 1064 'help_text': field_object.help_text, 1065 'unique': field_object.unique, 1066 'primary_key': True if field_name == pk_field_name else False, 1067 'verbose_name': field_object.verbose_name or field_name.replace("_", " "), 1068 } 1069 1070 if field_object.dehydrated_type == 'related': 1071 if field_object.is_m2m: 1072 related_type = 'to_many' 1073 else: 1074 related_type = 'to_one' 1075 data['fields'][field_name]['related_type'] = related_type 1076 try: 1077 uri = self._build_reverse_url('api_get_schema', kwargs={ 1078 'api_name': self._meta.api_name, 1079 'resource_name': field_object.to_class()._meta.resource_name 1080 }) 1081 except NoReverseMatch: 1082 uri = '' 1083 data['fields'][field_name]['related_schema'] = uri 1084 1085 return data 1086 1087 def dehydrate_resource_uri(self, bundle): 1088 """ 1089 For the automatically included ``resource_uri`` field, dehydrate 1090 the URI for the given bundle. 1091 1092 Returns empty string if no URI can be generated. 1093 """ 1094 try: 1095 return self.get_resource_uri(bundle) 1096 except NotImplementedError: 1097 return '' 1098 except NoReverseMatch: 1099 return '' 1100 1101 def generate_cache_key(self, *args, **kwargs): 1102 """ 1103 Creates a unique-enough cache key. 1104 1105 This is based off the current api_name/resource_name/args/kwargs. 1106 """ 1107 smooshed = ["%s=%s" % (key, value) for key, value in kwargs.items()] 1108 1109 # Use a list plus a ``.join()`` because it's faster than concatenation. 1110 return "%s:%s:%s:%s" % (self._meta.api_name, self._meta.resource_name, ':'.join(args), ':'.join(sorted(smooshed))) 1111 1112 # Data access methods. 1113 1114 def get_object_list(self, request): 1115 """ 1116 A hook to allow making returning the list of available objects. 1117 1118 This needs to be implemented at the user level. 1119 1120 ``ModelResource`` includes a full working version specific to Django's 1121 ``Models``. 1122 """ 1123 raise NotImplementedError() 1124 1125 def can_create(self): 1126 """ 1127 Checks to ensure ``post`` is within ``allowed_methods``. 1128 """ 1129 allowed = set(self._meta.list_allowed_methods + self._meta.detail_allowed_methods) 1130 return 'post' in allowed 1131 1132 def can_update(self): 1133 """ 1134 Checks to ensure ``put`` is within ``allowed_methods``. 1135 1136 Used when hydrating related data. 1137 """ 1138 allowed = set(self._meta.list_allowed_methods + self._meta.detail_allowed_methods) 1139 return 'put' in allowed 1140 1141 def can_delete(self): 1142 """ 1143 Checks to ensure ``delete`` is within ``allowed_methods``. 1144 """ 1145 allowed = set(self._meta.list_allowed_methods + self._meta.detail_allowed_methods) 1146 return 'delete' in allowed 1147 1148 def apply_filters(self, request, applicable_filters): 1149 """ 1150 A hook to alter how the filters are applied to the object list. 1151 1152 This needs to be implemented at the user level. 1153 1154 ``ModelResource`` includes a full working version specific to Django's 1155 ``Models``. 1156 """ 1157 raise NotImplementedError() 1158 1159 def obj_get_list(self, bundle, **kwargs): 1160 """ 1161 Fetches the list of objects available on the resource. 1162 1163 This needs to be implemented at the user level. 1164 1165 ``ModelResource`` includes a full working version specific to Django's 1166 ``Models``. 1167 """ 1168 raise NotImplementedError() 1169 1170 def cached_obj_get_list(self, bundle, **kwargs): 1171 """ 1172 A version of ``obj_get_list`` that uses the cache as a means to get 1173 commonly-accessed data faster. 1174 """ 1175 cache_key = self.generate_cache_key('list', **kwargs) 1176 obj_list = self._meta.cache.get(cache_key) 1177 1178 if obj_list is None: 1179 obj_list = self.obj_get_list(bundle=bundle, **kwargs) 1180 self._meta.cache.set(cache_key, obj_list) 1181 1182 return obj_list 1183 1184 def obj_get(self, bundle, **kwargs): 1185 """ 1186 Fetches an individual object on the resource. 1187 1188 This needs to be implemented at the user level. If the object can not 1189 be found, this should raise a ``NotFound`` exception. 1190 1191 ``ModelResource`` includes a full working version specific to Django's 1192 ``Models``. 1193 """ 1194 raise NotImplementedError() 1195 1196 def cached_obj_get(self, bundle, **kwargs): 1197 """ 1198 A version of ``obj_get`` that uses the cache as a means to get 1199 commonly-accessed data faster. 1200 """ 1201 cache_key = self.generate_cache_key('detail', **kwargs) 1202 cached_bundle = self._meta.cache.get(cache_key) 1203 1204 if cached_bundle is None: 1205 cached_bundle = self.obj_get(bundle=bundle, **kwargs) 1206 self._meta.cache.set(cache_key, cached_bundle) 1207 1208 return cached_bundle 1209 1210 def obj_create(self, bundle, **kwargs): 1211 """ 1212 Creates a new object based on the provided data. 1213 1214 This needs to be implemented at the user level. 1215 1216 ``ModelResource`` includes a full working version specific to Django's 1217 ``Models``. 1218 """ 1219 raise NotImplementedError() 1220 1221 def obj_update(self, bundle, **kwargs): 1222 """ 1223 Updates an existing object (or creates a new object) based on the 1224 provided data. 1225 1226 This needs to be implemented at the user level. 1227 1228 ``ModelResource`` includes a full working version specific to Django's 1229 ``Models``. 1230 """ 1231 raise NotImplementedError() 1232 1233 def obj_delete_list(self, bundle, **kwargs): 1234 """ 1235 Deletes an entire list of objects. 1236 1237 This needs to be implemented at the user level. 1238 1239 ``ModelResource`` includes a full working version specific to Django's 1240 ``Models``. 1241 """ 1242 raise NotImplementedError() 1243 1244 def obj_delete_list_for_update(self, bundle, **kwargs): 1245 """ 1246 Deletes an entire list of objects, specific to PUT list. 1247 1248 This needs to be implemented at the user level. 1249 1250 ``ModelResource`` includes a full working version specific to Django's 1251 ``Models``. 1252 """ 1253 raise NotImplementedError() 1254 1255 def obj_delete(self, bundle, **kwargs): 1256 """ 1257 Deletes a single object. 1258 1259 This needs to be implemented at the user level. 1260 1261 ``ModelResource`` includes a full working version specific to Django's 1262 ``Models``. 1263 """ 1264 raise NotImplementedError() 1265 1266 def create_response(self, request, data, response_class=HttpResponse, **response_kwargs): 1267 """ 1268 Extracts the common "which-format/serialize/return-response" cycle. 1269 1270 Mostly a useful shortcut/hook. 1271 """ 1272 desired_format = self.determine_format(request) 1273 serialized = self.serialize(request, data, desired_format) 1274 return response_class(content=serialized, content_type=build_content_type(desired_format), **response_kwargs) 1275 1276 def error_response(self, request, errors, response_class=None): 1277 """ 1278 Extracts the common "which-format/serialize/return-error-response" 1279 cycle. 1280 1281 Should be used as much as possible to return errors. 1282 """ 1283 if response_class is None: 1284 response_class = http.HttpBadRequest 1285 1286 desired_format = None 1287 1288 if request: 1289 if request.GET.get('callback', None) is None: 1290 try: 1291 desired_format = self.determine_format(request) 1292 except BadRequest: 1293 pass # Fall through to default handler below 1294 else: 1295 # JSONP can cause extra breakage. 1296 desired_format = 'application/json' 1297 1298 if not desired_format: 1299 desired_format = self._meta.default_format 1300 1301 try: 1302 serialized = self.serialize(request, errors, desired_format) 1303 except BadRequest as e: 1304 error = "Additional errors occurred, but serialization of those errors failed." 1305 1306 if settings.DEBUG: 1307 error += " %s" % e 1308 1309 return response_class(content=error, content_type='text/plain') 1310 1311 return response_class(content=serialized, content_type=build_content_type(desired_format)) 1312 1313 def is_valid(self, bundle): 1314 """ 1315 Handles checking if the data provided by the user is valid. 1316 1317 Mostly a hook, this uses class assigned to ``validation`` from 1318 ``Resource._meta``. 1319 1320 If validation fails, an error is raised with the error messages 1321 serialized inside it. 1322 """ 1323 errors = self._meta.validation.is_valid(bundle, bundle.request) 1324 1325 if errors: 1326 bundle.errors[self._meta.resource_name] = errors 1327 return False 1328 1329 return True 1330 1331 def rollback(self, bundles): 1332 """ 1333 Given the list of bundles, delete all objects pertaining to those 1334 bundles. 1335 1336 This needs to be implemented at the user level. No exceptions should 1337 be raised if possible. 1338 1339 ``ModelResource`` includes a full working version specific to Django's 1340 ``Models``. 1341 """ 1342 raise NotImplementedError() 1343 1344 # Views. 1345 1346 def get_list(self, request, **kwargs): 1347 """ 1348 Returns a serialized list of resources. 1349 1350 Calls ``obj_get_list`` to provide the data, then handles that result 1351 set and serializes it. 1352 1353 Should return a HttpResponse (200 OK). 1354 """ 1355 # TODO: Uncached for now. Invalidation that works for everyone may be 1356 # impossible. 1357 base_bundle = self.build_bundle(request=request) 1358 objects = self.obj_get_list(bundle=base_bundle, **self.remove_api_resource_names(kwargs)) 1359 sorted_objects = self.apply_sorting(objects, options=request.GET) 1360 1361 paginator = self._meta.paginator_class(request.GET, sorted_objects, resource_uri=self.get_resource_uri(), limit=self._meta.limit, max_limit=self._meta.max_limit, collection_name=self._meta.collection_name) 1362 to_be_serialized = paginator.page() 1363 1364 # Dehydrate the bundles in preparation for serialization. 1365 bundles = [ 1366 self.full_dehydrate(self.build_bundle(obj=obj, request=request), for_list=True) 1367 for obj in to_be_serialized[self._meta.collection_name] 1368 ] 1369 1370 to_be_serialized[self._meta.collection_name] = bundles 1371 to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized) 1372 return self.create_response(request, to_be_serialized) 1373 1374 def get_detail(self, request, **kwargs): 1375 """ 1376 Returns a single serialized resource. 1377 1378 Calls ``cached_obj_get/obj_get`` to provide the data, then handles that result 1379 set and serializes it. 1380 1381 Should return a HttpResponse (200 OK). 1382 """ 1383 basic_bundle = self.build_bundle(request=request) 1384 1385 try: 1386 obj = self.cached_obj_get(bundle=basic_bundle, **self.remove_api_resource_names(kwargs)) 1387 except ObjectDoesNotExist: 1388 return http.HttpNotFound() 1389 except MultipleObjectsReturned: 1390 return http.HttpMultipleChoices("More than one resource is found at this URI.") 1391 1392 bundle = self.build_bundle(obj=obj, request=request) 1393 bundle = self.full_dehydrate(bundle) 1394 bundle = self.alter_detail_data_to_serialize(request, bundle) 1395 return self.create_response(request, bundle) 1396 1397 def post_list(self, request, **kwargs): 1398 """ 1399 Creates a new resource/object with the provided data. 1400 1401 Calls ``obj_create`` with the provided data and returns a response 1402 with the new resource's location. 1403 1404 If a new resource is created, return ``HttpCreated`` (201 Created). 1405 If ``Meta.always_return_data = True``, there will be a populated body 1406 of serialized data. 1407 """ 1408 deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) 1409 deserialized = self.alter_deserialized_detail_data(request, deserialized) 1410 bundle = self.build_bundle(data=dict_strip_unicode_keys(deserialized), request=request) 1411 updated_bundle = self.obj_create(bundle, **self.remove_api_resource_names(kwargs)) 1412 location = self.get_resource_uri(updated_bundle) 1413 1414 if not self._meta.always_return_data: 1415 return http.HttpCreated(location=location) 1416 else: 1417 updated_bundle = self.full_dehydrate(updated_bundle) 1418 updated_bundle = self.alter_detail_data_to_serialize(request, updated_bundle) 1419 return self.create_response(request, updated_bundle, response_class=http.HttpCreated, location=location) 1420 1421 def post_detail(self, request, **kwargs): 1422 """ 1423 Creates a new subcollection of the resource under a resource. 1424 1425 This is not implemented by default because most people's data models 1426 aren't self-referential. 1427 1428 If a new resource is created, return ``HttpCreated`` (201 Created). 1429 """ 1430 return http.HttpNotImplemented() 1431 1432 def put_list(self, request, **kwargs): 1433 """ 1434 Replaces a collection of resources with another collection. 1435 1436 Calls ``delete_list`` to clear out the collection then ``obj_create`` 1437 with the provided the data to create the new collection. 1438 1439 Return ``HttpNoContent`` (204 No Content) if 1440 ``Meta.always_return_data = False`` (default). 1441 1442 Return ``HttpResponse`` (200 OK) with new data if 1443 ``Meta.always_return_data = True``. 1444 """ 1445 deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) 1446 deserialized = self.alter_deserialized_list_data(request, deserialized) 1447 1448 if self._meta.collection_name not in deserialized: 1449 raise BadRequest("Invalid data sent: missing '%s'" % self._meta.collection_name) 1450 1451 basic_bundle = self.build_bundle(request=request) 1452 self.obj_delete_list_for_update(bundle=basic_bundle, **self.remove_api_resource_names(kwargs)) 1453 bundles_seen = [] 1454 1455 for object_data in deserialized[self._meta.collection_name]: 1456 bundle = self.build_bundle(data=dict_strip_unicode_keys(object_data), request=request) 1457 1458 # Attempt to be transactional, deleting any previously created 1459 # objects if validation fails. 1460 try: 1461 self.obj_create(bundle=bundle, **self.remove_api_resource_names(kwargs)) 1462 bundles_seen.append(bundle) 1463 except ImmediateHttpResponse: 1464 self.rollback(bundles_seen) 1465 raise 1466 1467 if not self._meta.always_return_data: 1468 return http.HttpNoContent() 1469 else: 1470 to_be_serialized = { 1471 self._meta.collection_name: [ 1472 self.full_dehydrate(b, for_list=True) 1473 for b in bundles_seen 1474 ] 1475 } 1476 to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized) 1477 return self.create_response(request, to_be_serialized) 1478 1479 def put_detail(self, request, **kwargs): 1480 """ 1481 Either updates an existing resource or creates a new one with the 1482 provided data. 1483 1484 Calls ``obj_update`` with the provided data first, but falls back to 1485 ``obj_create`` if the object does not already exist. 1486 1487 If a new resource is created, return ``HttpCreated`` (201 Created). 1488 If ``Meta.always_return_data = True``, there will be a populated body 1489 of serialized data. 1490 1491 If an existing resource is modified and 1492 ``Meta.always_return_data = False`` (default), return ``HttpNoContent`` 1493 (204 No Content). 1494 If an existing resource is modified and 1495 ``Meta.always_return_data = True``, return ``HttpAccepted`` (200 1496 OK). 1497 """ 1498 deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) 1499 deserialized = self.alter_deserialized_detail_data(request, deserialized) 1500 bundle = self.build_bundle(data=dict_strip_unicode_keys(deserialized), request=request) 1501 1502 try: 1503 updated_bundle = self.obj_update(bundle=bundle, **self.remove_api_resource_names(kwargs)) 1504 1505 if not self._meta.always_return_data: 1506 return http.HttpNoContent() 1507 else: 1508 # Invalidate prefetched_objects_cache for bundled object 1509 # because we might have changed a prefetched field 1510 updated_bundle.obj._prefetched_objects_cache = {} 1511 updated_bundle = self.full_dehydrate(updated_bundle) 1512 updated_bundle = self.alter_detail_data_to_serialize(request, updated_bundle) 1513 return self.create_response(request, updated_bundle) 1514 except (NotFound, MultipleObjectsReturned): 1515 updated_bundle = self.obj_create(bundle=bundle, **self.remove_api_resource_names(kwargs)) 1516 location = self.get_resource_uri(updated_bundle) 1517 1518 if not self._meta.always_return_data: 1519 return http.HttpCreated(location=location) 1520 else: 1521 updated_bundle = self.full_dehydrate(updated_bundle) 1522 updated_bundle = self.alter_detail_data_to_serialize(request, updated_bundle) 1523 return self.create_response(request, updated_bundle, response_class=http.HttpCreated, location=location) 1524 1525 def delete_list(self, request, **kwargs): 1526 """ 1527 Destroys a collection of resources/objects. 1528 1529 Calls ``obj_delete_list``. 1530 1531 If the resources are deleted, return ``HttpNoContent`` (204 No Content). 1532 """ 1533 bundle = self.build_bundle(request=request) 1534 self.obj_delete_list(bundle=bundle, request=request, **self.remove_api_resource_names(kwargs)) 1535 return http.HttpNoContent() 1536 1537 def delete_detail(self, request, **kwargs): 1538 """ 1539 Destroys a single resource/object. 1540 1541 Calls ``obj_delete``. 1542 1543 If the resource is deleted, return ``HttpNoContent`` (204 No Content). 1544 If the resource did not exist, return ``Http404`` (404 Not Found). 1545 """ 1546 # Manually construct the bundle here, since we don't want to try to 1547 # delete an empty instance. 1548 bundle = Bundle(request=request) 1549 1550 try: 1551 self.obj_delete(bundle=bundle, **self.remove_api_resource_names(kwargs)) 1552 return http.HttpNoContent() 1553 except NotFound: 1554 return http.HttpNotFound() 1555 1556 def patch_list(self, request, **kwargs): 1557 """ 1558 Updates a collection in-place. 1559 1560 The exact behavior of ``PATCH`` to a list resource is still the matter of 1561 some debate in REST circles, and the ``PATCH`` RFC isn't standard. So the 1562 behavior this method implements (described below) is something of a 1563 stab in the dark. It's mostly cribbed from GData, with a smattering 1564 of ActiveResource-isms and maybe even an original idea or two. 1565 1566 The ``PATCH`` format is one that's similar to the response returned from 1567 a ``GET`` on a list resource:: 1568 1569 { 1570 "objects": [{object}, {object}, ...], 1571 "deleted_objects": ["URI", "URI", "URI", ...], 1572 } 1573 1574 For each object in ``objects``: 1575 1576 * If the dict does not have a ``resource_uri`` key then the item is 1577 considered "new" and is handled like a ``POST`` to the resource list. 1578 1579 * If the dict has a ``resource_uri`` key and the ``resource_uri`` refers 1580 to an existing resource then the item is a update; it's treated 1581 like a ``PATCH`` to the corresponding resource detail. 1582 1583 * If the dict has a ``resource_uri`` but the resource *doesn't* exist, 1584 then this is considered to be a create-via-``PUT``. 1585 1586 Each entry in ``deleted_objects`` referes to a resource URI of an existing 1587 resource to be deleted; each is handled like a ``DELETE`` to the relevent 1588 resource. 1589 1590 In any case: 1591 1592 * If there's a resource URI it *must* refer to a resource of this 1593 type. It's an error to include a URI of a different resource. 1594 1595 * ``PATCH`` is all or nothing. If a single sub-operation fails, the 1596 entire request will fail and all resources will be rolled back. 1597 1598 * For ``PATCH`` to work, you **must** have ``put`` in your 1599 :ref:`detail-allowed-methods` setting. 1600 1601 * To delete objects via ``deleted_objects`` in a ``PATCH`` request you 1602 **must** have ``delete`` in your :ref:`detail-allowed-methods` 1603 setting. 1604 1605 Substitute appropriate names for ``objects`` and 1606 ``deleted_objects`` if ``Meta.collection_name`` is set to something 1607 other than ``objects`` (default). 1608 """ 1609 request = convert_post_to_patch(request) 1610 deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) 1611 1612 collection_name = self._meta.collection_name 1613 deleted_collection_name = 'deleted_%s' % collection_name 1614 if collection_name not in deserialized: 1615 raise BadRequest("Invalid data sent: missing '%s'" % collection_name) 1616 1617 if len(deserialized[collection_name]) and 'put' not in self._meta.detail_allowed_methods: 1618 raise ImmediateHttpResponse(response=http.HttpMethodNotAllowed()) 1619 1620 bundles_seen = [] 1621 1622 for data in deserialized[collection_name]: 1623 # If there's a resource_uri then this is either an 1624 # update-in-place or a create-via-PUT. 1625 if "resource_uri" in data: 1626 uri = data.pop('resource_uri') 1627 1628 try: 1629 obj = self.get_via_uri(uri, request=request) 1630 1631 # The object does exist, so this is an update-in-place. 1632 bundle = self.build_bundle(obj=obj, request=request) 1633 bundle = self.full_dehydrate(bundle, for_list=True) 1634 bundle = self.alter_detail_data_to_serialize(request, bundle) 1635 self.update_in_place(request, bundle, data) 1636 except (ObjectDoesNotExist, MultipleObjectsReturned): 1637 # The object referenced by resource_uri doesn't exist, 1638 # so this is a create-by-PUT equivalent. 1639 data = self.alter_deserialized_detail_data(request, data) 1640 bundle = self.build_bundle(data=dict_strip_unicode_keys(data), request=request) 1641 self.obj_create(bundle=bundle) 1642 else: 1643 # There's no resource URI, so this is a create call just 1644 # like a POST to the list resource. 1645 data = self.alter_deserialized_detail_data(request, data) 1646 bundle = self.build_bundle(data=dict_strip_unicode_keys(data), request=request) 1647 self.obj_create(bundle=bundle) 1648 1649 bundles_seen.append(bundle) 1650 1651 deleted_collection = deserialized.get(deleted_collection_name, []) 1652 1653 if deleted_collection: 1654 if 'delete' not in self._meta.detail_allowed_methods: 1655 raise ImmediateHttpResponse(response=http.HttpMethodNotAllowed()) 1656 1657 for uri in deleted_collection: 1658 obj = self.get_via_uri(uri, request=request) 1659 bundle = self.build_bundle(obj=obj, request=request) 1660 self.obj_delete(bundle=bundle) 1661 1662 if not self._meta.always_return_data: 1663 return http.HttpAccepted() 1664 else: 1665 to_be_serialized = { 1666 'objects': [ 1667 self.full_dehydrate(b, for_list=True) 1668 for b in bundles_seen 1669 ] 1670 } 1671 to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized) 1672 return self.create_response(request, to_be_serialized, response_class=http.HttpAccepted) 1673 1674 def patch_detail(self, request, **kwargs): 1675 """ 1676 Updates a resource in-place. 1677 1678 Calls ``obj_update``. 1679 1680 If the resource is updated, return ``HttpAccepted`` (202 Accepted). 1681 If the resource did not exist, return ``HttpNotFound`` (404 Not Found). 1682 """ 1683 request = convert_post_to_patch(request) 1684 basic_bundle = self.build_bundle(request=request) 1685 1686 # We want to be able to validate the update, but we can't just pass 1687 # the partial data into the validator since all data needs to be 1688 # present. Instead, we basically simulate a PUT by pulling out the 1689 # original data and updating it in-place. 1690 # So first pull out the original object. This is essentially 1691 # ``get_detail``. 1692 try: 1693 obj = self.cached_obj_get(bundle=basic_bundle, **self.remove_api_resource_names(kwargs)) 1694 except ObjectDoesNotExist: 1695 return http.HttpNotFound() 1696 except MultipleObjectsReturned: 1697 return http.HttpMultipleChoices("More than one resource is found at this URI.") 1698 1699 bundle = self.build_bundle(obj=obj, request=request) 1700 bundle = self.full_dehydrate(bundle) 1701 bundle = self.alter_detail_data_to_serialize(request, bundle) 1702 1703 # Now update the bundle in-place. 1704 deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) 1705 self.update_in_place(request, bundle, deserialized) 1706 1707 if not self._meta.always_return_data: 1708 return http.HttpAccepted() 1709 else: 1710 # Invalidate prefetched_objects_cache for bundled object 1711 # because we might have changed a prefetched field 1712 bundle.obj._prefetched_objects_cache = {} 1713 bundle = self.full_dehydrate(bundle) 1714 bundle = self.alter_detail_data_to_serialize(request, bundle) 1715 return self.create_response(request, bundle, response_class=http.HttpAccepted) 1716 1717 def update_in_place(self, request, original_bundle, new_data): 1718 """ 1719 Update the object in original_bundle in-place using new_data. 1720 """ 1721 original_bundle.data.update(**dict_strip_unicode_keys(new_data)) 1722 1723 # Now we've got a bundle with the new data sitting in it and we're 1724 # we're basically in the same spot as a PUT request. SO the rest of this 1725 # function is cribbed from put_detail. 1726 self.alter_deserialized_detail_data(request, original_bundle.data) 1727 kwargs = { 1728 self._meta.detail_uri_name: self.get_bundle_detail_data(original_bundle), 1729 'request': request, 1730 } 1731 return self.obj_update(bundle=original_bundle, **kwargs) 1732 1733 def get_schema(self, request, **kwargs): 1734 """ 1735 Returns a serialized form of the schema of the resource. 1736 1737 Calls ``build_schema`` to generate the data. This method only responds 1738 to HTTP GET. 1739 1740 Should return a HttpResponse (200 OK). 1741 """ 1742 self.method_check(request, allowed=['get']) 1743 self.is_authenticated(request) 1744 self.throttle_check(request) 1745 self.log_throttled_access(request) 1746 bundle = self.build_bundle(request=request) 1747 self.authorized_read_detail(self.get_object_list(bundle.request), bundle) 1748 return self.create_response(request, self.build_schema()) 1749 1750 def get_multiple(self, request, **kwargs): 1751 """ 1752 Returns a serialized list of resources based on the identifiers 1753 from the URL. 1754 1755 Calls ``obj_get_list`` to fetch only the objects requests in 1756 a single query. This method only responds to HTTP GET. 1757 1758 For backward compatibility the method ``obj_get`` is used if 1759 ``obj_get_list`` is not implemented. 1760 1761 Should return a HttpResponse (200 OK). 1762 """ 1763 self.method_check(request, allowed=['get']) 1764 self.is_authenticated(request) 1765 self.throttle_check(request) 1766 1767 # Rip apart the list then iterate. 1768 kwarg_name = '%s_list' % self._meta.detail_uri_name 1769 obj_identifiers = kwargs.get(kwarg_name, '').split(';') 1770 objects = [] 1771 not_found = [] 1772 base_bundle = self.build_bundle(request=request) 1773 1774 # We will try to get a queryset from obj_get_list. 1775 queryset = None 1776 1777 try: 1778 queryset = self.obj_get_list(bundle=base_bundle).filter( 1779 **{self._meta.detail_uri_name + '__in': obj_identifiers}) 1780 except NotImplementedError: 1781 pass 1782 1783 if queryset is not None: 1784 # Fetch the objects from the queryset to a dictionary. 1785 objects_dict = {} 1786 for obj in queryset: 1787 objects_dict[str(getattr(obj, self._meta.detail_uri_name))] = obj 1788 1789 # Walk the list of identifiers in order and get the objects or feed the not_found list. 1790 for identifier in obj_identifiers: 1791 if identifier in objects_dict: 1792 bundle = self.build_bundle(obj=objects_dict[identifier], request=request) 1793 bundle = self.full_dehydrate(bundle, for_list=True) 1794 objects.append(bundle) 1795 else: 1796 not_found.append(identifier) 1797 else: 1798 # Use the old way. 1799 for identifier in obj_identifiers: 1800 try: 1801 obj = self.obj_get(bundle=base_bundle, **{self._meta.detail_uri_name: identifier}) 1802 bundle = self.build_bundle(obj=obj, request=request) 1803 bundle = self.full_dehydrate(bundle, for_list=True) 1804 objects.append(bundle) 1805 except (ObjectDoesNotExist, Unauthorized): 1806 not_found.append(identifier) 1807 1808 object_list = { 1809 self._meta.collection_name: objects, 1810 } 1811 1812 if len(not_found): 1813 object_list['not_found'] = not_found 1814 1815 self.log_throttled_access(request) 1816 return self.create_response(request, object_list) 1817 1818 1819class ModelDeclarativeMetaclass(DeclarativeMetaclass): 1820 def __new__(cls, name, bases, attrs): 1821 meta = attrs.get('Meta') 1822 if getattr(meta, 'abstract', False): 1823 # abstract base classes do nothing on declaration 1824 new_class = super(ModelDeclarativeMetaclass, cls).__new__(cls, name, bases, attrs) 1825 return new_class 1826 1827 # Sanity check: ModelResource needs either a queryset or object_class: 1828 if meta and not hasattr(meta, 'queryset') and not hasattr(meta, 'object_class'): 1829 msg = "ModelResource (%s) requires Meta.object_class or Meta.queryset" 1830 raise ImproperlyConfigured(msg % name) 1831 1832 if hasattr(meta, 'queryset') and not hasattr(meta, 'object_class'): 1833 setattr(meta, 'object_class', meta.queryset.model) 1834 1835 new_class = super(ModelDeclarativeMetaclass, cls).__new__(cls, name, bases, attrs) 1836 specified_fields = getattr(new_class._meta, 'fields', None) 1837 excludes = getattr(new_class._meta, 'excludes', []) 1838 field_names = list(new_class.base_fields.keys()) 1839 1840 include_fields = specified_fields 1841 1842 if include_fields is None: 1843 if meta and meta.object_class: 1844 include_fields = [f.name for f in meta.object_class._meta.fields] 1845 else: 1846 include_fields = [] 1847 1848 for field_name in field_names: 1849 if field_name == 'resource_uri': 1850 continue 1851 if field_name in new_class.declared_fields: 1852 continue 1853 if specified_fields is not None and field_name not in include_fields: 1854 del(new_class.base_fields[field_name]) 1855 if field_name in excludes: 1856 del(new_class.base_fields[field_name]) 1857 1858 # Add in the new fields. 1859 new_class.base_fields.update(new_class.get_fields(include_fields, excludes)) 1860 1861 if getattr(new_class._meta, 'include_absolute_url', True): 1862 if 'absolute_url' not in new_class.base_fields: 1863 new_class.base_fields['absolute_url'] = fields.CharField(attribute='get_absolute_url', readonly=True) 1864 elif 'absolute_url' in new_class.base_fields and 'absolute_url' not in attrs: 1865 del(new_class.base_fields['absolute_url']) 1866 1867 return new_class 1868 1869 1870class BaseModelResource(Resource): 1871 """ 1872 A subclass of ``Resource`` designed to work with Django's ``Models``. 1873 1874 This class will introspect a given ``Model`` and build a field list based 1875 on the fields found on the model (excluding relational fields). 1876 1877 Given that it is aware of Django's ORM, it also handles the CRUD data 1878 operations of the resource. 1879 """ 1880 @classmethod 1881 def should_skip_field(cls, field): 1882 """ 1883 Given a Django model field, return if it should be included in the 1884 contributed ApiFields. 1885 """ 1886 if isinstance(field, ForeignKey): 1887 return True 1888 # Ignore certain fields (related fields). 1889 if hasattr(field, 'remote_field'): 1890 if field.remote_field: 1891 return True 1892 elif getattr(field, 'rel'): 1893 return True 1894 1895 return False 1896 1897 @classmethod 1898 def api_field_from_django_field(cls, f, default=fields.CharField): 1899 """ 1900 Returns the field type that would likely be associated with each 1901 Django type. 1902 """ 1903 result = default 1904 internal_type = f.get_internal_type() 1905 1906 if internal_type == 'DateField': 1907 result = fields.DateField 1908 elif internal_type == 'DateTimeField': 1909 result = fields.DateTimeField 1910 elif internal_type in ('BooleanField', 'NullBooleanField'): 1911 result = fields.BooleanField 1912 elif internal_type in ('FloatField',): 1913 result = fields.FloatField 1914 elif internal_type in ('DecimalField',): 1915 result = fields.DecimalField 1916 elif internal_type in ('IntegerField', 'PositiveIntegerField', 'PositiveSmallIntegerField', 'SmallIntegerField', 'AutoField', 'BigIntegerField', 'BigAutoField'): 1917 result = fields.IntegerField 1918 elif internal_type in ('FileField', 'ImageField'): 1919 result = fields.FileField 1920 elif internal_type == 'TimeField': 1921 result = fields.TimeField 1922 # TODO: Perhaps enable these via introspection. The reason they're not enabled 1923 # by default is the very different ``__init__`` they have over 1924 # the other fields. 1925 # elif internal_type == 'ForeignKey': 1926 # result = ForeignKey 1927 # elif internal_type == 'ManyToManyField': 1928 # result = ManyToManyField 1929 1930 return result 1931 1932 @classmethod 1933 def get_fields(cls, fields=None, excludes=None): 1934 """ 1935 Given any explicit fields to include and fields to exclude, add 1936 additional fields based on the associated model. 1937 """ 1938 final_fields = {} 1939 fields = fields or [] 1940 excludes = excludes or [] 1941 1942 if not cls._meta.object_class: 1943 return final_fields 1944 1945 for f in cls._meta.object_class._meta.fields: 1946 # If the field name is already present, skip 1947 if f.name in cls.base_fields: 1948 continue 1949 1950 # If field is not present in explicit field listing, skip 1951 if f.name not in fields: 1952 continue 1953 1954 # If field is in exclude list, skip 1955 if f.name in excludes: 1956 continue 1957 1958 if cls.should_skip_field(f): 1959 continue 1960 1961 api_field_class = cls.api_field_from_django_field(f) 1962 1963 kwargs = { 1964 'attribute': f.name, 1965 'help_text': f.help_text, 1966 'verbose_name': f.verbose_name, 1967 } 1968 1969 if f.null is True: 1970 kwargs['null'] = True 1971 1972 kwargs['unique'] = f.unique 1973 1974 if not f.null and f.blank is True: 1975 kwargs['default'] = '' 1976 kwargs['blank'] = True 1977 1978 if f.get_internal_type() == 'TextField': 1979 kwargs['default'] = '' 1980 1981 if f.has_default(): 1982 kwargs['default'] = f.default 1983 1984 if getattr(f, 'auto_now', False): 1985 kwargs['default'] = f.auto_now 1986 1987 if getattr(f, 'auto_now_add', False): 1988 kwargs['default'] = f.auto_now_add 1989 1990 final_fields[f.name] = api_field_class(**kwargs) 1991 final_fields[f.name].instance_name = f.name 1992 1993 return final_fields 1994 1995 def check_filtering(self, field_name, filter_type='exact', filter_bits=None): 1996 """ 1997 Given a field name, a optional filter type and an optional list of 1998 additional relations, determine if a field can be filtered on. 1999 2000 If a filter does not meet the needed conditions, it should raise an 2001 ``InvalidFilterError``. 2002 2003 If the filter meets the conditions, a list of attribute names (not 2004 field names) will be returned. 2005 """ 2006 if filter_bits is None: 2007 filter_bits = [] 2008 2009 if field_name not in self._meta.filtering: 2010 raise InvalidFilterError("The '%s' field does not allow filtering." % field_name) 2011 2012 # Check to see if it's an allowed lookup type. 2013 if self._meta.filtering[field_name] not in (ALL, ALL_WITH_RELATIONS): 2014 # Must be an explicit whitelist. 2015 if filter_type not in self._meta.filtering[field_name]: 2016 raise InvalidFilterError("'%s' is not an allowed filter on the '%s' field." % (filter_type, field_name)) 2017 2018 if self.fields[field_name].attribute is None: 2019 raise InvalidFilterError("The '%s' field has no 'attribute' for searching with." % field_name) 2020 2021 # Check to see if it's a relational lookup and if that's allowed. 2022 if len(filter_bits): 2023 if not getattr(self.fields[field_name], 'is_related', False): 2024 raise InvalidFilterError("The '%s' field does not support relations." % field_name) 2025 2026 if not self._meta.filtering[field_name] == ALL_WITH_RELATIONS: 2027 raise InvalidFilterError("Lookups are not allowed more than one level deep on the '%s' field." % field_name) 2028 2029 # Recursively descend through the remaining lookups in the filter, 2030 # if any. We should ensure that all along the way, we're allowed 2031 # to filter on that field by the related resource. 2032 related_resource = self.fields[field_name].get_related_resource(None) 2033 return [self.fields[field_name].attribute] + related_resource.check_filtering(filter_bits[0], filter_type, filter_bits[1:]) 2034 2035 return [self.fields[field_name].attribute] 2036 2037 def filter_value_to_python(self, value, field_name, filters, filter_expr, 2038 filter_type): 2039 """ 2040 Turn the string ``value`` into a python object. 2041 """ 2042 # Simple values 2043 value = string_to_python(value) 2044 2045 # Split on ',' if not empty string and either an in or range filter. 2046 if filter_type in ('in', 'range') and len(value): 2047 if hasattr(filters, 'getlist'): 2048 value = [] 2049 2050 for part in filters.getlist(filter_expr): 2051 value.extend(part.split(',')) 2052 else: 2053 value = value.split(',') 2054 2055 return value 2056 2057 def build_filters(self, filters=None, ignore_bad_filters=False): 2058 """ 2059 Given a dictionary of filters, create the necessary ORM-level filters. 2060 2061 Keys should be resource fields, **NOT** model fields. 2062 2063 Valid values are either a list of Django filter types (i.e. 2064 ``['startswith', 'exact', 'lte']``), the ``ALL`` constant or the 2065 ``ALL_WITH_RELATIONS`` constant. 2066 """ 2067 # At the declarative level: 2068 # filtering = { 2069 # 'resource_field_name': ['exact', 'startswith', 'endswith', 'contains'], 2070 # 'resource_field_name_2': ['exact', 'gt', 'gte', 'lt', 'lte', 'range'], 2071 # 'resource_field_name_3': ALL, 2072 # 'resource_field_name_4': ALL_WITH_RELATIONS, 2073 # ... 2074 # } 2075 # Accepts the filters as a dict. None by default, meaning no filters. 2076 if filters is None: 2077 filters = {} 2078 2079 qs_filters = {} 2080 2081 for filter_expr, value in filters.items(): 2082 filter_bits = filter_expr.split(LOOKUP_SEP) 2083 field_name = filter_bits.pop(0) 2084 filter_type = 'exact' 2085 2086 if field_name not in self.fields: 2087 # It's not a field we know about. Move along citizen. 2088 continue 2089 2090 # Validate filter types other than 'exact' that are supported by the field type 2091 try: 2092 django_field_name = self.fields[field_name].attribute 2093 django_field = self._meta.object_class._meta.get_field(django_field_name) 2094 if hasattr(django_field, 'field'): 2095 django_field = django_field.field # related field 2096 except FieldDoesNotExist: 2097 raise InvalidFilterError("The '%s' field is not a valid field name" % field_name) 2098 2099 query_terms = django_field.get_lookups().keys() 2100 if len(filter_bits) and filter_bits[-1] in query_terms: 2101 filter_type = filter_bits.pop() 2102 2103 try: 2104 lookup_bits = self.check_filtering(field_name, filter_type, filter_bits) 2105 except InvalidFilterError: 2106 if ignore_bad_filters: 2107 continue 2108 else: 2109 raise 2110 value = self.filter_value_to_python(value, field_name, filters, filter_expr, filter_type) 2111 2112 db_field_name = LOOKUP_SEP.join(lookup_bits) 2113 qs_filter = "%s%s%s" % (db_field_name, LOOKUP_SEP, filter_type) 2114 qs_filters[qs_filter] = value 2115 2116 return dict_strip_unicode_keys(qs_filters) 2117 2118 def apply_sorting(self, obj_list, options=None): 2119 """ 2120 Given a dictionary of options, apply some ORM-level sorting to the 2121 provided ``QuerySet``. 2122 2123 Looks for the ``order_by`` key and handles either ascending (just the 2124 field name) or descending (the field name with a ``-`` in front). 2125 2126 The field name should be the resource field, **NOT** model field. 2127 """ 2128 if options is None: 2129 options = {} 2130 2131 parameter_name = 'order_by' 2132 2133 if 'order_by' not in options: 2134 if 'sort_by' not in options: 2135 # Nothing to alter the order. Return what we've got. 2136 return obj_list 2137 else: 2138 warnings.warn("'sort_by' is a deprecated parameter. Please use 'order_by' instead.") 2139 parameter_name = 'sort_by' 2140 2141 order_by_args = [] 2142 2143 if hasattr(options, 'getlist'): 2144 order_bits = options.getlist(parameter_name) 2145 else: 2146 order_bits = options.get(parameter_name) 2147 2148 if not isinstance(order_bits, (list, tuple)): 2149 order_bits = [order_bits] 2150 2151 for order_by in order_bits: 2152 order_by_bits = order_by.split(LOOKUP_SEP) 2153 2154 field_name = order_by_bits[0] 2155 order = '' 2156 2157 if order_by_bits[0].startswith('-'): 2158 field_name = order_by_bits[0][1:] 2159 order = '-' 2160 2161 if field_name not in self.fields: 2162 # It's not a field we know about. Move along citizen. 2163 raise InvalidSortError("No matching '%s' field for ordering on." % field_name) 2164 2165 if field_name not in self._meta.ordering: 2166 raise InvalidSortError("The '%s' field does not allow ordering." % field_name) 2167 2168 if self.fields[field_name].attribute is None: 2169 raise InvalidSortError("The '%s' field has no 'attribute' for ordering with." % field_name) 2170 2171 order_by_args.append("%s%s" % (order, LOOKUP_SEP.join([self.fields[field_name].attribute] + order_by_bits[1:]))) 2172 2173 return obj_list.order_by(*order_by_args) 2174 2175 def apply_filters(self, request, applicable_filters): 2176 """ 2177 An ORM-specific implementation of ``apply_filters``. 2178 2179 The default simply applies the ``applicable_filters`` as ``**kwargs``, 2180 but should make it possible to do more advanced things. 2181 """ 2182 return self.get_object_list(request).filter(**applicable_filters) 2183 2184 def get_object_list(self, request): 2185 """ 2186 An ORM-specific implementation of ``get_object_list``. 2187 2188 Returns a queryset that may have been limited by other overrides. 2189 """ 2190 return self._meta.queryset._clone() 2191 2192 def obj_get_list(self, bundle, **kwargs): 2193 """ 2194 A ORM-specific implementation of ``obj_get_list``. 2195 2196 ``GET`` dictionary of bundle.request can be used to narrow the query. 2197 """ 2198 filters = {} 2199 2200 if hasattr(bundle.request, 'GET'): 2201 # Grab a mutable copy. 2202 filters = bundle.request.GET.copy() 2203 2204 # Update with the provided kwargs. 2205 filters.update(kwargs) 2206 applicable_filters = self.build_filters(filters=filters) 2207 2208 try: 2209 objects = self.apply_filters(bundle.request, applicable_filters) 2210 return self.authorized_read_list(objects, bundle) 2211 except ValueError: 2212 raise BadRequest("Invalid resource lookup data provided (mismatched type).") 2213 2214 def obj_get(self, bundle, **kwargs): 2215 """ 2216 A ORM-specific implementation of ``obj_get``. 2217 2218 Takes optional ``kwargs``, which are used to narrow the query to find 2219 the instance. 2220 """ 2221 # Use ignore_bad_filters=True. `obj_get_list` filters based on 2222 # request.GET, but `obj_get` usually filters based on `detail_uri_name` 2223 # or data from a related field, so we don't want to raise errors if 2224 # something doesn't explicitly match a configured filter. 2225 applicable_filters = self.build_filters(filters=kwargs, ignore_bad_filters=True) 2226 if self._meta.detail_uri_name in kwargs: 2227 applicable_filters[self._meta.detail_uri_name] = kwargs[self._meta.detail_uri_name] 2228 2229 try: 2230 object_list = self.apply_filters(bundle.request, applicable_filters) 2231 stringified_kwargs = ', '.join(["%s=%s" % (k, v) for k, v in applicable_filters.items()]) 2232 2233 if len(object_list) <= 0: 2234 raise self._meta.object_class.DoesNotExist("Couldn't find an instance of '%s' which matched '%s'." % (self._meta.object_class.__name__, stringified_kwargs)) 2235 elif len(object_list) > 1: 2236 raise MultipleObjectsReturned("More than one '%s' matched '%s'." % (self._meta.object_class.__name__, stringified_kwargs)) 2237 2238 bundle.obj = object_list[0] 2239 self.authorized_read_detail(object_list, bundle) 2240 return bundle.obj 2241 except ValueError: 2242 raise NotFound("Invalid resource lookup data provided (mismatched type).") 2243 2244 def obj_create(self, bundle, **kwargs): 2245 """ 2246 A ORM-specific implementation of ``obj_create``. 2247 """ 2248 bundle.obj = self._meta.object_class() 2249 2250 for key, value in kwargs.items(): 2251 setattr(bundle.obj, key, value) 2252 2253 bundle = self.full_hydrate(bundle) 2254 return self.save(bundle) 2255 2256 def lookup_kwargs_with_identifiers(self, bundle, kwargs): 2257 """ 2258 Kwargs here represent uri identifiers Ex: /repos/<user_id>/<repo_name>/ 2259 We need to turn those identifiers into Python objects for generating 2260 lookup parameters that can find them in the DB 2261 """ 2262 lookup_kwargs = {} 2263 2264 # Handle detail_uri_name specially 2265 if self._meta.detail_uri_name in kwargs: 2266 lookup_kwargs[self._meta.detail_uri_name] = kwargs.pop(self._meta.detail_uri_name) 2267 2268 bundle.obj = self.get_object_list(bundle.request).model() 2269 # Override data values, we rely on uri identifiers 2270 bundle.data.update(kwargs) 2271 # We're going to manually hydrate, as opposed to calling 2272 # ``full_hydrate``, to ensure we don't try to flesh out related 2273 # resources & keep things speedy. 2274 bundle = self.hydrate(bundle) 2275 2276 for identifier in kwargs: 2277 field_object = self.fields[identifier] 2278 2279 # Skip readonly or related fields. 2280 if field_object.readonly or field_object.is_related or\ 2281 not field_object.attribute: 2282 continue 2283 2284 # Check for an optional method to do further hydration. 2285 method = getattr(self, "hydrate_%s" % identifier, None) 2286 2287 if method: 2288 bundle = method(bundle) 2289 2290 lookup_kwargs[identifier] = field_object.hydrate(bundle) 2291 2292 return lookup_kwargs 2293 2294 def obj_update(self, bundle, skip_errors=False, **kwargs): 2295 """ 2296 A ORM-specific implementation of ``obj_update``. 2297 """ 2298 bundle_detail_data = self.get_bundle_detail_data(bundle) 2299 arg_detail_data = kwargs.get(self._meta.detail_uri_name) 2300 2301 if bundle_detail_data is None or (arg_detail_data is not None and str(bundle_detail_data) != str(arg_detail_data)): 2302 try: 2303 lookup_kwargs = self.lookup_kwargs_with_identifiers(bundle, kwargs) 2304 except: # noqa 2305 # if there is trouble hydrating the data, fall back to just 2306 # using kwargs by itself (usually it only contains a "pk" key 2307 # and this will work fine. 2308 lookup_kwargs = kwargs 2309 2310 try: 2311 bundle.obj = self.obj_get(bundle=bundle, **lookup_kwargs) 2312 except ObjectDoesNotExist: 2313 raise NotFound("A model instance matching the provided arguments could not be found.") 2314 2315 bundle = self.full_hydrate(bundle) 2316 return self.save(bundle, skip_errors=skip_errors) 2317 2318 def obj_delete_list(self, bundle, **kwargs): 2319 """ 2320 A ORM-specific implementation of ``obj_delete_list``. 2321 """ 2322 objects_to_delete = self.obj_get_list(bundle=bundle, **kwargs) 2323 deletable_objects = self.authorized_delete_list(objects_to_delete, bundle) 2324 2325 if hasattr(deletable_objects, 'delete'): 2326 # It's likely a ``QuerySet``. Call ``.delete()`` for efficiency. 2327 deletable_objects.delete() 2328 else: 2329 for authed_obj in deletable_objects: 2330 authed_obj.delete() 2331 2332 def obj_delete_list_for_update(self, bundle, **kwargs): 2333 """ 2334 A ORM-specific implementation of ``obj_delete_list_for_update``. 2335 """ 2336 objects_to_delete = self.obj_get_list(bundle=bundle, **kwargs) 2337 deletable_objects = self.authorized_update_list(objects_to_delete, bundle) 2338 2339 if hasattr(deletable_objects, 'delete'): 2340 # It's likely a ``QuerySet``. Call ``.delete()`` for efficiency. 2341 deletable_objects.delete() 2342 else: 2343 for authed_obj in deletable_objects: 2344 authed_obj.delete() 2345 2346 def obj_delete(self, bundle, **kwargs): 2347 """ 2348 A ORM-specific implementation of ``obj_delete``. 2349 2350 Takes optional ``kwargs``, which are used to narrow the query to find 2351 the instance. 2352 """ 2353 if not hasattr(bundle.obj, 'delete'): 2354 try: 2355 bundle.obj = self.obj_get(bundle=bundle, **kwargs) 2356 except ObjectDoesNotExist: 2357 raise NotFound("A model instance matching the provided arguments could not be found.") 2358 2359 self.authorized_delete_detail(self.get_object_list(bundle.request), bundle) 2360 bundle.obj.delete() 2361 2362 @atomic_decorator() 2363 def patch_list(self, request, **kwargs): 2364 """ 2365 An ORM-specific implementation of ``patch_list``. 2366 2367 Necessary because PATCH should be atomic (all-success or all-fail) 2368 and the only way to do this neatly is at the database level. 2369 """ 2370 return super(BaseModelResource, self).patch_list(request, **kwargs) 2371 2372 def rollback(self, bundles): 2373 """ 2374 A ORM-specific implementation of ``rollback``. 2375 2376 Given the list of bundles, delete all models pertaining to those 2377 bundles. 2378 """ 2379 for bundle in bundles: 2380 if bundle.obj and self.get_bundle_detail_data(bundle): 2381 bundle.obj.delete() 2382 2383 def create_identifier(self, obj): 2384 return u"%s.%s.%s" % (obj._meta.app_label, get_module_name(obj._meta), obj.pk) 2385 2386 def save(self, bundle, skip_errors=False): 2387 if bundle.via_uri: 2388 return bundle 2389 2390 self.is_valid(bundle) 2391 2392 if bundle.errors and not skip_errors: 2393 raise ImmediateHttpResponse(response=self.error_response(bundle.request, bundle.errors)) 2394 2395 # Check if they're authorized. 2396 if bundle.obj.pk: 2397 self.authorized_update_detail(self.get_object_list(bundle.request), bundle) 2398 else: 2399 self.authorized_create_detail(self.get_object_list(bundle.request), bundle) 2400 2401 # Save FKs just in case. 2402 self.save_related(bundle) 2403 2404 # Save the main object. 2405 obj_id = self.create_identifier(bundle.obj) 2406 2407 if obj_id not in bundle.objects_saved or bundle.obj._state.adding: 2408 bundle.obj.save() 2409 bundle.objects_saved.add(obj_id) 2410 2411 # Now pick up the M2M bits. 2412 m2m_bundle = self.hydrate_m2m(bundle) 2413 self.save_m2m(m2m_bundle) 2414 return bundle 2415 2416 def save_related(self, bundle): 2417 """ 2418 Handles the saving of related non-M2M data. 2419 2420 Calling assigning ``child.parent = parent`` & then calling 2421 ``Child.save`` isn't good enough to make sure the ``parent`` 2422 is saved. 2423 2424 To get around this, we go through all our related fields & 2425 call ``save`` on them if they have related, non-M2M data. 2426 M2M data is handled by the ``ModelResource.save_m2m`` method. 2427 """ 2428 for field_name, field_object in self.fields.items(): 2429 if not field_object.is_related: 2430 continue 2431 2432 if field_object.is_m2m: 2433 continue 2434 2435 if not field_object.attribute: 2436 continue 2437 2438 if field_object.readonly: 2439 continue 2440 2441 if field_object.blank and field_name not in bundle.data: 2442 continue 2443 2444 # Get the object. 2445 try: 2446 related_obj = getattr(bundle.obj, field_object.attribute) 2447 except ObjectDoesNotExist: 2448 # Django 1.8: unset related objects default to None, no error 2449 related_obj = None 2450 2451 # We didn't get it, so maybe we created it but haven't saved it 2452 if related_obj is None: 2453 related_obj = bundle.related_objects_to_save.get(field_object.attribute, None) 2454 2455 if related_obj and field_object.related_name: 2456 # this might be a reverse relation, so we need to save this 2457 # model, attach it to the related object, and save the related 2458 # object. 2459 if not self.get_bundle_detail_data(bundle): 2460 bundle.obj.save() 2461 2462 setattr(related_obj, field_object.related_name, bundle.obj) 2463 2464 related_resource = field_object.get_related_resource(related_obj) 2465 2466 # Before we build the bundle & try saving it, let's make sure we 2467 # haven't already saved it. 2468 if related_obj: 2469 obj_id = self.create_identifier(related_obj) 2470 2471 if obj_id in bundle.objects_saved: 2472 # It's already been saved. We're done here. 2473 continue 2474 2475 if bundle.data.get(field_name): 2476 if hasattr(bundle.data[field_name], 'keys'): 2477 # Only build & save if there's data, not just a URI. 2478 related_bundle = related_resource.build_bundle( 2479 obj=related_obj, 2480 data=bundle.data.get(field_name), 2481 request=bundle.request, 2482 objects_saved=bundle.objects_saved 2483 ) 2484 related_resource.full_hydrate(related_bundle) 2485 related_resource.save(related_bundle) 2486 related_obj = related_bundle.obj 2487 elif field_object.related_name: 2488 # This condition probably means a URI for a reverse 2489 # relation was provided. 2490 related_bundle = related_resource.build_bundle( 2491 obj=related_obj, 2492 request=bundle.request, 2493 objects_saved=bundle.objects_saved 2494 ) 2495 related_resource.save(related_bundle) 2496 related_obj = related_bundle.obj 2497 2498 if related_obj: 2499 setattr(bundle.obj, field_object.attribute, related_obj) 2500 2501 def save_m2m(self, bundle): 2502 """ 2503 Handles the saving of related M2M data. 2504 2505 Due to the way Django works, the M2M data must be handled after the 2506 main instance, which is why this isn't a part of the main ``save`` bits. 2507 2508 Currently slightly inefficient in that it will clear out the whole 2509 relation and recreate the related data as needed. 2510 """ 2511 for field_name, field_object in self.fields.items(): 2512 if not field_object.is_m2m: 2513 continue 2514 2515 if not field_object.attribute: 2516 continue 2517 2518 if field_object.readonly: 2519 continue 2520 2521 # Get the manager. 2522 related_mngr = None 2523 2524 if isinstance(field_object.attribute, six.string_types): 2525 related_mngr = getattr(bundle.obj, field_object.attribute) 2526 elif callable(field_object.attribute): 2527 related_mngr = field_object.attribute(bundle) 2528 2529 if not related_mngr: 2530 continue 2531 2532 if hasattr(related_mngr, 'clear'): 2533 # FIXME: Dupe the original bundle, copy in the new object & 2534 # check the perms on that (using the related resource)? 2535 2536 # Clear it out, just to be safe. 2537 related_mngr.clear() 2538 2539 related_objs = [] 2540 2541 for related_bundle in bundle.data[field_name]: 2542 related_resource = field_object.get_related_resource(bundle.obj) 2543 2544 # Only build & save if there's data, not just a URI. 2545 updated_related_bundle = related_resource.build_bundle( 2546 obj=related_bundle.obj, 2547 data=related_bundle.data, 2548 request=bundle.request, 2549 objects_saved=bundle.objects_saved, 2550 via_uri=related_bundle.via_uri, 2551 ) 2552 2553 related_resource.save(updated_related_bundle) 2554 related_objs.append(updated_related_bundle.obj) 2555 2556 related_mngr.add(*related_objs) 2557 2558 2559class ModelResource(six.with_metaclass(ModelDeclarativeMetaclass, BaseModelResource)): 2560 pass 2561 2562 2563class NamespacedModelResource(ModelResource): 2564 """ 2565 A ModelResource subclass that respects Django namespaces. 2566 """ 2567 def _build_reverse_url(self, name, args=None, kwargs=None): 2568 namespaced = "%s:%s" % (self._meta.urlconf_namespace, name) 2569 return reverse(namespaced, args=args, kwargs=kwargs) 2570 2571 2572# Based off of ``piston.utils.coerce_put_post``. Similarly BSD-licensed. 2573# And no, the irony is not lost on me. 2574def convert_post_to_VERB(request, verb): 2575 """ 2576 Force Django to process the VERB. 2577 """ 2578 if request.method == verb: 2579 if not hasattr(request, '_read_started'): 2580 request._read_started = False 2581 2582 if hasattr(request, '_post'): 2583 del request._post 2584 del request._files 2585 2586 try: 2587 request.method = "POST" 2588 request._load_post_and_files() 2589 request.method = verb 2590 except AttributeError: 2591 request.META['REQUEST_METHOD'] = 'POST' 2592 request._load_post_and_files() 2593 request.META['REQUEST_METHOD'] = verb 2594 setattr(request, verb, request.POST) 2595 2596 return request 2597 2598 2599def convert_post_to_put(request): 2600 return convert_post_to_VERB(request, verb='PUT') 2601 2602 2603def convert_post_to_patch(request): 2604 return convert_post_to_VERB(request, verb='PATCH') 2605