Commit 279fa0d3 by Sébastien Piquemal

merge

parents 152c385f b2fcfffb
...@@ -27,6 +27,8 @@ Natim <natim> ...@@ -27,6 +27,8 @@ Natim <natim>
Sebastian Żurek <sebzur> Sebastian Żurek <sebzur>
Benoit C <dzen> Benoit C <dzen>
Chris Pickett <bunchesofdonald> Chris Pickett <bunchesofdonald>
Ben Timby <btimby>
Michele Lazzeri <michelelazzeri-nextage>
THANKS TO: THANKS TO:
......
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.
* 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 0.3.0
-----
* JSONP Support * JSONP Support
* Bugfixes, including support for latest markdown release * Bugfixes, including support for latest markdown release
0.2.4 0.2.4
-----
* Fix broken IsAdminUser permission. * Fix broken IsAdminUser permission.
* OPTIONS support. * OPTIONS support.
...@@ -11,20 +45,24 @@ ...@@ -11,20 +45,24 @@
* Drop mentions of Blog, BitBucket. * Drop mentions of Blog, BitBucket.
0.2.3 0.2.3
-----
* Fix some throttling bugs. * Fix some throttling bugs.
* ``X-Throttle`` header on throttling. * ``X-Throttle`` header on throttling.
* Support for nesting resources on related models. * Support for nesting resources on related models.
0.2.2 0.2.2
-----
* Throttling support complete. * Throttling support complete.
0.2.1 0.2.1
-----
* Couple of simple bugfixes over 0.2.0 * Couple of simple bugfixes over 0.2.0
0.2.0 0.2.0
-----
* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear. * 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. The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
...@@ -49,9 +87,11 @@ ...@@ -49,9 +87,11 @@
You can reuse these mixin classes individually without using the ``View`` class. You can reuse these mixin classes individually without using the ``View`` class.
0.1.1 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. * 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 0.1.0
-----
* Initial release. * Initial release.
...@@ -16,7 +16,7 @@ Full documentation for the project is available at http://django-rest-framework. ...@@ -16,7 +16,7 @@ Full documentation for the project is available at http://django-rest-framework.
Issue tracking is on `GitHub <https://github.com/tomchristie/django-rest-framework/issues>`_. Issue tracking is on `GitHub <https://github.com/tomchristie/django-rest-framework/issues>`_.
General questions should be taken to the `discussion group <http://groups.google.com/group/django-rest-framework>`_. General questions should be taken to the `discussion group <http://groups.google.com/group/django-rest-framework>`_.
We also have a `Jenkins service <http://jenkins.tibold.nl/job/djangorestframework/>`_ which runs our test suite. We also have a `Jenkins service <http://jenkins.tibold.nl/job/djangorestframework1/>`_ which runs our test suite.
Requirements: Requirements:
......
__version__ = '0.3.2-dev' __version__ = '0.3.3-dev'
VERSION = __version__ # synonym VERSION = __version__ # synonym
...@@ -87,25 +87,12 @@ class UserLoggedInAuthentication(BaseAuthentication): ...@@ -87,25 +87,12 @@ class UserLoggedInAuthentication(BaseAuthentication):
Returns a :obj:`User` if the request session currently has a logged in user. Returns a :obj:`User` if the request session currently has a logged in user.
Otherwise returns :const:`None`. Otherwise returns :const:`None`.
""" """
# TODO: Might be cleaner to switch this back to using request.POST, request.DATA # Make sure our generic parsing runs first
# and let FormParser/MultiPartParser deal with the consequences.
if getattr(request, 'user', None) and request.user.is_active: if getattr(request, 'user', None) and request.user.is_active:
# Enforce CSRF validation for session based authentication. # Enforce CSRF validation for session based authentication.
# Temporarily replace request.POST with .DATA, to use our generic parsing.
# If DATA is not dict-like, use an empty dict.
if request.method.upper() == 'POST':
if hasattr(request.DATA, 'get'):
request._post = request.DATA
else:
request._post = {}
resp = CsrfViewMiddleware().process_view(request, None, (), {}) resp = CsrfViewMiddleware().process_view(request, None, (), {})
# Replace request.POST
if request.method.upper() == 'POST':
del(request._post)
if resp is None: # csrf passed if resp is None: # csrf passed
return request.user return request.user
return None return None
......
...@@ -384,11 +384,6 @@ class ModelMixin(object): ...@@ -384,11 +384,6 @@ class ModelMixin(object):
if BaseRenderer._FORMAT_QUERY_PARAM in tmp: if BaseRenderer._FORMAT_QUERY_PARAM in tmp:
del tmp[BaseRenderer._FORMAT_QUERY_PARAM] del tmp[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 Q(**tmp)
def get_instance_data(self, model, content, **kwargs): def get_instance_data(self, model, content, **kwargs):
......
...@@ -188,7 +188,7 @@ class PerUserThrottling(BaseThrottle): ...@@ -188,7 +188,7 @@ class PerUserThrottling(BaseThrottle):
def get_cache_key(self): def get_cache_key(self):
if self.auth.is_authenticated(): if self.auth.is_authenticated():
ident = str(self.auth) ident = self.auth.id
else: else:
ident = self.view.request.META.get('REMOTE_ADDR', None) ident = self.view.request.META.get('REMOTE_ADDR', None)
return 'throttle_user_%s' % ident return 'throttle_user_%s' % ident
......
...@@ -12,10 +12,9 @@ from django.template import RequestContext, loader ...@@ -12,10 +12,9 @@ from django.template import RequestContext, loader
from django.utils import simplejson as json from django.utils import simplejson as json
from djangorestframework.compat import apply_markdown, yaml from djangorestframework.compat import yaml
from djangorestframework.utils import dict2xml, url_resolves from djangorestframework.utils import dict2xml, url_resolves
from djangorestframework.utils.breadcrumbs import get_breadcrumbs from djangorestframework.utils.breadcrumbs import get_breadcrumbs
from djangorestframework.utils.description import get_name, get_description
from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
from djangorestframework import VERSION from djangorestframework import VERSION
...@@ -296,6 +295,20 @@ class DocumentingTemplateRenderer(BaseRenderer): ...@@ -296,6 +295,20 @@ class DocumentingTemplateRenderer(BaseRenderer):
# Okey doke, let's do it # Okey doke, let's do it
return GenericContentForm(view.request) return GenericContentForm(view.request)
def get_name(self):
try:
return self.view.get_name()
except AttributeError:
return self.view.__doc__
def get_description(self, html=None):
if html is None:
html = bool('html' in self.format)
try:
return self.view.get_description(html)
except AttributeError:
return self.view.__doc__
def render(self, obj=None, media_type=None): def render(self, obj=None, media_type=None):
""" """
Renders *obj* using the :attr:`template` set on the class. Renders *obj* using the :attr:`template` set on the class.
...@@ -316,15 +329,8 @@ class DocumentingTemplateRenderer(BaseRenderer): ...@@ -316,15 +329,8 @@ class DocumentingTemplateRenderer(BaseRenderer):
login_url = None login_url = None
logout_url = None logout_url = None
name = get_name(self.view) name = self.get_name()
description = get_description(self.view) description = self.get_description()
markeddown = None
if apply_markdown:
try:
markeddown = apply_markdown(description)
except AttributeError:
markeddown = None
breadcrumb_list = get_breadcrumbs(self.view.request.path) breadcrumb_list = get_breadcrumbs(self.view.request.path)
...@@ -337,7 +343,6 @@ class DocumentingTemplateRenderer(BaseRenderer): ...@@ -337,7 +343,6 @@ class DocumentingTemplateRenderer(BaseRenderer):
'description': description, 'description': description,
'name': name, 'name': name,
'version': VERSION, 'version': VERSION,
'markeddown': markeddown,
'breadcrumblist': breadcrumb_list, 'breadcrumblist': breadcrumb_list,
'available_formats': self.view._rendered_formats, 'available_formats': self.view._rendered_formats,
'put_form': put_form_instance, 'put_form': put_form_instance,
......
...@@ -97,6 +97,12 @@ INSTALLED_APPS = ( ...@@ -97,6 +97,12 @@ INSTALLED_APPS = (
'djangorestframework', 'djangorestframework',
) )
import django
if django.VERSION < (1, 3):
INSTALLED_APPS += ('staticfiles',)
# OAuth support is optional, so we only test oauth if it's installed. # OAuth support is optional, so we only test oauth if it's installed.
try: try:
import oauth_provider import oauth_provider
......
/********************** admin 'base.css' ************************/
body {
margin: 0;
padding: 0;
font-size: 12px;
font-family: "Lucida Grande","DejaVu Sans","Bitstream Vera Sans",Verdana,Arial,sans-serif;
color: #333;
background: #fff;
}
/* LINKS */
a:link, a:visited {
color: #5b80b2;
text-decoration: none;
}
a:hover {
color: #036;
}
a img {
border: none;
}
a.section:link, a.section:visited {
color: white;
text-decoration: none;
}
/* GLOBAL DEFAULTS */
p, ol, ul, dl {
margin: .2em 0 .8em 0;
}
p {
padding: 0;
line-height: 140%;
}
h1,h2,h3,h4,h5 {
font-weight: bold;
}
h1 {
font-size: 18px;
color: #666;
padding: 0 6px 0 0;
margin: 0 0 .2em 0;
}
h2 {
font-size: 16px;
margin: 1em 0 .5em 0;
}
h2.subhead {
font-weight: normal;
margin-top: 0;
}
h3 {
font-size: 14px;
margin: .8em 0 .3em 0;
color: #666;
font-weight: bold;
}
h4 {
font-size: 12px;
margin: 1em 0 .8em 0;
padding-bottom: 3px;
}
h5 {
font-size: 10px;
margin: 1.5em 0 .5em 0;
color: #666;
text-transform: uppercase;
letter-spacing: 1px;
}
ul li {
list-style-type: square;
padding: 1px 0;
}
ul.plainlist {
margin-left: 0 !important;
}
ul.plainlist li {
list-style-type: none;
}
li ul {
margin-bottom: 0;
}
li, dt, dd {
font-size: 11px;
line-height: 14px;
}
dt {
font-weight: bold;
margin-top: 4px;
}
dd {
margin-left: 0;
}
form {
margin: 0;
padding: 0;
}
fieldset {
margin: 0;
padding: 0;
}
blockquote {
font-size: 11px;
color: #777;
margin-left: 2px;
padding-left: 10px;
border-left: 5px solid #ddd;
}
code, pre {
font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace;
background: inherit;
color: #666;
font-size: 11px;
}
pre.literal-block {
margin: 10px;
background: #eee;
padding: 6px 8px;
}
code strong {
color: #930;
}
hr {
clear: both;
color: #eee;
background-color: #eee;
height: 1px;
border: none;
margin: 0;
padding: 0;
font-size: 1px;
line-height: 1px;
}
/* TEXT STYLES & MODIFIERS */
.small {
font-size: 11px;
}
.tiny {
font-size: 10px;
}
p.tiny {
margin-top: -2px;
}
.mini {
font-size: 9px;
}
p.mini {
margin-top: -3px;
}
.help, p.help {
font-size: 10px !important;
color: #999;
}
p img, h1 img, h2 img, h3 img, h4 img, td img {
vertical-align: middle;
}
.quiet, a.quiet:link, a.quiet:visited {
color: #999 !important;
font-weight: normal !important;
}
.quiet strong {
font-weight: bold !important;
}
.float-right {
float: right;
}
.float-left {
float: left;
}
.clear {
clear: both;
}
.align-left {
text-align: left;
}
.align-right {
text-align: right;
}
.example {
margin: 10px 0;
padding: 5px 10px;
background: #efefef;
}
.nowrap {
white-space: nowrap;
}
/* TABLES */
table {
border-collapse: collapse;
border-color: #ccc;
}
td, th {
font-size: 11px;
line-height: 13px;
border-bottom: 1px solid #eee;
vertical-align: top;
padding: 5px;
font-family: "Lucida Grande", Verdana, Arial, sans-serif;
}
th {
text-align: left;
font-size: 12px;
font-weight: bold;
}
thead th,
tfoot td {
color: #666;
padding: 2px 5px;
font-size: 11px;
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
border-left: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
tfoot td {
border-bottom: none;
border-top: 1px solid #ddd;
}
thead th:first-child,
tfoot td:first-child {
border-left: none !important;
}
thead th.optional {
font-weight: normal !important;
}
fieldset table {
border-right: 1px solid #eee;
}
tr.row-label td {
font-size: 9px;
padding-top: 2px;
padding-bottom: 0;
border-bottom: none;
color: #666;
margin-top: -1px;
}
tr.alt {
background: #f6f6f6;
}
.row1 {
background: #EDF3FE;
}
.row2 {
background: white;
}
/* SORTABLE TABLES */
thead th a:link, thead th a:visited {
color: #666;
display: block;
}
table thead th.sorted {
background-position: bottom left !important;
}
table thead th.sorted a {
padding-right: 13px;
}
table thead th.ascending a {
background: url(../img/admin/arrow-up.gif) right .4em no-repeat;
}
table thead th.descending a {
background: url(../img/admin/arrow-down.gif) right .4em no-repeat;
}
/* ORDERABLE TABLES */
table.orderable tbody tr td:hover {
cursor: move;
}
table.orderable tbody tr td:first-child {
padding-left: 14px;
background-image: url(../img/admin/nav-bg-grabber.gif);
background-repeat: repeat-y;
}
table.orderable-initalized .order-cell, body>tr>td.order-cell {
display: none;
}
/* FORM DEFAULTS */
input, textarea, select, .form-row p {
margin: 2px 0;
padding: 2px 3px;
vertical-align: middle;
font-family: "Lucida Grande", Verdana, Arial, sans-serif;
font-weight: normal;
font-size: 11px;
}
textarea {
vertical-align: top !important;
}
input[type=text], input[type=password], textarea, select, .vTextField {
border: 1px solid #ccc;
}
/* FORM BUTTONS */
.button, input[type=submit], input[type=button], .submit-row input {
background: white url(../img/admin/nav-bg.gif) bottom repeat-x;
padding: 3px 5px;
color: black;
border: 1px solid #bbb;
border-color: #ddd #aaa #aaa #ddd;
}
.button:active, input[type=submit]:active, input[type=button]:active {
background-image: url(../img/admin/nav-bg-reverse.gif);
background-position: top;
}
.button[disabled], input[type=submit][disabled], input[type=button][disabled] {
background-image: url(../img/admin/nav-bg.gif);
background-position: bottom;
opacity: 0.4;
}
.button.default, input[type=submit].default, .submit-row input.default {
border: 2px solid #5b80b2;
background: #7CA0C7 url(../img/admin/default-bg.gif) bottom repeat-x;
font-weight: bold;
color: white;
float: right;
}
.button.default:active, input[type=submit].default:active {
background-image: url(../img/admin/default-bg-reverse.gif);
background-position: top;
}
.button[disabled].default, input[type=submit][disabled].default, input[type=button][disabled].default {
background-image: url(../img/admin/default-bg.gif);
background-position: bottom;
opacity: 0.4;
}
/* MODULES */
.module {
border: 1px solid #ccc;
margin-bottom: 5px;
background: white;
}
.module p, .module ul, .module h3, .module h4, .module dl, .module pre {
padding-left: 10px;
padding-right: 10px;
}
.module blockquote {
margin-left: 12px;
}
.module ul, .module ol {
margin-left: 1.5em;
}
.module h3 {
margin-top: .6em;
}
.module h2, .module caption, .inline-group h2 {
margin: 0;
padding: 2px 5px 3px 5px;
font-size: 11px;
text-align: left;
font-weight: bold;
background: #7CA0C7 url(../img/admin/default-bg.gif) top left repeat-x;
color: white;
}
.module table {
border-collapse: collapse;
}
/* MESSAGES & ERRORS */
ul.messagelist {
padding: 0 0 5px 0;
margin: 0;
}
ul.messagelist li {
font-size: 12px;
display: block;
padding: 4px 5px 4px 25px;
margin: 0 0 3px 0;
border-bottom: 1px solid #ddd;
color: #666;
background: #ffc url(../img/admin/icon_success.gif) 5px .3em no-repeat;
}
ul.messagelist li.warning{
background-image: url(../img/admin/icon_alert.gif);
}
ul.messagelist li.error{
background-image: url(../img/admin/icon_error.gif);
}
.errornote {
font-size: 12px !important;
display: block;
padding: 4px 5px 4px 25px;
margin: 0 0 3px 0;
border: 1px solid red;
color: red;
background: #ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat;
}
ul.errorlist {
margin: 0 !important;
padding: 0 !important;
}
.errorlist li {
font-size: 12px !important;
display: block;
padding: 4px 5px 4px 25px;
margin: 0 0 3px 0;
border: 1px solid red;
color: white;
background: red url(../img/admin/icon_alert.gif) 5px .3em no-repeat;
}
.errorlist li a {
color: white;
text-decoration: underline;
}
td ul.errorlist {
margin: 0 !important;
padding: 0 !important;
}
td ul.errorlist li {
margin: 0 !important;
}
.errors {
background: #ffc;
}
.errors input, .errors select, .errors textarea {
border: 1px solid red;
}
div.system-message {
background: #ffc;
margin: 10px;
padding: 6px 8px;
font-size: .8em;
}
div.system-message p.system-message-title {
padding: 4px 5px 4px 25px;
margin: 0;
color: red;
background: #ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat;
}
.description {
font-size: 12px;
padding: 5px 0 0 12px;
}
/* BREADCRUMBS */
div.breadcrumbs {
background: white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x;
padding: 2px 8px 3px 8px;
font-size: 11px;
color: #999;
border-top: 1px solid white;
border-bottom: 1px solid #ccc;
text-align: left;
}
/* ACTION ICONS */
.addlink {
padding-left: 12px;
background: url(../img/admin/icon_addlink.gif) 0 .2em no-repeat;
}
.changelink {
padding-left: 12px;
background: url(../img/admin/icon_changelink.gif) 0 .2em no-repeat;
}
.deletelink {
padding-left: 12px;
background: url(../img/admin/icon_deletelink.gif) 0 .25em no-repeat;
}
a.deletelink:link, a.deletelink:visited {
color: #CC3434;
}
a.deletelink:hover {
color: #993333;
}
/* OBJECT TOOLS */
.object-tools {
font-size: 10px;
font-weight: bold;
font-family: Arial,Helvetica,sans-serif;
padding-left: 0;
float: right;
position: relative;
margin-top: -2.4em;
margin-bottom: -2em;
}
.form-row .object-tools {
margin-top: 5px;
margin-bottom: 5px;
float: none;
height: 2em;
padding-left: 3.5em;
}
.object-tools li {
display: block;
float: left;
background: url(../img/admin/tool-left.gif) 0 0 no-repeat;
padding: 0 0 0 8px;
margin-left: 2px;
height: 16px;
}
.object-tools li:hover {
background: url(../img/admin/tool-left_over.gif) 0 0 no-repeat;
}
.object-tools a:link, .object-tools a:visited {
display: block;
float: left;
color: white;
padding: .1em 14px .1em 8px;
height: 14px;
background: #999 url(../img/admin/tool-right.gif) 100% 0 no-repeat;
}
.object-tools a:hover, .object-tools li:hover a {
background: #5b80b2 url(../img/admin/tool-right_over.gif) 100% 0 no-repeat;
}
.object-tools a.viewsitelink, .object-tools a.golink {
background: #999 url(../img/admin/tooltag-arrowright.gif) top right no-repeat;
padding-right: 28px;
}
.object-tools a.viewsitelink:hover, .object-tools a.golink:hover {
background: #5b80b2 url(../img/admin/tooltag-arrowright_over.gif) top right no-repeat;
}
.object-tools a.addlink {
background: #999 url(../img/admin/tooltag-add.gif) top right no-repeat;
padding-right: 28px;
}
.object-tools a.addlink:hover {
background: #5b80b2 url(../img/admin/tooltag-add_over.gif) top right no-repeat;
}
/* OBJECT HISTORY */
table#change-history {
width: 100%;
}
table#change-history tbody th {
width: 16em;
}
/* PAGE STRUCTURE */
#container {
position: relative;
width: 100%;
min-width: 760px;
padding: 0;
}
#content {
margin: 10px 15px;
}
#header {
width: 100%;
}
#content-main {
float: left;
width: 100%;
}
#content-related {
float: right;
width: 18em;
position: relative;
margin-right: -19em;
}
#footer {
clear: both;
padding: 10px;
}
/* COLUMN TYPES */
.colMS {
margin-right: 20em !important;
}
.colSM {
margin-left: 20em !important;
}
.colSM #content-related {
float: left;
margin-right: 0;
margin-left: -19em;
}
.colSM #content-main {
float: right;
}
.popup .colM {
width: 95%;
}
.subcol {
float: left;
width: 46%;
margin-right: 15px;
}
.dashboard #content {
width: 500px;
}
/* HEADER */
#header {
background: #417690;
color: #ffc;
overflow: hidden;
}
#header a:link, #header a:visited {
color: white;
}
#header a:hover {
text-decoration: underline;
}
#branding h1 {
padding: 0 10px;
font-size: 18px;
margin: 8px 0;
font-weight: normal;
color: #f4f379;
}
#branding h2 {
padding: 0 10px;
font-size: 14px;
margin: -8px 0 8px 0;
font-weight: normal;
color: #ffc;
}
#user-tools {
position: absolute;
top: 0;
right: 0;
padding: 1.2em 10px;
font-size: 11px;
text-align: right;
}
/* SIDEBAR */
#content-related h3 {
font-size: 12px;
color: #666;
margin-bottom: 3px;
}
#content-related h4 {
font-size: 11px;
}
#content-related .module h2 {
background: #eee url(../img/admin/nav-bg.gif) bottom left repeat-x;
color: #666;
}
/********************** admin 'forms.css' ************************/
/* FORM ROWS */
.form-row {
overflow: hidden;
padding: 8px 12px;
font-size: 11px;
border-bottom: 1px solid #eee;
}
.form-row img, .form-row input {
vertical-align: middle;
}
form .form-row p {
padding-left: 0;
font-size: 11px;
}
/* FORM LABELS */
form h4 {
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
label {
font-weight: normal !important;
color: #666;
font-size: 12px;
}
.required label, label.required {
font-weight: bold !important;
color: #333 !important;
}
/* RADIO BUTTONS */
form ul.radiolist li {
list-style-type: none;
}
form ul.radiolist label {
float: none;
display: inline;
}
form ul.inline {
margin-left: 0;
padding: 0;
}
form ul.inline li {
float: left;
padding-right: 7px;
}
/* ALIGNED FIELDSETS */
.aligned label {
display: block;
padding: 3px 10px 0 0;
float: left;
width: 8em;
}
.aligned ul label {
display: inline;
float: none;
width: auto;
}
.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
width: 350px;
}
form .aligned p, form .aligned ul {
margin-left: 7em;
padding-left: 30px;
}
form .aligned table p {
margin-left: 0;
padding-left: 0;
}
form .aligned p.help {
padding-left: 38px;
}
.aligned .vCheckboxLabel {
float: none !important;
display: inline;
padding-left: 4px;
}
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
width: 610px;
}
.checkbox-row p.help {
margin-left: 0;
padding-left: 0 !important;
}
fieldset .field-box {
float: left;
margin-right: 20px;
}
/* WIDE FIELDSETS */
.wide label {
width: 15em !important;
}
form .wide p {
margin-left: 15em;
}
form .wide p.help {
padding-left: 38px;
}
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
width: 450px;
}
/* COLLAPSED FIELDSETS */
fieldset.collapsed * {
display: none;
}
fieldset.collapsed h2, fieldset.collapsed {
display: block !important;
}
fieldset.collapsed h2 {
background-image: url(../img/admin/nav-bg.gif);
background-position: bottom left;
color: #999;
}
fieldset.collapsed .collapse-toggle {
background: transparent;
display: inline !important;
}
/* MONOSPACE TEXTAREAS */
fieldset.monospace textarea {
font-family: "Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace;
}
/* SUBMIT ROW */
.submit-row {
padding: 5px 7px;
text-align: right;
background: white url(../img/admin/nav-bg.gif) 0 100% repeat-x;
border: 1px solid #ccc;
margin: 5px 0;
overflow: hidden;
}
.submit-row input {
margin: 0 0 0 5px;
}
.submit-row p {
margin: 0.3em;
}
.submit-row p.deletelink-box {
float: left;
}
.submit-row .deletelink {
background: url(../img/admin/icon_deletelink.gif) 0 50% no-repeat;
padding-left: 14px;
}
/* CUSTOM FORM FIELDS */
.vSelectMultipleField {
vertical-align: top !important;
}
.vCheckboxField {
border: none;
}
.vDateField, .vTimeField {
margin-right: 2px;
}
.vURLField {
width: 30em;
}
.vLargeTextField, .vXMLLargeTextField {
width: 48em;
}
.flatpages-flatpage #id_content {
height: 40.2em;
}
.module table .vPositiveSmallIntegerField {
width: 2.2em;
}
.vTextField {
width: 20em;
}
.vIntegerField {
width: 5em;
}
.vForeignKeyRawIdAdminField {
width: 5em;
}
/* INLINES */
.inline-group {
padding: 0;
border: 1px solid #ccc;
margin: 10px 0;
}
.inline-group .aligned label {
width: 8em;
}
.inline-related {
position: relative;
}
.inline-related h3 {
margin: 0;
color: #666;
padding: 3px 5px;
font-size: 11px;
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
border-bottom: 1px solid #ddd;
}
.inline-related h3 span.delete {
float: right;
}
.inline-related h3 span.delete label {
margin-left: 2px;
font-size: 11px;
}
.inline-related fieldset {
margin: 0;
background: #fff;
border: none;
}
.inline-related fieldset.module h3 {
margin: 0;
padding: 2px 5px 3px 5px;
font-size: 11px;
text-align: left;
font-weight: bold;
background: #bcd;
color: #fff;
}
.inline-group .tabular fieldset.module {
border: none;
border-bottom: 1px solid #ddd;
}
.inline-related.tabular fieldset.module table {
width: 100%;
}
.last-related fieldset {
border: none;
}
.inline-group .tabular tr.has_original td {
padding-top: 2em;
}
.inline-group .tabular tr td.original {
padding: 2px 0 0 0;
width: 0;
_position: relative;
}
.inline-group .tabular th.original {
width: 0px;
padding: 0;
}
.inline-group .tabular td.original p {
position: absolute;
left: 0;
height: 1.1em;
padding: 2px 7px;
overflow: hidden;
font-size: 9px;
font-weight: bold;
color: #666;
_width: 700px;
}
.inline-group ul.tools {
padding: 0;
margin: 0;
list-style: none;
}
.inline-group ul.tools li {
display: inline;
padding: 0 5px;
}
.inline-group div.add-row,
.inline-group .tabular tr.add-row td {
color: #666;
padding: 3px 5px;
border-bottom: 1px solid #ddd;
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
}
.inline-group .tabular tr.add-row td {
padding: 4px 5px 3px;
border-bottom: none;
}
.inline-group ul.tools a.add,
.inline-group div.add-row a,
.inline-group .tabular tr.add-row td a {
background: url(../img/admin/icon_addlink.gif) 0 50% no-repeat;
padding-left: 14px;
font-size: 11px;
outline: 0; /* Remove dotted border around link */
}
.empty-form {
display: none;
}
/* IE7 specific bug fixes */
.submit-row input {
float: right;
}
/* Overrides specific to REST framework */
#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;
}
...@@ -5,7 +5,6 @@ See RFC 2616 - Sec 10: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html ...@@ -5,7 +5,6 @@ See RFC 2616 - Sec 10: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
Also see django.core.handlers.wsgi.STATUS_CODE_TEXT Also see django.core.handlers.wsgi.STATUS_CODE_TEXT
""" """
# Verbose format
HTTP_100_CONTINUE = 100 HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101 HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_200_OK = 200 HTTP_200_OK = 200
......
{% 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" <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> "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"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<style> <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}css/djangorestframework.css'/>
/* Override some of the Django admin styling */ <title>Django REST framework - {{ name }}</title>
#site-name a {color: #F4F379 !important;} </head>
.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>
<body> <body>
<div id="container"> <div id="container">
...@@ -34,7 +23,7 @@ ...@@ -34,7 +23,7 @@
<div class="breadcrumbs"> <div class="breadcrumbs">
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %} {% 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 %} {% endfor %}
</div> </div>
...@@ -50,7 +39,7 @@ ...@@ -50,7 +39,7 @@
<div class='content-main'> <div class='content-main'>
<h1>{{ name }}</h1> <h1>{{ name }}</h1>
<p>{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}</p> <p>{{ description }}</p>
<div class='module'> <div class='module'>
<pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %} <pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %}
{% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }} {% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
......
{{ name }} {% autoescape off %}{{ name }}
{{ description }} {{ 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 }} {% for key, val in response.headers.items %}{{ key }}: {{ val }}
{% endfor %} {% endfor %}
{{ content }}{% endautoescape %} {{ content }}{% endautoescape %}
...@@ -5,7 +5,7 @@ register = Library() ...@@ -5,7 +5,7 @@ register = Library()
def add_query_param(url, param): def add_query_param(url, param):
(key, sep, val) = param.partition('=') (key, sep, val) = param.partition('=')
return unicode(URLObject(url) & (key, val)) return unicode(URLObject.parse(url) & (key, val))
register.filter('add_query_param', add_query_param) register.filter('add_query_param', add_query_param)
...@@ -11,7 +11,7 @@ import base64 ...@@ -11,7 +11,7 @@ import base64
class MockView(View): class MockView(View):
permissions = ( permissions.IsAuthenticated, ) permissions = (permissions.IsAuthenticated,)
def post(self, request): def post(self, request):
return {'a': 1, 'b': 2, 'c': 3} return {'a': 1, 'b': 2, 'c': 3}
...@@ -74,24 +74,32 @@ class SessionAuthTests(TestCase): ...@@ -74,24 +74,32 @@ class SessionAuthTests(TestCase):
self.csrf_client.logout() self.csrf_client.logout()
def test_post_form_session_auth_failing_csrf(self): def test_post_form_session_auth_failing_csrf(self):
"""Ensure POSTing form over session authentication without CSRF token fails.""" """
Ensure POSTing form over session authentication without CSRF token fails.
"""
self.csrf_client.login(username=self.username, password=self.password) self.csrf_client.login(username=self.username, password=self.password)
response = self.csrf_client.post('/', {'example': 'example'}) response = self.csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_post_form_session_auth_passing(self): def test_post_form_session_auth_passing(self):
"""Ensure POSTing form over session authentication with logged in user and CSRF token passes.""" """
Ensure POSTing form over session authentication with logged in user and CSRF token passes.
"""
self.non_csrf_client.login(username=self.username, password=self.password) self.non_csrf_client.login(username=self.username, password=self.password)
response = self.non_csrf_client.post('/', {'example': 'example'}) response = self.non_csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_put_form_session_auth_passing(self): def test_put_form_session_auth_passing(self):
"""Ensure PUTting form over session authentication with logged in user and CSRF token passes.""" """
Ensure PUTting form over session authentication with logged in user and CSRF token passes.
"""
self.non_csrf_client.login(username=self.username, password=self.password) self.non_csrf_client.login(username=self.username, password=self.password)
response = self.non_csrf_client.put('/', {'example': 'example'}) response = self.non_csrf_client.put('/', {'example': 'example'})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_post_form_session_auth_failing(self): def test_post_form_session_auth_failing(self):
"""Ensure POSTing form over session authentication without logged in user fails.""" """
Ensure POSTing form over session authentication without logged in user fails.
"""
response = self.csrf_client.post('/', {'example': 'example'}) response = self.csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
from django.test import TestCase from django.test import TestCase
from djangorestframework.views import View from djangorestframework.views import View
from djangorestframework.compat import apply_markdown from djangorestframework.compat import apply_markdown
from djangorestframework.utils.description import get_name, get_description
# We check that docstrings get nicely un-indented. # We check that docstrings get nicely un-indented.
DESCRIPTION = """an example docstring DESCRIPTION = """an example docstring
...@@ -51,15 +50,15 @@ class TestViewNamesAndDescriptions(TestCase): ...@@ -51,15 +50,15 @@ class TestViewNamesAndDescriptions(TestCase):
"""Ensure Resource names are based on the classname by default.""" """Ensure Resource names are based on the classname by default."""
class MockView(View): class MockView(View):
pass pass
self.assertEquals(get_name(MockView()), 'Mock') self.assertEquals(MockView().get_name(), 'Mock')
# This has been turned off now. def test_resource_name_can_be_set_explicitly(self):
#def test_resource_name_can_be_set_explicitly(self): """Ensure Resource names can be set using the 'get_name' method."""
# """Ensure Resource names can be set using the 'name' class attribute.""" example = 'Some Other Name'
# example = 'Some Other Name' class MockView(View):
# class MockView(View): def get_name(self):
# name = example return example
# self.assertEquals(get_name(MockView()), example) self.assertEquals(MockView().get_name(), example)
def test_resource_description_uses_docstring_by_default(self): def test_resource_description_uses_docstring_by_default(self):
"""Ensure Resource names are based on the docstring by default.""" """Ensure Resource names are based on the docstring by default."""
...@@ -79,29 +78,30 @@ class TestViewNamesAndDescriptions(TestCase): ...@@ -79,29 +78,30 @@ class TestViewNamesAndDescriptions(TestCase):
# hash style header #""" # hash style header #"""
self.assertEquals(get_description(MockView()), DESCRIPTION) self.assertEquals(MockView().get_description(), DESCRIPTION)
# This has been turned off now
#def test_resource_description_can_be_set_explicitly(self):
# """Ensure Resource descriptions can be set using the 'description' class attribute."""
# example = 'Some other description'
# class MockView(View):
# """docstring"""
# description = example
# self.assertEquals(get_description(MockView()), example)
#def test_resource_description_does_not_require_docstring(self): def test_resource_description_can_be_set_explicitly(self):
# """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute.""" """Ensure Resource descriptions can be set using the 'get_description' method."""
# example = 'Some other description' example = 'Some other description'
# class MockView(View): class MockView(View):
# description = example """docstring"""
# self.assertEquals(get_description(MockView()), example) def get_description(self):
return example
self.assertEquals(MockView().get_description(), example)
def test_resource_description_does_not_require_docstring(self):
"""Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'get_description' method."""
example = 'Some other description'
class MockView(View):
def get_description(self):
return example
self.assertEquals(MockView().get_description(), example)
def test_resource_description_can_be_empty(self): def test_resource_description_can_be_empty(self):
"""Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string""" """Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string."""
class MockView(View): class MockView(View):
pass pass
self.assertEquals(get_description(MockView()), '') self.assertEquals(MockView().get_description(), '')
def test_markdown(self): def test_markdown(self):
"""Ensure markdown to HTML works as expected""" """Ensure markdown to HTML works as expected"""
......
...@@ -30,7 +30,7 @@ class TestModelRead(TestModelsTestCase): ...@@ -30,7 +30,7 @@ class TestModelRead(TestModelsTestCase):
mixin = ReadModelMixin() mixin = ReadModelMixin()
mixin.resource = GroupResource mixin.resource = GroupResource
response = mixin.get(request, group.id) response = mixin.get(request, id=group.id)
self.assertEquals(group.name, response.name) self.assertEquals(group.name, response.name)
def test_read_404(self): def test_read_404(self):
...@@ -41,8 +41,7 @@ class TestModelRead(TestModelsTestCase): ...@@ -41,8 +41,7 @@ class TestModelRead(TestModelsTestCase):
mixin = ReadModelMixin() mixin = ReadModelMixin()
mixin.resource = GroupResource mixin.resource = GroupResource
with self.assertRaises(ErrorResponse): self.assertRaises(ErrorResponse, mixin.get, request, id=12345)
response = mixin.get(request, 12345)
class TestModelCreation(TestModelsTestCase): class TestModelCreation(TestModelsTestCase):
......
import re
from django.conf.urls.defaults import patterns, url from django.conf.urls.defaults import patterns, url
from django.test import TestCase from django.test import TestCase
...@@ -174,6 +176,12 @@ class RendererIntegrationTests(TestCase): ...@@ -174,6 +176,12 @@ class RendererIntegrationTests(TestCase):
_flat_repr = '{"foo": ["bar", "baz"]}' _flat_repr = '{"foo": ["bar", "baz"]}'
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
def strip_trailing_whitespace(content):
"""
Seems to be some inconsistencies re. trailing whitespace with
different versions of the json lib.
"""
return re.sub(' +\n', '\n', content)
class JSONRendererTests(TestCase): class JSONRendererTests(TestCase):
""" """
...@@ -187,6 +195,7 @@ class JSONRendererTests(TestCase): ...@@ -187,6 +195,7 @@ class JSONRendererTests(TestCase):
obj = {'foo': ['bar', 'baz']} obj = {'foo': ['bar', 'baz']}
renderer = JSONRenderer(None) renderer = JSONRenderer(None)
content = renderer.render(obj, 'application/json') content = renderer.render(obj, 'application/json')
# Fix failing test case which depends on version of JSON library.
self.assertEquals(content, _flat_repr) self.assertEquals(content, _flat_repr)
def test_with_content_type_args(self): def test_with_content_type_args(self):
...@@ -196,7 +205,7 @@ class JSONRendererTests(TestCase): ...@@ -196,7 +205,7 @@ class JSONRendererTests(TestCase):
obj = {'foo': ['bar', 'baz']} obj = {'foo': ['bar', 'baz']}
renderer = JSONRenderer(None) renderer = JSONRenderer(None)
content = renderer.render(obj, 'application/json; indent=2') content = renderer.render(obj, 'application/json; indent=2')
self.assertEquals(content, _indented_repr) self.assertEquals(strip_trailing_whitespace(content), _indented_repr)
def test_render_and_parse(self): def test_render_and_parse(self):
""" """
......
from django import forms from django import forms
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from djangorestframework.compat import RequestFactory from djangorestframework.resources import FormResource, ModelResource
from djangorestframework.resources import Resource, FormResource, ModelResource
from djangorestframework.response import ErrorResponse from djangorestframework.response import ErrorResponse
from djangorestframework.views import View from djangorestframework.views import View
from djangorestframework.resources import Resource
class TestDisabledValidations(TestCase): class TestDisabledValidations(TestCase):
...@@ -22,7 +19,7 @@ class TestDisabledValidations(TestCase): ...@@ -22,7 +19,7 @@ class TestDisabledValidations(TestCase):
resource = DisabledFormResource resource = DisabledFormResource
view = MockView() view = MockView()
content = {'qwerty':'uiop'} content = {'qwerty': 'uiop'}
self.assertEqual(FormResource(view).validate_request(content, None), content) self.assertEqual(FormResource(view).validate_request(content, None), content)
def test_disabled_form_validator_get_bound_form_returns_none(self): def test_disabled_form_validator_get_bound_form_returns_none(self):
...@@ -35,10 +32,9 @@ class TestDisabledValidations(TestCase): ...@@ -35,10 +32,9 @@ class TestDisabledValidations(TestCase):
resource = DisabledFormResource resource = DisabledFormResource
view = MockView() view = MockView()
content = {'qwerty':'uiop'} content = {'qwerty': 'uiop'}
self.assertEqual(FormResource(view).get_bound_form(content), None) self.assertEqual(FormResource(view).get_bound_form(content), None)
def test_disabled_model_form_validator_returns_content_unchanged(self): def test_disabled_model_form_validator_returns_content_unchanged(self):
"""If the view's form is None and does not have a Resource with a model set then """If the view's form is None and does not have a Resource with a model set then
ModelFormValidator(view).validate_request(content, None) should just return the content unmodified.""" ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
...@@ -47,8 +43,8 @@ class TestDisabledValidations(TestCase): ...@@ -47,8 +43,8 @@ class TestDisabledValidations(TestCase):
resource = ModelResource resource = ModelResource
view = DisabledModelFormView() view = DisabledModelFormView()
content = {'qwerty':'uiop'} content = {'qwerty': 'uiop'}
self.assertEqual(ModelResource(view).get_bound_form(content), None)# self.assertEqual(ModelResource(view).get_bound_form(content), None)
def test_disabled_model_form_validator_get_bound_form_returns_none(self): def test_disabled_model_form_validator_get_bound_form_returns_none(self):
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None.""" """If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
...@@ -56,9 +52,10 @@ class TestDisabledValidations(TestCase): ...@@ -56,9 +52,10 @@ class TestDisabledValidations(TestCase):
resource = ModelResource resource = ModelResource
view = DisabledModelFormView() view = DisabledModelFormView()
content = {'qwerty':'uiop'} content = {'qwerty': 'uiop'}
self.assertEqual(ModelResource(view).get_bound_form(content), None) self.assertEqual(ModelResource(view).get_bound_form(content), None)
class TestNonFieldErrors(TestCase): class TestNonFieldErrors(TestCase):
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)""" """Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
...@@ -72,7 +69,7 @@ class TestNonFieldErrors(TestCase): ...@@ -72,7 +69,7 @@ class TestNonFieldErrors(TestCase):
def clean(self): def clean(self):
if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data: if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
raise forms.ValidationError(self.ERROR_TEXT) raise forms.ValidationError(self.ERROR_TEXT)
return self.cleaned_data #pragma: no cover return self.cleaned_data
class MockResource(FormResource): class MockResource(FormResource):
form = MockForm form = MockForm
...@@ -87,7 +84,7 @@ class TestNonFieldErrors(TestCase): ...@@ -87,7 +84,7 @@ class TestNonFieldErrors(TestCase):
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]}) self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
else: else:
self.fail('ErrorResponse was not raised') #pragma: no cover self.fail('ErrorResponse was not raised')
class TestFormValidation(TestCase): class TestFormValidation(TestCase):
...@@ -115,10 +112,9 @@ class TestFormValidation(TestCase): ...@@ -115,10 +112,9 @@ class TestFormValidation(TestCase):
self.MockFormView = MockFormView self.MockFormView = MockFormView
self.MockModelFormView = MockModelFormView self.MockModelFormView = MockModelFormView
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator): def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
"""If the content is already valid and clean then validate(content) should just return the content unmodified.""" """If the content is already valid and clean then validate(content) should just return the content unmodified."""
content = {'qwerty':'uiop'} content = {'qwerty': 'uiop'}
self.assertEqual(validator.validate_request(content, None), content) self.assertEqual(validator.validate_request(content, None), content)
def validation_failure_raises_response_exception(self, validator): def validation_failure_raises_response_exception(self, validator):
...@@ -143,7 +139,9 @@ class TestFormValidation(TestCase): ...@@ -143,7 +139,9 @@ class TestFormValidation(TestCase):
raise errors on unexpected request data""" raise errors on unexpected request data"""
content = {'qwerty': 'uiop', 'extra': 'extra'} content = {'qwerty': 'uiop', 'extra': 'extra'}
validator.allow_unknown_form_fields = True validator.allow_unknown_form_fields = True
self.assertDictEqual({'qwerty': u'uiop'}, validator.validate_request(content, None), "Resource didn't accept unknown fields.") self.assertEqual({'qwerty': u'uiop'},
validator.validate_request(content, None),
"Resource didn't accept unknown fields.")
validator.allow_unknown_form_fields = False validator.allow_unknown_form_fields = False
def validation_does_not_require_extra_fields_if_explicitly_set(self, validator): def validation_does_not_require_extra_fields_if_explicitly_set(self, validator):
...@@ -159,7 +157,7 @@ class TestFormValidation(TestCase): ...@@ -159,7 +157,7 @@ class TestFormValidation(TestCase):
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}}) self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ResourceException was not raised')
def validation_failed_due_to_field_error_returns_appropriate_message(self, validator): def validation_failed_due_to_field_error_returns_appropriate_message(self, validator):
"""If validation fails due to a field error, ensure the response contains a single field error""" """If validation fails due to a field error, ensure the response contains a single field error"""
...@@ -169,7 +167,7 @@ class TestFormValidation(TestCase): ...@@ -169,7 +167,7 @@ class TestFormValidation(TestCase):
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}}) self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ResourceException was not raised')
def validation_failed_due_to_invalid_field_returns_appropriate_message(self, validator): def validation_failed_due_to_invalid_field_returns_appropriate_message(self, validator):
"""If validation fails due to an invalid field, ensure the response contains a single field error""" """If validation fails due to an invalid field, ensure the response contains a single field error"""
...@@ -179,7 +177,7 @@ class TestFormValidation(TestCase): ...@@ -179,7 +177,7 @@ class TestFormValidation(TestCase):
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}}) self.assertEqual(exc.response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ResourceException was not raised')
def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator): def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator):
"""If validation for multiple reasons, ensure the response contains each error""" """If validation for multiple reasons, ensure the response contains each error"""
...@@ -190,7 +188,7 @@ class TestFormValidation(TestCase): ...@@ -190,7 +188,7 @@ class TestFormValidation(TestCase):
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.'], self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
'extra': ['This field does not exist.']}}) 'extra': ['This field does not exist.']}})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ResourceException was not raised')
# Tests on FormResource # Tests on FormResource
...@@ -209,7 +207,7 @@ class TestFormValidation(TestCase): ...@@ -209,7 +207,7 @@ class TestFormValidation(TestCase):
def test_validation_allows_extra_fields_if_explicitly_set(self): def test_validation_allows_extra_fields_if_explicitly_set(self):
validator = self.MockFormResource(self.MockFormView()) validator = self.MockFormResource(self.MockFormView())
self.validation_allows_extra_fields_if_explicitly_set(validator) self.validation_allows_extra_fields_if_explicitly_set(validator)
def test_validation_allows_unknown_fields_if_explicitly_allowed(self): def test_validation_allows_unknown_fields_if_explicitly_allowed(self):
validator = self.MockFormResource(self.MockFormView()) validator = self.MockFormResource(self.MockFormView())
self.validation_allows_unknown_fields_if_explicitly_allowed(validator) self.validation_allows_unknown_fields_if_explicitly_allowed(validator)
...@@ -294,22 +292,21 @@ class TestModelFormValidator(TestCase): ...@@ -294,22 +292,21 @@ class TestModelFormValidator(TestCase):
self.validator = MockResource(MockView) self.validator = MockResource(MockView)
def test_property_fields_are_allowed_on_model_forms(self): def test_property_fields_are_allowed_on_model_forms(self):
"""Validation on ModelForms may include property fields that exist on the Model to be included in the input.""" """Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'} content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only'}
self.assertEqual(self.validator.validate_request(content, None), content) self.assertEqual(self.validator.validate_request(content, None), content)
def test_property_fields_are_not_required_on_model_forms(self): def test_property_fields_are_not_required_on_model_forms(self):
"""Validation on ModelForms does not require property fields that exist on the Model to be included in the input.""" """Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
content = {'qwerty':'example', 'uiop': 'example'} content = {'qwerty': 'example', 'uiop': 'example'}
self.assertEqual(self.validator.validate_request(content, None), content) self.assertEqual(self.validator.validate_request(content, None), content)
def test_extra_fields_not_allowed_on_model_forms(self): def test_extra_fields_not_allowed_on_model_forms(self):
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail. """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 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)""" broken clients more easily (eg submitting content with a misnamed field)"""
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'} content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'}
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None) self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
def test_validate_requires_fields_on_model_forms(self): def test_validate_requires_fields_on_model_forms(self):
...@@ -321,10 +318,8 @@ class TestModelFormValidator(TestCase): ...@@ -321,10 +318,8 @@ class TestModelFormValidator(TestCase):
def test_validate_does_not_require_blankable_fields_on_model_forms(self): def test_validate_does_not_require_blankable_fields_on_model_forms(self):
"""Test standard ModelForm validation behaviour - fields with blank=True are not required.""" """Test standard ModelForm validation behaviour - fields with blank=True are not required."""
content = {'qwerty':'example', 'readonly': 'read only'} content = {'qwerty': 'example', 'readonly': 'read only'}
self.validator.validate_request(content, None) self.validator.validate_request(content, None)
def test_model_form_validator_uses_model_forms(self): def test_model_form_validator_uses_model_forms(self):
self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm)) self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
from django.core.urlresolvers import resolve from django.core.urlresolvers import resolve
from djangorestframework.utils.description import get_name
def get_breadcrumbs(url): def get_breadcrumbs(url):
"""Given a url returns a list of breadcrumbs, which are each a tuple of (name, url).""" """Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
...@@ -17,7 +15,7 @@ def get_breadcrumbs(url): ...@@ -17,7 +15,7 @@ def get_breadcrumbs(url):
else: else:
# Check if this is a REST framework view, and if so add it to the breadcrumbs # Check if this is a REST framework view, and if so add it to the breadcrumbs
if isinstance(getattr(view, 'cls_instance', None), View): if isinstance(getattr(view, 'cls_instance', None), View):
breadcrumbs_list.insert(0, (get_name(view), url)) breadcrumbs_list.insert(0, (view.cls_instance.get_name(), url))
if url == '': if url == '':
# All done # All done
......
"""
Get a descriptive name and description for a view.
"""
import re
from djangorestframework.resources import Resource, FormResource, ModelResource
# These a a bit Grungy, but they do the job.
def get_name(view):
"""
Return a name for the view.
If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'.
"""
# If we're looking up the name of a view callable, as found by reverse,
# grok the class instance that we stored when as_view was called.
if getattr(view, 'cls_instance', None):
view = view.cls_instance
# If this view has a resource that's been overridden, then use that resource for the name
if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
name = view.resource.__name__
# Chomp of any non-descriptive trailing part of the resource class name
if name.endswith('Resource') and name != 'Resource':
name = name[:-len('Resource')]
# If the view has a descriptive suffix, eg '*** List', '*** Instance'
if getattr(view, '_suffix', None):
name += view._suffix
# Otherwise if it's a function view use the function's name
elif getattr(view, '__name__', None) is not None:
name = view.__name__
# If it's a view class with no resource then grok the name from the class name
elif getattr(view, '__class__', None) is not None:
name = view.__class__.__name__
# Chomp of any non-descriptive trailing part of the view class name
if name.endswith('View') and name != 'View':
name = name[:-len('View')]
# I ain't got nuthin fo' ya
else:
return ''
return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
def get_description(view):
"""
Provide a description for the view.
By default this is the view's docstring with nice unindention applied.
"""
# If we're looking up the name of a view callable, as found by reverse,
# grok the class instance that we stored when as_view was called.
if getattr(view, 'cls_instance', None):
view = view.cls_instance
# If this view has a resource that's been overridden, then use the resource's doctring
if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
doc = view.resource.__doc__
# Otherwise use the view doctring
elif getattr(view, '__doc__', None):
doc = view.__doc__
# I ain't got nuthin fo' ya
else:
return ''
if not doc:
return ''
whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in doc.splitlines()[1:] if line.lstrip()]
# unindent the docstring if needed
if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', doc)
# otherwise return it as-is
return doc
...@@ -5,15 +5,17 @@ be subclassing in your implementation. ...@@ -5,15 +5,17 @@ be subclassing in your implementation.
By setting or modifying class attributes on your view, you change it's predefined behaviour. By setting or modifying class attributes on your view, you change it's predefined behaviour.
""" """
import re
from django.core.urlresolvers import set_script_prefix, get_script_prefix from django.core.urlresolvers import set_script_prefix, get_script_prefix
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View as DjangoView from djangorestframework.compat import View as DjangoView, apply_markdown
from djangorestframework.response import Response, ErrorResponse from djangorestframework.response import Response, ErrorResponse
from djangorestframework.mixins import * from djangorestframework.mixins import *
from djangorestframework import resources, renderers, parsers, authentication, permissions, status from djangorestframework import resources, renderers, parsers, authentication, permissions, status
from djangorestframework.utils.description import get_name, get_description
__all__ = ( __all__ = (
...@@ -25,6 +27,48 @@ __all__ = ( ...@@ -25,6 +27,48 @@ __all__ = (
) )
def _remove_trailing_string(content, trailing):
"""
Strip trailing component `trailing` from `content` if it exists.
Used when generating names from view/resource classes.
"""
if content.endswith(trailing) and content != trailing:
return content[:-len(trailing)]
return content
def _remove_leading_indent(content):
"""
Remove leading indent from a block of text.
Used when generating descriptions from docstrings.
"""
whitespace_counts = [len(line) - len(line.lstrip(' '))
for line in content.splitlines()[1:] if line.lstrip()]
# unindent the content if needed
if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
return content
def _camelcase_to_spaces(content):
"""
Translate 'CamelCaseNames' to 'Camel Case Names'.
Used when generating names from view/resource classes.
"""
camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))'
return re.sub(camelcase_boundry, ' \\1', content).strip()
_resource_classes = (
None,
resources.Resource,
resources.FormResource,
resources.ModelResource
)
class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
""" """
Handles incoming requests and maps them to REST operations. Handles incoming requests and maps them to REST operations.
...@@ -48,7 +92,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -48,7 +92,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
""" """
authentication = (authentication.UserLoggedInAuthentication, authentication = (authentication.UserLoggedInAuthentication,
authentication.BasicAuthentication) authentication.BasicAuthentication)
""" """
List of all authenticating methods to attempt. List of all authenticating methods to attempt.
""" """
...@@ -76,6 +120,54 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -76,6 +120,54 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
""" """
return [method.upper() for method in self.http_method_names if hasattr(self, method)] return [method.upper() for method in self.http_method_names if hasattr(self, method)]
def get_name(self):
"""
Return the resource or view class name for use as this view's name.
Override to customize.
"""
# If this view has a resource that's been overridden, then use that resource for the name
if getattr(self, 'resource', None) not in _resource_classes:
name = self.resource.__name__
name = _remove_trailing_string(name, 'Resource')
name += getattr(self, '_suffix', '')
# If it's a view class with no resource then grok the name from the class name
else:
name = self.__class__.__name__
name = _remove_trailing_string(name, 'View')
return _camelcase_to_spaces(name)
def get_description(self, html=False):
"""
Return the resource or view docstring for use as this view's description.
Override to customize.
"""
description = None
# If this view has a resource that's been overridden,
# then try to use the resource's docstring
if getattr(self, 'resource', None) not in _resource_classes:
description = self.resource.__doc__
# Otherwise use the view docstring
if not description:
description = self.__doc__ or ''
description = _remove_leading_indent(description)
if html:
return self.markup_description(description)
return description
def markup_description(self, description):
if apply_markdown:
description = apply_markdown(description)
else:
description = escape(description).replace('\n', '<br />')
return mark_safe(description)
def http_method_not_allowed(self, request, *args, **kwargs): def http_method_not_allowed(self, request, *args, **kwargs):
""" """
Return an HTTP 405 error if an operation is called which does not have a handler method. Return an HTTP 405 error if an operation is called which does not have a handler method.
...@@ -164,8 +256,8 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -164,8 +256,8 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
def options(self, request, *args, **kwargs): def options(self, request, *args, **kwargs):
response_obj = { response_obj = {
'name': get_name(self), 'name': self.get_name(),
'description': get_description(self), 'description': self.get_description(),
'renders': self._rendered_media_types, 'renders': self._rendered_media_types,
'parses': request._parsed_media_types, 'parses': request._parsed_media_types,
} }
......
...@@ -105,6 +105,8 @@ The following example exposes your `MyModel` model through an api. It will provi ...@@ -105,6 +105,8 @@ The following example exposes your `MyModel` model through an api. It will provi
contents contents
.. include:: ../CHANGELOG.rst
Indices and tables Indices and tables
------------------ ------------------
......
...@@ -2,14 +2,23 @@ from djangorestframework.views import View ...@@ -2,14 +2,23 @@ from djangorestframework.views import View
from djangorestframework.permissions import PerUserThrottling, IsAuthenticated from djangorestframework.permissions import PerUserThrottling, IsAuthenticated
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
class PermissionsExampleView(View): class PermissionsExampleView(View):
""" """
A container view for permissions examples. A container view for permissions examples.
""" """
def get(self, request): def get(self, request):
return [{'name': 'Throttling Example', 'url': reverse('throttled-resource')}, return [
{'name': 'Logged in example', 'url': reverse('loggedin-resource')},] {
'name': 'Throttling Example',
'url': reverse('throttled-resource')
},
{
'name': 'Logged in example',
'url': reverse('loggedin-resource')
},
]
class ThrottlingExampleView(View): class ThrottlingExampleView(View):
...@@ -20,7 +29,7 @@ class ThrottlingExampleView(View): ...@@ -20,7 +29,7 @@ class ThrottlingExampleView(View):
throttle will be applied until 60 seconds have passed since the first request. throttle will be applied until 60 seconds have passed since the first request.
""" """
permissions = ( PerUserThrottling, ) permissions = (PerUserThrottling,)
throttle = '10/min' throttle = '10/min'
def get(self, request): def get(self, request):
...@@ -29,13 +38,15 @@ class ThrottlingExampleView(View): ...@@ -29,13 +38,15 @@ class ThrottlingExampleView(View):
""" """
return "Successful response to GET request because throttle is not yet active." return "Successful response to GET request because throttle is not yet active."
class LoggedInExampleView(View): class LoggedInExampleView(View):
""" """
You can login with **'test', 'test'.** or use curl: You can login with **'test', 'test'.** or use curl:
`curl -X GET -H 'Accept: application/json' -u test:test http://localhost:8000/permissions-example` `curl -X GET -H 'Accept: application/json' -u test:test http://localhost:8000/permissions-example`
""" """
permissions = (IsAuthenticated, ) permissions = (IsAuthenticated, )
def get(self, request): def get(self, request):
return 'Logged in or not?' return 'You have permission to view this resource'
Pygments==1.4 Pygments==1.4
Markdown==2.0.3 Markdown==2.0.3
djangorestframework git+git://github.com/tomchristie/django-rest-framework.git
...@@ -53,16 +53,10 @@ MEDIA_ROOT = os.path.join(os.getenv('EPIO_DATA_DIRECTORY', '.'), 'media') ...@@ -53,16 +53,10 @@ MEDIA_ROOT = os.path.join(os.getenv('EPIO_DATA_DIRECTORY', '.'), 'media')
# trailing slash if there is a path component (optional in other cases). # trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/" # Examples: "http://media.lawrence.com", "http://example.com/media/"
# NOTE: None of the djangorestframework examples serve media content via MEDIA_URL. # 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. # Make this unique, and don't share it with anybody.
SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu' SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu'
...@@ -102,6 +96,7 @@ INSTALLED_APPS = ( ...@@ -102,6 +96,7 @@ INSTALLED_APPS = (
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.staticfiles',
'django.contrib.messages', 'django.contrib.messages',
'djangorestframework', 'djangorestframework',
......
from django.conf.urls.defaults import patterns, include, url from django.conf.urls.defaults import patterns, include, url
from django.conf import settings from django.conf import settings
from sandbox.views import Sandbox from sandbox.views import Sandbox
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns = patterns('', urlpatterns = patterns('',
(r'^$', Sandbox.as_view()), (r'^$', Sandbox.as_view()),
...@@ -16,3 +17,4 @@ urlpatterns = patterns('', ...@@ -16,3 +17,4 @@ urlpatterns = patterns('',
(r'^', include('djangorestframework.urls')), (r'^', include('djangorestframework.urls')),
) )
urlpatterns += staticfiles_urlpatterns()
...@@ -32,6 +32,7 @@ commands= ...@@ -32,6 +32,7 @@ commands=
basepython=python2.5 basepython=python2.5
deps= deps=
django==1.2.4 django==1.2.4
django-staticfiles>=1.1.2
coverage==3.4 coverage==3.4
URLObject>=0.6.0 URLObject>=0.6.0
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
...@@ -43,6 +44,7 @@ deps= ...@@ -43,6 +44,7 @@ deps=
basepython=python2.6 basepython=python2.6
deps= deps=
django==1.2.4 django==1.2.4
django-staticfiles>=1.1.2
coverage==3.4 coverage==3.4
URLObject>=0.6.0 URLObject>=0.6.0
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
...@@ -54,6 +56,7 @@ deps= ...@@ -54,6 +56,7 @@ deps=
basepython=python2.7 basepython=python2.7
deps= deps=
django==1.2.4 django==1.2.4
django-staticfiles>=1.1.2
coverage==3.4 coverage==3.4
URLObject>=0.6.0 URLObject>=0.6.0
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
...@@ -135,6 +138,7 @@ commands= ...@@ -135,6 +138,7 @@ commands=
python examples/runtests.py python examples/runtests.py
deps= deps=
django==1.2.4 django==1.2.4
django-staticfiles>=1.1.2
coverage==3.4 coverage==3.4
URLObject>=0.6.0 URLObject>=0.6.0
wsgiref==0.1.2 wsgiref==0.1.2
...@@ -150,6 +154,7 @@ commands= ...@@ -150,6 +154,7 @@ commands=
python examples/runtests.py python examples/runtests.py
deps= deps=
django==1.2.4 django==1.2.4
django-staticfiles>=1.1.2
coverage==3.4 coverage==3.4
URLObject>=0.6.0 URLObject>=0.6.0
wsgiref==0.1.2 wsgiref==0.1.2
...@@ -165,6 +170,7 @@ commands= ...@@ -165,6 +170,7 @@ commands=
python examples/runtests.py python examples/runtests.py
deps= deps=
django==1.2.4 django==1.2.4
django-staticfiles>=1.1.2
coverage==3.4 coverage==3.4
URLObject>=0.6.0 URLObject>=0.6.0
wsgiref==0.1.2 wsgiref==0.1.2
......
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