Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strip legacy serialization and improve fields query param #96

Merged
merged 1 commit into from
Jun 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -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
Expand Down
27 changes: 8 additions & 19 deletions tests/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from hashlib import md5
from uuid import uuid4

from django.db import models
Expand Down Expand Up @@ -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):
Expand Down
42 changes: 38 additions & 4 deletions tests/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class UserSerializer(Serializer):

class Meta:
fields = [
"id",
"username",
"last_login",
"date_joined",
Expand All @@ -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")
Expand All @@ -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"]
Expand Down
1 change: 0 additions & 1 deletion tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
28 changes: 24 additions & 4 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class UserList(CreateAPI, ListAPI):
model = User
ordering = ["pk"]
serializer = UserSerializer(only=[
"id",
"username",
"date_joined",
"email",
Expand All @@ -78,5 +79,5 @@ class UserList(CreateAPI, ListAPI):

class UserDetail(UpdateAPI, DetailAPI):
model = User
serializer = UserSerializer
serializer = UserSerializer(exclude=["date_joined"])
permissions = [PublicEndpoint]
9 changes: 6 additions & 3 deletions worf/exceptions.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -47,9 +46,13 @@ class NamingThingsError(ValueError):
pass


class PermissionsException(Exception):
class PermissionsError(Exception):
pass


class NotImplementedInWorfYet(NotImplementedError):
pass


class SerializerError(ValueError):
pass
33 changes: 29 additions & 4 deletions worf/fields.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
Loading