aboutsummaryrefslogtreecommitdiffstats
path: root/djangorestframework
diff options
context:
space:
mode:
authorAlen Mujezinovic2012-02-09 11:13:42 +0000
committerAlen Mujezinovic2012-02-09 11:13:42 +0000
commitadd5f32e8a9ba530591c23b8123f36468595fd88 (patch)
tree7d4794e487614c9928d5f31420dfb423350cbff6 /djangorestframework
parentdd680d7a0ae997f2e562db383f23b56624e7ea98 (diff)
parentc5691cca0e5b61b6cf866f5b8085f950e4637f5a (diff)
downloaddjango-rest-framework-add5f32e8a9ba530591c23b8123f36468595fd88.tar.bz2
Merge remote branch 'tomchristie/master'
Diffstat (limited to 'djangorestframework')
-rw-r--r--djangorestframework/__init__.py2
-rw-r--r--djangorestframework/mixins.py66
-rw-r--r--djangorestframework/permissions.py2
-rw-r--r--djangorestframework/response.py2
-rw-r--r--djangorestframework/runtests/settings.py8
-rw-r--r--djangorestframework/static/css/djangorestframework.css1209
-rw-r--r--djangorestframework/templates/api_login.html90
-rw-r--r--djangorestframework/templates/renderer.html31
-rw-r--r--djangorestframework/templates/renderer.txt4
-rw-r--r--djangorestframework/tests/mixins.py13
-rw-r--r--djangorestframework/views.py7
11 files changed, 1309 insertions, 125 deletions
diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py
index 55f92cc4..0aaa2915 100644
--- a/djangorestframework/__init__.py
+++ b/djangorestframework/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '0.3.2-dev'
+__version__ = '0.3.3-dev'
VERSION = __version__ # synonym
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 70492d84..cb0b913a 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -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:
diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py
index 945023ce..dfe55ce9 100644
--- a/djangorestframework/permissions.py
+++ b/djangorestframework/permissions.py
@@ -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
diff --git a/djangorestframework/response.py b/djangorestframework/response.py
index 96345cee..13f0477e 100644
--- a/djangorestframework/response.py
+++ b/djangorestframework/response.py
@@ -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.
diff --git a/djangorestframework/runtests/settings.py b/djangorestframework/runtests/settings.py
index 07855fa7..f54a554b 100644
--- a/djangorestframework/runtests/settings.py
+++ b/djangorestframework/runtests/settings.py
@@ -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
diff --git a/djangorestframework/static/css/djangorestframework.css b/djangorestframework/static/css/djangorestframework.css
new file mode 100644
index 00000000..1e75b8e8
--- /dev/null
+++ b/djangorestframework/static/css/djangorestframework.css
@@ -0,0 +1,1209 @@
+/********************** 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;
+}
+
+body.login {
+ background: #eee;
+}
+
+.login #container {
+ background: white;
+ border: 1px solid #ccc;
+ width: 28em;
+ min-width: 300px;
+ margin-left: auto;
+ margin-right: auto;
+ margin-top: 100px;
+}
+
+.login #content-main {
+ width: 100%;
+}
+
+.login form {
+ margin-top: 1em;
+}
+
+.login .form-row {
+ padding: 4px 0;
+ float: left;
+ width: 100%;
+}
+
+.login .form-row label {
+ float: left;
+ width: 9em;
+ padding-right: 0.5em;
+ line-height: 2em;
+ text-align: right;
+ font-size: 1em;
+ color: #333;
+}
+
+.login .form-row #id_username, .login .form-row #id_password {
+ width: 14em;
+}
+
+.login span.help {
+ font-size: 10px;
+ display: block;
+}
+
+.login .submit-row {
+ clear: both;
+ padding: 1em 0 0 9.4em;
+}
+
+/* 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;
+}
+
+.form-row {
+ border-bottom: 0.25em !important;
+}
diff --git a/djangorestframework/templates/api_login.html b/djangorestframework/templates/api_login.html
index 750f898b..016a4e10 100644
--- a/djangorestframework/templates/api_login.html
+++ b/djangorestframework/templates/api_login.html
@@ -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>
- </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>
+ <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>
-
- <br class="clear">
- </div>
-
- <div id="footer"></div>
-
-</div>
-</body>
+ </div>
+ </body>
</html>
diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html
index ff761023..e396a58f 100644
--- a/djangorestframework/templates/renderer.html
+++ b/djangorestframework/templates/renderer.html
@@ -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 }}
diff --git a/djangorestframework/templates/renderer.txt b/djangorestframework/templates/renderer.txt
index 5be8c117..b584952c 100644
--- a/djangorestframework/templates/renderer.txt
+++ b/djangorestframework/templates/renderer.txt
@@ -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 %}
diff --git a/djangorestframework/tests/mixins.py b/djangorestframework/tests/mixins.py
index 88b13dd5..8268fdca 100644
--- a/djangorestframework/tests/mixins.py
+++ b/djangorestframework/tests/mixins.py
@@ -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'])
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 88d81d25..32d2437c 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -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):
"""