Django integration
******************
lazr.restful includes some code to make it easier to publish Django
model objects as web service resources.
Setup
=====
lazr.restful doesn't depend on Django, but these tests need access to
Django-like modules and classes. We're going to create some fake
modules and classes for purposes of the tests. If Django is actually
installed on this system, these modules will mask the real modules, but
it'll only last until the end of the test.
>>> import sys
>>> import types
>>> def create_fake_module(name):
... previous = None
... previous_names = []
... for module_name in name.split('.'):
... previous_names.append(module_name)
... full_name = '.'.join(previous_names)
... module = sys.modules.get(full_name)
... if module is None:
... module = types.ModuleType(full_name)
... if previous is not None:
... setattr(previous, module_name, module)
... previous = module
... sys.modules[full_name] = module
Before loading django.zcml, we need to create fake versions of all the
modules and classes we'll be using, because if the real Django happens
to be installed, loading the real Django classes will trigger all
sorts of Django code that we can't handle.
>>> create_fake_module('settings')
>>> import settings
>>> class ObjectDoesNotExist(Exception):
... pass
>>> create_fake_module('django.core.exceptions')
>>> import django.core.exceptions
>>> django.core.exceptions.ObjectDoesNotExist = ObjectDoesNotExist
>>> class Manager:
... """A fake version of Django's Manager class."""
... def __init__(self, l):
... self.list = l
... def iterator(self):
... return self.list.__iter__()
... def all(self):
... return self.list
... def count(self):
... return len(self.list)
>>> create_fake_module('django.db.models.manager')
>>> import django.db.models.manager
>>> django.db.models.manager.Manager = Manager
Now we can load a ZCML configuration typical of a lazr.restful
application that publishes Django model objects through a web service.
>>> from zope.configuration import xmlconfig
>>> ignore = xmlconfig.string("""
...
...
...
...
... """)
Configuration
=============
Like any lazr.restful app, a Django app can subclass
BaseWebServiceConfiguration and customize the subclass to set
configuration options. However, you might find it easier to subclass
DjangoWebServiceConfiguration instead and put the configuration in
your settings.py file.
An attribute 'foo_bar' defined in IWebServiceConfiguration can be set
in your settings.py file as LAZR_RESTFUL_FOO_BAR. Here, we'll override
the default value of 'show_tracebacks'.
>>> settings.LAZR_RESTFUL_SHOW_TRACEBACKS = False
>>> from lazr.restful.frameworks.django import (
... DjangoWebServiceConfiguration)
>>> class MyConfiguration(DjangoWebServiceConfiguration):
... pass
>>> MyConfiguration().show_tracebacks
True
Once grok is called on the configuration class...
>>> from grokcore.component.testing import grok_component
>>> ignored = grok_component('MyConfiguration', MyConfiguration)
...MyConfiguration is the registered utility for
IWebServiceConfiguration, and its value for use_https and
show_tracebacks have been taken from the 'settings' module.
>>> from zope.component import getUtility
>>> from lazr.restful.interfaces import IWebServiceConfiguration
>>> utility = getUtility(IWebServiceConfiguration)
>>> utility.show_tracebacks
False
Attributes that don't have values in settings.py are given their
default value.
>>> utility.active_versions
[]
>>> utility.use_https
True
IDjangoLocation
===============
The simplest way to generate URLs for your resources is to have your
service root resource implement IAbsoluteURL (to generate the root of
all web service URLs), and have all your model classes implement
ILocation (which defines the URL in terms of a parent object's
URL).
ILocation requires that you define a property called __name__. This is
part of the URL unique to an object, as opposed to the first part of
the URL, which comes from its parent. Unfortunately, the Django object
metaclass won't allow you to assign a property to __name__.
IDjangoLocation is a class provided by lazr.restful. It acts just like
ILocation, but the last part of the URL comes from __url_path__
instead of __name__.
Here's some setup; a root resource that has its own AbsoluteURL
implementation.
>>> from lazr.restful.testing.webservice import (
... StubRootResource, StubRootResourceURL)
>>> from zope.component import getSiteManager
>>> sm = getSiteManager()
>>> sm.registerAdapter(StubRootResourceURL)
Now here's a subordinate resource that just implements IDjangoLocatino.
>>> from zope.interface import implementer
>>> from lazr.restful.frameworks.django import IDjangoLocation
>>> @implementer(IDjangoLocation)
... class SubordinateResource:
...
... @property
... def __parent_object__(self):
... return StubRootResource()
...
... @property
... def __url_path__(self):
... return "myname"
The django.zcml file in lazr/restful/frameworks contains an adapter
between IDjangoLocation and ILocation. Thanks to that adapter, it's
possible to adapt a Django model object to ILocation and check on its
__name__.
>>> from zope.location.interfaces import ILocation
>>> resource = SubordinateResource()
>>> as_location = ILocation(resource)
>>> print(as_location.__name__)
myname
It's also possible to adapt a Django model object to IAbsoluteURL and
get its full URL.
>>> from zope.component import getMultiAdapter
>>> from zope.traversing.browser.interfaces import IAbsoluteURL
>>> from lazr.restful.simple import Request
>>> request = Request("", {})
>>> print(str(getMultiAdapter((resource, request), IAbsoluteURL)))
http://exampleurl/myname
ObjectDoesNotExist
==================
The django.zcml helper file contains an adapter registration so that
Django's ObjectDoesNotExist exceptions are treated as 404
errors.
To test this, we simply instantiate an ObjectDoesNotExist and then get
a view for it.
>>> exception = ObjectDoesNotExist()
>>> view = getMultiAdapter((exception, request), name="index.html")
>>> view()
''
The view for the ObjectDoesNotExist exception sets the HTTP response
code to 404.
>>> request.response.getStatus()
404
Managers
========
The django.zcml file includes an adapter between Django's Manager
class (which controls access to the database) and Zope's
IFiniteCollection interface (used by lazr.batchnavigator to batch data
sets).
>>> from zope.interface.common.sequence import IFiniteSequence
>>> data = ["foo", "bar", "baz"]
>>> from django.db.models.manager import Manager
>>> manager = Manager(data)
>>> sequence = IFiniteSequence(manager)
The adapter class is ManagerSequencer.
>>> sequence
A ManagerSequencer object makes a Manager object act like a Python
list, which is what IFiniteSequence needs.
>>> len(sequence)
3
>>> print(sequence[1])
bar
>>> sequence[1:3]
['bar', 'baz']
>>> [x for x in sequence]
['foo', 'bar', 'baz']