From da8187d2c293e095969c00751c21338d53c7f3f2 Mon Sep 17 00:00:00 2001 From: Camille Harang Date: Wed, 1 Feb 2012 23:56:54 +0100 Subject: [PATCH 01/22] * -> ** --- djangorestframework/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index c7b32d2d2..f4a9c998a 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -608,7 +608,7 @@ class UpdateModelMixin(ModelMixin): # 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_instance(*query_kwargs) + self.model_instance = self.get_instance(**query_kwargs) for (key, val) in self.CONTENT.items(): setattr(self.model_instance, key, val) From bd25e99f155b68e31239bf1c9e3d4e70d53bbddd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 2 Feb 2012 09:05:28 +0000 Subject: [PATCH 02/22] Python docs say inherit from Exception, not BaseException. --- djangorestframework/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/response.py b/djangorestframework/response.py index 96345cee2..13f0477ed 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. From 87ef85587dd58afa80ef2955c819c974edaa2cfc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 2 Feb 2012 16:31:40 +0000 Subject: [PATCH 03/22] Update AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 2466de043..7fae45784 100644 --- a/AUTHORS +++ b/AUTHORS @@ -29,6 +29,7 @@ Benoit C Chris Pickett Ben Timby Michele Lazzeri +Camille Harang THANKS TO: From 15fc26f50b94d41d1024a3f40fe21af5f2d07bfb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 7 Feb 2012 08:58:15 +0000 Subject: [PATCH 04/22] Fix up packaging and staticfiles changes. Fixes #155. Fixes #153. Fixes #150. --- CHANGELOG.rst | 1 + MANIFEST.in | 4 +- .../static/css/djangorestframework.css | 57 ++++++++++++ djangorestframework/templates/api_login.html | 92 +++++++++---------- setup.py | 83 ++++++++++++----- 5 files changed, 161 insertions(+), 76 deletions(-) mode change 100644 => 100755 setup.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9da9df2e1..d80eb93f4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ development - Easier to override. Won't conflict with customised admin styles (eg grappelli) * Drop implied 'pk' filter if last arg in urlconf is unnamed. - Too magical. Explict is better than implicit. +* Tider setup.py * Bugfixes: - Bug with PerUserThrottling when user contains unicode chars. diff --git a/MANIFEST.in b/MANIFEST.in index fc9ce9769..5c6a1c578 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ -recursive-include djangorestframework/static *.ico *.txt +recursive-include djangorestframework/static *.ico *.txt *.css recursive-include djangorestframework/templates *.txt *.html recursive-include examples .keep *.py *.txt recursive-include docs *.py *.rst *.html *.txt -include AUTHORS LICENSE requirements.txt tox.ini +include AUTHORS LICENSE CHANGELOG.rst requirements.txt tox.ini diff --git a/djangorestframework/static/css/djangorestframework.css b/djangorestframework/static/css/djangorestframework.css index 8fc4bace5..1e75b8e81 100644 --- a/djangorestframework/static/css/djangorestframework.css +++ b/djangorestframework/static/css/djangorestframework.css @@ -1129,6 +1129,58 @@ fieldset.monospace textarea { 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 { @@ -1147,6 +1199,11 @@ fieldset.monospace textarea { } /* 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 750f898bb..016a4e109 100644 --- a/djangorestframework/templates/api_login.html +++ b/djangorestframework/templates/api_login.html @@ -1,54 +1,44 @@ +{% load static %} - - {% if ADMIN_MEDIA_PREFIX %} - - - - {% else %} - - - - {% endif %} - - - -
- - + diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 690a7e0fd..919806297 --- a/setup.py +++ b/setup.py @@ -1,33 +1,70 @@ -#!/usr/bin/env/python +#!/usr/bin/env python # -*- coding: utf-8 -*- -from setuptools import setup +from distutils.core import setup +import re +import os +import sys -import os, re -path = os.path.join(os.path.dirname(__file__), 'djangorestframework', '__init__.py') -init_py = open(path).read() -VERSION = re.match("__version__ = '([^']+)'", init_py).group(1) +def get_version(package): + """ + Return package version as listed in `__version__` in `init.py`. + """ + init_py = open(os.path.join(package, '__init__.py')).read() + return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) + + +def get_packages(package): + """ + Return root package and all sub-packages. + """ + return [dirpath + for dirpath, dirnames, filenames in os.walk(package) + if os.path.exists(os.path.join(dirpath, '__init__.py'))] + + +def get_package_data(package): + """ + Return all files under the root package, that are not in a + package themselves. + """ + walk = [(dirpath.replace(package + os.sep, '', 1), filenames) + for dirpath, dirnames, filenames in os.walk(package) + if not os.path.exists(os.path.join(dirpath, '__init__.py'))] + + filepaths = [] + for base, filenames in walk: + filepaths.extend([os.path.join(base, filename) + for filename in filenames]) + return {package: filepaths} + + +version = get_version('djangorestframework') + + +if sys.argv[-1] == 'publish': + os.system("python setup.py sdist upload") + print "You probably want to also tag the version now:" + print " git tag -a %s -m 'version %s'" % (version, version) + print " git push --tags" + sys.exit() + setup( - name = 'djangorestframework', - version = VERSION, - url = 'http://django-rest-framework.org', - download_url = 'http://pypi.python.org/pypi/djangorestframework/', - license = 'BSD', - description = 'A lightweight REST framework for Django.', - author = 'Tom Christie', - author_email = 'tom@tomchristie.com', - packages = ['djangorestframework', - 'djangorestframework.templatetags', - 'djangorestframework.tests', - 'djangorestframework.runtests', - 'djangorestframework.utils'], - package_dir={'djangorestframework': 'djangorestframework'}, - package_data = {'djangorestframework': ['templates/*', 'static/*']}, - test_suite = 'djangorestframework.runtests.runcoverage.main', + name='djangorestframework', + version=version, + url='http://django-rest-framework.org', + download_url='http://pypi.python.org/pypi/djangorestframework/', + license='BSD', + description='A lightweight REST framework for Django.', + author='Tom Christie', + author_email='tom@tomchristie.com', + packages=get_packages('djangorestframework'), + package_data=get_package_data('djangorestframework'), + test_suite='djangorestframework.runtests.runcoverage.main', install_requires=['URLObject>=0.6.0'], - classifiers = [ + classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Framework :: Django', From ee2c8606af597820f146df164775937d7e62c811 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 7 Feb 2012 09:29:48 +0000 Subject: [PATCH 05/22] Tweaks. --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9cec01cf2..520f7fc6a 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,11 @@ Django REST framework ===================== -Django REST framework makes it easy to build well-connected, self-describing RESTful Web APIs. +**Django REST framework makes it easy to build well-connected, self-describing RESTful Web APIs.** +**Author:** Tom Christie. `Follow me on twitter `. + +Overview +======== Features: From 25b6718111b8f0ef30cdd40cdca125591972ae5f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 7 Feb 2012 09:30:08 +0000 Subject: [PATCH 06/22] Tweak --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 520f7fc6a..0d5608c19 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,8 @@ Django REST framework ===================== **Django REST framework makes it easy to build well-connected, self-describing RESTful Web APIs.** -**Author:** Tom Christie. `Follow me on twitter `. + +**Author:** Tom Christie. `Follow me on twitter `_. Overview ======== From 15edc88e70279ef46920da0ff5a9d289c69465f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 7 Feb 2012 09:31:08 +0000 Subject: [PATCH 07/22] Tweak --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0d5608c19..23a8075e8 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Django REST framework **Django REST framework makes it easy to build well-connected, self-describing RESTful Web APIs.** -**Author:** Tom Christie. `Follow me on twitter `_. +**Author:** Tom Christie. `Follow me on Twitter `_. Overview ======== From 304c99598346c7168e951bcd9f144b9adb537914 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 7 Feb 2012 09:47:15 +0000 Subject: [PATCH 08/22] Fix distutils errors --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 919806297..8bc44c0e5 100755 --- a/setup.py +++ b/setup.py @@ -62,8 +62,7 @@ setup( author_email='tom@tomchristie.com', packages=get_packages('djangorestframework'), package_data=get_package_data('djangorestframework'), - test_suite='djangorestframework.runtests.runcoverage.main', - install_requires=['URLObject>=0.6.0'], + requires=['URLObject>=0.6.0'], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', From 8c1e6beb85c55d6327a582fd87f60c92419a2697 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 7 Feb 2012 09:55:01 +0000 Subject: [PATCH 09/22] Distutils, you are a harsh master. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8bc44c0e5..ef2e7362c 100755 --- a/setup.py +++ b/setup.py @@ -62,7 +62,7 @@ setup( author_email='tom@tomchristie.com', packages=get_packages('djangorestframework'), package_data=get_package_data('djangorestframework'), - requires=['URLObject>=0.6.0'], + requires=['URLObject (>=0.6.0)'], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', From 6207791af1702ec4a80d689b39ca4e4d1c9d8157 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 7 Feb 2012 10:14:34 +0000 Subject: [PATCH 10/22] Attempt to stop banging head against brick wall. --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ef2e7362c..5cd2a28fa 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from distutils.core import setup +from setuptools import setup import re import os import sys @@ -62,7 +62,8 @@ setup( author_email='tom@tomchristie.com', packages=get_packages('djangorestframework'), package_data=get_package_data('djangorestframework'), - requires=['URLObject (>=0.6.0)'], + test_suite='djangorestframework.runtests.runcoverage.main', + install_requires=['URLObject>=0.6.0'], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', From 0f49f9b2b5c4e36c6edbb9b135e5653bdec46ce2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 7 Feb 2012 11:01:43 +0000 Subject: [PATCH 11/22] In examples settings, use staticfiles for 1.2, django.contrib.staticfiles otherwise --- examples/settings.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/settings.py b/examples/settings.py index a8846b1b3..5ff9fd0f3 100644 --- a/examples/settings.py +++ b/examples/settings.py @@ -1,4 +1,5 @@ # Settings for djangorestframework examples project +import django import os DEBUG = True @@ -84,19 +85,17 @@ TEMPLATE_DIRS = ( # Don't forget to use absolute paths, not relative paths. ) -# for loading initial data -##SERIALIZATION_MODULES = { - # 'yml': "django.core.serializers.pyyaml" - -#} - +if django.VERSION < (1, 3): + staticfiles = 'staticfiles' +else: + staticfiles = 'django.contrib.staticfiles' INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', - 'django.contrib.staticfiles', + staticfiles, 'django.contrib.messages', 'djangorestframework', From 76a7d35813b637bb199a0d388468f9265f8adaf2 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Tue, 7 Feb 2012 11:08:55 +0000 Subject: [PATCH 12/22] Ensure duplicate "page" parameters are not created Previously, URLObject.add_query_param was used to generate next/previous page links in PaginatorMixin. This resulted in (for example) page 2's "next" link having the params: ?page=2&page=3 Instead, URLObject.set_query_param should be used to replace the current value of the "page" parameter. --- djangorestframework/mixins.py | 2 +- djangorestframework/tests/mixins.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index f4a9c998a..836c3a594 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -679,7 +679,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/tests/mixins.py b/djangorestframework/tests/mixins.py index a7512efc7..8268fdca7 100644 --- a/djangorestframework/tests/mixins.py +++ b/djangorestframework/tests/mixins.py @@ -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']) From cbac9244ac93f50210aaf73f626366804581ee9b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 7 Feb 2012 11:22:47 +0000 Subject: [PATCH 13/22] Use app for Django 1.2 --- examples/urls.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/urls.py b/examples/urls.py index bd4087fdd..33297b550 100644 --- a/examples/urls.py +++ b/examples/urls.py @@ -1,7 +1,10 @@ -from django.conf.urls.defaults import patterns, include, url -from django.conf import settings +from django.conf.urls.defaults import patterns, include from sandbox.views import Sandbox -from django.contrib.staticfiles.urls import staticfiles_urlpatterns +try: + from django.contrib.staticfiles.urls import staticfiles_urlpatterns +except ImportError: # Django <= 1.2 + from staticfiles.urls import staticfiles_urlpatterns + urlpatterns = patterns('', (r'^$', Sandbox.as_view()), From bc80eb266f071e0c090fcf882722d4dd056ccf61 Mon Sep 17 00:00:00 2001 From: Camille Harang Date: Sat, 11 Feb 2012 01:49:28 +0100 Subject: [PATCH 14/22] DjangoModelPermisson --- djangorestframework/permissions.py | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index dfe55ce94..100a976e1 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -89,6 +89,46 @@ class IsUserOrIsAnonReadOnly(BasePermission): raise _403_FORBIDDEN_RESPONSE +class DjangoModelPermisson(BasePermission): + """ + """ + + def check_permission(self, user): + + # GET-style methods are always allowed. + if self.view.request.method in ('GET', 'OPTIONS', 'HEAD',): + return + + # User must be logged in to check permissions. + if not hasattr(self.view.request, 'user') or not self.view.request.user.is_authenticated(): + raise _403_FORBIDDEN_RESPONSE + + klass = self.view.resource.model + + # If it doesn't look like a model, we can't check permissions. + if not klass or not getattr(klass, '_meta', None): + return + + permission_map = { + 'POST': ['%s.add_%s'], + 'PUT': ['%s.change_%s'], + 'DELETE': ['%s.delete_%s'], + 'PATCH': ['%s.add_%s', '%s.change_%s', '%s.delete_%s'], + } + permission_codes = [] + + # If we don't recognize the HTTP method, we don't know what + # permissions to check. Deny. + if self.view.request.method not in permission_map: + raise _403_FORBIDDEN_RESPONSE + + for perm in permission_map[self.view.request.method]: + permission_codes.append(perm % (klass._meta.app_label, klass._meta.module_name)) + + if not self.view.request.user.has_perms(permission_codes): + raise _403_FORBIDDEN_RESPONSE + + class BaseThrottle(BasePermission): """ Rate throttling of requests. From b236241982b95a35cdb251e5020004050fb6567a Mon Sep 17 00:00:00 2001 From: Camille Harang Date: Sat, 11 Feb 2012 01:54:28 +0100 Subject: [PATCH 15/22] check authentication after checking ModelResource --- djangorestframework/permissions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index 100a976e1..92e90fc38 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -99,16 +99,16 @@ class DjangoModelPermisson(BasePermission): if self.view.request.method in ('GET', 'OPTIONS', 'HEAD',): return - # User must be logged in to check permissions. - if not hasattr(self.view.request, 'user') or not self.view.request.user.is_authenticated(): - raise _403_FORBIDDEN_RESPONSE - klass = self.view.resource.model # If it doesn't look like a model, we can't check permissions. if not klass or not getattr(klass, '_meta', None): return + # User must be logged in to check permissions. + if not hasattr(self.view.request, 'user') or not self.view.request.user.is_authenticated(): + raise _403_FORBIDDEN_RESPONSE + permission_map = { 'POST': ['%s.add_%s'], 'PUT': ['%s.change_%s'], From 963d2ecccbe30ca231621f85681049983248d08d Mon Sep 17 00:00:00 2001 From: Camille Harang Date: Sat, 11 Feb 2012 02:02:42 +0100 Subject: [PATCH 16/22] DjangoModelPermisson's desc --- djangorestframework/permissions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index 92e90fc38..cf556dd69 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -91,6 +91,8 @@ class IsUserOrIsAnonReadOnly(BasePermission): class DjangoModelPermisson(BasePermission): """ + The request is authenticated against the Django user's permissions on the + `Resource`'s `Model`, if the resource is a `ModelResource`. """ def check_permission(self, user): From 88561a4ee2762409810b1aa7f85bda923169b69d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 11 Feb 2012 13:00:38 +0000 Subject: [PATCH 17/22] Fix up DjangoModelPermissions. --- djangorestframework/permissions.py | 64 +++++++++++++++--------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index cf556dd69..7c61251de 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -89,45 +89,47 @@ class IsUserOrIsAnonReadOnly(BasePermission): raise _403_FORBIDDEN_RESPONSE -class DjangoModelPermisson(BasePermission): +class DjangoModelPermission(BasePermission): """ The request is authenticated against the Django user's permissions on the - `Resource`'s `Model`, if the resource is a `ModelResource`. + `Resource`'s `Model`. + + This permission should only be used on views with a `ModelResource`. """ - def check_permission(self, user): + # Map methods into required permission codes. + # Override this if you need to also provide 'read' permissions, + # or other custom behaviour. + perms_map = { + 'GET': [], + 'OPTIONS': [], + 'HEAD': [], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + } - # GET-style methods are always allowed. - if self.view.request.method in ('GET', 'OPTIONS', 'HEAD',): - return - - klass = self.view.resource.model - - # If it doesn't look like a model, we can't check permissions. - if not klass or not getattr(klass, '_meta', None): - return - - # User must be logged in to check permissions. - if not hasattr(self.view.request, 'user') or not self.view.request.user.is_authenticated(): - raise _403_FORBIDDEN_RESPONSE - - permission_map = { - 'POST': ['%s.add_%s'], - 'PUT': ['%s.change_%s'], - 'DELETE': ['%s.delete_%s'], - 'PATCH': ['%s.add_%s', '%s.change_%s', '%s.delete_%s'], + def get_required_permissions(self, method, model_cls): + """ + Given a model and an HTTP method, return the list of permission + codes that the user is required to have. + """ + kwargs = { + 'app_label': model_cls._meta.app_label, + 'model_name': model_cls.__name__.lower() } - permission_codes = [] + try: + return [perm % kwargs for perm in self.perms_map[method]] + except KeyError: + ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED) - # If we don't recognize the HTTP method, we don't know what - # permissions to check. Deny. - if self.view.request.method not in permission_map: - raise _403_FORBIDDEN_RESPONSE + def check_permission(self, user): + method = self.view.method + model_cls = self.view.resource.model + perms = self.get_required_permissions(method, model_cls) - for perm in permission_map[self.view.request.method]: - permission_codes.append(perm % (klass._meta.app_label, klass._meta.module_name)) - - if not self.view.request.user.has_perms(permission_codes): + if not user.has_perms(perms): raise _403_FORBIDDEN_RESPONSE From 2c11fd68f8d57b3675940d4d5bf04f815fe521a6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 11 Feb 2012 17:48:35 +0000 Subject: [PATCH 18/22] Minor name change --- djangorestframework/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index 7c61251de..d47ba3dc9 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -89,7 +89,7 @@ class IsUserOrIsAnonReadOnly(BasePermission): raise _403_FORBIDDEN_RESPONSE -class DjangoModelPermission(BasePermission): +class DjangoModelPermissions(BasePermission): """ The request is authenticated against the Django user's permissions on the `Resource`'s `Model`. From cb8d94b956c5a39f15c54f7662bdbd2275ee3e4d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 11 Feb 2012 18:29:24 +0000 Subject: [PATCH 19/22] Improve docstring on DjangoModelPermissions, and also ensure the user is authenticated. --- djangorestframework/permissions.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index d47ba3dc9..de24e23b7 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -91,15 +91,18 @@ class IsUserOrIsAnonReadOnly(BasePermission): class DjangoModelPermissions(BasePermission): """ - The request is authenticated against the Django user's permissions on the - `Resource`'s `Model`. + The request is authenticated using `django.contrib.auth` permissions. + See: https://docs.djangoproject.com/en/dev/topics/auth/#permissions - This permission should only be used on views with a `ModelResource`. + It ensures that the user is authenticated, and has the appropriate + `add`/`change`/`delete` permissions on the model. + + This permission should only be used on views with a `ModelResource`. """ # Map methods into required permission codes. # Override this if you need to also provide 'read' permissions, - # or other custom behaviour. + # or if you want to provide custom permisson codes. perms_map = { 'GET': [], 'OPTIONS': [], @@ -117,7 +120,7 @@ class DjangoModelPermissions(BasePermission): """ kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': model_cls.__name__.lower() + 'model_name': model_cls._meta.module_name } try: return [perm % kwargs for perm in self.perms_map[method]] @@ -129,7 +132,7 @@ class DjangoModelPermissions(BasePermission): model_cls = self.view.resource.model perms = self.get_required_permissions(method, model_cls) - if not user.has_perms(perms): + if not user.is_authenticated or not user.has_perms(perms): raise _403_FORBIDDEN_RESPONSE From 24911f37e47d7350b33d43342ce7662504e634df Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 11 Feb 2012 18:38:45 +0000 Subject: [PATCH 20/22] Updated CHANGELOG. 'Added DjangoModelPermissions'. --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d80eb93f4..010bf6c01 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,11 +4,12 @@ Release Notes development ----------- -* Saner template variable autoescaping. +* Added DjangoModelPermissions class to support `django.contrib.auth` style permissions. * 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. +* Saner template variable autoescaping. * Tider setup.py * Bugfixes: - Bug with PerUserThrottling when user contains unicode chars. From 1ec165f38c508d7ac4c158ec8d558c5d8f1bd15b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 11 Feb 2012 18:43:58 +0000 Subject: [PATCH 21/22] `OPTIONS` is also a safe method. --- djangorestframework/permissions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index de24e23b7..2d6d89226 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -20,6 +20,8 @@ __all__ = ( 'PerResourceThrottling' ) +SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] + _403_FORBIDDEN_RESPONSE = ErrorResponse( status.HTTP_403_FORBIDDEN, @@ -84,8 +86,7 @@ class IsUserOrIsAnonReadOnly(BasePermission): def check_permission(self, user): if (not user.is_authenticated() and - self.view.method != 'GET' and - self.view.method != 'HEAD'): + self.view.method not in SAFE_METHODS): raise _403_FORBIDDEN_RESPONSE From ba1e3b46998254e12578d75428669751e105735b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 11 Feb 2012 21:15:06 +0000 Subject: [PATCH 22/22] Fix typo. --- djangorestframework/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index 2d6d89226..03d78c2ea 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -103,7 +103,7 @@ class DjangoModelPermissions(BasePermission): # Map methods into required permission codes. # Override this if you need to also provide 'read' permissions, - # or if you want to provide custom permisson codes. + # or if you want to provide custom permission codes. perms_map = { 'GET': [], 'OPTIONS': [],