try: from hashlib import sha1 except ImportError: from sha import sha as sha1 from django.conf.urls.defaults import include, patterns, url from django.contrib.auth.models import User, Group from django.core.urlresolvers import reverse from django.db import models from django.db.models.query import QuerySet from django.http import HttpResponseNotAllowed, HttpResponse, \ HttpResponseNotModified from django.views.decorators.vary import vary_on_headers from djblets.util.decorators import augment_method_from from djblets.util.http import get_modified_since, etag_if_none_match, \ set_last_modified, set_etag from djblets.util.misc import never_cache_patterns from djblets.webapi.auth import check_login from djblets.webapi.core import WebAPIResponse, \ WebAPIResponseError, \ WebAPIResponsePaginated, \ SPECIAL_PARAMS from djblets.webapi.decorators import webapi_login_required, \ webapi_response_errors, \ webapi_request_fields from djblets.webapi.errors import WebAPIError, DOES_NOT_EXIST, \ NOT_LOGGED_IN, PERMISSION_DENIED _model_to_resources = {} _name_to_resources = {} _class_to_resources = {} class WebAPIResource(object): """A resource living at a specific URL, representing an object or list of objects. A WebAPIResource is a RESTful resource living at a specific URL. It can represent either an object or a list of objects, and can respond to various HTTP methods (GET, POST, PUT, DELETE). Subclasses are expected to override functions and variables in order to provide specific functionality, such as modifying the resource or creating a new resource. Representing Models ------------------- Most resources will have ``model`` set to a Model subclass, and ``fields`` set to list the fields that would be shown when Each resource will also include a ``link`` dictionary that maps a key (resource name or action) to a dictionary containing the URL (``href``) and the HTTP method that's to be used for that URL (``method``). This will include a special ``self`` key that links to that resource's actual location. An example of this might be:: 'links': { 'self': { 'method': 'GET', 'href': '/path/to/this/resource/' }, 'update': { 'method': 'PUT', 'href': '/path/to/this/resource/' } } Resources associated with a model may want to override the ``get_queryset`` function to return a queryset with a more specific query. By default, an individual object's key name in the resulting payloads will be set to the lowercase class name of the object, and the plural version used for lists will be the same but with 's' appended to it. This can be overridden by setting ``name`` and ``name_plural``. Matching Objects ---------------- Objects are generally queried by their numeric object ID and mapping that to the object's ``pk`` attribute. For this to work, the ``uri_object_key`` attribute must be set to the name in the regex for the URL that will be captured and passed to the handlers for this resource. The ``uri_object_key_regex`` attribute can be overridden to specify the regex for matching this ID (useful for capturing names instead of numeric IDs) and ``model_object_key`` can be overridden to specify the model field that will be matched against. Parents and URLs ---------------- Resources typically have a parent resource, of which the resource is a subclass. Resources will often list their children (by setting ``list_child_resources`` and ``item_child_resources`` in a subclass to lists of other WebAPIResource instances). This makes the entire tree navigatable. The URLs are built up automatically, so long as the result of get_url_patterns() from top-level resources are added to the Django url_patterns variables commonly found in urls.py. Child objects should set the ``model_parent_key`` variable to the field name of the object's parent in the resource hierarchy. This allows WebAPIResource to build a URL with the right values filled in in order to make a URL to this object. If the parent is dynamic based on certain conditions, then the ``get_parent_object`` function can be overridden instead. Object Serialization -------------------- Objects are serialized through the ``serialize_object`` function. This rarely needs to be overridden, but can be called from WebAPIEncoders in order to serialize the object. By default, this will loop through the ``fields`` variable and add each value to the resulting dictionary. Values can be specially serialized by creating functions in the form of ``serialize__field``. These functions take the object being serialized and must return a value that can be fed to the encoder. Handling Requests ----------------- WebAPIResource calls the following functions based on the type of HTTP request: * ``get`` - HTTP GET for individual objects. * ``get_list`` - HTTP GET for resources representing lists of objects. * ``create`` - HTTP POST on resources representing lists of objects. This is expected to return the object and an HTTP status of 201 CREATED, on success. * ``update`` - HTTP PUT on individual objects to modify their state based on full or partial data. * ``delete`` - HTTP DELETE on an individual object. This is expected to return a status of HTTP 204 No Content on success. The default implementation just deletes the object. Any function that is not implemented will return an HTTP 405 Method Not Allowed. Functions that have handlers provided should set ``allowed_methods`` to a tuple of the HTTP methods allowed. For example:: allowed_methods = ('GET', POST', 'DELETE') These functions are passed an HTTPRequest and a list of arguments captured in the URL and are expected to return standard HTTP response codes, along with a payload in most cases. The functions can return any of: * A HttpResponse * A WebAPIResponse * A WebAPIError * A tuple of (WebAPIError, Payload) * A tuple of (WebAPIError, Payload Dictionary, Headers Dictionary) * A tuple of (HTTP status, Payload) * A tuple of (HTTP status, Payload Dictionary, Headers Dictionary) In general, it's best to return one of the tuples containing an HTTP status, and not any object, but there are cases where an object is necessary. Commonly, a handler will need to fetch parent objects in order to make some request. The values for all captured object IDs in the URL are passed to the handler, but it's best to not use these directly. Instead, the handler should accept a **kwargs parameter, and then call the parent resource's ``get_object`` function and pass in that **kwargs. For example:: def create(self, request, *args, **kwargs): try: my_parent = myParentResource.get_object(request, *args, **kwargs) except ObjectDoesNotExist: return DOES_NOT_EXIST Expanding Resources ------------------- The resulting data returned from a resource will by default provide links to child resources. If a lot of aggregated data is needed, then instead of making several queries the caller can use the ``?expand=`` parameter. This takes a comma-separated list of keys in the resource names found in the payloads and expands them instead of linking to them. This can result in really large downloads, if deep expansion is made when accessing lists of resources. However, it can also result in less strain on the server if used correctly. Faking HTTP Methods ------------------- There are clients that can't actually request anything but HTTP POST and HTTP GET. An HTML form is one such example, and Flash applications are another. For these cases, an HTTP POST can be made, with a special ``_method`` parameter passed to the URL. This can be set to the HTTP method that's desired. For example, ``PUT`` or ``DELETE``. Permissions ----------- Unless overridden, an object cannot be modified, created, or deleted if the user is not logged in and if an appropriate permission function does not return True. These permission functions are: * ``has_access_permissions`` - Used for HTTP GET calls. Returns True by default. * ``has_modify_permissions`` - Used for HTTP POST or PUT calls, if called by the subclass. Returns False by default. * ``has_delete_permissions`` - Used for HTTP DELETE permissions. Returns False by default. Browser Caching --------------- To improve performance, resources can make use of browser-side caching. If a resource is accessed more than once, and it hasn't changed, the resource will return an :http:`304`. There are two methods for caching: Last Modified headers, and ETags. Last Modified ~~~~~~~~~~~~~ A resource can set ``last_modified_field`` to the name of a DateTimeField in the model. This will be used to determine if the resource has changed since the last request. If a bit more work is needed, the ``get_last_modified`` function can instead be overridden. This takes the request and object and is expected to return a timestamp. ETags ~~~~~ ETags are arbitrary, unique strings that represent the state of a resource. There should only ever be one possible ETag per state of the resource. A resource can set the ``etag_field`` to the name of a field in the model. If no field really works, ``autogenerate_etags`` can be set. This will generate a suitable ETag based on all fields in the resource. For this to work correctly, no custom data can be added to the payload, and links cannot be dynamic. If more work is needed, the ``get_etag`` function can instead be overridden. It will take a request and object and is expected to return a string. """ # Configuration model = None fields = {} uri_object_key_regex = '[0-9]+' uri_object_key = None model_object_key = 'pk' model_parent_key = None last_modified_field = None etag_field = None autogenerate_etags = False singleton = False list_child_resources = [] item_child_resources = [] allowed_methods = ('GET',) allowed_item_mimetypes = WebAPIResponse.supported_mimetypes allowed_list_mimetypes = WebAPIResponse.supported_mimetypes # State method_mapping = { 'GET': 'get', 'POST': 'post', 'PUT': 'put', 'DELETE': 'delete', } _parent_resource = None def __init__(self): _name_to_resources[self.name] = self _name_to_resources[self.name_plural] = self _class_to_resources[self.__class__] = self @vary_on_headers('Accept', 'Cookie') def __call__(self, request, api_format=None, *args, **kwargs): """Invokes the correct HTTP handler based on the type of request.""" check_login(request) method = request.method if method == 'POST': # Not all clients can do anything other than GET or POST. # So, in the case of POST, we allow overriding the method # used. method = request.POST.get('_method', kwargs.get('_method', method)) elif method == 'PUT': # Normalize the PUT data so we can get to it. # This is due to Django's treatment of PUT vs. POST. They claim # that PUT, unlike POST, is not necessarily represented as form # data, so they do not parse it. However, that gives us no clean way # of accessing the data. So we pretend it's POST for a second in # order to parse. # # This must be done only for legitimate PUT requests, not faked # ones using ?method=PUT. try: request.method = 'POST' request._load_post_and_files() request.method = 'PUT' except AttributeError: request.META['REQUEST_METHOD'] = 'POST' request._load_post_and_files() request.META['REQUEST_METHOD'] = 'PUT' request.PUT = request.POST if method in self.allowed_methods: if (method == "GET" and not self.singleton and (self.uri_object_key is None or self.uri_object_key not in kwargs)): view = self.get_list else: view = getattr(self, self.method_mapping.get(method, None)) else: view = None if view and callable(view): result = view(request, api_format=api_format, *args, **kwargs) if isinstance(result, WebAPIResponse): return result elif isinstance(result, WebAPIError): return WebAPIResponseError(request, err=result, api_format=api_format) elif isinstance(result, tuple): headers = {} if method == 'GET': request_params = request.GET else: request_params = request.POST if len(result) == 3: headers = result[2] if 'Location' in headers: extra_querystr = '&'.join([ '%s=%s' % (param, request_params[param]) for param in SPECIAL_PARAMS if param in request_params ]) if extra_querystr: if '?' in headers['Location']: headers['Location'] += '&' + extra_querystr else: headers['Location'] += '?' + extra_querystr if isinstance(result[0], WebAPIError): return WebAPIResponseError(request, err=result[0], headers=headers, extra_params=result[1], api_format=api_format) else: return WebAPIResponse(request, status=result[0], obj=result[1], headers=headers, api_format=api_format) elif isinstance(result, HttpResponse): return result else: raise AssertionError(result) else: return HttpResponseNotAllowed(self.allowed_methods) @property def __name__(self): return self.__class__.__name__ @property def name(self): """Returns the name of the object, used for keys in the payloads.""" if self.model: return self.model.__name__.lower() else: return self.__name__.lower() @property def name_plural(self): """Returns the plural name of the object, used for lists.""" if self.singleton: return self.name else: return self.name + 's' @property def item_result_key(self): """Returns the key for single objects in the payload.""" return self.name @property def list_result_key(self): """Returns the key for lists of objects in the payload.""" return self.name_plural @property def uri_name(self): """Returns the name of the resource in the URI. This can be overridden when the name in the URI needs to differ from the name used for the resource. """ return self.name_plural.replace('_', '-') def get_object(self, request, *args, **kwargs): """Returns an object, given captured parameters from a URL. This will perform a query for the object, taking into account ``model_object_key``, ``uri_object_key``, and any captured parameters from the URL. This requires that ``model`` and ``uri_object_key`` be set. """ assert self.model assert self.singleton or self.uri_object_key queryset = self.get_queryset(request, *args, **kwargs).select_related() if self.singleton: return queryset.get() else: return queryset.get(**{ self.model_object_key: kwargs[self.uri_object_key] }) def post(self, *args, **kwargs): """Handles HTTP POSTs. This is not meant to be overridden unless there are specific needs. This will invoke ``create`` if doing an HTTP POST on a list resource. By default, an HTTP POST is not allowed on individual object resourcces. """ if 'POST' not in self.allowed_methods: return HttpResponseNotAllowed(self.allowed_methods) if (self.uri_object_key is None or kwargs.get(self.uri_object_key, None) is None): return self.create(*args, **kwargs) # Don't allow POSTs on children by default. allowed_methods = list(self.allowed_methods) allowed_methods.remove('POST') return HttpResponseNotAllowed(allowed_methods) def put(self, request, *args, **kwargs): """Handles HTTP PUTs. This is not meant to be overridden unless there are specific needs. This will just invoke ``update``. """ return self.update(request, *args, **kwargs) @webapi_response_errors(DOES_NOT_EXIST, NOT_LOGGED_IN, PERMISSION_DENIED) def get(self, request, api_format, *args, **kwargs): """Handles HTTP GETs to individual object resources. By default, this will check for access permissions and query for the object. It will then return a serialized form of the object. This may need to be overridden if needing more complex logic. """ if (not self.model or (self.uri_object_key is None and not self.singleton)): return HttpResponseNotAllowed(self.allowed_methods) try: obj = self.get_object(request, *args, **kwargs) except self.model.DoesNotExist: return DOES_NOT_EXIST if not self.has_access_permissions(request, obj, *args, **kwargs): if request.user.is_authenticated(): return PERMISSION_DENIED else: return NOT_LOGGED_IN last_modified_timestamp = self.get_last_modified(request, obj) if (last_modified_timestamp and get_modified_since(request, last_modified_timestamp)): return HttpResponseNotModified() etag = self.get_etag(request, obj) if etag and etag_if_none_match(request, etag): return HttpResponseNotModified() data = { self.item_result_key: self.serialize_object(obj, request=request, *args, **kwargs), } response = WebAPIResponse(request, status=200, obj=data, api_format=api_format) if last_modified_timestamp: set_last_modified(response, last_modified_timestamp) if etag: set_etag(response, etag) return response @webapi_request_fields( optional={ 'start': { 'type': int, 'description': 'The 0-based index of the first result in the ' 'list. The start index is usually the previous ' 'start index plus the number of previous ' 'results. By default, this is 0.', }, 'max-results': { 'type': int, 'description': 'The maximum number of results to return in ' 'this list. By default, this is 25.', } }, allow_unknown=True ) def get_list(self, request, *args, **kwargs): """Handles HTTP GETs to list resources. By default, this will query for a list of objects and return the list in a serialized form. """ data = { 'links': self.get_links(self.list_child_resources, request=request, *args, **kwargs), } if self.model: return WebAPIResponsePaginated( request, queryset=self.get_queryset(request, is_list=True, *args, **kwargs).select_related(), results_key=self.list_result_key, serialize_object_func = lambda obj: get_resource_for_object(obj).serialize_object( obj, request=request, *args, **kwargs), extra_data=data) else: return 200, data @webapi_login_required def create(self, request, api_format, *args, **kwargs): """Handles HTTP POST requests to list resources. This is used to create a new object on the list, given the data provided in the request. It should usually return HTTP 201 Created upon success. By default, this returns HTTP 405 Method Not Allowed. """ return HttpResponseNotAllowed(self.allowed_methods) @webapi_login_required def update(self, request, api_format, *args, **kwargs): """Handles HTTP PUT requests to object resources. This is used to update an object, given full or partial data provided in the request. It should usually return HTTP 200 OK upon success. By default, this returns HTTP 405 Method Not Allowed. """ return HttpResponseNotAllowed(self.allowed_methods) @webapi_login_required @webapi_response_errors(DOES_NOT_EXIST, NOT_LOGGED_IN, PERMISSION_DENIED) def delete(self, request, api_format, *args, **kwargs): """Handles HTTP DELETE requests to object resources. This is used to delete an object, if the user has permissions to do so. By default, this deletes the object and returns HTTP 204 No Content. """ if not self.model or self.uri_object_key is None: return HttpResponseNotAllowed(self.allowed_methods) try: obj = self.get_object(request, *args, **kwargs) except self.model.DoesNotExist: return DOES_NOT_EXIST if not self.has_delete_permissions(request, obj, *args, **kwargs): if request.user.is_authenticated(): return PERMISSION_DENIED else: return NOT_LOGGED_IN obj.delete() return 204, {} def get_queryset(self, request, is_list=False, *args, **kwargs): """Returns a queryset used for querying objects or lists of objects. This can be overridden to filter the object list, such as for hiding non-public objects. The ``is_list`` parameter can be used to specialize the query based on whether an individual object or a list of objects is being queried. """ return self.model.objects.all() def get_url_patterns(self): """Returns the Django URL patterns for this object and its children. This is used to automatically build up the URL hierarchy for all objects. Projects should call this for top-level resources and return them in the ``urls.py`` files. """ urlpatterns = never_cache_patterns('', url(r'^$', self, name=self._build_named_url(self.name_plural)), ) for resource in self.list_child_resources: resource._parent_resource = self child_regex = r'^' + resource.uri_name + '/' urlpatterns += patterns('', url(child_regex, include(resource.get_url_patterns())), ) if self.uri_object_key or self.singleton: # If the resource has particular items in it... if self.uri_object_key: base_regex = r'^(?P<%s>%s)/' % (self.uri_object_key, self.uri_object_key_regex) elif self.singleton: base_regex = r'^' urlpatterns += never_cache_patterns('', url(base_regex + '$', self, name=self._build_named_url(self.name)) ) for resource in self.item_child_resources: resource._parent_resource = self child_regex = base_regex + resource.uri_name + '/' urlpatterns += patterns('', url(child_regex, include(resource.get_url_patterns())), ) return urlpatterns def has_access_permissions(self, request, obj, *args, **kwargs): """Returns whether or not the user has read access to this object.""" return True def has_modify_permissions(self, request, obj, *args, **kwargs): """Returns whether or not the user can modify this object.""" return False def has_delete_permissions(self, request, obj, *args, **kwargs): """Returns whether or not the user can delete this object.""" return False def serialize_object(self, obj, *args, **kwargs): """Serializes the object into a Python dictionary.""" data = { 'links': self.get_links(self.item_child_resources, obj, *args, **kwargs), } request = kwargs.get('request', None) expand = request.GET.get('expand', request.POST.get('expand', '')) expanded_resources = expand.split(',') for field in list(self.fields): serialize_func = getattr(self, "serialize_%s_field" % field, None) if serialize_func and callable(serialize_func): value = serialize_func(obj) else: value = getattr(obj, field) if isinstance(value, models.Manager): value = value.all() elif isinstance(value, models.ForeignKey): value = value.get() expand_field = field in expanded_resources if isinstance(value, models.Model) and not expand_field: resource = get_resource_for_object(value) assert resource data['links'][field] = { 'method': 'GET', 'href': resource.get_href(value, *args, **kwargs), 'title': unicode(value), } elif isinstance(value, QuerySet) and not expand_field: data[field] = [ { 'method': 'GET', 'href': get_resource_for_object(o).get_href( o, *args, **kwargs), 'title': unicode(o), } for o in value.all() ] else: data[field] = value for resource_name in expanded_resources: if resource_name not in data['links']: continue # Try to find the resource from the child list. found = False for resource in self.item_child_resources: if resource_name in [resource.name, resource.name_plural]: found = True break if not found or not resource.model: continue del data['links'][resource_name] extra_kwargs = { self.uri_object_key: getattr(obj, self.model_object_key), } extra_kwargs.update(**kwargs) extra_kwargs.update(self.get_href_parent_ids(obj)) data[resource_name] = resource.get_queryset( is_list=True, *args, **extra_kwargs) return data def get_links(self, resources=[], obj=None, request=None, *args, **kwargs): """Returns a dictionary of links coming off this resource. The resulting links will point to the resources passed in ``resources``, and will also provide special resources for ``self`` (which points back to the official location for this resource) and one per HTTP method/operation allowed on this resource. """ links = {} base_href = None if obj: base_href = self.get_href(obj, request, *args, **kwargs) if not base_href: # We may have received None from the URL above. if request: base_href = request.build_absolute_uri() else: base_href = '' links['self'] = { 'method': 'GET', 'href': base_href, } # base_href without any query arguments. i = base_href.find('?') if i != -1: clean_base_href = base_href[:i] else: clean_base_href = base_href if 'POST' in self.allowed_methods and not obj: links['create'] = { 'method': 'POST', 'href': clean_base_href, } if 'PUT' in self.allowed_methods and obj: links['update'] = { 'method': 'PUT', 'href': clean_base_href, } if 'DELETE' in self.allowed_methods and obj: links['delete'] = { 'method': 'DELETE', 'href': clean_base_href, } for resource in resources: links[resource.name_plural] = { 'method': 'GET', 'href': '%s%s/' % (clean_base_href, resource.uri_name), } for key, info in self.get_related_links(obj, request, *args, **kwargs).iteritems(): links[key] = { 'method': info['method'], 'href': info['href'], } if 'title' in info: links[key]['title'] = info['title'] return links def get_related_links(self, obj=None, request=None, *args, **kwargs): """Returns links related to this resource. The result should be a dictionary of link names to a dictionary of information. The information should contain: * 'method' - The HTTP method * 'href' - The URL * 'title' - The title of the link (optional) * 'resource' - The WebAPIResource instance * 'list-resource' - True if this links to a list resource (optional) """ return {} def get_href(self, obj, request, *args, **kwargs): """Returns the URL for this object.""" if not self.uri_object_key: return None href_kwargs = { self.uri_object_key: getattr(obj, self.model_object_key), } href_kwargs.update(self.get_href_parent_ids(obj)) return request.build_absolute_uri( reverse(self._build_named_url(self.name), kwargs=href_kwargs)) def get_href_parent_ids(self, obj): """Returns a dictionary mapping parent object keys to their values for an object. """ parent_ids = {} if self._parent_resource and self.model_parent_key: parent_obj = self.get_parent_object(obj) parent_ids = self._parent_resource.get_href_parent_ids(parent_obj) if self._parent_resource.uri_object_key: parent_ids[self._parent_resource.uri_object_key] = \ getattr(parent_obj, self._parent_resource.model_object_key) return parent_ids def get_parent_object(self, obj): """Returns the parent of an object. By default, this uses ``model_parent_key`` to figure out the parent, but it can be overridden for more complex behavior. """ parent_obj = getattr(obj, self.model_parent_key) if isinstance(parent_obj, (models.Manager, models.ForeignKey)): parent_obj = parent_obj.get() return parent_obj def get_last_modified(self, request, obj): """Returns the last modified timestamp of an object. By default, this uses ``last_modified_field`` to determine what field in the model represents the last modified timestamp of the object. This can be overridden for more complex behavior. """ if self.last_modified_field: return getattr(obj, self.last_modified_field) return None def get_etag(self, request, obj): """Returns the ETag representing the state of the object. By default, this uses ``etag_field`` to determine what field in the model is unique enough to represent the state of the object. This can be overridden for more complex behavior. """ if self.etag_field: return unicode(getattr(obj, self.etag_field)) elif self.autogenerate_etags: return self.generate_etag(obj, self.fields) return None def generate_etag(self, obj, fields): """Generates an ETag from the serialized values of all given fields.""" values = [] for field in fields: serialize_func = getattr(self, "serialize_%s_field" % field, None) if serialize_func and callable(serialize_func): values.append(serialize_func(obj)) else: values.append(unicode(getattr(obj, field))) return sha1(':'.join(fields)).hexdigest() def _build_named_url(self, name): """Builds a Django URL name from the provided name.""" return '%s-resource' % name.replace('_', '-') class RootResource(WebAPIResource): """The root of a resource tree. This is meant to be instantiated with a list of immediate child resources. The result of ``get_url_patterns`` should be included in a project's ``urls.py``. """ name = 'root' singleton = True def __init__(self, child_resources=[], include_uri_templates=True): super(RootResource, self).__init__() self.list_child_resources = child_resources self._uri_templates = {} self._include_uri_templates = include_uri_templates def get_etag(self, request, obj, *args, **kwargs): return sha1('%s:%s' % (self._include_uri_templates, ':'.join(repr(self.list_child_resources)))).hexdigest() def get(self, request, *args, **kwargs): """ Retrieves the list of top-level resources, and a list of :term:`URI templates` for accessing any resource in the tree. """ etag = self.get_etag(request, None) if etag_if_none_match(request, etag): return HttpResponseNotModified() data = { 'links': self.get_links(self.list_child_resources, request=request, *args, **kwargs), } if self._include_uri_templates: data['uri_templates'] = self.get_uri_templates(request, *args, **kwargs) return 200, data, { 'ETag': etag, } def get_uri_templates(self, request, *args, **kwargs): """Returns all URI templates in the resource tree. REST APIs can be very chatty if a client wants to be well-behaved and crawl the resource tree asking for the links, instead of hard-coding the paths. The benefit is that they can keep from breaking when paths change. The downside is that it can take many HTTP requests to get the right resource. This list of all URI templates allows clients who know the resource name and the data they care about to simply plug them into the URI template instead of trying to crawl over the whole tree. This can make things far more efficient. """ if not self._uri_templates: self._uri_templates = {} base_href = request.build_absolute_uri() for name, href in self._walk_resources(self, base_href): self._uri_templates[name] = href return self._uri_templates def _walk_resources(self, resource, list_href): yield resource.name_plural, list_href for child in resource.list_child_resources: child_href = list_href + child.uri_name + '/' for name, href in self._walk_resources(child, child_href): yield name, href if resource.uri_object_key: object_href = '%s{%s}/' % (list_href, resource.uri_object_key) yield resource.name, object_href for child in resource.item_child_resources: child_href = object_href + child.uri_name + '/' for name, href in self._walk_resources(child, child_href): yield name, href class UserResource(WebAPIResource): """A default resource for representing a Django User model.""" model = User fields = { 'id': { 'type': int, 'description': 'The numeric ID of the user.', }, 'username': { 'type': str, 'description': "The user's username.", }, 'first_name': { 'type': str, 'description': "The user's first name.", }, 'last_name': { 'type': str, 'description': "The user's last name.", }, 'fullname': { 'type': str, 'description': "The user's full name (first and last).", }, 'email': { 'type': str, 'description': "The user's e-mail address", }, 'url': { 'type': str, 'description': "The URL to the user's page on the site. " "This is deprecated and will be removed in a " "future version.", }, } uri_object_key = 'username' uri_object_key_regex = '[A-Za-z0-9@\._-]+' model_object_key = 'username' autogenerate_etags = True allowed_methods = ('GET',) def serialize_fullname_field(self, user): return user.get_full_name() def serialize_url_field(self, user): return user.get_absolute_url() def has_modify_permissions(self, request, user, *args, **kwargs): """Returns whether or not the user can modify this object.""" return request.user.is_authenticated() and user.pk == request.user.pk @augment_method_from(WebAPIResource) def get_list(self, *args, **kwargs): """Retrieves the list of users on the site.""" pass class GroupResource(WebAPIResource): """A default resource for representing a Django Group model.""" model = Group fields = ('id', 'name') uri_object_key = 'group_name' uri_object_key_regex = '[A-Za-z0-9_-]+' model_object_key = 'name' autogenerate_etags = True allowed_methods = ('GET',) def register_resource_for_model(model, resource): """Registers a resource as the official location for a model. ``resource`` can be a callable function that takes an instance of ``model`` and returns a ``WebAPIResource``. """ _model_to_resources[model] = resource def get_resource_for_object(obj): """Returns the resource for an object.""" resource = _model_to_resources.get(obj.__class__, None) if not isinstance(resource, WebAPIResource) and callable(resource): resource = resource(obj) return resource def get_resource_from_name(name): """Returns the resource of the specified name.""" return _name_to_resources.get(name, None) def get_resource_from_class(klass): """Returns the resource with the specified resource class.""" return _class_to_resources.get(klass, None) user_resource = UserResource() group_resource = GroupResource() # These are good defaults, and will be overridden if another class calls # register_resource_for_model on these models. register_resource_for_model(User, user_resource) register_resource_for_model(Group, group_resource)