diff --git a/.travis.yml b/.travis.yml index 0bf10ab4c59f91118d19a1a49e0e9e77142fdc67..529ca87f75c4295c8f51c73a0bbc250222482aed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,10 @@ python: services: - postgresql - - redis-server + +addons: + # Django 2.1+ requires PostgreSQL 9.4+ + postgresql: '9.4' install: - pip install -r requirements.txt @@ -17,9 +20,6 @@ install: - pip install git+https://github.com/Supervisor/supervisor.git before_script: - # Start Celery using the supervisor config - - supervisord - # Create local PostgreSQL database # NOTE: the database name (here 'oser_backend_db') must match the name # in one of these DATABASE_URL setting: diff --git a/Procfile b/Procfile index 2a56f02829ca9006e52a3fd82f2a88d90090a069..672688e1e2398c6deb51abf484f41afcc37b0374 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1 @@ web: gunicorn oser_backend.wsgi:application --bind 0.0.0.0:$PORT - -# Toggle next line to start Celery in a worker process -# worker: celery -A oser_backend worker --beat -l info diff --git a/README.md b/README.md index 3630ff14bc73de803311573598745969c7d29bae..ef37427cb693380eefb3ff81971a962997b8f0a3 100644 --- a/README.md +++ b/README.md @@ -50,18 +50,6 @@ Le site utilise une base de données SQL. Plusieurs technologies existent mais o Après avoir installé PostgreSQL, démarrez le serveur en ouvrant pgAdmin, l'interface graphique qui sera installée en même temps que Postgres. -#### Optionnel : Redis, supervisord - -Le backend Django est relié à [Celery](http://www.celeryproject.org), une librairie Python permet d'effectuer des traitements ou opérations en tâche de fond. - -> NOTE : Pour l'instant, Celery n'est utilisé que pour effectuer un nettoyage périodique des fichiers de médias inutilisés, opération qui peut de toute façon être déclenchée par `$ python manage.py cleanmedia`. Il n'est donc **pas obligatoire d'installer ce qui suit en développement.** - -Celery a besoin d'un système de *messaging* pour fonctionner, on utilise donc ici [Redis](https://redis.io). - -Enfin, [supervisord](http://supervisord.org) est un gestionnaire de processus qui nous permet de lancer Redis et Celery en une seule commande. - -Le plus simple est de se référer aux sites de chaque logiciel/librairie pour leur installation. :wink: - ### Installation du projet - (Recommandé) Créez un environnement virtuel (ici appelé `env`) puis activez-le : @@ -162,29 +150,6 @@ $ curl -X GET "localhost:8000/api/articles/" -H "Authorization: Token b6302cebe7 [{"id": 39, "content": ...}, ...] ``` -### Tâches de fond - -Le daemon Celery gère le calendrier des tâches de fond (nettoyage des fichiers de médias non-utilisés ou autres tâches définies dans le `settings.py`). Pour fonctionner, Celery nécessite un serveur de messages, on utilise ici Redis. - -Les opérations nécessaires pour lancer Celery ainsi que la configuration avec Redis sont rassemblées dans le fichier `supervisord.conf`. Assurez-vous donc d'avoir installé Redis et Supervisor puis démarrez Supervisor au même niveau que le fichier `supervisord.conf` : - -``` -# Supervisor ne supporte toujours pas officiellement Python 3, -# mais la dernière version de développement oui. -$ pip install git+https://github.com/Supervisor/supervisor.git -$ supervisord -``` - -Pour accéder aux derniers logs de Celery ou de Redis, utilisez `supervisorctl tail (celery|redis)`: - -``` -$ supervisorctl tail celery -[2018-04-29 10:59:31,550: INFO/MainProcess] Connected to redis://localhost:6379// -[2018-04-29 10:59:31,566: INFO/MainProcess] mingle: searching for neighbors -[2018-04-29 10:59:32,601: INFO/MainProcess] mingle: all alone -[2018-04-29 10:59:32,657: INFO/MainProcess] celery@MacBook-Pro-de-Florimond-2.local ready. -``` - ### Envoi de mails Le backend utilise le plan gratuit de [SendGrid](https://sendgrid.com) (jusqu'à 100 emails par jour) pour envoyer des emails et notifications aux utilisateurs. diff --git a/api/urls.py b/api/urls.py index d5641dcaa93f2727f881396f5ac399cd5eebea55..dbd3f62fe5ce8ba2ed67d64dca500e2b15060aee 100644 --- a/api/urls.py +++ b/api/urls.py @@ -6,7 +6,6 @@ from api.auth import obtain_auth_token from core import views as core_views from profiles import views as profiles_views from register import views as register_views -from tutoring import views as tutoring_views from users import views as users_views from visits import views as visits_views import projects.views @@ -29,13 +28,6 @@ router.register('users', users_views.UserViewSet) router.register('tutors', profiles_views.TutorViewSet) router.register('students', profiles_views.StudentViewSet) -# Tutoring views -router.register('schools', tutoring_views.SchoolViewSet) -router.register('groups', tutoring_views.TutoringGroupViewSet, - base_name='tutoring_group') -router.register('sessions', tutoring_views.TutoringSessionViewSet, - base_name='tutoring_session') - # Register views router.register('registrations', register_views.RegistrationViewSet) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index c1ad72650b1b74e6026357c8e0935128a3361e46..e7c95e755f75d922bab1f69531383614981d5471 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -1,6 +1,8 @@ -# Generated by Django 2.0.3 on 2018-03-17 15:20 +# Generated by Django 2.0.7 on 2018-09-11 17:38 from django.db import migrations, models +import django_countries.fields +import markdownx.models class Migration(migrations.Migration): @@ -12,14 +14,29 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Link', + name='Address', fields=[ - ('slug', models.SlugField(help_text="Un identifiant unique pour ce lien. Privilégiez 'ce-format-de-slug'.", primary_key=True, serialize=False, unique=True)), - ('url', models.URLField(verbose_name='URL')), - ('description', models.TextField(help_text='Précisez ce que contient ce lien et comment il est utilisé.')), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('line1', models.CharField(help_text='Numéro, voie, rue…', max_length=300, verbose_name='ligne 1')), + ('line2', models.CharField(blank=True, default='', help_text='Résidence, appartement, lieu-dit…', max_length=300, verbose_name='ligne 2')), + ('post_code', models.CharField(help_text="Code postal. Note : le format n'est pas vérifié.", max_length=20, verbose_name='code postal')), + ('city', models.CharField(help_text='Ville', max_length=100, verbose_name='ville')), + ('country', django_countries.fields.CountryField(default='FR', help_text='Pays (FR par défaut).', max_length=2, verbose_name='pays')), ], options={ - 'verbose_name': 'lien', + 'verbose_name': 'adresse', + }, + ), + migrations.CreateModel( + name='Document', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(help_text='Titre du document', max_length=300, verbose_name='titre')), + ('slug', models.SlugField(help_text='Un court identifiant généré après la création du document.', max_length=100, unique=True)), + ('content', markdownx.models.MarkdownxField(help_text='Contenu du document (Markdown est supporté).', verbose_name='contenu')), + ], + options={ + 'ordering': ('title',), }, ), ] diff --git a/core/migrations/0002_delete_link.py b/core/migrations/0002_delete_link.py deleted file mode 100644 index 8aaa68754c8c7eb647a5d08c1d2b93a66106745f..0000000000000000000000000000000000000000 --- a/core/migrations/0002_delete_link.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.0.3 on 2018-03-19 21:54 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ] - - operations = [ - migrations.DeleteModel( - name='Link', - ), - ] diff --git a/core/migrations/0003_document.py b/core/migrations/0003_document.py deleted file mode 100644 index 0a61c566173f279a5cf469b7024f108ac3af7a16..0000000000000000000000000000000000000000 --- a/core/migrations/0003_document.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 2.0.3 on 2018-03-24 18:37 - -from django.db import migrations, models -import markdownx.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('core', '0002_delete_link'), - ] - - operations = [ - migrations.CreateModel( - name='Document', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(help_text='Titre du document', max_length=300, verbose_name='titre')), - ('slug', models.SlugField(help_text='Un court identifiant généré après la création du document.', max_length=100, unique=True)), - ('content', markdownx.models.MarkdownxField(help_text='Contenu du document (Markdown est supporté).', verbose_name='contenu')), - ], - options={ - 'ordering': ('title',), - }, - ), - ] diff --git a/core/migrations/0004_address.py b/core/migrations/0004_address.py deleted file mode 100644 index 8d37478b3e1b132746353728e4168bbca652df15..0000000000000000000000000000000000000000 --- a/core/migrations/0004_address.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-08 13:18 - -from django.db import migrations, models -import django_countries.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0003_document'), - ] - - operations = [ - migrations.CreateModel( - name='Address', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('line1', models.CharField(help_text='Numéro, voie, rue…', max_length=300, verbose_name='ligne 1')), - ('line2', models.CharField(blank=True, default='', help_text='Résidence, appartement, lieu-dit…', max_length=300, verbose_name='ligne 2')), - ('post_code', models.CharField(help_text="Code postal. Note : le format n'est pas vérifié.", max_length=20, verbose_name='code postal')), - ('city', models.CharField(help_text='Ville', max_length=100, verbose_name='ville')), - ('country', django_countries.fields.CountryField(default='FR', help_text='Pays (FR par défaut).', max_length=2)), - ], - ), - ] diff --git a/core/migrations/0005_auto_20180408_1525.py b/core/migrations/0005_auto_20180408_1525.py deleted file mode 100644 index 4666f56cd97a8f1650cd0ab314fa37be8ed8f96b..0000000000000000000000000000000000000000 --- a/core/migrations/0005_auto_20180408_1525.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-08 13:25 - -from django.db import migrations -import django_countries.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0004_address'), - ] - - operations = [ - migrations.AlterModelOptions( - name='address', - options={'verbose_name': 'adresse'}, - ), - migrations.AlterField( - model_name='address', - name='country', - field=django_countries.fields.CountryField(default='FR', help_text='Pays (FR par défaut).', max_length=2, verbose_name='pays'), - ), - ] diff --git a/core/tasks.py b/core/tasks.py deleted file mode 100644 index 621e04608f6d76d355f10fb0a32887f71a4d3ecd..0000000000000000000000000000000000000000 --- a/core/tasks.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Core Celery tasks.""" - -from oser_backend.celery import app -from django.core.management import call_command - - -@app.task -def cleanmedia(): - """Clean unused media files.""" - call_command('cleanmedia') diff --git a/dynamicforms/migrations/0001_initial.py b/dynamicforms/migrations/0001_initial.py index 55d2002216d179944a22b2fb9a7e414f71471bfb..b8fb902153da059f631b594d680032b9c5604d54 100644 --- a/dynamicforms/migrations/0001_initial.py +++ b/dynamicforms/migrations/0001_initial.py @@ -1,7 +1,8 @@ -# Generated by Django 2.0.6 on 2018-06-15 21:24 +# Generated by Django 2.0.7 on 2018-09-11 17:38 from django.db import migrations, models import django.db.models.deletion +import dynamicforms.utils class Migration(migrations.Migration): @@ -18,15 +19,32 @@ class Migration(migrations.Migration): ('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')), ], + options={ + 'verbose_name': 'réponse', + }, + ), + migrations.CreateModel( + name='File', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=300, verbose_name='nom')), + ('file', models.FileField(upload_to=dynamicforms.utils.file_upload_to, verbose_name='fichier')), + ], + options={ + 'verbose_name': 'fichier', + 'verbose_name_plural': 'fichiers', + }, ), 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')), + ('slug', models.SlugField(blank=True, default='', max_length=100)), ('created', models.DateTimeField(auto_now_add=True, verbose_name='créé le')), ], options={ + 'verbose_name': 'formulaire', 'ordering': ('-created',), }, ), @@ -35,9 +53,11 @@ class Migration(migrations.Migration): 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')), + ('form', 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')), ], options={ + 'verbose_name': 'entrée de formulaire', + 'verbose_name_plural': 'entrées de formulaire', 'ordering': ('-submitted',), }, ), @@ -45,16 +65,42 @@ class Migration(migrations.Migration): 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')), + ('text', models.CharField(help_text='intitulé de la question', max_length=300, verbose_name='intitulé')), + ('type', 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')), + ('help_text', models.CharField(blank=True, default='', help_text='Apporte des précisions sur la question', max_length=300, verbose_name='aide')), + ('required', models.BooleanField(default=True, verbose_name='requis')), + ('order', models.PositiveIntegerField(default=0, verbose_name='position')), + ], + options={ + 'ordering': ('order',), + }, + ), + 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')), + ('order', models.PositiveIntegerField(default=0, verbose_name='position')), + ('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')), ], + options={ + 'ordering': ('order',), + }, + ), + migrations.AddField( + model_name='question', + name='section', + field=models.ForeignKey(help_text='Section de formulaire associée à la question.', on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='dynamicforms.Section', verbose_name='section'), + ), + migrations.AddField( + model_name='file', + name='form', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='dynamicforms.Form', verbose_name='formulaire'), ), 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'), + 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.AddField( model_name='answer', diff --git a/dynamicforms/migrations/0002_auto_20180615_2325.py b/dynamicforms/migrations/0002_auto_20180615_2325.py deleted file mode 100644 index 0e69174a842362e44149c28905a0353e843fb328..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0002_auto_20180615_2325.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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'}, - ), - ] diff --git a/dynamicforms/migrations/0003_auto_20180615_2338.py b/dynamicforms/migrations/0003_auto_20180615_2338.py deleted file mode 100644 index 4113138308aa84f3dbc0ed714bc5ff813151d6f2..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0003_auto_20180615_2338.py +++ /dev/null @@ -1,34 +0,0 @@ -# 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'), - ), - ] diff --git a/dynamicforms/migrations/0004_auto_20180615_2339.py b/dynamicforms/migrations/0004_auto_20180615_2339.py deleted file mode 100644 index 0e3d50a345a09256a0f3061a9ab47086b64474dc..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0004_auto_20180615_2339.py +++ /dev/null @@ -1,18 +0,0 @@ -# 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', - ), - ] diff --git a/dynamicforms/migrations/0005_question_help_text.py b/dynamicforms/migrations/0005_question_help_text.py deleted file mode 100644 index 410c852a22e7a7b327505b1e4ad5ecfd709b8647..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0005_question_help_text.py +++ /dev/null @@ -1,18 +0,0 @@ -# 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'), - ), - ] diff --git a/dynamicforms/migrations/0006_auto_20180615_2357.py b/dynamicforms/migrations/0006_auto_20180615_2357.py deleted file mode 100644 index 513ff1801133777976bf3843a0e5a530e0a223ab..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0006_auto_20180615_2357.py +++ /dev/null @@ -1,23 +0,0 @@ -# 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é'), - ), - ] diff --git a/dynamicforms/migrations/0007_auto_20180616_0000.py b/dynamicforms/migrations/0007_auto_20180616_0000.py deleted file mode 100644 index 99086f7e38ae01cfbcdbdabdc07203de112aec97..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0007_auto_20180616_0000.py +++ /dev/null @@ -1,18 +0,0 @@ -# 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'), - ), - ] diff --git a/dynamicforms/migrations/0008_auto_20180616_0007.py b/dynamicforms/migrations/0008_auto_20180616_0007.py deleted file mode 100644 index 9a903f9cc7e05aaa0d1a587cfde743fa28dbf69e..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0008_auto_20180616_0007.py +++ /dev/null @@ -1,31 +0,0 @@ -# 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'), - ), - ] diff --git a/dynamicforms/migrations/0009_auto_20180616_0105.py b/dynamicforms/migrations/0009_auto_20180616_0105.py deleted file mode 100644 index 3cc1f5c97b0b296274583865e17764afb6d2a350..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0009_auto_20180616_0105.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 2.0.6 on 2018-06-15 23:05 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('dynamicforms', '0008_auto_20180616_0007'), - ] - - operations = [ - migrations.CreateModel( - name='File', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=300, verbose_name='nom')), - ('file', models.FileField(upload_to='forms/attachments/', verbose_name='fichier')), - ('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='dynamicforms.Form', verbose_name='formulaire')), - ], - options={ - 'verbose_name': 'fichier', - 'verbose_name_plural': 'fichiers', - }, - ), - migrations.AlterField( - model_name='question', - name='type', - field=models.CharField(choices=[('text-small', 'Texte court'), ('text-small', 'Texte long'), ('yes-no', 'Oui/Non'), ('date', 'Date'), ('sex', 'Sexe')], max_length=100, verbose_name='type de question'), - ), - ] diff --git a/dynamicforms/migrations/0010_auto_20180616_0121.py b/dynamicforms/migrations/0010_auto_20180616_0121.py deleted file mode 100644 index cd7165f75bd3cff952b01abe4af9f978a52b8361..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0010_auto_20180616_0121.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.0.6 on 2018-06-15 23:21 - -from django.db import migrations, models -import dynamicforms.utils - - -class Migration(migrations.Migration): - - dependencies = [ - ('dynamicforms', '0009_auto_20180616_0105'), - ] - - operations = [ - migrations.AddField( - model_name='form', - name='slug', - field=models.SlugField(default='noname', max_length=100), - ), - migrations.AlterField( - model_name='file', - name='file', - field=models.FileField(upload_to=dynamicforms.utils.file_upload_to, verbose_name='fichier'), - ), - ] diff --git a/dynamicforms/migrations/0011_auto_20180616_0141.py b/dynamicforms/migrations/0011_auto_20180616_0141.py deleted file mode 100644 index b372744a89693ee399d3ef85ab0270f077445f5f..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0011_auto_20180616_0141.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.6 on 2018-06-15 23:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dynamicforms', '0010_auto_20180616_0121'), - ] - - operations = [ - migrations.AlterField( - model_name='form', - name='slug', - field=models.SlugField(blank=True, default='', max_length=100), - ), - ] diff --git a/dynamicforms/migrations/0012_auto_20180616_0145.py b/dynamicforms/migrations/0012_auto_20180616_0145.py deleted file mode 100644 index a34858e1cb5ed7b7e6be6bb0adafef7466a65e87..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0012_auto_20180616_0145.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.0.6 on 2018-06-15 23:45 - -from django.db import migrations, models -import django.db.models.deletion - - -def remove_questions_with_null_sections(apps, schema_editor): - Question = apps.get_model('dynamicforms', 'Question') - Question.objects.filter(section__isnull=True).delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('dynamicforms', '0011_auto_20180616_0141'), - ] - - operations = [ - migrations.RunPython(remove_questions_with_null_sections), - ] diff --git a/dynamicforms/migrations/0013_auto_20180616_0149.py b/dynamicforms/migrations/0013_auto_20180616_0149.py deleted file mode 100644 index c964bcc273366592ffa3438bf56ec3609ebcfe1a..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0013_auto_20180616_0149.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.0.6 on 2018-06-15 23:49 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('dynamicforms', '0012_auto_20180616_0145'), - ] - - operations = [ - migrations.AlterField( - model_name='question', - name='section', - field=models.ForeignKey(help_text='Section de formulaire associée à la question.', on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='dynamicforms.Section', verbose_name='section'), - ), - ] diff --git a/dynamicforms/migrations/0014_auto_20180616_1812.py b/dynamicforms/migrations/0014_auto_20180616_1812.py deleted file mode 100644 index 5933a33697e2811600c1ac8e51b7c4038a5efb20..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0014_auto_20180616_1812.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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/migrations/0015_auto_20180616_2136.py b/dynamicforms/migrations/0015_auto_20180616_2136.py deleted file mode 100644 index 59bc79cb1a42d7b31a59d3b7ca2a11674ed53e13..0000000000000000000000000000000000000000 --- a/dynamicforms/migrations/0015_auto_20180616_2136.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 2.0.6 on 2018-06-16 19:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dynamicforms', '0014_auto_20180616_1812'), - ] - - operations = [ - migrations.AlterModelOptions( - name='question', - options={'ordering': ('order',)}, - ), - migrations.AlterModelOptions( - name='section', - options={'ordering': ('order',)}, - ), - migrations.AddField( - model_name='question', - name='order', - field=models.PositiveIntegerField(default=0, verbose_name='position'), - ), - migrations.AddField( - model_name='section', - name='order', - field=models.PositiveIntegerField(default=0, verbose_name='position'), - ), - ] diff --git a/oser_backend/__init__.py b/oser_backend/__init__.py index f272f82a626bb76362bfc3770c4686f8a967ca6e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/oser_backend/__init__.py +++ b/oser_backend/__init__.py @@ -1,5 +0,0 @@ -# Celery: make sure the app is always imported when -# Django starts so that shared_task will use this app. -from .celery import app as celery_app - -__all__ = ('celery_app',) diff --git a/oser_backend/celery.py b/oser_backend/celery.py deleted file mode 100644 index b17f8cb4d2f85144ccc21cd0e8318723ddd55d43..0000000000000000000000000000000000000000 --- a/oser_backend/celery.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Celery config for oser_backend project. - -It exposes the Celery application that other apps can use to -schedule and execute tasks in the background. - -See: -http://docs.celeryproject.org/en/latest/django/first-steps-with-django.html#using-celery-with-django -""" -import os -from celery import Celery - -# set the default Django settings module for Celery -os.environ.setdefault( - 'DJANGO_SETTINGS_MODULE', 'oser_backend.settings.production') - -app = Celery('oser_backend') - -# Using a string here means the worker doesn't have to serialize -# the configuration object to child processes. -# - namespace='CELERY' means all celery-related configuration keys -# should have a `CELERY_` prefix. -app.config_from_object('django.conf:settings', namespace='CELERY') - -# Load tasks from 'tasks.py' modules from all registered Django apps. -app.autodiscover_tasks() - - -@app.task(bind=True) -def debug_task(self): - """Log Celery requests information.""" - print('Request: {0!r}'.format(self.request)) diff --git a/oser_backend/settings/common.py b/oser_backend/settings/common.py index 85aadb61e3d8b6251379cbdf43d5223f4cb7b788..d8da47f708501d33d814ef4f28d522daed5b8d77 100644 --- a/oser_backend/settings/common.py +++ b/oser_backend/settings/common.py @@ -35,6 +35,7 @@ DJANGO_APPS = [ 'whitenoise.runserver_nostatic', 'django.contrib.staticfiles', 'django.forms', + 'django.contrib.sites', ] THIRD_PARTY_APPS = [ @@ -60,11 +61,11 @@ THIRD_PARTY_APPS = [ # Easy filtering on the API 'django_filters', ] + PROJECT_APPS = [ 'core.apps.CoreConfig', 'users.apps.UsersConfig', 'profiles.apps.ProfilesConfig', - 'tutoring.apps.TutoringConfig', 'visits.apps.VisitsConfig', 'register.apps.RegisterConfig', 'api.apps.ApiConfig', @@ -72,8 +73,14 @@ PROJECT_APPS = [ 'dynamicforms.apps.DynamicformsConfig', 'projects.apps.ProjectsConfig', ] + INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS +# Activate the sites framework +# It is used to define the domain of the frontend website in +# the admin (via the 'Sites' section) +SITE_ID = 1 + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'corsheaders.middleware.CorsMiddleware', @@ -256,11 +263,3 @@ STATICFILES_DIRS = [ MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') - -# Celery settings - -CELERY_BROKER_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379') -CELERY_RESULT_BACKEND = CELERY_BROKER_URL -CELERY_ACCEPT_CONTENT = ('application/json',) -CELERY_TASK_SERIALIZER = 'json' -CELERY_RESULT_SERIALIZER = 'json' diff --git a/oser_backend/settings/production.py b/oser_backend/settings/production.py index 29cd58b244fd1c4c19ad4a7e84167e14186d3908..f7a85fa5ef8d19b8838d9dd530ea1b29b36db2a9 100644 --- a/oser_backend/settings/production.py +++ b/oser_backend/settings/production.py @@ -2,8 +2,6 @@ import os -from celery.schedules import crontab - from aws.conf import * from .common import * @@ -29,13 +27,3 @@ MAILS_RAISE_EXCEPTIONS = os.environ.get('MAILS_RAISE_EXCEPTIONS', False) # SendGrid # Allow Sandbox if DEBUG is True (we're in prod anyway) SENDGRID_SANDBOX_MODE_IN_DEBUG = False - -# Celery settings - -CELERY_BEAT_SCHEDULE = { - # Clean media files every day at 22:00 - 'clean-media-every-hour': { - 'task': 'core.tasks.cleanmedia', - 'schedule': crontab(minute='0', hour='22'), - }, -} diff --git a/profiles/admin.py b/profiles/admin.py index dfd5fbf041a08d4214d13177046fec50ed0c1a62..82207b0dd026276567ef92a1583a8bcd5b74d246 100644 --- a/profiles/admin.py +++ b/profiles/admin.py @@ -2,7 +2,6 @@ from django.contrib import admin from .models import Student, Tutor -from tutoring.models import TutoringGroup class ProfileAdminMixin: @@ -11,21 +10,10 @@ class ProfileAdminMixin: search_fields = ('user__email', 'user__first_name', 'user__last_name',) -class TutorTutoringGroupsInline(admin.TabularInline): - """Inline for tutor tutoring groups.""" - - model = TutoringGroup.tutors.through - extra = 0 - max_num = 0 - readonly_fields = ('tutoring_group', 'is_leader') - can_delete = False - - @admin.register(Tutor) class TutorAdmin(ProfileAdminMixin, admin.ModelAdmin): """Tutor admin panel.""" - inlines = (TutorTutoringGroupsInline,) autocomplete_fields = ('address',) class Meta: # noqa diff --git a/profiles/factory.py b/profiles/factory.py index ea03ddd9b733673ce25ad06b25aea18143a7d93c..886b5dde680d2945e4fbc1b30455990a390fa491 100644 --- a/profiles/factory.py +++ b/profiles/factory.py @@ -1,21 +1,19 @@ """Profile factories.""" -import random from datetime import datetime import factory import factory.django from django.contrib.auth.models import Group -from tutoring.factory import TutoringGroupFactory -from tutoring.models import TutoringGroup +from core.factory import AddressFactory from users.factory import UserFactory from . import models class StudentFactory(factory.DjangoModelFactory): - """Student object factory. Not assigned to a tutoring group.""" + """Student object factory.""" class Meta: # noqa model = models.Student @@ -23,21 +21,6 @@ class StudentFactory(factory.DjangoModelFactory): user = factory.SubFactory(UserFactory) -class StudentInTutoringGroupFactory(StudentFactory): - """Student object factory, member of a tutoring group.""" - - @factory.lazy_attribute - def tutoring_group(self): - """Return an existing tutoring group in 70% of cases.""" - groups = TutoringGroup.objects.all() - if groups and random.random() > .3: - return random.choice(groups) - return TutoringGroupFactory.create() - - # student's school is the same as the student's tutoring group's - school = factory.SelfAttribute('tutoring_group.school') - - _this_year = datetime.today().year @@ -49,6 +32,7 @@ class TutorFactory(factory.DjangoModelFactory): user = factory.SubFactory(UserFactory) promotion = factory.Iterator([_this_year, _this_year + 1, _this_year + 2]) + address = factory.SubFactory(AddressFactory) class TutorInGroupFactory(TutorFactory): diff --git a/profiles/migrations/0001_initial.py b/profiles/migrations/0001_initial.py index 25365006d866860cc12b004e97711c9dc22138e8..8720e63e4f24ec7112ca916034e9eb52252f0654 100644 --- a/profiles/migrations/0001_initial.py +++ b/profiles/migrations/0001_initial.py @@ -1,6 +1,5 @@ -# Generated by Django 2.0.4 on 2018-05-05 15:34 +# Generated by Django 2.0.7 on 2018-09-11 17:38 -from django.conf import settings from django.db import migrations, models import django.db.models.deletion import profiles.models @@ -11,8 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('register', '0015_auto_20180505_1403'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('core', '0001_initial'), ] operations = [ @@ -20,10 +18,6 @@ class Migration(migrations.Migration): name='Student', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('registration', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='student', to='register.Registration', verbose_name="dossier d'inscription")), - ('school', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='tutoring.School', verbose_name='lycée')), - ('tutoring_group', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='tutoring.TutoringGroup', verbose_name='groupe de tutorat')), - ('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='student', to=settings.AUTH_USER_MODEL, verbose_name='utilisateur')), ], options={ 'verbose_name': 'lycéen', @@ -34,8 +28,8 @@ class Migration(migrations.Migration): name='Tutor', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('promotion', models.IntegerField(choices=[(2020, '2020'), (2019, '2019'), (2018, '2018'), (2017, '2017'), (2016, '2016')], default=2020)), - ('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tutor', to=settings.AUTH_USER_MODEL, verbose_name='utilisateur')), + ('promotion', models.IntegerField(choices=[(2021, '2021'), (2020, '2020'), (2019, '2019'), (2018, '2018'), (2017, '2017')], default=2021)), + ('address', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tutor', to='core.Address', verbose_name='adresse')), ], options={ 'verbose_name': 'tuteur', diff --git a/profiles/migrations/0002_auto_20180512_1344.py b/profiles/migrations/0002_auto_20180512_1344.py deleted file mode 100644 index 27a7fa5d858a7cdc5630c747972d6008dd794719..0000000000000000000000000000000000000000 --- a/profiles/migrations/0002_auto_20180512_1344.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-12 11:44 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('profiles', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='student', - name='school', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='tutoring.School', verbose_name='lycée'), - ), - migrations.AlterField( - model_name='student', - name='tutoring_group', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='tutoring.TutoringGroup', verbose_name='groupe de tutorat'), - ), - ] diff --git a/profiles/migrations/0002_auto_20180911_1938.py b/profiles/migrations/0002_auto_20180911_1938.py new file mode 100644 index 0000000000000000000000000000000000000000..aefb82e9b5a2a7ff8f48c27230c6f674fc0a2299 --- /dev/null +++ b/profiles/migrations/0002_auto_20180911_1938.py @@ -0,0 +1,34 @@ +# Generated by Django 2.0.7 on 2018-09-11 17:38 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('profiles', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('register', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='tutor', + name='user', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tutor', to=settings.AUTH_USER_MODEL, verbose_name='utilisateur'), + ), + migrations.AddField( + model_name='student', + name='registration', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='student', to='register.Registration', verbose_name="dossier d'inscription"), + ), + migrations.AddField( + model_name='student', + name='user', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='student', to=settings.AUTH_USER_MODEL, verbose_name='utilisateur'), + ), + ] diff --git a/profiles/migrations/0003_tutor_address.py b/profiles/migrations/0003_tutor_address.py deleted file mode 100644 index b5bc917146171d6bc1a1e122d43846984733aacb..0000000000000000000000000000000000000000 --- a/profiles/migrations/0003_tutor_address.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.0.6 on 2018-06-15 23:41 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0005_auto_20180408_1525'), - ('profiles', '0002_auto_20180512_1344'), - ] - - operations = [ - migrations.AddField( - model_name='tutor', - name='address', - field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tutor', to='core.Address', verbose_name='adresse'), - ), - ] diff --git a/profiles/models.py b/profiles/models.py index 25cb1ebe2d297e13a51eac85e78dacf657eb295a..28061fa97817f7fd8dd4c0a58ad90d3e8da141bd 100644 --- a/profiles/models.py +++ b/profiles/models.py @@ -43,22 +43,6 @@ class Student(ProfileMixin, models.Model): verbose_name='utilisateur', related_name='student') - tutoring_group = models.ForeignKey( - 'tutoring.TutoringGroup', - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='students', - verbose_name='groupe de tutorat') - - school = models.ForeignKey( - 'tutoring.School', - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='students', - verbose_name='lycée') - registration = models.OneToOneField( 'register.Registration', on_delete=models.SET_NULL, @@ -68,16 +52,6 @@ class Student(ProfileMixin, models.Model): related_name='student', ) - @property - def address(self): - """Address of the student defined in their registration.""" - return getattr(self.registration, 'address', None) - - @property - def emergency_contact(self): - """Emergency contact of the student defined in their registration.""" - return getattr(self.registration, 'emergency_contact', None) - class Meta: # noqa verbose_name = 'lycéen' diff --git a/profiles/serializers.py b/profiles/serializers.py index e15ff43f1360638580ce48be87c15dd4ff7a3577..31b20310d293647aacde450dca91509f8af66088 100644 --- a/profiles/serializers.py +++ b/profiles/serializers.py @@ -2,10 +2,7 @@ from rest_framework import serializers -from core.serializers import AddressSerializer -from register.serializers import (EmergencyContactSerializer, - StudentRegistrationSerializer) -from tutoring.models import School, TutoringGroup +from register.serializers import StudentRegistrationSerializer from users.serializers import UserSerializer from .models import Student, Tutor @@ -15,13 +12,10 @@ class TutorSerializer(serializers.HyperlinkedModelSerializer): """Hyperlinked serializer for Tutor.""" user = UserSerializer() - tutoring_groups = serializers.PrimaryKeyRelatedField( - many=True, read_only=True) - address = AddressSerializer() class Meta: # noqa model = Tutor - fields = ('user', 'address', 'promotion', 'tutoring_groups', 'url',) + fields = ('user', 'promotion', 'url',) extra_kwargs = { 'url': {'view_name': 'api:tutor-detail'}, } @@ -31,25 +25,17 @@ class StudentSerializer(serializers.HyperlinkedModelSerializer): """Hyperlinked serializer for Student.""" user = UserSerializer() - tutoring_group = serializers.PrimaryKeyRelatedField( - queryset=TutoringGroup.objects.all(), - ) - school = serializers.PrimaryKeyRelatedField( - queryset=School.objects.all(), - ) visits = serializers.PrimaryKeyRelatedField( source='user.visit_set', many=True, read_only=True) - address = AddressSerializer() - emergency_contact = EmergencyContactSerializer() registration = StudentRegistrationSerializer() class Meta: # noqa model = Student - fields = ('user_id', 'user', 'address', 'tutoring_group', - 'school', 'emergency_contact', 'registration', - 'visits', 'url',) + fields = ( + 'user_id', 'user', 'registration', 'visits', 'url', + ) extra_kwargs = { 'url': {'view_name': 'api:student-detail'}, } diff --git a/profiles/views.py b/profiles/views.py index f27b3f76a79b84d421fb9c3d7b8ca00dc67b7dc3..0c157ef80761cfde6b830d6820eae66d48ab365b 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -6,7 +6,6 @@ from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response -from tutoring.serializers import TutoringGroupSerializer from visits.serializers import VisitSerializer from .models import Student, Tutor @@ -23,15 +22,6 @@ class TutorViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = TutorSerializer permission_classes = (DRYPermissions,) - @action(detail=True) - def tutoringgroups(self, request, pk=None): - """Retrieve the tutoring groups of a tutor.""" - tutor = self.get_object() - tutoring_groups = tutor.tutoring_groups.all() - serializer = TutoringGroupSerializer(tutoring_groups, many=True, - context={'request': request}) - return Response(serializer.data) - class StudentViewSet(viewsets.ReadOnlyModelViewSet): """API endpoint that allows students to be viewed. @@ -54,30 +44,8 @@ class StudentViewSet(viewsets.ReadOnlyModelViewSet): "profile_type": null, "first_name": "", "last_name": "", - "gender": null, - "phone_number": null, - "date_of_birth": null, "url": "http://localhost:8000/api/users/4/" }, - "address": { - "line1": "88 bis rue Jules Guesde", - "line2": "", - "post_code": "93100", - "city": "Montreuil", - "country": { - "code": "FR", - "name": "France" - } - }, - "tutoring_group": 1, - "school": "0930965U", - "emergency_contact": { - "first_name": "Marie-Claude", - "last_name": "Perret", - "email": null, - "home_phone": "+33312344556", - "mobile_phone": null - }, "registration": { "id": 3, "submitted": "2018-05-05T14:15:10.998206+02:00", @@ -92,15 +60,6 @@ class StudentViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = StudentSerializer permission_classes = (DRYPermissions,) - @action(detail=True) - def tutoringgroup(self, request, pk=None): - """Retrieve the tutoring group of a student.""" - student = self.get_object() - tutoring_group = student.tutoring_group - serializer = TutoringGroupSerializer(tutoring_group, - context={'request': request}) - return Response(serializer.data) - @action(detail=True) def visits(self, request, pk=None): """List detailed info about the visits a student participates in.""" diff --git a/projects/migrations/0001_initial.py b/projects/migrations/0001_initial.py index 93d192e7521b47365d53778184ad523d9b7a000c..2a575ed903e4db5e1f85c83b443b8e0f1682ed3f 100644 --- a/projects/migrations/0001_initial.py +++ b/projects/migrations/0001_initial.py @@ -1,10 +1,9 @@ -# Generated by Django 2.0.4 on 2018-06-06 21:11 +# Generated by Django 2.0.7 on 2018-09-11 17:38 -from django.conf import settings from django.db import migrations, models import django.db.models.deletion import markdownx.models -import projects.models +import projects.utils class Migration(migrations.Migration): @@ -12,7 +11,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('dynamicforms', '0001_initial'), ] operations = [ @@ -20,21 +19,33 @@ class Migration(migrations.Migration): name='Edition', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('year', models.IntegerField(default=projects.utils.this_year, help_text="L'année où se déroule cette édition.", verbose_name='année')), ('name', models.CharField(blank=True, default='', help_text='Un nom optionnel pour cette édition (exemple : "Berlin").', max_length=200, verbose_name='nom')), - ('year', models.IntegerField(default=projects.models.this_year, help_text="L'année où se déroule cette édition.", verbose_name='année')), - ('description', markdownx.models.MarkdownxField(help_text='Une description spécifique pour cette édition.')), + ('description', markdownx.models.MarkdownxField(blank=True, default='', help_text='Une description spécifique pour cette édition.')), ], options={ 'verbose_name': 'édition', 'ordering': ('-year',), + 'get_latest_by': 'year', + }, + ), + migrations.CreateModel( + name='EditionForm', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('deadline', models.DateField(help_text="Les lycéens ne pourront plus s'inscrire après cette date.", verbose_name='date butoir')), + ], + options={ + 'verbose_name': 'formulaire projet', + 'verbose_name_plural': 'formulaires projet', + 'ordering': ('deadline',), }, ), migrations.CreateModel( name='EditionOrganizer', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('edition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.Edition', verbose_name='édition')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='utilisateur')), + ('role', models.CharField(blank=True, default='', help_text='Exemple : responsable projet, responsable inscriptions…', max_length=100, verbose_name='rôle')), ], options={ 'verbose_name': 'organisateur', @@ -45,9 +56,9 @@ class Migration(migrations.Migration): 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 de soumission de la participation', verbose_name='soumis le')), - ('status', models.CharField(choices=[('pending', 'En attente'), ('valid', 'Validé'), ('accepted', 'Accepté'), ('rejected', 'Refusé'), ('cancelled', 'Annulé')], help_text="L'état de la participation. En attente = en cours de validation par les organisateurs. Validé = toutes les pièces ont été reçues et sont conformes. Accepté = le lycéen a été sélectionné pour participer. Refusé = le lycéen n'a pas été sélectionné pour participer. Annulé = le lycéen a annulé sa participation.", max_length=10, verbose_name='statut')), - ('edition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participations', to='projects.Edition', verbose_name='sortie')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_participations', to=settings.AUTH_USER_MODEL, verbose_name='utilisateur')), + ('state', models.CharField(choices=[('pending', 'En attente'), ('valid', 'Validé'), ('accepted', 'Accepté'), ('rejected', 'Refusé'), ('cancelled', 'Annulé')], default='pending', help_text="État de la participation. En attente = en cours de validation par les organisateurs. Validé = toutes les pièces ont été reçues et sont conformes. Accepté = le lycéen a été sélectionné pour participer. Refusé = le lycéen n'a pas été sélectionné pour participer. Annulé = le lycéen a annulé sa participation.", max_length=10, verbose_name='état')), + ('edition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participations', to='projects.Edition', verbose_name='édition')), + ('entry', models.OneToOneField(help_text="Réponses au formulaire d'inscription", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='project_participation', to='dynamicforms.FormEntry', verbose_name='entrée')), ], options={ 'ordering': ('-submitted',), @@ -58,7 +69,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(help_text='Le nom du projet.', max_length=200, verbose_name='nom')), - ('description', markdownx.models.MarkdownxField(help_text='Une description générale du projet')), + ('description', markdownx.models.MarkdownxField(blank=True, default='', help_text='Une description générale du projet')), ('logo', models.ImageField(blank=True, help_text='Le logo du projet ou une image représentative.', null=True, upload_to='projects/logos/')), ], options={ @@ -66,14 +77,4 @@ class Migration(migrations.Migration): 'ordering': ('name',), }, ), - migrations.AddField( - model_name='edition', - name='organizers', - field=models.ManyToManyField(through='projects.EditionOrganizer', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='edition', - name='project', - field=models.ForeignKey(help_text='Le projet dont ceci est une édition.', on_delete=django.db.models.deletion.CASCADE, related_name='editions', to='projects.Project', verbose_name='projet'), - ), ] diff --git a/projects/migrations/0002_auto_20180606_2329.py b/projects/migrations/0002_auto_20180606_2329.py deleted file mode 100644 index 065dfdfcf3bd1d710ffbc4d50630c239bc30f19a..0000000000000000000000000000000000000000 --- a/projects/migrations/0002_auto_20180606_2329.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.0.4 on 2018-06-06 21:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='participation', - name='status', - ), - migrations.AddField( - model_name='participation', - name='state', - field=models.CharField(choices=[('pending', 'En attente'), ('valid', 'Validé'), ('accepted', 'Accepté'), ('rejected', 'Refusé'), ('cancelled', 'Annulé')], default='pending', help_text="État de la participation. En attente = en cours de validation par les organisateurs. Validé = toutes les pièces ont été reçues et sont conformes. Accepté = le lycéen a été sélectionné pour participer. Refusé = le lycéen n'a pas été sélectionné pour participer. Annulé = le lycéen a annulé sa participation.", max_length=10, verbose_name='état'), - ), - ] diff --git a/projects/migrations/0002_auto_20180911_1938.py b/projects/migrations/0002_auto_20180911_1938.py new file mode 100644 index 0000000000000000000000000000000000000000..d170e209dc1690e2437ec57177369cf97c3f05b0 --- /dev/null +++ b/projects/migrations/0002_auto_20180911_1938.py @@ -0,0 +1,61 @@ +# Generated by Django 2.0.7 on 2018-09-11 17:38 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import projects.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('dynamicforms', '0001_initial'), + ('profiles', '0002_auto_20180911_1938'), + ('projects', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='participation', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_participations', to=settings.AUTH_USER_MODEL, verbose_name='utilisateur'), + ), + migrations.AddField( + model_name='editionorganizer', + name='edition', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.Edition', verbose_name='édition'), + ), + migrations.AddField( + model_name='editionorganizer', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='utilisateur'), + ), + migrations.AddField( + 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.AddField( + 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"), + ), + migrations.AddField( + model_name='editionform', + name='recipient', + field=models.ForeignKey(blank=True, help_text='Tuteur/tutrice à qui envoyer les pièces justificatives. Son adresse doit être renseignée dans son profil.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='profiles.Tutor', validators=[projects.models._validate_address_is_set], verbose_name='destinataire'), + ), + migrations.AddField( + model_name='edition', + name='organizers', + field=models.ManyToManyField(through='projects.EditionOrganizer', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='edition', + name='project', + field=models.ForeignKey(help_text='Le projet dont ceci est une édition.', on_delete=django.db.models.deletion.CASCADE, related_name='editions', to='projects.Project', verbose_name='projet'), + ), + ] diff --git a/projects/migrations/0003_auto_20180615_2323.py b/projects/migrations/0003_auto_20180615_2323.py deleted file mode 100644 index 64a96009accc9a01266c2a9307c9795d964c5cd9..0000000000000000000000000000000000000000 --- a/projects/migrations/0003_auto_20180615_2323.py +++ /dev/null @@ -1,34 +0,0 @@ -# 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'), - ), - ] diff --git a/projects/migrations/0004_editionform.py b/projects/migrations/0004_editionform.py deleted file mode 100644 index 4dc610a999b84e9d988c9e9630fa3c6e9303c0a1..0000000000000000000000000000000000000000 --- a/projects/migrations/0004_editionform.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 2.0.6 on 2018-06-15 23:41 - -from django.db import migrations, models -import django.db.models.deletion -import projects.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('profiles', '0003_tutor_address'), - ('dynamicforms', '0011_auto_20180616_0141'), - ('projects', '0003_auto_20180615_2323'), - ] - - operations = [ - migrations.CreateModel( - name='EditionForm', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('deadline', models.DateField(help_text="Les lycéens ne pourront plus s'inscrire après cette date.", verbose_name='date butoir')), - ('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dynamicforms.Form', verbose_name='formulaire')), - ('recipient', models.ForeignKey(blank=True, help_text='Tuteur/tutrice à qui envoyer les pièces justificatives. Son adresse doit être renseignée dans son profil.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='profiles.Tutor', validators=[projects.models.validate_address_is_set], verbose_name='destinataire')), - ], - options={ - 'verbose_name': 'formulaire projet', - 'verbose_name_plural': 'formulaires projet', - 'ordering': ('deadline',), - }, - ), - ] diff --git a/projects/migrations/0005_auto_20180616_0144.py b/projects/migrations/0005_auto_20180616_0144.py deleted file mode 100644 index 73439c42a1b5de4e2c28b48b23facdc6a1a18dc2..0000000000000000000000000000000000000000 --- a/projects/migrations/0005_auto_20180616_0144.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.0.6 on 2018-06-15 23:44 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0004_editionform'), - ] - - operations = [ - migrations.AddField( - model_name='editionform', - name='edition', - field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='projects.Edition', verbose_name='édition'), - ), - migrations.AlterField( - model_name='editionform', - name='form', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='dynamicforms.Form', verbose_name='formulaire'), - ), - ] diff --git a/projects/migrations/0006_auto_20180616_0145.py b/projects/migrations/0006_auto_20180616_0145.py deleted file mode 100644 index ba4934abeabbad5380fafc30589738035a479e2d..0000000000000000000000000000000000000000 --- a/projects/migrations/0006_auto_20180616_0145.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.0.6 on 2018-06-15 23:45 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0005_auto_20180616_0144'), - ] - - operations = [ - migrations.AlterField( - model_name='editionform', - name='edition', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='projects.Edition', verbose_name='édition'), - ), - ] diff --git a/projects/migrations/0007_auto_20180616_1812.py b/projects/migrations/0007_auto_20180616_1812.py deleted file mode 100644 index 7835d9d7f1539e21b13ba82aecd56e7a0d1be6fc..0000000000000000000000000000000000000000 --- a/projects/migrations/0007_auto_20180616_1812.py +++ /dev/null @@ -1,30 +0,0 @@ -# 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 deleted file mode 100644 index 0a3740d95f0795c16037c58061be8fc61c505913..0000000000000000000000000000000000000000 --- a/projects/migrations/0008_auto_20180616_1919.py +++ /dev/null @@ -1,19 +0,0 @@ -# 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/migrations/0009_auto_20180630_1457.py b/projects/migrations/0009_auto_20180630_1457.py deleted file mode 100644 index 672dfbb8e2c5cc30d82a0d405c94241f23cbd51d..0000000000000000000000000000000000000000 --- a/projects/migrations/0009_auto_20180630_1457.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.0.6 on 2018-06-30 12:57 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0008_auto_20180616_1919'), - ] - - operations = [ - migrations.AddField( - model_name='editionorganizer', - name='role', - field=models.CharField(blank=True, default='', help_text='Exemple : responsable projet, responsable inscriptions…', max_length=100, verbose_name='rôle'), - ), - 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.SET_NULL, related_name='project_participation', to='dynamicforms.FormEntry', verbose_name='entrée'), - ), - ] diff --git a/projects/models.py b/projects/models.py index c31a7249738670d31d91f1f6050511da9b29a882..cd5783fe39a32c73f0004f45ced25afc72531d34 100644 --- a/projects/models.py +++ b/projects/models.py @@ -1,6 +1,7 @@ """Projects models.""" from django.db import models +from django.contrib.sites.models import Site from django.core.validators import ValidationError from markdownx.models import MarkdownxField @@ -53,10 +54,6 @@ class Project(models.Model): class Edition(models.Model): """Represents an instance of a project for a given year.""" - name = models.CharField( - 'nom', max_length=200, default='', blank=True, - help_text='Un nom optionnel pour cette édition (exemple : "Berlin").') - year = models.IntegerField( 'année', default=this_year, help_text="L'année où se déroule cette édition.") @@ -66,6 +63,10 @@ class Edition(models.Model): verbose_name='projet', related_name='editions', help_text='Le projet dont ceci est une édition.') + name = models.CharField( + 'nom', max_length=200, default='', blank=True, + help_text='Un nom optionnel pour cette édition (exemple : "Berlin").') + description = MarkdownxField( blank=True, default='', help_text=( @@ -82,6 +83,14 @@ class Edition(models.Model): verbose_name = 'édition' get_latest_by = 'year' + def get_projects_site_url(self) -> str: + site = Site.objects.get_current() + return f'https://{site.domain}/projets/' + + def get_registration_url(self) -> str: + site = Site.objects.get_current() + return f'https://{site.domain}/projets/mes-inscriptions' + def __str__(self) -> str: """Represent using the project name, the year and the edition name.""" s = f'{self.project} édition {self.year}' @@ -219,6 +228,16 @@ class Participation(models.Model): class Meta: # noqa ordering = ('-submitted',) + def __init__(self, *args, **kwargs): + """Store the initial value of `state` to detect changes.""" + super().__init__(*args, **kwargs) + self.initial_state = self.state + + @property + def state_changed(self) -> bool: + """Return whether the `state` field has changed.""" + return self.initial_state != self.state + def __str__(self): """Represent by its user.""" return str(self.user) diff --git a/projects/notifications.py b/projects/notifications.py new file mode 100644 index 0000000000000000000000000000000000000000..7ee9008841f675eed76cf5ad53a507656df69f8b --- /dev/null +++ b/projects/notifications.py @@ -0,0 +1,123 @@ +"""Projects notifications.""" + +from django.shortcuts import reverse +from django.utils.timezone import now + +from mails import Notification +from users.models import User + +from .models import Edition, Project + + +class _BaseParticipationNotification(Notification): + """Base notification for project participations.""" + + args = ('user', 'edition',) + + @classmethod + def example(cls): + """Example notification.""" + user = User(email='john.doe@example.com', first_name='John') + project = Project(title='Focus Europe') + edition = Edition(project=project, year=now().year) + return cls(user=user, edition=edition) + + +class _NotifyOrgnizers(_BaseParticipationNotification): + """Notify the edition organizers a participation has changed.""" + + title: str + + def get_subject(self): + return f'{self.title}: {self.edition}' + + def _get_editionform_admin_url(self) -> str: + base = 'https://oser-backend.herokuapp.com' + view = reverse('admin:projects_editionform_changelist') + return base + view + + def get_context(self) -> dict: + context = super().get_context() + context['editionform_admin_url'] = self._get_editionform_admin_url() + return context + + def get_recipients(self): + """Return the email of each organizer.""" + edition = self.kwargs['edition'] + # TODO add the project team's email + return list(edition.organizers.values_list('email', flat=True)) + + +class _NotifyUser(_BaseParticipationNotification): + """Notify a user their participation state has changed.""" + + verb: str + + def get_subject(self): + return f'Participation {self.verb}: {self.edition}' + + def get_recipients(self): + return [self.kwargs['user'].email] + + +class OrganizersReceived(_NotifyOrgnizers): + """Notify the edition organizers that a new participation was created.""" + + title = 'Nouvelle participation' + template_name = 'projects/organizers_participation_received.md' + + +class UserReceived(_NotifyUser): + """Notify a user their participation was well received.""" + + verb = 'en attente' + template_name = 'projects/participation_received.md' + + +class UserValid(_NotifyUser): + """Notify a user their participation was marked as valid.""" + + verb = 'vérifiée' + template_name = 'projects/participation_valid.md' + + +class UserAccepted(_NotifyUser): + """Notify a user their participation was marked as accepted.""" + + verb = 'acceptée' + template_name = 'projects/participation_accepted.md' + + +class UserRejected(_NotifyUser): + """Notify a user their participation was marked as rejected.""" + + verb = 'rejetée' + template_name = 'projects/participation_rejected.md' + + +class UserCancelled(_NotifyUser): + """Notify a user their participation was correctly cancelled.""" + + verb = 'annulée' + template_name = 'projects/participation_cancelled.md' + + +class UserDeleted(_NotifyUser): + """Notify a user their participation was correctly deleted.""" + + verb = 'supprimée' + template_name = 'projects/participation_deleted.md' + + +class OrganizersCancelled(_NotifyOrgnizers): + """Notify organizers that a user has cancelled their participation.""" + + title = 'Participation annulée' + template_name = 'projects/organizers_participation_cancelled.md' + + +class OrganizersDeleted(_NotifyOrgnizers): + """Notify organizers a user has deleted their participation.""" + + title = 'Participation supprimée' + template_name = 'projects/organizers_participation_deleted.md' diff --git a/projects/signals.py b/projects/signals.py index 4b75cea9a475f587c0874e5a2b8582cb55b24124..25f4fc9dddc41931febcee1683108a1dc00984aa 100644 --- a/projects/signals.py +++ b/projects/signals.py @@ -1,20 +1,92 @@ """Projects app signals.""" import logging -from django.db.models.signals import pre_delete -from django.dispatch import receiver -from .models import Participation +from django.db.models.signals import post_save, pre_delete +from django.dispatch import receiver, Signal +from . import notifications +from .models import Participation logger = logging.getLogger('web.projects.signals') +pending = Signal(providing_args=('instance',)) +valid = Signal(providing_args=('instance',)) +accepted = Signal(providing_args=('instance',)) +rejected = Signal(providing_args=('instance',)) +cancelled = Signal(providing_args=('instance',)) +deleted = Signal(providing_args=('instance',)) +deleted_organizers = Signal(providing_args=('instance',)) + + +def _send(cls, instance: Participation): + cls(user=instance.user, edition=instance.edition).send() + + @receiver(pre_delete, sender=Participation) -def delete_associated_form_entry(sender, instance: Participation, - *args, **kwargs): +def delete_associated_form_entry(sender, instance: Participation, **kwargs): """Delete the form entry associated to a participation being deleted.""" entry = instance.entry if entry: entry.delete() logger.info('entry %s deleted', entry.id) + + +@receiver(post_save, sender=Participation) +def send_state_notifications(sender, instance: Participation, + created, **kwargs): + """Send notifications when the state of a participation has changed.""" + if not created and not instance.state_changed: + return + signals = { + Participation.STATE_PENDING: pending, + Participation.STATE_VALIDATED: valid, + Participation.STATE_ACCEPTED: accepted, + Participation.STATE_REJECTED: rejected, + Participation.STATE_CANCELLED: cancelled, + } + if instance.state in signals.keys(): + signals[instance.state].send(Participation, instance=instance) + + +@receiver(pre_delete, sender=Participation) +def send_participation_deleted_notifications(sender, instance: Participation, + **kwargs): + """Send notifications when a participation is deleted.""" + deleted.send(Participation, instance=instance) + + +# Notiication senders + +@receiver(pending) +def notify_pending(sender, instance, **kwargs): + _send(notifications.UserReceived, instance) + _send(notifications.OrganizersReceived, instance) + + +@receiver(valid) +def notify_valid(sender, instance, **kwargs): + _send(notifications.UserValid, instance) + + +@receiver(accepted) +def notify_accepted(sender, instance, **kwargs): + _send(notifications.UserAccepted, instance) + + +@receiver(rejected) +def notify_rejected(sender, instance, **kwargs): + _send(notifications.UserRejected, instance) + + +@receiver(cancelled) +def notify_cancelled(sender, instance, **kwargs): + _send(notifications.UserCancelled, instance) + _send(notifications.OrganizersCancelled, instance) + + +@receiver(deleted) +def notify_deleted(sender, instance, **kwargs): + _send(notifications.UserDeleted, instance) + _send(notifications.OrganizersDeleted, instance) diff --git a/projects/templates/projects/organizers_participation_cancelled.md b/projects/templates/projects/organizers_participation_cancelled.md new file mode 100644 index 0000000000000000000000000000000000000000..6cb2ca7fd8da492a3a9387ed9c20d3f43db229df --- /dev/null +++ b/projects/templates/projects/organizers_participation_cancelled.md @@ -0,0 +1,9 @@ +{% extends 'projects/to_user.md' %} + +{% block body %} +L'utilisateur {{ user }} a supprimé sa participation à {{ edition }}. + +Si besoin, vous pouvez contacter {{ user }} via son adresse email : {{ user.email }}. + +Vous pouvez télécharger la feuille des inscrits mise à jour sur [le site d'administration]({{ editionform_admin_url }}). +{% endblock %} diff --git a/projects/templates/projects/organizers_participation_deleted.md b/projects/templates/projects/organizers_participation_deleted.md new file mode 100644 index 0000000000000000000000000000000000000000..edd36920c1658e8a618743e69cf106b74e8c3fa0 --- /dev/null +++ b/projects/templates/projects/organizers_participation_deleted.md @@ -0,0 +1,9 @@ +{% extends 'projects/to_user.md' %} + +{% block body %} +L'utilisateur {{ user }} a annulé sa participation à {{ edition }}. + +Si besoin, vous pouvez contacter {{ user }} via son adresse email : {{ user.email }}. + +Vous pouvez télécharger la feuille des inscrits mise à jour sur [le site d'administration]({{ editionform_admin_url }}). +{% endblock %} diff --git a/projects/templates/projects/organizers_participation_received.md b/projects/templates/projects/organizers_participation_received.md new file mode 100644 index 0000000000000000000000000000000000000000..98f48d8199e7fc844e374be452a1185d17a09cea --- /dev/null +++ b/projects/templates/projects/organizers_participation_received.md @@ -0,0 +1,7 @@ +{% extends 'projects/to_user.md' %} + +{% block body %} +{{ user }} s'est inscrit à {{ edition }}. + +Vous pouvez télécharger la feuille des inscrits mise à jour sur [le site d'administration]({{ editionform_admin_url }}). +{% endblock %} diff --git a/projects/templates/projects/participation_accepted.md b/projects/templates/projects/participation_accepted.md new file mode 100644 index 0000000000000000000000000000000000000000..2774311572aaee752fa24ee787c4da28197ae3b2 --- /dev/null +++ b/projects/templates/projects/participation_accepted.md @@ -0,0 +1,8 @@ +{% extends 'projects/to_user.md' %} + +{% block body %} + +Nous avons le plaisir de te confirmer ta participation à {{ edition }} ! 🎉 + +Nous te recontacterons très prochainement pour te communiquer les derniers détails pratiques. +{% endblock %} diff --git a/projects/templates/projects/participation_cancelled.md b/projects/templates/projects/participation_cancelled.md new file mode 100644 index 0000000000000000000000000000000000000000..58eef49ca2a93650c11608d5fb16a4019f019d42 --- /dev/null +++ b/projects/templates/projects/participation_cancelled.md @@ -0,0 +1,9 @@ +{% extends 'projects/to_user.md' %} + +{% block body %} +Ta participation à {{ edition }} a bien été annulée. + +Si tu souhaites réactiver ta demande de participation, tu peux le faire +avant le {{ edition.edition_form.deadline | date }} dans la section +[Mes inscriptions]({{ edition.get_registration_url }}). +{% endblock %} diff --git a/projects/templates/projects/participation_deleted.md b/projects/templates/projects/participation_deleted.md new file mode 100644 index 0000000000000000000000000000000000000000..def88a1caee4dcc3c06f462eaf2cf1743fd7229f --- /dev/null +++ b/projects/templates/projects/participation_deleted.md @@ -0,0 +1,7 @@ +{% extends 'projects/to_user.md' %} + +{% block body %} +Ta participation à {{ edition }} a bien été supprimée. + +Si tu souhaites finalement participer à ce projet, tu devras te réinscrire en te rendant sur [l'espace projets]({{ edition.get_projects_site_url }}). +{% endblock %} diff --git a/projects/templates/projects/participation_received.md b/projects/templates/projects/participation_received.md new file mode 100644 index 0000000000000000000000000000000000000000..37609f270e1b49109c0b86e764ba7090c78f1ee5 --- /dev/null +++ b/projects/templates/projects/participation_received.md @@ -0,0 +1,20 @@ +{% extends 'projects/to_user.md' %} + +{% block body %} +Tu as demandé t'inscrire à {{ edition }} via l'espace projets. + +Nous avons bien reçu ta demande et allons vérifier ton dossier dès que possible. + +{% if edition.edition_form.form.files.count %} +📖 **Rappel** : l'inscription à ce projet nécessite de fournir des documents complémentaires. + +🔗 Tu peux télécharger ces documents à tout moment en te rendant dans la section [Mes inscriptions]({{ edition.get_registration_url }}). + +Fais-nous parvenir ces documents **impérativement avant le {{ edition.edition_form.deadline | date }}** à l'adresse suivante : + +{{ edition.edition_form.recipient.user.get_full_name }} +{{ edition.edition_form.recipient.address }} + +⚠️ Nous ne pourrons valider ton dossier qu'une fois ces documents reçus. +{% endif %} +{% endblock %} diff --git a/projects/templates/projects/participation_rejected.md b/projects/templates/projects/participation_rejected.md new file mode 100644 index 0000000000000000000000000000000000000000..e55a80cc09322d4754e880273f0cc7ec77bc8d66 --- /dev/null +++ b/projects/templates/projects/participation_rejected.md @@ -0,0 +1,9 @@ +{% extends 'projects/to_user.md' %} + +{% block body %} +Nous avons le regret de t'annoncer qu'en raison du nombre de places limité, +nous n'avons pas pu retenir ta demande de participation à {{ edition }}. 😔 + +Ne t'en fais pas ! Tu as été placé sur la liste d'attente et +nous te recontacterons si des places se libèrent suite à des désistements. +{% endblock %} diff --git a/projects/templates/projects/participation_valid.md b/projects/templates/projects/participation_valid.md new file mode 100644 index 0000000000000000000000000000000000000000..f31c3b649cac2fa9ce734ec1ce2e47c832c9d3f8 --- /dev/null +++ b/projects/templates/projects/participation_valid.md @@ -0,0 +1,10 @@ +{% extends 'projects/to_user.md' %} + +{% block body %} +Nous venons de valider ton dossier pour {{ edition }} : celui-ci est bien complet ! ✅ + +Une fois la période des inscriptions terminée, nous étudierons chaque demande +avec soin pour établir la liste des inscrits. Cela se fera à partir du {{ edition.edition_form.deadline | date }}. + +Nous te recontacterons alors pour te confirmer ta participation. +{% endblock %} diff --git a/projects/templates/projects/to_user.md b/projects/templates/projects/to_user.md new file mode 100644 index 0000000000000000000000000000000000000000..00fc3dffc3de9c18ca092ddeaa54f288df36f031 --- /dev/null +++ b/projects/templates/projects/to_user.md @@ -0,0 +1,11 @@ +{% extends 'mails/notification.md' %} + +{% block greeting %} +Bonjour{% if participation.user.first_name %} {{ participation.user.first_name }}{% endif %}, +{% endblock %} + +{% block signature %} +À très bientôt, + +Les organisateurs +{% endblock %} diff --git a/projects/views.py b/projects/views.py index 92e1e5a499e02d9d47043a9c1e87bc96dcfc21a2..95227ceff466924ce6c41de627afbb31235c9f23 100644 --- a/projects/views.py +++ b/projects/views.py @@ -197,25 +197,9 @@ class EditionViewSet(viewsets.ReadOnlyModelViewSet): "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/" } } @@ -305,23 +289,9 @@ class EditionViewSet(viewsets.ReadOnlyModelViewSet): "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/" }, "deadline": "2018-07-29", diff --git a/register/admin.py b/register/admin.py index a33c425f9f23a69c7f17bf666e0fd7e95955c1b0..fa128f3a0bc4369d2d89b303696a4ab1a3b155d1 100644 --- a/register/admin.py +++ b/register/admin.py @@ -1,36 +1,15 @@ """Register admin panels.""" from django.contrib import admin -from django.utils.html import format_html -from core.admin import AutocompleteAddressMixin -from .models import Registration, EmergencyContact +from .models import Registration # Register your models here. @admin.register(Registration) -class RegistrationAdmin(AutocompleteAddressMixin, admin.ModelAdmin): +class RegistrationAdmin(admin.ModelAdmin): """Admin panel for registrations.""" - list_display = ('last_name', 'first_name', 'school', 'grade', 'submitted') + list_display = ('last_name', 'first_name', 'submitted') readonly_fields = ('submitted',) - list_filter = ('submitted', 'school', 'grade',) - autocomplete_fields = ('emergency_contact', 'school',) - - -@admin.register(EmergencyContact) -class EmergencyContactAdmin(admin.ModelAdmin): - """Admin panel for emergency contacts.""" - - list_display = ('last_name', 'first_name', - 'email', 'home_phone', 'mobile_phone', 'related_student',) - search_fields = ('last_name', 'first_name',) - - def related_student(self, obj): - """Link to the contact's registration object.""" - url = '/admin/register/registration/{}'.format(obj.registration.pk) - return format_html( - '<a href="{}">{}</a>', - url, str(obj.registration) - ) - related_student.short_description = "Inscription administrative associée" + list_filter = ('submitted',) diff --git a/register/factory.py b/register/factory.py index 7d2b7145a713ccb72cf0391461b1455a0724258f..973d6e793e7b4abeef18708936f0b0eeef3b44fd 100644 --- a/register/factory.py +++ b/register/factory.py @@ -1,35 +1,13 @@ """Register factories.""" -import random - import factory import factory.django -from core.factory import AddressFactory from utils import printable_only from . import models -class EmergencyContactFactory(factory.DjangoModelFactory): - """Emergency contact object factory.""" - - class Meta: # noqa - model = models.EmergencyContact - - first_name = factory.Faker('first_name', locale='fr') - last_name = factory.Faker('last_name', locale='fr') - - @factory.lazy_attribute - def email(self): - return '{}.{}@fake.net'.format( - printable_only(self.first_name.lower()), - printable_only(self.last_name.lower())) - - home_phone = factory.Faker('phone_number', locale='fr') - mobile_phone = factory.Faker('phone_number', locale='fr') - - class RegistrationFactory(factory.DjangoModelFactory): """Registration object factory.""" @@ -46,13 +24,4 @@ class RegistrationFactory(factory.DjangoModelFactory): printable_only(self.first_name.lower()), printable_only(self.last_name.lower())) - phone = factory.Faker('phone_number', locale='fr') - date_of_birth = factory.Faker('past_date', start_date='-20y') - address = factory.SubFactory(AddressFactory) - emergency_contact = factory.SubFactory(EmergencyContactFactory) - - @factory.lazy_attribute - def grade(self): - level = random.choice(['Seconde', 'Première', 'Terminale']) - section = random.choice(['S', 'L', 'ES', 'Pro']) - return f'{level} {section}' + phone_number = factory.Faker('phone_number', locale='fr') diff --git a/register/migrations/0001_initial.py b/register/migrations/0001_initial.py index 5f884ead0b5826979939dc2fc596bf6904c58c84..f8ff25f7dcf676246bdf3b324632b826f3254899 100644 --- a/register/migrations/0001_initial.py +++ b/register/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.3 on 2018-04-07 21:04 +# Generated by Django 2.0.7 on 2018-09-11 17:38 from django.db import migrations, models @@ -15,14 +15,15 @@ class Migration(migrations.Migration): name='Registration', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('first_name', models.CharField(max_length=50, verbose_name='prénom')), - ('last_name', models.CharField(max_length=50, verbose_name='nom')), - ('email', models.EmailField(max_length=254, verbose_name='adresse email')), - ('phone', models.CharField(blank=True, max_length=15, null=True, verbose_name='téléphone')), - ('date_of_birth', models.DateField(null=True, verbose_name='date de naissance')), - ('submitted', models.DateTimeField(auto_now_add=True, verbose_name='envoyé le')), + ('first_name', models.CharField(help_text='Prénom du lycéen (50 caractères max)', max_length=50, verbose_name='prénom')), + ('last_name', models.CharField(help_text='Nom du lycéen (50 caracèteres max)', max_length=50, verbose_name='nom')), + ('email', models.EmailField(help_text='Adresse email personnelle du lycéen. Note : doit être une adresse mail valide.', max_length=254, verbose_name='adresse email')), + ('submitted', models.DateTimeField(auto_now_add=True, help_text="Date d'envoi du dossier d'inscription", verbose_name='envoyé le')), + ('validated', models.BooleanField(default=False, help_text="Cocher pour valider le dossier d'inscription. Le lycéen pourra alors avoir accès à toutes les fonctionnalités associées à son profil.", verbose_name='validé')), ], options={ + 'verbose_name': "dossier d'inscription", + 'verbose_name_plural': "dossiers d'inscription", 'ordering': ('-submitted',), }, ), diff --git a/register/migrations/0002_auto_20180407_2314.py b/register/migrations/0002_auto_20180407_2314.py deleted file mode 100644 index 4b0647839441dd30a4c3fda89bbccbd8612b94bc..0000000000000000000000000000000000000000 --- a/register/migrations/0002_auto_20180407_2314.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-07 21:14 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='registration', - options={'ordering': ('-submitted',), 'verbose_name': 'inscription administrative', 'verbose_name_plural': 'inscriptions administratives'}, - ), - ] diff --git a/register/migrations/0002_registration_phone_number.py b/register/migrations/0002_registration_phone_number.py new file mode 100644 index 0000000000000000000000000000000000000000..14f20a952682c759e377f468d83a8627d1172ab4 --- /dev/null +++ b/register/migrations/0002_registration_phone_number.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.1 on 2018-09-20 08:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('register', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='registration', + name='phone_number', + field=models.CharField(default='', help_text='Numéro de téléphone du lycéen (20 caractères max)', max_length=20, verbose_name='téléphone'), + ), + ] diff --git a/register/migrations/0003_auto_20180407_2342.py b/register/migrations/0003_auto_20180407_2342.py deleted file mode 100644 index b156411d82df9d3db857559b34be250e839121fc..0000000000000000000000000000000000000000 --- a/register/migrations/0003_auto_20180407_2342.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-07 21:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0002_auto_20180407_2314'), - ] - - operations = [ - migrations.AlterField( - model_name='registration', - name='phone', - field=models.CharField(blank=True, max_length=30, null=True, verbose_name='téléphone'), - ), - ] diff --git a/register/migrations/0004_auto_20180408_1412.py b/register/migrations/0004_auto_20180408_1412.py deleted file mode 100644 index 9075386599a8d1c7b9a45977006930b744fd3e1f..0000000000000000000000000000000000000000 --- a/register/migrations/0004_auto_20180408_1412.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-08 12:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0003_auto_20180407_2342'), - ] - - operations = [ - migrations.AlterModelOptions( - name='registration', - options={'ordering': ('-submitted',), 'verbose_name': "dossier d'inscription", 'verbose_name_plural': "dossiers d'inscription"}, - ), - migrations.AlterField( - model_name='registration', - name='date_of_birth', - field=models.DateField(help_text='Date de naissance du lycéen', null=True, verbose_name='date de naissance'), - ), - migrations.AlterField( - model_name='registration', - name='email', - field=models.EmailField(help_text='Adresse email personnelle du lycéen (doit être une adresse mail valide)', max_length=254, verbose_name='adresse email'), - ), - migrations.AlterField( - model_name='registration', - name='first_name', - field=models.CharField(help_text='Prénom du lycéen (50 caractères max)', max_length=50, verbose_name='prénom'), - ), - migrations.AlterField( - model_name='registration', - name='last_name', - field=models.CharField(help_text='Nom du lycéen (50 caracèteres max)', max_length=50, verbose_name='nom'), - ), - migrations.AlterField( - model_name='registration', - name='phone', - field=models.CharField(blank=True, help_text="Numéro de téléphone personnel du lycéen (30 caracètres max). Note : le format n'est pas vérifié.", max_length=30, null=True, verbose_name='téléphone'), - ), - migrations.AlterField( - model_name='registration', - name='submitted', - field=models.DateTimeField(auto_now_add=True, help_text="Date d'envoi du dossier d'inscription", verbose_name='envoyé le'), - ), - ] diff --git a/register/migrations/0005_auto_20180408_1416.py b/register/migrations/0005_auto_20180408_1416.py deleted file mode 100644 index 21a2af050bf990bdc15284899ae1b98bd12e3894..0000000000000000000000000000000000000000 --- a/register/migrations/0005_auto_20180408_1416.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-08 12:16 - -import datetime -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0004_auto_20180408_1412'), - ] - - operations = [ - migrations.AlterField( - model_name='registration', - name='date_of_birth', - field=models.DateField(default=datetime.date(2018, 4, 8), help_text='Date de naissance du lycéen', verbose_name='date de naissance'), - preserve_default=False, - ), - ] diff --git a/register/migrations/0006_address.py b/register/migrations/0006_address.py deleted file mode 100644 index 5c8d5eaa7c31b0d0af653b96c775815cd2f73460..0000000000000000000000000000000000000000 --- a/register/migrations/0006_address.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-08 12:53 - -from django.db import migrations, models -import django_countries.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0005_auto_20180408_1416'), - ] - - operations = [ - migrations.CreateModel( - name='Address', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('line1', models.CharField(help_text='Numéro, voie, rue…', max_length=300, verbose_name='ligne 1')), - ('line2', models.CharField(blank=True, default='', help_text='Résidence, appartement, lieu-dit…', max_length=300, verbose_name='ligne 2')), - ('post_code', models.CharField(help_text="Code postal. Note : le format n'est pas vérifié.", max_length=20, verbose_name='code postal')), - ('city', models.CharField(help_text='Ville', max_length=100, verbose_name='ville')), - ('country', django_countries.fields.CountryField(default='FR', help_text='Pays (FR par défaut).', max_length=2)), - ], - ), - ] diff --git a/register/migrations/0007_auto_20180408_1457.py b/register/migrations/0007_auto_20180408_1457.py deleted file mode 100644 index 075ad46ce1bca6738ebdc695e318836624524147..0000000000000000000000000000000000000000 --- a/register/migrations/0007_auto_20180408_1457.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-08 12:57 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0006_address'), - ] - - operations = [ - migrations.AddField( - model_name='registration', - name='address', - field=models.ForeignKey(blank=True, help_text='Adresse du lycéen', null=True, on_delete=django.db.models.deletion.CASCADE, to='register.Address'), - ), - migrations.AddField( - model_name='registration', - name='emergency_contact', - field=models.CharField(blank=True, help_text="Contact en cas d'urgence Exemple : adresse mail ou numéro de téléphone d'un parent.", max_length=100, null=True, verbose_name="contact d'urgence"), - ), - ] diff --git a/register/migrations/0008_auto_20180408_1518.py b/register/migrations/0008_auto_20180408_1518.py deleted file mode 100644 index e5b81ba9d2c7d2e7bd51eff2c22546d5e9f93b96..0000000000000000000000000000000000000000 --- a/register/migrations/0008_auto_20180408_1518.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-08 13:18 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0007_auto_20180408_1457'), - ] - - operations = [ - migrations.AlterField( - model_name='registration', - name='address', - field=models.ForeignKey(blank=True, help_text='Adresse du lycéen', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Address'), - ), - migrations.DeleteModel( - name='Address', - ), - ] diff --git a/register/migrations/0009_auto_20180408_1525.py b/register/migrations/0009_auto_20180408_1525.py deleted file mode 100644 index 9931621ead48edc0c52106f7b313cb8b2f07d430..0000000000000000000000000000000000000000 --- a/register/migrations/0009_auto_20180408_1525.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-08 13:25 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0008_auto_20180408_1518'), - ] - - operations = [ - migrations.AlterField( - model_name='registration', - name='address', - field=models.ForeignKey(blank=True, help_text='Adresse du lycéen', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Address', verbose_name='adresse'), - ), - migrations.AlterField( - model_name='registration', - name='email', - field=models.EmailField(help_text='Adresse email personnelle du lycéen. Note : doit être une adresse mail valide.', max_length=254, verbose_name='adresse email'), - ), - migrations.AlterField( - model_name='registration', - name='emergency_contact', - field=models.CharField(blank=True, help_text="Contact en cas d'urgence (100 caractères max). Exemple : adresse mail ou numéro de téléphone d'un parent.", max_length=100, null=True, verbose_name="contact d'urgence"), - ), - ] diff --git a/register/migrations/0010_auto_20180414_0000.py b/register/migrations/0010_auto_20180414_0000.py deleted file mode 100644 index 3eb0211bc527310367cd8cc7ea455a66fd8c75fc..0000000000000000000000000000000000000000 --- a/register/migrations/0010_auto_20180414_0000.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-13 22:00 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0009_auto_20180408_1525'), - ] - - operations = [ - migrations.CreateModel( - name='EmergencyContact', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('first_name', models.CharField(help_text='Prénom du contact (50 caractères max).', max_length=50, verbose_name='prénom')), - ('last_name', models.CharField(help_text='Nom du contact (50 caractères max).', max_length=50, verbose_name='nom')), - ('contact', models.CharField(help_text='Téléphone, adresse email…', max_length=100)), - ], - options={ - 'verbose_name': "contact d'urgence", - 'verbose_name_plural': "contacts d'urgence", - 'ordering': ('last_name', 'first_name'), - }, - ), - migrations.AlterField( - model_name='registration', - name='emergency_contact', - field=models.ForeignKey(blank=True, help_text="Contact en cas d'urgence.", null=True, on_delete=django.db.models.deletion.CASCADE, to='register.EmergencyContact', verbose_name="contact d'urgence"), - ), - ] diff --git a/register/migrations/0011_auto_20180414_0010.py b/register/migrations/0011_auto_20180414_0010.py deleted file mode 100644 index 73bbea6416af40926d7f684dc611f4c9904b977b..0000000000000000000000000000000000000000 --- a/register/migrations/0011_auto_20180414_0010.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-13 22:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0010_auto_20180414_0000'), - ] - - operations = [ - migrations.AlterField( - model_name='emergencycontact', - name='first_name', - field=models.CharField(blank=True, default='', help_text='Prénom du contact (50 caractères max).', max_length=50, verbose_name='prénom'), - ), - migrations.AlterField( - model_name='emergencycontact', - name='last_name', - field=models.CharField(blank=True, default='', help_text='Nom du contact (50 caractères max).', max_length=50, verbose_name='nom'), - ), - ] diff --git a/register/migrations/0012_auto_20180414_0016.py b/register/migrations/0012_auto_20180414_0016.py deleted file mode 100644 index d889829098515e0272d8738d9c5fbf07d56dec92..0000000000000000000000000000000000000000 --- a/register/migrations/0012_auto_20180414_0016.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-13 22:16 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0011_auto_20180414_0010'), - ] - - operations = [ - migrations.AlterField( - model_name='registration', - name='emergency_contact', - field=models.OneToOneField(blank=True, help_text="Contact en cas d'urgence.", null=True, on_delete=django.db.models.deletion.CASCADE, to='register.EmergencyContact', verbose_name="contact d'urgence"), - ), - ] diff --git a/register/migrations/0013_auto_20180505_1046.py b/register/migrations/0013_auto_20180505_1046.py deleted file mode 100644 index a079b8ebf39b55c6285d3add9782050d4d08ec72..0000000000000000000000000000000000000000 --- a/register/migrations/0013_auto_20180505_1046.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-05 08:46 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0012_auto_20180414_0016'), - ] - - operations = [ - migrations.RemoveField( - model_name='emergencycontact', - name='contact', - ), - migrations.AddField( - model_name='emergencycontact', - name='email', - field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='adresse email'), - ), - migrations.AddField( - model_name='emergencycontact', - name='home_phone', - field=models.CharField(blank=True, max_length=50, null=True, verbose_name='téléphone fixe'), - ), - migrations.AddField( - model_name='emergencycontact', - name='mobile_phone', - field=models.CharField(blank=True, max_length=50, null=True, verbose_name='téléphone portable'), - ), - migrations.AlterField( - model_name='emergencycontact', - name='first_name', - field=models.CharField(help_text='Prénom du contact (50 caractères max).', max_length=50, verbose_name='prénom'), - ), - migrations.AlterField( - model_name='emergencycontact', - name='last_name', - field=models.CharField(help_text='Nom du contact (50 caractères max).', max_length=50, verbose_name='nom'), - ), - ] diff --git a/register/migrations/0014_registration_validated.py b/register/migrations/0014_registration_validated.py deleted file mode 100644 index df488838ff6ef139016a375cccceb08c5b69ee70..0000000000000000000000000000000000000000 --- a/register/migrations/0014_registration_validated.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-05 12:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0013_auto_20180505_1046'), - ] - - operations = [ - migrations.AddField( - model_name='registration', - name='validated', - field=models.BooleanField(default=False, help_text="Cocher pour valider le dossier d'inscription. Le lycéen pourra alors avoir accès à toutes les fonctionnalités associées à son profil.", verbose_name='validé'), - ), - ] diff --git a/register/migrations/0015_auto_20180505_1403.py b/register/migrations/0015_auto_20180505_1403.py deleted file mode 100644 index 7df3903f547fef9da4f70b09f993091a0a6d9e50..0000000000000000000000000000000000000000 --- a/register/migrations/0015_auto_20180505_1403.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-05 12:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0014_registration_validated'), - ] - - operations = [ - migrations.AlterField( - model_name='registration', - name='date_of_birth', - field=models.DateField(blank=True, help_text='Date de naissance du lycéen', null=True, verbose_name='date de naissance'), - ), - ] diff --git a/register/migrations/0016_auto_20180512_1047.py b/register/migrations/0016_auto_20180512_1047.py deleted file mode 100644 index 1653bbadc81706aa6734d122b505aee3f7c1b977..0000000000000000000000000000000000000000 --- a/register/migrations/0016_auto_20180512_1047.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-12 08:47 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('tutoring', '0004_auto_20180429_1159'), - ('register', '0015_auto_20180505_1403'), - ] - - operations = [ - migrations.AddField( - model_name='registration', - name='level', - field=models.CharField(blank=True, help_text='Classe du lycée (texte libre)', max_length=200, null=True), - ), - migrations.AddField( - model_name='registration', - name='school', - field=models.ForeignKey(blank=True, help_text='Lycée du lycéen', null=True, on_delete=django.db.models.deletion.SET_NULL, to='tutoring.School', verbose_name='lycée'), - ), - ] diff --git a/register/migrations/0017_auto_20180512_1050.py b/register/migrations/0017_auto_20180512_1050.py deleted file mode 100644 index e603ee30d3b36a5827cb9c36c76e25e18a8976d9..0000000000000000000000000000000000000000 --- a/register/migrations/0017_auto_20180512_1050.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-12 08:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0016_auto_20180512_1047'), - ] - - operations = [ - migrations.RemoveField( - model_name='registration', - name='level', - ), - migrations.AddField( - model_name='registration', - name='grade', - field=models.CharField(blank=True, help_text='Classe/filière du lycéen (texte libre)', max_length=200, null=True), - ), - ] diff --git a/register/migrations/0018_auto_20180512_1112.py b/register/migrations/0018_auto_20180512_1112.py deleted file mode 100644 index 07bcbcde3e33abec686d511159716956bd5f0445..0000000000000000000000000000000000000000 --- a/register/migrations/0018_auto_20180512_1112.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-12 09:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0017_auto_20180512_1050'), - ] - - operations = [ - migrations.AlterField( - model_name='registration', - name='grade', - field=models.CharField(blank=True, help_text='Classe/filière du lycéen (texte libre)', max_length=200, null=True, verbose_name='classe'), - ), - ] diff --git a/register/models.py b/register/models.py index 2d465803d846b27e872bffbadaf07d4d9d45d663..d4966c41200ee3a37b9b324b75b5519205b6dbbb 100644 --- a/register/models.py +++ b/register/models.py @@ -20,38 +20,11 @@ class Registration(models.Model): 'Adresse email personnelle du lycéen. ' 'Note : doit être une adresse mail valide.' )) - phone = models.CharField( - max_length=30, blank=True, null=True, verbose_name='téléphone', - help_text=( - "Numéro de téléphone personnel du lycéen (30 caracètres max). " - "Note : le format n'est pas vérifié." - )) - date_of_birth = models.DateField( - verbose_name='date de naissance', - help_text="Date de naissance du lycéen", - blank=True, null=True - ) - address = models.ForeignKey( - 'core.Address', on_delete=models.CASCADE, blank=True, null=True, - verbose_name='adresse', - help_text="Adresse du lycéen") - school = models.ForeignKey( - 'tutoring.School', on_delete=models.SET_NULL, null=True, blank=True, - verbose_name='lycée', - help_text='Lycée du lycéen', + phone_number = models.CharField( + max_length=20, verbose_name='téléphone', + help_text='Numéro de téléphone du lycéen (20 caractères max)', + blank=False, default='', ) - grade = models.CharField( - max_length=200, - verbose_name='classe', - help_text='Classe/filière du lycéen (texte libre)', - blank=True, - null=True, - ) - emergency_contact = models.OneToOneField( - 'EmergencyContact', - on_delete=models.CASCADE, blank=True, null=True, - verbose_name="contact d'urgence", - help_text="Contact en cas d'urgence.") submitted = models.DateTimeField( auto_now_add=True, verbose_name='envoyé le', help_text="Date d'envoi du dossier d'inscription") @@ -86,37 +59,3 @@ class Registration(models.Model): def __str__(self): return '{o.full_name} ({o.submitted})'.format(o=self) - - -class EmergencyContact(models.Model): - """Represents an emergency contact for a student.""" - - first_name = models.CharField( - 'prénom', max_length=50, - help_text='Prénom du contact (50 caractères max).' - ) - last_name = models.CharField( - 'nom', max_length=50, - help_text='Nom du contact (50 caractères max).' - ) - email = models.EmailField( - verbose_name='adresse email', - blank=True, null=True, - ) - home_phone = models.CharField( - 'téléphone fixe', max_length=50, - blank=True, null=True, - ) - mobile_phone = models.CharField( - 'téléphone portable', max_length=50, - blank=True, null=True, - ) - - def __str__(self): - """Represent the emergency contact by its full name.""" - return '{o.first_name} {o.last_name}'.format(o=self) - - class Meta: # noqa - verbose_name = "contact d'urgence" - verbose_name_plural = "contacts d'urgence" - ordering = ('last_name', 'first_name',) diff --git a/register/serializers.py b/register/serializers.py index 3e8c1b4bec14d1d425fb280480fb79d0baef0b65..01cea366ada0994d6e2b0d6ef2bd5ea185d32e11 100644 --- a/register/serializers.py +++ b/register/serializers.py @@ -1,30 +1,15 @@ """Register serializers.""" from django.contrib.auth import get_user_model -from django.db import transaction from rest_framework import serializers -from core.models import Address -from core.serializers import AddressSerializer -from tutoring.models import School -from profiles.models import Student - -from .models import EmergencyContact, Registration +from .models import Registration from .signals import registration_created User = get_user_model() -class EmergencyContactSerializer(serializers.ModelSerializer): - """Serializer for emergency contacts.""" - - class Meta: # noqa - model = EmergencyContact - fields = ('first_name', 'last_name', - 'email', 'home_phone', 'mobile_phone') - - class RegistrationSerializer(serializers.ModelSerializer): """Serializer for documents.""" @@ -33,28 +18,13 @@ class RegistrationSerializer(serializers.ModelSerializer): write_only=True, style={'input_type': 'password'}, ) - school = serializers.PrimaryKeyRelatedField( - label='Lycée', - help_text='Lycée du lycéen', - queryset=School.objects.all(), - required=False, - allow_null=True, - ) - address = AddressSerializer( - required=False, - help_text="Adresse du lycéen") - emergency_contact = EmergencyContactSerializer( - required=False, - help_text="Contact en cas d'urgence") validated = serializers.HiddenField(default=False) class Meta: # noqa model = Registration fields = ('id', 'email', 'password', - 'first_name', 'last_name', 'date_of_birth', 'phone', - 'school', 'grade', - 'submitted', 'validated', - 'address', 'emergency_contact',) + 'first_name', 'last_name', 'phone_number', + 'submitted', 'validated',) extra_kwargs = { 'submitted': {'read_only': True}, @@ -74,43 +44,15 @@ class RegistrationSerializer(serializers.ModelSerializer): - Build/save a user and a student profile """ password = validated_data.pop('password') - address_data = validated_data.pop('address', None) - emergency_contact_data = validated_data.pop('emergency_contact', None) - - # The following block will create a bunch of objects and save them - # in the database. We don't want them to be saved separately. - # => Use an atomic transaction to not save anything in case an - # exception is raised. - # (Hint: it disables the autocommit mode and commit all queries at - # the end of the "with" block.) - # See the Django docs on atomic transactions for more info. - with transaction.atomic(): - - # Create the address if given - if address_data: - address = Address.objects.create(**address_data) - else: - address = None - - # Create the emergency contact if given - if emergency_contact_data: - emergency_contact = EmergencyContact.objects.create( - **emergency_contact_data) - else: - emergency_contact = None - - registration = Registration.objects.create( - **validated_data, - address=address, - emergency_contact=emergency_contact, - ) - - # Fire a registration_created signal - registration_created.send( - sender=Registration, - instance=registration, - password=password, - ) + + registration = Registration.objects.create(**validated_data) + + # Fire a registration_created signal + registration_created.send( + sender=Registration, + instance=registration, + password=password, + ) return registration diff --git a/register/signals.py b/register/signals.py index 81e77a2a4dcc3c2ddc0889dd88423aa279acb9fa..05723916545d82fbd1d52383913b6884eefc2e92 100644 --- a/register/signals.py +++ b/register/signals.py @@ -22,12 +22,10 @@ def create_user_and_student(sender, instance: Registration, password=password, first_name=instance.first_name, last_name=instance.last_name, - date_of_birth=instance.date_of_birth, - phone_number=instance.phone, + phone_number=instance.phone_number, ) Student.objects.create( user=user, - school=instance.school, registration=instance, ) diff --git a/register/views.py b/register/views.py index 1ff4028b4185130e0795e7ee31af2f32aad0f48f..f7efb2810e2ea485f7a25bde791f1adaf4cacf79 100644 --- a/register/views.py +++ b/register/views.py @@ -26,28 +26,7 @@ class RegistrationViewSet( "email": "charles.dumont@example.net", "first_name": "Charles", "last_name": "Dumont", - "date_of_birth": null, - "phone": null, - "school": "0930965U", - "grade": "Première S", "submitted": "2018-05-05T14:15:10.998206+02:00", - "address": { - "line1": "88 bis rue Jules Guesde", - "line2": "", - "post_code": "93100", - "city": "Montreuil", - "country": { - "code": "FR", - "name": "France" - } - }, - "emergency_contact": { - "first_name": "Marie-Claude", - "last_name": "Perret", - "email": null, - "home_phone": "+33312344556", - "mobile_phone": null - } } ] @@ -60,49 +39,6 @@ class RegistrationViewSet( 1. Create a user for the student 2. Create the registration 3. Create a student profile and link it to the user and the registration. - - ### Date of birth - - Date of birth must be sent in a ISO-compliant format (in Javascript, - `Date.toISOString()` can be used for this). - - ### Address - - Address format is the following : - - { - "line1": "...", - "line2": "...", - "post_code": "...", - "city": "...", - "country": "..." - } - - `line2` : optional, default is `""`. - - `country` : optional, default is `"FR"`. Must be given as a - [country code](https://en.wikipedia.org/wiki/Country_code). - - ### Emergency contact - - Emergency contact format is the following: - - { - "first_name": "...", - "last_name": "...", - "email": "...", - "home_phone": "...", - "mobile_phone": "..." - } - - `email` must be a valid email address. Phone format for `home_phone` - and `mobile_phone` is not verified. - - ### School - - The value given must be the school's `uai_code`. - You can retrieve the list of available schools thanks to the - [schools-choices](#schools-choices) endpoint. """ queryset = Registration.objects.all() diff --git a/requirements.txt b/requirements.txt index 0db00a41c89108fbf81dbe23c667dceb1341cdb6..276fb91beef51d4861124dc26ce65bebd897a9ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,6 @@ gunicorn django-storages boto3 whitenoise -celery[redis] django-countries django-sendgrid-v5 django-filter diff --git a/supervisord.conf b/supervisord.conf deleted file mode 100644 index f01181a18e936e6f5d42311e2fb045c16d82d276..0000000000000000000000000000000000000000 --- a/supervisord.conf +++ /dev/null @@ -1,18 +0,0 @@ -[supervisord] - -[supervisorctl] - -[inet_http_server] -port = 127.0.0.1:9001 - -[rpcinterface:supervisor] -supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface - -[program:redis] -command = redis-server - -[program:celery] -command = celery -A oser_backend worker --beat -l info -stdout_logfile=celery.log -stderr_logfile=celery.log -autorestart=true diff --git a/tests/test_core/test_tasks.py b/tests/test_core/test_tasks.py deleted file mode 100644 index 1b36bdfc87e4f2ae3c874e3cc726595a70b5b97c..0000000000000000000000000000000000000000 --- a/tests/test_core/test_tasks.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Test Celery tasks.""" - -from django.test import TestCase -from celery.exceptions import TimeoutError - -from core.tasks import cleanmedia - - -class CleanMediaTaskTest(TestCase): - """Test the cleanmedia Celery task.""" - - def test_run_task(self): - try: - cleanmedia.delay().get(timeout=2) - except TimeoutError as e: - message = str(e) + ' Is the Celery worker running?' - raise TimeoutError(message) diff --git a/tests/test_profiles/test_student.py b/tests/test_profiles/test_student.py index 7ca9935d57c7c7017f225f0ecc61362827c6e2f9..0d9d65fc47e1619475c78325b8eb82f4af3641b2 100644 --- a/tests/test_profiles/test_student.py +++ b/tests/test_profiles/test_student.py @@ -1,9 +1,8 @@ """Student model tests.""" -from profiles.factory import StudentInTutoringGroupFactory +from profiles.factory import StudentFactory from profiles.models import Student from tests.utils import ModelTestCase -from tutoring.models import School, TutoringGroup from users.factory import UserFactory @@ -15,16 +14,6 @@ class StudentTestCase(ModelTestCase): 'user': { 'verbose_name': 'utilisateur', }, - 'tutoring_group': { - 'verbose_name': 'groupe de tutorat', - 'null': True, - 'blank': True, - }, - 'school': { - 'verbose_name': 'lycée', - 'null': True, - 'blank': True, - }, 'registration': { 'verbose_name': "dossier d'inscription", 'null': True, @@ -37,19 +26,11 @@ class StudentTestCase(ModelTestCase): @classmethod def setUpTestData(self): - self.obj = StudentInTutoringGroupFactory.create() + self.obj = StudentFactory.create() def test_user_relationship(self): self.assertEqual(self.obj, self.obj.user.student) - def test_school_relationship(self): - self.assertEqual(School.objects.get(), self.obj.school) - self.assertIn(self.obj, School.objects.get().students.all()) - - def test_tutoring_group_relationship(self): - self.assertEqual(TutoringGroup.objects.get(), self.obj.tutoring_group) - self.assertIn(self.obj, TutoringGroup.objects.get().students.all()) - def test_get_absolute_url(self): self.client.force_login(UserFactory.create()) url = self.obj.get_absolute_url() diff --git a/tests/test_profiles/test_student_api.py b/tests/test_profiles/test_student_api.py index 47237616dec3accdb997655ed8055521f543a782..204649b47eadba66ce7be8a9e17c09568244d664 100644 --- a/tests/test_profiles/test_student_api.py +++ b/tests/test_profiles/test_student_api.py @@ -1,7 +1,7 @@ """Student API tests.""" from rest_framework import status -from profiles.factory import StudentInTutoringGroupFactory +from profiles.factory import StudentFactory from profiles.serializers import StudentSerializer from tests.utils.api import HyperlinkedAPITestCase @@ -9,7 +9,7 @@ from tests.utils.api import HyperlinkedAPITestCase class StudentEndpointsTest(HyperlinkedAPITestCase): """Test access to the students endpoints.""" - factory = StudentInTutoringGroupFactory + factory = StudentFactory serializer_class = StudentSerializer def perform_list(self): @@ -31,14 +31,3 @@ class StudentEndpointsTest(HyperlinkedAPITestCase): self.assertRequiresAuth( self.perform_retrieve, expected_status_code=status.HTTP_200_OK) - - def test_retrieve_tutoring_group(self): - def perform_retrieve_tutoring_group(): - obj = self.factory.create() - response = self.client.get( - '/api/students/{}/tutoringgroup/'.format(obj.pk)) - return response - - self.assertRequiresAuth( - perform_retrieve_tutoring_group, - expected_status_code=status.HTTP_200_OK) diff --git a/tests/test_profiles/test_tutor_api.py b/tests/test_profiles/test_tutor_api.py index 154181776463b8e06ec6ddc9c3b32d26396b3f51..901c57a9daa76956f48c4e610d3647bfc3614c96 100644 --- a/tests/test_profiles/test_tutor_api.py +++ b/tests/test_profiles/test_tutor_api.py @@ -4,7 +4,6 @@ from rest_framework import status from profiles.factory import TutorFactory from profiles.serializers import TutorSerializer from tests.utils.api import HyperlinkedAPITestCase -from tutoring.factory import TutorTutoringGroupFactory class TutorEndpointsTest(HyperlinkedAPITestCase): @@ -32,16 +31,3 @@ class TutorEndpointsTest(HyperlinkedAPITestCase): self.assertRequiresAuth( self.perform_retrieve, expected_status_code=status.HTTP_200_OK) - - def test_list_tutoring_groups(self): - def perform_list_tutoring_groups(): - obj = self.factory.create() - # add tutor to several tutoring groups - TutorTutoringGroupFactory.create_batch(3, tutor=obj) - url = '/api/tutors/{}/tutoringgroups/'.format(obj.pk) - response = self.client.get(url) - return response - - self.assertRequiresAuth( - perform_list_tutoring_groups, - expected_status_code=status.HTTP_200_OK) diff --git a/tests/test_projects/test_signals.py b/tests/test_projects/test_signals.py new file mode 100644 index 0000000000000000000000000000000000000000..52649c99a6fd000c75b5240a26935a4ccbfdd3e1 --- /dev/null +++ b/tests/test_projects/test_signals.py @@ -0,0 +1,69 @@ +"""Test the projects app signals.""" + +from django.test import TestCase +from django.utils.timezone import now +from tests.utils.mixins import SignalTestMixin + +from dynamicforms.models import Form +from profiles.factory import TutorFactory +from projects.factory import (EditionFactory, ParticipationFactory, + ProjectFactory) +from projects.models import EditionForm, Participation, EditionOrganizer +from projects.signals import (accepted, cancelled, deleted, pending, rejected, + valid) + + +class NotifyParticipationTest(SignalTestMixin, TestCase): + """Test project participation signal handlers.""" + + def setUp(self): + # Create all objects that need to exist for rendering emails + project = ProjectFactory.create(name='Focus Europe') + recipient = TutorFactory.create() + self.edition = EditionFactory.create(project=project, year=2018) + EditionOrganizer.objects.create( + user=recipient.user, + edition=self.edition) + form = Form.objects.create(title=f'Inscriptions à {self.edition}') + EditionForm.objects.create( + edition=self.edition, + form=form, + recipient=recipient, + deadline=now(), + ) + self.obj = self.create_obj() + + def create_obj(self): + return ParticipationFactory.create(edition=self.edition) + + def change(self, state): + self.obj.state = state + self.obj.save() + + def test_create_participation_pending_called(self): + with self.assertCalled(pending): + self.create_obj() + + def test_save_without_changing_state_not_called(self): + with self.assertNotCalled(pending): + self.obj.save() + + def test_valid_called(self): + with self.assertCalled(valid): + self.change(Participation.STATE_VALIDATED) + + def test_accepted_called(self): + with self.assertCalled(accepted): + self.change(Participation.STATE_ACCEPTED) + + def test_rejected_called(self): + with self.assertCalled(rejected): + self.change(Participation.STATE_REJECTED) + + def test_cancelled_called(self): + with self.assertCalled(cancelled): + self.change(Participation.STATE_CANCELLED) + + def test_deleted_called(self): + with self.assertCalled(deleted): + self.obj.delete() diff --git a/tests/test_register/test_emergency_contact.py b/tests/test_register/test_emergency_contact.py deleted file mode 100644 index 80a9748c83f45cfcb554d67ebc5a2f0086dd5639..0000000000000000000000000000000000000000 --- a/tests/test_register/test_emergency_contact.py +++ /dev/null @@ -1,49 +0,0 @@ -"""EmergencyContact model tests.""" - -from register.models import EmergencyContact -from register.factory import EmergencyContactFactory -from tests.utils import ModelTestCase - - -class EmergencyContactTest(ModelTestCase): - """Test the EmergencyContact model.""" - - model = EmergencyContact - field_tests = { - 'first_name': { - 'max_length': 50, - 'verbose_name': 'prénom', - }, - 'last_name': { - 'max_length': 50, - 'verbose_name': 'nom', - }, - 'email': { - 'verbose_name': 'adresse email', - 'blank': True, - 'null': True, - }, - 'home_phone': { - 'verbose_name': 'téléphone fixe', - 'blank': True, - 'null': True, - }, - 'mobile_phone': { - 'verbose_name': 'téléphone portable', - 'blank': True, - 'null': True, - }, - } - model_tests = { - 'ordering': ('last_name', 'first_name',), - 'verbose_name': "contact d'urgence", - 'verbose_name_plural': "contacts d'urgence", - } - - @classmethod - def setUpTestData(cls): - cls.obj = EmergencyContactFactory.create() - - def test_str(self): - expected = '{o.first_name} {o.last_name}'.format(o=self.obj) - self.assertEqual(expected, str(self.obj)) diff --git a/tests/test_register/test_registration.py b/tests/test_register/test_registration.py index b1dd44e2c5ad57a652fe41d0fe1c9d8a56f88c90..2691697bace11bb8c897d7ed1cafab2512f1c4c5 100644 --- a/tests/test_register/test_registration.py +++ b/tests/test_register/test_registration.py @@ -18,17 +18,6 @@ class RegistrationTest(ModelTestCase): 'max_length': 50, 'verbose_name': 'nom', }, - 'date_of_birth': { - 'blank': True, - 'null': True, - 'verbose_name': 'date de naissance', - }, - 'phone': { - 'max_length': 30, - 'blank': True, - 'null': True, - 'verbose_name': 'téléphone', - }, 'email': { 'verbose_name': 'adresse email', }, @@ -40,16 +29,6 @@ class RegistrationTest(ModelTestCase): 'verbose_name': 'validé', 'default': False, }, - 'address': { - 'blank': True, - 'null': True, - 'verbose_name': 'adresse', - }, - 'emergency_contact': { - 'blank': True, - 'null': True, - 'verbose_name': "contact d'urgence", - }, } model_tests = { 'ordering': ('-submitted',), diff --git a/tests/test_register/test_registration_api.py b/tests/test_register/test_registration_api.py index aaab7080792b267a47ba3024a46446c63ef92597..84389b07a62ddc8e5a6f557827bf1798fdfba1ba 100644 --- a/tests/test_register/test_registration_api.py +++ b/tests/test_register/test_registration_api.py @@ -4,14 +4,10 @@ from django.contrib.auth import get_user_model from rest_framework import status from tests.utils import SimpleAPITestCase -from core.factory import AddressFactory -from core.serializers import AddressSerializer from profiles.models import Student -from register.factory import EmergencyContactFactory, RegistrationFactory +from register.factory import RegistrationFactory from register.models import Registration -from register.serializers import (EmergencyContactSerializer, - RegistrationSerializer) -from tutoring.factory import SchoolFactory, TutoringGroupFactory +from register.serializers import RegistrationSerializer from users.factory import UserFactory User = get_user_model() @@ -42,6 +38,7 @@ class RegistrationCreateTest(SimpleAPITestCase): There is some complex logic which requires a few dedicated tests. """ + factory = RegistrationFactory serializer_class = RegistrationSerializer @@ -80,40 +77,6 @@ class RegistrationCreateTest(SimpleAPITestCase): user = User.objects.get(email=email) self.assertEqual(obj.student, Student.objects.get(user=user)) - def test_create_with_address_and_emergency_contact(self): - data = self.get_create_data() - address = AddressFactory.build() - emergency_contact = EmergencyContactFactory.build() - data['address'] = AddressSerializer(address).data - data['emergency_contact'] = EmergencyContactSerializer( - emergency_contact).data - - response = self._create(data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, - response.data) - - # Verify that address and emergency contact were set on registration - pk = response.data['id'] - obj = Registration.objects.get(pk=pk) - self.assertEqual(obj.address.line1, address.line1) - self.assertEqual(obj.emergency_contact.first_name, - emergency_contact.first_name) - - def test_create_with_school(self): - school = SchoolFactory.create() - data = self.get_create_data() - data['school'] = school.pk - - response = self._create(data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, - response.data) - - # Verify that school was set on registration and student - pk = response.data['id'] - obj = Registration.objects.get(pk=pk) - self.assertEqual(obj.school.pk, school.pk) - self.assertEqual(obj.student.school.pk, school.pk) - def test_if_email_of_existing_user_returns_bad_request(self): user = UserFactory.create() data = self.get_create_data() diff --git a/tests/test_tutoring/__init__.py b/tests/test_tutoring/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/tests/test_tutoring/test_school.py b/tests/test_tutoring/test_school.py deleted file mode 100644 index b6c2b2408d67b42ccfb6c632b79c042c17f586ca..0000000000000000000000000000000000000000 --- a/tests/test_tutoring/test_school.py +++ /dev/null @@ -1,59 +0,0 @@ -"""School model tests.""" - -from django.contrib.auth import get_user_model -from tutoring.factory import SchoolFactory -from users.factory import UserFactory -from tests.utils import ModelTestCase - -import tutoring.models - -User = get_user_model() - - -class SchoolTest(ModelTestCase): - """Test the School model.""" - - model = tutoring.models.School - field_tests = { - 'uai_code': { - 'unique': True, - 'primary_key': True, - 'max_length': 8, - 'verbose_name': 'code UAI', - }, - 'name': { - 'verbose_name': 'nom', - }, - 'address': { - 'verbose_name': 'adresse', - 'null': True, - } - } - model_tests = { - 'verbose_name': 'lycée', - 'ordering': ('name',), - } - - @classmethod - def setUpTestData(cls): - cls.obj = SchoolFactory.create(name='Lycée Michelin') - - def test_uai_code_help_text_indicates_format(self): - help_text = self.model._meta.get_field('uai_code').help_text - self.assertIsNotNone(help_text) - self.assertIn('UAI', help_text) - self.assertIn('ex-RNE', help_text) - self.assertIn('7 chiffres', help_text) - self.assertIn('une lettre', help_text) - - def test_uai_code_help_text_indicates_where_to_find_it(self): - help_text = self.model._meta.get_field('uai_code').help_text - self.assertIn("site du ministère de l'Éducation Nationale", help_text) - - def test_get_absolute_url(self): - self.client.force_login(UserFactory.create()) - url = self.obj.get_absolute_url() - expected = '/api/schools/{}/'.format(self.obj.uai_code) - self.assertEqual(url, expected) - response = self.client.get(url) - self.assertEqual(200, response.status_code) diff --git a/tests/test_tutoring/test_school_api.py b/tests/test_tutoring/test_school_api.py deleted file mode 100644 index 81269f5636d759c69236577328d039a6b947abd6..0000000000000000000000000000000000000000 --- a/tests/test_tutoring/test_school_api.py +++ /dev/null @@ -1,44 +0,0 @@ -"""School API tests.""" - -from rest_framework import status -from tutoring.factory import SchoolFactory -from tests.utils.api import HyperlinkedAPITestCase - -from tutoring.serializers import SchoolSerializer - - -class SchoolEndpointsTest(HyperlinkedAPITestCase): - """Test access to the school endpoints.""" - - factory = SchoolFactory - serializer_class = SchoolSerializer - - def perform_list(self): - url = '/api/schools/' - response = self.client.get(url) - return response - - def test_list(self): - self.assertRequestResponse( - self.perform_list, - user=None, - expected_status_code=status.HTTP_200_OK) - - def perform_retrieve(self): - obj = self.factory.create() - url = '/api/schools/{obj.pk}/'.format(obj=obj) - response = self.client.get(url) - return response - - def test_retrieve(self): - self.assertRequiresAuth( - self.perform_retrieve, - expected_status_code=status.HTTP_200_OK) - - def test_choices_returns_list_of_uai_codes_and_names(self): - self.factory.create_batch(5) - url = '/api/schools/choices/' - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_200_OK) - for item in response.data: - self.assertSetEqual(set(item), {'uai_code', 'name'}) diff --git a/tests/test_tutoring/test_tutoring_group.py b/tests/test_tutoring/test_tutoring_group.py deleted file mode 100644 index 4f4c33fa0be333c8fd4febc77a9c7f92f87c7433..0000000000000000000000000000000000000000 --- a/tests/test_tutoring/test_tutoring_group.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Tutoring group model tests.""" - -from tests.utils import ModelTestCase -from tutoring.factory import TutoringGroupFactory, TutorTutoringGroupFactory -from tutoring.models import TutoringGroup -from users.factory import UserFactory -from profiles.factory import TutorFactory - - -class TutoringGroupTest(ModelTestCase): - """Test the TutoringGroup model.""" - - model = TutoringGroup - field_tests = { - 'name': { - 'verbose_name': 'nom', - 'max_length': 200, - }, - 'tutors': { - 'verbose_name': 'tuteurs', - 'blank': True, - }, - } - model_tests = { - 'verbose_name': 'groupe de tutorat', - 'verbose_name_plural': 'groupes de tutorat', - 'ordering': ('name',), - } - - @classmethod - def setUpTestData(cls): - cls.obj = TutoringGroupFactory.create() - - def test_get_absolute_url(self): - self.client.force_login(UserFactory.create()) - response = self.client.get( - '/api/groups/{}/'.format(self.obj.pk)) - self.assertEqual(200, response.status_code) - - def test_tutors_many_to_many_relationship(self): - tutor = TutorFactory.create() - membership = TutorTutoringGroupFactory.create( - tutor=tutor, - tutoring_group=self.obj) - tutor = membership.tutor - self.assertIn(tutor, self.obj.tutors.all()) - self.assertIn(self.obj, tutor.tutoring_groups.all()) diff --git a/tests/test_tutoring/test_tutoring_group_api.py b/tests/test_tutoring/test_tutoring_group_api.py deleted file mode 100644 index e7d9905c924c44a09134ab1501d400e509d6c139..0000000000000000000000000000000000000000 --- a/tests/test_tutoring/test_tutoring_group_api.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Tutoring group API tests.""" - -from rest_framework import status -from tutoring.factory import TutoringGroupFactory -from tests.utils.api import HyperlinkedAPITestCase - -from tutoring.serializers import TutoringGroupSerializer - - -class TutoringGroupEndpointsTest(HyperlinkedAPITestCase): - """Test access to the tutoring group endpoints.""" - - factory = TutoringGroupFactory - serializer_class = TutoringGroupSerializer - - def perform_list(self): - url = '/api/groups/' - response = self.client.get(url) - return response - - def test_list_requires_authentication(self): - self.assertRequiresAuth(self.perform_list, - expected_status_code=status.HTTP_200_OK) - - def perform_retrieve(self): - obj = self.factory.create() - url = '/api/groups/{obj.pk}/'.format(obj=obj) - response = self.client.get(url) - return response - - def test_retrieve_requires_authentication(self): - self.assertRequiresAuth(self.perform_retrieve, - expected_status_code=status.HTTP_200_OK) - - def test_list_students(self): - pass # TODO - - def test_add_student(self): - pass # TODO - - def test_remove_student(self): - pass # TODO - - def test_list_tutors(self): - pass # TODO - - def test_add_tutor(self): - pass # TODO - - def test_remove_tutor(self): - pass # TODO - - def test_list_meetings(self): - pass # TODO - - def test_list_past_meetings(self): - pass # TODO - - def test_list_next_meetings(self): - pass # TODO - - def test_add_meeting(self): - pass # TODO - - def test_remove_meeting(self): - pass # TODO diff --git a/tests/test_tutoring/test_tutoring_session.py b/tests/test_tutoring/test_tutoring_session.py deleted file mode 100644 index 972daff3075dd44755d599947ce45564584e01f3..0000000000000000000000000000000000000000 --- a/tests/test_tutoring/test_tutoring_session.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Tutoring session model tests.""" - -import tutoring.models -from tests.utils import ModelTestCase -from tutoring.factory import TutoringSessionFactory - - -class TutoringSessionTest(ModelTestCase): - """Test the TutoringSession model.""" - - model = tutoring.models.TutoringSession - field_tests = { - 'date': { - 'verbose_name': 'date', - }, - 'start_time': { - 'verbose_name': 'heure de début', - }, - 'end_time': { - 'verbose_name': 'heure de fin', - }, - 'tutoring_group': { - 'verbose_name': 'groupe de tutorat', - }, - } - model_tests = { - 'verbose_name': 'séance de tutorat', - 'verbose_name_plural': 'séances de tutorat', - 'ordering': ('date', 'start_time'), - } - - @classmethod - def setUpTestData(self): - self.obj = TutoringSessionFactory.create() - - def test_get_absolute_url(self): - url = '/api/sessions/{}/'.format(self.obj.pk) - response = self.client.get(url) - self.assertEqual(200, response.status_code) - - def test_tutoring_group_one_to_many_relationship(self): - self.assertEqual(tutoring.models.TutoringGroup.objects.get(), - self.obj.tutoring_group) - self.assertIn(self.obj, - tutoring.models.TutoringGroup.objects.get() - .sessions.all()) diff --git a/tests/test_users/test_user.py b/tests/test_users/test_user.py index d7ff66b53d4b84f2adce276effd620b35de90565..79a56fcd242c8118cbb129f933a0fa51e17d6ef5 100644 --- a/tests/test_users/test_user.py +++ b/tests/test_users/test_user.py @@ -56,22 +56,6 @@ class UserModelTest(ModelTestCase): 'blank': False, 'null': False, }, - 'date_of_birth': { - 'verbose_name': 'date de naissance', - 'blank': True, - 'null': True, - }, - 'gender': { - 'verbose_name': 'sexe', - 'max_length': 1, - 'choices': (('M', 'Homme'), ('F', 'Femme')), - 'blank': True, - }, - 'phone_number': { - 'verbose_name': 'téléphone', - 'blank': True, - 'null': True, - }, 'profile_type': { 'verbose_name': 'type de profil', 'blank': False, @@ -80,6 +64,10 @@ class UserModelTest(ModelTestCase): (User.PROFILE_STUDENT, 'Lycéen'), (User.PROFILE_TUTOR, 'Tuteur'), ) + }, + 'phone_number': { + 'blank': True, + 'null': True, } } model_tests = { diff --git a/tests/test_visits/test_signals.py b/tests/test_visits/test_signals.py index 01027bc744db579ed305fc79f5bb90a585fb8133..188bd95bf69c6669e32b1999182c07aeae732141 100644 --- a/tests/test_visits/test_signals.py +++ b/tests/test_visits/test_signals.py @@ -4,7 +4,6 @@ from django.test import TestCase from tests.utils.mixins import SignalTestMixin from mails.signals import notification_sent -from users.factory import UserFactory from visits.factory import ParticipationFactory, VisitFactory from visits.notifications import ConfirmParticipation from visits.signals import accepted_changed diff --git a/tests/utils/mixins.py b/tests/utils/mixins.py index b7cd15e056ef14ec1f40db339caf9ba3b551c101..37de2965a851af517cbfce6ff3420726b565c1e5 100644 --- a/tests/utils/mixins.py +++ b/tests/utils/mixins.py @@ -52,7 +52,7 @@ class SignalTestMixin: Pass `sender` to check the signal's sender too. """ - called = {'value': None} + called = {'value': False} def listen(sender, **kwargs): called['value'] = True @@ -65,3 +65,17 @@ class SignalTestMixin: self.assertTrue(called['value']) if 'sender' in kwargs: self.assertEqual(called['sender'], kwargs['sender']) + + @contextmanager + def assertNotCalled(self, signal): + """Verify that a signal is NOT called.""" + called = {'value': False} + + def listen(sender, **kwargs): + called['value'] = True + + signal.connect(listen) + yield + signal.disconnect(listen) + + self.assertFalse(called['value']) diff --git a/tutoring/__init__.py b/tutoring/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/tutoring/admin.py b/tutoring/admin.py deleted file mode 100644 index e3a0d3ce9b8867cd9f727578a5c52e0396425d03..0000000000000000000000000000000000000000 --- a/tutoring/admin.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Tutoring admin panel configuration.""" - -from django.contrib import admin -from django.shortcuts import reverse -from django.utils.html import format_html -from core.admin import AutocompleteAddressMixin -from profiles.models import Student -from .models import TutoringGroup, School, TutoringSession - -# Register your models here. - - -class TutoringGroupMembershipInline(admin.TabularInline): - """Inline for tutoring group membership.""" - - model = TutoringGroup.tutors.through - extra = 0 - - -class TutoringGroupStudentsInline(admin.TabularInline): - """Inline to show students in a tutoring group.""" - - model = Student - extra = 0 - max_num = 0 - readonly_fields = ('user', 'school',) - can_delete = False - - -@admin.register(TutoringGroup) -class TutoringGroupAdmin(admin.ModelAdmin): - """Tutoring group admin panel.""" - - inlines = [ - TutoringGroupStudentsInline, - TutoringGroupMembershipInline, - ] - search_fields = ('name',) - - class Meta: # noqa - model = TutoringGroup - - -@admin.register(School) -class SchoolAdmin(AutocompleteAddressMixin, admin.ModelAdmin): - """School admin panel.""" - - list_display = ('__str__', 'uai_code', - 'get_student_count', 'get_groups_count') - search_fields = ('name',) - - def get_student_count(self, obj): - """Display number of students.""" - return obj.students.count() - get_student_count.short_description = 'Nombre de lycéens' - - def get_groups_count(self, obj): - """Display number of tutoring groups.""" - return obj.tutoring_groups.count() - get_groups_count.short_description = 'Nombre de groupes de tutorat' - - def get_readonly_fields(self, request, obj=None): - """Make the UAI code (school's ID) read-only when editing.""" - if obj is not None: - return ['uai_code'] - return [] - - class Meta: # noqa - model = School - - -@admin.register(TutoringSession) -class TutoringSessionAdmin(admin.ModelAdmin): - """Tutoring session admin panel.""" - - list_display = ('__str__', 'link_tutoring_group', 'link_school', 'date',) - autocomplete_fields = ('tutoring_group',) - - def link_tutoring_group(self, obj): - link = reverse('admin:tutoring_tutoringgroup_change', - args=[obj.tutoring_group.pk]) - s = str(obj.tutoring_group) - return format_html("<a href='{link}'>{s}</a>", link=link, s=s) - link_tutoring_group.admin_order_field = 'groupe de tutorat' - link_tutoring_group.short_description = 'groupe de tutorat' - - def link_school(self, obj): - if not obj.school: - return None - link = reverse('admin:tutoring_school_change', - args=[obj.school.pk]) - s = str(obj.school) - return format_html("<a href='{link}'>{s}</a>", link=link, s=s) - link_school.admin_order_field = 'lycée' - link_school.short_description = 'lycée' - - class Meta: # noqa - model = TutoringSession diff --git a/tutoring/apps.py b/tutoring/apps.py deleted file mode 100644 index 7b1885f543e7f66fe6d2be7f788500cfcdbf03ae..0000000000000000000000000000000000000000 --- a/tutoring/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class TutoringConfig(AppConfig): - name = 'tutoring' - verbose_name = 'Tutorat' diff --git a/tutoring/conf.py b/tutoring/conf.py deleted file mode 100644 index 0533d33a8e88a4100b757aff4474a8b0fc197d9d..0000000000000000000000000000000000000000 --- a/tutoring/conf.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Tutoring settings.""" - -from django.conf import settings -from utils import setdefault - - -setdefault(settings, 'DEFAULT_SESSION_START_TIME', (17, 0)) # (h, m) -setdefault(settings, 'DEFAULT_SESSION_END_TIME', (19, 0)) # (h, m) diff --git a/tutoring/factory.py b/tutoring/factory.py deleted file mode 100644 index c00b6897088378d1b237c18389cb91838fd82329..0000000000000000000000000000000000000000 --- a/tutoring/factory.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Tutoring factories.""" - -from datetime import timedelta - -import factory -import factory.django -import pytz -from django.contrib.auth import get_user_model -from django.utils import timezone - -from core.factory import AddressFactory -from tutoring.utils import random_uai_code - -from . import models - -User = get_user_model() -utc = pytz.UTC - - -# Create test objects factories here - -class SchoolFactory(factory.DjangoModelFactory): - """School object factory.""" - - class Meta: # noqa - model = models.School - exclude = ('school_name',) - - uai_code = factory.LazyFunction(random_uai_code) - school_name = factory.Faker('name', locale='fr') - name = factory.LazyAttribute(lambda o: 'Lycée {o.school_name}'.format(o=o)) - address = factory.SubFactory(AddressFactory) - - -class TutoringGroupFactory(factory.DjangoModelFactory): - """TutoringGroup object factory.""" - - class Meta: # noqa - model = models.TutoringGroup - exclude = ('level',) - - level = factory.Iterator(['Seconde', 'Première', 'Terminale']) - name = factory.LazyAttribute( - lambda o: '{o.school} ({o.level})'.format(o=o)) - school = factory.SubFactory(SchoolFactory) - - -class TutorTutoringGroupFactory(factory.DjangoModelFactory): - """Intermediate tutor-tutoring group object factory.""" - - class Meta: # noqa - model = models.TutorTutoringGroup - - # tutor can be passed on creation - tutoring_group = factory.SubFactory(TutoringGroupFactory) - is_leader = False - - -class TutoringSessionFactory(factory.DjangoModelFactory): - """Tutoring session object factory.""" - - class Meta: # noqa - model = models.TutoringSession - - # random date 30 days ahead in time - date = factory.Faker('future_date', end_date='+30d') - start_time = factory.LazyFunction(timezone.now) - end_time = factory.LazyAttribute( - lambda o: o.start_time + timedelta(hours=2)) - tutoring_group = factory.SubFactory(TutoringGroupFactory) diff --git a/tutoring/migrations/0001_initial.py b/tutoring/migrations/0001_initial.py deleted file mode 100644 index d169b2883f77e2d443b9bb632ac058607d72eedf..0000000000000000000000000000000000000000 --- a/tutoring/migrations/0001_initial.py +++ /dev/null @@ -1,65 +0,0 @@ -# Generated by Django 2.0.4 on 2018-04-21 12:25 - -import datetime -import django.core.validators -from django.db import migrations, models -import tutoring.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='School', - fields=[ - ('name', models.CharField(help_text='Nom du lycée', max_length=200, verbose_name='nom')), - ('uai_code', models.CharField(help_text="Code UAI (ex-RNE) de l'établissement qui sert à l'identifier. Celui-ci est composé de 7 chiffres et une lettre. Il est répertorié dans l'annuaire des établissements sur le site du ministère de l'Éducation Nationale.", max_length=8, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(message="Un code UAI doit être composé de 7 chiffres suivis d'une lettre.", regex='^\\d{7}[a-zA-Z]$')], verbose_name='code UAI')), - ], - options={ - 'verbose_name': 'lycée', - 'ordering': ('name',), - }, - ), - migrations.CreateModel( - name='TutoringGroup', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200, verbose_name='nom')), - ], - options={ - 'verbose_name': 'groupe de tutorat', - 'verbose_name_plural': 'groupes de tutorat', - 'ordering': ('name',), - }, - ), - migrations.CreateModel( - name='TutoringSession', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateField(default=datetime.datetime.now)), - ('start_time', models.TimeField(default=tutoring.models.default_start_time, verbose_name='heure de début')), - ('end_time', models.TimeField(default=tutoring.models.default_end_time, verbose_name='heure de fin')), - ], - options={ - 'verbose_name': 'séance de tutorat', - 'verbose_name_plural': 'séances de tutorat', - 'ordering': ('date', 'start_time'), - }, - ), - migrations.CreateModel( - name='TutorTutoringGroup', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_leader', models.BooleanField(default=False, verbose_name='Responsable')), - ], - options={ - 'verbose_name': 'membre du groupe de tutorat', - 'verbose_name_plural': 'membres du groupe de tutorat', - }, - ), - ] diff --git a/tutoring/migrations/0002_auto_20180421_1425.py b/tutoring/migrations/0002_auto_20180421_1425.py deleted file mode 100644 index 98994b0f652b791e656d6b63526c19f9d13e3e89..0000000000000000000000000000000000000000 --- a/tutoring/migrations/0002_auto_20180421_1425.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 2.0.4 on 2018-04-21 12:25 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('tutoring', '0001_initial'), - ('core', '0005_auto_20180408_1525'), - ('users', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='tutortutoringgroup', - name='tutoring_group', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tutoring.TutoringGroup', verbose_name='groupe de tutorat'), - ), - migrations.AddField( - model_name='tutoringsession', - name='tutoring_group', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tutoring_sessions', to='tutoring.TutoringGroup', verbose_name='groupe de tutorat'), - ), - migrations.AddField( - model_name='tutoringgroup', - name='school', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tutoring_groups', to='tutoring.School', verbose_name='lycée'), - ), - migrations.AddField( - model_name='school', - name='address', - field=models.ForeignKey(help_text='Adresse complète du lycée', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.Address', verbose_name='adresse'), - ), - ] diff --git a/tutoring/migrations/0003_auto_20180429_1053.py b/tutoring/migrations/0003_auto_20180429_1053.py deleted file mode 100644 index 3c8ee0634c099682206f19b0452a9277b6e2877b..0000000000000000000000000000000000000000 --- a/tutoring/migrations/0003_auto_20180429_1053.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.0.4 on 2018-04-29 08:53 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('tutoring', '0002_auto_20180421_1425'), - ] - - operations = [ - migrations.AlterField( - model_name='school', - name='address', - field=models.OneToOneField(help_text='Adresse complète du lycée', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.Address', verbose_name='adresse'), - ), - ] diff --git a/tutoring/migrations/0004_auto_20180429_1159.py b/tutoring/migrations/0004_auto_20180429_1159.py deleted file mode 100644 index 3f0dc70f3fb9c164aca8f30f8470111717c04683..0000000000000000000000000000000000000000 --- a/tutoring/migrations/0004_auto_20180429_1159.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.0.4 on 2018-04-29 09:59 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('tutoring', '0003_auto_20180429_1053'), - ] - - operations = [ - migrations.AlterField( - model_name='tutoringsession', - name='tutoring_group', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sessions', to='tutoring.TutoringGroup', verbose_name='groupe de tutorat'), - ), - ] diff --git a/tutoring/migrations/0005_auto_20180512_1325.py b/tutoring/migrations/0005_auto_20180512_1325.py deleted file mode 100644 index aaa0cfd392dc2c500b105b1f09c5d6f21ffc2615..0000000000000000000000000000000000000000 --- a/tutoring/migrations/0005_auto_20180512_1325.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-12 11:25 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('profiles', '0001_initial'), - ('tutoring', '0004_auto_20180429_1159'), - ] - - operations = [ - migrations.AddField( - model_name='tutoringgroup', - name='tutors', - field=models.ManyToManyField(blank=True, null=True, related_name='tutoring_groups', through='tutoring.TutorTutoringGroup', to='profiles.Tutor', verbose_name='tuteurs'), - ), - migrations.AddField( - model_name='tutortutoringgroup', - name='tutor', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='profiles.Tutor', verbose_name='Tuteur'), - ), - ] diff --git a/tutoring/migrations/0006_auto_20180512_1344.py b/tutoring/migrations/0006_auto_20180512_1344.py deleted file mode 100644 index c409bb05e7b3759985206630f178c902070cc84c..0000000000000000000000000000000000000000 --- a/tutoring/migrations/0006_auto_20180512_1344.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-12 11:44 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('tutoring', '0005_auto_20180512_1325'), - ] - - operations = [ - migrations.AlterField( - model_name='tutoringgroup', - name='tutors', - field=models.ManyToManyField(blank=True, related_name='tutoring_groups', through='tutoring.TutorTutoringGroup', to='profiles.Tutor', verbose_name='tuteurs'), - ), - migrations.AlterField( - model_name='tutortutoringgroup', - name='tutor', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='profiles.Tutor', verbose_name='Tuteur'), - ), - ] diff --git a/tutoring/migrations/__init__.py b/tutoring/migrations/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/tutoring/models.py b/tutoring/models.py deleted file mode 100644 index 8fcc10f051e1190344907b6692a61cbec43fe404..0000000000000000000000000000000000000000 --- a/tutoring/models.py +++ /dev/null @@ -1,212 +0,0 @@ -"""Tutoring models.""" - -from datetime import datetime, timedelta - -from django.db import models -from django.shortcuts import reverse -from django.template.defaulttags import date as date_tag -from dry_rest_permissions.generics import (allow_staff_or_superuser, - authenticated_users) - -from .conf import settings -from .validators import uai_code_validator - - -# Create your models here. - - -class TutorTutoringGroup(models.Model): - """Intermediate model for tutoring group and tutors n-n relationship.""" - - tutoring_group = models.ForeignKey( - 'TutoringGroup', on_delete=models.CASCADE, - verbose_name='groupe de tutorat') - tutor = models.ForeignKey( - 'profiles.Tutor', on_delete=models.CASCADE, - verbose_name='Tuteur') - is_leader = models.BooleanField(default=False, verbose_name='Responsable') - - class Meta: # noqa - verbose_name = 'membre du groupe de tutorat' - verbose_name_plural = 'membres du groupe de tutorat' - - -class TutoringGroup(models.Model): - """Represents a tutoring group to which tutors and students participate. - - Fields - ------ - name : char - tutors : n-n with profiles.Tutor - school : 1-n with tutoring.School - Deletion rule: SET_NULL - - Relationships - ------------- - students : n-1 with profiles.Student - """ - - name = models.CharField('nom', max_length=200) - tutors = models.ManyToManyField('profiles.Tutor', - related_name='tutoring_groups', - verbose_name='tuteurs', - blank=True, - through='TutorTutoringGroup') - school = models.ForeignKey('School', on_delete=models.SET_NULL, - null=True, - related_name='tutoring_groups', - verbose_name='lycée') - - class Meta: # noqa - ordering = ('name',) - verbose_name = 'groupe de tutorat' - verbose_name_plural = 'groupes de tutorat' - - def get_absolute_url(self): - return reverse('api:tutoring_group-detail', args=[str(self.id)]) - - @staticmethod - @authenticated_users - def has_read_permission(request): - return True - - @authenticated_users - def has_object_read_permission(self, request): - return True - - @staticmethod - @allow_staff_or_superuser - def has_write_permission(request): - return True - - @allow_staff_or_superuser - def has_object_write_permission(self, request): - """Can only be written by admin, leader tutor.""" - is_leader = (self.tutors - .filter(user_id=request.user.id, - tutortutoringgroup__is_leader=True) - .exists()) - return is_leader - - def __str__(self): - return str(self.name) - - -class School(models.Model): - """Represents a (high) school. - - Fields - ------ - uai_code : char, primary key - UAI code of the school. - name : char - address : char - - Relationships - ------------- - students : n-1 with profiles.Student - - Meta - ---- - ordering : by name - """ - - name = models.CharField('nom', max_length=200, help_text='Nom du lycée') - - # TODO add UAI code validation - uai_code = models.CharField( - 'code UAI', - max_length=8, - primary_key=True, - validators=[uai_code_validator], - help_text=( - "Code UAI (ex-RNE) de l'établissement qui sert à l'identifier. " - "Celui-ci est composé de 7 chiffres et une lettre. " - "Il est répertorié dans " - "l'annuaire des établissements sur le site du " - "ministère de l'Éducation Nationale.")) - - address = models.OneToOneField( - 'core.Address', on_delete=models.SET_NULL, verbose_name='adresse', - null=True, help_text='Adresse complète du lycée') - - class Meta: # noqa - ordering = ('name',) - verbose_name = 'lycée' - - def save(self, *args, **kwargs): - if self.pk is None: - # ensure the letter in UAI code is always uppercase. - # do only at object creation to prevent PK from changing while - # object is alive. - self.uai_code = self.uai_code.upper() - super().save(*args, **kwargs) - - def get_absolute_url(self): - return reverse('api:school-detail', args=[str(self.uai_code)]) - - @staticmethod - def has_read_permission(request): - return True - - @authenticated_users - def has_object_read_permission(self, request): - return True - - @staticmethod - @authenticated_users - @allow_staff_or_superuser - def has_write_permission(request): - return True - - @authenticated_users - @allow_staff_or_superuser - def has_object_write_permission(self, request): - return True - - def __str__(self): - return str(self.name) - - -def default_start_time(): - """Return the default tutoring session start time.""" - h, m = settings.DEFAULT_SESSION_START_TIME - now = datetime.now() - start = now.replace(hour=h, minute=m, second=0, microsecond=0) - return (start if start > now else start + timedelta(days=1)).time() - - -def default_end_time(): - """Return the default tutoring session end time.""" - h, m = settings.DEFAULT_SESSION_END_TIME - now = datetime.now() - end = now.replace(hour=h, minute=m, second=0, microsecond=0) - return (end if end > now else end + timedelta(days=1)).time() - - -class TutoringSession(models.Model): - """Represents a tutoring session event.""" - - date = models.DateField(default=datetime.now) - start_time = models.TimeField('heure de début', - default=default_start_time) - end_time = models.TimeField('heure de fin', - default=default_end_time) - tutoring_group = models.ForeignKey('TutoringGroup', - on_delete=models.CASCADE, - verbose_name='groupe de tutorat', - related_name='sessions') - - class Meta: # noqa - verbose_name = 'séance de tutorat' - verbose_name_plural = 'séances de tutorat' - ordering = ('date', 'start_time',) - - @property - def school(self): - return self.tutoring_group.school - school.fget.short_description = 'lycée' - - def __str__(self): - date = date_tag(self.date, 'SHORT_DATE_FORMAT') - return '{} ({})'.format(self.tutoring_group, date) diff --git a/tutoring/serializers.py b/tutoring/serializers.py deleted file mode 100644 index 9376948b7e7792f8971c489f589b17335e1c21be..0000000000000000000000000000000000000000 --- a/tutoring/serializers.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Tutoring API serializers.""" - -from rest_framework import serializers - -from profiles.models import Student -from core.serializers import AddressSerializer - -from .models import School, TutoringGroup, TutoringSession - - -class SchoolSerializer(serializers.HyperlinkedModelSerializer): - """Serializer for School. - - Suited for: list, retrieve, update, partial_update, delete - """ - - address = AddressSerializer() - students = serializers.PrimaryKeyRelatedField( - many=True, - queryset=Student.objects.all(), - help_text='Lycéens inscrits à ce lycée', - ) - students_count = serializers.IntegerField(source='students.count', - read_only=True) - - class Meta: # noqa - model = School - fields = ('uai_code', 'name', 'address', 'students', - 'students_count', 'url',) - extra_kwargs = { - 'url': {'view_name': 'api:school-detail'}, - 'uai_code': {'read_only': True}, - } - - @staticmethod - def setup_eager_loading(queryset): - """Setup eager loading in advance. - - Prevents the N+1 query problem by pre-fetching the students. - - Source: http://ses4j.github.io/2015/11/23/ - optimizing-slow-django-rest-framework-performance - """ - queryset = queryset.prefetch_related('students') - return queryset - - -class SchoolChoicesSerializer(serializers.ModelSerializer): - """Serializer for available schools.""" - - class Meta: # noqa - model = School - fields = ('uai_code', 'name') - - -class TutoringGroupSerializer(serializers.HyperlinkedModelSerializer): - """Serializer for TutoringGroup.""" - - tutors = serializers.PrimaryKeyRelatedField( - many=True, - read_only=True, - ) - students = serializers.PrimaryKeyRelatedField( - many=True, - read_only=True, - ) - school = serializers.PrimaryKeyRelatedField( - read_only=True, - ) - students_count = serializers.IntegerField(source='students.count', - read_only=True) - tutors_count = serializers.IntegerField(source='tutors.count', - read_only=True) - - class Meta: # noqa - model = TutoringGroup - fields = ('id', 'name', 'tutors', 'students', 'school', - 'students_count', 'tutors_count', 'url',) - extra_kwargs = { - 'url': {'view_name': 'api:tutoring_group-detail'}, - } - - @staticmethod - def setup_eager_loading(queryset): - queryset = queryset.select_related('school') - queryset = queryset.prefetch_related('tutors') - queryset = queryset.prefetch_related('students') - return queryset - - -class TutoringSessionSerializer(serializers.HyperlinkedModelSerializer): - """Serializer for TutoringSession.""" - - tutoring_group = serializers.HyperlinkedRelatedField( - queryset=TutoringGroup.objects.all(), - view_name='api:tutoring_group-detail', - ) - - class Meta: # noqa - model = TutoringSession - fields = ('id', 'date', 'start_time', 'end_time', - 'tutoring_group', 'url',) - extra_kwargs = { - 'url': {'view_name': 'api:tutoring_session-detail'}, - } diff --git a/tutoring/utils.py b/tutoring/utils.py deleted file mode 100644 index 68cd978b02ac2b3a64573eac18b8760d6a28f67c..0000000000000000000000000000000000000000 --- a/tutoring/utils.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Tutoring utilities.""" - -import random -from string import ascii_lowercase, digits - - -def random_uai_code(): - """Return a random UAI code (French school identifier).""" - seven_digits = ''.join(random.choices(digits, k=7)) - one_letter = random.choice(ascii_lowercase) - return seven_digits + one_letter diff --git a/tutoring/validators.py b/tutoring/validators.py deleted file mode 100644 index 6979d73b51508b3ab789945cc3dc0f137a43fbb7..0000000000000000000000000000000000000000 --- a/tutoring/validators.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Tutoring validators.""" - -from django.core.validators import RegexValidator - - -uai_code_validator = RegexValidator( - regex=r'^\d{7}[a-zA-Z]$', - message=("Un code UAI doit être composé de 7 chiffres " - "suivis d'une lettre.") -) diff --git a/tutoring/views.py b/tutoring/views.py deleted file mode 100644 index 95f607d68ff5067a89556bacd7bd689c6db603f0..0000000000000000000000000000000000000000 --- a/tutoring/views.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Tutoring API views.""" - -from rest_framework.response import Response -from rest_framework.viewsets import ReadOnlyModelViewSet -from rest_framework.decorators import action -from dry_rest_permissions.generics import DRYPermissions - -from .models import TutoringGroup, School, TutoringSession -from .serializers import ( - TutoringGroupSerializer, - SchoolSerializer, - SchoolChoicesSerializer, - TutoringSessionSerializer, -) - -# Create your views here. - - -class TutoringGroupViewSet(ReadOnlyModelViewSet): - """API endpoint that allows tutoring groups to be viewed or edited. - - Actions: list, retrieve, create, update, partial_update, destroy - """ - - queryset = TutoringGroup.objects.all() - serializer_class = TutoringGroupSerializer - permission_classes = (DRYPermissions,) - - def get_queryset(self): - queryset = self.queryset - queryset = self.get_serializer_class().setup_eager_loading(queryset) - return queryset - - -class SchoolViewSet(ReadOnlyModelViewSet): - """API endpoint that allows schools to be viewed. - - list: - - List all schools. - - ### Example response - - [ - { - "uai_code": "0930965U", - "url": "http://localhost:8000/api/schools/0930965U/", - "name": "Henri Matisse", - "address": { - "line1": "88 Bis rue Rules Guesde", - "line2": "", - "post_code": "93100", - "city": "Montreuil", - "country": { - "code": "FR", - "name": "France" - } - }, - "students": [ ], - "students_count": 0 - } - ] - - retrieve: - - Retrieve information about a specific school. - - ### Example response - - { - "uai_code": "0930965U", - "url": "http://localhost:8000/api/schools/0930965U/", - "name": "Henri Matisse", - "address": { - "line1": "88 Bis rue Rules Guesde", - "line2": "", - "post_code": "93100", - "city": "Montreuil", - "country": { - "code": "FR", - "name": "France" - } - }, - "students": [ ], - "students_count": 0 - } - """ - - queryset = School.objects.all() - permission_classes = (DRYPermissions,) - - def get_queryset(self): - queryset = self.queryset - queryset = self.get_serializer_class().setup_eager_loading(queryset) - return queryset - - def get_serializer_class(self): - if self.action == 'choices': - return SchoolChoicesSerializer - return SchoolSerializer - - @action(methods=['get'], detail=False) - def choices(self, request): - """Return list of available schools. - - ### Example response - - [ - { - "uai_code": "0930965U", - "name": "Lycée Henri Matisse" - } - ] - """ - serializer = self.get_serializer(School.objects.all(), many=True) - return Response(serializer.data) - - -class TutoringSessionViewSet(ReadOnlyModelViewSet): - """API endpoint that allows tutoring sessions to be viewed or edited. - - Actions: list, retrieve, create, update, partial_update, destroy - """ - - queryset = TutoringSession.objects.all() - serializer_class = TutoringSessionSerializer diff --git a/users/admin.py b/users/admin.py index 1e452282829b122d2f4c56ce184e7a4afa70669c..5131546c64907e3e684fb5d537becff1efd161f9 100644 --- a/users/admin.py +++ b/users/admin.py @@ -40,8 +40,7 @@ class CustomUserAdmin(UserAdmin): fieldsets = ( (None, {'fields': ('email', 'password')}), (_('Personal info'), {'fields': ( - 'first_name', 'last_name', 'date_of_birth', 'gender', - 'phone_number', + 'first_name', 'last_name', 'phone_number', )}), (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}), diff --git a/users/factory.py b/users/factory.py index 3901957bd09c9970116cb598069b19c6114dff0b..b197e55f088bb9b1901794aaad9f16272576836c 100644 --- a/users/factory.py +++ b/users/factory.py @@ -34,11 +34,6 @@ class UserFactory(factory.DjangoModelFactory): # this is a default, override by passing `profile_type='...'` in create() profile_type = None - date_of_birth = factory.Faker('date_this_century', - before_today=True, after_today=False, - locale='fr') - phone_number = factory.Faker('phone_number', locale='fr') - gender = factory.Iterator([User.MALE, User.FEMALE]) @classmethod def _create(cls, model_class, *args, **kwargs): diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index 2e98f8e7380e32f8d0591018f2615149189ed67b..b0daf0c49a47e1fda356d3d402abb275bad6493c 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -1,9 +1,7 @@ -# Generated by Django 2.0.4 on 2018-04-21 12:25 +# Generated by Django 2.0.7 on 2018-09-11 17:38 -from django.conf import settings import django.contrib.auth.validators from django.db import migrations, models -import django.db.models.deletion import django.utils.timezone import users.models @@ -13,8 +11,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('tutoring', '0001_initial'), - ('core', '0005_auto_20180408_1525'), ('auth', '0009_alter_user_last_name_max_length'), ] @@ -49,28 +45,4 @@ class Migration(migrations.Migration): ('objects', users.models.UserManager()), ], ), - migrations.CreateModel( - name='Student', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('address', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.Address', verbose_name='adresse')), - ('school', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='tutoring.School', verbose_name='lycée')), - ('tutoring_group', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='tutoring.TutoringGroup', verbose_name='groupe de tutorat')), - ('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='student', to=settings.AUTH_USER_MODEL, verbose_name='utilisateur')), - ], - options={ - 'verbose_name': 'lycéen', - }, - ), - migrations.CreateModel( - name='Tutor', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('promotion', models.IntegerField(choices=[(2020, '2020'), (2019, '2019'), (2018, '2018'), (2017, '2017'), (2016, '2016')], default=2020)), - ('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tutor', to=settings.AUTH_USER_MODEL, verbose_name='utilisateur')), - ], - options={ - 'verbose_name': 'tuteur', - }, - ), ] diff --git a/users/migrations/0002_auto_20180911_2223.py b/users/migrations/0002_auto_20180911_2223.py new file mode 100644 index 0000000000000000000000000000000000000000..5d9ec40a8222ef98ab5782f8315997d3598f4a4d --- /dev/null +++ b/users/migrations/0002_auto_20180911_2223.py @@ -0,0 +1,25 @@ +# Generated by Django 2.1.1 on 2018-09-11 20:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='date_of_birth', + ), + migrations.RemoveField( + model_name='user', + name='gender', + ), + migrations.RemoveField( + model_name='user', + name='phone_number', + ), + ] diff --git a/users/migrations/0002_student_registration.py b/users/migrations/0002_student_registration.py deleted file mode 100644 index 18dd63068ab0a33ef777fe08c3902309a8d16e1b..0000000000000000000000000000000000000000 --- a/users/migrations/0002_student_registration.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-05 12:14 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('register', '0015_auto_20180505_1403'), - ('users', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='student', - name='registration', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='student', to='register.Registration', verbose_name="dossier d'inscription"), - ), - ] diff --git a/users/migrations/0003_auto_20180505_1734.py b/users/migrations/0003_auto_20180505_1734.py deleted file mode 100644 index c32c2368fe94f6455aa1ea8b6b3c743b418eef1b..0000000000000000000000000000000000000000 --- a/users/migrations/0003_auto_20180505_1734.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-05 15:34 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0002_student_registration'), - ] - - operations = [ - migrations.RemoveField( - model_name='student', - name='address', - ), - migrations.RemoveField( - model_name='student', - name='registration', - ), - migrations.RemoveField( - model_name='student', - name='school', - ), - migrations.RemoveField( - model_name='student', - name='tutoring_group', - ), - migrations.RemoveField( - model_name='student', - name='user', - ), - migrations.RemoveField( - model_name='tutor', - name='user', - ), - migrations.DeleteModel( - name='Student', - ), - migrations.DeleteModel( - name='Tutor', - ), - ] diff --git a/users/migrations/0003_user_phone_number.py b/users/migrations/0003_user_phone_number.py new file mode 100644 index 0000000000000000000000000000000000000000..e21f92f36549728df4075addd578036a9cdf6fb9 --- /dev/null +++ b/users/migrations/0003_user_phone_number.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.1 on 2018-09-20 07:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_auto_20180911_2223'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='phone_number', + field=models.CharField(blank=True, max_length=20, null=True, verbose_name='téléphone'), + ), + ] diff --git a/users/models.py b/users/models.py index 5125af71812b220785bc8312c929f44112734188..264547d35d316da3c3bb6d4c5301f2a23ec5cd61 100644 --- a/users/models.py +++ b/users/models.py @@ -65,22 +65,6 @@ class User(AbstractUser): objects = UserManager() - date_of_birth = models.DateField(blank=True, null=True, - verbose_name='date de naissance') - - MALE = 'M' - FEMALE = 'F' - GENDER_CHOICES = ( - (MALE, 'Homme'), - (FEMALE, 'Femme'), - ) - gender = models.CharField('sexe', blank=True, null=True, - max_length=1, choices=GENDER_CHOICES) - - # TODO add a proper phone number validator - phone_number = models.CharField('téléphone', - max_length=20, null=True, blank=True) - # type of profile of the user PROFILE_STUDENT = 0 PROFILE_TUTOR = 1 @@ -93,6 +77,9 @@ class User(AbstractUser): choices=PROFILE_CHOICES, verbose_name='type de profil') + phone_number = models.CharField('téléphone', + max_length=20, null=True, blank=True) + @property def student(self): return getattr(self, 'student', None) diff --git a/users/serializers.py b/users/serializers.py index 2a294e195df3aadbf5a889a934349dd3f763b99d..fa0d945045b66544a26a25ab4fe243c098ca2425 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -12,9 +12,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: # noqa model = User fields = ('id', 'email', 'profile_type', - 'first_name', 'last_name', - 'gender', - 'phone_number', 'date_of_birth', 'url',) + 'first_name', 'last_name', 'phone_number', 'url',) extra_kwargs = { 'email': {'read_only': True}, 'url': {'view_name': 'api:user-detail'}, diff --git a/visits/migrations/0001_initial.py b/visits/migrations/0001_initial.py index 3435609c2d7510dca67a1f296e20e051dab02c39..590b42b244f9793387eb2828640e3d881ac76070 100644 --- a/visits/migrations/0001_initial.py +++ b/visits/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.2 on 2018-03-01 22:32 +# Generated by Django 2.0.7 on 2018-09-11 17:38 from django.conf import settings from django.db import migrations, models @@ -12,17 +12,32 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('auth', '0009_alter_user_last_name_max_length'), + ('profiles', '0001_initial'), + ('core', '0001_initial'), ] operations = [ + migrations.CreateModel( + name='Participation', + 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 de soumission de la participation', null=True, verbose_name='soumis le')), + ('accepted', models.NullBooleanField(help_text='Cocher pour confirmer au tutoré sa participation à la sortie.', verbose_name='accepté')), + ('present', models.NullBooleanField(help_text='Une fois la sortie passée, indiquer si le lycéen était présent.', verbose_name='présent')), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='participations', to=settings.AUTH_USER_MODEL, verbose_name='utilisateur')), + ], + options={ + 'verbose_name': 'participation', + 'ordering': ('-submitted',), + }, + ), migrations.CreateModel( name='Place', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=200, verbose_name='nom')), - ('address', models.CharField(help_text="L'adresse complète de ce lieu : numéro, rue ou voie, code postal, ville, pays si pertinent.", max_length=200, verbose_name='adresse')), ('description', markdownx.models.MarkdownxField(blank=True, default='', help_text="Une description de ce lieu : de quoi s'agit-il ? Ce champ supporte Markdown.")), + ('address', models.ForeignKey(help_text='Adresse complète de ce lieu', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Address', verbose_name='adresse')), ], options={ 'verbose_name': 'lieu', @@ -37,43 +52,53 @@ class Migration(migrations.Migration): ('title', models.CharField(help_text='Préciser si besoin le type de sortie (exposition, concert…) ', max_length=100, verbose_name='titre')), ('summary', models.CharField(blank=True, default='', help_text='Une ou deux phrases décrivant la sortie de manière attrayante.', max_length=300, verbose_name='résumé')), ('description', markdownx.models.MarkdownxField(blank=True, default='', help_text='Une description plus complète des activités proposées durant la sortie. Ce champ supporte Markdown.')), - ('date', models.DateTimeField(help_text="Heure de début de la sortie. Format de l'heure : hh:mm.")), - ('deadline', models.DateTimeField(help_text="Note : les lycéens ne pourront plus s'inscrire passé cette date. Format de l'heure : hh:mm.", verbose_name="date limite d'inscription")), - ('image', models.ImageField(blank=True, help_text='Une illustration représentative de la sortie. Dimensions : ???x???', null=True, upload_to='', verbose_name='illustration')), - ('fact_sheet', models.FileField(blank=True, help_text='Formats supportés : PDF', null=True, upload_to='', verbose_name='fiche sortie')), - ('organizers_group', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='auth.Group', verbose_name='organisateurs')), + ('date', models.DateField(help_text='Date de la sortie.')), + ('start_time', models.TimeField(help_text='Heure de début de la sortie. Format : hh:mm.', verbose_name='heure de début')), + ('end_time', models.TimeField(help_text='Heure de fin de la sortie. Format : hh:mm.', verbose_name='heure de fin')), + ('meeting', models.CharField(blank=True, default='', help_text='Indiquez aux tutorés où ils devront vous retrouver. Exemple : "devant le musée".', max_length=100, verbose_name='lieu de rendez-vous')), + ('deadline', models.DateTimeField(help_text="Note : les lycéens ne pourront plus s'inscrire passée cette date. Format de l'heure : hh:mm.", verbose_name="date limite d'inscription")), + ('image', models.ImageField(blank=True, help_text='Une illustration représentative de la sortie.', null=True, upload_to='visits/images/', verbose_name='illustration')), + ('fact_sheet', models.FileField(blank=True, help_text='Informe le lycéen de détails sur la sortie. Tous formats supportés, PDF recommandé.', null=True, upload_to='visits/fact_sheets/', verbose_name='fiche sortie')), + ('permission', models.FileField(blank=True, help_text='À mettre à disposition pour que le lycéen la remplisse. Tout format supporté, PDF recommandé.', null=True, upload_to='visits/visit_permissions/', verbose_name='autorisation de sortie')), ], options={ 'verbose_name': 'sortie', 'ordering': ('date',), - 'permissions': (('manage_visit', 'Can manage visit'),), }, ), migrations.CreateModel( - name='VisitParticipant', + name='VisitOrganizer', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('present', models.NullBooleanField(verbose_name='présent')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='utilisateur')), + ('tutor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='profiles.Tutor', verbose_name='tuteur')), ('visit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='visits.Visit', verbose_name='sortie')), ], options={ - 'verbose_name': 'participant à la sortie', - 'verbose_name_plural': 'participants à la sortie', + 'verbose_name': 'organisateur', }, ), + migrations.AddField( + model_name='visit', + name='organizers', + field=models.ManyToManyField(related_name='organized_visits', through='visits.VisitOrganizer', to='profiles.Tutor'), + ), migrations.AddField( model_name='visit', name='participants', - field=models.ManyToManyField(through='visits.VisitParticipant', to=settings.AUTH_USER_MODEL), + field=models.ManyToManyField(through='visits.Participation', to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='visit', name='place', field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='visits.Place', verbose_name='lieu'), ), + migrations.AddField( + model_name='participation', + name='visit', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participations', to='visits.Visit', verbose_name='sortie'), + ), migrations.AlterUniqueTogether( - name='visitparticipant', + name='participation', unique_together={('user', 'visit')}, ), ] diff --git a/visits/migrations/0002_visitparticipant_accepted.py b/visits/migrations/0002_visitparticipant_accepted.py deleted file mode 100644 index 13082c273f34d9625ff04c93f792becb4ef0f0a7..0000000000000000000000000000000000000000 --- a/visits/migrations/0002_visitparticipant_accepted.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.2 on 2018-03-02 09:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='visitparticipant', - name='accepted', - field=models.BooleanField(default=False, verbose_name='accepté'), - ), - ] diff --git a/visits/migrations/0003_auto_20180302_1104.py b/visits/migrations/0003_auto_20180302_1104.py deleted file mode 100644 index cd9ffff5479f837834c0711cd0c5548c3694c3bc..0000000000000000000000000000000000000000 --- a/visits/migrations/0003_auto_20180302_1104.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.0.2 on 2018-03-02 10:04 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0002_visitparticipant_accepted'), - ] - - operations = [ - migrations.AlterField( - model_name='visitparticipant', - name='accepted', - field=models.NullBooleanField(help_text='Cocher si les pièces jointes envoyées sont valides.', verbose_name='dossier validé'), - ), - migrations.AlterField( - model_name='visitparticipant', - name='present', - field=models.NullBooleanField(help_text='Une fois la sortie passée, indiquer si le lycéen était présent.', verbose_name='présent'), - ), - ] diff --git a/visits/migrations/0004_visitattachedfile.py b/visits/migrations/0004_visitattachedfile.py deleted file mode 100644 index 5b487815709cf11c469d184d1baea97bd793c15e..0000000000000000000000000000000000000000 --- a/visits/migrations/0004_visitattachedfile.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 2.0.2 on 2018-03-02 10:19 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0003_auto_20180302_1104'), - ] - - operations = [ - migrations.CreateModel( - name='VisitAttachedFile', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200, verbose_name='nom')), - ('file', models.FileField(upload_to='', verbose_name='fichier')), - ('visit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attached_files', to='visits.Visit')), - ], - options={ - 'verbose_name': 'pièce jointe', - 'verbose_name_plural': 'pièces jointes', - 'ordering': ('visit',), - }, - ), - ] diff --git a/visits/migrations/0005_remove_visitattachedfile_file.py b/visits/migrations/0005_remove_visitattachedfile_file.py deleted file mode 100644 index 3c893f90549b5a22f089be05948f204dfd7f9a22..0000000000000000000000000000000000000000 --- a/visits/migrations/0005_remove_visitattachedfile_file.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.0.2 on 2018-03-02 10:22 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0004_visitattachedfile'), - ] - - operations = [ - migrations.RemoveField( - model_name='visitattachedfile', - name='file', - ), - ] diff --git a/visits/migrations/0006_visitattachedfile_required.py b/visits/migrations/0006_visitattachedfile_required.py deleted file mode 100644 index 00123fde9488339d5c8916d0244eb16522265f06..0000000000000000000000000000000000000000 --- a/visits/migrations/0006_visitattachedfile_required.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.2 on 2018-03-02 10:23 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0005_remove_visitattachedfile_file'), - ] - - operations = [ - migrations.AddField( - model_name='visitattachedfile', - name='required', - field=models.BooleanField(default=True, verbose_name='requis'), - ), - ] diff --git a/visits/migrations/0007_auto_20180304_1413.py b/visits/migrations/0007_auto_20180304_1413.py deleted file mode 100644 index 76a225a85467cab2b772cfa89c5c95e16bcbcc2d..0000000000000000000000000000000000000000 --- a/visits/migrations/0007_auto_20180304_1413.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.0.2 on 2018-03-04 13:13 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0006_visitattachedfile_required'), - ] - - operations = [ - migrations.AlterField( - model_name='visitattachedfile', - name='visit', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attached_files', to='visits.Visit', verbose_name='sortie'), - ), - ] diff --git a/visits/migrations/0008_auto_20180319_2127.py b/visits/migrations/0008_auto_20180319_2127.py deleted file mode 100644 index 22d1f91d2eaceb82d3fec55ca3cea299ef93eecd..0000000000000000000000000000000000000000 --- a/visits/migrations/0008_auto_20180319_2127.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.0.3 on 2018-03-19 20:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0007_auto_20180304_1413'), - ] - - operations = [ - migrations.AlterField( - model_name='visit', - name='fact_sheet', - field=models.FileField(blank=True, help_text='Formats supportés : PDF', null=True, upload_to='visits/fact_sheets/', verbose_name='fiche sortie'), - ), - migrations.AlterField( - model_name='visit', - name='image', - field=models.ImageField(blank=True, help_text='Une illustration représentative de la sortie. Dimensions : ???x???', null=True, upload_to='visits/images/', verbose_name='illustration'), - ), - ] diff --git a/visits/migrations/0009_auto_20180520_1230.py b/visits/migrations/0009_auto_20180520_1230.py deleted file mode 100644 index cf10471aa22729423fe72e5f66bc018d7fbcb002..0000000000000000000000000000000000000000 --- a/visits/migrations/0009_auto_20180520_1230.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-20 10:30 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0008_auto_20180319_2127'), - ] - - operations = [ - migrations.RenameModel( - old_name='VisitAttachedFile', - new_name='AttachedFile', - ), - ] diff --git a/visits/migrations/0010_auto_20180520_1248.py b/visits/migrations/0010_auto_20180520_1248.py deleted file mode 100644 index 75bb2c91e0886477d99e6d30c5d1323fd809e912..0000000000000000000000000000000000000000 --- a/visits/migrations/0010_auto_20180520_1248.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-20 10:48 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('profiles', '0002_auto_20180512_1344'), - ('visits', '0009_auto_20180520_1230'), - ] - - operations = [ - migrations.RemoveField( - model_name='visit', - name='organizers_group', - ), - migrations.AddField( - model_name='visit', - name='organizers', - field=models.ManyToManyField(related_name='organized_visits', to='profiles.Tutor'), - ), - ] diff --git a/visits/migrations/0011_auto_20180520_1253.py b/visits/migrations/0011_auto_20180520_1253.py deleted file mode 100644 index 428e03139ba844969fa561836404b789e036959c..0000000000000000000000000000000000000000 --- a/visits/migrations/0011_auto_20180520_1253.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-20 10:53 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0010_auto_20180520_1248'), - ] - - operations = [ - migrations.AlterField( - model_name='place', - name='address', - field=models.ForeignKey(help_text='Adresse complète de ce lieu', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Address'), - ), - ] diff --git a/visits/migrations/0012_auto_20180520_1305.py b/visits/migrations/0012_auto_20180520_1305.py deleted file mode 100644 index 9a09da5ea898af0828ab05255d816a97b81afaa5..0000000000000000000000000000000000000000 --- a/visits/migrations/0012_auto_20180520_1305.py +++ /dev/null @@ -1,50 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-20 11:05 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('profiles', '0002_auto_20180512_1344'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('visits', '0011_auto_20180520_1253'), - ] - - operations = [ - migrations.CreateModel( - name='VisitOrganizer', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('tutor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='profiles.Tutor', verbose_name='tuteur')), - ], - ), - migrations.RenameModel( - old_name='VisitParticipant', - new_name='Participation', - ), - migrations.AlterModelOptions( - name='participation', - options={'verbose_name': 'participation'}, - ), - migrations.AlterModelOptions( - name='visit', - options={'ordering': ('date',), 'verbose_name': 'sortie'}, - ), - migrations.RemoveField( - model_name='visit', - name='organizers', - ), - migrations.AlterField( - model_name='place', - name='address', - field=models.ForeignKey(help_text='Adresse complète de ce lieu', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Address', verbose_name='adresse'), - ), - migrations.AddField( - model_name='visitorganizer', - name='visit', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='visits.Visit', verbose_name='sortie'), - ), - ] diff --git a/visits/migrations/0013_visit_organizers.py b/visits/migrations/0013_visit_organizers.py deleted file mode 100644 index 785d8b4186b1aee9f06c6e74bd586ee305ddaaa0..0000000000000000000000000000000000000000 --- a/visits/migrations/0013_visit_organizers.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-20 11:05 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('profiles', '0002_auto_20180512_1344'), - ('visits', '0012_auto_20180520_1305'), - ] - - operations = [ - migrations.AddField( - model_name='visit', - name='organizers', - field=models.ManyToManyField(related_name='organized_visits', through='visits.VisitOrganizer', to='profiles.Tutor'), - ), - ] diff --git a/visits/migrations/0014_auto_20180520_1317.py b/visits/migrations/0014_auto_20180520_1317.py deleted file mode 100644 index c73c7301c0d9e3358bc2002150b4c2b14ee12cef..0000000000000000000000000000000000000000 --- a/visits/migrations/0014_auto_20180520_1317.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-20 11:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0013_visit_organizers'), - ] - - operations = [ - migrations.AlterModelOptions( - name='visitorganizer', - options={'verbose_name': 'organisateur'}, - ), - ] diff --git a/visits/migrations/0015_auto_20180520_1403.py b/visits/migrations/0015_auto_20180520_1403.py deleted file mode 100644 index bf7694c0676758a49b2683e24dabed9c1561911c..0000000000000000000000000000000000000000 --- a/visits/migrations/0015_auto_20180520_1403.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-20 12:03 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0014_auto_20180520_1317'), - ] - - operations = [ - migrations.AlterField( - model_name='participation', - name='user', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='participations', to=settings.AUTH_USER_MODEL, verbose_name='utilisateur'), - ), - migrations.AlterField( - model_name='participation', - name='visit', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participations', to='visits.Visit', verbose_name='sortie'), - ), - ] diff --git a/visits/migrations/0016_auto_20180522_2139.py b/visits/migrations/0016_auto_20180522_2139.py deleted file mode 100644 index 642d4b7881ac5919767db35e76d8c31443ffae00..0000000000000000000000000000000000000000 --- a/visits/migrations/0016_auto_20180522_2139.py +++ /dev/null @@ -1,56 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-22 19:39 - -import datetime -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0015_auto_20180520_1403'), - ] - - operations = [ - migrations.AddField( - model_name='visit', - name='end_time', - field=models.TimeField(default=datetime.time(21, 39, 23, 229059), help_text='Heure de fin de la sortie. Format : hh:mm.', verbose_name='heure de fin'), - preserve_default=False, - ), - migrations.AddField( - model_name='visit', - name='meeting', - field=models.CharField(blank=True, default='', help_text='Indiquez aux tutorés où ils devront vous retrouver. Exemple : "devant le musée".', max_length=100, verbose_name='lieu de rendez-vous'), - ), - migrations.AddField( - model_name='visit', - name='permission', - field=models.FileField(blank=True, help_text='À mettre à disposition pour que le lycéen la remplisse. Tout format supporté, PDF recommandé.', null=True, upload_to='visits/visit_permissions/', verbose_name='autorisation de sortie'), - ), - migrations.AddField( - model_name='visit', - name='start_time', - field=models.TimeField(default=datetime.time(21, 39, 32, 604487), help_text='Heure de début de la sortie. Format : hh:mm.', verbose_name='heure de début'), - preserve_default=False, - ), - migrations.AlterField( - model_name='visit', - name='date', - field=models.DateField(help_text='Date de la sortie.'), - ), - migrations.AlterField( - model_name='visit', - name='deadline', - field=models.DateTimeField(help_text="Note : les lycéens ne pourront plus s'inscrire passée cette date. Format de l'heure : hh:mm.", verbose_name="date limite d'inscription"), - ), - migrations.AlterField( - model_name='visit', - name='fact_sheet', - field=models.FileField(blank=True, help_text='Informe le lycéen de détails sur la sortie. Tous formats supportés, PDF recommandé.', null=True, upload_to='visits/fact_sheets/', verbose_name='fiche sortie'), - ), - migrations.AlterField( - model_name='visit', - name='image', - field=models.ImageField(blank=True, help_text='Une illustration représentative de la sortie.', null=True, upload_to='visits/images/', verbose_name='illustration'), - ), - ] diff --git a/visits/migrations/0017_auto_20180522_2233.py b/visits/migrations/0017_auto_20180522_2233.py deleted file mode 100644 index c8e6e052e579b1fd0d4b5892e7e2eeb85e82ca09..0000000000000000000000000000000000000000 --- a/visits/migrations/0017_auto_20180522_2233.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-22 20:33 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0016_auto_20180522_2139'), - ] - - operations = [ - migrations.RemoveField( - model_name='attachedfile', - name='visit', - ), - migrations.DeleteModel( - name='AttachedFile', - ), - ] diff --git a/visits/migrations/0018_auto_20180523_2156.py b/visits/migrations/0018_auto_20180523_2156.py deleted file mode 100644 index 1d41d58ce1609a57d954696a241c1ec80ffdf32a..0000000000000000000000000000000000000000 --- a/visits/migrations/0018_auto_20180523_2156.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-23 19:56 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0017_auto_20180522_2233'), - ] - - operations = [ - migrations.AlterField( - model_name='participation', - name='accepted', - field=models.NullBooleanField(help_text='Cocher pour confirmer au tutoré sa participation à la sortie.', verbose_name='accepté'), - ), - ] diff --git a/visits/migrations/0019_auto_20180526_2034.py b/visits/migrations/0019_auto_20180526_2034.py deleted file mode 100644 index c5a12a0982f35cf87fa4391a66d1f2ba3d9e8e05..0000000000000000000000000000000000000000 --- a/visits/migrations/0019_auto_20180526_2034.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.0.4 on 2018-05-26 18:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('visits', '0018_auto_20180523_2156'), - ] - - operations = [ - migrations.AlterModelOptions( - name='participation', - options={'ordering': ('-submitted',), 'verbose_name': 'participation'}, - ), - migrations.AddField( - model_name='participation', - name='submitted', - field=models.DateTimeField(auto_now_add=True, help_text='Date de soumission de la participation', null=True, verbose_name='soumis le'), - ), - ] diff --git a/visits/models.py b/visits/models.py index 62aef12da83f59786fb49b24ebd251169244e56b..63fadd8684e1024d63dc718407bcc9f1d2df22e1 100644 --- a/visits/models.py +++ b/visits/models.py @@ -1,5 +1,6 @@ """Visits models.""" from django.db import models +from django.contrib.sites.models import Site from django.shortcuts import reverse from django.utils.timezone import now from dry_rest_permissions.generics import authenticated_users @@ -214,7 +215,8 @@ class Visit(models.Model): return reverse('api:visit-detail', args=[str(self.pk)]) def get_site_url(self): - return f'http://oser-cs.fr/visits/{self.pk}' + site = Site.objects.get_current() + return f'https://{site.domain}/visits/{self.pk}' def __str__(self): return str(self.title)