From 0d16dc27587d5c023c7fb6068abccbb1d5986608 Mon Sep 17 00:00:00 2001 From: Steve Lacey Date: Thu, 23 Jun 2022 16:38:56 +0100 Subject: [PATCH] Strip legacy serialization and improve fields query param --- README.md | 2 +- pytest.ini | 2 +- tests/models.py | 27 ++++------- tests/serializers.py | 42 +++++++++++++++-- tests/test_validators.py | 1 - tests/test_views.py | 28 ++++++++++-- tests/views.py | 3 +- worf/exceptions.py | 9 ++-- worf/fields.py | 33 ++++++++++++-- worf/serializers.py | 98 ++++++++++++++++++++++++++-------------- worf/validators.py | 7 +-- worf/views/base.py | 92 ++++++++++++++++++------------------- worf/views/create.py | 10 ++-- worf/views/delete.py | 6 +-- worf/views/detail.py | 17 ++----- worf/views/list.py | 43 +++++------------- worf/views/update.py | 12 ++--- 17 files changed, 250 insertions(+), 182 deletions(-) diff --git a/README.md b/README.md index 8963c64..f28166d 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ Validators Validation handling can be found in `worf.validators`. -The basics come from `ValidationMixin` which `AbstractBaseAPI` inherits from, it +The basics come from `ValidateFields` which `AbstractBaseAPI` inherits from, it performs some coercion on `self.bundle`, potentially resulting in a different bundle than what was originally passed to the view. diff --git a/pytest.ini b/pytest.ini index ab8f965..8755fe8 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,7 @@ [pytest] addopts = --cov - --cov-fail-under 88 + --cov-fail-under 90 --cov-report term:skip-covered --cov-report html --no-cov-on-fail diff --git a/tests/models.py b/tests/models.py index b4e22b9..a913f30 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,3 +1,4 @@ +from hashlib import md5 from uuid import uuid4 from django.db import models @@ -26,31 +27,19 @@ class Profile(models.Model): recovery_email = models.EmailField(blank=True, max_length=320, null=True) recovery_phone = models.CharField(blank=True, max_length=32, null=True) + resume = models.FileField(upload_to="resumes/", blank=True) last_active = models.DateField(blank=True, null=True) created_at = models.DateTimeField(blank=True, null=True) - def api(self): - return dict(id=self.id, email=self.email, phone=self.phone) + def get_avatar_url(self): + return self.avatar.url if self.avatar else self.get_gravatar_url() - def api_update_fields(self): - return [ - "id", - "email", - "phone", + def get_gravatar_hash(self): + return md5(self.user.email.lower().encode()).hexdigest() - "boolean", - "integer", - "json", - "positive_integer", - "slug", - "small_integer", - - "recovery_email", - - "last_active", - "created_at", - ] + def get_gravatar_url(self, default="identicon", size=512): + return f"https://www.gravatar.com/avatar/{self.get_gravatar_hash()}?d={default}&s={size}" class Role(models.Model): diff --git a/tests/serializers.py b/tests/serializers.py index 0f5010e..8859e6f 100644 --- a/tests/serializers.py +++ b/tests/serializers.py @@ -7,6 +7,7 @@ class UserSerializer(Serializer): class Meta: fields = [ + "id", "username", "last_login", "date_joined", @@ -15,8 +16,9 @@ class Meta: class ProfileSerializer(Serializer): - username = fields.Function(lambda obj: obj.user.username) - email = fields.Function(lambda obj: obj.user.email) + username = fields.String(attribute="user.username") + email = fields.String(attribute="user.email") + avatar = fields.File(lambda profile: profile.get_avatar_url()) role = fields.Nested("RoleSerializer") skills = fields.Nested("RatedSkillSerializer", attribute="ratedskill_set", many=True) team = fields.Nested("TeamSerializer") @@ -25,21 +27,53 @@ class ProfileSerializer(Serializer): class Meta: fields = [ + "id", "username", + "email", + "phone", "avatar", + "boolean", + "integer", + "json", + "positive_integer", + "slug", + "small_integer", + "recovery_email", + "resume", + "role", + "skills", + "team", + "tags", + "user", + "last_active", + "created_at", + ] + writable = [ + "id", "email", "phone", + "avatar", + "boolean", + "integer", + "json", + "positive_integer", + "slug", + "small_integer", + "recovery_email", + "resume", "role", "skills", "team", "tags", "user", + "last_active", + "created_at", ] class RatedSkillSerializer(Serializer): - id = fields.Function(lambda obj: obj.skill.id) - name = fields.Function(lambda obj: obj.skill.name) + id = fields.Integer(attribute="skill.id") + name = fields.String(attribute="skill.name") class Meta: fields = ["id", "name", "rating"] diff --git a/tests/test_validators.py b/tests/test_validators.py index 5a8e5d0..a17d659 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -34,7 +34,6 @@ def profile_view_fixture(db, now, profile_factory): )) view.request = RequestFactory().patch(f"/{uuid}/") view.kwargs = dict(id=str(uuid)) - view.serializer = None return view diff --git a/tests/test_views.py b/tests/test_views.py index 416432c..0793e5d 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -146,13 +146,13 @@ def test_profile_multipart_create(mock_save, client, db, role, user): @patch('django.core.files.storage.FileSystemStorage.save') @pytest.mark.parametrize("method", ["PATCH", "PUT"]) def test_profile_multipart_update(mock_save, client, db, method, profile, role, user): - avatar = SimpleUploadedFile("avatar.jpg", b"", content_type="image/jpeg") - mock_save.return_value = "avatar.jpg" - payload = dict(avatar=avatar, role=role.pk, user=user.pk) + resume = SimpleUploadedFile("resume.pdf", b"", content_type="application/pdf") + mock_save.return_value = "resume.pdf" + payload = dict(resume=resume, role=role.pk, user=user.pk) response = client.generic(method, f"/profiles/{profile.pk}/", payload) result = response.json() assert response.status_code == 200, result - assert result["avatar"] == "/avatar.jpg" + assert result["resume"] == "/resume.pdf" assert result["role"]["id"] == role.pk assert result["role"]["name"] == role.name assert result["user"]["username"] == user.username @@ -285,17 +285,37 @@ def test_user_detail(client, db, user): response = client.get(f"/users/{user.pk}/") result = response.json() assert response.status_code == 200, result + assert result["id"] == user.pk assert result["username"] == user.username +def test_user_detail_fields(client, db, user): + response = client.get(f"/users/{user.pk}/?fields=username") + result = response.json() + assert response.status_code == 200, result + assert result == dict(username=user.username) + + def test_user_list(client, db, user): response = client.get("/users/") result = response.json() assert response.status_code == 200, result assert len(result["users"]) == 1 + assert result["users"][0]["id"] == user.pk assert result["users"][0]["username"] == user.username +def test_user_list_fields(client, db, user): + response = client.get("/users/?fields=username") + result = response.json() + assert response.status_code == 200, result + assert result["users"] == [dict(username=user.username)] + response = client.get("/users/?fields=invalid") + result = response.json() + assert response.status_code == 400, result + assert result == dict(message="Invalid fields: OrderedSet(['invalid'])") + + def test_user_list_filters(client, db, user_factory): january = "2021-01-01T00:00:00Z" february = "2021-02-01T00:00:00Z" diff --git a/tests/views.py b/tests/views.py index 70afb42..7c1fe6d 100644 --- a/tests/views.py +++ b/tests/views.py @@ -56,6 +56,7 @@ class UserList(CreateAPI, ListAPI): model = User ordering = ["pk"] serializer = UserSerializer(only=[ + "id", "username", "date_joined", "email", @@ -78,5 +79,5 @@ class UserList(CreateAPI, ListAPI): class UserDetail(UpdateAPI, DetailAPI): model = User - serializer = UserSerializer + serializer = UserSerializer(exclude=["date_joined"]) permissions = [PublicEndpoint] diff --git a/worf/exceptions.py b/worf/exceptions.py index 08fcbd4..67b8e9e 100644 --- a/worf/exceptions.py +++ b/worf/exceptions.py @@ -1,8 +1,7 @@ class HTTPException(Exception): def __init__(self, message=None): super().__init__(message) - if message is not None: - self.message = message + self.message = message or self.message class HTTP400(HTTPException): @@ -47,9 +46,13 @@ class NamingThingsError(ValueError): pass -class PermissionsException(Exception): +class PermissionsError(Exception): pass class NotImplementedInWorfYet(NotImplementedError): pass + + +class SerializerError(ValueError): + pass diff --git a/worf/fields.py b/worf/fields.py index e5825bf..e6d8c1e 100644 --- a/worf/fields.py +++ b/worf/fields.py @@ -1,22 +1,47 @@ -import marshmallow.fields +from marshmallow import fields, utils +from marshmallow.exceptions import ValidationError from marshmallow.fields import * # noqa: F401, F403 from django.db.models import Manager -class File(marshmallow.fields.Field): +class File(fields.Field): + _CHECK_ATTRIBUTE = False + + def __init__(self, serialize=None, deserialize=None, **kwargs): + super().__init__(**kwargs) + self.serialize_func = serialize and utils.callable_or_raise(serialize) + self.deserialize_func = deserialize and utils.callable_or_raise(deserialize) + def _serialize(self, value, attr, obj, **kwargs): + if self.serialize_func: + return self._call_or_raise(self.serialize_func, obj, attr) return value.url if value.name else None + # Worf serializers are not really used for deserialization yet, so no cover + + def _deserialize(self, value, attr, data, **kwargs): # pragma: no cover + if self.deserialize_func: + return self._call_or_raise(self.deserialize_func, value, attr) + return value + + def _call_or_raise(self, func, value, attr): + if len(utils.get_func_args(func)) > 1: # pragma: no cover + if self.parent.context is None: + msg = f"No context available for Function field {attr!r}" + raise ValidationError(msg) + return func(value, self.parent.context) + return func(value) + -class Nested(marshmallow.fields.Nested): +class Nested(fields.Nested): def _serialize(self, nested_obj, attr, obj, **kwargs): if isinstance(nested_obj, Manager): nested_obj = nested_obj.all() return super()._serialize(nested_obj, attr, obj, **kwargs) -class Pluck(marshmallow.fields.Pluck): +class Pluck(fields.Pluck): def _serialize(self, nested_obj, attr, obj, **kwargs): if isinstance(nested_obj, Manager): nested_obj = nested_obj.all() diff --git a/worf/serializers.py b/worf/serializers.py index 266028b..3c630be 100644 --- a/worf/serializers.py +++ b/worf/serializers.py @@ -3,9 +3,53 @@ from django.core.exceptions import ImproperlyConfigured from django.db.models.fields.files import FieldFile -from worf import fields # noqa: F401 -from worf.casing import snake_to_camel +from worf import fields +from worf.casing import camel_to_snake, snake_to_camel from worf.conf import settings +from worf.exceptions import SerializerError + + +class SerializeModels: + serializer = None + staff_serializer = None + + def get_serializer(self): + serializer = self.serializer + + if self.staff_serializer and self.request.user.is_staff: # pragma: no cover + serializer = self.staff_serializer + + if not serializer: # pragma: no cover + msg = f"{type(self).__name__}.get_serializer() did not return a serializer" + raise ImproperlyConfigured(msg) + + return serializer(**self.get_serializer_kwargs()) + + def get_serializer_context(self): + return {} + + def get_serializer_kwargs(self): + return dict( + context=dict(request=self.request, **self.get_serializer_context()), + only=self.get_serializer_only(), + ) + + def get_serializer_only(self): + only = self.bundle.get("fields") + if isinstance(only, str): + only = only.split(",") + if isinstance(only, list): + only = [".".join(map(camel_to_snake, field.split("."))) for field in only] + return only + + def load_serializer(self): + try: + return self.get_serializer() + except ValueError as e: + if str(e).startswith("Invalid fields"): + invalid_fields = str(e).partition(": ")[2].strip(".") + raise SerializerError(f"Invalid fields: {invalid_fields}") + raise e # pragma: no cover class SerializerOptions(marshmallow.SchemaOpts): @@ -46,13 +90,28 @@ class Serializer(marshmallow.Schema): } def __call__(self, **kwargs): + only = self.only + if self.only and kwargs.get("only"): + invalid_fields = set(kwargs.get("only")) - self.only + if invalid_fields: + raise SerializerError(f"Invalid fields: {invalid_fields}") + only = set(kwargs.get("only")) + elif kwargs.get("only"): + only = kwargs.get("only") + + exclude = self.exclude + if self.exclude and kwargs.get("exclude"): + exclude = self.exclude | set(kwargs.get("exclude")) + elif kwargs.get("exclude"): + exclude = kwargs.get("exclude") + return type(self)( context=kwargs.get("context", self.context), dump_only=kwargs.get("dump_only", self.dump_only), - exclude=kwargs.get("exclude", self.exclude), + exclude=exclude, load_only=kwargs.get("load_only", self.load_only), many=kwargs.get("many", self.many), - only=kwargs.get("only", self.only), + only=only, partial=kwargs.get("partial", self.partial), unknown=kwargs.get("unknown", self.unknown), ) @@ -70,39 +129,8 @@ def __repr__(self): def dict_class(self): return dict - def list(self, items): - return [self.read(item) for item in items] - def on_bind_field(self, field_name, field_obj): field_obj.data_key = snake_to_camel(field_obj.data_key or field_name) - def read(self, obj): - return self.dump(obj) - - def write(self): - return list(self.load_fields.keys()) - class Meta: ordered = True - - -class LegacySerializer: - def __init__(self, model_class, api_method): - self.api_method = api_method - self.model_class = model_class - - def __repr__(self): - return f'<{self.__class__.__name__}(model_class={self.model_class.__name__}, api_method="{self.api_method}")>' - - def list(self, items): - return [self.read(item) for item in items] - - def read(self, obj): - payload = getattr(obj, self.api_method)() - if not isinstance(payload, dict): - msg = f"{obj.__name__}.{self.api_method}() did not return a dictionary" - raise ImproperlyConfigured(msg) - return payload - - def write(self): - return getattr(self.model_class(), f"{self.api_method}_update_fields")() diff --git a/worf/validators.py b/worf/validators.py index 552f545..784b0c6 100644 --- a/worf/validators.py +++ b/worf/validators.py @@ -10,7 +10,7 @@ from worf.exceptions import NotImplementedInWorfYet -class ValidationMixin: +class ValidateFields: boolean_values = { "1": True, "0": False, @@ -159,10 +159,11 @@ def validate_bundle(self, key): We expect to set a fully validated bundle keys and values. """ - serializer = self.get_serializer() + serializer = self.load_serializer() + write_fields = list(serializer.load_fields.keys()) write_methods = ("PATCH", "POST", "PUT") - if self.request.method in write_methods and key not in serializer.write(): + if self.request.method in write_methods and key not in write_fields: message = f"{self.keymap[key]} is not editable" if settings.WORF_DEBUG: message += f":: {serializer}" diff --git a/worf/views/base.py b/worf/views/base.py index 5b0ee4f..8798185 100644 --- a/worf/views/base.py +++ b/worf/views/base.py @@ -17,10 +17,17 @@ from worf.casing import camel_to_snake, snake_to_camel from worf.conf import settings -from worf.exceptions import HTTP404, HTTP422, HTTP_EXCEPTIONS, PermissionsException +from worf.exceptions import ( + HTTP400, + HTTP404, + HTTP422, + HTTP_EXCEPTIONS, + PermissionsError, + SerializerError, +) from worf.renderers import render_response -from worf.serializers import LegacySerializer -from worf.validators import ValidationMixin +from worf.serializers import SerializeModels +from worf.validators import ValidateFields @method_decorator(never_cache, name="dispatch") @@ -44,12 +51,9 @@ def render_to_response(self, data=None, status_code=200): return render_response(self.request, data, status_code) -class AbstractBaseAPI(APIResponse, ValidationMixin): +class AbstractBaseAPI(APIResponse, SerializeModels, ValidateFields): model = None permissions = [] - api_method = "api" - serializer = None - staff_serializer = None payload_key = None def __init__(self, *args, **kwargs): @@ -85,6 +89,38 @@ def name(self): verbose_name_plural = self.model._meta.verbose_name_plural return snake_to_camel(verbose_name_plural.replace(" ", "_").lower()) + def dispatch(self, request, *args, **kwargs): + method = request.method.lower() + handler = self.http_method_not_allowed + + if method in self.http_method_names: + handler = getattr(self, method, self.http_method_not_allowed) + + try: + self._check_permissions() # only returns 200 or HTTP_EXCEPTIONS + self.set_bundle_from_request(request) + return handler(request, *args, **kwargs) # calls self.serialize() + except HTTP_EXCEPTIONS as e: + message = e.message + status = e.status + except ObjectDoesNotExist as e: + if self.model and not isinstance(e, self.model.DoesNotExist): + raise e + message = HTTP404.message + status = HTTP404.status + except RequestDataTooBig: + self.request._body = self.request.read(None) # prevent further raises + message = f"Max upload size is {filesizeformat(settings.DATA_UPLOAD_MAX_MEMORY_SIZE)}" + status = HTTP422.status + except SerializerError as e: + message = str(e) + status = HTTP400.status + except ValidationError as e: + message = e.message + status = HTTP422.status + + return self.render_to_response(dict(message=message), status) + def _check_permissions(self): """Return a permissions exception when in debug mode instead of 404.""" for method in self.permissions: @@ -94,7 +130,7 @@ def _check_permissions(self): continue if settings.WORF_DEBUG: - raise PermissionsException( + raise PermissionsError( "Permissions function {}.{} returned {}. You'd normally see a 404 here but WORF_DEBUG=True.".format( method.__module__, method.__name__, response ) @@ -115,20 +151,6 @@ def get_instance(self): def get_related_model(self, field): return self.model._meta.get_field(field).related_model - def get_serializer(self): - context = dict(request=self.request, **self.get_serializer_context()) - if self.staff_serializer and self.request.user.is_staff: - return self.staff_serializer(context=context) - if self.serializer: - return self.serializer(context=context) - if self.api_method: - return LegacySerializer(self.model, self.api_method) - msg = f"{type(self).__name__}.get_serializer() did not return a serializer" - raise ImproperlyConfigured(msg) - - def get_serializer_context(self): - return {} - def flatten_bundle(self, raw_bundle): # parse_qs gives us a dictionary where all values are lists return { @@ -191,29 +213,3 @@ def set_bundle_from_request_body(self, request): pass self.set_bundle(raw_bundle) - - def dispatch(self, request, *args, **kwargs): - method = request.method.lower() - handler = self.http_method_not_allowed - - if method in self.http_method_names: - handler = getattr(self, method, self.http_method_not_allowed) - - try: - self._check_permissions() # only returns 200 or HTTP_EXCEPTIONS - self.set_bundle_from_request(request) - return handler(request, *args, **kwargs) # calls self.serialize() - except HTTP_EXCEPTIONS as e: - return self.render_to_response(dict(message=e.message), e.status) - except ObjectDoesNotExist as e: - if self.model and not isinstance(e, self.model.DoesNotExist): - raise e - return self.render_to_response( - dict(message=HTTP404.message), HTTP404.status - ) - except RequestDataTooBig: - self.request._body = self.request.read(None) # prevent further raises - message = f"Max upload size is {filesizeformat(settings.DATA_UPLOAD_MAX_MEMORY_SIZE)}" - return self.render_to_response(dict(message=message), HTTP422.status) - except ValidationError as e: - return self.render_to_response(dict(message=e.message), HTTP422.status) diff --git a/worf/views/create.py b/worf/views/create.py index e5f4092..b10d662 100644 --- a/worf/views/create.py +++ b/worf/views/create.py @@ -5,7 +5,7 @@ class CreateAPI(AssignAttributes, AbstractBaseAPI): create_serializer = None - def create(self): + def create(self, *args, **kwargs): self.instance = self.new_instance() self.validate() self.save(self.instance, self.bundle) @@ -14,11 +14,13 @@ def create(self): def get_serializer(self): if self.create_serializer and self.request.method == "POST": - return self.create_serializer(context=self.get_serializer_context()) + return self.create_serializer(**self.get_serializer_kwargs()) return super().get_serializer() def new_instance(self): return self.model() - def post(self, request, *args, **kwargs): - return self.render_to_response(self.get_serializer().read(self.create()), 201) + def post(self, *args, **kwargs): + instance = self.create(*args, **kwargs) + result = self.load_serializer().dump(instance) + return self.render_to_response(result, 201) diff --git a/worf/views/delete.py b/worf/views/delete.py index ccc34e8..a1564c9 100644 --- a/worf/views/delete.py +++ b/worf/views/delete.py @@ -2,9 +2,9 @@ class DeleteAPI(AbstractBaseAPI): - def delete(self, request, *args, **kwargs): - self.destroy() + def delete(self, *args, **kwargs): + self.destroy(*args, **kwargs) return self.render_to_response("", 204) - def destroy(self): + def destroy(self, *args, **kwargs): self.get_instance().delete() diff --git a/worf/views/detail.py b/worf/views/detail.py index 28f44a7..b682add 100644 --- a/worf/views/detail.py +++ b/worf/views/detail.py @@ -1,5 +1,3 @@ -from django.core.exceptions import ImproperlyConfigured - from worf.lookups import FindInstance from worf.views.base import AbstractBaseAPI from worf.views.update import UpdateAPI @@ -8,24 +6,17 @@ class DetailAPI(FindInstance, AbstractBaseAPI): detail_serializer = None - def get(self, request, *args, **kwargs): + def get(self, *args, **kwargs): return self.render_to_response() def get_serializer(self): if self.detail_serializer and self.request.method == "GET": - return self.detail_serializer(context=self.get_serializer_context()) + return self.detail_serializer(**self.get_serializer_kwargs()) return super().get_serializer() def serialize(self): - """Return the model api, used for responses.""" - serializer = self.get_serializer() - payload = serializer.read(self.get_instance()) - if not isinstance(payload, dict): - raise ImproperlyConfigured(f"{serializer} did not return a dictionary") - return payload + return self.load_serializer().dump(self.get_instance()) class DetailUpdateAPI(UpdateAPI, DetailAPI): - def patch(self, request, *args, **kwargs): - self.update() - return self.get(request) + pass diff --git a/worf/views/list.py b/worf/views/list.py index 4c39058..359c967 100644 --- a/worf/views/list.py +++ b/worf/views/list.py @@ -61,7 +61,7 @@ def __init__(self, *args, **kwargs): ) self.search_fields = self.search_fields.get("or", []) - def get(self, request, *args, **kwargs): + def get(self, *args, **kwargs): return self.render_to_response() def _set_base_lookup_kwargs(self): @@ -175,7 +175,7 @@ def get_sort_field(self, field, descending=False): def get_serializer(self): if self.list_serializer and self.request.method == "GET": - return self.list_serializer(context=self.get_serializer_context()) + return self.list_serializer(**self.get_serializer_kwargs()) return super().get_serializer() def paginated_results(self): @@ -205,21 +205,10 @@ def paginated_results(self): except EmptyPage: return [] - def specific_fields(self, result): - fields = self.bundle.get("fields", []) - if fields: - return {key: value for key, value in result.items() if key in fields} - return result - def serialize(self): - serializer = self.get_serializer() + serializer = self.load_serializer() - payload = { - str(self.name): [ - self.specific_fields(serializer.read(instance)) - for instance in self.paginated_results() - ] - } + payload = {str(self.name): serializer(many=True).dump(self.paginated_results())} if self.per_page: payload.update( @@ -232,24 +221,14 @@ def serialize(self): } ) - if not settings.WORF_DEBUG: - return payload - - if not hasattr(self, "lookup_kwargs"): - # Debug throws an error in the event there are no lookup_kwargs - self.lookup_kwargs = {} - - payload.update( - { - "debug": { - "bundle": self.bundle, - "lookup_kwargs": self.lookup_kwargs, - "query": self.query, - "search_query": str(self.search_query), - "serializer": str(serializer).strip("<>"), - } + if settings.WORF_DEBUG: + payload["debug"] = { + "bundle": self.bundle, + "lookup_kwargs": getattr(self, "lookup_kwargs", {}), + "query": self.query, + "search_query": str(self.search_query), + "serializer": str(serializer).strip("<>"), } - ) return payload diff --git a/worf/views/update.py b/worf/views/update.py index 83fe093..b31d25a 100644 --- a/worf/views/update.py +++ b/worf/views/update.py @@ -8,18 +8,18 @@ class UpdateAPI(AssignAttributes, FindInstance, AbstractBaseAPI): def get_serializer(self): if self.update_serializer and self.request.method in ("PATCH", "PUT"): - return self.update_serializer(context=self.get_serializer_context()) + return self.update_serializer(**self.get_serializer_kwargs()) return super().get_serializer() - def patch(self, request, *args, **kwargs): - self.update() + def patch(self, *args, **kwargs): + self.update(*args, **kwargs) return self.render_to_response() - def put(self, request, *args, **kwargs): - self.update() + def put(self, *args, **kwargs): + self.update(*args, **kwargs) return self.render_to_response() - def update(self): + def update(self, *args, **kwargs): instance = self.get_instance() self.validate() self.save(instance, self.bundle)