Merge remote branch 'tomchristie/master'

This commit is contained in:
Alen Mujezinovic 2012-02-09 11:13:42 +00:00
commit add5f32e8a
21 changed files with 1445 additions and 169 deletions

View File

@ -28,6 +28,8 @@ Sebastian Żurek <sebzur>
Benoit C <dzen>
Chris Pickett <bunchesofdonald>
Ben Timby <btimby>
Michele Lazzeri <michelelazzeri-nextage>
Camille Harang <mammique>
THANKS TO:

View File

@ -1,9 +1,44 @@
Release Notes
=============
development
-----------
* Saner template variable autoescaping.
* Use `staticfiles` for css files.
- Easier to override. Won't conflict with customised admin styles (eg grappelli)
* Drop implied 'pk' filter if last arg in urlconf is unnamed.
- Too magical. Explict is better than implicit.
* Tider setup.py
* Bugfixes:
- Bug with PerUserThrottling when user contains unicode chars.
0.3.2
-----
* Bugfixes:
* Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115)
* serialize_model method in serializer.py may cause wrong value (#73)
* Fix Error when clicking OPTIONS button (#146)
* And many other fixes
* Remove short status codes
- Zen of Python: "There should be one-- and preferably only one --obvious way to do it."
* get_name, get_description become methods on the view - makes them overridable.
* Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering
0.3.1
-----
* [not documented]
0.3.0
-----
* JSONP Support
* Bugfixes, including support for latest markdown release
0.2.4
-----
* Fix broken IsAdminUser permission.
* OPTIONS support.
@ -11,20 +46,24 @@
* Drop mentions of Blog, BitBucket.
0.2.3
-----
* Fix some throttling bugs.
* ``X-Throttle`` header on throttling.
* Support for nesting resources on related models.
0.2.2
-----
* Throttling support complete.
0.2.1
-----
* Couple of simple bugfixes over 0.2.0
0.2.0
-----
* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.
The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
@ -49,9 +88,11 @@
You can reuse these mixin classes individually without using the ``View`` class.
0.1.1
-----
* Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1.
0.1.0
-----
* Initial release.

View File

@ -1,5 +1,5 @@
recursive-include djangorestframework/static *.ico *.txt
recursive-include djangorestframework/static *.ico *.txt *.css
recursive-include djangorestframework/templates *.txt *.html
recursive-include examples .keep *.py *.txt
recursive-include docs *.py *.rst *.html *.txt
include AUTHORS LICENSE requirements.txt tox.ini
include AUTHORS LICENSE CHANGELOG.rst requirements.txt tox.ini

View File

@ -1,7 +1,12 @@
Django REST framework
=====================
Django REST framework makes it easy to build well-connected, self-describing RESTful Web APIs.
**Django REST framework makes it easy to build well-connected, self-describing RESTful Web APIs.**
**Author:** Tom Christie. `Follow me on Twitter <https://twitter.com/_tomchristie>`_.
Overview
========
Features:

View File

@ -1,3 +1,3 @@
__version__ = '0.3.2-dev'
__version__ = '0.3.3-dev'
VERSION = __version__ # synonym

View File

@ -6,7 +6,6 @@ classes that can be added to a `View`.
from django.contrib.auth.models import AnonymousUser
from django.core.paginator import Paginator
from django.db.models.fields.related import ForeignKey
from django.db.models.query import Q
from django.http import HttpResponse
from urlobject import URLObject
@ -486,44 +485,25 @@ class ModelMixin(object):
queryset = None
def build_query(self, *args, **kwargs):
""" Returns django.db.models.Q object to be used for the objects retrival.
Arguments:
- args: unnamed URL arguments
- kwargs: named URL arguments
If a URL passes any arguments to the view being the QueryMixin subclass
build_query manages the arguments and provides the Q object that will be
used for the objects retrival with filter/get queryset methods.
Technically, neither args nor kwargs have to be provided, however the default
behaviour is to map all kwargs as the query constructors so that if this
method is not overriden only kwargs keys being model fields are valid.
If positional args are provided, the last one argument is understood
as the primary key. However this usage should be considered
deperecated, and will be removed in a future version.
def get_query_kwargs(self, *args, **kwargs):
"""
Return a dict of kwargs that will be used to build the
model instance retrieval or to filter querysets.
"""
tmp = dict(kwargs)
kwargs = dict(kwargs)
# If the URLconf includes a .(?P<format>\w+) pattern to match against
# a .json, .xml suffix, then drop the 'format' kwarg before
# constructing the query.
if BaseRenderer._FORMAT_QUERY_PARAM in tmp:
del tmp[BaseRenderer._FORMAT_QUERY_PARAM]
if BaseRenderer._FORMAT_QUERY_PARAM in kwargs:
del kwargs[BaseRenderer._FORMAT_QUERY_PARAM]
if args:
# If we have any no kwargs then assume the last arg represents the
# primrary key. Otherwise assume the kwargs uniquely identify the
# model.
tmp.update({'pk': args[-1]})
return Q(**tmp)
return kwargs
def get_instance_data(self, model, content, **kwargs):
"""
Returns the dict with the data for model instance creation/update query.
Returns the dict with the data for model instance creation/update.
Arguments:
- model: model class (django.db.models.Model subclass) to work with
@ -548,12 +528,11 @@ class ModelMixin(object):
return all_kw_args
def get_object(self, *args, **kwargs):
def get_instance(self, **kwargs):
"""
Get the instance object for read/update/delete requests.
Get a model instance for read/update/delete requests.
"""
model = self.resource.model
return model.objects.get(self.build_query(*args, **kwargs))
return self.get_queryset().get(**kwargs)
def get_queryset(self):
"""
@ -575,21 +554,15 @@ class ReadModelMixin(ModelMixin):
"""
def get(self, request, *args, **kwargs):
model = self.resource.model
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
try:
self.model_instance = self.get_object(*args, **kwargs)
self.model_instance = self.get_instance(**query_kwargs)
except model.DoesNotExist:
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
return self.model_instance
def build_query(self, *args, **kwargs):
# Build query is overriden to filter the kwargs priori
# to use them as build_query argument
filtered_keywords = kwargs.copy()
return super(ReadModelMixin, self).build_query(*args, **filtered_keywords)
class CreateModelMixin(ModelMixin):
"""
@ -637,11 +610,12 @@ class UpdateModelMixin(ModelMixin):
"""
def put(self, request, *args, **kwargs):
model = self.resource.model
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
# TODO: update on the url of a non-existing resource url doesn't work
# correctly at the moment - will end up with a new url
try:
self.model_instance = self.get_object(*args, **kwargs)
self.model_instance = self.get_instance(**query_kwargs)
for (key, val) in self.CONTENT.items():
setattr(self.model_instance, key, val)
@ -657,9 +631,10 @@ class DeleteModelMixin(ModelMixin):
"""
def delete(self, request, *args, **kwargs):
model = self.resource.model
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
try:
instance = self.get_object(*args, **kwargs)
instance = self.get_instance(**query_kwargs)
except model.DoesNotExist:
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
@ -675,8 +650,9 @@ class ListModelMixin(ModelMixin):
def get(self, request, *args, **kwargs):
queryset = self.get_queryset()
ordering = self.get_ordering()
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
queryset = queryset.filter(self.build_query(**kwargs))
queryset = queryset.filter(**query_kwargs)
if ordering:
queryset = queryset.order_by(*ordering)
@ -710,7 +686,7 @@ class PaginatorMixin(object):
Constructs a url used for getting the next/previous urls
"""
url = URLObject.parse(self.request.get_full_path())
url = url.add_query_param('page', page_number)
url = url.set_query_param('page', page_number)
limit = self.get_limit()
if limit != self.limit:

View File

@ -188,7 +188,7 @@ class PerUserThrottling(BaseThrottle):
def get_cache_key(self):
if self.auth.is_authenticated():
ident = str(self.auth)
ident = self.auth.id
else:
ident = self.view.request.META.get('REMOTE_ADDR', None)
return 'throttle_user_%s' % ident

View File

@ -34,7 +34,7 @@ class Response(object):
return STATUS_CODE_TEXT.get(self.status, '')
class ErrorResponse(BaseException):
class ErrorResponse(Exception):
"""
An exception representing an Response that should be returned immediately.
Any content should be serialized as-is, without being filtered.

View File

@ -97,6 +97,14 @@ INSTALLED_APPS = (
'djangorestframework',
)
STATIC_URL = '/static/'
import django
if django.VERSION < (1, 3):
INSTALLED_APPS += ('staticfiles',)
# OAuth support is optional, so we only test oauth if it's installed.
try:
import oauth_provider

File diff suppressed because it is too large Load Diff

View File

@ -1,54 +1,44 @@
{% load static %}
<html>
<head>
{% if ADMIN_MEDIA_PREFIX %}
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/base.css'/>
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/forms.css'/>
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/login.css' />
{% else %}
<link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/base.css'/>
<link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/forms.css'/>
<link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/login.css' />
{% endif %}
<style>
.form-row {border-bottom: 0.25em !important}</style>
</head>
<body class="login">
<div id="container">
<div id="header">
<div id="branding">
<h1 id="site-name">Django REST framework</h1>
<head>
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}css/djangorestframework.css'/>
</head>
<body class="login">
<div id="container">
<div id="header">
<div id="branding">
<h1 id="site-name">Django REST framework</h1>
</div>
</div>
<div id="content" class="colM">
<div id="content-main">
<form method="post" action="{% url djangorestframework.utils.staticviews.api_login %}" id="login-form">
{% csrf_token %}
<div class="form-row">
<label for="id_username">Username:</label> {{ form.username }}
</div>
<div class="form-row">
<label for="id_password">Password:</label> {{ form.password }}
<input type="hidden" name="next" value="{{ next }}" />
</div>
<div class="form-row">
<label>&nbsp;</label><input type="submit" value="Log in">
</div>
</form>
<script type="text/javascript">
document.getElementById('id_username').focus()
</script>
</div>
<br class="clear">
</div>
<div id="footer"></div>
</div>
</div>
<div id="content" class="colM">
<div id="content-main">
<form method="post" action="{% url djangorestframework.utils.staticviews.api_login %}" id="login-form">
{% csrf_token %}
<div class="form-row">
<label for="id_username">Username:</label> {{ form.username }}
</div>
<div class="form-row">
<label for="id_password">Password:</label> {{ form.password }}
<input type="hidden" name="next" value="{{ next }}" />
</div>
<div class="form-row">
<label>&nbsp;</label><input type="submit" value="Log in">
</div>
</form>
<script type="text/javascript">
document.getElementById('id_username').focus()
</script>
</div>
<br class="clear">
</div>
<div id="footer"></div>
</div>
</body>
</body>
</html>

View File

@ -1,25 +1,14 @@
{% load urlize_quoted_links %}{% load add_query_param %}<?xml version="1.0" encoding="UTF-8"?>
<?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">
{% load urlize_quoted_links %}
{% load add_query_param %}
{% load static %}
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<style>
/* Override some of the Django admin styling */
#site-name a {color: #F4F379 !important;}
.errorlist {display: inline !important}
.errorlist li {display: inline !important; background: white !important; color: black !important; border: 0 !important;}
/* Custom styles */
.version{font-size:8px;}
</style>
{% if ADMIN_MEDIA_PREFIX %}
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/base.css'/>
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/forms.css'/>
{% else %}
<link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/base.css'/>
<link rel="stylesheet" type="text/css" href='{{STATIC_URL}}admin/css/forms.css'/>
{% endif %}
<title>Django REST framework - {{ name }}</title>
</head>
<head>
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}css/djangorestframework.css'/>
<title>Django REST framework - {{ name }}</title>
</head>
<body>
<div id="container">
@ -34,7 +23,7 @@
<div class="breadcrumbs">
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
<a href="{{breadcrumb_url}}">{{breadcrumb_name}}</a> {% if not forloop.last %}&rsaquo;{% endif %}
<a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a> {% if not forloop.last %}&rsaquo;{% endif %}
{% endfor %}
</div>
@ -50,7 +39,7 @@
<div class='content-main'>
<h1>{{ name }}</h1>
<p>{% autoescape off %}{{ description }}{% endautoescape %}</p>
<p>{{ description }}</p>
<div class='module'>
<pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %}
{% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}

View File

@ -1,8 +1,8 @@
{{ name }}
{% autoescape off %}{{ name }}
{{ description }}
{% autoescape off %}HTTP/1.0 {{ response.status }} {{ response.status_text }}
HTTP/1.0 {{ response.status }} {{ response.status_text }}
{% for key, val in response.headers.items %}{{ key }}: {{ val }}
{% endfor %}
{{ content }}{% endautoescape %}

View File

@ -30,7 +30,7 @@ class TestModelRead(TestModelsTestCase):
mixin = ReadModelMixin()
mixin.resource = GroupResource
response = mixin.get(request, group.id)
response = mixin.get(request, id=group.id)
self.assertEquals(group.name, response.name)
def test_read_404(self):
@ -41,7 +41,7 @@ class TestModelRead(TestModelsTestCase):
mixin = ReadModelMixin()
mixin.resource = GroupResource
self.assertRaises(ErrorResponse, mixin.get, request, 12345)
self.assertRaises(ErrorResponse, mixin.get, request, id=12345)
class TestModelCreation(TestModelsTestCase):
@ -280,3 +280,12 @@ class TestPagination(TestCase):
self.assertTrue('foo=bar' in content['next'])
self.assertTrue('another=something' in content['next'])
self.assertTrue('page=2' in content['next'])
def test_duplicate_parameters_are_not_created(self):
""" Regression: ensure duplicate "page" parameters are not added to
paginated URLs. So page 1 should contain ?page=2, not ?page=1&page=2 """
request = self.req.get('/paginator/?page=1')
response = MockPaginatorView.as_view()(request)
content = json.loads(response.content)
self.assertTrue('page=2' in content['next'])
self.assertFalse('page=1' in content['next'])

View File

@ -36,6 +36,7 @@ def _remove_trailing_string(content, trailing):
return content[:-len(trailing)]
return content
def _remove_leading_indent(content):
"""
Remove leading indent from a block of text.
@ -50,6 +51,7 @@ def _remove_leading_indent(content):
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
return content
def _camelcase_to_spaces(content):
"""
Translate 'CamelCaseNames' to 'Camel Case Names'.
@ -161,9 +163,10 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
def markup_description(self, description):
if apply_markdown:
return apply_markdown(description)
description = apply_markdown(description)
else:
return mark_safe(escape(description).replace('\n', '<br />'))
description = escape(description).replace('\n', '<br />')
return mark_safe(description)
def http_method_not_allowed(self, request, *args, **kwargs):
"""

View File

@ -105,6 +105,8 @@ The following example exposes your `MyModel` model through an api. It will provi
contents
.. include:: ../CHANGELOG.rst
Indices and tables
------------------

View File

@ -1,3 +1,3 @@
Pygments==1.4
Markdown==2.0.3
djangorestframework
git+git://github.com/tomchristie/django-rest-framework.git

View File

@ -1,4 +1,5 @@
# Settings for djangorestframework examples project
import django
import os
DEBUG = True
@ -53,16 +54,10 @@ MEDIA_ROOT = os.path.join(os.getenv('EPIO_DATA_DIRECTORY', '.'), 'media')
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
# NOTE: None of the djangorestframework examples serve media content via MEDIA_URL.
MEDIA_URL = ''
MEDIA_URL = '/uploads/'
STATIC_URL = '/static/'
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
# NOTE: djangorestframework does not require the admin app to be installed,
# but it does require the admin media be served. Django's test server will do
# this for you automatically, but in production you'll want to make sure you
# serve the admin media from somewhere.
ADMIN_MEDIA_PREFIX = '/static/admin'
# Make this unique, and don't share it with anybody.
SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu'
@ -90,18 +85,17 @@ TEMPLATE_DIRS = (
# Don't forget to use absolute paths, not relative paths.
)
# for loading initial data
##SERIALIZATION_MODULES = {
# 'yml': "django.core.serializers.pyyaml"
#}
if django.VERSION < (1, 3):
staticfiles = 'staticfiles'
else:
staticfiles = 'django.contrib.staticfiles'
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
staticfiles,
'django.contrib.messages',
'djangorestframework',

View File

@ -1,6 +1,10 @@
from django.conf.urls.defaults import patterns, include, url
from django.conf import settings
from django.conf.urls.defaults import patterns, include
from sandbox.views import Sandbox
try:
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
except ImportError: # Django <= 1.2
from staticfiles.urls import staticfiles_urlpatterns
urlpatterns = patterns('',
(r'^$', Sandbox.as_view()),
@ -15,3 +19,4 @@ urlpatterns = patterns('',
(r'^', include('djangorestframework.urls')),
)
urlpatterns += staticfiles_urlpatterns()

81
setup.py Normal file → Executable file
View File

@ -1,33 +1,70 @@
#!/usr/bin/env/python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import setup
import re
import os
import sys
import os, re
path = os.path.join(os.path.dirname(__file__), 'djangorestframework', '__init__.py')
init_py = open(path).read()
VERSION = re.match("__version__ = '([^']+)'", init_py).group(1)
def get_version(package):
"""
Return package version as listed in `__version__` in `init.py`.
"""
init_py = open(os.path.join(package, '__init__.py')).read()
return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
def get_packages(package):
"""
Return root package and all sub-packages.
"""
return [dirpath
for dirpath, dirnames, filenames in os.walk(package)
if os.path.exists(os.path.join(dirpath, '__init__.py'))]
def get_package_data(package):
"""
Return all files under the root package, that are not in a
package themselves.
"""
walk = [(dirpath.replace(package + os.sep, '', 1), filenames)
for dirpath, dirnames, filenames in os.walk(package)
if not os.path.exists(os.path.join(dirpath, '__init__.py'))]
filepaths = []
for base, filenames in walk:
filepaths.extend([os.path.join(base, filename)
for filename in filenames])
return {package: filepaths}
version = get_version('djangorestframework')
if sys.argv[-1] == 'publish':
os.system("python setup.py sdist upload")
print "You probably want to also tag the version now:"
print " git tag -a %s -m 'version %s'" % (version, version)
print " git push --tags"
sys.exit()
setup(
name = 'djangorestframework',
version = VERSION,
url = 'http://django-rest-framework.org',
download_url = 'http://pypi.python.org/pypi/djangorestframework/',
license = 'BSD',
description = 'A lightweight REST framework for Django.',
author = 'Tom Christie',
author_email = 'tom@tomchristie.com',
packages = ['djangorestframework',
'djangorestframework.templatetags',
'djangorestframework.tests',
'djangorestframework.runtests',
'djangorestframework.utils'],
package_dir={'djangorestframework': 'djangorestframework'},
package_data = {'djangorestframework': ['templates/*', 'static/*']},
test_suite = 'djangorestframework.runtests.runcoverage.main',
name='djangorestframework',
version=version,
url='http://django-rest-framework.org',
download_url='http://pypi.python.org/pypi/djangorestframework/',
license='BSD',
description='A lightweight REST framework for Django.',
author='Tom Christie',
author_email='tom@tomchristie.com',
packages=get_packages('djangorestframework'),
package_data=get_package_data('djangorestframework'),
test_suite='djangorestframework.runtests.runcoverage.main',
install_requires=['URLObject>=0.6.0'],
classifiers = [
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Framework :: Django',

View File

@ -32,6 +32,7 @@ commands=
basepython=python2.5
deps=
django==1.2.4
django-staticfiles>=1.1.2
coverage==3.4
URLObject>=0.6.0
unittest-xml-reporting==1.2
@ -43,6 +44,7 @@ deps=
basepython=python2.6
deps=
django==1.2.4
django-staticfiles>=1.1.2
coverage==3.4
URLObject>=0.6.0
unittest-xml-reporting==1.2
@ -54,6 +56,7 @@ deps=
basepython=python2.7
deps=
django==1.2.4
django-staticfiles>=1.1.2
coverage==3.4
URLObject>=0.6.0
unittest-xml-reporting==1.2
@ -135,6 +138,7 @@ commands=
python examples/runtests.py
deps=
django==1.2.4
django-staticfiles>=1.1.2
coverage==3.4
URLObject>=0.6.0
wsgiref==0.1.2
@ -150,6 +154,7 @@ commands=
python examples/runtests.py
deps=
django==1.2.4
django-staticfiles>=1.1.2
coverage==3.4
URLObject>=0.6.0
wsgiref==0.1.2
@ -165,6 +170,7 @@ commands=
python examples/runtests.py
deps=
django==1.2.4
django-staticfiles>=1.1.2
coverage==3.4
URLObject>=0.6.0
wsgiref==0.1.2