Huge stack of refactoring getting stuff into Mixin classes, and loads of tests. Kickass.

This commit is contained in:
tom christie tom@tomchristie.com 2011-02-04 21:52:21 +00:00
parent eebcdc4dc0
commit fcd7f414c4
12 changed files with 578 additions and 1 deletions

View File

@ -0,0 +1,55 @@
"""Mixin classes that provide a determine_content(request) method to return the content type and content of a request.
We use this more generic behaviour to allow for overloaded content in POST forms.
"""
class ContentMixin(object):
"""Base class for all ContentMixin classes, which simply defines the interface they provide."""
def determine_content(self, request):
"""If the request contains content return a tuple of (content_type, content) otherwise return None.
Note that content_type may be None if it is unset.
Must be overridden to be implemented."""
raise NotImplementedError()
class StandardContentMixin(ContentMixin):
"""Standard HTTP request content behaviour.
See RFC 2616 sec 4.3 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3"""
def determine_content(self, request):
"""If the request contains content return a tuple of (content_type, content) otherwise return None.
Note that content_type may be None if it is unset."""
if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None):
return None
return (request.META.get('CONTENT_TYPE', None), request.raw_post_data)
class OverloadedContentMixin(ContentMixin):
"""HTTP request content behaviour that also allows arbitrary content to be tunneled in form data."""
"""The name to use for the content override field in the POST form."""
FORM_PARAM_CONTENT = '_content'
"""The name to use for the content-type override field in the POST form."""
FORM_PARAM_CONTENTTYPE = '_contenttype'
def determine_content(self, request):
"""If the request contains content return a tuple of (content_type, content) otherwise return None.
Note that content_type may be None if it is unset."""
if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None):
return None
content_type = request.META.get('CONTENT_TYPE', None)
if (request.method == 'POST' and self.FORM_PARAM_CONTENT and
request.POST.get(self.FORM_PARAM_CONTENT, None) is not None):
# Set content type if form contains a none empty FORM_PARAM_CONTENTTYPE field
content_type = None
if self.FORM_PARAM_CONTENTTYPE and request.POST.get(self.FORM_PARAM_CONTENTTYPE, None):
content_type = request.POST.get(self.FORM_PARAM_CONTENTTYPE, None)
return (content_type, request.POST[self.FORM_PARAM_CONTENT])
return (content_type, request.raw_post_data)

View File

@ -0,0 +1,35 @@
"""Mixin classes that provide a determine_method(request) function to determine the HTTP
method that a given request should be treated as. We use this more generic behaviour to
allow for overloaded methods in POST forms.
See Richardson & Ruby's RESTful Web Services for justification.
"""
class MethodMixin(object):
"""Base class for all MethodMixin classes, which simply defines the interface they provide."""
def determine_method(self, request):
"""Simply return GET, POST etc... as appropriate."""
raise NotImplementedError()
class StandardMethodMixin(MethodMixin):
"""Provide for standard HTTP behaviour, with no overloaded POST."""
def determine_method(self, request):
"""Simply return GET, POST etc... as appropriate."""
return request.method.upper()
class OverloadedPOSTMethodMixin(MethodMixin):
"""Provide for overloaded POST behaviour."""
"""The name to use for the method override field in the POST form."""
FORM_PARAM_METHOD = '_method'
def determine_method(self, request):
"""Simply return GET, POST etc... as appropriate, allowing for POST overloading
by setting a form field with the requested method name."""
method = request.method.upper()
if method == 'POST' and self.FORM_PARAM_METHOD and request.POST.has_key(self.FORM_PARAM_METHOD):
method = request.POST[self.FORM_PARAM_METHOD].upper()
return method

View File

@ -106,7 +106,7 @@ class NoContent(object):
class Response(object):
def __init__(self, status, content=NoContent, headers={}):
def __init__(self, status=200, content=NoContent, headers={}):
self.status = status
self.has_content_body = not content is NoContent
self.raw_content = content # content prior to filtering

View File

@ -0,0 +1,9 @@
"""Force import of all modules in this package in order to get the standard test runner to pick up the tests. Yowzers."""
import os
modules = [filename.rsplit('.', 1)[0]
for filename in os.listdir(os.path.dirname(__file__))
if filename.endswith('.py') and not filename.startswith('_')]
for module in modules:
exec("from djangorestframework.tests.%s import *" % module)

View File

@ -0,0 +1,64 @@
from django.test import TestCase
from djangorestframework.tests.utils import RequestFactory
from djangorestframework.resource import Resource
# See: http://www.useragentstring.com/
MSIE_9_USER_AGENT = 'Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))'
MSIE_8_USER_AGENT = 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)'
MSIE_7_USER_AGENT = 'Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)'
FIREFOX_4_0_USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/4.0 (.NET CLR 3.5.30729)'
CHROME_11_0_USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.655.0 Safari/534.17'
SAFARI_5_0_USER_AGENT = 'Mozilla/5.0 (X11; U; Linux x86_64; en-ca) AppleWebKit/531.2+ (KHTML, like Gecko) Version/5.0 Safari/531.2+'
OPERA_11_0_MSIE_USER_AGENT = 'Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; pl) Opera 11.00'
OPERA_11_0_OPERA_USER_AGENT = 'Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00'
class UserAgentMungingTest(TestCase):
"""We need to fake up the accept headers when we deal with MSIE. Blergh.
http://www.gethifi.com/blog/browser-rest-http-accept-headers"""
def setUp(self):
class MockResource(Resource):
anon_allowed_methods = allowed_methods = ('GET',)
def get(self, request, auth):
return {'a':1, 'b':2, 'c':3}
self.req = RequestFactory()
self.MockResource = MockResource
def test_munge_msie_accept_header(self):
"""Send MSIE user agent strings and ensure that we get an HTML response,
even if we set a */* accept header."""
for user_agent in (MSIE_9_USER_AGENT,
MSIE_8_USER_AGENT,
MSIE_7_USER_AGENT):
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
resp = self.MockResource(req)
self.assertEqual(resp['Content-Type'], 'text/html')
def test_dont_munge_msie_accept_header(self):
"""Turn off _MUNGE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
that we get a JSON response if we set a */* accept header."""
self.MockResource._MUNGE_IE_ACCEPT_HEADER = False
for user_agent in (MSIE_9_USER_AGENT,
MSIE_8_USER_AGENT,
MSIE_7_USER_AGENT):
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
resp = self.MockResource(req)
self.assertEqual(resp['Content-Type'], 'application/json')
def test_dont_munge_nice_browsers_accept_header(self):
"""Send Non-MSIE user agent strings and ensure that we get a JSON response,
if we set a */* Accept header. (Other browsers will correctly set the Accept header)"""
for user_agent in (FIREFOX_4_0_USER_AGENT,
CHROME_11_0_USER_AGENT,
SAFARI_5_0_USER_AGENT,
OPERA_11_0_MSIE_USER_AGENT,
OPERA_11_0_OPERA_USER_AGENT):
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
resp = self.MockResource(req)
self.assertEqual(resp['Content-Type'], 'application/json')

View File

@ -0,0 +1,120 @@
from django.test import TestCase
from djangorestframework.tests.utils import RequestFactory
from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
class TestContentMixins(TestCase):
def setUp(self):
self.req = RequestFactory()
# Interface tests
def test_content_mixin_interface(self):
"""Ensure the ContentMixin interface is as expected."""
self.assertRaises(NotImplementedError, ContentMixin().determine_content, None)
def test_standard_content_mixin_interface(self):
"""Ensure the OverloadedContentMixin interface is as expected."""
self.assertTrue(issubclass(StandardContentMixin, ContentMixin))
getattr(StandardContentMixin, 'determine_content')
def test_overloaded_content_mixin_interface(self):
"""Ensure the OverloadedContentMixin interface is as expected."""
self.assertTrue(issubclass(OverloadedContentMixin, ContentMixin))
getattr(OverloadedContentMixin, 'FORM_PARAM_CONTENT')
getattr(OverloadedContentMixin, 'FORM_PARAM_CONTENTTYPE')
getattr(OverloadedContentMixin, 'determine_content')
# Common functionality to test with both StandardContentMixin and OverloadedContentMixin
def ensure_determines_no_content_GET(self, mixin):
"""Ensure determine_content(request) returns None for GET request with no content."""
request = self.req.get('/')
self.assertEqual(mixin.determine_content(request), None)
def ensure_determines_form_content_POST(self, mixin):
"""Ensure determine_content(request) returns content for POST request with content."""
form_data = {'qwerty': 'uiop'}
request = self.req.post('/', data=form_data)
self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
def ensure_determines_non_form_content_POST(self, mixin):
"""Ensure determine_content(request) returns (content type, content) for POST request with content."""
content = 'qwerty'
content_type = 'text/plain'
request = self.req.post('/', content, content_type=content_type)
self.assertEqual(mixin.determine_content(request), (content_type, content))
def ensure_determines_form_content_PUT(self, mixin):
"""Ensure determine_content(request) returns content for PUT request with content."""
form_data = {'qwerty': 'uiop'}
request = self.req.put('/', data=form_data)
self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
def ensure_determines_non_form_content_PUT(self, mixin):
"""Ensure determine_content(request) returns (content type, content) for PUT request with content."""
content = 'qwerty'
content_type = 'text/plain'
request = self.req.put('/', content, content_type=content_type)
self.assertEqual(mixin.determine_content(request), (content_type, content))
# StandardContentMixin behavioural tests
def test_standard_behaviour_determines_no_content_GET(self):
"""Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
self.ensure_determines_no_content_GET(StandardContentMixin())
def test_standard_behaviour_determines_form_content_POST(self):
"""Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
self.ensure_determines_form_content_POST(StandardContentMixin())
def test_standard_behaviour_determines_non_form_content_POST(self):
"""Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
self.ensure_determines_non_form_content_POST(StandardContentMixin())
def test_standard_behaviour_determines_form_content_PUT(self):
"""Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
self.ensure_determines_form_content_PUT(StandardContentMixin())
def test_standard_behaviour_determines_non_form_content_PUT(self):
"""Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
self.ensure_determines_non_form_content_PUT(StandardContentMixin())
# OverloadedContentMixin behavioural tests
def test_overloaded_behaviour_determines_no_content_GET(self):
"""Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
self.ensure_determines_no_content_GET(OverloadedContentMixin())
def test_overloaded_behaviour_determines_form_content_POST(self):
"""Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
self.ensure_determines_form_content_POST(OverloadedContentMixin())
def test_overloaded_behaviour_determines_non_form_content_POST(self):
"""Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
self.ensure_determines_non_form_content_POST(OverloadedContentMixin())
def test_overloaded_behaviour_determines_form_content_PUT(self):
"""Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
self.ensure_determines_form_content_PUT(OverloadedContentMixin())
def test_overloaded_behaviour_determines_non_form_content_PUT(self):
"""Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
self.ensure_determines_non_form_content_PUT(OverloadedContentMixin())
def test_overloaded_behaviour_allows_content_tunnelling(self):
"""Ensure determine_content(request) returns (content type, content) for overloaded POST request"""
content = 'qwerty'
content_type = 'text/plain'
form_data = {OverloadedContentMixin.FORM_PARAM_CONTENT: content,
OverloadedContentMixin.FORM_PARAM_CONTENTTYPE: content_type}
request = self.req.post('/', form_data)
self.assertEqual(OverloadedContentMixin().determine_content(request), (content_type, content))
def test_overloaded_behaviour_allows_content_tunnelling_content_type_not_set(self):
"""Ensure determine_content(request) returns (None, content) for overloaded POST request with content type not set"""
content = 'qwerty'
request = self.req.post('/', {OverloadedContentMixin.FORM_PARAM_CONTENT: content})
self.assertEqual(OverloadedContentMixin().determine_content(request), (None, content))

View File

@ -0,0 +1,52 @@
from django.test import TestCase
from djangorestframework.tests.utils import RequestFactory
from djangorestframework.methods import MethodMixin, StandardMethodMixin, OverloadedPOSTMethodMixin
class TestMethodMixins(TestCase):
def setUp(self):
self.req = RequestFactory()
# Interface tests
def test_method_mixin_interface(self):
"""Ensure the base ContentMixin interface is as expected."""
self.assertRaises(NotImplementedError, MethodMixin().determine_method, None)
def test_standard_method_mixin_interface(self):
"""Ensure the StandardMethodMixin interface is as expected."""
self.assertTrue(issubclass(StandardMethodMixin, MethodMixin))
getattr(StandardMethodMixin, 'determine_method')
def test_overloaded_method_mixin_interface(self):
"""Ensure the OverloadedPOSTMethodMixin interface is as expected."""
self.assertTrue(issubclass(OverloadedPOSTMethodMixin, MethodMixin))
getattr(OverloadedPOSTMethodMixin, 'FORM_PARAM_METHOD')
getattr(OverloadedPOSTMethodMixin, 'determine_method')
# Behavioural tests
def test_standard_behaviour_determines_GET(self):
"""GET requests identified as GET method with StandardMethodMixin"""
request = self.req.get('/')
self.assertEqual(StandardMethodMixin().determine_method(request), 'GET')
def test_standard_behaviour_determines_POST(self):
"""POST requests identified as POST method with StandardMethodMixin"""
request = self.req.post('/')
self.assertEqual(StandardMethodMixin().determine_method(request), 'POST')
def test_overloaded_POST_behaviour_determines_GET(self):
"""GET requests identified as GET method with OverloadedPOSTMethodMixin"""
request = self.req.get('/')
self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'GET')
def test_overloaded_POST_behaviour_determines_POST(self):
"""POST requests identified as POST method with OverloadedPOSTMethodMixin"""
request = self.req.post('/')
self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'POST')
def test_overloaded_POST_behaviour_determines_overloaded_method(self):
"""POST requests can be overloaded to another method by setting a reserved form field with OverloadedPOSTMethodMixin"""
request = self.req.post('/', {OverloadedPOSTMethodMixin.FORM_PARAM_METHOD: 'DELETE'})
self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'DELETE')

View File

@ -0,0 +1,25 @@
from django.test import TestCase
from djangorestframework.response import Response
try:
import unittest2
except:
unittest2 = None
else:
import warnings
warnings.filterwarnings("ignore")
if unittest2:
class TestResponse(TestCase, unittest2.TestCase):
# Interface tests
# This is mainly to remind myself that the Response interface needs to change slightly
@unittest2.expectedFailure
def test_response_interface(self):
"""Ensure the Response interface is as expected."""
response = Response()
getattr(response, 'status')
getattr(response, 'content')
getattr(response, 'headers')

View File

@ -0,0 +1,40 @@
from django.test import Client
from django.core.handlers.wsgi import WSGIRequest
# From: http://djangosnippets.org/snippets/963/
# Lovely stuff
class RequestFactory(Client):
"""
Class that lets you create mock Request objects for use in testing.
Usage:
rf = RequestFactory()
get_request = rf.get('/hello/')
post_request = rf.post('/submit/', {'foo': 'bar'})
This class re-uses the django.test.client.Client interface, docs here:
http://www.djangoproject.com/documentation/testing/#the-test-client
Once you have a request object you can pass it to any view function,
just as if that view had been hooked up using a URLconf.
"""
def request(self, **request):
"""
Similar to parent class, but returns the request object as soon as it
has created it.
"""
environ = {
'HTTP_COOKIE': self.cookies,
'PATH_INFO': '/',
'QUERY_STRING': '',
'REQUEST_METHOD': 'GET',
'SCRIPT_NAME': '',
'SERVER_NAME': 'testserver',
'SERVER_PORT': 80,
'SERVER_PROTOCOL': 'HTTP/1.1',
}
environ.update(self.defaults)
environ.update(request)
return WSGIRequest(environ)

View File

@ -0,0 +1,151 @@
from django import forms
from django.test import TestCase
from djangorestframework.tests.utils import RequestFactory
from djangorestframework.validators import ValidatorMixin, FormValidatorMixin, ModelFormValidatorMixin
from djangorestframework.response import ResponseException
class TestValidatorMixins(TestCase):
def setUp(self):
self.req = RequestFactory()
class MockForm(forms.Form):
qwerty = forms.CharField(required=True)
class MockValidator(FormValidatorMixin):
form = MockForm
class DisabledValidator(FormValidatorMixin):
form = None
self.MockValidator = MockValidator
self.DisabledValidator = DisabledValidator
# Interface tests
def test_validator_mixin_interface(self):
"""Ensure the ContentMixin interface is as expected."""
self.assertRaises(NotImplementedError, ValidatorMixin().validate, None)
def test_form_validator_mixin_interface(self):
"""Ensure the OverloadedContentMixin interface is as expected."""
self.assertTrue(issubclass(FormValidatorMixin, ValidatorMixin))
getattr(FormValidatorMixin, 'form')
getattr(FormValidatorMixin, 'validate')
def test_model_form_validator_mixin_interface(self):
"""Ensure the OverloadedContentMixin interface is as expected."""
self.assertTrue(issubclass(ModelFormValidatorMixin, FormValidatorMixin))
getattr(ModelFormValidatorMixin, 'model')
getattr(ModelFormValidatorMixin, 'form')
getattr(ModelFormValidatorMixin, 'validate')
# Behavioural tests - FormValidatorMixin
def test_validate_returns_content_unchanged_if_no_form_is_set(self):
"""If the form attribute is None then validate(content) should just return the content unmodified."""
content = {'qwerty':'uiop'}
self.assertEqual(self.DisabledValidator().validate(content), content)
def test_get_bound_form_returns_none_if_no_form_is_set(self):
"""If the form attribute is None then get_bound_form(content) should just return None."""
content = {'qwerty':'uiop'}
self.assertEqual(self.DisabledValidator().get_bound_form(content), None)
def test_validate_returns_content_unchanged_if_validates_and_does_not_need_cleanup(self):
"""If the content is already valid and clean then validate(content) should just return the content unmodified."""
content = {'qwerty':'uiop'}
self.assertEqual(self.MockValidator().validate(content), content)
def test_form_validation_failure_raises_response_exception(self):
"""If form validation fails a ResourceException 400 (Bad Request) should be raised."""
content = {}
self.assertRaises(ResponseException, self.MockValidator().validate, content)
def test_validate_does_not_allow_extra_fields(self):
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
broken clients more easily (eg submitting content with a misnamed field)"""
content = {'qwerty': 'uiop', 'extra': 'extra'}
self.assertRaises(ResponseException, self.MockValidator().validate, content)
def test_validate_allows_extra_fields_if_explicitly_set(self):
"""If we include an extra_fields paramater on _validate, then allow fields with those names."""
content = {'qwerty': 'uiop', 'extra': 'extra'}
self.MockValidator()._validate(content, extra_fields=('extra',))
def test_validate_checks_for_extra_fields_if_explicitly_set(self):
"""If we include an extra_fields paramater on _validate, then fail unless we have fields with those names."""
content = {'qwerty': 'uiop'}
try:
self.MockValidator()._validate(content, extra_fields=('extra',))
except ResponseException, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field is required.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
def test_validate_failed_due_to_no_content_returns_appropriate_message(self):
"""If validation fails due to no content, ensure the response contains a single non-field error"""
content = {}
try:
self.MockValidator().validate(content)
except ResponseException, exc:
self.assertEqual(exc.response.raw_content, {'errors': ['No content was supplied.']})
else:
self.fail('ResourceException was not raised') #pragma: no cover
def test_validate_failed_due_to_field_error_returns_appropriate_message(self):
"""If validation fails due to a field error, ensure the response contains a single field error"""
content = {'qwerty': ''}
try:
self.MockValidator().validate(content)
except ResponseException, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
def test_validate_failed_due_to_invalid_field_returns_appropriate_message(self):
"""If validation fails due to an invalid field, ensure the response contains a single field error"""
content = {'qwerty': 'uiop', 'extra': 'extra'}
try:
self.MockValidator().validate(content)
except ResponseException, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field does not exist.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
def test_validate_failed_due_to_multiple_errors_returns_appropriate_message(self):
"""If validation for multiple reasons, ensure the response contains each error"""
content = {'qwerty': '', 'extra': 'extra'}
try:
self.MockValidator().validate(content)
except ResponseException, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'],
'extra': ['This field does not exist.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
def test_validate_failed_due_to_non_field_error_returns_appropriate_message(self):
"""If validation for with a non-field error, ensure the response a non-field error"""
class MockForm(forms.Form):
field1 = forms.CharField(required=False)
field2 = forms.CharField(required=False)
ERROR_TEXT = 'You may not supply both field1 and field2'
def clean(self):
if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
raise forms.ValidationError(self.ERROR_TEXT)
return self.cleaned_data #pragma: no cover
class MockValidator(FormValidatorMixin):
form = MockForm
content = {'field1': 'example1', 'field2': 'example2'}
try:
MockValidator().validate(content)
except ResponseException, exc:
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
else:
self.fail('ResourceException was not raised') #pragma: no cover

View File

@ -9,6 +9,17 @@ except ImportError:
import StringIO
def as_tuple(obj):
"""Given obj return a tuple"""
if obj is None:
return ()
elif isinstance(obj, list):
return tuple(obj)
elif isinstance(obj, tuple):
return obj
return (obj,)
def url_resolves(url):
"""Return True if the given URL is mapped to a view in the urlconf, False otherwise."""
try:

View File

@ -8,4 +8,19 @@
{% block htmltitle %}<title>{% if pagename == 'index' %}Django REST framework{% else %}{{ titleprefix }}{{ title|striptags|e }}{% endif %}</title>{% endblock %}
{% block extrahead %}
{{ super() }}
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-18852272-2']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
{% endblock %}