diff --git a/AUTHORS.rst b/AUTHORS.rst index 7a5b8b4..a77c023 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -12,4 +12,4 @@ django-virtual-models is maintained by [Vinta Software](https://www.vintasoftwar Contributors ------------ -None yet. Why not be the first? +* Jackson Alves Sousa (https://github.com/jackson541/) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f6fbbcc..11fe636 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -69,7 +69,7 @@ this is how you set up your fork for local development:: 4. Install the project and the dev requirements:: - $ pip install -e .[doc,dev,test] + $ pip install -e ".[doc,dev,test]" 5. Install pre-commit checks:: diff --git a/django_virtual_models/serializers.py b/django_virtual_models/serializers.py index 2cfc0a5..5492aa9 100644 --- a/django_virtual_models/serializers.py +++ b/django_virtual_models/serializers.py @@ -74,7 +74,9 @@ def get_optimized_queryset(self, initial_queryset): "Using virtual models on %(cls_name)s. Finding lookup_list...", {"cls_name": cls_name}, ) - virtual_model_instance = virtual_model(user=self.get_request_user()) + virtual_model_instance = virtual_model( + user=self.get_request_user(), serializer_context=self.context + ) lookup_list = serializer_optimization.LookupFinder( serializer_instance=self, virtual_model=virtual_model_instance, diff --git a/docs/tutorial.md b/docs/tutorial.md index b2b9754..b2d9e15 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -314,8 +314,8 @@ Imagine you have a `UserMovieRating` model that relates users with movies and ha ```python class UserMovieRating(models.Model): - user = models.ForeignKey("User") - movie = models.ForeignKey("Movie", related_name="ratings") + user = models.ForeignKey("User", on_delete=models.CASCADE) + movie = models.ForeignKey("Movie", related_name="ratings", on_delete=models.CASCADE) rating = models.DecimalField(max_digits=3, decimal_places=1) ``` @@ -367,6 +367,51 @@ class VirtualUserMovieRating(v.VirtualModel): !!! warning An advice: in general, you should avoid returning data relative to the current user in HTTP APIs, as this makes caching very hard or even impossible. Use this only if you really need it, as in a request that's only specific for users like user profile pages. Avoid nesting data related to the current user inside global data. Consider adding an additional request to fetch data relative to the current user, and then "hydrate" the previous request data on the frontend. + +### Using Serializer's context +Similar to how `user` param works, it's possible to use in virtual models any data that the view passes to the serializer by context. + +Using the previous example, suppose you want to get the vote count equal to a specific value that is defined in the view. To do this, use the serializer context: + +```python +class MovieListView(v.VirtualModelListAPIView): + serializer_class = MovieSerializer + queryset = Movie.objects.all() + + def get_serializer_context(self): + context = super().get_serializer_context() + vote_value = 5 + return {**context, 'vote_value': vote_value} +``` + +Then, annotate the number of votes in the Virtual Model with the `serializer_context` param: + +```python +class VirtualMovie(v.VirtualModel): + voting_count = v.Annotation( + lambda qs, user, serializer_context, **kwargs: qs.annotate( + voting_count=Count("ratings", filter=Q(ratings__rating=serializer_context['vote_value'])) + ) + ) + + class Meta: + model = Movie +``` + +And, to reflect the change, add the new field in `MovieSerializer`: + +```python +class MovieSerializer(v.VirtualModelSerializer): + voting_count = serializers.IntegerField() + + class Meta: + model = Movie + virtual_model = VirtualMovie + fields = ["name", "voting_count"] +``` + +Just like the `user` param, `serializer_context` can be used in the `get_prefetch_queryset` method. + ### Ignoring a serializer field If you have a serializer field that fetches data from somewhere other than your Django models, you cannot prefetch data for it with a Virtual Model. So you need to make the Virtual Model ignore that field. diff --git a/tests/virtual_model_serializers/__init__.py b/tests/virtual_model_serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/virtual_model_serializers/test_virtual_model_serializers.py b/tests/virtual_model_serializers/test_virtual_model_serializers.py new file mode 100644 index 0000000..90f2e97 --- /dev/null +++ b/tests/virtual_model_serializers/test_virtual_model_serializers.py @@ -0,0 +1,45 @@ +from unittest.mock import MagicMock + +from django.test import TestCase + +from rest_framework import serializers + +from model_bakery import baker + +import django_virtual_models as v + +from ..virtual_models.models import Course, User + + +class VirtualModelSerializerTest(TestCase): + def test_pass_serializer_context_to_virtual_model(self): + class MockedVirtualCourse(v.VirtualModel): + something = v.Annotation( + lambda qs, user, **kwargs: qs.annotate_something(user=user, **kwargs) + ) + + class Meta: + model = Course + deferred_fields = ["name", "description", "something"] + + class CourseSerializer(v.VirtualModelSerializer): + something = serializers.CharField() + + class Meta: + model = Course + virtual_model = MockedVirtualCourse + fields = ["name", "description", "something"] + + user = baker.make(User) + user.is_anonymous = False + request = MagicMock() + request.method = "GET" + request.user = user + serializer_context = {"request": request, "value": 12345} + mock_qs = MagicMock() + virtual_course_serializer = CourseSerializer(instance=None, context=serializer_context) + virtual_course_serializer.get_optimized_queryset(mock_qs) + + mock_qs.annotate_something.assert_called_once_with( + user=user, serializer_context=serializer_context + )