Sphinx docs, examples, lots of refactoring

parent 4100242f
[
{
"pk": 1,
"model": "auth.user",
"fields": {
"username": "admin",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": true,
"is_staff": true,
"last_login": "2010-01-01 00:00:00",
"groups": [],
"user_permissions": [],
"password": "sha1$6cbce$e4e808893d586a3301ac3c14da6c84855999f1d8",
"email": "test@example.com",
"date_joined": "2010-01-01 00:00:00"
}
}
]
\ No newline at end of file
#!/usr/bin/env python
from django.core.management import execute_manager
try:
import settings # Assumed to be in the same directory.
except ImportError:
import sys
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
sys.exit(1)
if __name__ == "__main__":
execute_manager(settings)
from django.template import RequestContext, loader
import json
from utils import dict2xml
class BaseEmitter(object):
uses_forms = False
def __init__(self, resource):
self.resource = resource
def emit(self, output):
return output
class TemplatedEmitter(BaseEmitter):
template = None
def emit(self, output):
if output is None:
content = ''
else:
content = json.dumps(output, indent=4, sort_keys=True)
template = loader.get_template(self.template)
context = RequestContext(self.resource.request, {
'content': content,
'resource': self.resource,
})
ret = template.render(context)
# Munge DELETE Response code to allow us to return content
# (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
if self.resource.resp_status == 204:
self.resource.resp_status = 200
return ret
class JSONEmitter(BaseEmitter):
def emit(self, output):
if output is None:
# Treat None as no message body, rather than serializing
return ''
return json.dumps(output)
class XMLEmitter(BaseEmitter):
def emit(self, output):
if output is None:
# Treat None as no message body, rather than serializing
return ''
return dict2xml(output)
class HTMLEmitter(TemplatedEmitter):
template = 'emitter.html'
uses_forms = True
class TextEmitter(TemplatedEmitter):
template = 'emitter.txt'
import json
from rest.status import ResourceException, Status
class BaseParser(object):
def __init__(self, resource):
self.resource = resource
def parse(self, input):
return {}
class JSONParser(BaseParser):
def parse(self, input):
try:
return json.loads(input)
except ValueError, exc:
raise ResourceException(Status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
class XMLParser(BaseParser):
pass
class FormParser(BaseParser):
"""The default parser for form data.
Return a dict containing a single value for each non-reserved parameter
"""
def __init__(self, resource):
if resource.request.method == 'PUT':
# Fix from piston to force Django to give PUT requests the same
# form processing that POST requests get...
#
# Bug fix: if _load_post_and_files has already been called, for
# example by middleware accessing request.POST, the below code to
# pretend the request is a POST instead of a PUT will be too late
# to make a difference. Also calling _load_post_and_files will result
# in the following exception:
# AttributeError: You cannot set the upload handlers after the upload has been processed.
# The fix is to check for the presence of the _post field which is set
# the first time _load_post_and_files is called (both by wsgi.py and
# modpython.py). If it's set, the request has to be 'reset' to redo
# the query value parsing in POST mode.
if hasattr(resource.request, '_post'):
del request._post
del request._files
try:
resource.request.method = "POST"
resource.request._load_post_and_files()
resource.request.method = "PUT"
except AttributeError:
resource.request.META['REQUEST_METHOD'] = 'POST'
resource.request._load_post_and_files()
resource.request.META['REQUEST_METHOD'] = 'PUT'
#
self.data = {}
for (key, val) in resource.request.POST.items():
if key not in resource.RESERVED_PARAMS:
self.data[key] = val
def parse(self, input):
return self.data
class Status(object):
"""Descriptive HTTP status codes, for code readability."""
HTTP_200_OK = 200
HTTP_201_CREATED = 201
HTTP_202_ACCEPTED = 202
HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
HTTP_204_NO_CONTENT = 204
HTTP_205_RESET_CONTENT = 205
HTTP_206_PARTIAL_CONTENT = 206
HTTP_400_BAD_REQUEST = 400
HTTP_401_UNAUTHORIZED = 401
HTTP_402_PAYMENT_REQUIRED = 402
HTTP_403_FORBIDDEN = 403
HTTP_404_NOT_FOUND = 404
HTTP_405_METHOD_NOT_ALLOWED = 405
HTTP_406_NOT_ACCEPTABLE = 406
HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
HTTP_408_REQUEST_TIMEOUT = 408
HTTP_409_CONFLICT = 409
HTTP_410_GONE = 410
HTTP_411_LENGTH_REQUIRED = 411
HTTP_412_PRECONDITION_FAILED = 412
HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
HTTP_414_REQUEST_URI_TOO_LONG = 414
HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
HTTP_417_EXPECTATION_FAILED = 417
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_300_MULTIPLE_CHOICES = 300
HTTP_301_MOVED_PERMANENTLY = 301
HTTP_302_FOUND = 302
HTTP_303_SEE_OTHER = 303
HTTP_304_NOT_MODIFIED = 304
HTTP_305_USE_PROXY = 305
HTTP_306_RESERVED = 306
HTTP_307_TEMPORARY_REDIRECT = 307
HTTP_500_INTERNAL_SERVER_ERROR = 500
HTTP_501_NOT_IMPLEMENTED = 501
HTTP_502_BAD_GATEWAY = 502
HTTP_503_SERVICE_UNAVAILABLE = 503
HTTP_504_GATEWAY_TIMEOUT = 504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
class ResourceException(Exception):
def __init__(self, status, content='', headers={}):
self.status = status
self.content = content
self.headers = headers
{% load urlize_quoted_links %}{% load add_query_param %}<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<style>
pre {border: 1px solid black; padding: 1em; background: #ffd}
div.action {padding: 0.5em 1em; margin-bottom: 0.5em; background: #ddf}
ul.accepttypes {float: right; list-style-type: none; margin: 0; padding: 0}
ul.accepttypes li {display: inline;}
form div {margin: 0.5em 0}
form div * {vertical-align: top}
form ul.errorlist {display: inline; margin: 0; padding: 0}
form ul.errorlist li {display: inline; color: red;}
.clearing {display: block; margin: 0; padding: 0; clear: both;}
</style>
<title>API - {{ resource.name }}</title>
</head>
<body>
<h1>{{ resource.name }}</h1>
<p>{{ resource.description|linebreaksbr }}</p>
<pre><b>{{ resource.resp_status }} {{ resource.resp_status_text }}</b>{% autoescape off %}
{% for key, val in resource.resp_headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
{% endfor %}
{{ content|urlize_quoted_links }} </pre>{% endautoescape %}
{% if 'read' in resource.allowed_operations %}
<div class='action'>
<a href='{{ resource.request.path }}'>Read</a>
<ul class="accepttypes">
{% for content_type in resource.available_content_types %}
{% with resource.ACCEPT_PARAM|add:"="|add:content_type as param %}
<li>[<a href='{{ resource.request.path|add_query_param:param }}'>{{ content_type }}</a>]</li>
{% endwith %}
{% endfor %}
</ul>
<div class="clearing"></div>
</div>
{% endif %}
{% if 'create' in resource.allowed_operations %}
<div class='action'>
<form action="{{ resource.request.path }}" method="post">
{% csrf_token %}
{% with resource.form_instance as form %}
{% for field in form %}
<div>
{{ field.label_tag }}:
{{ field }}
{{ field.help_text }}
{{ field.errors }}
</div>
{% endfor %}
{% endwith %}
<div class="clearing"></div>
<input type="submit" value="Create" />
</form>
</div>
{% endif %}
{% if 'update' in resource.allowed_operations %}
<div class='action'>
<form action="{{ resource.request.path }}" method="post">
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" />
{% csrf_token %}
{% with resource.form_instance as form %}
{% for field in form %}
<div>
{{ field.label_tag }}:
{{ field }}
{{ field.help_text }}
{{ field.errors }}
</div>
{% endfor %}
{% endwith %}
<div class="clearing"></div>
<input type="submit" value="Update" />
</form>
</div>
{% endif %}
{% if 'delete' in resource.allowed_operations %}
<div class='action'>
<form action="{{ resource.request.path }}" method="post">
{% csrf_token %}
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="DELETE" />
<input type="submit" value="Delete" />
</form>
</div>
{% endif %}
</body>
</html>
\ No newline at end of file
{{ resource.name }}
{{ resource.description }}
{% autoescape off %}HTTP/1.0 {{ resource.resp_status }} {{ resource.resp_status_text }}
{% for key, val in resource.resp_headers.items %}{{ key }}: {{ val }}
{% endfor %}
{{ content }}{% endautoescape %}
HTML:
{{ content }}
\ No newline at end of file
from django.template import Library
from urlparse import urlparse, urlunparse
from urllib import quote
register = Library()
def add_query_param(url, param):
(key, val) = param.split('=')
param = '%s=%s' % (key, quote(val))
(scheme, netloc, path, params, query, fragment) = urlparse(url)
if query:
query += "&" + param
else:
query = param
return urlunparse((scheme, netloc, path, params, query, fragment))
register.filter('add_query_param', add_query_param)
"""Adds the custom filter 'urlize_quoted_links'
This is identical to the built-in filter 'urlize' with the exception that
single and double quotes are permitted as leading or trailing punctuation.
"""
# Almost all of this code is copied verbatim from django.utils.html
# LEADING_PUNCTUATION and TRAILING_PUNCTUATION have been modified
import re
import string
from django.utils.safestring import SafeData, mark_safe
from django.utils.encoding import force_unicode
from django.utils.http import urlquote
from django.utils.html import escape
from django import template
# Configuration for urlize() function.
LEADING_PUNCTUATION = ['(', '<', '&lt;', '"', "'"]
TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '&gt;', '"', "'"]
# List of possible strings used for bullets in bulleted lists.
DOTS = ['&middot;', '*', '\xe2\x80\xa2', '&#149;', '&bull;', '&#8226;']
unencoded_ampersands_re = re.compile(r'&(?!(\w+|#\d+);)')
word_split_re = re.compile(r'(\s+)')
punctuation_re = re.compile('^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$' % \
('|'.join([re.escape(x) for x in LEADING_PUNCTUATION]),
'|'.join([re.escape(x) for x in TRAILING_PUNCTUATION])))
simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
link_target_attribute_re = re.compile(r'(<a [^>]*?)target=[^\s>]+')
html_gunk_re = re.compile(r'(?:<br clear="all">|<i><\/i>|<b><\/b>|<em><\/em>|<strong><\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE)
hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL)
trailing_empty_content_re = re.compile(r'(?:<p>(?:&nbsp;|\s|<br \/>)*?</p>\s*)+\Z')
def urlize_quoted_links(text, trim_url_limit=None, nofollow=False, autoescape=True):
"""
Converts any URLs in text into clickable links.
Works on http://, https://, www. links and links ending in .org, .net or
.com. Links can have trailing punctuation (periods, commas, close-parens)
and leading punctuation (opening parens) and it'll still do the right
thing.
If trim_url_limit is not None, the URLs in link text longer than this limit
will truncated to trim_url_limit-3 characters and appended with an elipsis.
If nofollow is True, the URLs in link text will get a rel="nofollow"
attribute.
If autoescape is True, the link text and URLs will get autoescaped.
"""
trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
safe_input = isinstance(text, SafeData)
words = word_split_re.split(force_unicode(text))
nofollow_attr = nofollow and ' rel="nofollow"' or ''
for i, word in enumerate(words):
match = None
if '.' in word or '@' in word or ':' in word:
match = punctuation_re.match(word)
if match:
lead, middle, trail = match.groups()
# Make URL we want to point to.
url = None
if middle.startswith('http://') or middle.startswith('https://'):
url = urlquote(middle, safe='/&=:;#?+*')
elif middle.startswith('www.') or ('@' not in middle and \
middle and middle[0] in string.ascii_letters + string.digits and \
(middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))):
url = urlquote('http://%s' % middle, safe='/&=:;#?+*')
elif '@' in middle and not ':' in middle and simple_email_re.match(middle):
url = 'mailto:%s' % middle
nofollow_attr = ''
# Make link.
if url:
trimmed = trim_url(middle)
if autoescape and not safe_input:
lead, trail = escape(lead), escape(trail)
url, trimmed = escape(url), escape(trimmed)
middle = '<a href="%s"%s>%s</a>' % (url, nofollow_attr, trimmed)
words[i] = mark_safe('%s%s%s' % (lead, middle, trail))
else:
if safe_input:
words[i] = mark_safe(word)
elif autoescape:
words[i] = escape(word)
elif safe_input:
words[i] = mark_safe(word)
elif autoescape:
words[i] = escape(word)
return u''.join(words)
#urlize_quoted_links.needs_autoescape = True
urlize_quoted_links.is_safe = True
# Register urlize_quoted_links as a custom filter
# http://docs.djangoproject.com/en/dev/howto/custom-template-tags/
register = template.Library()
register.filter(urlize_quoted_links)
\ No newline at end of file
import re
import xml.etree.ElementTree as ET
from django.utils.encoding import smart_unicode
from django.utils.xmlutils import SimplerXMLGenerator
try:
import cStringIO as StringIO
except ImportError:
import StringIO
# From piston
def coerce_put_post(request):
"""
Django doesn't particularly understand REST.
In case we send data over PUT, Django won't
actually look at the data and load it. We need
to twist its arm here.
The try/except abominiation here is due to a bug
in mod_python. This should fix it.
"""
if request.method != 'PUT':
return
# Bug fix: if _load_post_and_files has already been called, for
# example by middleware accessing request.POST, the below code to
# pretend the request is a POST instead of a PUT will be too late
# to make a difference. Also calling _load_post_and_files will result
# in the following exception:
# AttributeError: You cannot set the upload handlers after the upload has been processed.
# The fix is to check for the presence of the _post field which is set
# the first time _load_post_and_files is called (both by wsgi.py and
# modpython.py). If it's set, the request has to be 'reset' to redo
# the query value parsing in POST mode.
if hasattr(request, '_post'):
del request._post
del request._files
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
# From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml
#class object_dict(dict):
# """object view of dict, you can
# >>> a = object_dict()
# >>> a.fish = 'fish'
# >>> a['fish']
# 'fish'
# >>> a['water'] = 'water'
# >>> a.water
# 'water'
# >>> a.test = {'value': 1}
# >>> a.test2 = object_dict({'name': 'test2', 'value': 2})
# >>> a.test, a.test2.name, a.test2.value
# (1, 'test2', 2)
# """
# def __init__(self, initd=None):
# if initd is None:
# initd = {}
# dict.__init__(self, initd)
#
# def __getattr__(self, item):
# d = self.__getitem__(item)
# # if value is the only key in object, you can omit it
# if isinstance(d, dict) and 'value' in d and len(d) == 1:
# return d['value']
# else:
# return d
#
# def __setattr__(self, item, value):
# self.__setitem__(item, value)
# From xml2dict
class XML2Dict(object):
def __init__(self):
pass
def _parse_node(self, node):
node_tree = {}
# Save attrs and text, hope there will not be a child with same name
if node.text:
node_tree = node.text
for (k,v) in node.attrib.items():
k,v = self._namespace_split(k, v)
node_tree[k] = v
#Save childrens
for child in node.getchildren():
tag, tree = self._namespace_split(child.tag, self._parse_node(child))
if tag not in node_tree: # the first time, so store it in dict
node_tree[tag] = tree
continue
old = node_tree[tag]
if not isinstance(old, list):
node_tree.pop(tag)
node_tree[tag] = [old] # multi times, so change old dict to a list
node_tree[tag].append(tree) # add the new one
return node_tree
def _namespace_split(self, tag, value):
"""
Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
ns = http://cs.sfsu.edu/csc867/myscheduler
name = patients
"""
result = re.compile("\{(.*)\}(.*)").search(tag)
if result:
print tag
value.namespace, tag = result.groups()
return (tag, value)
def parse(self, file):
"""parse a xml file to a dict"""
f = open(file, 'r')
return self.fromstring(f.read())
def fromstring(self, s):
"""parse a string"""
t = ET.fromstring(s)
unused_root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
return root_tree
def xml2dict(input):
return XML2Dict().fromstring(input)
# Piston:
class XMLEmitter():
def _to_xml(self, xml, data):
if isinstance(data, (list, tuple)):
for item in data:
xml.startElement("list-item", {})
self._to_xml(xml, item)
xml.endElement("list-item")
elif isinstance(data, dict):
for key, value in data.iteritems():
xml.startElement(key, {})
self._to_xml(xml, value)
xml.endElement(key)
else:
xml.characters(smart_unicode(data))
def dict2xml(self, data):
stream = StringIO.StringIO()
xml = SimplerXMLGenerator(stream, "utf-8")
xml.startDocument()
xml.startElement("root", {})
self._to_xml(xml, data)
xml.endElement("root")
xml.endDocument()
return stream.getvalue()
def dict2xml(input):
return XMLEmitter().dict2xml(input)
# Django settings for src project.
DEBUG = True
TEMPLATE_DEBUG = DEBUG
ADMINS = (
# ('Your Name', 'your_email@domain.com'),
)
MANAGERS = ADMINS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
'NAME': 'sqlite3.db', # Or path to database file if using sqlite3.
'USER': '', # Not used with sqlite3.
'PASSWORD': '', # Not used with sqlite3.
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '', # Set to empty string for default. Not used with sqlite3.
}
}
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# On Unix systems, a value of None will cause Django to use the same
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'Europe/London'
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-uk'
SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale
USE_L10N = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/"
MEDIA_ROOT = ''
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
MEDIA_URL = ''
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
ADMIN_MEDIA_PREFIX = '/media/'
# Make this unique, and don't share it with anybody.
SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu'
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
# 'django.template.loaders.eggs.Loader',
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
)
ROOT_URLCONF = 'urls'
TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
)
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
# Uncomment the next line to enable the admin:
'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'testapp',
'rest',
)
from django import forms
class ExampleForm(forms.Form):
title = forms.CharField(max_length=100)
message = forms.CharField()
sender = forms.EmailField()
valid = forms.BooleanField(required=False)
from django.db import models
from django.template.defaultfilters import slugify
import uuid
def uuid_str():
return str(uuid.uuid1())
#class ExampleModel(models.Model):
# num = models.IntegerField(default=2, choices=((1,'one'), (2, 'two')))
# hidden_num = models.IntegerField(verbose_name='Something', help_text='HELP')
# text = models.TextField(blank=False)
# another = models.CharField(max_length=10)
#class ExampleContainer(models.Model):
# """Container. Has a key, a name, and some internal data, and contains a set of items."""
# key = models.CharField(primary_key=True, default=uuid_str, max_length=36, editable=False)
# name = models.CharField(max_length=256)
# internal = models.IntegerField(default=0)
# @models.permalink
# def get_absolute_url(self):
# return ('testapp.views.ContainerInstance', [self.key])
#class ExampleItem(models.Model):
# """Item. Belongs to a container and has an index number and a note.
# Items are uniquely identified by their container and index number."""
# container = models.ForeignKey(ExampleContainer, related_name='items')
# index = models.IntegerField()
# note = models.CharField(max_length=1024)
# unique_together = (container, index)
RATING_CHOICES = ((0, 'Awful'),
(1, 'Poor'),
(2, 'OK'),
(3, 'Good'),
(4, 'Excellent'))
class BlogPost(models.Model):
key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False)
title = models.CharField(max_length=128)
content = models.TextField()
created = models.DateTimeField(auto_now_add=True)
slug = models.SlugField(editable=False, default='')
class Meta:
ordering = ('created',)
@models.permalink
def get_absolute_url(self):
return ('testapp.views.BlogPostInstance', (self.key,))
@property
@models.permalink
def comments_url(self):
"""Link to a resource which lists all comments for this blog post."""
return ('testapp.views.CommentList', (self.key,))
@property
@models.permalink
def comment_url(self):
"""Link to a resource which can create a comment for this blog post."""
return ('testapp.views.CommentCreator', (self.key,))
def __unicode__(self):
return self.title
def save(self, *args, **kwargs):
self.slug = slugify(self.title)
super(self.__class__, self).save(*args, **kwargs)
class Comment(models.Model):
blogpost = models.ForeignKey(BlogPost, editable=False, related_name='comments')
username = models.CharField(max_length=128)
comment = models.TextField()
rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='How did you rate this post?')
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ('created',)
@models.permalink
def get_absolute_url(self):
return ('testapp.views.CommentInstance', (self.blogpost.key, self.id))
@property
@models.permalink
def blogpost_url(self):
"""Link to the blog post resource which this comment corresponds to."""
return ('testapp.views.BlogPostInstance', (self.blogpost.key,))
"""Test a range of REST API usage of the example application.
"""
from django.test import TestCase
from django.core.urlresolvers import reverse
from testapp import views
#import json
#from rest.utils import xml2dict, dict2xml
class AcceptHeaderTests(TestCase):
"""Test correct behaviour of the Accept header as specified by RFC 2616:
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1"""
def assert_accept_mimetype(self, mimetype, expect=None):
"""Assert that a request with given mimetype in the accept header,
gives a response with the appropriate content-type."""
if expect is None:
expect = mimetype
resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT=mimetype)
self.assertEquals(resp['content-type'], expect)
def test_accept_json(self):
"""Ensure server responds with Content-Type of JSON when requested."""
self.assert_accept_mimetype('application/json')
def test_accept_xml(self):
"""Ensure server responds with Content-Type of XML when requested."""
self.assert_accept_mimetype('application/xml')
def test_accept_json_when_prefered_to_xml(self):
"""Ensure server responds with Content-Type of JSON when it is the client's prefered choice."""
self.assert_accept_mimetype('application/json,q=0.9;application/xml,q=0.1', expect='application/json')
def test_accept_xml_when_prefered_to_json(self):
"""Ensure server responds with Content-Type of XML when it is the client's prefered choice."""
self.assert_accept_mimetype('application/xml,q=0.9;application/json,q=0.1', expect='application/xml')
def test_default_json_prefered(self):
"""Ensure server responds with JSON in preference to XML."""
self.assert_accept_mimetype('application/json;application/xml', expect='application/json')
def test_accept_generic_subtype_format(self):
"""Ensure server responds with an appropriate type, when the subtype is left generic."""
self.assert_accept_mimetype('text/*', expect='text/html')
def test_accept_generic_type_format(self):
"""Ensure server responds with an appropriate type, when the type and subtype are left generic."""
self.assert_accept_mimetype('*/*', expect='application/json')
def test_invalid_accept_header_returns_406(self):
"""Ensure server returns a 406 (not acceptable) response if we set the Accept header to junk."""
resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT='invalid/invalid')
self.assertNotEquals(resp['content-type'], 'invalid/invalid')
self.assertEquals(resp.status_code, 406)
def test_prefer_specific_over_generic(self): # This test is broken right now
"""More specific accept types have precedence over less specific types."""
self.assert_accept_mimetype('application/xml;*/*', expect='application/xml')
class AllowedMethodsTests(TestCase):
"""Basic tests to check that only allowed operations may be performed on a Resource"""
def test_reading_a_read_only_resource_is_allowed(self):
"""GET requests on a read only resource should default to a 200 (OK) response"""
resp = self.client.get(reverse(views.RootResource))
self.assertEquals(resp.status_code, 200)
def test_writing_to_read_only_resource_is_not_allowed(self):
"""PUT requests on a read only resource should default to a 405 (method not allowed) response"""
resp = self.client.put(reverse(views.RootResource), {})
self.assertEquals(resp.status_code, 405)
#
# def test_reading_write_only_not_allowed(self):
# resp = self.client.get(reverse(views.WriteOnlyResource))
# self.assertEquals(resp.status_code, 405)
#
# def test_writing_write_only_allowed(self):
# resp = self.client.put(reverse(views.WriteOnlyResource), {})
# self.assertEquals(resp.status_code, 200)
#
#
#class EncodeDecodeTests(TestCase):
# def setUp(self):
# super(self.__class__, self).setUp()
# self.input = {'a': 1, 'b': 'example'}
#
# def test_encode_form_decode_json(self):
# content = self.input
# resp = self.client.put(reverse(views.WriteOnlyResource), content)
# output = json.loads(resp.content)
# self.assertEquals(self.input, output)
#
# def test_encode_json_decode_json(self):
# content = json.dumps(self.input)
# resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json')
# output = json.loads(resp.content)
# self.assertEquals(self.input, output)
#
# #def test_encode_xml_decode_json(self):
# # content = dict2xml(self.input)
# # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json')
# # output = json.loads(resp.content)
# # self.assertEquals(self.input, output)
#
# #def test_encode_form_decode_xml(self):
# # content = self.input
# # resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/xml')
# # output = xml2dict(resp.content)
# # self.assertEquals(self.input, output)
#
# #def test_encode_json_decode_xml(self):
# # content = json.dumps(self.input)
# # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
# # output = xml2dict(resp.content)
# # self.assertEquals(self.input, output)
#
# #def test_encode_xml_decode_xml(self):
# # content = dict2xml(self.input)
# # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
# # output = xml2dict(resp.content)
# # self.assertEquals(self.input, output)
#
#class ModelTests(TestCase):
# def test_create_container(self):
# content = json.dumps({'name': 'example'})
# resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json')
# output = json.loads(resp.content)
# self.assertEquals(resp.status_code, 201)
# self.assertEquals(output['name'], 'example')
# self.assertEquals(set(output.keys()), set(('absolute_uri', 'name', 'key')))
#
#class CreatedModelTests(TestCase):
# def setUp(self):
# content = json.dumps({'name': 'example'})
# resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json', HTTP_ACCEPT='application/json')
# self.container = json.loads(resp.content)
#
# def test_read_container(self):
# resp = self.client.get(self.container["absolute_uri"])
# self.assertEquals(resp.status_code, 200)
# container = json.loads(resp.content)
# self.assertEquals(container, self.container)
#
# def test_delete_container(self):
# resp = self.client.delete(self.container["absolute_uri"])
# self.assertEquals(resp.status_code, 204)
# self.assertEquals(resp.content, '')
#
# def test_update_container(self):
# self.container['name'] = 'new'
# content = json.dumps(self.container)
# resp = self.client.put(self.container["absolute_uri"], content, 'application/json')
# self.assertEquals(resp.status_code, 200)
# container = json.loads(resp.content)
# self.assertEquals(container, self.container)
from django.conf.urls.defaults import patterns
urlpatterns = patterns('testapp.views',
(r'^$', 'RootResource'),
#(r'^read-only$', 'ReadOnlyResource'),
#(r'^write-only$', 'WriteOnlyResource'),
#(r'^read-write$', 'ReadWriteResource'),
#(r'^model$', 'ModelFormResource'),
#(r'^container$', 'ContainerFactory'),
#(r'^container/((?P<key>[^/]+))$', 'ContainerInstance'),
(r'^blog-posts/$', 'BlogPostList'),
(r'^blog-post/$', 'BlogPostCreator'),
(r'^blog-post/(?P<key>[^/]+)/$', 'BlogPostInstance'),
(r'^blog-post/(?P<blogpost_id>[^/]+)/comments/$', 'CommentList'),
(r'^blog-post/(?P<blogpost_id>[^/]+)/comment/$', 'CommentCreator'),
(r'^blog-post/(?P<blogpost>[^/]+)/comments/(?P<id>[^/]+)/$', 'CommentInstance'),
)
from rest.resource import Resource
from rest.modelresource import ModelResource, QueryModelResource
from testapp.models import BlogPost, Comment
##### Root Resource #####
class RootResource(Resource):
"""This is the top level resource for the API.
All the sub-resources are discoverable from here."""
allowed_operations = ('read',)
def read(self, headers={}, *args, **kwargs):
return (200, {'blog-posts': self.reverse(BlogPostList),
'blog-post': self.reverse(BlogPostCreator)}, {})
##### Blog Post Resources #####
BLOG_POST_FIELDS = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url')
class BlogPostList(QueryModelResource):
"""A resource which lists all existing blog posts."""
allowed_operations = ('read', )
model = BlogPost
fields = BLOG_POST_FIELDS
class BlogPostCreator(ModelResource):
"""A resource with which blog posts may be created."""
allowed_operations = ('create',)
model = BlogPost
fields = BLOG_POST_FIELDS
class BlogPostInstance(ModelResource):
"""A resource which represents a single blog post."""
allowed_operations = ('read', 'update', 'delete')
model = BlogPost
fields = BLOG_POST_FIELDS
##### Comment Resources #####
COMMENT_FIELDS = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url')
class CommentList(QueryModelResource):
"""A resource which lists all existing comments for a given blog post."""
allowed_operations = ('read', )
model = Comment
fields = COMMENT_FIELDS
class CommentCreator(ModelResource):
"""A resource with which blog comments may be created for a given blog post."""
allowed_operations = ('create',)
model = Comment
fields = COMMENT_FIELDS
class CommentInstance(ModelResource):
"""A resource which represents a single comment."""
allowed_operations = ('read', 'update', 'delete')
model = Comment
fields = COMMENT_FIELDS
#
#'read-only-api': self.reverse(ReadOnlyResource),
# 'write-only-api': self.reverse(WriteOnlyResource),
# 'read-write-api': self.reverse(ReadWriteResource),
# 'model-api': self.reverse(ModelFormResource),
# 'create-container': self.reverse(ContainerFactory),
#
#class ReadOnlyResource(Resource):
# """This is my docstring
# """
# allowed_operations = ('read',)
#
# def read(self, headers={}, *args, **kwargs):
# return (200, {'ExampleString': 'Example',
# 'ExampleInt': 1,
# 'ExampleDecimal': 1.0}, {})
#
#
#class WriteOnlyResource(Resource):
# """This is my docstring
# """
# allowed_operations = ('update',)
#
# def update(self, data, headers={}, *args, **kwargs):
# return (200, data, {})
#
#
#class ReadWriteResource(Resource):
# allowed_operations = ('read', 'update', 'delete')
# create_form = ExampleForm
# update_form = ExampleForm
#
#
#class ModelFormResource(ModelResource):
# allowed_operations = ('read', 'update', 'delete')
# model = ExampleModel
#
## Nice things: form validation is applied to any input type
## html forms for output
## output always serialized nicely
#class ContainerFactory(ModelResource):
# allowed_operations = ('create',)
# model = ExampleContainer
# fields = ('absolute_uri', 'name', 'key')
# form_fields = ('name',)
#
#
#class ContainerInstance(ModelResource):
# allowed_operations = ('read', 'update', 'delete')
# model = ExampleContainer
# fields = ('absolute_uri', 'name', 'key')
# form_fields = ('name',)
#######################
from django.conf.urls.defaults import patterns, include
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
# Example:
(r'^', include('testapp.urls')),
# Uncomment the admin/doc line below to enable admin documentation:
(r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
(r'^admin/', include(admin.site.urls)),
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment