diff --git a/README.md b/README.md index f5660a894ffe13924442c6e42d60335850084327..1b64c6d4b88fbcfc7c93584b5475ebaf5b2aa118 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,17 @@ 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. +- Créez une nouvelle base de données : + +Dans le menu de gauche, dépliez `Servers (1)`, puis votre serveur (nommé `PostgreSQL XX` par défaut), puis faites un clic droit sur l'onglet `Databases` et créez une base de données nommée `oser_backend_db`. + ### Installation du projet -- (Recommandé) Créez un environnement virtuel (ici appelé `env`) puis activez-le : +- (Recommandé) Créez un environnement virtuel (appelé ici oser-back) à l'aide de conda et activez-le : ```bash -$ python -m venv env -$ source env/bin/activate +$ conda create -n oser-back +$ conda activate oser-back ``` - Installez les dépendances : @@ -70,6 +74,7 @@ $ pip install -r requirements.txt ```bash $ python manage.py migrate ``` +(En cas d'erreur, les logs du serveur PostgreSQL sont disponibles dans : %PROGRAMFILES%\PostgreSQL\POSTGRESQL_VERSION_NUM\data\log) Il ne vous reste plus qu'à lancer le serveur de développement : diff --git a/api/urls.py b/api/urls.py index dbd3f62fe5ce8ba2ed67d64dca500e2b15060aee..46497be11fa1b6e898d2887dd91eaf1fe3c6933d 100644 --- a/api/urls.py +++ b/api/urls.py @@ -26,7 +26,7 @@ router.register('users', users_views.UserViewSet) # Profiles views router.register('tutors', profiles_views.TutorViewSet) -router.register('students', profiles_views.StudentViewSet) +router.register('students', profiles_views.StudentViewSet, base_name='student') # Register views router.register('registrations', register_views.RegistrationViewSet) diff --git a/core/factory.py b/core/factory.py index a812eedf66da680b44e704315bd4a2fc0f7810f6..7167327d48510efeb20e2ae1e70c981bee7fb623 100644 --- a/core/factory.py +++ b/core/factory.py @@ -5,7 +5,7 @@ import factory.django from . import models -class DocumentFactory(factory.DjangoModelFactory): +class DocumentFactory(factory.django.DjangoModelFactory): """Document object factory.""" class Meta: # noqa @@ -15,7 +15,7 @@ class DocumentFactory(factory.DjangoModelFactory): content = factory.Faker('text', max_nb_chars=1000, locale='fr') -class AddressFactory(factory.DjangoModelFactory): +class AddressFactory(factory.django.DjangoModelFactory): """Address object factory.""" class Meta: # noqa diff --git a/oser_backend/serializers.py b/oser_backend/serializers.py index 576a486df57cd73a4b7479200552c27528ddfb8a..d1535112a9014e66d3ef565658b270c84ff9fe32 100644 --- a/oser_backend/serializers.py +++ b/oser_backend/serializers.py @@ -28,7 +28,7 @@ class PasswordResetSerializer(serializers.Serializer): ###### USE YOUR TEXT FILE ###### 'email_template_name': 'email-reset-template.txt', - + 'subject_template_name': 'subject-reset-template.txt', 'request': request, } self.reset_form.save(**opts) \ No newline at end of file diff --git a/oser_backend/settings/common.py b/oser_backend/settings/common.py index ca0a6074c8124e6ee6b3a9c04c8e964b97c107e5..9579af4b53b12213685b37613ac01aa7bdc42a30 100644 --- a/oser_backend/settings/common.py +++ b/oser_backend/settings/common.py @@ -5,10 +5,13 @@ Common settings suitable for all environmebts. """ import os +from dotenv import load_dotenv import dj_database_url import pymdownx.emoji +load_dotenv() + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) dn = os.path.dirname BASE_DIR = dn(dn(dn(os.path.abspath(__file__)))) @@ -35,7 +38,7 @@ DJANGO_APPS = [ 'whitenoise.runserver_nostatic', 'django.contrib.staticfiles', 'django.forms', - 'django.contrib.sites', + 'django.contrib.sites' ] THIRD_PARTY_APPS = [ @@ -233,7 +236,7 @@ LOGGING = { # Connect custom PasswordResetSerializer to override default REST_AUTH_SERIALIZERS = { - 'PASSWORD_RESET_SERIALIZER': + 'PASSWORD_RESET_SERIALIZER': 'oser_backend.serializers.PasswordResetSerializer', } @@ -243,7 +246,7 @@ DEFAULT_FROM_EMAIL = "admin@oser-cs.fr" EMAIL_BACKEND = 'sendgrid_backend.SendgridBackend' SENDGRID_API_KEY = os.getenv('SENDGRID_API_KEY') -# Sendgrid configuration +# Sendgrid configuration EMAIL_HOST = 'smtp.sendgrid.net' EMAIL_HOST_USER = 'apikey' diff --git a/oser_backend/settings/dev.py b/oser_backend/settings/dev.py index 18aab0f6f479d72f69479446573d9a10e84a5cdd..524f70a2c3a53b2060e43e14e8703097b55df5fd 100644 --- a/oser_backend/settings/dev.py +++ b/oser_backend/settings/dev.py @@ -5,7 +5,7 @@ from .common import * from .common import BASE_DIR DEBUG = True -ALLOWED_HOSTS = ['localhost'] +ALLOWED_HOSTS = ['localhost','127.0.0.1'] # Static files (CSS, JavaScript, Images) and media files (user-uploaded) @@ -25,4 +25,4 @@ DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' # Mails config # However emails won't be delivered by SendGrid (use dev_sendgrid settings) -MAILS_ENABLED = True +MAILS_ENABLED = False diff --git a/oser_backend/settings/dev_sendgrid.py b/oser_backend/settings/dev_sendgrid.py index 868345b995a13c8bd6d99d56dc48cee8a9ea87b9..468b0aa3277bd7fb2f300f556e6781920fb0f145 100644 --- a/oser_backend/settings/dev_sendgrid.py +++ b/oser_backend/settings/dev_sendgrid.py @@ -5,3 +5,4 @@ from .dev import * # Allow to send emails with SendGrid while in DEBUG mode. # See: https://github.com/sklarsa/django-sendgrid-v5#other-settings SENDGRID_SANDBOX_MODE_IN_DEBUG = False + diff --git a/profiles/MultiSelectFieldListFilter.py b/profiles/MultiSelectFieldListFilter.py new file mode 100644 index 0000000000000000000000000000000000000000..53f2eefdb0d64f73e21483b9e3fe02175e2267d9 --- /dev/null +++ b/profiles/MultiSelectFieldListFilter.py @@ -0,0 +1,65 @@ +from django.contrib import admin +from django.contrib.admin.utils import reverse_field_path +from django.utils.translation import gettext_lazy as _ + +class MultiSelectFieldListFilter(admin.FieldListFilter): + def __init__(self, field, request, params, model, model_admin, field_path): + self.lookup_kwarg = field_path + '__in' + self.lookup_kwarg_isnull = field_path + '__isnull' + + super().__init__(field, request, params, model, model_admin, field_path) + + self.lookup_val = self.used_parameters.get(self.lookup_kwarg, []) + if len(self.lookup_val) == 1 and self.lookup_val[0] == '': + self.lookup_val = [] + self.lookup_val_isnull = self.used_parameters.get(self.lookup_kwarg_isnull) + + self.empty_value_display = model_admin.get_empty_value_display() + parent_model, reverse_path = reverse_field_path(model, field_path) + # Obey parent ModelAdmin queryset when deciding which options to show + if model == parent_model: + queryset = model_admin.get_queryset(request) + else: + queryset = parent_model._default_manager.all() + self.lookup_choices = queryset.distinct().order_by(field.name).values_list(field.name, flat=True) + + def expected_parameters(self): + return [self.lookup_kwarg, self.lookup_kwarg_isnull] + + def choices(self, changelist): + yield { + 'selected': not self.lookup_val and self.lookup_val_isnull is None, + 'query_string': changelist.get_query_string(remove=[self.lookup_kwarg, self.lookup_kwarg_isnull]), + 'display': _('All'), + } + include_none = False + for val in self.lookup_choices: + if val is None: + include_none = True + continue + val = str(val) + + if val in self.lookup_val: + values = [v for v in self.lookup_val if v != val] + else: + values = self.lookup_val + [ val ] + + if values: + yield { + 'selected': val in self.lookup_val, + 'query_string': changelist.get_query_string({self.lookup_kwarg: ','.join(values)}, [self.lookup_kwarg_isnull]), + 'display': val, + } + else: + yield { + 'selected': val in self.lookup_val, + 'query_string': changelist.get_query_string(remove=[self.lookup_kwarg]), + 'display': val, + } + + if include_none: + yield { + 'selected': bool(self.lookup_val_isnull), + 'query_string': changelist.get_query_string({self.lookup_kwarg_isnull: 'True'}, [self.lookup_kwarg]), + 'display': self.empty_value_display, + } \ No newline at end of file diff --git a/profiles/admin.py b/profiles/admin.py index 4f00aa12ef96c41e9b9b99522ef931d97c651dec..9109a6fd5889f333c659c9a89008376f3cf8f758 100644 --- a/profiles/admin.py +++ b/profiles/admin.py @@ -2,6 +2,8 @@ from django.contrib import admin from .models import Student, Tutor +from .MultiSelectFieldListFilter import MultiSelectFieldListFilter +import codecs import csv from django.http import HttpResponse @@ -13,7 +15,8 @@ class ExportCsvMixin: response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta) - writer = csv.writer(response) + response.write(codecs.BOM_UTF8) #force response to be UTF-8 + writer = csv.writer(response, delimiter=';') writer.writerow(field_names) for obj in queryset: @@ -21,7 +24,7 @@ class ExportCsvMixin: return response - export_as_csv.short_description = "Export Selected" + export_as_csv.short_description = "Exporter sélection (en .csv)" class ProfileAdminMixin: @@ -40,10 +43,11 @@ class TutorAdmin(ProfileAdminMixin, admin.ModelAdmin,ExportCsvMixin): model = Tutor actions = ["export_as_csv"] + @admin.register(Student) class StudentAdmin(ProfileAdminMixin, admin.ModelAdmin,ExportCsvMixin): """Student admin panel.""" - + list_filter = (('school',MultiSelectFieldListFilter), 'year') class Meta: # noqa model = Student actions = ["export_as_csv"] \ No newline at end of file diff --git a/profiles/factory.py b/profiles/factory.py index 886b5dde680d2945e4fbc1b30455990a390fa491..6b8b4ff6b212ccb5c6c58d0c47b62cf855a09b0c 100644 --- a/profiles/factory.py +++ b/profiles/factory.py @@ -12,7 +12,7 @@ from users.factory import UserFactory from . import models -class StudentFactory(factory.DjangoModelFactory): +class StudentFactory(factory.django.DjangoModelFactory): """Student object factory.""" class Meta: # noqa @@ -24,7 +24,7 @@ class StudentFactory(factory.DjangoModelFactory): _this_year = datetime.today().year -class TutorFactory(factory.DjangoModelFactory): +class TutorFactory(factory.django.DjangoModelFactory): """Tutor object factory.""" class Meta: # noqa diff --git a/profiles/migrations/0003_auto_20200914_0057.py b/profiles/migrations/0003_auto_20200914_0057.py new file mode 100644 index 0000000000000000000000000000000000000000..ebe33bdb9876e88f5967032f96c1bf3adc9f4bbb --- /dev/null +++ b/profiles/migrations/0003_auto_20200914_0057.py @@ -0,0 +1,88 @@ +# Generated by Django 2.2 on 2020-09-13 22:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0002_auto_20180911_1938'), + ] + + operations = [ + migrations.AddField( + model_name='student', + name='addressNumber', + field=models.IntegerField(blank=True, null=True, verbose_name='numéro de rue'), + ), + migrations.AddField( + model_name='student', + name='city', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='nom de ville'), + ), + migrations.AddField( + model_name='student', + name='dependantsNumber', + field=models.CharField(blank=True, max_length=12, null=True, verbose_name="numero d'urgence"), + ), + migrations.AddField( + model_name='student', + name='fatherActivity', + field=models.CharField(blank=True, max_length=70, null=True, verbose_name='metier du pere'), + ), + migrations.AddField( + model_name='student', + name='gender', + field=models.CharField(blank=True, max_length=20, null=True, verbose_name='genre'), + ), + migrations.AddField( + model_name='student', + name='grade', + field=models.CharField(blank=True, max_length=20, null=True, verbose_name='nom du niveau de la classe'), + ), + migrations.AddField( + model_name='student', + name='motherActivity', + field=models.CharField(blank=True, max_length=70, null=True, verbose_name='metier de la mere'), + ), + migrations.AddField( + model_name='student', + name='parentsEmail', + field=models.EmailField(blank=True, max_length=20, null=True, verbose_name='adresse mail parentale'), + ), + migrations.AddField( + model_name='student', + name='parentsPhone', + field=models.CharField(blank=True, max_length=12, null=True, verbose_name='numéro de téléphone parental'), + ), + migrations.AddField( + model_name='student', + name='parentsStatus', + field=models.CharField(blank=True, max_length=30, null=True, verbose_name='statut des parents'), + ), + migrations.AddField( + model_name='student', + name='personnalPhone', + field=models.CharField(blank=True, max_length=12, null=True, verbose_name='numéro de téléphone personnel'), + ), + migrations.AddField( + model_name='student', + name='scholarship', + field=models.BooleanField(blank=True, null=True, verbose_name='boursier'), + ), + migrations.AddField( + model_name='student', + name='school', + field=models.CharField(blank=True, max_length=70, null=True, verbose_name="nom de l'ecole"), + ), + migrations.AddField( + model_name='student', + name='street', + field=models.CharField(blank=True, max_length=70, null=True, verbose_name='nom de rue'), + ), + migrations.AlterField( + model_name='tutor', + name='promotion', + field=models.IntegerField(choices=[(2023, '2023'), (2022, '2022'), (2021, '2021'), (2020, '2020'), (2019, '2019')], default=2023), + ), + ] diff --git a/profiles/migrations/0004_auto_20200915_1827.py b/profiles/migrations/0004_auto_20200915_1827.py new file mode 100644 index 0000000000000000000000000000000000000000..d94b486a824cbe4d55b98228c8065bb28520bf80 --- /dev/null +++ b/profiles/migrations/0004_auto_20200915_1827.py @@ -0,0 +1,43 @@ +# Generated by Django 2.2 on 2020-09-15 16:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0003_auto_20200914_0057'), + ] + + operations = [ + migrations.AlterField( + model_name='student', + name='dependantsNumber', + field=models.CharField(blank=True, max_length=12, null=True, verbose_name="numéro d'urgence"), + ), + migrations.AlterField( + model_name='student', + name='fatherActivity', + field=models.CharField(blank=True, max_length=70, null=True, verbose_name='métier du père'), + ), + migrations.AlterField( + model_name='student', + name='grade', + field=models.CharField(blank=True, max_length=20, null=True, verbose_name='niveau de la classe'), + ), + migrations.AlterField( + model_name='student', + name='motherActivity', + field=models.CharField(blank=True, max_length=70, null=True, verbose_name='métier de la mère'), + ), + migrations.AlterField( + model_name='student', + name='scholarship', + field=models.NullBooleanField(verbose_name='boursier'), + ), + migrations.AlterField( + model_name='student', + name='school', + field=models.CharField(blank=True, max_length=70, null=True, verbose_name="nom de l'école"), + ), + ] diff --git a/profiles/migrations/0004_auto_20200918_1320.py b/profiles/migrations/0004_auto_20200918_1320.py new file mode 100644 index 0000000000000000000000000000000000000000..0b1e26b5ff27618b90b05d48a5b4959b5c196d05 --- /dev/null +++ b/profiles/migrations/0004_auto_20200918_1320.py @@ -0,0 +1,43 @@ +# Generated by Django 2.2 on 2020-09-18 11:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0003_auto_20200914_0057'), + ] + + operations = [ + migrations.AlterField( + model_name='student', + name='dependantsNumber', + field=models.CharField(blank=True, max_length=12, null=True, verbose_name="numéro d'urgence"), + ), + migrations.AlterField( + model_name='student', + name='fatherActivity', + field=models.CharField(blank=True, max_length=70, null=True, verbose_name='métier du père'), + ), + migrations.AlterField( + model_name='student', + name='grade', + field=models.CharField(blank=True, max_length=20, null=True, verbose_name='niveau de la classe'), + ), + migrations.AlterField( + model_name='student', + name='motherActivity', + field=models.CharField(blank=True, max_length=70, null=True, verbose_name='métier de la mère'), + ), + migrations.AlterField( + model_name='student', + name='scholarship', + field=models.NullBooleanField(verbose_name='boursier'), + ), + migrations.AlterField( + model_name='student', + name='school', + field=models.CharField(blank=True, max_length=70, null=True, verbose_name="nom de l'école"), + ), + ] diff --git a/profiles/migrations/0005_auto_20200918_1805.py b/profiles/migrations/0005_auto_20200918_1805.py new file mode 100644 index 0000000000000000000000000000000000000000..7e5756b35f30031a1461c9e722ade5bd3c0d56b9 --- /dev/null +++ b/profiles/migrations/0005_auto_20200918_1805.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2020-09-18 16:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0004_auto_20200918_1320'), + ] + + operations = [ + migrations.AlterField( + model_name='student', + name='scholarship', + field=models.CharField(blank=True, max_length=10, null=True, verbose_name='boursier'), + ), + ] diff --git a/profiles/migrations/0006_auto_20200918_1818.py b/profiles/migrations/0006_auto_20200918_1818.py new file mode 100644 index 0000000000000000000000000000000000000000..a1d92c84cb5637c04014142e8f0eb6a0ce7047ee --- /dev/null +++ b/profiles/migrations/0006_auto_20200918_1818.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2 on 2020-09-18 16:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0005_auto_20200918_1805'), + ] + + operations = [ + migrations.AddField( + model_name='student', + name='nationality', + field=models.CharField(blank=True, max_length=30, null=True, verbose_name='nationalité'), + ), + migrations.AddField( + model_name='student', + name='specialTeaching', + field=models.CharField(blank=True, max_length=30, null=True, verbose_name='enseignement de spécialité'), + ), + migrations.AddField( + model_name='student', + name='zipCode', + field=models.CharField(blank=True, max_length=10, null=True, verbose_name='code postal'), + ), + ] diff --git a/profiles/migrations/0007_merge_20200918_1824.py b/profiles/migrations/0007_merge_20200918_1824.py new file mode 100644 index 0000000000000000000000000000000000000000..ff45a3495fcaffea7b785342967fdf43431f60e6 --- /dev/null +++ b/profiles/migrations/0007_merge_20200918_1824.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2 on 2020-09-18 16:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0006_auto_20200918_1818'), + ('profiles', '0004_auto_20200915_1827'), + ] + + operations = [ + ] diff --git a/profiles/migrations/0008_auto_20200918_1913.py b/profiles/migrations/0008_auto_20200918_1913.py new file mode 100644 index 0000000000000000000000000000000000000000..cdf1810b92ba16c81f69a0c1d03524b70671e8a3 --- /dev/null +++ b/profiles/migrations/0008_auto_20200918_1913.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2020-09-18 17:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0007_merge_20200918_1824'), + ] + + operations = [ + migrations.AlterField( + model_name='student', + name='scholarship', + field=models.CharField(blank=True, max_length=10, null=True, verbose_name='boursier'), + ), + ] diff --git a/profiles/migrations/0009_auto_20200923_1329.py b/profiles/migrations/0009_auto_20200923_1329.py new file mode 100644 index 0000000000000000000000000000000000000000..5dc6c13dc8941b3a97754791132d8f584da47eb8 --- /dev/null +++ b/profiles/migrations/0009_auto_20200923_1329.py @@ -0,0 +1,43 @@ +# Generated by Django 2.2 on 2020-09-23 11:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0008_auto_20200918_1913'), + ] + + operations = [ + migrations.AlterField( + model_name='student', + name='dependantsNumber', + field=models.IntegerField(blank=True, null=True, verbose_name='nombre de personnes à charge'), + ), + migrations.AlterField( + model_name='student', + name='nationality', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='nationalité'), + ), + migrations.AlterField( + model_name='student', + name='parentsEmail', + field=models.EmailField(blank=True, max_length=70, null=True, verbose_name='adresse mail parentale'), + ), + migrations.AlterField( + model_name='student', + name='parentsStatus', + field=models.CharField(blank=True, max_length=70, null=True, verbose_name='statut des parents'), + ), + migrations.AlterField( + model_name='student', + name='scholarship', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='boursier'), + ), + migrations.AlterField( + model_name='student', + name='specialTeaching', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='enseignement de spécialité'), + ), + ] diff --git a/profiles/migrations/0010_auto_20200923_1444.py b/profiles/migrations/0010_auto_20200923_1444.py new file mode 100644 index 0000000000000000000000000000000000000000..fd5da54118b9f0ff8ba3e85af509d8cf7ac87a14 --- /dev/null +++ b/profiles/migrations/0010_auto_20200923_1444.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2020-09-23 12:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0009_auto_20200923_1329'), + ] + + operations = [ + migrations.RenameField( + model_name='student', + old_name='personnalPhone', + new_name='personalPhone', + ), + ] diff --git a/profiles/migrations/0011_student_classtype.py b/profiles/migrations/0011_student_classtype.py new file mode 100644 index 0000000000000000000000000000000000000000..04821189e139d63ba4fcb74c6986fb7b0e67b3af --- /dev/null +++ b/profiles/migrations/0011_student_classtype.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2020-09-24 16:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0010_auto_20200923_1444'), + ] + + operations = [ + migrations.AddField( + model_name='student', + name='classType', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='général/techno'), + ), + ] diff --git a/profiles/migrations/0012_student_year.py b/profiles/migrations/0012_student_year.py new file mode 100644 index 0000000000000000000000000000000000000000..359b6cbd51b920471e2203e942a3805120f98cc2 --- /dev/null +++ b/profiles/migrations/0012_student_year.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2020-09-25 12:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0011_student_classtype'), + ] + + operations = [ + migrations.AddField( + model_name='student', + name='year', + field=models.CharField(blank=True, max_length=10, null=True, verbose_name='année'), + ), + ] diff --git a/profiles/models.py b/profiles/models.py index 28061fa97817f7fd8dd4c0a58ad90d3e8da141bd..6f9dd0dafe33d456124e641e49b78175b2b085ed 100644 --- a/profiles/models.py +++ b/profiles/models.py @@ -4,6 +4,8 @@ from django.db import models from django.shortcuts import reverse from dry_rest_permissions.generics import authenticated_users from .utils import get_promotion_range +from datetime import datetime +from .notifications import SendDocs class ProfileMixin: @@ -52,6 +54,147 @@ class Student(ProfileMixin, models.Model): related_name='student', ) + classType = models.CharField(max_length=50, + null=True, + blank=True, + verbose_name="général/techno", + ) + + nationality = models.CharField(max_length=50, + null=True, + blank=True, + verbose_name="nationalité", + ) + + specialTeaching = models.CharField(max_length=50, + null=True, + blank=True, + verbose_name="enseignement de spécialité", + ) + + zipCode = models.CharField(max_length=10, + null=True, + blank=True, + verbose_name="code postal", + ) + + + gender = models.CharField(max_length=20, + null=True, + blank=True, + verbose_name="genre", + ) + + addressNumber = models.IntegerField( + null=True, + blank=True, + verbose_name="numéro de rue" + ) + + street = models.CharField(max_length=70, + null=True, + blank=True, + verbose_name="nom de rue" + ) + + city = models.CharField(max_length=50, + null=True, + blank=True, + verbose_name="nom de ville" + ) + + personalPhone = models.CharField(max_length=12, + null=True, + blank=True, + verbose_name="numéro de téléphone personnel" + ) + + parentsPhone = models.CharField(max_length=12, + null=True, + blank=True, + verbose_name="numéro de téléphone parental" + ) + + parentsEmail = models.EmailField(max_length=70, + null=True, + blank=True, + verbose_name="adresse mail parentale" + ) + + + school = models.CharField(max_length=70, + null=True, + blank=True, + verbose_name="établissement" + ) + + + grade = models.CharField(max_length=20, + null=True, + blank=True, + verbose_name="niveau de la classe" + ) + + + scholarship = models.CharField(max_length=50, + null=True, + blank=True, + verbose_name="boursier" + ) + + + fatherActivity = models.CharField(max_length=70, + null=True, + blank=True, + verbose_name="métier du père" + ) + + + motherActivity = models.CharField(max_length=70, + null=True, + blank=True, + verbose_name="métier de la mère" + ) + + + parentsStatus = models.CharField(max_length=70, + null=True, + blank=True, + verbose_name="statut des parents" + ) + + + dependantsNumber = models.IntegerField( + null=True, + blank=True, + verbose_name="nombre de personnes à charge" + ) + + year = models.CharField(max_length=10, + null=True, + blank=True, + verbose_name="année" + ) + + @staticmethod + def has_write_permission(request): + return True + + def has_object_write_permission(self, request): + return request.user == self.user + + def save(self, *args, **kwargs): + """Updates the year field based on the last modified date""" + date_now = datetime.now() + if date_now.month>=9: + self.year = f"{date_now.year}/{date_now.year+1}" + else: + self.year = f"{date_now.year-1}/{date_now.year}" + + SendDocs(user=self.user).send() # send email with link to registration docs + + return super(Student,self).save(*args, **kwargs) + class Meta: # noqa verbose_name = 'lycéen' diff --git a/profiles/notifications.py b/profiles/notifications.py new file mode 100644 index 0000000000000000000000000000000000000000..bdd010bc663b378239b51d14bb18ddcb951685dd --- /dev/null +++ b/profiles/notifications.py @@ -0,0 +1,11 @@ +from mails import Notification + +class SendDocs(Notification): + """Sends a link to the google docs containing the registration documents""" + subject = "Dossier d'inscription OSER" + + template_name = "profiles/registration_docs.md" + args = ('user',) + def get_recipients(self): + return [self.user.email] + diff --git a/profiles/serializers.py b/profiles/serializers.py index 31b20310d293647aacde450dca91509f8af66088..778dc21ff9563ca1a6e9b848915ae409146112ab 100644 --- a/profiles/serializers.py +++ b/profiles/serializers.py @@ -31,11 +31,11 @@ class StudentSerializer(serializers.HyperlinkedModelSerializer): read_only=True) registration = StudentRegistrationSerializer() + class Meta: # noqa model = Student fields = ( - 'user_id', 'user', 'registration', 'visits', 'url', - ) + 'user_id', 'user', 'url', 'registration', 'visits', 'gender', 'addressNumber', 'street', 'city', 'personalPhone', 'parentsPhone', 'parentsEmail', 'school', 'grade', 'scholarship', 'fatherActivity', 'motherActivity', 'parentsStatus', 'dependantsNumber', 'specialTeaching', 'nationality', 'zipCode', 'classType') extra_kwargs = { 'url': {'view_name': 'api:student-detail'}, } diff --git a/profiles/templates/profiles/registration_docs.md b/profiles/templates/profiles/registration_docs.md new file mode 100644 index 0000000000000000000000000000000000000000..bf90ad938b2f09564b9ca8ac771eeab1d01bd095 --- /dev/null +++ b/profiles/templates/profiles/registration_docs.md @@ -0,0 +1,18 @@ +{% extends 'mails/notification.md' %} + +{% block greeting %} +Bonjour, +{% endblock %} + +{% block body %} +Si tu reçois ce mail c’est que tu as fait un compte sur le site d’OSER et que tu as correctement rempli tes données personnelles. Félicitations ! ✅ + +Avant de te laisser tranquille et que tu aies accès aux inscriptions pour les sorties et les projets, il te reste une dernière tâche : remplir le dossier d’inscription. Tu peux le trouver à ce lien : [https://drive.google.com/drive/folders/1TSGPRP2dAu07nkucpioWYGToccSU2vLD?usp=sharing](https://drive.google.com/drive/folders/1TSGPRP2dAu07nkucpioWYGToccSU2vLD?usp=sharing) . +Tu peux télécharger le document et l’imprimer. Si tu n’as pas d’imprimante tu peux demander un dossier papier à un tuteur de ta séance. Il faut rendre ce dossier sous format papier rempli aux tuteurs de ta séance. Dès que l’on recevra ton dossier, on validera ton compte ! +{% endblock %} + +{% block signature %} +On espère passer une super année avec toi, + +L’équipe OSER +{% endblock %} \ No newline at end of file diff --git a/profiles/views.py b/profiles/views.py index 0c157ef80761cfde6b830d6820eae66d48ab365b..b81ec2cdb13554e7f45e37f7b4736f1a9a0c5d85 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -5,6 +5,7 @@ from dry_rest_permissions.generics import DRYPermissions from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response +from django_filters.rest_framework import DjangoFilterBackend from visits.serializers import VisitSerializer @@ -23,40 +24,21 @@ class TutorViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = (DRYPermissions,) -class StudentViewSet(viewsets.ReadOnlyModelViewSet): - """API endpoint that allows students to be viewed. +class StudentViewSet(viewsets.ModelViewSet): + """API endpoint that allows students to be viewed, and profiles to be updated.""" - list: + def get_serializer(self, *args, **kwargs): + kwargs['partial'] = True + return super(StudentViewSet, self).get_serializer(*args, **kwargs) - ### Example response + def get_queryset(self): + user = self.request.user + if user.is_staff: + return Student.objects.all() + else: + return Student.objects.filter(user_id = user.id) - List of results from `retrieve` (see the example response for `retrieve`). - retrieve: - - ### Example response - - { - "user_id": 4, - "user": { - "id": 4, - "email": "charles.dumont@example.net", - "profile_type": null, - "first_name": "", - "last_name": "", - "url": "http://localhost:8000/api/users/4/" - }, - "registration": { - "id": 3, - "submitted": "2018-05-05T14:15:10.998206+02:00", - "validated": false - }, - "visits": [], - "url": "http://localhost:8000/api/students/2/" - } - """ - - queryset = Student.objects.all() serializer_class = StudentSerializer permission_classes = (DRYPermissions,) diff --git a/projects/factory.py b/projects/factory.py index f826ab8d36261eb8a99acd51cdcfb2a725f1fcfb..8a539812d5abff29ff8657c0a36a9292e4789920 100644 --- a/projects/factory.py +++ b/projects/factory.py @@ -7,7 +7,7 @@ from users.factory import UserFactory from .models import Project, Edition, Participation, EditionForm -class ProjectFactory(factory.DjangoModelFactory): +class ProjectFactory(factory.django.DjangoModelFactory): """Project object factory.""" class Meta: # noqa @@ -20,7 +20,7 @@ class ProjectFactory(factory.DjangoModelFactory): logo = factory.django.ImageField(color='green') -class EditionFactory(factory.DjangoModelFactory): +class EditionFactory(factory.django.DjangoModelFactory): """Edition object factory.""" class Meta: # noqa @@ -37,7 +37,7 @@ class EditionFactory(factory.DjangoModelFactory): return project and project or ProjectFactory.create() -class EditionFormFactory(factory.DjangoModelFactory): +class EditionFormFactory(factory.django.DjangoModelFactory): class Meta: # noqa model = EditionForm @@ -45,7 +45,7 @@ class EditionFormFactory(factory.DjangoModelFactory): deadline = factory.Faker('future_date') -class ParticipationFactory(factory.DjangoModelFactory): +class ParticipationFactory(factory.django.DjangoModelFactory): """Participation object factory.""" class Meta: # noqa diff --git a/register/factory.py b/register/factory.py index 973d6e793e7b4abeef18708936f0b0eeef3b44fd..4fe95805b716f63146d51b8b76e76fb3c99a3fcd 100644 --- a/register/factory.py +++ b/register/factory.py @@ -8,7 +8,7 @@ from utils import printable_only from . import models -class RegistrationFactory(factory.DjangoModelFactory): +class RegistrationFactory(factory.django.DjangoModelFactory): """Registration object factory.""" class Meta: # noqa diff --git a/requirements.txt b/requirements.txt index 924e008527b9f4141c9146a3629b7428dd5767f6..85baa939feb52944045b1648211bc371283a6767 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,6 +35,9 @@ Pillow # Testing factory-boy +# Miscellanneous +python-dotenv==0.11 + # Deployment django-heroku # Heroku integration whitenoise # Static files serving diff --git a/templates/email-reset-template.txt b/templates/email-reset-template.txt index f222d5919241d2b60062eb0d6451bd619c690fb4..36aab0790716a61c71efe0a26d6e3cd5c837bc26 100644 --- a/templates/email-reset-template.txt +++ b/templates/email-reset-template.txt @@ -1,15 +1,15 @@ {% load i18n %}{% autoescape off %} -{% blocktrans %}Vous recevez ce courriel car vous avez demander à -réinitialiser le mot de passe de votre compte Oser.{% endblocktrans %} +{% blocktrans %}Vous recevez ce courriel car vous avez demandé à +réinitialiser le mot de passe de votre compte OSER.{% endblocktrans %} {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} -http://localhost:4200{% url 'password_reset_confirm' uidb64=uid token=token %} +https://www.oser-cs.fr/{% url 'password_reset_confirm' uidb64=uid token=token %} {% endblock %} {% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} {% trans "Thanks for using our site!" %} -{% blocktrans %}The Oser team{% endblocktrans %} +{% blocktrans %}La Team OSER {% endblocktrans %} {% endautoescape %} \ No newline at end of file diff --git a/templates/subject-reset-template.txt b/templates/subject-reset-template.txt new file mode 100644 index 0000000000000000000000000000000000000000..88499bb15e4f974b37963428650e3189f6675b72 --- /dev/null +++ b/templates/subject-reset-template.txt @@ -0,0 +1,3 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}Réinitialisation du mot de passe de votre compte OSER{% endblocktrans %} +{% endautoescape %} diff --git a/tests/test_profiles/test_student.py b/tests/test_profiles/test_student.py index 0d9d65fc47e1619475c78325b8eb82f4af3641b2..f5b6541951f94976be5db30cde8b0fe1b8f7f402 100644 --- a/tests/test_profiles/test_student.py +++ b/tests/test_profiles/test_student.py @@ -3,7 +3,7 @@ from profiles.factory import StudentFactory from profiles.models import Student from tests.utils import ModelTestCase -from users.factory import UserFactory +from users.factory import UserFactory, StaffUserFactory class StudentTestCase(ModelTestCase): @@ -32,7 +32,8 @@ class StudentTestCase(ModelTestCase): self.assertEqual(self.obj, self.obj.user.student) def test_get_absolute_url(self): - self.client.force_login(UserFactory.create()) + staff_user = StaffUserFactory.create() + self.client.force_login(staff_user) url = self.obj.get_absolute_url() response = self.client.get(url) self.assertEqual(200, response.status_code) diff --git a/tests/test_profiles/test_student_api.py b/tests/test_profiles/test_student_api.py index 204649b47eadba66ce7be8a9e17c09568244d664..d9caacb0d140e89e4d3724b35fcc0780647d2e74 100644 --- a/tests/test_profiles/test_student_api.py +++ b/tests/test_profiles/test_student_api.py @@ -19,7 +19,7 @@ class StudentEndpointsTest(HyperlinkedAPITestCase): def perform_retrieve(self, obj=None): if obj is None: obj = self.factory.create() - response = self.client.get('/api/students/{obj.pk}/'.format(obj=obj)) + response = self.client.get('/api/students/') return response def test_list(self): diff --git a/users/factory.py b/users/factory.py index b197e55f088bb9b1901794aaad9f16272576836c..850d0c4b7a167af11e3dfc7d016e85a79218ea98 100644 --- a/users/factory.py +++ b/users/factory.py @@ -9,7 +9,7 @@ from utils import printable_only User = get_user_model() -class UserFactory(factory.DjangoModelFactory): +class UserFactory(factory.django.DjangoModelFactory): """User object factory.""" class Meta: # noqa @@ -41,3 +41,7 @@ class UserFactory(factory.DjangoModelFactory): manager = cls._get_manager(model_class) # The default would use ``manager.create(*args, **kwargs)`` return manager.create_user(*args, **kwargs) + +class StaffUserFactory(UserFactory): + """Staff user object factory.""" + is_staff = True \ No newline at end of file diff --git a/visits/factory.py b/visits/factory.py index 11817aa2c4692fcf5483f3029886a64cb5b9eae3..05fce07d0a39d1fcd1d5257c7a68390876e28f30 100644 --- a/visits/factory.py +++ b/visits/factory.py @@ -15,7 +15,7 @@ from . import models User = get_user_model() -class PlaceFactory(factory.DjangoModelFactory): +class PlaceFactory(factory.django.DjangoModelFactory): """Place object factory.""" class Meta: # noqa @@ -28,7 +28,7 @@ class PlaceFactory(factory.DjangoModelFactory): description = factory.LazyAttribute(lambda o: '\n'.join(o._description)) -class VisitFactory(factory.DjangoModelFactory): +class VisitFactory(factory.django.DjangoModelFactory): """Visit object factory.""" deadline_random_range = (-5, -1) @@ -81,7 +81,7 @@ class VisitWithClosedRegistrationsFactory(VisitFactory): deadline_random_range = (-10, -6) # guaranteed to be before today -class ParticipationFactory(factory.DjangoModelFactory): +class ParticipationFactory(factory.django.DjangoModelFactory): """Visit participant object factory. Users and visit are picked from pre-existing objects, diff --git a/visits/templates/visits/confirm_participation.md b/visits/templates/visits/confirm_participation.md index 8772191966ebd1875daed20fe0decc878606a5b1..01466352b7109e7616e393a213df1f23ebb96964 100644 --- a/visits/templates/visits/confirm_participation.md +++ b/visits/templates/visits/confirm_participation.md @@ -10,7 +10,7 @@ Bonjour{% if participation.user.first_name %} {{ participation.user.first_name } {% if participation.accepted %} Bonne nouvelle : nous avons validé ta participation à la sortie. ✅ -En te rendant sur [l'espace sorties]({{ participation.visit.get_site_url }}), tu peux dès à présent: +Avant la sortie, tu pourras, en te rendant sur [l'espace sorties]({{ participation.visit.get_site_url }}) : - Consulter les informations pratiques ; - Télécharger la fiche sortie ;