diff --git a/dynamicforms/migrations/0014_auto_20180616_1812.py b/dynamicforms/migrations/0014_auto_20180616_1812.py new file mode 100644 index 0000000000000000000000000000000000000000..5933a33697e2811600c1ac8e51b7c4038a5efb20 --- /dev/null +++ b/dynamicforms/migrations/0014_auto_20180616_1812.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.6 on 2018-06-16 16:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamicforms', '0013_auto_20180616_0149'), + ] + + operations = [ + migrations.AlterField( + model_name='answer', + name='entry', + field=models.ForeignKey(help_text='Entrée associée à la réponse.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='dynamicforms.FormEntry', verbose_name='entrée'), + ), + migrations.AlterField( + model_name='question', + name='type', + field=models.CharField(choices=[('text-small', 'Texte court'), ('text-long', 'Texte long'), ('yes-no', 'Oui/Non'), ('date', 'Date'), ('sex', 'Sexe')], max_length=100, verbose_name='type de question'), + ), + ] diff --git a/dynamicforms/models.py b/dynamicforms/models.py index 8985fbd479e286caffc1707376cf551e52a52c69..fff267b995eec0900f193553d4095865ad3a9ace 100644 --- a/dynamicforms/models.py +++ b/dynamicforms/models.py @@ -141,6 +141,7 @@ class Answer(models.Model): entry = models.ForeignKey( 'FormEntry', on_delete=models.CASCADE, + null=True, related_name='answers', verbose_name='entrée', help_text="Entrée associée à la réponse.") diff --git a/dynamicforms/serializers.py b/dynamicforms/serializers.py index d1e77cf99ef1b20d6b6afbd8897688a89b5c68f7..eba1369ca1101c989fbacf84c4cdb322c9bd9d07 100644 --- a/dynamicforms/serializers.py +++ b/dynamicforms/serializers.py @@ -1,5 +1,6 @@ """Dynamic forms serializers.""" +from typing import List from rest_framework import serializers from .models import Form, Section, Question, Answer, FormEntry, File @@ -74,6 +75,21 @@ class FormEntrySerializer(serializers.ModelSerializer): ) answers = AnswerSerializer(many=True) + def create(self, validated_data: dict) -> FormEntry: + """Create a form entry for validated input data.""" + form = validated_data['form'] + answers: List[dict] = validated_data['answers'] + answer_serializer = AnswerSerializer() + + form_entry = FormEntry.objects.create(form=form) + + # Assign the newly created entry to each answer + for answer_data in answers: + answer = answer_serializer.create(answer_data) + form_entry.answers.add(answer) + + return form_entry + class Meta: # noqa model = FormEntry fields = ('id', 'form', 'submitted', 'answers',) diff --git a/projects/admin.py b/projects/admin.py index 55de80060423203f87cb87286bcd14b7ff5ceb05..341c647b5619dfe54598015d1b634cfe606cd231 100644 --- a/projects/admin.py +++ b/projects/admin.py @@ -85,4 +85,5 @@ class ParticipationAdmin(admin.ModelAdmin): list_display = ('user', 'edition', 'submitted', 'state') list_filter = ('edition', 'submitted', 'state',) + readonly_fields = ('submitted',) search_fields = ('user__first_name', 'user__last_name', 'user__email',) diff --git a/projects/migrations/0007_auto_20180616_1812.py b/projects/migrations/0007_auto_20180616_1812.py new file mode 100644 index 0000000000000000000000000000000000000000..7835d9d7f1539e21b13ba82aecd56e7a0d1be6fc --- /dev/null +++ b/projects/migrations/0007_auto_20180616_1812.py @@ -0,0 +1,30 @@ +# Generated by Django 2.0.6 on 2018-06-16 16:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dynamicforms', '0014_auto_20180616_1812'), + ('projects', '0006_auto_20180616_0145'), + ] + + operations = [ + migrations.AddField( + model_name='participation', + name='entry', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_participation', to='dynamicforms.FormEntry'), + ), + migrations.AlterField( + model_name='editionform', + name='edition', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='edition_form', to='projects.Edition', verbose_name='édition'), + ), + migrations.AlterField( + model_name='editionform', + name='form', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dynamicforms.Form', verbose_name="formulaire d'inscription"), + ), + ] diff --git a/projects/migrations/0008_auto_20180616_1919.py b/projects/migrations/0008_auto_20180616_1919.py new file mode 100644 index 0000000000000000000000000000000000000000..0a3740d95f0795c16037c58061be8fc61c505913 --- /dev/null +++ b/projects/migrations/0008_auto_20180616_1919.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.6 on 2018-06-16 17:19 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0007_auto_20180616_1812'), + ] + + operations = [ + migrations.AlterField( + model_name='participation', + name='entry', + field=models.OneToOneField(help_text="Réponses au formulaire d'inscription", null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_participation', to='dynamicforms.FormEntry', verbose_name='entrée'), + ), + ] diff --git a/projects/models.py b/projects/models.py index 559f3fa7075c51176df3b48712cdf53b991400ee..a29e38be85b4af202ecdc4a1fa71cd130ddf3ab1 100644 --- a/projects/models.py +++ b/projects/models.py @@ -180,7 +180,14 @@ class Participation(models.Model): auto_now_add=True, verbose_name='soumis le', help_text='Date de soumission de la participation') - # TODO link to the edition form? to a form entry? + entry = models.OneToOneField( + 'dynamicforms.FormEntry', + on_delete=models.CASCADE, + null=True, + related_name='project_participation', + verbose_name='entrée', + help_text="Réponses au formulaire d'inscription", + ) STATE_PENDING = 'pending' STATE_VALIDATED = 'valid' diff --git a/projects/serializers.py b/projects/serializers.py index 7b4fd6db1f1ddf60928e226e5736b6e86e3acf2f..7017c3e410ea270c6688fedfc3f50f5cf54661e0 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -1,12 +1,13 @@ """Projects serializers.""" +from django.db import transaction from rest_framework import serializers from core.fields import MarkdownField from users.fields import UserField from users.serializers import UserSerializer from profiles.serializers import TutorSerializer -from dynamicforms.serializers import FormDetailSerializer +from dynamicforms.serializers import FormDetailSerializer, FormEntrySerializer from .models import Edition, Participation, Project, EditionForm @@ -36,9 +37,26 @@ class ParticipationSerializer(serializers.ModelSerializer): 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',) + fields = ('id', 'submitted', 'user', 'edition', 'state', 'entry',) extra_kwargs = { 'state': { 'label': 'State of the participation.' diff --git a/projects/views.py b/projects/views.py index 55f34ad68e262de51ec0d46d69f54026d69cd275..6d5d1815d5d499a3705a810ee533638126a1331a 100644 --- a/projects/views.py +++ b/projects/views.py @@ -46,8 +46,25 @@ class ProjectViewSet(viewsets.ReadOnlyModelViewSet): "id": 1, "url": "http://localhost:8000/api/projects/1/", "name": "Oser la Prépa", - "description": "Oser la Prépa est un stage d'acclimatation aux Classes Préparatoires de deux semaines qui se déroule chaque été.", - "logo": null + "description": "Oser la Prépa est un stage d'acclimatation…", + "logo": null, + "editions": [ + { + "id": 1, + "url": "http://localhost:8000/api/editions/1/", + "name": "", + "year": 2018, + "project": 1, + "description": "", + "organizers": 0, + "participations": 2, + "edition_form": { + "id": 1, + "edition": 1, + "deadline": "2018-06-30" + } + } + ] } """ @@ -82,16 +99,15 @@ class EditionViewSet(viewsets.ReadOnlyModelViewSet): "url": "http://localhost:8000/api/editions/1/", "name": "", "year": 2018, - "project": { - "id": 1, - "url": "http://localhost:8000/api/projects/1/", - "name": "Oser la Prépa", - "description": "Oser la Prépa est un stage…", - "logo": null - }, + "project": 1, "description": "", "organizers": 0, - "participations": 1 + "participations": 2, + "edition_form": { + "id": 1, + "edition": 1, + "deadline": "2018-06-30" + } } ] @@ -99,42 +115,103 @@ class EditionViewSet(viewsets.ReadOnlyModelViewSet): Retrieve a specific edition. + Each `participation` in `participations` has the following format: + + { + "id": 3, + "submitted": "2018-06-07T00:31:37.947085+02:00", + "user": { + "id": 3, + "email": "john.doe@example.com", + "profile_type": null, + "first_name": "John", + "last_name": "Doe", + "gender": null, + "phone_number": "+33 6 12 34 56 78", + "date_of_birth": null, + "url": "http://localhost:8000/api/users/3/" + }, + "edition": 1, + "state": "valid" + } + ### Example response - [ - { + { + "id": 1, + "url": "http://localhost:8000/api/editions/1/", + "name": "", + "year": 2018, + "project": 1, + "description": "", + "organizers": [], + "participations": [], + "edition_form": { "id": 1, - "url": "http://localhost:8000/api/editions/1/", - "name": "", - "year": 2018, - "project": { - "id": 1, - "url": "http://localhost:8000/api/projects/1/", - "name": "Oser la Prépa", - "description": "Oser la Prépa est un stage…", - "logo": null + "edition": 1, + "deadline": "2018-06-30", + "form": { + "id": 2, + "url": "http://localhost:8000/api/forms/2/", + "slug": "inscriptions-a-oser-la-prepa-2018", + "title": "Inscription à Oser la Prépa 2018", + "entries_count": 1, + "sections": [ + { + "id": 1, + "title": "Enfant", + "form": 2, + "questions": [ + { + "id": 14, + "type": "text-small", + "text": "Nom", + "help_text": "", + "required": true, + "section": 1 + }, + ] + } + ], + "files": [ + { + "id": 1, + "name": "Autorisation parentale", + "file": "http://localhost:8000/file.pdf", + "form": 2 + } + ] }, - "description": "", - "organizers": [ ], - "participations": [ - { - "id": 3, - "submitted": "2018-06-07T00:31:37.947085+02:00", + "recipient": { "user": { - "id": 3, - "email": "john.doe@example.com", - "profile_type": null, - "first_name": "John", - "last_name": "Doe", - "gender": null, - "phone_number": "+33 6 12 34 56 78", - "date_of_birth": null, - "url": "http://localhost:8000/api/users/3/" - }, - "edition": 1, - "state": "valid" - }, - ] + "id": 3, + "email": "john.doe@example.com", + "profile_type": null, + "first_name": "John", + "last_name": "Doe", + "gender": null, + "phone_number": "+33 6 12 34 56 78", + "date_of_birth": null, + "url": "http://localhost:8000/api/users/3/" + }, + "address": { + "line1": "Rue de Rivoli", + "line2": "", + "post_code": "75001", + "city": "Paris", + "country": { + "code": "FR", + "name": "France" + } + }, + "promotion": 2020, + "tutoring_groups": [ + 1 + ], + "url": "http://localhost:8000/api/tutors/1/" + } + } + } """ queryset = Edition.objects.all()