diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 2db6ce1e3..719ac1eff 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -90,12 +90,17 @@ This permission is suitable if you want to your API to allow read permissions to ## DjangoModelPermissions -This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. When applied to a view that has a `.model` property, authorization will only be granted if the user has the relevant model permissions assigned. +This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. When applied to a view that has a `.model` property, authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. * `POST` requests require the user to have the `add` permission on the model. * `PUT` and `PATCH` requests require the user to have the `change` permission on the model. * `DELETE` requests require the user to have the `delete` permission on the model. +If you want to use `DjangoModelPermissions` but also allow unauthenticated users to have read permission, override the class and set the `authenticated_users_only` property to `False`. For example: + + class HasModelPermissionsOrReadOnly(DjangoModelPermissions): + authenticated_users_only = False + The default behaviour can also be overridden to support custom model permissions. For example, you might want to include a `view` model permission for `GET` requests. To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details. diff --git a/docs/topics/credits.md b/docs/topics/credits.md index bdd3e27eb..b0f0cfa28 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -109,6 +109,7 @@ The following people have helped make REST framework great. * Wiliam Souza - [waa] * Jonas Braun - [iekadou] * Ian Dash - [bitmonkey] +* Bouke Haarsma - [bouke] * Pierre Dulac - [dulaccc] Many thanks to everyone who's contributed to the project. @@ -253,4 +254,5 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [waa]: https://github.com/wiliamsouza [iekadou]: https://github.com/iekadou [bitmonkey]: https://github.com/bitmonkey +[bouke]: https://github.com/bouke [dulaccc]: https://github.com/dulaccc diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index eb4d378eb..ff487826f 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,6 +44,8 @@ You can determine your currently installed version using `pip freeze`: * Filtering backends are now applied to the querysets for object lookups as well as lists. (Eg you can use a filtering backend to control which objects should 404) * Deal with error data nicely when deserializing lists of objects. +* Extra override hook to configure `DjangoModelPermissions` for unauthenticated users. +* Bugfix: Fix pk relationship bug for some types of 1-to-1 relations. * Bugfix: Workaround for Django bug causing case where `Authtoken` could be registered for cascade delete from `User` even if not installed. ### 2.2.3 diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index c477474c9..92f8215ac 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -104,6 +104,8 @@ class DjangoModelPermissions(BasePermission): 'DELETE': ['%(app_label)s.delete_%(model_name)s'], } + authenticated_users_only = True + def get_required_permissions(self, method, model_cls): """ Given a model and an HTTP method, return the list of permission @@ -117,13 +119,18 @@ class DjangoModelPermissions(BasePermission): def has_permission(self, request, view): model_cls = getattr(view, 'model', None) - if not model_cls: - return True + queryset = getattr(view, 'queryset', None) + + if model_cls is None and queryset is not None: + model_cls = queryset.model + + assert model_cls, ('Cannot apply DjangoModelPermissions on a view that' + ' does not have `.model` or `.queryset` property.') perms = self.get_required_permissions(request.method, model_cls) if (request.user and - request.user.is_authenticated() and + (request.user.is_authenticated() or not self.authenticated_users_only) and request.user.has_perms(perms)): return True return False diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 0c108717f..2a10e9af5 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -235,7 +235,6 @@ class PrimaryKeyRelatedField(RelatedField): pk = getattr(obj, self.source or field_name).pk except ObjectDoesNotExist: return None - return self.to_native(obj.pk) # Forward relationship return self.to_native(pk) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 106e3f17a..2ae7c215f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -391,11 +391,17 @@ class BaseSerializer(Field): return self._data + def save_object(self, obj): + obj.save() + def save(self): """ Save the deserialized object and return it. """ - self.object.save() + if isinstance(self.object, list): + [self.save_object(item) for item in self.object] + else: + self.save_object(self.object) return self.object @@ -612,11 +618,11 @@ class ModelSerializer(Serializer): if instance: return self.full_clean(instance) - def save(self): + def save_object(self, obj): """ Save the deserialized object and return it. """ - self.object.save() + obj.save() if getattr(self, 'm2m_data', None): for accessor_name, object_list in self.m2m_data.items(): @@ -628,8 +634,6 @@ class ModelSerializer(Serializer): setattr(self.object, accessor_name, object_list) self.related_data = {} - return self.object - class HyperlinkedModelSerializerOptions(ModelSerializerOptions): """ diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index d6ae31760..f08e18086 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -407,14 +407,14 @@ class PKNullableOneToOneTests(TestCase): target.save() new_target = OneToOneTarget(name='target-2') new_target.save() - source = NullableOneToOneSource(name='source-1', target=target) + source = NullableOneToOneSource(name='source-1', target=new_target) source.save() def test_reverse_foreign_key_retrieve_with_null(self): queryset = OneToOneTarget.objects.all() serializer = NullableOneToOneTargetSerializer(queryset, many=True) expected = [ - {'id': 1, 'name': 'target-1', 'nullable_source': 1}, - {'id': 2, 'name': 'target-2', 'nullable_source': None}, + {'id': 1, 'name': 'target-1', 'nullable_source': None}, + {'id': 2, 'name': 'target-2', 'nullable_source': 1}, ] self.assertEqual(serializer.data, expected)