diff --git a/projects/factory.py b/projects/factory.py index 7df4ee01405ea46db64e827510985e290a18ae8c..f826ab8d36261eb8a99acd51cdcfb2a725f1fcfb 100644 --- a/projects/factory.py +++ b/projects/factory.py @@ -4,7 +4,7 @@ import factory import factory.django from users.factory import UserFactory -from .models import Project, Edition, Participation +from .models import Project, Edition, Participation, EditionForm class ProjectFactory(factory.DjangoModelFactory): @@ -37,6 +37,14 @@ class EditionFactory(factory.DjangoModelFactory): return project and project or ProjectFactory.create() +class EditionFormFactory(factory.DjangoModelFactory): + + class Meta: # noqa + model = EditionForm + + deadline = factory.Faker('future_date') + + class ParticipationFactory(factory.DjangoModelFactory): """Participation object factory.""" diff --git a/projects/serializers.py b/projects/serializers.py index 7017c3e410ea270c6688fedfc3f50f5cf54661e0..6f7033d73dd2cae83667df89f726628e8021b2ef 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -4,12 +4,12 @@ from django.db import transaction from rest_framework import serializers from core.fields import MarkdownField +from dynamicforms.serializers import FormDetailSerializer, FormEntrySerializer +from profiles.serializers import TutorSerializer from users.fields import UserField from users.serializers import UserSerializer -from profiles.serializers import TutorSerializer -from dynamicforms.serializers import FormDetailSerializer, FormEntrySerializer -from .models import Edition, Participation, Project, EditionForm +from .models import Edition, EditionForm, Participation, Project class ProjectSerializer(serializers.HyperlinkedModelSerializer): @@ -25,53 +25,20 @@ class ProjectSerializer(serializers.HyperlinkedModelSerializer): } -class ParticipationSerializer(serializers.ModelSerializer): - """Serializer for project edition participations.""" - - user = UserField( - label='Utilisateur', - help_text='Identifier for the user who participates.') - - edition = serializers.PrimaryKeyRelatedField( - queryset=Edition.objects.all(), - label='Édition', - help_text='Identifier for the associated edition.') - - entry = FormEntrySerializer(write_only=True) - - def create(self, validated_data: dict) -> Participation: - """Explicitly create as entry is a nested serializer.""" - with transaction.atomic(): - entry_data = validated_data['entry'] - entry = FormEntrySerializer().create(entry_data) - - participation = Participation.objects.create( - user=validated_data['user'], - edition=validated_data['edition'], - state=Participation.STATE_PENDING, - entry=entry, - ) - - return participation - - class Meta: # noqa - model = Participation - fields = ('id', 'submitted', 'user', 'edition', 'state', 'entry',) - extra_kwargs = { - 'state': { - 'label': 'State of the participation.' - } - } - - class EditionFormSerializer(serializers.ModelSerializer): """Serializer for edition form objects.""" edition = serializers.PrimaryKeyRelatedField(read_only=True) + title = serializers.SerializerMethodField() + + def get_title(self, obj) -> str: + """Return the form's title if form is set.""" + form = getattr(obj, 'form', None) + return form and str(form) or None class Meta: # noqa model = EditionForm - fields = ('id', 'edition', 'deadline') + fields = ('id', 'title', 'edition', 'deadline') class EditionFormDetailSerializer(EditionFormSerializer): @@ -92,6 +59,7 @@ class EditionListSerializer(serializers.HyperlinkedModelSerializer): organizers = serializers.SerializerMethodField() participations = serializers.SerializerMethodField() edition_form = EditionFormSerializer() + participates = serializers.SerializerMethodField() def get_organizers(self, obj: Edition) -> int: """Return the number of organizers.""" @@ -101,15 +69,71 @@ class EditionListSerializer(serializers.HyperlinkedModelSerializer): """Return the number of participations.""" return obj.participations.count() + def get_participates(self, obj: Edition) -> bool: + """Return whether the current user participates in the edition.""" + request = self.context['request'] + if not request.user: + return False + return request.user.pk in obj.participations.values_list('user__pk') + class Meta: # noqa model = Edition fields = ('id', 'url', 'name', 'year', 'project', 'description', - 'organizers', 'participations', 'edition_form',) + 'organizers', 'participations', 'edition_form', + 'participates',) extra_kwargs = { 'url': {'view_name': 'api:edition-detail'}, } +class ParticipationSerializer(serializers.ModelSerializer): + """Serializer for project edition participations.""" + + user = UserField( + label='Utilisateur', + help_text='Identifier for the user who participates.') + + edition_id = serializers.PrimaryKeyRelatedField( + source='edition', + queryset=Edition.objects.all(), + label='Édition', + help_text='Identifier for the associated edition.') + + edition_form_title = serializers.SerializerMethodField() + + entry = FormEntrySerializer(write_only=True) + + def get_edition_form_title(self, obj: Participation) -> str: + form = getattr(obj.edition, 'edition_form', None) + return form and str(form) or None + + def create(self, validated_data: dict) -> Participation: + """Explicitly create as entry is a nested serializer.""" + with transaction.atomic(): + entry_data = validated_data['entry'] + entry = FormEntrySerializer().create(entry_data) + + participation = Participation.objects.create( + user=validated_data['user'], + edition=validated_data['edition'], + state=Participation.STATE_PENDING, + entry=entry, + ) + + return participation + + class Meta: # noqa + model = Participation + fields = ('id', 'submitted', 'user', + 'edition_id', 'edition_form_title', + 'state', 'entry',) + extra_kwargs = { + 'state': { + 'label': 'State of the participation.' + } + } + + class EditionDetailSerializer(EditionListSerializer): """Detail serializer for Edition objects.""" diff --git a/projects/views.py b/projects/views.py index 6d5d1815d5d499a3705a810ee533638126a1331a..6d9548be770e8084f49749c82a82c2c07fb3d2ca 100644 --- a/projects/views.py +++ b/projects/views.py @@ -1,6 +1,10 @@ """Projects views.""" +from django.utils.timezone import now from rest_framework import mixins, permissions, viewsets +from rest_framework.response import Response +from rest_framework.decorators import action +from django_filters.rest_framework.backends import DjangoFilterBackend from django_filters import rest_framework as filters @@ -214,16 +218,50 @@ class EditionViewSet(viewsets.ReadOnlyModelViewSet): } """ - queryset = Edition.objects.all() + queryset = Edition.objects.all().prefetch_related( + 'participations', 'organizers' + ) permission_classes = (permissions.IsAuthenticated,) filter_backends = (filters.DjangoFilterBackend,) filter_fields = ('project', 'year',) def get_serializer_class(self): - if self.action == 'list': - return EditionListSerializer - elif self.action == 'retrieve': + if self.action == 'retrieve': return EditionDetailSerializer + return EditionListSerializer + + @action(methods=['get'], detail=False) + def open_registrations(self, request, **kwargs): + """Return a list of the editions with open registrations. + + These are the editions that have an edition form set and whose + deadline is a future date. + + ### Example response + + [ + { + "id": 1, + "url": "http://localhost:8000/api/editions/1/", + "name": "", + "year": 2018, + "project": 1, + "description": "", + "organizers": 0, + "participations": 3, + "edition_form": { + "id": 1, + "edition": 1, + "deadline": "2018-06-30" + } + } + ] + """ + queryset = self.get_queryset().filter( + edition_form__isnull=False, + edition_form__deadline__gte=now().date()) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) class ParticipationViewSet(mixins.CreateModelMixin, @@ -251,7 +289,8 @@ class ParticipationViewSet(mixins.CreateModelMixin, "date_of_birth": null, "url": "http://localhost:8000/api/users/3/" }, - "edition": 1, + "edition_id": 1, + "edition_form_title": "Inscriptions à Oser la Prépa 2018" "state": "valid" }, ] @@ -276,7 +315,8 @@ class ParticipationViewSet(mixins.CreateModelMixin, "date_of_birth": null, "url": "http://localhost:8000/api/users/3/" }, - "edition": 1, + "edition_id": 1, + "edition_form_title": "Inscriptions à Oser la Prépa 2018" "state": "valid" } """ @@ -284,3 +324,5 @@ class ParticipationViewSet(mixins.CreateModelMixin, queryset = Participation.objects.all() serializer_class = ParticipationSerializer permission_classes = (permissions.IsAuthenticated,) + filter_backends = (DjangoFilterBackend,) + filter_fields = ('user', 'state',) diff --git a/tests/test_projects/test_edition_api.py b/tests/test_projects/test_edition_api.py index f8c5dc1680dabbf6ceebbbc82238734f5ce3ff59..ff59f407491264f7f5203e3390bb90bbd34d2613 100644 --- a/tests/test_projects/test_edition_api.py +++ b/tests/test_projects/test_edition_api.py @@ -3,7 +3,7 @@ from rest_framework import status from tests.utils import SimpleAPITestCase, logged_in -from projects.factory import EditionFactory +from projects.factory import EditionFactory, EditionFormFactory class EditionEndpointsTest(SimpleAPITestCase): @@ -13,7 +13,7 @@ class EditionEndpointsTest(SimpleAPITestCase): read_expected_fields = {'id', 'url', 'name', 'year', 'project', 'description', 'organizers', 'participations', - 'edition_form',} + 'edition_form', 'participates'} def setUp(self): self.factory.create_batch(3) @@ -51,3 +51,12 @@ class EditionEndpointsTest(SimpleAPITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) fields = set(response.data) self.assertSetEqual(fields, self.read_expected_fields) + + @logged_in + def test_list_open_registrations(self): + edition = self.factory.create() + EditionFormFactory.create(edition=edition) + url = '/api/editions/open_registrations/' + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) diff --git a/tests/test_projects/test_participation_api.py b/tests/test_projects/test_participation_api.py index 75422d1795db8c2f9a656416eca968eb878b2947..83781452e648c07a231ba23f30ca668c45171549 100644 --- a/tests/test_projects/test_participation_api.py +++ b/tests/test_projects/test_participation_api.py @@ -13,7 +13,7 @@ class ParticipationReadTest(SimpleAPITestCase): factory = ParticipationFactory - read_expected_fields = {'id', 'user', 'edition', + read_expected_fields = {'id', 'user', 'edition_id', 'edition_form_title', 'state', 'submitted'} def setUp(self): @@ -66,7 +66,7 @@ class ParticipationCreateTest(SimpleAPITestCase): } payload = { 'user': user.pk, - 'edition': edition.pk, + 'edition_id': edition.pk, 'entry': entry, } return self.client.post('/api/project-participations/', @@ -80,5 +80,7 @@ class ParticipationCreateTest(SimpleAPITestCase): def test_returns_expected_fields(self): response = self.perform_create() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - expected = {'id', 'user', 'edition', 'state', 'submitted'} + expected = { + 'id', 'user', 'edition_id', 'edition_form_title', + 'state', 'submitted'} self.assertSetEqual(expected, set(response.data))