From 9ea615af14303663a47fd155ab541d8302d609e4 Mon Sep 17 00:00:00 2001 From: donewell Date: Tue, 10 Feb 2015 17:41:03 +0000 Subject: [PATCH 001/487] add message to custom permission change detail to message and update text --- docs/api-guide/permissions.md | 10 ++++ rest_framework/views.py | 8 +++- tests/test_permissions.py | 86 +++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 743ca435..e299b418 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -205,6 +205,16 @@ If you need to test if a request is a read operation or a write operation, you s --- +Custom permissions will raise a `PermissionDenied` exception if the test fails. To change the error message associated with the exception, implement a `message` attribute directly on your custom permission. Otherwise the `default_detail` attribute from `PermissionDenied` will be used. + + from rest_framework import permissions + + class CustomerAccessPermission(permissions.BasePermission): + message = 'Adding customers not allowed.' + + def has_permission(self, request, view): + ... + ## Examples The following is an example of a permission class that checks the incoming request's IP address against a blacklist, and denies the request if the IP has been blacklisted. diff --git a/rest_framework/views.py b/rest_framework/views.py index bc870417..4fa97124 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -134,12 +134,14 @@ class APIView(View): """ raise exceptions.MethodNotAllowed(request.method) - def permission_denied(self, request): + def permission_denied(self, request, message=None): """ If request is not permitted, determine what kind of exception to raise. """ if not request.successful_authenticator: raise exceptions.NotAuthenticated() + if message is not None: + raise exceptions.PermissionDenied(message) raise exceptions.PermissionDenied() def throttled(self, request, wait): @@ -280,6 +282,8 @@ class APIView(View): """ for permission in self.get_permissions(): if not permission.has_permission(request, self): + if hasattr(permission, 'message'): + self.permission_denied(request, permission.message) self.permission_denied(request) def check_object_permissions(self, request, obj): @@ -289,6 +293,8 @@ class APIView(View): """ for permission in self.get_permissions(): if not permission.has_object_permission(request, self, obj): + if hasattr(permission, 'message'): + self.permission_denied(request, permission.message) self.permission_denied(request) def check_throttles(self, request): diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 97bac33d..68d34785 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -310,3 +310,89 @@ class ObjectPermissionsIntegrationTests(TestCase): response = object_permissions_list_view(request) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertListEqual(response.data, []) + + +class BasicPerm(permissions.BasePermission): + def has_permission(self, request, view): + return False + + +class BasicPermWithDetail(permissions.BasePermission): + message = 'Custom: You cannot post to this resource' + + def has_permission(self, request, view): + return False + + +class BasicObjectPerm(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + return False + + +class BasicObjectPermWithDetail(permissions.BasePermission): + message = 'Custom: You cannot post to this resource' + + def has_object_permission(self, request, view, obj): + return False + + +class PermissionInstanceView(generics.RetrieveUpdateDestroyAPIView): + queryset = BasicModel.objects.all() + serializer_class = BasicSerializer + + +class DeniedView(PermissionInstanceView): + permission_classes = (BasicPerm,) + + +class DeniedViewWithDetail(PermissionInstanceView): + permission_classes = (BasicPermWithDetail,) + + +class DeniedObjectView(PermissionInstanceView): + permission_classes = (BasicObjectPerm,) + + +class DeniedObjectViewWithDetail(PermissionInstanceView): + permission_classes = (BasicObjectPermWithDetail,) + +denied_view = DeniedView.as_view() + +denied_view_with_detail = DeniedViewWithDetail.as_view() + +denied_object_view = DeniedObjectView.as_view() + +denied_object_view_with_detail = DeniedObjectViewWithDetail.as_view() + + +class CustomPermissionsTests(TestCase): + def setUp(self): + BasicModel(text='foo').save() + User.objects.create_user('username', 'username@example.com', 'password') + credentials = basic_auth_header('username', 'password') + self.request = factory.get('/1', format='json', HTTP_AUTHORIZATION=credentials) + self.custom_message = 'Custom: You cannot post to this resource' + + def test_permission_denied(self): + response = denied_view(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertNotEqual(detail, self.custom_message) + + def test_permission_denied_with_custom_detail(self): + response = denied_view_with_detail(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, self.custom_message) + + def test_permission_denied_for_object(self): + response = denied_object_view(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertNotEqual(detail, self.custom_message) + + def test_permission_denied_for_object_with_custom_detail(self): + response = denied_object_view_with_detail(self.request, pk=1) + detail = response.data.get('detail') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(detail, self.custom_message) From 9a9a00bff2f61d08573f764012e389e7f8e5c6ae Mon Sep 17 00:00:00 2001 From: donewell Date: Wed, 11 Feb 2015 11:15:01 +0000 Subject: [PATCH 002/487] simplify argument handling --- rest_framework/views.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 4fa97124..435796c4 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -140,9 +140,7 @@ class APIView(View): """ if not request.successful_authenticator: raise exceptions.NotAuthenticated() - if message is not None: - raise exceptions.PermissionDenied(message) - raise exceptions.PermissionDenied() + raise exceptions.PermissionDenied(detail=message) def throttled(self, request, wait): """ @@ -282,9 +280,9 @@ class APIView(View): """ for permission in self.get_permissions(): if not permission.has_permission(request, self): - if hasattr(permission, 'message'): - self.permission_denied(request, permission.message) - self.permission_denied(request) + self.permission_denied( + request, message=getattr(permission, 'message', None) + ) def check_object_permissions(self, request, obj): """ @@ -293,9 +291,9 @@ class APIView(View): """ for permission in self.get_permissions(): if not permission.has_object_permission(request, self, obj): - if hasattr(permission, 'message'): - self.permission_denied(request, permission.message) - self.permission_denied(request) + self.permission_denied( + request, message=getattr(permission, 'message', None) + ) def check_throttles(self, request): """ From 3d25dadbf36930439f6cb5bfa81166c0ddcf0b38 Mon Sep 17 00:00:00 2001 From: donewell Date: Wed, 11 Feb 2015 11:20:03 +0000 Subject: [PATCH 003/487] change custom message for clarity --- tests/test_permissions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 68d34785..2c0caa48 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -318,7 +318,7 @@ class BasicPerm(permissions.BasePermission): class BasicPermWithDetail(permissions.BasePermission): - message = 'Custom: You cannot post to this resource' + message = 'Custom: You cannot access this resource' def has_permission(self, request, view): return False @@ -330,7 +330,7 @@ class BasicObjectPerm(permissions.BasePermission): class BasicObjectPermWithDetail(permissions.BasePermission): - message = 'Custom: You cannot post to this resource' + message = 'Custom: You cannot access this resource' def has_object_permission(self, request, view, obj): return False @@ -371,7 +371,7 @@ class CustomPermissionsTests(TestCase): User.objects.create_user('username', 'username@example.com', 'password') credentials = basic_auth_header('username', 'password') self.request = factory.get('/1', format='json', HTTP_AUTHORIZATION=credentials) - self.custom_message = 'Custom: You cannot post to this resource' + self.custom_message = 'Custom: You cannot access this resource' def test_permission_denied(self): response = denied_view(self.request, pk=1) From c20a0250dfbcd0b86ffd23f4657cf050ce6a2a2a Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Tue, 3 Mar 2015 14:34:06 +0300 Subject: [PATCH 004/487] add FilePathField, update docs --- docs/api-guide/fields.md | 14 ++++++++++++++ rest_framework/fields.py | 38 ++++++++++++++++++++++++++++++++++++- rest_framework/renderers.py | 5 ++++- tests/test_fields.py | 28 +++++++++++++++++++++++---- 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index f113bb23..1117fb8c 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -189,6 +189,20 @@ A field that ensures the input is a valid UUID string. The `to_internal_value` m "de305d54-75b4-431b-adb2-eb6b9e546013" +## FilePathField + +A field whose choices are limited to the filenames in a certain directory on the filesystem + +Corresponds to `django.forms.fields.FilePathField`. + +**Signature:** `FilePathField(path, match=None, recursive=False, allow_files=True, allow_folders=False, required=None, **kwargs)` + +- `path` - The absolute filesystem path to a directory from which this FilePathField should get its choice. +- `match` - A regular expression, as a string, that FilePathField will use to filter filenames. +- `recursive` - Specifies whether all subdirectories of path should be included. Default is `False`. +- `allow_files` - Specifies whether files in the specified location should be included. Default is `True`. Either this or `allow_folders` must be `True`. +- `allow_folders` - Specifies whether folders in the specified location should be included. Default is `False`. Either this or `allow_files` must be `True`. + --- # Numeric fields diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 13301f31..e99e6047 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -3,7 +3,9 @@ from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ValidationError as DjangoValidationError from django.core.validators import RegexValidator -from django.forms import ImageField as DjangoImageField +from django.forms import ( + ImageField as DjangoImageField, FilePathField as DjangoFilePathField +) from django.utils import six, timezone from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.encoding import is_protected_type, smart_text @@ -653,6 +655,40 @@ class UUIDField(Field): return str(value) +class FilePathField(CharField): + default_error_messages = { + 'invalid_choice': _('"{input}" is not a valid path choice.') + } + + def __init__(self, path, match=None, recursive=False, allow_files=True, + allow_folders=False, required=None, **kwargs): + super(FilePathField, self).__init__(**kwargs) + # create field and get options to avoid code duplication + field = DjangoFilePathField( + path, match=match, recursive=recursive, allow_files=allow_files, + allow_folders=allow_folders, required=required + ) + + self.choices = OrderedDict(field.choices) + self.choice_strings_to_values = { + six.text_type(key): key for key in self.choices.keys() + } + + def to_internal_value(self, data): + if data == '' and self.allow_blank: + return '' + + try: + return self.choice_strings_to_values[six.text_type(data)] + except KeyError: + self.fail('invalid_choice', input=data) + + def to_representation(self, value): + if value in ('', None): + return value + return self.choice_strings_to_values[six.text_type(value)] + + # Number types... class IntegerField(Field): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 920d2bc4..134b41b3 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -301,7 +301,10 @@ class HTMLFormRenderer(BaseRenderer): }, serializers.ListSerializer: { 'base_template': 'list_fieldset.html' - } + }, + serializers.FilePathField: { + 'base_template': 'select.html', + }, }) def render_field(self, field, parent_style): diff --git a/tests/test_fields.py b/tests/test_fields.py index 7f5f8102..3d2138a0 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,10 +1,12 @@ -from decimal import Decimal -from django.utils import timezone -from rest_framework import serializers import datetime +import os +import uuid +from decimal import Decimal + import django import pytest -import uuid +from django.utils import timezone +from rest_framework import serializers # Tests for field keyword arguments and core functionality. @@ -518,6 +520,24 @@ class TestUUIDField(FieldValues): field = serializers.UUIDField() +class TestFilePathField(FieldValues): + """ + Valid and invalid values for `FilePathField` + """ + + valid_inputs = { + __file__: __file__, + } + invalid_inputs = { + 'wrong_path': ['"wrong_path" is not a valid path choice.'] + } + outputs = { + } + field = serializers.FilePathField( + path=os.path.abspath(os.path.dirname(__file__)) + ) + + # Number types... class TestIntegerField(FieldValues): From 04cc1964bd84b78d328ecdd2735b884ea075a877 Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Tue, 3 Mar 2015 16:45:20 +0300 Subject: [PATCH 005/487] remove dict compehension --- rest_framework/fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 63fb0ae8..b6f049ea 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -672,9 +672,9 @@ class FilePathField(CharField): ) self.choices = OrderedDict(field.choices) - self.choice_strings_to_values = { - six.text_type(key): key for key in self.choices.keys() - } + self.choice_strings_to_values = dict([ + (six.text_type(key), key) for key in self.choices.keys() + ]) def to_internal_value(self, data): if data == '' and self.allow_blank: From e5d95e319e127c1da39f7b227abd0d00654f4b72 Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Tue, 3 Mar 2015 16:58:04 +0300 Subject: [PATCH 006/487] fix FilePathField kwargs for django < 1.5 --- rest_framework/fields.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index b6f049ea..f2e0d9a4 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -23,6 +23,7 @@ import collections import copy import datetime import decimal +import django import inspect import re import uuid @@ -665,11 +666,18 @@ class FilePathField(CharField): def __init__(self, path, match=None, recursive=False, allow_files=True, allow_folders=False, required=None, **kwargs): super(FilePathField, self).__init__(**kwargs) + # create field and get options to avoid code duplication - field = DjangoFilePathField( - path, match=match, recursive=recursive, allow_files=allow_files, - allow_folders=allow_folders, required=required - ) + if django.VERSION < (1, 5): + # django field doesn't have allow_folders, allow_files kwargs + field = DjangoFilePathField( + path, match=match, recursive=recursive, required=required + ) + else: + field = DjangoFilePathField( + path, match=match, recursive=recursive, allow_files=allow_files, + allow_folders=allow_folders, required=required + ) self.choices = OrderedDict(field.choices) self.choice_strings_to_values = dict([ From 8ba66e2c10803fa53a6c2b3d1bae19311bb797f3 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Fri, 20 Mar 2015 15:06:30 +0100 Subject: [PATCH 007/487] Document per-view versioning settings That is, document the default_version, allowed_version and version_param class variables. --- docs/api-guide/versioning.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/api-guide/versioning.md b/docs/api-guide/versioning.md index a227a4a3..795b46ca 100644 --- a/docs/api-guide/versioning.md +++ b/docs/api-guide/versioning.md @@ -72,6 +72,19 @@ The following settings keys are also used to control versioning: * `ALLOWED_VERSIONS`. If set, this value will restrict the set of versions that may be returned by the versioning scheme, and will raise an error if the provided version if not in this set. Defaults to `None`. * `VERSION_PARAMETER`. The string that should used for any versioning parameters, such as in the media type or URL query parameters. Defaults to `'version'`. +You can also set your versioning class plus those three values on a per-view or a per-viewset basis by defining your own versioning scheme and using the `default_version`, `allowed_versions` and `version_param` class variables. For example, if you want to use `URLPathVersioning`: + + from rest_framework.versioning import URLPathVersioning + from rest_framework.views import APIView + + class ExampleVersioning(URLPathVersioning): + default_version = ... + allowed_versions = ... + version_param = ... + + class ExampleView(APIVIew): + versioning_class = ExampleVersioning + --- # API Reference From b95ef25cbbb4e18dd25b169f79fb282b7f895dd1 Mon Sep 17 00:00:00 2001 From: Udi Oron Date: Thu, 26 Mar 2015 20:13:35 +0200 Subject: [PATCH 008/487] Wrong error message on bad field lookup --- rest_framework/utils/field_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index c97ec5d0..a3ebabfe 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -36,7 +36,7 @@ class ClassLookupDict(object): for cls in inspect.getmro(base_class): if cls in self.mapping: return self.mapping[cls] - raise KeyError('Class %s not found in lookup.', cls.__name__) + raise KeyError('Class %s not found in lookup.' % base_class.__name__) def __setitem__(self, key, value): self.mapping[key] = value From 1ee31735835a10564d33a7b9dfcf1935b9702487 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sat, 9 May 2015 14:37:49 +0300 Subject: [PATCH 009/487] Cache fields list for to_internal_value and to_representation. --- rest_framework/serializers.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8e1e50bc..dcee723c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -14,6 +14,7 @@ from __future__ import unicode_literals from django.db import models from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField from django.db.models import query +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import postgres_fields, unicode_to_repr from rest_framework.utils import model_meta @@ -314,6 +315,17 @@ class Serializer(BaseSerializer): self._fields[key] = value return self._fields + @cached_property + def _to_internal_value_fields(self): + return [ + field for field in self.fields.values() + if (not field.read_only) or (field.default is not empty) + ] + + @cached_property + def _to_representation_fields(self): + return [field for field in self.fields.values() if not field.write_only] + def get_fields(self): """ Returns a dictionary of {field_name: field_instance}. @@ -388,10 +400,7 @@ class Serializer(BaseSerializer): ret = OrderedDict() errors = OrderedDict() - fields = [ - field for field in self.fields.values() - if (not field.read_only) or (field.default is not empty) - ] + fields = self._to_internal_value_fields for field in fields: validate_method = getattr(self, 'validate_' + field.field_name, None) @@ -419,7 +428,7 @@ class Serializer(BaseSerializer): Object instance -> Dict of primitive datatypes. """ ret = OrderedDict() - fields = [field for field in self.fields.values() if not field.write_only] + fields = self._to_representation_fields for field in fields: try: From 478b1dbd06d748ecc22f9a4b769e8b305ea37983 Mon Sep 17 00:00:00 2001 From: Marco Vellinga Date: Mon, 11 May 2015 15:28:10 +0200 Subject: [PATCH 010/487] Use get_queryset() for RelatedField choices property instead of self.queryset.all() --- rest_framework/relations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 00a4a265..d4a63b83 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -109,7 +109,7 @@ class RelatedField(Field): six.text_type(self.to_representation(item)), six.text_type(item) ) - for item in self.queryset.all() + for item in self.get_queryset() ]) From 995aa475706a3df466aad7c136d715cd837a64e1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 May 2015 14:49:09 +0100 Subject: [PATCH 011/487] First pass templates for admin style --- rest_framework/pagination.py | 3 + rest_framework/renderers.py | 37 +++ .../templates/rest_framework/admin.html | 212 ++++++++++++++++++ .../rest_framework/admin/detail.html | 9 + .../templates/rest_framework/admin/list.html | 20 ++ 5 files changed, 281 insertions(+) create mode 100644 rest_framework/templates/rest_framework/admin.html create mode 100644 rest_framework/templates/rest_framework/admin/detail.html create mode 100644 rest_framework/templates/rest_framework/admin/list.html diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index b6be6b7c..b1c88d8d 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -200,6 +200,9 @@ class BasePagination(object): def to_html(self): # pragma: no cover raise NotImplementedError('to_html() must be implemented to display page controls.') + def get_results(self, data): + return data['results'] + class PageNumberPagination(BasePagination): """ diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 6c7cdf53..c439b1f2 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -668,6 +668,43 @@ class BrowsableAPIRenderer(BaseRenderer): return ret +class AdminRenderer(BrowsableAPIRenderer): + template = 'rest_framework/admin.html' + format = 'admin' + + def get_context(self, data, accepted_media_type, renderer_context): + """ + Render the HTML for the browsable API representation. + """ + context = super(AdminRenderer, self).get_context( + data, accepted_media_type, renderer_context + ) + + paginator = getattr(context['view'], 'paginator', None) + try: + results = data if paginator is None else paginator.get_results(data) + except KeyError: + results = data + + if isinstance(results, list): + header = results[0] + style = 'list' + else: + header = results + style = 'detail' + + columns = [key for key in header.keys() if key != 'url'] + details = [key for key in header.keys() if key != 'url'] + linked = [columns[0]] + + context['style'] = style + context['columns'] = columns + context['details'] = details + context['linked'] = linked + context['results'] = results + return context + + class MultiPartRenderer(BaseRenderer): media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' format = 'multipart' diff --git a/rest_framework/templates/rest_framework/admin.html b/rest_framework/templates/rest_framework/admin.html new file mode 100644 index 00000000..eec88f8a --- /dev/null +++ b/rest_framework/templates/rest_framework/admin.html @@ -0,0 +1,212 @@ +{% load url from future %} +{% load staticfiles %} +{% load rest_framework %} + + + + {% block head %} + + {% block meta %} + + + {% endblock %} + + {% block title %}Django REST framework{% endblock %} + + {% block style %} + {% block bootstrap_theme %} + + + {% endblock %} + + + {% endblock %} + + {% endblock %} + + + {% block body %} + + +
+ + {% block navbar %} + + {% endblock %} + +
+ {% block breadcrumbs %} + + {% endblock %} + + +
+ + {% if 'GET' in allowed_methods %} +
+
+
+ Format + + + +
+
+
+ {% endif %} + + {% if post_form %} + + {% endif %} + + {% if put_form %} + + {% endif %} + + {% if delete_form %} +
+ {% csrf_token %} + + +
+ {% endif %} + +
+ +
+ {% block description %} + {{ description }} + {% endblock %} +
+ + {% if paginator %} + + {% endif %} + +
+ {% if style == 'list' %} + {% include "rest_framework/admin/list.html" %} + {% else %} + {% include "rest_framework/admin/detail.html" %} + {% endif %} +
+ + {% if paginator %} + + {% endif %} +
+ +
+ +
+
+ + + + + + + + {% block script %} + + + + + {% endblock %} + + {% endblock %} + diff --git a/rest_framework/templates/rest_framework/admin/detail.html b/rest_framework/templates/rest_framework/admin/detail.html new file mode 100644 index 00000000..705c203e --- /dev/null +++ b/rest_framework/templates/rest_framework/admin/detail.html @@ -0,0 +1,9 @@ + + + {% for key, value in results.items %} + {% if key in details %} + + {% endif %} + {% endfor %} + +
{{ key|capfirst }}{{ value }}
diff --git a/rest_framework/templates/rest_framework/admin/list.html b/rest_framework/templates/rest_framework/admin/list.html new file mode 100644 index 00000000..c48a72b0 --- /dev/null +++ b/rest_framework/templates/rest_framework/admin/list.html @@ -0,0 +1,20 @@ + + + {% for column in columns%}{% endfor %} + + + {% for row in results %} + + {% for key, value in row.items %} + {% if key in columns %} + + {% endif %} + {% endfor %} + + {% endfor %} + +
{{ column|capfirst }}
+ {% if key in linked %}{% endif %} + {{ value }} + {% if key in linked %}{% endif %} +
From 9a504efd268db1ffeab42682bc4afd43e70707dc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 May 2015 15:21:49 +0100 Subject: [PATCH 012/487] Basic formatting for admin cells --- rest_framework/renderers.py | 4 ++-- .../templates/rest_framework/admin.html | 6 ++---- .../rest_framework/admin/detail.html | 3 ++- .../templates/rest_framework/admin/list.html | 3 ++- rest_framework/templatetags/rest_framework.py | 21 +++++++++++++++++++ 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index c439b1f2..93400c0b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -687,7 +687,7 @@ class AdminRenderer(BrowsableAPIRenderer): results = data if isinstance(results, list): - header = results[0] + header = results[0] if results else {} style = 'list' else: header = results @@ -695,7 +695,7 @@ class AdminRenderer(BrowsableAPIRenderer): columns = [key for key in header.keys() if key != 'url'] details = [key for key in header.keys() if key != 'url'] - linked = [columns[0]] + linked = [columns[0]] if columns else [] context['style'] = style context['columns'] = columns diff --git a/rest_framework/templates/rest_framework/admin.html b/rest_framework/templates/rest_framework/admin.html index eec88f8a..336f4315 100644 --- a/rest_framework/templates/rest_framework/admin.html +++ b/rest_framework/templates/rest_framework/admin.html @@ -73,11 +73,9 @@
- Format + Format -