Source

django-rest-api / rest_api / sites.py

The default branch has multiple heads

from rest_api import http
from rest_api.options import ModelApi
from rest_api import serializers

from django import template
from django.core.exceptions import ImproperlyConfigured
from django.db.models.base import ModelBase
from django.utils.functional import update_wrapper
from django.views.decorators.cache import never_cache
from django.conf import settings
from django.utils.translation import ugettext as _
from django.utils.text import capfirst
from django.utils.safestring import mark_safe
from django.core.exceptions import ObjectDoesNotExist
from django.conf.urls.defaults import patterns, url, include
from django.core.urlresolvers import reverse
from django.views.decorators.csrf import csrf_exempt

import operator
import logging

from auth import http_auth

class AlreadyRegistered(Exception):
    pass

class NotRegistered(Exception):
    pass

class ApiSite(object):
    """
    An ApiSite object encapsulates an instance of this api application,
    ready to be hooked into your URLconf. Models are registered with the
    ApiSite using the register() method, and the root() method can then be
    used as a Django view function that presents a full API for the
    collection of registered models.
    """
    
    try:
        http_realm = settings.HTTP_AUTH_REALM
    except AttributeError:
        http_realm = "Unknown Realm"
    
    try:
        require_auth = settings.API_REQUIRE_AUTH
    except AttributeError:
        require_auth = False
    
    # Allow for situations where the app_label is not used.
    use_app_label = True
    
    try:
        index_provides_data = settings.API_INDEX_PROVIDES_DATA
    except AttributeError:
        index_provides_data = False
    
    def __init__(self, name=None, app_name='api'):
        self._registry = {} # model_class class -> api_class instance
        self._registry_extra = [] # api_class, ...
        self.root_path = None
        if name is None:
            self.name = 'api'
        else:
            self.name = name
        
        self.app_name = app_name
    
    def register(self, model_or_iterable, api_class=None, **options):
        """
        Registers the given model(s) with the given api class.
        
        The model(s) should be Model classes, not instances.
        
        If an api class is not given, it will use ModelApi (the default api
        options). If keyword arguments are given, they will be used as
        options to the api class.
        
        If a model is already registered, this will raise AlreadyRegistered.
        """
        
        if not api_class:
            api_class = ModelApi
        
        if api_class and settings.DEBUG:
            from rest_api.validation import validate
        else:
            validate = lambda model, api_class: None
        
        if isinstance(model_or_iterable, ModelBase):
            model_or_iterable = [model_or_iterable]
        
        for model in model_or_iterable:
            if model in self._registry:
                pass
                # raise AlreadyRegistered('The model %s is already registered' % model.__name__)
            
            if options:
                options['__module__'] = __name__
                api_class = type("%sApi" % model.__name, (api_class,), options)
            
            validate(api_class, model)
            
            api_object = api_class(model, self)
            
            def api_url(self):
                if not self.pk:
                    return reverse('api:%s_%s_index' % (
                        api_object.opts.app_label, api_object.opts.module_name
                    ))
                return reverse('api:%s_%s_object' % (
                    api_object.opts.app_label, api_object.opts.module_name
                ), args=(self.pk,))
            
            model.api_url = api_url
            
            if not hasattr(api_object, 'require_auth'):
                api_object.require_auth = self.require_auth
            
            self._registry[model] = api_object
            
    
    def register_extra(self, api_class, **options):
        """
        Register a non-model-based api.
        """
        for api in self._registry_extra:
            if api.__class__ == api_class:
                raise AlreadyRegistered('The class %s is already registered' % api_class.__name__)
        for model, api in self._registry.iteritems():
            if api.__class__ == api_class:
                raise AlreadyRegistered('The class %s is registered with the model %s' % (api_class.__name__, model.__name__))
        api_object = api_class(self)
        
        if not hasattr(api_object, 'require_auth'):
            api_object.require_auth = self.require_auth

        
        self._registry_extra.append(api_object)
        
    def unregister(self, model_or_iterable):
        """
        Unregisters the given model(s).
        
        If a model isn't already registered, this will raise NotRegistered.
        """
        
        if isinstance(model_or_iterable, ModelBase):
            model_or_iterable = [model_or_iterable]
        
        for model in model_or_iterable:
            if model not in self._registry:
                raise NotRegistered('The model %s is not registered' % model.__name__)
            del self._registry[model]
    
    def get_registered(self, model):
        if model in self._registry:
            return self._registry[model]
        return False
    
    def has_permission(self, request, view=None):
        """
        Returns True if the given HttpRequest has permission to access
        *at least one* object from the api site.
        """
        # Check to see if we require authentication.
        if getattr(view, "require_auth", self.require_auth):
            return request.user.is_authenticated() and request.user.is_active
        return True
    
    def check_dependencies(self):
        """
        Check that all things needed to run the api have been correctly
        installed.
        
        
        """
        from rest_api.models import LogEntry
        if not LogEntry._meta.installed:
            raise ImproperlyConfigured("Put 'rest_api' in your INSTALLED_APPS in order to use the api application.")
    
    def api_view(self, view, cacheable=False, fields=None):
        """
        Decorator to create an api_view attached to this ``ApiSite``.
        This wraps the view and provides permission checking by calling
        ``self.has_permission``.
        
        You'll want to use this from within ``ApiSite.get_urls()``:
        
            class MyApiSite(ApiSite):
                def get_urls(self):
                    from django.conf.urls.defaults import patterns, url
                    
                    urls = super(MyApiSite, self).get_urls()
                    urls += patterns('',
                        url(r'^my_view/$', self.api_view(some_view)),
                    )
                    return urls
        
        By default, api_views are marked as non-cacheable, using the
        ``never_cache`` decorator.  If the view can be safely cached, set
        cacheable=True.
        
        """
        
        def inner(request, *args, **kwargs):
            try:
                # See if the user requesting has permission to access this
                # resource. We raise the exception so we can handle it in
                # the one place, along with any http.Unauthorized that may
                # have been raised by the actual view call.
                if not self.has_permission(request, view):
                    raise http.Unauthorized()
                # See if the object can be deserialized.
                # TODO: move this to a middleware that looks at the content-type
                # if request.raw_post_data:
                #     # logging.info(request.raw_post_data)
                #     request.data = serializers.deserialize(request)
                # We now call the actual view. Because it may or may not
                # return an HttpResponse, we can wrap the response in one
                # if it returns something else (probably a queryset).
                response = view(request, *args, **kwargs)
                if isinstance(response, http.BaseHttpResponse):
                    return response
                else:
                    return http.OK(response, fields=fields)
            except http.Unauthorized, response:
                # Ensure we have the WWW-Authenticate header.
                response['WWW-Authenticate'] = "Basic realm=%s" % self.http_realm
                return response
            except http.BaseHttpResponse, response:
                # If we raise another subclass of http.BaseHttpResponse,
                # other than Unauthorized, then we need to just return the
                # thing that was raised. This means we can raise a redirect
                # at any call depth, and know it will actually redirect.
                return response
            except ObjectDoesNotExist, data:
                return http.NotFound(data.args[0])
                
        # This is based on the contrib.admin code.
        if not cacheable:
            inner = never_cache(inner)
        # Check for http authentication if this view, if applicable.
        if self.require_auth and 'rest_api.middleware.HttpAuthMiddleware' not in settings.MIDDLEWARE_CLASSES:
            inner = http_auth(inner)
        return update_wrapper(inner, view)
    
    def get_urls(self):
        if settings.DEBUG:
            self.check_dependencies()
            
        def wrap(view, cacheable=False):
            def wrapper(*args, **kwargs):
                return self.api_view(view, cacheable)(*args, **kwargs)
            return update_wrapper(wrapper, view)
        
        # Api-site-wide views.
        urlpatterns = patterns('',
            url(r'^$', wrap(self.index), name='index'),
            )
    
        # add in each model's views
        for model, model_api in self._registry.iteritems():
            urlpatterns += patterns('',
                url(r'^%s/' % model_api.root_path,
                    include(model_api.urls))
            )
        
        for api_class in self._registry_extra:
            urlpatterns += patterns('',
                url(r'^%s/' % api_class.root_path,
                    include(api_class.urls))
            )
        return urlpatterns
    
    def urls(self):
        return self.get_urls(), self.app_name, self.name
    urls = property(urls)
    
    ## The views used by the base site.

    def index(self, request):
        user = request.user
        model_list = []
        for model, model_api in self._registry.items():
            if user.has_module_perms(model._meta.app_label):
                perms = model_api.get_model_perms(request)
                
                if True in perms.values():
                    model_list.append({
                        'name':unicode(capfirst(model._meta.verbose_name_plural)),
                        'href': '%s%s/' % (request.path, model_api.root_path),
                        'perms': perms,
                    })
        model_list.sort(key=operator.itemgetter('name'))
        return http.OK(model_list)
        
site = ApiSite()