Skip to content
Snippets Groups Projects
Commit 27d618a4 authored by florimondmanca's avatar florimondmanca
Browse files

add forms api

parent aa9a2b3e
No related branches found
No related tags found
No related merge requests found
Showing
with 558 additions and 0 deletions
......@@ -10,6 +10,7 @@ from tutoring import views as tutoring_views
from users import views as users_views
from visits import views as visits_views
import projects.views
import dynamicforms.views
app_name = 'api'
......@@ -48,7 +49,12 @@ router.register('projects', projects.views.ProjectViewSet)
router.register('project-participations', projects.views.ParticipationViewSet)
router.register('editions', projects.views.EditionViewSet)
# Dynamic forms views
router.register('forms', dynamicforms.views.FormViewSet)
router.register('form-entries', dynamicforms.views.FormEntryViewSet)
# Core views
router.register('documents', core_views.DocumentViewSet)
urlpatterns += router.urls
"""Dynamic forms admin panels."""
from django.contrib import admin
from .models import Form, Section, Question, FormEntry, Answer
class SectionInline(admin.StackedInline):
"""Inline for sections."""
model = Section
extra = 0
class QuestionInline(admin.StackedInline):
"""Inline for questions."""
model = Question
extra = 0
@admin.register(Form)
class FormAdmin(admin.ModelAdmin):
"""Admin panel for forms."""
list_display = ('title', 'created',)
list_filter = ('created',)
search_fields = ('title',)
inlines = (SectionInline,)
@admin.register(Section)
class SectionAdmin(admin.ModelAdmin):
"""Admin panel for form sections."""
list_display = ('title', 'form',)
list_filter = ('form',)
search_fields = ('title',)
inlines = (QuestionInline,)
@admin.register(Question)
class QuestionAdmin(admin.ModelAdmin):
"""Admin panel for questions."""
list_display = ('text', 'required', 'section', 'type',)
list_filter = ('section__form',)
search_fields = ('text', 'section__title, section__form__title',)
@admin.register(FormEntry)
class FormEntryAdmin(admin.ModelAdmin):
"""Admin panel for form entries."""
list_display = ('form', 'submitted',)
list_filter = ('form', 'submitted',)
search_fields = ('form__title',)
@admin.register(Answer)
class AnswerAdmin(admin.ModelAdmin):
"""Admin panel for answers."""
list_display = ('answer', 'question', 'entry')
list_filter = ('entry__form',)
search_fields = ('answer', 'question__text',)
from django.apps import AppConfig
class DynamicformsConfig(AppConfig):
name = 'dynamicforms'
verbose_name = 'Formulaires'
# Generated by Django 2.0.6 on 2018-06-15 21:24
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Answer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('answer', models.TextField(blank=True, null=True, verbose_name='réponse')),
],
),
migrations.CreateModel(
name='Form',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=300, verbose_name='titre')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='créé le')),
],
options={
'ordering': ('-created',),
},
),
migrations.CreateModel(
name='FormEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('submitted', models.DateTimeField(auto_now_add=True, help_text="Date et heure de soumission de l'entrée.", verbose_name='soumis le')),
('form', models.ForeignKey(help_text="Formulaire associé à l'entrée.", on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='dynamicforms.Form')),
],
options={
'ordering': ('-submitted',),
},
),
migrations.CreateModel(
name='Question',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question_type', models.CharField(max_length=100, verbose_name='type de question')),
('text', models.TextField(help_text='intitulé de la question', verbose_name='intitulé')),
('required', models.BooleanField(verbose_name='requis')),
('form', models.ForeignKey(help_text='Formulaire associé à la question.', on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='dynamicforms.Form')),
],
),
migrations.AddField(
model_name='answer',
name='entry',
field=models.ForeignKey(help_text='Entrée associée à la réponse.', on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='dynamicforms.FormEntry'),
),
migrations.AddField(
model_name='answer',
name='question',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='dynamicforms.Question'),
),
]
# Generated by Django 2.0.6 on 2018-06-15 21:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='answer',
options={'verbose_name': 'réponse'},
),
migrations.AlterModelOptions(
name='form',
options={'ordering': ('-created',), 'verbose_name': 'formulaire'},
),
migrations.AlterModelOptions(
name='formentry',
options={'ordering': ('-submitted',), 'verbose_name': 'entrée de formulaire', 'verbose_name_plural': 'entrées de formulaire'},
),
]
# Generated by Django 2.0.6 on 2018-06-15 21:38
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0002_auto_20180615_2325'),
]
operations = [
migrations.AlterField(
model_name='answer',
name='entry',
field=models.ForeignKey(help_text='Entrée associée à la réponse.', on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='dynamicforms.FormEntry', verbose_name='entrée'),
),
migrations.AlterField(
model_name='formentry',
name='form',
field=models.ForeignKey(help_text="Formulaire associé à l'entrée.", on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='dynamicforms.Form', verbose_name='formulaire'),
),
migrations.AlterField(
model_name='question',
name='form',
field=models.ForeignKey(help_text='Formulaire associé à la question.', on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='dynamicforms.Form', verbose_name='formulaire'),
),
migrations.AlterField(
model_name='question',
name='required',
field=models.BooleanField(default=True, verbose_name='requis'),
),
]
# Generated by Django 2.0.6 on 2018-06-15 21:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0003_auto_20180615_2338'),
]
operations = [
migrations.RenameField(
model_name='question',
old_name='question_type',
new_name='type',
),
]
# Generated by Django 2.0.6 on 2018-06-15 21:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0004_auto_20180615_2339'),
]
operations = [
migrations.AddField(
model_name='question',
name='help_text',
field=models.CharField(blank=True, default='', help_text='Apporte des précisions sur la question', max_length=500, verbose_name='aide'),
),
]
# Generated by Django 2.0.6 on 2018-06-15 21:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0005_question_help_text'),
]
operations = [
migrations.AlterField(
model_name='question',
name='help_text',
field=models.CharField(blank=True, default='', help_text='Apporte des précisions sur la question', max_length=300, verbose_name='aide'),
),
migrations.AlterField(
model_name='question',
name='text',
field=models.CharField(help_text='intitulé de la question', max_length=300, verbose_name='intitulé'),
),
]
# Generated by Django 2.0.6 on 2018-06-15 22:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0006_auto_20180615_2357'),
]
operations = [
migrations.AlterField(
model_name='question',
name='type',
field=models.CharField(choices=[('text-small', 'Texte court'), ('text-small', 'Texte long'), ('date', 'Date'), ('sex', 'Sexe')], max_length=100, verbose_name='type de question'),
),
]
# Generated by Django 2.0.6 on 2018-06-15 22:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0007_auto_20180616_0000'),
]
operations = [
migrations.CreateModel(
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100, verbose_name='titre')),
('form', models.ForeignKey(help_text='Formulaire associé à la section.', on_delete=django.db.models.deletion.CASCADE, related_name='sections', to='dynamicforms.Form', verbose_name='formulaire')),
],
),
migrations.RemoveField(
model_name='question',
name='form',
),
migrations.AddField(
model_name='question',
name='section',
field=models.ForeignKey(help_text='Section de formulaire associée à la question.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='dynamicforms.Section', verbose_name='section'),
),
]
"""Dynamic forms models.
Concept
-------
A dynamic form is a form stored in the database which can have an undetermined
number of questions of various types (small text, long text, file upload…).
A user can reply to a form by submitting a form entry, made of
multiple answers (one per question).
"""
from django.utils.text import Truncator
from django.db import models
class Form(models.Model):
"""Represents a form with multiple questions."""
title = models.CharField('titre', max_length=300)
created = models.DateTimeField('créé le', auto_now_add=True)
class Meta: # noqa
ordering = ('-created',)
verbose_name = 'formulaire'
@property
def entries_count(self) -> int:
"""Return the number of entries in this form."""
return self.entries.count()
def __str__(self):
return str(self.title)
class Section(models.Model):
"""Represents a group of related questions."""
title = models.CharField('titre', max_length=100)
form = models.ForeignKey(
'Form', on_delete=models.CASCADE, related_name='sections',
verbose_name='formulaire',
help_text="Formulaire associé à la section.")
def __str__(self):
return str(self.title)
class Question(models.Model):
"""Represents a question in a form."""
TYPE_TEXT_SMALL = 'text-small'
TYPE_TEXT_LONG = 'text-long'
TYPE_DATE = 'date'
TYPE_YES_NO = 'yes-no'
TYPE_SEX = 'sex'
TYPES = (
(TYPE_TEXT_SMALL, 'Texte court'),
(TYPE_TEXT_SMALL, 'Texte long'),
(TYPE_YES_NO, 'Oui/Non'),
(TYPE_DATE, 'Date'),
(TYPE_SEX, 'Sexe'),
)
text = models.CharField(
'intitulé', max_length=300,
help_text='intitulé de la question')
type = models.CharField(
'type de question', max_length=100, choices=TYPES)
help_text = models.CharField(
'aide', max_length=300, blank=True, default='',
help_text='Apporte des précisions sur la question')
required = models.BooleanField('requis', default=True)
section = models.ForeignKey(
'Section', on_delete=models.CASCADE, related_name='questions',
null=True, verbose_name='section',
help_text="Section de formulaire associée à la question.")
def __str__(self) -> str:
return f'{self.text}{self.required and "*"}'
class FormEntry(models.Model):
"""Represents answers to a form."""
form = models.ForeignKey(
'Form', on_delete=models.CASCADE, related_name='entries',
verbose_name='formulaire',
help_text="Formulaire associé à l'entrée.")
submitted = models.DateTimeField(
'soumis le', auto_now_add=True,
help_text="Date et heure de soumission de l'entrée.")
class Meta: # noqa
ordering = ('-submitted',)
verbose_name = 'entrée de formulaire'
verbose_name_plural = 'entrées de formulaire'
def __str__(self):
return f'{self.form} ({self.submitted})'
class Answer(models.Model):
"""Represents an answer to a particular question in a form."""
question = models.ForeignKey(
'Question', on_delete=models.CASCADE, related_name='answers')
entry = models.ForeignKey(
'FormEntry', on_delete=models.CASCADE, related_name='answers',
verbose_name='entrée',
help_text="Entrée associée à la réponse.")
answer = models.TextField('réponse', blank=True, null=True)
class Meta: # noqa
verbose_name = 'réponse'
def __str__(self):
answer = Truncator(self.answer).chars(140)
return f'{self.question} : {answer}'
"""Dynamic forms serializers."""
from rest_framework import serializers
from .models import Form, Section, Question, Answer, FormEntry
class QuestionSerializer(serializers.ModelSerializer):
"""Serializer for form questions."""
class Meta: # noqa
model = Question
fields = ('id', 'type', 'text', 'help_text', 'required', 'section',)
class SectionSerializer(serializers.ModelSerializer):
"""Serializer for form sections."""
questions = QuestionSerializer(many=True)
class Meta: # noqa
model = Section
fields = ('id', 'title', 'questions',)
class FormSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for forms."""
class Meta: # noqa
model = Form
fields = ('id', 'url', 'title', 'entries_count',)
extra_kwargs = {
'url': {'view_name': 'api:form-detail'},
}
class FormDetailSerializer(FormSerializer):
"""Serializer for form detail."""
sections = SectionSerializer(many=True)
class Meta(FormSerializer.Meta): # noqa
fields = FormSerializer.Meta.fields + ('sections',)
class AnswerSerializer(serializers.ModelSerializer):
"""Serializer for submitting answers."""
question = serializers.PrimaryKeyRelatedField(
queryset=Question.objects.all(),
)
class Meta: # noqa
model = Answer
fields = ('id', 'question', 'entry', 'answer')
extra_kwargs = {
'entry': {'read_only': True},
}
class FormEntrySerializer(serializers.ModelSerializer):
"""Serializer for creating form entries."""
form = serializers.PrimaryKeyRelatedField(
queryset=Form.objects.all(),
)
answers = AnswerSerializer(many=True)
class Meta: # noqa
model = FormEntry
fields = ('id', 'form', 'submitted', 'answers',)
"""Dynamic forms views and API endpoints."""
from rest_framework import mixins, viewsets
from .models import Form, FormEntry
from .serializers import (FormDetailSerializer, FormEntrySerializer,
FormSerializer)
class FormViewSet(viewsets.ReadOnlyModelViewSet):
"""List and retrieve forms."""
serializer_class = FormSerializer
queryset = Form.objects.all()
def get_serializer_class(self):
if self.action == 'retrieve':
return FormDetailSerializer
return FormSerializer
class FormEntryViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
"""Create form entries."""
serializer_class = FormEntrySerializer
queryset = FormEntry.objects.all()
......@@ -69,6 +69,7 @@ PROJECT_APPS = [
'register.apps.RegisterConfig',
'api.apps.ApiConfig',
'mails.apps.MailsConfig',
'dynamicforms.apps.DynamicformsConfig',
'projects.apps.ProjectsConfig',
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS
......
# Generated by Django 2.0.6 on 2018-06-15 21:23
from django.db import migrations, models
import django.db.models.deletion
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('projects', '0002_auto_20180606_2329'),
]
operations = [
migrations.AlterModelOptions(
name='edition',
options={'get_latest_by': 'year', 'ordering': ('-year',), 'verbose_name': 'édition'},
),
migrations.AlterField(
model_name='edition',
name='description',
field=markdownx.models.MarkdownxField(blank=True, default='', help_text='Une description spécifique pour cette édition.'),
),
migrations.AlterField(
model_name='participation',
name='edition',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participations', to='projects.Edition', verbose_name='édition'),
),
migrations.AlterField(
model_name='project',
name='description',
field=markdownx.models.MarkdownxField(blank=True, default='', help_text='Une description générale du projet'),
),
]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment