mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 08:14:16 +03:00
Mostly improving documentation
This commit is contained in:
parent
b0ce3f92c6
commit
9979903272
|
@ -3,5 +3,8 @@ syntax: glob
|
||||||
*.pyc
|
*.pyc
|
||||||
*.db
|
*.db
|
||||||
env
|
env
|
||||||
|
cache
|
||||||
|
html
|
||||||
.project
|
.project
|
||||||
.pydevproject
|
.pydevproject
|
||||||
|
.settings
|
||||||
|
|
|
@ -11,3 +11,7 @@ source ./env/bin/activate
|
||||||
pip install -r ./requirements.txt
|
pip install -r ./requirements.txt
|
||||||
python ./src/manage.py test
|
python ./src/manage.py test
|
||||||
|
|
||||||
|
# To build the documentation...
|
||||||
|
|
||||||
|
sphinx-build -c docs -b html -d cache docs html
|
||||||
|
|
||||||
|
|
220
docs/conf.py
Normal file
220
docs/conf.py
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Asset Platform documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Fri Nov 19 20:24:09 2010.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'src'))
|
||||||
|
import settings
|
||||||
|
from django.core.management import setup_environ
|
||||||
|
setup_environ(settings)
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
# -- General configuration -----------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||||
|
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
|
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode']
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = []
|
||||||
|
|
||||||
|
# The suffix of source filenames.
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The encoding of source files.
|
||||||
|
#source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = u'FlyWheel'
|
||||||
|
copyright = u'2011, Tom Christie'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
version = '0.1'
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = '0.1'
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#language = None
|
||||||
|
|
||||||
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
|
# non-false value, then it is used:
|
||||||
|
#today = ''
|
||||||
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
|
#today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
exclude_patterns = ['_build']
|
||||||
|
|
||||||
|
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||||
|
#default_role = None
|
||||||
|
|
||||||
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
|
#add_function_parentheses = True
|
||||||
|
|
||||||
|
# If true, the current module name will be prepended to all description
|
||||||
|
# unit titles (such as .. function::).
|
||||||
|
#add_module_names = True
|
||||||
|
|
||||||
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
|
# output. They are ignored by default.
|
||||||
|
#show_authors = False
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# A list of ignored prefixes for module index sorting.
|
||||||
|
#modindex_common_prefix = []
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ---------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
html_theme = 'default'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
|
#html_theme_path = []
|
||||||
|
|
||||||
|
# The name for this set of Sphinx documents. If None, it defaults to
|
||||||
|
# "<project> v<release> documentation".
|
||||||
|
#html_title = None
|
||||||
|
|
||||||
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
|
#html_short_title = None
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
|
# of the sidebar.
|
||||||
|
#html_logo = None
|
||||||
|
|
||||||
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
# pixels large.
|
||||||
|
#html_favicon = None
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = []
|
||||||
|
|
||||||
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
|
# using the given strftime format.
|
||||||
|
#html_last_updated_fmt = '%b %d, %Y'
|
||||||
|
|
||||||
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
|
# typographically correct entities.
|
||||||
|
#html_use_smartypants = True
|
||||||
|
|
||||||
|
# Custom sidebar templates, maps document names to template names.
|
||||||
|
#html_sidebars = {}
|
||||||
|
|
||||||
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
|
# template names.
|
||||||
|
#html_additional_pages = {}
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#html_domain_indices = True
|
||||||
|
|
||||||
|
# If false, no index is generated.
|
||||||
|
#html_use_index = True
|
||||||
|
|
||||||
|
# If true, the index is split into individual pages for each letter.
|
||||||
|
#html_split_index = False
|
||||||
|
|
||||||
|
# If true, links to the reST sources are added to the pages.
|
||||||
|
#html_show_sourcelink = True
|
||||||
|
|
||||||
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
|
#html_show_sphinx = True
|
||||||
|
|
||||||
|
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||||
|
#html_show_copyright = True
|
||||||
|
|
||||||
|
# If true, an OpenSearch description file will be output, and all pages will
|
||||||
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
|
# base URL from which the finished HTML is served.
|
||||||
|
#html_use_opensearch = ''
|
||||||
|
|
||||||
|
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
|
#html_file_suffix = None
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'restfulloggingdoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output --------------------------------------------------
|
||||||
|
|
||||||
|
# The paper size ('letter' or 'a4').
|
||||||
|
#latex_paper_size = 'letter'
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#latex_font_size = '10pt'
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||||
|
latex_documents = [
|
||||||
|
('index', 'restfullogging.tex', u'restful logging Documentation',
|
||||||
|
u'tom c', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
# the title page.
|
||||||
|
#latex_logo = None
|
||||||
|
|
||||||
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
# not chapters.
|
||||||
|
#latex_use_parts = False
|
||||||
|
|
||||||
|
# If true, show page references after internal links.
|
||||||
|
#latex_show_pagerefs = False
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#latex_show_urls = False
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#latex_preamble = ''
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#latex_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#latex_domain_indices = True
|
||||||
|
|
||||||
|
# -- Options for manual page output --------------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
('index', 'restfullogging', u'restful logging Documentation',
|
||||||
|
[u'tom c'], 1)
|
||||||
|
]
|
12
docs/index.rst
Normal file
12
docs/index.rst
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
FlyWheel Documentation
|
||||||
|
======================
|
||||||
|
|
||||||
|
This is the online documentation for FlyWheel - A REST framework for Django.
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
|
@ -12,10 +12,38 @@ import re
|
||||||
|
|
||||||
|
|
||||||
class ModelResource(Resource):
|
class ModelResource(Resource):
|
||||||
|
"""A specialized type of Resource, for RESTful resources that map directly to a Django Model.
|
||||||
|
Useful things this provides:
|
||||||
|
|
||||||
|
0. Default input validation based on ModelForms.
|
||||||
|
1. Nice serialization of returned Models and QuerySets.
|
||||||
|
2. A default set of create/read/update/delete operations."""
|
||||||
|
|
||||||
|
# The model attribute refers to the Django Model which this Resource maps to.
|
||||||
|
# (The Model's class, rather than an instance of the Model)
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
|
# By default the set of returned fields will be the set of:
|
||||||
|
#
|
||||||
|
# 0. All the fields on the model, excluding 'id'.
|
||||||
|
# 1. All the properties on the model.
|
||||||
|
# 2. The absolute_url of the model, if a get_absolute_url method exists for the model.
|
||||||
|
#
|
||||||
|
# If you wish to override this behaviour,
|
||||||
|
# you should explicitly set the fields attribute on your class.
|
||||||
fields = None
|
fields = None
|
||||||
|
|
||||||
|
# By default the form used with be a ModelForm for self.model
|
||||||
|
# If you wish to override this behaviour or provide a sub-classed ModelForm
|
||||||
|
# you should explicitly set the form attribute on your class.
|
||||||
|
form = None
|
||||||
|
|
||||||
|
# By default the set of input fields will be the same as the set of output fields
|
||||||
|
# If you wish to override this behaviour you should explicitly set the
|
||||||
|
# form_fields attribute on your class.
|
||||||
form_fields = None
|
form_fields = None
|
||||||
|
|
||||||
|
|
||||||
def get_bound_form(self, data=None, is_response=False):
|
def get_bound_form(self, data=None, is_response=False):
|
||||||
"""Return a form that may be used in validation and/or rendering an html emitter"""
|
"""Return a form that may be used in validation and/or rendering an html emitter"""
|
||||||
if self.form:
|
if self.form:
|
||||||
|
@ -25,7 +53,7 @@ class ModelResource(Resource):
|
||||||
class NewModelForm(ModelForm):
|
class NewModelForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = self.model
|
model = self.model
|
||||||
fields = self.form_fields if self.form_fields else None #self.fields
|
fields = self.form_fields if self.form_fields else None
|
||||||
|
|
||||||
if data and not is_response:
|
if data and not is_response:
|
||||||
return NewModelForm(data)
|
return NewModelForm(data)
|
||||||
|
@ -38,6 +66,26 @@ class ModelResource(Resource):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_request(self, data, form_instance):
|
||||||
|
"""Override cleanup_request to drop read-only fields from the input prior to validation.
|
||||||
|
This ensures that we don't error out with 'non-existent field' when these fields are supplied,
|
||||||
|
and allows for a pragmatic approach to resources which include read-only elements.
|
||||||
|
|
||||||
|
I would actually like to be strict and verify the value of correctness of the values in these fields,
|
||||||
|
although that gets tricky as it involves validating at the point that we get the model instance.
|
||||||
|
|
||||||
|
See here for another example of this approach:
|
||||||
|
http://fedoraproject.org/wiki/Cloud_APIs_REST_Style_Guide
|
||||||
|
https://www.redhat.com/archives/rest-practices/2010-April/thread.html#00041"""
|
||||||
|
read_only_fields = set(self.fields) - set(self.form_instance.fields)
|
||||||
|
input_fields = set(data.keys())
|
||||||
|
|
||||||
|
clean_data = {}
|
||||||
|
for key in input_fields - read_only_fields:
|
||||||
|
clean_data[key] = data[key]
|
||||||
|
|
||||||
|
return super(ModelResource, self).cleanup_request(clean_data, form_instance)
|
||||||
|
|
||||||
|
|
||||||
def cleanup_response(self, data):
|
def cleanup_response(self, data):
|
||||||
"""A munging of Piston's pre-serialization. Returns a dict"""
|
"""A munging of Piston's pre-serialization. Returns a dict"""
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import json
|
import json
|
||||||
|
from rest.status import ResourceException, Status
|
||||||
|
|
||||||
class BaseParser(object):
|
class BaseParser(object):
|
||||||
def __init__(self, resource):
|
def __init__(self, resource):
|
||||||
|
@ -10,7 +11,10 @@ class BaseParser(object):
|
||||||
|
|
||||||
class JSONParser(BaseParser):
|
class JSONParser(BaseParser):
|
||||||
def parse(self, input):
|
def parse(self, input):
|
||||||
|
try:
|
||||||
return json.loads(input)
|
return json.loads(input)
|
||||||
|
except ValueError, exc:
|
||||||
|
raise ResourceException(Status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
|
||||||
|
|
||||||
class XMLParser(BaseParser):
|
class XMLParser(BaseParser):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,39 +1,29 @@
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
||||||
|
from django.http import HttpResponse
|
||||||
from rest import emitters, parsers
|
from rest import emitters, parsers
|
||||||
|
from rest.status import Status, ResourceException
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
# TODO: Authentication
|
||||||
# TODO: Display user login in top panel: http://stackoverflow.com/questions/806835/django-redirect-to-previous-page-after-login
|
# TODO: Display user login in top panel: http://stackoverflow.com/questions/806835/django-redirect-to-previous-page-after-login
|
||||||
# TODO: Return basic object, not tuple
|
# TODO: Return basic object, not tuple of status code, content, headers
|
||||||
# TODO: Take request, not headers
|
# TODO: Take request, not headers
|
||||||
# TODO: Remove self.blah munging (Add a ResponseContext object)
|
# TODO: Standard exception classes
|
||||||
# TODO: Erroring on non-existent fields
|
|
||||||
# TODO: Standard exception classes and module for status codes
|
|
||||||
# TODO: Figure how out references and named urls need to work nicely
|
# TODO: Figure how out references and named urls need to work nicely
|
||||||
# TODO: POST on existing 404 URL, PUT on existing 404 URL
|
# TODO: POST on existing 404 URL, PUT on existing 404 URL
|
||||||
# TODO: Authentication
|
#
|
||||||
|
# NEXT: Generic content form
|
||||||
|
# NEXT: Remove self.blah munging (Add a ResponseContext object?)
|
||||||
|
# NEXT: Caching cleverness
|
||||||
|
# NEXT: Test non-existent fields on ModelResources
|
||||||
#
|
#
|
||||||
# FUTURE: Erroring on read-only fields
|
# FUTURE: Erroring on read-only fields
|
||||||
|
|
||||||
# Documentation, Release
|
# Documentation, Release
|
||||||
|
|
||||||
#
|
|
||||||
STATUS_400_BAD_REQUEST = 400
|
|
||||||
STATUS_405_METHOD_NOT_ALLOWED = 405
|
|
||||||
STATUS_406_NOT_ACCEPTABLE = 406
|
|
||||||
STATUS_415_UNSUPPORTED_MEDIA_TYPE = 415
|
|
||||||
STATUS_500_INTERNAL_SERVER_ERROR = 500
|
|
||||||
STATUS_501_NOT_IMPLEMENTED = 501
|
|
||||||
|
|
||||||
|
|
||||||
class ResourceException(Exception):
|
|
||||||
def __init__(self, status, content='', headers={}):
|
|
||||||
self.status = status
|
|
||||||
self.content = content
|
|
||||||
self.headers = headers
|
|
||||||
|
|
||||||
|
|
||||||
class Resource(object):
|
class Resource(object):
|
||||||
|
@ -110,13 +100,16 @@ class Resource(object):
|
||||||
|
|
||||||
def reverse(self, view, *args, **kwargs):
|
def reverse(self, view, *args, **kwargs):
|
||||||
"""Return a fully qualified URI for a given view or resource.
|
"""Return a fully qualified URI for a given view or resource.
|
||||||
Use the Sites framework if possible, otherwise fallback to using the current request."""
|
Add the domain using the Sites framework if possible, otherwise fallback to using the current request."""
|
||||||
return self.add_domain(reverse(view, *args, **kwargs))
|
return self.add_domain(reverse(view, *args, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
def add_domain(self, path):
|
def add_domain(self, path):
|
||||||
"""Given a path, return an fully qualified URI.
|
"""Given a path, return an fully qualified URI.
|
||||||
Use the Sites framework if possible, otherwise fallback to using the domain from the current request."""
|
Use the Sites framework if possible, otherwise fallback to using the domain from the current request."""
|
||||||
|
|
||||||
|
# Note that out-of-the-box the Sites framework uses the reserved domain 'example.com'
|
||||||
|
# See RFC 2606 - http://www.faqs.org/rfcs/rfc2606.html
|
||||||
try:
|
try:
|
||||||
site = Site.objects.get_current()
|
site = Site.objects.get_current()
|
||||||
if site.domain and site.domain != 'example.com':
|
if site.domain and site.domain != 'example.com':
|
||||||
|
@ -150,7 +143,7 @@ class Resource(object):
|
||||||
def not_implemented(self, operation):
|
def not_implemented(self, operation):
|
||||||
"""Return an HTTP 500 server error if an operation is called which has been allowed by
|
"""Return an HTTP 500 server error if an operation is called which has been allowed by
|
||||||
allowed_operations, but which has not been implemented."""
|
allowed_operations, but which has not been implemented."""
|
||||||
raise ResourceException(STATUS_500_INTERNAL_SERVER_ERROR,
|
raise ResourceException(Status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
{'detail': '%s operation on this resource has not been implemented' % (operation, )})
|
{'detail': '%s operation on this resource has not been implemented' % (operation, )})
|
||||||
|
|
||||||
|
|
||||||
|
@ -172,18 +165,18 @@ class Resource(object):
|
||||||
# if anon_user and not anon_allowed_operations raise PermissionDenied
|
# if anon_user and not anon_allowed_operations raise PermissionDenied
|
||||||
# return
|
# return
|
||||||
|
|
||||||
|
|
||||||
def check_method_allowed(self, method):
|
def check_method_allowed(self, method):
|
||||||
"""Ensure the request method is acceptable for this resource."""
|
"""Ensure the request method is acceptable for this resource."""
|
||||||
if not method in self.CALLMAP.keys():
|
if not method in self.CALLMAP.keys():
|
||||||
raise ResourceException(STATUS_501_NOT_IMPLEMENTED,
|
raise ResourceException(Status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
{'detail': 'Unknown or unsupported method \'%s\'' % method})
|
{'detail': 'Unknown or unsupported method \'%s\'' % method})
|
||||||
|
|
||||||
if not self.CALLMAP[method] in self.allowed_operations:
|
if not self.CALLMAP[method] in self.allowed_operations:
|
||||||
raise ResourceException(STATUS_405_METHOD_NOT_ALLOWED,
|
raise ResourceException(Status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||||
{'detail': 'Method \'%s\' not allowed on this resource.' % method})
|
{'detail': 'Method \'%s\' not allowed on this resource.' % method})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_bound_form(self, data=None, is_response=False):
|
def get_bound_form(self, data=None, is_response=False):
|
||||||
"""Optionally return a Django Form instance, which may be used for validation
|
"""Optionally return a Django Form instance, which may be used for validation
|
||||||
and/or rendered by an HTML/XHTML emitter.
|
and/or rendered by an HTML/XHTML emitter.
|
||||||
|
@ -208,15 +201,30 @@ class Resource(object):
|
||||||
if form_instance is None:
|
if form_instance is None:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
if not form_instance.is_valid():
|
# Default form validation does not check for additional invalid fields
|
||||||
if not form_instance.errors:
|
non_existent_fields = []
|
||||||
details = 'No content was supplied'
|
for key in set(data.keys()) - set(form_instance.fields.keys()):
|
||||||
else:
|
non_existent_fields.append(key)
|
||||||
details = dict((key, map(unicode, val)) for (key, val) in form_instance.errors.iteritems())
|
|
||||||
if form_instance.non_field_errors():
|
|
||||||
details['_extra'] = self.form.non_field_errors()
|
|
||||||
|
|
||||||
raise ResourceException(STATUS_400_BAD_REQUEST, {'detail': details})
|
if not form_instance.is_valid() or non_existent_fields:
|
||||||
|
if not form_instance.errors and not non_existent_fields:
|
||||||
|
# If no data was supplied the errors property will be None
|
||||||
|
details = 'No content was supplied'
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Add standard field errors
|
||||||
|
details = dict((key, map(unicode, val)) for (key, val) in form_instance.errors.iteritems())
|
||||||
|
|
||||||
|
# Add any non-field errors
|
||||||
|
if form_instance.non_field_errors():
|
||||||
|
details['errors'] = self.form.non_field_errors()
|
||||||
|
|
||||||
|
# Add any non-existent field errors
|
||||||
|
for key in non_existent_fields:
|
||||||
|
details[key] = ['This field does not exist']
|
||||||
|
|
||||||
|
# Bail. Note that we will still serialize this response with the appropriate content type
|
||||||
|
raise ResourceException(Status.HTTP_400_BAD_REQUEST, {'detail': details})
|
||||||
|
|
||||||
return form_instance.cleaned_data
|
return form_instance.cleaned_data
|
||||||
|
|
||||||
|
@ -241,7 +249,7 @@ class Resource(object):
|
||||||
try:
|
try:
|
||||||
return self.parsers[content_type]
|
return self.parsers[content_type]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ResourceException(STATUS_415_UNSUPPORTED_MEDIA_TYPE,
|
raise ResourceException(Status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
||||||
{'detail': 'Unsupported media type \'%s\'' % content_type})
|
{'detail': 'Unsupported media type \'%s\'' % content_type})
|
||||||
|
|
||||||
|
|
||||||
|
@ -295,14 +303,13 @@ class Resource(object):
|
||||||
(accept_mimetype == mimetype)):
|
(accept_mimetype == mimetype)):
|
||||||
return (mimetype, emitter)
|
return (mimetype, emitter)
|
||||||
|
|
||||||
raise ResourceException(STATUS_406_NOT_ACCEPTABLE,
|
raise ResourceException(Status.HTTP_406_NOT_ACCEPTABLE,
|
||||||
{'detail': 'Could not statisfy the client\'s accepted content type',
|
{'detail': 'Could not statisfy the client\'s accepted content type',
|
||||||
'accepted_types': [item[0] for item in self.emitters]})
|
'accepted_types': [item[0] for item in self.emitters]})
|
||||||
|
|
||||||
|
|
||||||
def _handle_request(self, request, *args, **kwargs):
|
def _handle_request(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Broadly this consists of the following procedure:
|
Broadly this consists of the following procedure:
|
||||||
|
|
||||||
0. ensure the operation is permitted
|
0. ensure the operation is permitted
|
||||||
|
@ -347,9 +354,14 @@ class Resource(object):
|
||||||
|
|
||||||
|
|
||||||
except ResourceException, exc:
|
except ResourceException, exc:
|
||||||
|
# On exceptions we still serialize the response appropriately
|
||||||
(self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers)
|
(self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers)
|
||||||
|
|
||||||
|
# Fall back to the default emitter if we failed to perform content negotiation
|
||||||
if emitter is None:
|
if emitter is None:
|
||||||
mimetype, emitter = self.emitters[0]
|
mimetype, emitter = self.emitters[0]
|
||||||
|
|
||||||
|
# Provide an empty bound form if we do not have an existing form and if one is required
|
||||||
if self.form_instance is None and emitter.uses_forms:
|
if self.form_instance is None and emitter.uses_forms:
|
||||||
self.form_instance = self.get_bound_form()
|
self.form_instance = self.get_bound_form()
|
||||||
|
|
||||||
|
|
50
src/rest/status.py
Normal file
50
src/rest/status.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
|
||||||
|
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
|
|
@ -4,7 +4,7 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from testapp import views
|
from testapp import views
|
||||||
import json
|
#import json
|
||||||
#from rest.utils import xml2dict, dict2xml
|
#from rest.utils import xml2dict, dict2xml
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user