From e470a2a26892c16d79958e1a2fd89bcd0943d1de Mon Sep 17 00:00:00 2001 From: Robin Champenois Date: Sun, 7 Feb 2021 18:23:24 +0100 Subject: [PATCH] =?UTF-8?q?Mise=20=C3=A0=20jour=202021?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- avisstage/admin.py | 5 + avisstage/allauth_adapter.py | 57 --- avisstage/apps.py | 3 - avisstage/forms.py | 44 ++- .../management/commands/termine_scolarite.py | 16 + .../migrations/0003_auto_20210117_1208.py | 45 +++ .../migrations/0004_allauth_to_authens.py | 107 ++++++ .../migrations/0005_normalien_en_scolarite.py | 18 + .../migrations/0006_auto_20210131_1954.py | 32 ++ avisstage/models.py | 136 ++++--- avisstage/sass/_responsive.scss | 4 +- avisstage/sass/screen.scss | 58 ++- avisstage/static/css/screen.css | 343 ++++++++++-------- avisstage/static/js/select_lieu.js | 4 +- avisstage/templates/avisstage/base.html | 4 +- .../avisstage/compte/aconfirmer.html | 19 + .../templates/avisstage/compte/edit_mdp.html | 29 ++ .../avisstage/compte/email_supprime.html | 18 + .../avisstage/compte/parametres.html | 76 ++++ .../templates/avisstage/detail/profil.html | 4 +- .../templates/avisstage/detail/stage.html | 2 +- .../avisstage/formulaires/profil.html | 9 +- avisstage/templates/avisstage/index.html | 4 +- .../templates/avisstage/mails/reinit_mdp.html | 8 + .../templates/avisstage/mails/reinit_mdp.txt | 1 + avisstage/templates/avisstage/perso.html | 54 +-- avisstage/tests.py | 107 ++++-- avisstage/urls.py | 54 ++- avisstage/utils.py | 8 +- avisstage/views.py | 141 ++++++- experiENS/auth.py | 15 +- experiENS/settings_base.py | 35 +- experiENS/settings_dev.py | 6 +- experiENS/urls.py | 15 +- requirements.txt | 19 +- 36 files changed, 1082 insertions(+), 420 deletions(-) delete mode 100644 avisstage/allauth_adapter.py create mode 100644 avisstage/management/commands/termine_scolarite.py create mode 100644 avisstage/migrations/0003_auto_20210117_1208.py create mode 100644 avisstage/migrations/0004_allauth_to_authens.py create mode 100644 avisstage/migrations/0005_normalien_en_scolarite.py create mode 100644 avisstage/migrations/0006_auto_20210131_1954.py create mode 100644 avisstage/templates/avisstage/compte/aconfirmer.html create mode 100644 avisstage/templates/avisstage/compte/edit_mdp.html create mode 100644 avisstage/templates/avisstage/compte/email_supprime.html create mode 100644 avisstage/templates/avisstage/compte/parametres.html create mode 100644 avisstage/templates/avisstage/mails/reinit_mdp.html create mode 100644 avisstage/templates/avisstage/mails/reinit_mdp.txt diff --git a/README.md b/README.md index 93536c0..ba23394 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Il est visible sur https://www.eleves.ens.fr/experiens/ Clonez le dépôt. Installez les pré-requis : - sudo apt-get install libxlst-dev python3.4-dev + sudo apt-get install libxlst-dev libsals2-dev libxml2-dev libldap2-dev libssl-dev On a besoin de SpatiaLite pour une base de données GIS. Essayez diff --git a/avisstage/admin.py b/avisstage/admin.py index 5d3581a..c57f678 100644 --- a/avisstage/admin.py +++ b/avisstage/admin.py @@ -3,6 +3,8 @@ from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User from avisstage.models import * +import authens.models as authmod + class NormalienInline(admin.StackedInline): model = Normalien inline_classes = ("collapse open",) @@ -32,3 +34,6 @@ admin.site.register(User, UserAdmin) admin.site.register(Lieu) admin.site.register(StageMatiere, StageMatiereAdmin) admin.site.register(Stage, StageAdmin) + +admin.site.register(authmod.CASAccount) +admin.site.register(authmod.OldCASAccount) diff --git a/avisstage/allauth_adapter.py b/avisstage/allauth_adapter.py deleted file mode 100644 index 1935e47..0000000 --- a/avisstage/allauth_adapter.py +++ /dev/null @@ -1,57 +0,0 @@ -from allauth.account.adapter import DefaultAccountAdapter -from allauth.socialaccount.models import SocialAccount -from allauth_ens.adapter import LongTermClipperAccountAdapter, get_ldap_infos - -class AccountAdapter(DefaultAccountAdapter): - def is_open_for_signup(self, request): - return False - - -class SocialAccountAdapter(LongTermClipperAccountAdapter): - def is_open_for_signup(self, request, sociallogin): - # sociallogin.account is a SocialAccount instance. - # See https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/models.py - - if sociallogin.account.provider == 'clipper': - return True - - # It returns AccountAdapter.is_open_for_signup(). - # See https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/adapter.py - return super().is_open_for_signup(request, sociallogin) - - - # TODO : HOTFIX pour un bug d'allauth_ens - # On remplace la déduplication des comptes faites avec "entrance_year" - # par une déduplication sur le nom d'utilisateur - # (Copié de allauth_ens) - def pre_social_login(self, request, sociallogin): - if sociallogin.account.provider != "clipper": - return super(LongTermClipperAccountAdapter, - self).pre_social_login(request, sociallogin) - - clipper_uid = sociallogin.account.uid - try: - old_conn = SocialAccount.objects.get(provider='clipper_inactive', - uid=clipper_uid) - except SocialAccount.DoesNotExist: - return - - ldap_data = get_ldap_infos(clipper_uid) - sociallogin._ldap_data = ldap_data - - if ldap_data is None or 'entrance_year' not in ldap_data: - raise ValueError("No entrance year in LDAP data") - - old_conn_username = old_conn.user.username - - # HOTFIX ICI - if self.get_username(clipper_uid, ldap_data) != old_conn_username: - email = ldap_data.get('email', get_clipper_email(clipper_uid)) - remove_email(old_conn.user, email) - - return - - old_conn.provider = 'clipper' - old_conn.save() - - sociallogin.lookup() diff --git a/avisstage/apps.py b/avisstage/apps.py index afc1822..4fa2dd9 100644 --- a/avisstage/apps.py +++ b/avisstage/apps.py @@ -1,7 +1,4 @@ -from __future__ import unicode_literals - from django.apps import AppConfig - class AvisstageConfig(AppConfig): name = 'avisstage' diff --git a/avisstage/forms.py b/avisstage/forms.py index 8af5c57..12e88ea 100644 --- a/avisstage/forms.py +++ b/avisstage/forms.py @@ -1,11 +1,14 @@ -# coding: utf-8 +import unicodedata from django import forms +from django.contrib.auth.forms import PasswordResetForm from django.utils import timezone +from simple_email_confirmation.models import EmailAddress + import re -from .models import Normalien, Stage, Lieu, AvisLieu, AvisStage +from .models import Normalien, Stage, Lieu, AvisLieu, AvisStage, User from .widgets import LatLonField # Sur-classe utile @@ -107,3 +110,40 @@ class FeedbackForm(forms.Form): objet = forms.CharField(label="Objet", required=True) message = forms.CharField(label="Message", required=True, widget=forms.widgets.Textarea()) +# Nouvelle adresse mail +class AdresseEmailForm(forms.Form): + def __init__(self, _user, **kwargs): + self._user = _user + super().__init__(**kwargs) + email = forms.EmailField(widget=forms.widgets.EmailInput(attrs={"placeholder": "Nouvelle adresse"})) + + def clean_email(self): + email = self.cleaned_data["email"] + if EmailAddress.objects.filter(user=self._user, email=email).exists(): + raise forms.ValidationError( + "Cette adresse est déjà associée à ce compte") + return email + + +def _unicode_ci_compare(s1, s2): + """ + Perform case-insensitive comparison of two identifiers, using the + recommended algorithm from Unicode Technical Report 36, section + 2.11.2(B)(2). + """ + return unicodedata.normalize('NFKC', s1).casefold() == unicodedata.normalize('NFKC', s2).casefold() + + +# (Ré)initialisation du mot de passe +class ReinitMdpForm(PasswordResetForm): + def get_users(self, email): + """Override default method to allow unusable passwords""" + email_field_name = User.get_email_field_name() + active_users = User._default_manager.filter(**{ + '%s__iexact' % email_field_name: email, + 'is_active': True, + }) + return ( + u for u in active_users + if _unicode_ci_compare(email, getattr(u, email_field_name)) + ) diff --git a/avisstage/management/commands/termine_scolarite.py b/avisstage/management/commands/termine_scolarite.py new file mode 100644 index 0000000..6779604 --- /dev/null +++ b/avisstage/management/commands/termine_scolarite.py @@ -0,0 +1,16 @@ +from django.core.management.base import BaseCommand, CommandError +from avisstage.models import Normalien + +from django.utils import timezone +from datetime import timedelta + +class Command(BaseCommand): + help = 'Réinitialise les statuts "en scolarité" de tout le monde' + + def add_arguments(self, parser): + return + + def handle(self, *args, **options): + old_conn = timezone.now() - timedelta(days=365) + Normalien.objects.all().update(last_cas_connect=t) + self.stdout.write(self.style.SUCCESS(u'Terminé')) diff --git a/avisstage/migrations/0003_auto_20210117_1208.py b/avisstage/migrations/0003_auto_20210117_1208.py new file mode 100644 index 0000000..307760c --- /dev/null +++ b/avisstage/migrations/0003_auto_20210117_1208.py @@ -0,0 +1,45 @@ +# Generated by Django 2.2.17 on 2021-01-17 11:08 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('avisstage', '0002_auto_20171002_2243'), + ] + + operations = [ + migrations.AlterField( + model_name='lieu', + name='pays', + field=models.CharField(choices=[('AF', 'Afghanistan'), ('AL', 'Albanie'), ('AQ', 'Antarctique'), ('DZ', 'Algérie'), ('AS', 'Samoa Américaines'), ('AD', 'Andorre'), ('AO', 'Angola'), ('AG', 'Antigua-et-Barbuda'), ('AZ', 'Azerbaïdjan'), ('AR', 'Argentine'), ('AU', 'Australie'), ('AT', 'Autriche'), ('BS', 'Bahamas'), ('BH', 'Bahreïn'), ('BD', 'Bangladesh'), ('AM', 'Arménie'), ('BB', 'Barbade'), ('BE', 'Belgique'), ('BM', 'Bermudes'), ('BT', 'Bhoutan'), ('BO', 'Bolivie'), ('BA', 'Bosnie-Herzégovine'), ('BW', 'Botswana'), ('BV', 'Île Bouvet'), ('BR', 'Brésil'), ('BZ', 'Belize'), ('IO', "Territoire Britannique de l'Océan Indien"), ('SB', 'Îles Salomon'), ('VG', 'Îles Vierges Britanniques'), ('BN', 'Brunéi Darussalam'), ('BG', 'Bulgarie'), ('MM', 'Myanmar'), ('BI', 'Burundi'), ('BY', 'Bélarus'), ('KH', 'Cambodge'), ('CM', 'Cameroun'), ('CA', 'Canada'), ('CV', 'Cap-vert'), ('KY', 'Îles Caïmanes'), ('CF', 'République Centrafricaine'), ('LK', 'Sri Lanka'), ('TD', 'Tchad'), ('CL', 'Chili'), ('CN', 'Chine'), ('TW', 'Taïwan'), ('CX', 'Île Christmas'), ('CC', 'Îles Cocos (Keeling)'), ('CO', 'Colombie'), ('KM', 'Comores'), ('YT', 'Mayotte'), ('CG', 'République du Congo'), ('CD', 'République Démocratique du Congo'), ('CK', 'Îles Cook'), ('CR', 'Costa Rica'), ('HR', 'Croatie'), ('CU', 'Cuba'), ('CY', 'Chypre'), ('CZ', 'République Tchèque'), ('BJ', 'Bénin'), ('DK', 'Danemark'), ('DM', 'Dominique'), ('DO', 'République Dominicaine'), ('EC', 'Équateur'), ('SV', 'El Salvador'), ('GQ', 'Guinée Équatoriale'), ('ET', 'Éthiopie'), ('ER', 'Érythrée'), ('EE', 'Estonie'), ('FO', 'Îles Féroé'), ('FK', 'Îles (malvinas) Falkland'), ('GS', 'Géorgie du Sud et les Îles Sandwich du Sud'), ('FJ', 'Fidji'), ('FI', 'Finlande'), ('AX', 'Îles Åland'), ('FR', 'France'), ('GF', 'Guyane Française'), ('PF', 'Polynésie Française'), ('TF', 'Terres Australes Françaises'), ('DJ', 'Djibouti'), ('GA', 'Gabon'), ('GE', 'Géorgie'), ('GM', 'Gambie'), ('PS', 'Territoire Palestinien Occupé'), ('DE', 'Allemagne'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('KI', 'Kiribati'), ('GR', 'Grèce'), ('GL', 'Groenland'), ('GD', 'Grenade'), ('GP', 'Guadeloupe'), ('GU', 'Guam'), ('GT', 'Guatemala'), ('GN', 'Guinée'), ('GY', 'Guyana'), ('HT', 'Haïti'), ('HM', 'Îles Heard et Mcdonald'), ('VA', 'Saint-Siège (état de la Cité du Vatican)'), ('HN', 'Honduras'), ('HK', 'Hong-Kong'), ('HU', 'Hongrie'), ('IS', 'Islande'), ('IN', 'Inde'), ('ID', 'Indonésie'), ('IR', "République Islamique d'Iran"), ('IQ', 'Iraq'), ('IE', 'Irlande'), ('IL', 'Israël'), ('IT', 'Italie'), ('CI', "Côte d'Ivoire"), ('JM', 'Jamaïque'), ('JP', 'Japon'), ('KZ', 'Kazakhstan'), ('JO', 'Jordanie'), ('KE', 'Kenya'), ('KP', 'République Populaire Démocratique de Corée'), ('KR', 'République de Corée'), ('KW', 'Koweït'), ('KG', 'Kirghizistan'), ('LA', 'République Démocratique Populaire Lao'), ('LB', 'Liban'), ('LS', 'Lesotho'), ('LV', 'Lettonie'), ('LR', 'Libéria'), ('LY', 'Jamahiriya Arabe Libyenne'), ('LI', 'Liechtenstein'), ('LT', 'Lituanie'), ('LU', 'Luxembourg'), ('MO', 'Macao'), ('MG', 'Madagascar'), ('MW', 'Malawi'), ('MY', 'Malaisie'), ('MV', 'Maldives'), ('ML', 'Mali'), ('MT', 'Malte'), ('MQ', 'Martinique'), ('MR', 'Mauritanie'), ('MU', 'Maurice'), ('MX', 'Mexique'), ('MC', 'Monaco'), ('MN', 'Mongolie'), ('MD', 'République de Moldova'), ('MS', 'Montserrat'), ('MA', 'Maroc'), ('MZ', 'Mozambique'), ('OM', 'Oman'), ('NA', 'Namibie'), ('NR', 'Nauru'), ('NP', 'Népal'), ('NL', 'Pays-Bas'), ('AN', 'Antilles Néerlandaises'), ('AW', 'Aruba'), ('NC', 'Nouvelle-Calédonie'), ('VU', 'Vanuatu'), ('NZ', 'Nouvelle-Zélande'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigéria'), ('NU', 'Niué'), ('NF', 'Île Norfolk'), ('NO', 'Norvège'), ('MP', 'Îles Mariannes du Nord'), ('UM', 'Îles Mineures Éloignées des États-Unis'), ('FM', 'États Fédérés de Micronésie'), ('MH', 'Îles Marshall'), ('PW', 'Palaos'), ('PK', 'Pakistan'), ('PA', 'Panama'), ('PG', 'Papouasie-Nouvelle-Guinée'), ('PY', 'Paraguay'), ('PE', 'Pérou'), ('PH', 'Philippines'), ('PN', 'Pitcairn'), ('PL', 'Pologne'), ('PT', 'Portugal'), ('GW', 'Guinée-Bissau'), ('TL', 'Timor-Leste'), ('PR', 'Porto Rico'), ('QA', 'Qatar'), ('RE', 'Réunion'), ('RO', 'Roumanie'), ('RU', 'Fédération de Russie'), ('RW', 'Rwanda'), ('SH', 'Sainte-Hélène'), ('KN', 'Saint-Kitts-et-Nevis'), ('AI', 'Anguilla'), ('LC', 'Sainte-Lucie'), ('PM', 'Saint-Pierre-et-Miquelon'), ('VC', 'Saint-Vincent-et-les Grenadines'), ('SM', 'Saint-Marin'), ('ST', 'Sao Tomé-et-Principe'), ('SA', 'Arabie Saoudite'), ('SN', 'Sénégal'), ('SC', 'Seychelles'), ('SL', 'Sierra Leone'), ('SG', 'Singapour'), ('SK', 'Slovaquie'), ('VN', 'Viet Nam'), ('SI', 'Slovénie'), ('SO', 'Somalie'), ('ZA', 'Afrique du Sud'), ('ZW', 'Zimbabwe'), ('ES', 'Espagne'), ('EH', 'Sahara Occidental'), ('SD', 'Soudan'), ('SR', 'Suriname'), ('SJ', 'Svalbard etÎle Jan Mayen'), ('SZ', 'Swaziland'), ('SE', 'Suède'), ('CH', 'Suisse'), ('SY', 'République Arabe Syrienne'), ('TJ', 'Tadjikistan'), ('TH', 'Thaïlande'), ('TG', 'Togo'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TT', 'Trinité-et-Tobago'), ('AE', 'Émirats Arabes Unis'), ('TN', 'Tunisie'), ('TR', 'Turquie'), ('TM', 'Turkménistan'), ('TC', 'Îles Turks et Caïques'), ('TV', 'Tuvalu'), ('UG', 'Ouganda'), ('UA', 'Ukraine'), ('MK', "L'ex-République Yougoslave de Macédoine"), ('EG', 'Égypte'), ('GB', 'Royaume-Uni'), ('IM', 'Île de Man'), ('TZ', 'République-Unie de Tanzanie'), ('US', 'États-Unis'), ('VI', 'Îles Vierges des États-Unis'), ('BF', 'Burkina Faso'), ('UY', 'Uruguay'), ('UZ', 'Ouzbékistan'), ('VE', 'Venezuela'), ('WF', 'Wallis et Futuna'), ('WS', 'Samoa'), ('YE', 'Yémen'), ('CS', 'Serbie-et-Monténégro'), ('ZM', 'Zambie')], max_length=2, verbose_name='Pays'), + ), + migrations.AlterField( + model_name='lieu', + name='type_lieu', + field=models.CharField(choices=[('universite', 'Université'), ('entreprise', 'Entreprise'), ('centrerecherche', 'Centre de recherche'), ('administration', 'Administration'), ('autre', 'Autre')], default='universite', max_length=15, verbose_name="Type de structure d'accueil"), + ), + migrations.AlterField( + model_name='normalien', + name='user', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='profil', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='stage', + name='auteur', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stages', to='avisstage.Normalien'), + ), + migrations.AlterField( + model_name='stage', + name='niveau_scol', + field=models.CharField(blank=True, choices=[('L3', 'Licence 3'), ('M1', 'Master 1'), ('M2', 'Master 2'), ('DOC', 'Pré-doctorat'), ('CST', 'Césure'), ('BLA', 'Année blanche'), ('VAC', 'Vacances scolaires'), ('MIT', 'Mi-temps en parallèle des études'), ('', 'Autre')], default='', max_length=3, verbose_name='Année de scolarité'), + ), + migrations.AlterField( + model_name='stage', + name='type_stage', + field=models.CharField(choices=[('Recherche :', (('recherche', 'Stage académique'), ('recherche_autre', 'Stage non-académique'), ('sejour_dri', 'Séjour de recherche DRI'))), ('Stage sans visée de recherche :', (('pro', 'Stage en entreprise'), ('admin', 'Stage en admin./ONG/orga. internationale'))), ('Enseignement :', (('lectorat', 'Lectorat DRI'), ('autre_teach', "Autre expérience d'enseignement"))), ('autre', 'Autre')], default='stage', max_length=31, verbose_name='Type'), + ), + ] diff --git a/avisstage/migrations/0004_allauth_to_authens.py b/avisstage/migrations/0004_allauth_to_authens.py new file mode 100644 index 0000000..9de7655 --- /dev/null +++ b/avisstage/migrations/0004_allauth_to_authens.py @@ -0,0 +1,107 @@ +from django.apps import apps as global_apps +from django.db import migrations + +from django.utils import timezone + +def forwards(apps, schema_editor): + User = apps.get_model('auth', 'User') + + try: + CASAccount = apps.get_model('authens', 'CASAccount') + except LookupError: + return + + try: + SocialAccount = apps.get_model('socialaccount', 'SocialAccount') + OldEmailAddress = apps.get_model('account', 'EmailAddress') + except LookupError: + # Allauth not installed + # Simply create CAS accounts for every profile + # This procedure is not meant to be fast + from authens.shortcuts import fetch_cas_account + + def migrate_user(user): + ldap_info = fetch_cas_account(user.username) + if ldap_info: + entrance_year = ldap_info["entrance_year"] + CASAccount.objects.create( + user=user, cas_login=user.username, + entrance_year=entrance_year + ) + + for user in User.objects.all(): + migrate_user(user) + return + + + NewEmailAddress = apps.get_model('simple_email_confirmation', + 'EmailAddress') + from simple_email_confirmation.models import EmailAddressManager + + # Transfer from allauth to authens + # Assumes usernames have the format @ + # Assumes no clashing clipper accounts have ever been found + oldusers = ( + User.objects.all().prefetch_related( + "emailaddress_set", "socialaccount_set") + ) + + is_ens_mail = lambda mail: ( + mail is not None and (mail.endswith("ens.fr") or mail.endswith("ens.psl.eu"))) + new_conns = [] + new_mails = [] + + for user in oldusers: + # Move EmailAddress to new model + addresses = user.emailaddress_set.all() + for addr in addresses: + newaddr = NewEmailAddress( + user=user, email=addr.email, + set_at=timezone.now(), + confirmed_at=(timezone.now() if addr.verified else None), + key=EmailAddressManager().generate_key(), + ) + if addr.primary and user.email != addr.email: + print("Adresse principale inconsistante", + user.email, addr.email) + new_mails.append(newaddr) + + # Create new CASAccount connexion + saccounts = user.socialaccount_set.all() + if not saccounts: + continue + if len(saccounts) > 1: + print(saccounts) + saccount = saccounts[0] + clipper = saccount.uid + if "@" not in user.username: + print(user.username) + continue + entrance_year = saccount.extra_data.get( + "entrance_year", user.username.split("@")[1]) + try: + entrance_year = 2000 + int(entrance_year) + except ValueError: + print(entrance_year) + continue + + new_conns.append(CASAccount(user=user, cas_login=clipper, + entrance_year=int(entrance_year))) + + NewEmailAddress.objects.bulk_create(new_mails) + CASAccount.objects.bulk_create(new_conns) + +class Migration(migrations.Migration): + operations = [ + migrations.RunPython(forwards, migrations.RunPython.noop), + ] + dependencies = [ + ('avisstage', '0003_auto_20210117_1208'), + ('authens', '0002_old_cas_account'), + ] + + if global_apps.is_installed('allauth'): + dependencies.append(('socialaccount', '0003_extra_data_default_dict')) + + if global_apps.is_installed('simple_email_confirmation'): + dependencies.append(('simple_email_confirmation', '0001_initial')) diff --git a/avisstage/migrations/0005_normalien_en_scolarite.py b/avisstage/migrations/0005_normalien_en_scolarite.py new file mode 100644 index 0000000..da85767 --- /dev/null +++ b/avisstage/migrations/0005_normalien_en_scolarite.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.17 on 2021-01-17 20:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('avisstage', '0004_allauth_to_authens'), + ] + + operations = [ + migrations.AddField( + model_name='normalien', + name='en_scolarite', + field=models.BooleanField(blank=True, default=False), + ), + ] diff --git a/avisstage/migrations/0006_auto_20210131_1954.py b/avisstage/migrations/0006_auto_20210131_1954.py new file mode 100644 index 0000000..1ea3ac5 --- /dev/null +++ b/avisstage/migrations/0006_auto_20210131_1954.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2.17 on 2021-01-31 18:54 + +import avisstage.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('avisstage', '0005_normalien_en_scolarite'), + ] + + operations = [ + migrations.RemoveField( + model_name='normalien', + name='en_scolarite', + ), + migrations.RemoveField( + model_name='normalien', + name='mail', + ), + migrations.AddField( + model_name='normalien', + name='last_cas_login', + field=models.DateField(default=avisstage.models._default_cas_login), + ), + migrations.AlterField( + model_name='normalien', + name='contactez_moi', + field=models.BooleanField(default=True, help_text='Affiche votre adresse e-mail principale sur votre profil public', verbose_name='Inviter les visiteurs à me contacter'), + ), + ] diff --git a/avisstage/models.py b/avisstage/models.py index 4cbf780..1b63c8d 100644 --- a/avisstage/models.py +++ b/avisstage/models.py @@ -1,10 +1,3 @@ -# coding: utf-8 - -from __future__ import unicode_literals - -from allauth.account.models import EmailAddress -from allauth.socialaccount.models import SocialAccount - from django.db import models from django.db.models.signals import post_save from django.contrib.auth.models import User @@ -20,26 +13,36 @@ from django.utils.html import strip_tags from taggit_autosuggest.managers import TaggableManager from tinymce.models import HTMLField as RichTextField -from .utils import choices_length -from .statics import DEPARTEMENTS_DEFAUT, PAYS_OPTIONS, TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS, TYPE_LIEU_DICT, TYPE_STAGE_DICT, NIVEAU_SCOL_OPTIONS, NIVEAU_SCOL_DICT +from authens.signals import post_cas_connect +from authens.models import CASAccount -import ldap +from datetime import timedelta + +from .utils import choices_length, is_email_ens +from .statics import ( + DEPARTEMENTS_DEFAUT, PAYS_OPTIONS, TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS, TYPE_LIEU_DICT, + TYPE_STAGE_DICT, NIVEAU_SCOL_OPTIONS, NIVEAU_SCOL_DICT +) + +def _default_cas_login(): + return (timezone.now()-timedelta(days=365)).date() # # Profil Normalien (extension du modèle User) # class Normalien(models.Model): - user = models.OneToOneField(User, related_name="profil") + user = models.OneToOneField(User, related_name="profil", + on_delete=models.SET_NULL, null=True) # Infos spécifiques nom = models.CharField(u"Nom complet", max_length=255, blank=True) promotion = models.CharField(u"Promotion", max_length=40, blank=True) - mail = models.EmailField(u"Adresse e-mail permanente", - max_length=200, blank=True) - contactez_moi = models.BooleanField(u"Inviter les visiteurs à me contacter", - default=True) - bio = models.TextField(u"À propos de moi", blank=True, default=""); + contactez_moi = models.BooleanField( + u"Inviter les visiteurs à me contacter", + default=True, help_text="Affiche votre adresse e-mail principale sur votre profil public") + bio = models.TextField(u"À propos de moi", blank=True, default="") + last_cas_login = models.DateField(default=_default_cas_login) class Meta: verbose_name = u"Profil élève" @@ -52,53 +55,66 @@ class Normalien(models.Model): def stages_publics(self): return self.stages.filter(public=True).order_by('-date_debut') - @cached_property - def en_scolarite(self): - return SocialAccount.objects.filter(user_id=self.user_id, - provider="clipper").exists() - def has_nonENS_email(self): - a = EmailAddress.objects.filter(user_id=self.user_id, - verified=True) \ - .exclude(email__endswith="ens.fr") - return a.exists() + return ( + self.user.email_address_set + .exclude(confirmed_at__isnull=True) + .exclude(email__endswith="ens.fr") + .exclude(email__endswith="ens.psl.eu") + .exists() + ) + + def nom_complet(self): + if self.nom.strip(): + return self.nom + return self.user.username + + @property + def en_scolarite(self): + return self.last_cas_login > (timezone.now() - timedelta(days=60)).date() @property def preferred_email(self): - a = EmailAddress.objects.filter(user_id=self.user_id, - verified=True) \ - .exclude(email__endswith="ens.fr")\ - .order_by('-primary') - if len(a) == 0: - a = EmailAddress.objects.filter(user_id=self.user_id, - verified=True) \ - .order_by('-primary') - if len(a) == 0: - return "" - else: - return a[0].email + return self.user.email -# Hook à la création d'un nouvel utilisateur : récupération de ses infos par LDAP -def create_user_profile(sender, instance, created, **kwargs): +# Hook à la création d'un nouvel utilisateur : information de base +def create_basic_user_profile(sender, instance, created, **kwargs): if created: profil, created = Normalien.objects.get_or_create(user=instance) - try: - saccount = SocialAccount.objects.get(user=instance, - provider="clipper") - except SocialAccount.DoesNotExist: - profil.save() - return - edata = saccount.extra_data.get("ldap", {}) - dep = "" - if "department_code" in edata: - dep = dict(DEPARTEMENTS_DEFAUT).get( - edata["department_code"].lower(), '') - profil.promotion = "%s %s" % (dep, edata["entrance_year"]) - profil.nom = edata.get("name", "") - profil.save() + if not created and profil.promotion != "": + return + + if "@" in instance.username: + profil.promotion = instance.username.split("@")[1] + profil.save() -post_save.connect(create_user_profile, sender=User) +post_save.connect(create_basic_user_profile, sender=User) + +# Hook d'authENS : information du CAS +def handle_cas_connection(sender, instance, created, cas_login, attributes, **kwargs): + profil, created = Normalien.objects.get_or_create(user=instance) + + profil.last_cas_login = timezone.now().date() + if not created: + profil.save() + return + + dirs = attributes.get("homeDirectory", "").split("/") + if len(dirs) < 4: + print("HomeDirectory invalide", dirs) + return + + year = dirs[2] + departement = dirs[3] + + dep = dict(DEPARTEMENTS_DEFAUT).get(departement.lower(), "") + + profil.promotion = "%s %s" % (dep, year) + profil.nom = attributes.get("name", "") + profil.save() + +post_cas_connect.connect(handle_cas_connection, sender=User) # # Lieu de stage @@ -121,7 +137,7 @@ class Lieu(models.Model): max_length=choices_length(PAYS_OPTIONS)) # Coordonnées - objects = geomodels.GeoManager() # Requis par GeoDjango + #objects = geomodels.GeoManager() # Requis par GeoDjango coord = geomodels.PointField(u"Coordonnées", geography=True, srid = 4326) @@ -163,7 +179,8 @@ class StageMatiere(models.Model): class Stage(models.Model): # Misc - auteur = models.ForeignKey(Normalien, related_name="stages") + auteur = models.ForeignKey(Normalien, related_name="stages", + on_delete=models.SET_NULL, null=True) public = models.BooleanField(u"Visible publiquement", default=False) date_creation = models.DateTimeField(u"Créé le", default=timezone.now) date_maj = models.DateTimeField(u"Mis à jour le", default=timezone.now) @@ -260,7 +277,8 @@ class Stage(models.Model): # class AvisStage(models.Model): - stage = models.OneToOneField(Stage, related_name="avis_stage") + stage = models.OneToOneField(Stage, related_name="avis_stage", + on_delete=models.CASCADE) chapo = models.TextField(u"En quelques mots", blank=True) avis_ambiance = RichTextField(u"L'ambiance de travail", blank=True) @@ -283,8 +301,8 @@ class AvisStage(models.Model): class AvisLieu(models.Model): - stage = models.ForeignKey(Stage) - lieu = models.ForeignKey(Lieu) + stage = models.ForeignKey(Stage, on_delete=models.CASCADE) + lieu = models.ForeignKey(Lieu, on_delete=models.CASCADE) order = models.IntegerField("Ordre", default=0) chapo = models.TextField(u"En quelques mots", blank=True) diff --git a/avisstage/sass/_responsive.scss b/avisstage/sass/_responsive.scss index c37acef..fb1457c 100644 --- a/avisstage/sass/_responsive.scss +++ b/avisstage/sass/_responsive.scss @@ -177,14 +177,14 @@ display: block; text-align: left; font-size: 0.95em; - color: $compl * 0.8; + color: darken($compl, 20%); margin-top: 0; width: auto; } .help_text { text-align: right; - color: $fond * 0.4; + color: darken($fond, 60%); } .input { diff --git a/avisstage/sass/screen.scss b/avisstage/sass/screen.scss index 9147b3a..8ee09e5 100644 --- a/avisstage/sass/screen.scss +++ b/avisstage/sass/screen.scss @@ -46,7 +46,7 @@ em, i { a { font-weight: bold; - color: $compl * 0.9; + color: darken($compl, 10%); text-decoration: none; } @@ -107,7 +107,7 @@ header { color: lighten($fond, 40%); &:hover { - background: $barre * 0.6; + background: darken($barre, 40%); } } } @@ -181,7 +181,6 @@ p.warning { li { display: table; width: 100%; - //border: 1px solid $fond * 1.3; background: #fff; margin: 12px; @@ -363,6 +362,57 @@ section.profil { } } +section.two-cols { + display: flex; + display: flexbox; + align-items: center; + & > * { + flex: 1; + width: 50%; + margin: 10px; + } +} + +ul.mes-emails { + li { + display: flex; + background: #fff; + margin: 5px; + padding: 10px; + min-height: 70px; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + + & > * { + flex: 1; + text-align: center; + } + + .adresse { + text-align: left; + font-weight: bold; + } + + .confirmee { + width: 20px; + } + + .supprimer { + flex: 0.7; + } + + form { + display: flex; + align-items: center; + justify-content: space-around; + .field { + flex: 1; + } + } + } +} + // // // Détail d'un stage @@ -432,7 +482,7 @@ input[type="submit"], .btn { font: $textfontsize $textfont; background-color: $fond; color: #fff; - border: 1px solid $fond * 0.7; + border: 1px solid darken($fond, 30%); border-radius: 5px; padding: 8px 12px; display: inline-block; diff --git a/avisstage/static/css/screen.css b/avisstage/static/css/screen.css index 0acc127..c1a8b02 100644 --- a/avisstage/static/css/screen.css +++ b/avisstage/static/css/screen.css @@ -1,5 +1,5 @@ @charset "UTF-8"; -/* line 5, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 5, ../../../../../../../../var/lib/gems/2.7.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, @@ -21,45 +21,45 @@ time, mark, audio, video { vertical-align: baseline; } -/* line 22, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 22, ../../../../../../../../var/lib/gems/2.7.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ html { line-height: 1; } -/* line 24, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 24, ../../../../../../../../var/lib/gems/2.7.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ ol, ul { list-style: none; } -/* line 26, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 26, ../../../../../../../../var/lib/gems/2.7.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ table { border-collapse: collapse; border-spacing: 0; } -/* line 28, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 28, ../../../../../../../../var/lib/gems/2.7.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ caption, th, td { text-align: left; font-weight: normal; vertical-align: middle; } -/* line 30, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 30, ../../../../../../../../var/lib/gems/2.7.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ q, blockquote { quotes: none; } -/* line 103, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 103, ../../../../../../../../var/lib/gems/2.7.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ q:before, q:after, blockquote:before, blockquote:after { content: ""; content: none; } -/* line 32, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 32, ../../../../../../../../var/lib/gems/2.7.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ a img { border: none; } -/* line 116, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 116, ../../../../../../../../var/lib/gems/2.7.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { display: block; } @@ -170,7 +170,7 @@ em, i { /* line 47, ../../sass/screen.scss */ a { font-weight: bold; - color: #e08c1d; + color: #e08206; text-decoration: none; } @@ -235,7 +235,7 @@ header nav ul li a { } /* line 109, ../../sass/screen.scss */ header nav ul li a:hover { - background: #446219; + background: black; } /* line 117, ../../sass/screen.scss */ header a { @@ -306,22 +306,22 @@ p.warning { background: #fff; margin: 12px; } -/* line 188, ../../sass/screen.scss */ +/* line 187, ../../sass/screen.scss */ .condensed-stages li > *, .condensed-stages li:before { display: table-cell; vertical-align: middle; padding: 15px; } -/* line 193, ../../sass/screen.scss */ +/* line 192, ../../sass/screen.scss */ .condensed-stages li a { width: auto; } -/* line 195, ../../sass/screen.scss */ +/* line 194, ../../sass/screen.scss */ .condensed-stages li a:hover { background: #e08206; color: #fff; } -/* line 200, ../../sass/screen.scss */ +/* line 199, ../../sass/screen.scss */ .condensed-stages li:before { content: ""; text-align: right; @@ -329,78 +329,78 @@ p.warning { opacity: 0.8; color: #000; } -/* line 207, ../../sass/screen.scss */ +/* line 206, ../../sass/screen.scss */ .condensed-stages li.stage-brouillon:before { content: "Brouillon"; background: #f93a93; } -/* line 211, ../../sass/screen.scss */ +/* line 210, ../../sass/screen.scss */ .condensed-stages li.stage-publie:before { content: "Publié"; background: #419be9; } -/* line 215, ../../sass/screen.scss */ +/* line 214, ../../sass/screen.scss */ .condensed-stages li.stage-ajout:before { content: "+"; color: #000; } -/* line 223, ../../sass/screen.scss */ +/* line 222, ../../sass/screen.scss */ .stage-liste li { display: block; position: relative; } -/* line 227, ../../sass/screen.scss */ +/* line 226, ../../sass/screen.scss */ .stage-liste li.date-maj { font-weight: 300; font-size: 0.9em; padding: 3px 0; font-style: italic; } -/* line 233, ../../sass/screen.scss */ +/* line 232, ../../sass/screen.scss */ .stage-liste li.stage { padding: 10px; background: #fff; margin: 10px; border-left: 5px solid #f99b20; } -/* line 239, ../../sass/screen.scss */ +/* line 238, ../../sass/screen.scss */ .stage-liste li.stage h3 { font-size: 1.4em; padding-left: 5px; } -/* line 243, ../../sass/screen.scss */ +/* line 242, ../../sass/screen.scss */ .stage-liste li.stage h3 > a { color: #0f4c82; } -/* line 247, ../../sass/screen.scss */ +/* line 246, ../../sass/screen.scss */ .stage-liste li.stage h3 .auteur { font-size: 0.8em; } -/* line 250, ../../sass/screen.scss */ +/* line 249, ../../sass/screen.scss */ .stage-liste li.stage h3 .auteur, .stage-liste li.stage h3 .auteur a { font-family: "Dosis", sans-serif; font-weight: normal; } -/* line 257, ../../sass/screen.scss */ +/* line 256, ../../sass/screen.scss */ .stage-liste li .misc-hdr { margin-bottom: 10px; } -/* line 261, ../../sass/screen.scss */ +/* line 260, ../../sass/screen.scss */ .stage-liste li .misc-hdr .dates > span { display: table-cell; vertical-align: middle; } -/* line 265, ../../sass/screen.scss */ +/* line 264, ../../sass/screen.scss */ .stage-liste li .misc-hdr .dates .year { padding-left: 8px; } -/* line 268, ../../sass/screen.scss */ +/* line 267, ../../sass/screen.scss */ .stage-liste li .misc-hdr .dates svg text { font-size: 0.8; } -/* line 276, ../../sass/screen.scss */ +/* line 275, ../../sass/screen.scss */ a.hoverlink { position: absolute; display: block; @@ -411,7 +411,7 @@ a.hoverlink { z-index: 2; } -/* line 286, ../../sass/screen.scss */ +/* line 285, ../../sass/screen.scss */ ul.infos { margin: 0 -3px; padding: 0; @@ -420,7 +420,7 @@ ul.infos { justify-content: space-between; width: 100; } -/* line 294, ../../sass/screen.scss */ +/* line 293, ../../sass/screen.scss */ ul.infos li { display: inline-block; padding: 5px; @@ -432,28 +432,28 @@ ul.infos li { text-align: center; background-color: #ddd; } -/* line 305, ../../sass/screen.scss */ +/* line 304, ../../sass/screen.scss */ ul.infos li.thematique { color: #0d3f6b; background-color: #86bff1; } -/* line 309, ../../sass/screen.scss */ +/* line 308, ../../sass/screen.scss */ ul.infos li.matiere { color: #395214; background-color: #c7e699; } -/* line 313, ../../sass/screen.scss */ +/* line 312, ../../sass/screen.scss */ ul.infos li.lieu { color: #7c043c; background-color: #fb84bc; } -/* line 317, ../../sass/screen.scss */ +/* line 316, ../../sass/screen.scss */ ul.infos li.year { background-color: #950548; color: #fff; display: none; } -/* line 322, ../../sass/screen.scss */ +/* line 321, ../../sass/screen.scss */ ul.infos li.avis-len { background-color: transparent; border: 1px solid #eee; @@ -461,29 +461,29 @@ ul.infos li.avis-len { padding: 4px; padding-bottom: 2px; } -/* line 329, ../../sass/screen.scss */ +/* line 328, ../../sass/screen.scss */ ul.infos li.avis-len.avis-vide { border-bottom-color: #ddd; } -/* line 332, ../../sass/screen.scss */ +/* line 331, ../../sass/screen.scss */ ul.infos li.avis-len.avis-court { border-bottom-color: #ffff66; } -/* line 335, ../../sass/screen.scss */ +/* line 334, ../../sass/screen.scss */ ul.infos li.avis-len.avis-moyen { border-bottom-color: #86bff1; } -/* line 338, ../../sass/screen.scss */ +/* line 337, ../../sass/screen.scss */ ul.infos li.avis-len.avis-long { border-bottom-color: #a5d65c; } -/* line 344, ../../sass/screen.scss */ +/* line 343, ../../sass/screen.scss */ ul.infos:after { content: ""; flex: 1000; } -/* line 350, ../../sass/screen.scss */ +/* line 349, ../../sass/screen.scss */ section.profil { background: #fff; max-width: 600px; @@ -491,7 +491,7 @@ section.profil { margin: 5px auto; margin-bottom: 15px; } -/* line 357, ../../sass/screen.scss */ +/* line 356, ../../sass/screen.scss */ section.profil div.infos { border-bottom: 3px solid #1a82dd; display: flex; @@ -500,6 +500,59 @@ section.profil div.infos { flex-wrap: wrap; } +/* line 365, ../../sass/screen.scss */ +section.two-cols { + display: flex; + display: flexbox; + align-items: center; +} +/* line 369, ../../sass/screen.scss */ +section.two-cols > * { + flex: 1; + width: 50%; + margin: 10px; +} + +/* line 377, ../../sass/screen.scss */ +ul.mes-emails li { + display: flex; + background: #fff; + margin: 5px; + padding: 10px; + min-height: 70px; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; +} +/* line 387, ../../sass/screen.scss */ +ul.mes-emails li > * { + flex: 1; + text-align: center; +} +/* line 392, ../../sass/screen.scss */ +ul.mes-emails li .adresse { + text-align: left; + font-weight: bold; +} +/* line 397, ../../sass/screen.scss */ +ul.mes-emails li .confirmee { + width: 20px; +} +/* line 401, ../../sass/screen.scss */ +ul.mes-emails li .supprimer { + flex: 0.7; +} +/* line 405, ../../sass/screen.scss */ +ul.mes-emails li form { + display: flex; + align-items: center; + justify-content: space-around; +} +/* line 409, ../../sass/screen.scss */ +ul.mes-emails li form .field { + flex: 1; +} + /* line 4, ../../sass/_stage_detail.scss */ article.stage .avis ul, article.stage .avis ol, div.tinymce ul, div.tinymce ol { list-style: unset; @@ -783,7 +836,7 @@ article.stage .section-wrapper .toc .toc-active a { border: 1px solid #ad0654; } -/* line 378, ../../sass/screen.scss */ +/* line 428, ../../sass/screen.scss */ input, textarea, select, div.tinymce, option, optgroup:before { background: #fff; font-size: 1em; @@ -793,13 +846,13 @@ input, textarea, select, div.tinymce, option, optgroup:before { padding: 5px; text-align: left; } -/* line 387, ../../sass/screen.scss */ +/* line 437, ../../sass/screen.scss */ input:focus, input.mce-edit-focus, textarea:focus, textarea.mce-edit-focus, select:focus, select.mce-edit-focus, div.tinymce:focus, div.tinymce.mce-edit-focus, option:focus, option.mce-edit-focus, optgroup:before:focus, optgroup:before.mce-edit-focus { background-color: #e9f5d6; outline: none; } -/* line 394, ../../sass/screen.scss */ +/* line 444, ../../sass/screen.scss */ input[type='text'], input[type='password'], input[type='email'], input[type='number'], textarea, select { border: none; @@ -809,7 +862,7 @@ input[type='email'], input[type='number'], textarea, select { transition: border 1s ease-out, background 1s ease-out; } -/* line 403, ../../sass/screen.scss */ +/* line 453, ../../sass/screen.scss */ select { -moz-appearance: none; appearance: none; @@ -823,42 +876,42 @@ select { background-color: #fff; background-size: contain; } -/* line 416, ../../sass/screen.scss */ +/* line 466, ../../sass/screen.scss */ select option { padding: 3px; white-space: pre-wrap; } -/* line 422, ../../sass/screen.scss */ +/* line 472, ../../sass/screen.scss */ select optgroup option { padding-left: 10px; } -/* line 425, ../../sass/screen.scss */ +/* line 475, ../../sass/screen.scss */ select optgroup:before { font-weight: bold; } -/* line 431, ../../sass/screen.scss */ +/* line 481, ../../sass/screen.scss */ input[type="submit"], .btn { font: 19px "Dosis", sans-serif; background-color: #8fcc33; color: #fff; - border: 1px solid #648f24; + border: 1px solid #395214; border-radius: 5px; padding: 8px 12px; display: inline-block; } -/* line 441, ../../sass/screen.scss */ +/* line 491, ../../sass/screen.scss */ p.submits { text-align: right; } -/* line 445, ../../sass/screen.scss */ +/* line 495, ../../sass/screen.scss */ form .commentaire { font-style: italic; } -/* line 449, ../../sass/screen.scss */ +/* line 499, ../../sass/screen.scss */ .edit-btn { border-color: #706c00; color: #000; @@ -867,14 +920,14 @@ form .commentaire { background-origin: content-box; background-size: contain; } -/* line 457, ../../sass/screen.scss */ +/* line 507, ../../sass/screen.scss */ .edit-btn:after { content: ""; width: 30px; display: inline-block; } -/* line 464, ../../sass/screen.scss */ +/* line 514, ../../sass/screen.scss */ textarea, div.tinymce { font-family: "Lato", sans-serif; border: none; @@ -884,20 +937,20 @@ textarea, div.tinymce { transition: border 1s ease-out, background 1s ease-out; } -/* line 473, ../../sass/screen.scss */ +/* line 523, ../../sass/screen.scss */ textarea { height: 200px; resize: vertical; } -/* line 481, ../../sass/screen.scss */ +/* line 531, ../../sass/screen.scss */ form .field { margin: 5px 0; display: flex; background: #fff; padding: 10px; } -/* line 487, ../../sass/screen.scss */ +/* line 537, ../../sass/screen.scss */ form .field label, form .field .label { display: inline-block; width: 250px; @@ -906,48 +959,48 @@ form .field label, form .field .label { padding-top: 5px; flex-shrink: 0; } -/* line 495, ../../sass/screen.scss */ +/* line 545, ../../sass/screen.scss */ form .field label.required:before, form .field .label.required:before { margin-right: 5px; content: "*"; color: #f70978; } -/* line 501, ../../sass/screen.scss */ +/* line 551, ../../sass/screen.scss */ form .field label { font-family: Alegreya, serif; font-weight: bold; } -/* line 505, ../../sass/screen.scss */ +/* line 555, ../../sass/screen.scss */ form .field .help_text { font-style: italic; font-size: 0.9em; } -/* line 509, ../../sass/screen.scss */ +/* line 559, ../../sass/screen.scss */ form .field .input { display: inline-block; flex-grow: 1; margin-right: 10px; } -/* line 519, ../../sass/screen.scss */ +/* line 569, ../../sass/screen.scss */ ul.as-selections, .selectize-control.multi { display: flex; flex-wrap: wrap; } -/* line 524, ../../sass/screen.scss */ +/* line 574, ../../sass/screen.scss */ ul.as-selections li, .selectize-control.multi li { display: inline-block; } -/* line 527, ../../sass/screen.scss */ +/* line 577, ../../sass/screen.scss */ ul.as-selections .selectize-input, ul.as-selections .selectize-dropdown, .selectize-control.multi .selectize-input, .selectize-control.multi .selectize-dropdown { font-size: 100%; line-height: 1.1; } -/* line 531, ../../sass/screen.scss */ +/* line 581, ../../sass/screen.scss */ ul.as-selections .as-selection-item, ul.as-selections .selectize-input > div, .selectize-control.multi .as-selection-item, @@ -959,7 +1012,7 @@ ul.as-selections .selectize-input > div, border-radius: 2px; font-weight: 500; } -/* line 540, ../../sass/screen.scss */ +/* line 590, ../../sass/screen.scss */ ul.as-selections .as-selection-item a.as-close, ul.as-selections .selectize-input > div a.as-close, .selectize-control.multi .as-selection-item a.as-close, @@ -969,51 +1022,51 @@ ul.as-selections .selectize-input > div a.as-close, cursor: pointer; margin-right: 5px; } -/* line 547, ../../sass/screen.scss */ +/* line 597, ../../sass/screen.scss */ ul.as-selections .as-selection-item.selected, ul.as-selections .selectize-input > div.selected, .selectize-control.multi .as-selection-item.selected, .selectize-control.multi .selectize-input > div.selected { background: #8fcc33; } -/* line 552, ../../sass/screen.scss */ +/* line 602, ../../sass/screen.scss */ ul.as-selections .as-original, .selectize-control.multi .as-original { flex-grow: 1; min-width: 200px; } -/* line 556, ../../sass/screen.scss */ +/* line 606, ../../sass/screen.scss */ ul.as-selections .as-original input, .selectize-control.multi .as-original input { width: 100%; } -/* line 562, ../../sass/screen.scss */ +/* line 612, ../../sass/screen.scss */ div.as-results { position: relative; z-index: 2; } -/* line 566, ../../sass/screen.scss */ +/* line 616, ../../sass/screen.scss */ div.as-results ul { position: absolute; width: 100%; background: #fff; border: 1px solid #d2ebad; } -/* line 573, ../../sass/screen.scss */ +/* line 623, ../../sass/screen.scss */ div.as-results ul li { padding: 3px 5px; } -/* line 579, ../../sass/screen.scss */ +/* line 629, ../../sass/screen.scss */ div.as-results ul li.as-result-item.active { background: #fddeb5; } -/* line 584, ../../sass/screen.scss */ +/* line 634, ../../sass/screen.scss */ div.as-results ul li.as-message { font-style: italic; } -/* line 594, ../../sass/screen.scss */ +/* line 644, ../../sass/screen.scss */ .window { display: none; position: fixed; @@ -1024,11 +1077,11 @@ div.as-results ul li.as-message { left: 0; z-index: 50; } -/* line 604, ../../sass/screen.scss */ +/* line 654, ../../sass/screen.scss */ .window.visible { display: block; } -/* line 608, ../../sass/screen.scss */ +/* line 658, ../../sass/screen.scss */ .window .window-bg { background: #000; opacity: 0.7; @@ -1039,7 +1092,7 @@ div.as-results ul li.as-message { top: 0; z-index: -1; } -/* line 619, ../../sass/screen.scss */ +/* line 669, ../../sass/screen.scss */ .window .window-content { position: relative; margin: 0 auto; @@ -1053,11 +1106,11 @@ div.as-results ul li.as-message { max-height: 100%; overflow: auto; } -/* line 633, ../../sass/screen.scss */ +/* line 683, ../../sass/screen.scss */ .window .window-content form label, .window .window-content form .label { width: 150px; } -/* line 639, ../../sass/screen.scss */ +/* line 689, ../../sass/screen.scss */ .window .window-closer { position: absolute; top: 0; @@ -1065,65 +1118,65 @@ div.as-results ul li.as-message { padding: 12px; z-index: 3; } -/* line 645, ../../sass/screen.scss */ +/* line 695, ../../sass/screen.scss */ .window .window-closer:after { content: "×"; } -/* line 656, ../../sass/screen.scss */ +/* line 706, ../../sass/screen.scss */ #lieu_widget .window-content { max-width: 800px; } -/* line 660, ../../sass/screen.scss */ +/* line 710, ../../sass/screen.scss */ #lieu_widget .lieu-ui { position: relative; width: 100%; min-width: 150px; flex: 2; } -/* line 666, ../../sass/screen.scss */ +/* line 716, ../../sass/screen.scss */ #lieu_widget .lieu-ui .map { height: 400px; width: 100%; } -/* line 670, ../../sass/screen.scss */ +/* line 720, ../../sass/screen.scss */ #lieu_widget .lieu-ui.hidden { display: none; } -/* line 673, ../../sass/screen.scss */ +/* line 723, ../../sass/screen.scss */ #lieu_widget .lieu-ui .masked { visibility: hidden; } -/* line 678, ../../sass/screen.scss */ +/* line 728, ../../sass/screen.scss */ #lieu_widget .lieu-choixmodif, #lieu_widget .lieu-options { display: none; } -/* line 682, ../../sass/screen.scss */ +/* line 732, ../../sass/screen.scss */ #lieu_widget .lieu-global { display: flex; width: 100%; flex-wrap: wrap; } -/* line 687, ../../sass/screen.scss */ +/* line 737, ../../sass/screen.scss */ #lieu_widget .lieu-global.with-options .lieu-options { display: block; } -/* line 694, ../../sass/screen.scss */ +/* line 744, ../../sass/screen.scss */ #lieu_widget.modif .lieu-global.with-options .lieu-options, #lieu_widget.edit .lieu-global.with-options .lieu-options { display: none; } -/* line 699, ../../sass/screen.scss */ +/* line 749, ../../sass/screen.scss */ #lieu_widget .lieu-options { padding: 7px; max-width: 350px; flex: 3; } -/* line 704, ../../sass/screen.scss */ +/* line 754, ../../sass/screen.scss */ #lieu_widget .lieu-options .lieu-suggestions { max-height: 300px; overflow-y: auto; } -/* line 708, ../../sass/screen.scss */ +/* line 758, ../../sass/screen.scss */ #lieu_widget .lieu-options .lieu-suggestions li { position: relative; background: #fff; @@ -1131,64 +1184,64 @@ div.as-results ul li.as-message { padding: 4px; font-size: 0.9em; } -/* line 714, ../../sass/screen.scss */ +/* line 764, ../../sass/screen.scss */ #lieu_widget .lieu-options .lieu-suggestions li:hover { background: #ccc; } -/* line 717, ../../sass/screen.scss */ +/* line 767, ../../sass/screen.scss */ #lieu_widget .lieu-options .lieu-suggestions li p { margin: 2px 0; } -/* line 720, ../../sass/screen.scss */ +/* line 770, ../../sass/screen.scss */ #lieu_widget .lieu-options .lieu-suggestions li .lieu-nom { font-weight: bold; } -/* line 723, ../../sass/screen.scss */ +/* line 773, ../../sass/screen.scss */ #lieu_widget .lieu-options .lieu-suggestions li .lieu-infos { font-size: 0.8em; display: flex; width: 100%; justify-content: space-between; } -/* line 729, ../../sass/screen.scss */ +/* line 779, ../../sass/screen.scss */ #lieu_widget .lieu-options .lieu-suggestions li .lieu-infos span { display: inline-block; text-overflow: ellipsis; overlow: hidden; } -/* line 740, ../../sass/screen.scss */ +/* line 790, ../../sass/screen.scss */ #lieu_widget.modif .lieu-choixmodif { display: unset; } -/* line 745, ../../sass/screen.scss */ +/* line 795, ../../sass/screen.scss */ #lieu_widget.modif .lieu-ui, #lieu_widget.attente .lieu-ui { display: none; } -/* line 752, ../../sass/screen.scss */ +/* line 802, ../../sass/screen.scss */ #lieu_widget.edit .lieu-ui .lieu-acinput { display: none; } -/* line 755, ../../sass/screen.scss */ +/* line 805, ../../sass/screen.scss */ #lieu_widget.edit .lieu-ui .map { height: 200px; } -/* line 761, ../../sass/screen.scss */ +/* line 811, ../../sass/screen.scss */ #lieu_widget #avis_lieu_vide { display: none; } -/* line 765, ../../sass/screen.scss */ +/* line 815, ../../sass/screen.scss */ #lieu_widget .message { background: #fddeb5; padding: 5px; font-style: italic; font-size: 0.9em; } -/* line 771, ../../sass/screen.scss */ +/* line 821, ../../sass/screen.scss */ #lieu_widget .message.hidden { display: none; } -/* line 777, ../../sass/screen.scss */ +/* line 827, ../../sass/screen.scss */ a.lieu-change { color: #fff; background: #f99b20; @@ -1201,25 +1254,25 @@ a.lieu-change { border-radius: 5px; margin-right: 7px; } -/* line 789, ../../sass/screen.scss */ +/* line 839, ../../sass/screen.scss */ a.lieu-change.ajout:before { content: "+"; margin-right: 5px; } -/* line 795, ../../sass/screen.scss */ +/* line 845, ../../sass/screen.scss */ #stages-map { width: 100%; height: 600px; max-height: 90vh; } -/* line 802, ../../sass/screen.scss */ +/* line 852, ../../sass/screen.scss */ #id_stage-thematiques { display: none; } -/* line 808, ../../sass/screen.scss */ +/* line 858, ../../sass/screen.scss */ .homeh1 { display: flex; justify-content: space-between; @@ -1229,26 +1282,26 @@ a.lieu-change.ajout:before { border-bottom: 3px solid #000; margin-bottom: 15px; } -/* line 817, ../../sass/screen.scss */ +/* line 867, ../../sass/screen.scss */ .homeh1 h1 { margin-bottom: 3px; } -/* line 821, ../../sass/screen.scss */ +/* line 871, ../../sass/screen.scss */ .homeh1 > * { display: inline-block; } -/* line 824, ../../sass/screen.scss */ +/* line 874, ../../sass/screen.scss */ .homeh1 p { text-align: right; } -/* line 829, ../../sass/screen.scss */ +/* line 879, ../../sass/screen.scss */ .betacadre { background: #fa6cae; padding: 10px; } -/* line 834, ../../sass/screen.scss */ +/* line 884, ../../sass/screen.scss */ .entrer { background: #fff; max-width: 500px; @@ -1256,7 +1309,7 @@ a.lieu-change.ajout:before { text-align: center; margin: 15px auto; } -/* line 841, ../../sass/screen.scss */ +/* line 891, ../../sass/screen.scss */ .entrer .archicubes { border-top: 2px solid #1a82dd; margin-top: 5px; @@ -1264,84 +1317,84 @@ a.lieu-change.ajout:before { font-size: 0.9em; } -/* line 849, ../../sass/screen.scss */ +/* line 899, ../../sass/screen.scss */ article.promo { display: block; font-size: 1.1em; } -/* line 853, ../../sass/screen.scss */ +/* line 903, ../../sass/screen.scss */ article.promo .explications { display: table; } -/* line 856, ../../sass/screen.scss */ +/* line 906, ../../sass/screen.scss */ article.promo .explications:first-child { direction: rtl; } -/* line 858, ../../sass/screen.scss */ +/* line 908, ../../sass/screen.scss */ article.promo .explications:first-child > * { direction: ltr; } -/* line 863, ../../sass/screen.scss */ +/* line 913, ../../sass/screen.scss */ article.promo .explications > div { display: table-cell; vertical-align: middle; text-align: center; } -/* line 868, ../../sass/screen.scss */ +/* line 918, ../../sass/screen.scss */ article.promo .explications > div p { margin: 15px 15px; } -/* line 876, ../../sass/screen.scss */ +/* line 926, ../../sass/screen.scss */ .faq-toc { font-family: "Lato", sans-serif; display: block; max-width: 700px; margin: 0 auto; } -/* line 881, ../../sass/screen.scss */ +/* line 931, ../../sass/screen.scss */ .faq-toc ul { margin: 20px; } -/* line 885, ../../sass/screen.scss */ +/* line 935, ../../sass/screen.scss */ .faq-toc ul li a { color: #000; display: block; padding: 5px; } -/* line 891, ../../sass/screen.scss */ +/* line 941, ../../sass/screen.scss */ .faq-toc ul li.toc-h1 { display: none; } -/* line 895, ../../sass/screen.scss */ +/* line 945, ../../sass/screen.scss */ .faq-toc ul li.toc-h2 a { background: #fcc883; } -/* line 899, ../../sass/screen.scss */ +/* line 949, ../../sass/screen.scss */ .faq-toc ul li.toc-h3 a { padding-left: 10px; background: #fff; font-weight: normal; } -/* line 905, ../../sass/screen.scss */ +/* line 955, ../../sass/screen.scss */ .faq-toc ul li a:hover { color: #395214; background: #bce085 !important; } -/* line 914, ../../sass/screen.scss */ +/* line 964, ../../sass/screen.scss */ .faq article { background: #fff; padding: 15px; } -/* line 917, ../../sass/screen.scss */ +/* line 967, ../../sass/screen.scss */ .faq article h2 { background-color: #fcc883; color: #ae6505; margin: -15px; padding: 15px; } -/* line 924, ../../sass/screen.scss */ +/* line 974, ../../sass/screen.scss */ .faq article h3 { color: #0f4c82; background-color: #9dcbf3; @@ -1349,19 +1402,19 @@ article.promo .explications > div p { margin-top: 30px; padding: 10px 15px; } -/* line 931, ../../sass/screen.scss */ +/* line 981, ../../sass/screen.scss */ .faq article h3:nth-child(2) { margin-top: 0; } -/* line 936, ../../sass/screen.scss */ +/* line 986, ../../sass/screen.scss */ .faq article ul { padding-left: 20px; } -/* line 938, ../../sass/screen.scss */ +/* line 988, ../../sass/screen.scss */ .faq article ul li { list-style: initial; } -/* line 943, ../../sass/screen.scss */ +/* line 993, ../../sass/screen.scss */ .faq article p, .faq article ul { font-family: "Lato", sans-serif; font-size: 18px; @@ -1370,20 +1423,20 @@ article.promo .explications > div p { margin-right: 5%; } -/* line 957, ../../sass/screen.scss */ +/* line 1007, ../../sass/screen.scss */ table.stats { width: 100%; background: #fff; margin: 20px 0; cellspacing: 1px; } -/* line 962, ../../sass/screen.scss */ +/* line 1012, ../../sass/screen.scss */ table.stats th { font-weight: bold; border-top: 1px solid #000; border-bottom: 1px solid #999; } -/* line 967, ../../sass/screen.scss */ +/* line 1017, ../../sass/screen.scss */ table.stats td, table.stats th { padding: 5px 3px; text-align: center; @@ -1971,14 +2024,14 @@ body.recherche.vue-hybride.vue-details .recherche-liste.recherche-details { display: block; text-align: left; font-size: 0.95em; - color: #c77c1a; + color: #ae6505; margin-top: 0; width: auto; } /* line 185, ../../sass/_responsive.scss */ form .field .help_text { text-align: right; - color: #395214; + color: black; } /* line 190, ../../sass/_responsive.scss */ form .field .input { diff --git a/avisstage/static/js/select_lieu.js b/avisstage/static/js/select_lieu.js index ecb4f56..6e0c097 100644 --- a/avisstage/static/js/select_lieu.js +++ b/avisstage/static/js/select_lieu.js @@ -63,10 +63,10 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, MAPBOX_API_KEY, target, callbac // Affiche la carte map = L.map(map_el[0]).setView([48.8422411,2.3430553], 15); - var layer = L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', { + var layer = L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', { attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox', maxZoom: 18, - id: 'mapbox.streets', + id: 'mapbox/streets-v11', accessToken: MAPBOX_API_KEY }); map.addLayer(layer); diff --git a/avisstage/templates/avisstage/base.html b/avisstage/templates/avisstage/base.html index 33eda80..1bd1ced 100644 --- a/avisstage/templates/avisstage/base.html +++ b/avisstage/templates/avisstage/base.html @@ -37,9 +37,9 @@
  • Modo
  • {% endif %} {% if user.is_authenticated %} -
  • {{ user.username }}
    Déconnexion
  • +
  • {{ user.username }}
    Déconnexion
  • {% else %} -
  • Connexion
  • +
  • Connexion
  • {% endif %} diff --git a/avisstage/templates/avisstage/compte/aconfirmer.html b/avisstage/templates/avisstage/compte/aconfirmer.html new file mode 100644 index 0000000..bbe6398 --- /dev/null +++ b/avisstage/templates/avisstage/compte/aconfirmer.html @@ -0,0 +1,19 @@ +{% extends "avisstage/base.html" %} +{% load staticfiles %} + +{% block title %}Confirmation requise - ExperiENS{% endblock %} + +{% block content %} +

    Confirmation requise

    + +
    + {% if object.confirmed_at %} +

    L'adresse {{ object.email }} a déjà été confirmée.

    + {% else %} +

    Un mail de confirmation vous a été envoyé à l'adresse {{ object.email }} pour la vérifier.

    +

    Merci de cliquer sur le lien inclus pour confirmer qu'elle est correcte.

    +

    Si vous ne recevez rien, vérifier dans vos indésirables.

    + {% endif %} +

    Retour

    +
    +{% endblock %} diff --git a/avisstage/templates/avisstage/compte/edit_mdp.html b/avisstage/templates/avisstage/compte/edit_mdp.html new file mode 100644 index 0000000..ff07aae --- /dev/null +++ b/avisstage/templates/avisstage/compte/edit_mdp.html @@ -0,0 +1,29 @@ +{% extends "avisstage/base.html" %} +{% load staticfiles %} + +{% block content %} +

    Définir un mot de passe

    +
    + {% csrf_token %} + {{ form.non_field_errors }} +
    + +
    + {{ user.username }} +
    +
    + {% for field in form %} + {{ field.errors }} +
    + +
    + {{ field }} + {% if field.help_text %} +

    {{ field.help_text }}

    + {% endif %} +
    +
    + {% endfor %} + +
    +{% endblock %} diff --git a/avisstage/templates/avisstage/compte/email_supprime.html b/avisstage/templates/avisstage/compte/email_supprime.html new file mode 100644 index 0000000..494d27a --- /dev/null +++ b/avisstage/templates/avisstage/compte/email_supprime.html @@ -0,0 +1,18 @@ +{% extends "avisstage/base.html" %} +{% load staticfiles %} + +{% block title %}Supprimer une adresse mail - ExperiENS{% endblock %} + +{% block content %} +

    Supprimer une adresse mail

    + +
    +
    +
    + {% csrf_token %} +

    Êtes-vous sûr⋅e de vouloir supprimer l'adresse mail {{ object.email }} ?

    +

    Retour  

    +
    +
    +
    +{% endblock %} diff --git a/avisstage/templates/avisstage/compte/parametres.html b/avisstage/templates/avisstage/compte/parametres.html new file mode 100644 index 0000000..36a9547 --- /dev/null +++ b/avisstage/templates/avisstage/compte/parametres.html @@ -0,0 +1,76 @@ +{% extends "avisstage/base.html" %} +{% load staticfiles %} + +{% block title %}Mes paramètres - ExperiENS{% endblock %} + +{% block content %} +

    Mes paramètres

    + +
    +

    Adresses e-mail

    +
      + {% for email in request.user.email_address_set.all %} +
    • + {{ email.email }} + {{ email.confirmed_at|yesno:"✓,✗"|safe }} + {% if email.confirmed_at %} + + {% if email.email == user.email %} + Principale + {% else %} +
      + {% csrf_token %} + +
      + {% endif %} +
      + + {% else %} + +
      + {% csrf_token %} + +
      +
      + {% endif %} + + {% if not email.email == user.email %} + Supprimer + {% endif %} + +
    • + {% endfor %} +
    • +
      + {% csrf_token %} + {{ form.non_field_errors }} + {% for field in form %} + {{ field.errors }} +
      +
      + {{ field }} +
      +
      + {% endfor %} + +
      +
    • +
    +
    + +
    +

    Mot de passe

    +
    + {% if request.user.password and request.user.has_usable_password %} +

    Un mot de passe interne est déjà défini pour ce compte.

    + {% else %} +

    Aucun mot de passe n'est défini pour ce compte. Créez-en un pour pouvoir vous connecter après la fin de votre scolarité à l'ENS.

    + {% endif %} +
    + {% csrf_token %} + +
    +

    En cliquant sur ce bouton, un lien unique vous sera envoyé à votre adresse e-mail principale ({{ request.user.email }}) qui vous donnera accès au formulaire d'édition du mot de passe.

    +
    +
    +{% endblock %} diff --git a/avisstage/templates/avisstage/detail/profil.html b/avisstage/templates/avisstage/detail/profil.html index 68281d9..75ca174 100644 --- a/avisstage/templates/avisstage/detail/profil.html +++ b/avisstage/templates/avisstage/detail/profil.html @@ -5,10 +5,10 @@ {% endblock %} -{% block title %}Profil de {{ object.nom }} - ExperiENS{% endblock %} +{% block title %}Profil de {{ object.nom_complet }} - ExperiENS{% endblock %} {% block content %} -

    Profil de {{ object.nom }} +

    Profil de {{ object.nom_complet }} {% if object.user == user %} Modifier mes infos {% endif %} diff --git a/avisstage/templates/avisstage/detail/stage.html b/avisstage/templates/avisstage/detail/stage.html index 9ec7b37..cb0d587 100644 --- a/avisstage/templates/avisstage/detail/stage.html +++ b/avisstage/templates/avisstage/detail/stage.html @@ -72,7 +72,7 @@

    {{ object.sujet }}

    {{ object.date_debut|date:"Y" }}{{ object.date_debut|date:"d/m" }}{{ object.date_fin|date:"d/m" }}

    -

    {{ object.auteur.nom }} +

    {{ object.auteur.nom_complet }} a fait {{ object.type_stage_fem|yesno:"cette,ce" }} {{ object.type_stage_fancy }} {% if object.niveau_scol %}{{ object.niveau_scol_fancy }},{% endif %} {% if object.structure %}au sein de {{ object.structure }}{% endif %}{% if object.encadrants %}, supervisé par {{ object.encadrants }}{% endif %}.

    diff --git a/avisstage/templates/avisstage/formulaires/profil.html b/avisstage/templates/avisstage/formulaires/profil.html index e5ac89d..7d1fce0 100644 --- a/avisstage/templates/avisstage/formulaires/profil.html +++ b/avisstage/templates/avisstage/formulaires/profil.html @@ -18,6 +18,13 @@ {% endfor %} - +
    + +
    + {{ request.user.email }} +

    Allez dans les paramètres de connexion pour modifier votre adresse principale

    +
    +
    + {% endblock %} diff --git a/avisstage/templates/avisstage/index.html b/avisstage/templates/avisstage/index.html index a7d664b..f9c20da 100644 --- a/avisstage/templates/avisstage/index.html +++ b/avisstage/templates/avisstage/index.html @@ -11,9 +11,9 @@ {% if not user.is_authenticated %}
    -

    Connexion

    +

    Connexion

    Connexion via le serveur central d'authentification ENS
    (identifiants clipper)

    -

    Accès archicubes
    Pour continuer à tenir à jour ses fiches, sans voir celles des autres

    +

    Accès archicubes
    Pour continuer à tenir à jour ses fiches, sans voir celles des autres

    {% endif %} diff --git a/avisstage/templates/avisstage/mails/reinit_mdp.html b/avisstage/templates/avisstage/mails/reinit_mdp.html new file mode 100644 index 0000000..f9e3c98 --- /dev/null +++ b/avisstage/templates/avisstage/mails/reinit_mdp.html @@ -0,0 +1,8 @@ +Bonjour, + +Pour créer ou modifier le mot de passe associé à votre compte {{ user.get_username }}, merci de cliquer sur le lien suivant ou de le copier dans votre navigateur : + + {{ protocol }}://{{ domain }}{% url 'avisstage:mdp_edit' uidb64=uid token=token %} + +Cordialement, +L'équipe ExperiENS diff --git a/avisstage/templates/avisstage/mails/reinit_mdp.txt b/avisstage/templates/avisstage/mails/reinit_mdp.txt new file mode 100644 index 0000000..cc5a9de --- /dev/null +++ b/avisstage/templates/avisstage/mails/reinit_mdp.txt @@ -0,0 +1 @@ +[ExperiENS] Définition du mot de passe diff --git a/avisstage/templates/avisstage/perso.html b/avisstage/templates/avisstage/perso.html index a21d66c..b6f15e4 100644 --- a/avisstage/templates/avisstage/perso.html +++ b/avisstage/templates/avisstage/perso.html @@ -4,38 +4,42 @@ {% block title %}Espace personnel - ExperiENS{% endblock %} {% block content %} -

    Bonjour {{ user.profil.nom }} !

    +

    Bonjour {{ user.profil.nom_complet }} !

    Mon compte

    -
    - {% if user.profil.en_scolarite %} -

    Statut : En scolarité

    -

    Vous pouvez accéder à l'ensemble du site, et aux fiches de stages.

    -

    Quand vous n'aurez plus de compte clipper (après votre scolarité), votre accès sera restreint à vos propres expériences, que vous pourrez ajouter, modifier, supprimer.

    -

    Pensez à renseigner une adresse e-mail non-ENS pour conserver cet accès, et permettre aux futur⋅e⋅s normalien⋅ne⋅s de toujours vous contacter !

    - {% else %} -

    Statut : Archicube

    -

    Vous ne pouvez plus accéder qu'à vos propres expériences pour les modifier, et tenir à jour votre profil.

    -

    Si vous êtes encore en scolarité, merci de vous reconnecter en passant par le serveur d'authentification de l'ENS pour mettre à jour votre statut.

    - {% endif %} -

    Le statut est mis à jour automatiquement chaque année selon le mode de connexion que vous utilisez.

    -
    -
    -

    Adresses e-mail

    - {% if not user.profil.has_nonENS_email %}

    Vous n'avez pas renseigné d'adresse mail autre que celle de l'ENS. Pensez à le faire, pour que les générations futures puissent toujours vous contacter !

    {% endif %} -

    Gérer les adresses e-mail liées à mon compte

    -
    -
    -

    Mode de connexion

    - {% if user.profil.en_scolarite %}

    En scolarité, utilisez le serveur central d'authentification pour vous connecter. Quand vous n'aurez plus de compte clipper, vous devrez vous connecter directement via l'accès archicubes, avec l'identifiant {{ user.username }}

    {% endif %} - {% if not user.password %}

    Vous n'avez pas créé de mot de passe interne à ExperiENS. Pensez-y pour garder l'accès au site quand vous n'aurez plus de compte clipper !

    {% endif %} -

    Créer / changer mon mot de passe ExperiENS

    +
    +
    + {% if user.profil.en_scolarite %} +

    Statut : En scolarité

    +

    Vous pouvez accéder à l'ensemble du site, et aux fiches de stages.

    +

    Quand vous n'aurez plus de compte clipper (après votre scolarité), votre accès sera restreint à vos propres expériences, que vous pourrez ajouter, modifier, supprimer.

    +

    Pensez à renseigner une adresse e-mail non-ENS pour conserver cet accès, et permettre aux futur⋅e⋅s normalien⋅ne⋅s de toujours vous contacter !

    + {% else %} +

    Statut : Archicube

    +

    Vous ne pouvez plus accéder qu'à vos propres expériences pour les modifier, et tenir à jour votre profil.

    +

    Si vous êtes encore en scolarité, merci de vous reconnecter en passant par le serveur d'authentification de l'ENS pour mettre à jour votre statut.

    + {% endif %} +
    +

    Le statut est mis à jour automatiquement tous les deux mois selon le mode de connexion que vous utilisez.

    +
    +
    +

    Connexion

    +

    Adresse e-mail principale :
    {{ user.email }}

    + {% if not user.profil.has_nonENS_email %}

    Vous n'avez pas renseigné d'adresse mail autre que celle de l'ENS. Pensez à le faire, pour que les générations futures puissent toujours vous contacter !

    {% endif %} +
    + +

    Mot de passe interne : {% if user.password and user.has_usable_password %}Défini{% else %}Non défini{% endif %}

    + {% if not user.password or not user.has_usable_password %}

    Pensez à définir un mot de passe propre à ExperiENS pour garder l'accès au site quand vous n'aurez plus de compte clipper !

    {% endif %} + {% if user.profil.en_scolarite %}

    En scolarité, utilisez le serveur central d'authentification pour vous connecter. Quand vous n'aurez plus de compte clipper, vous devrez vous connecter directement via l'accès archicubes, avec votre login {{ user.username }} et le mot de passe spécifique à ExperiENS que vous aurez défini.

    {% endif %} +
    +

    Gérer mes paramètres de connexion

    +
    -

    Mon profil public Modifier mes infos

    +

    Mon profil public Voir Modifier mes infos

    {% with object=user.profil %}
    diff --git a/avisstage/tests.py b/avisstage/tests.py index ed8721a..0ea4bc7 100644 --- a/avisstage/tests.py +++ b/avisstage/tests.py @@ -1,32 +1,36 @@ -from allauth.socialaccount.models import SocialAccount -from allauth_cas.test.testcases import CASTestCase -from allauth_ens.adapter import deprecate_clippers +from authens.tests.cas_utils import FakeCASClient +from authens.models import CASAccount, OldCASAccount -from datetime import date +from datetime import date, timedelta from django.test import TestCase from django.urls import reverse from django.conf import settings +from django.utils import timezone + +from unittest import mock from .models import User, Normalien, Lieu, Stage, StageMatiere, AvisLieu -class ExperiENSTestCase(CASTestCase): +class ExperiENSTestCase(TestCase): # Dummy database - + def setUp(self): self.u_conscrit = User.objects.create_user('conscrit', 'conscrit@ens.fr', 'conscrit') self.p_conscrit = self.u_conscrit.profil - self.p_conscrit.nom="Petit conscrit" - self.p_conscrit.promotion="Serpentard 2000" - self.p_conscrit.bio="Je suis un petit conscrit" + self.p_conscrit.nom = "Petit conscrit" + self.p_conscrit.promotion = "Serpentard 2020" + self.p_conscrit.bio = "Je suis un petit conscrit" self.p_conscrit.save() - self.sa_conscrit = SocialAccount(user=self.u_conscrit, - provider="clipper", - uid="conscrit") + self.sa_conscrit = CASAccount( + user=self.u_conscrit, + cas_login="conscrit", + entrance_year=2020, + ) self.sa_conscrit.save() self.u_archi = User.objects.create_user('archicube', @@ -34,7 +38,7 @@ class ExperiENSTestCase(CASTestCase): 'archicube') self.p_archi = self.u_archi.profil self.p_archi.nom="Vieil archicube" - self.p_archi.promotion="Gryffondor 1994" + self.p_archi.promotion="Gryffondor 2014" self.p_archi.bio="Je suis un vieil archicube" self.lieu1 = Lieu(nom="Beaux-Bâtons", type_lieu="universite", @@ -52,8 +56,8 @@ class ExperiENSTestCase(CASTestCase): self.matiere2.save() self.cstage1 = Stage(auteur=self.p_conscrit, sujet="Wingardium Leviosa", - date_debut=date(2000, 5, 10), - date_fin=date(2000, 8, 26), + date_debut=date(2020, 5, 10), + date_fin=date(2020, 8, 26), type_stage="recherche", niveau_scol="M1", public=True) self.cstage1.save() @@ -63,8 +67,8 @@ class ExperiENSTestCase(CASTestCase): alieu1.save() self.cstage2 = Stage(auteur=self.p_conscrit, sujet="Avada Kedavra", - date_debut=date(2001, 5, 10), - date_fin=date(2001, 8, 26), + date_debut=date(2021, 5, 10), + date_fin=date(2021, 8, 26), type_stage="sejour_dri", niveau_scol="M2", public=False) self.cstage2.save() @@ -75,8 +79,8 @@ class ExperiENSTestCase(CASTestCase): self.astage1 = Stage(auteur=self.p_archi, sujet="Alohomora", - date_debut=date(1994, 5, 10), - date_fin=date(1994, 8, 26), + date_debut=date(2014, 5, 10), + date_fin=date(2014, 8, 26), type_stage="recherche", niveau_scol="M2", public=True) self.astage1.save() @@ -100,7 +104,7 @@ class ExperiENSTestCase(CASTestCase): """ -ACCÈS PUBLICS +ACCÈS PUBLIC """ class PublicViewsTest(ExperiENSTestCase): """ @@ -196,6 +200,7 @@ ACCÈS ARCHICUBE class ArchicubeViewsTest(ExperiENSTestCase): def setUp(self): super().setUp() + # Connexion with password self.client.login(username='archicube', password='archicube') def assert403Archicubes(self, testurl): @@ -309,16 +314,29 @@ class ArchicubeViewsTest(ExperiENSTestCase): class DeprecatedArchicubeViewsTest(ArchicubeViewsTest): - def setUp(self): + @mock.patch("authens.backends.get_cas_client") + def setUp(self, mock_cas_client): super().setUp() - - self.sa_archi = SocialAccount(user=self.u_archi, - provider="clipper", - uid="archicube") + + fake_cas_client = FakeCASClient(cas_login="archicube", entrance_year=2012) + mock_cas_client.return_value = fake_cas_client + + self.sa_archi = OldCASAccount( + user=self.u_archi, + cas_login="archicube", + entrance_year=2012, + ) self.sa_archi.save() - deprecate_clippers() - + # First connexion through CAS + self.client.login(ticket="dummy") + self.client.logout() + + # Time flies + self.p_archi.last_cas_login = (timezone.now() - timedelta(days=365)).date() + self.p_archi.save() + + # New connexion with password self.client.login(username='archicube', password='archicube') @@ -328,26 +346,34 @@ class DeprecatedArchicubeViewsTest(ArchicubeViewsTest): ACCÈS EN SCOLARITE """ class ScolariteViewsTest(ExperiENSTestCase): - def setUp(self): + @mock.patch("authens.backends.get_cas_client") + def setUp(self, mock_cas_client): super().setUp() + + fake_cas_client = FakeCASClient(cas_login="vieuxcon", entrance_year=2017) + mock_cas_client.return_value = fake_cas_client - self.u_vieuxcon = User.objects.create_user('vieuxcon', - 'vieuxcon@ens.fr', - 'vieuxcon') + self.u_vieuxcon = User.objects.create_user( + 'vieuxcon', + 'vieuxcon@ens.fr', + 'vieuxcon' + ) self.p_vieuxcon = self.u_vieuxcon.profil self.p_vieuxcon.nom="Vieux con" - self.p_vieuxcon.promotion="Poufsouffle 1997" + self.p_vieuxcon.promotion="Poufsouffle 2017" self.p_vieuxcon.bio="Je suis un vieux con encore en scolarité" self.p_vieuxcon.save() - self.sa_vieuxcon = SocialAccount(user=self.u_vieuxcon, - provider="clipper", - uid="vieuxcon") + self.sa_vieuxcon = CASAccount( + user=self.u_vieuxcon, + cas_login="vieuxcon", + entrance_year=2017, + ) self.sa_vieuxcon.save() self.vstage1 = Stage(auteur=self.p_vieuxcon, sujet="Oubliettes", - date_debut=date(1998, 5, 10), - date_fin=date(1998, 8, 26), + date_debut=date(2018, 5, 10), + date_fin=date(2018, 8, 26), type_stage="recherche", niveau_scol="M1", public=False) self.vstage1.save() @@ -355,8 +381,9 @@ class ScolariteViewsTest(ExperiENSTestCase): alieu1 = AvisLieu(stage=self.vstage1, lieu=self.lieu2, chapo="Pas si mal") alieu1.save() - - self.client.login(username='vieuxcon', password='vieuxcon') + + # Connexion through CAS + self.client.login(ticket="dummy") """ Vérifie que les seules fiches de stages visibles sont les siennes ou celles @@ -400,7 +427,7 @@ class ScolariteViewsTest(ExperiENSTestCase): """ - Vérifie que la recherche et les autres pages sont accessible + Vérifie que la recherche et les autres pages sont accessibles """ def test_pages_visibility_scolarite(self): testurl = reverse('avisstage:recherche') diff --git a/avisstage/urls.py b/avisstage/urls.py index 3c39700..93f0978 100644 --- a/avisstage/urls.py +++ b/avisstage/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import include, url +from django.urls import include, path from . import views, api from tastypie.api import Api @@ -7,27 +7,45 @@ v1_api.register(api.LieuResource()) v1_api.register(api.StageResource()) v1_api.register(api.AuteurResource()) +app_name = "avisstage" urlpatterns = [ - url(r'^$', views.index, name='index'), - url(r'^perso/$', views.perso, name='perso'), - url(r'^faq/$', views.faq, name='faq'), - url(r'^stage/nouveau/$', views.manage_stage, name='stage_ajout'), - url(r'^stage/(?P\w+)/$', views.StageView.as_view(), name='stage'), - url(r'^stage/(?P\w+)/edit/$', views.manage_stage, name='stage_edit'), - url(r'^stage/(?P\w+)/publication/$', views.publier_stage, + path('', views.index, name='index'), + path('perso/', views.perso, name='perso'), + path('faq/', views.faq, name='faq'), + path('stage/nouveau/', views.manage_stage, name='stage_ajout'), + path('stage//', views.StageView.as_view(), name='stage'), + path('stage//edit/', views.manage_stage, name='stage_edit'), + path('stage//publication/', views.publier_stage, name='stage_publication'), - url(r'^403/archicubes/$', views.archicubes_interdits, + path('403/archicubes/', views.archicubes_interdits, name='403-archicubes'), - url(r'^lieu/save/$', views.save_lieu, name='lieu_ajout'), - url(r'^profil/show/(?P[\w@]+)/$', views.ProfilView.as_view(), + path('lieu/save/', views.save_lieu, name='lieu_ajout'), + path('profil/show//', views.ProfilView.as_view(), name='profil'), - url(r'^profil/edit/$', views.ProfilEdit.as_view(), name='profil_edit'), - url(r'^recherche/$', views.recherche, name='recherche'), - url(r'^recherche/resultats/$', views.recherche_resultats, + path('profil/edit/', views.ProfilEdit.as_view(), name='profil_edit'), + path('profil/parametres/', views.MesParametres.as_view(), name='parametres'), + path('profil/emails//aconfirmer/', + views.AdresseAConfirmer.as_view(), name="emails_aconfirmer"), + path('profil/emails//supprime/', views.SupprimeAdresse.as_view(), + name="emails_supprime"), + path('profil/emails//reconfirme/', + views.ReConfirmeAdresse.as_view(), + name="emails_reconfirme"), + path('profil/emails//principal/', + views.RendAdressePrincipale.as_view(), name="emails_principal"), + path('profil/emails/confirme//', views.ConfirmeAdresse.as_view(), + name="emails_confirme"), + path('profil/mdp/demande/', + views.EnvoieLienMotDePasse.as_view(), name="mdp_demande"), + path('profil/mdp///', + views.DefinirMotDePasse.as_view(), name="mdp_edit"), + + path('recherche/', views.recherche, name='recherche'), + path('recherche/resultats/', views.recherche_resultats, name='recherche_resultats'), - url(r'^recherche/items/$', views.stage_items, name='stage_items'), - url(r'^feedback/$', views.feedback, name='feedback'), - url(r'^moderation/$', views.statistiques, name='moderation'), - url(r'^api/', include(v1_api.urls)), + path('recherche/items/', views.stage_items, name='stage_items'), + path('feedback/', views.feedback, name='feedback'), + path('moderation/', views.statistiques, name='moderation'), + path('api/', include(v1_api.urls)), ] diff --git a/avisstage/utils.py b/avisstage/utils.py index 9aad26d..822f8ae 100644 --- a/avisstage/utils.py +++ b/avisstage/utils.py @@ -1,6 +1,3 @@ -# coding: utf-8 - -from allauth.socialaccount.models import SocialAccount from functools import reduce from math import cos, radians, sqrt @@ -18,3 +15,8 @@ def approximate_distance(a, b): dlat = (lat_a - lat_b) distance = 6371000 * sqrt(dlon*dlon + dlat*dlat) return distance + +def is_email_ens(mail, none=False): + if mail is None: + return none + return mail.endswith("ens.fr") or mail.endswith("ens.psl.eu") diff --git a/avisstage/views.py b/avisstage/views.py index 2bcb19c..897b124 100644 --- a/avisstage/views.py +++ b/avisstage/views.py @@ -2,21 +2,31 @@ from django.shortcuts import render, redirect, get_object_or_404 -from django.views.generic import DetailView, ListView -from django.views.generic.edit import UpdateView, CreateView +from django.views.generic import ( + DetailView, ListView, UpdateView, CreateView, TemplateView, DeleteView, + FormView, View +) +from django.views.generic.detail import SingleObjectMixin from django import forms -from django.urls import reverse +from django.urls import reverse, reverse_lazy from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required +from django.contrib.auth.tokens import default_token_generator +from django.contrib.auth.views import PasswordResetConfirmView +from django.contrib import messages from braces.views import LoginRequiredMixin from django.http import JsonResponse, HttpResponseForbidden, Http404 from django.core.mail import send_mail from django.db.models import Q, Count from collections import Counter, defaultdict +from simple_email_confirmation.models import EmailAddress from .models import Normalien, Stage, Lieu, AvisLieu, AvisStage -from .forms import StageForm, LieuForm, AvisStageForm, AvisLieuForm, FeedbackForm +from .forms import ( + StageForm, LieuForm, AvisStageForm, AvisLieuForm, FeedbackForm, AdresseEmailForm, + ReinitMdpForm +) from .utils import en_scolarite from .views_search import * @@ -343,3 +353,126 @@ def statistiques(request): 'num_lieux_utiles': nlieux, 'num_par_longueur': nbylength, }) + +# +# Compte +# + +class MesAdressesMixin(LoginRequiredMixin): + slug_url_kwarg = "email" + slug_field = "email" + confirmed_only = False + + def get_queryset(self, *args, **kwargs): + qs = self.request.user.email_address_set.all() + if self.confirmed_only: + qs = qs.filter(confirmed_at__isnull=False) + return qs + +def _send_confirm_mail(email, request): + confirm_url = request.build_absolute_uri( + reverse("avisstage:emails_confirme", kwargs={"key": email.key})) + send_mail( + "[ExperiENS] Confirmez votre adresse a-mail", +"""Bonjour, + +Vous venez d'ajouter cette adresse e-mail à votre compte ExperiENS. + +Pour la vérifier, merci de cliquer sur le lien suivant, ou de copier l'adresse dans votre navigateur : + + {confirm_url} + +Cordialement, +L'équipe ExperiENS""".format(confirm_url=confirm_url), + 'experiens-nepasrepondre@eleves.ens.fr', + [email.email], + fail_silently=False, + ) + return redirect(reverse("avisstage:emails_aconfirmer", + kwargs={"email": email.email})) + +class MesParametres(LoginRequiredMixin, FormView): + model = EmailAddress + template_name = "avisstage/compte/parametres.html" + form_class = AdresseEmailForm + + def get_form_kwargs(self, *args, **kwargs): + kwargs = super().get_form_kwargs(*args, **kwargs) + kwargs["_user"] = self.request.user + return kwargs + + def form_valid(self, form): + new = EmailAddress.objects.create_unconfirmed( + form.cleaned_data["email"], self.request.user) + return _send_confirm_mail(new, self.request) + +class RendAdressePrincipale(MesAdressesMixin, SingleObjectMixin, View): + model = EmailAddress + confirmed_only = True + + def post(self, *args, **kwargs): + if not hasattr(self, "object"): + self.object = self.get_object() + self.request.user.email = self.object.email + self.request.user.save() + return redirect(reverse("avisstage:parametres")) + +class AdresseAConfirmer(MesAdressesMixin, DetailView): + model = EmailAddress + template_name = "avisstage/compte/aconfirmer.html" + +class ReConfirmeAdresse(MesAdressesMixin, DetailView): + model = EmailAddress + + def post(self, *args, **kwargs): + email = self.get_object() + if email.confirmed_at is None: + return _send_confirm_mail(email, self.request) + return redirect(reverse("avisstage:parametres")) + +class ConfirmeAdresse(LoginRequiredMixin, View): + def get(self, *args, **kwargs): + try: + email = EmailAddress.objects.confirm(self.kwargs["key"], + self.request.user, True) + except Exception as e: + raise Http404() + messages.add_message( + self.request, messages.SUCCESS, + "L'adresse email {email} a bien été confirmée".format(email=email.email)) + return redirect(reverse("avisstage:parametres")) + +class SupprimeAdresse(MesAdressesMixin, DeleteView): + model = EmailAddress + template_name = "avisstage/compte/email_supprime.html" + success_url = reverse_lazy("avisstage:parametres") + + def get_queryset(self, *args, **kwargs): + qs = super().get_queryset(*args, **kwargs) + return qs.exclude(email=self.request.user.email) + +class EnvoieLienMotDePasse(LoginRequiredMixin, View): + def post(self, *args, **kwargs): + form = ReinitMdpForm({"email": self.request.user.email}) + form.is_valid() + form.save( + email_template_name = 'avisstage/mails/reinit_mdp.html', + from_email = 'experiens-nepasrepondre@eleves.ens.fr', + subject_template_name = 'avisstage/mails/reinit_mdp.txt', + ) + messages.add_message( + self.request, messages.INFO, + "Un mail a été envoyé à {email}. Merci de vérifier vos indésirables si vous ne le recevez pas bientôt".format(email=self.request.user.email) + ) + return redirect(reverse("avisstage:parametres")) + +class DefinirMotDePasse(PasswordResetConfirmView): + template_name = "avisstage/compte/edit_mdp.html" + success_url = reverse_lazy("avisstage:perso") + + def get_user(self, *args, **kwargs): + user = super().get_user(*args, **kwargs) + if self.request.user.is_authenticated and user != self.request.user: + raise Http404("Ce token n'est pas valide pour votre compte") + return user + diff --git a/experiENS/auth.py b/experiENS/auth.py index e310a39..b8f8d41 100644 --- a/experiENS/auth.py +++ b/experiENS/auth.py @@ -1,5 +1,12 @@ -from django_cas_ng.backends import CASBackend +from authens.backends import ENSCASBackend as AuthENSBackend +from authens.utils import parse_entrance_year -class ENSCASBackend(CASBackend): - def clean_username(self, username): - return username.lower().strip() +class ENSCASBackend(AuthENSBackend): + # Override AuthENS backend user creation to implement the @ logic + + def get_free_username(self, cas_login, attributes): + entrance_year = parse_entrance_year(attributes.get("homeDirectory")) + if entrance_year is None: + return super().get_free_username(cas_login, attributes) + entrance_year %= 100 + return "%s@%02d" % (cas_login, entrance_year) diff --git a/experiENS/settings_base.py b/experiENS/settings_base.py index 8e7f517..8a2ed36 100644 --- a/experiENS/settings_base.py +++ b/experiENS/settings_base.py @@ -10,7 +10,7 @@ https://docs.djangoproject.com/en/1.7/ref/settings/ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os -from django.core.urlresolvers import reverse_lazy +from django.urls import reverse_lazy from .secrets import SECRET_KEY, GOOGLE_API_KEY, MAPBOX_API_KEY @@ -35,16 +35,12 @@ INSTALLED_APPS = [ 'django_elasticsearch_dsl', - 'widget_tweaks', - 'allauth_ens', - - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - 'allauth_cas', - - 'allauth_ens.providers.clipper', + #'allauth', # Uncomment that part when you + #'allauth.account', # apply migration + #'allauth.socialaccount', # Allauth -> AuthENS + 'simple_email_confirmation', + 'authens', 'tastypie', 'braces', 'tinymce', @@ -53,12 +49,12 @@ INSTALLED_APPS = [ 'avisstage' ] -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( + 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) @@ -121,20 +117,13 @@ AUTHENTICATION_BACKENDS = ( ) CAS_SERVER_URL = "https://cas.eleves.ens.fr/" #SPI CAS -CAS_VERIFY_URL = "https://cas.eleves.ens.fr/" -CAS_IGNORE_REFERER = True -CAS_REDIRECT_URL = reverse_lazy('avisstage:perso') -CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" -CAS_FORCE_CHANGE_USERNAME_CASE = "lower" -CAS_VERSION = 'CAS_2_SAML_1_0' -ACCOUNT_ADAPTER = 'avisstage.allauth_adapter.AccountAdapter' -SOCIALACCOUNT_ADAPTER = 'avisstage.allauth_adapter.SocialAccountAdapter' +AUTHENS_USE_OLDCAS = False -LOGIN_URL = reverse_lazy('account_login') -LOGOUT_URL = reverse_lazy('account_logout') +LOGIN_URL = reverse_lazy('authens:login') +LOGOUT_URL = reverse_lazy('authens:logout') LOGIN_REDIRECT_URL = reverse_lazy('avisstage:perso') -ACCOUNT_HOME_URL = reverse_lazy('avisstage:index') +LOGOUT_REDIRECT_URL = reverse_lazy('avisstage:index') LOGGING = { 'version': 1, diff --git a/experiENS/settings_dev.py b/experiENS/settings_dev.py index 295def3..8c0c9b3 100644 --- a/experiENS/settings_dev.py +++ b/experiENS/settings_dev.py @@ -9,16 +9,16 @@ DATABASES = { } } -USE_DEBUG_TOOLBAR = True +USE_DEBUG_TOOLBAR = False if USE_DEBUG_TOOLBAR: INSTALLED_APPS += [ 'debug_toolbar', ] - MIDDLEWARE_CLASSES = ( + MIDDLEWARE = ( 'debug_toolbar.middleware.DebugToolbarMiddleware', - ) + MIDDLEWARE_CLASSES + ) + MIDDLEWARE INTERNAL_IPS = ['127.0.0.1'] diff --git a/experiENS/urls.py b/experiENS/urls.py index 91c6191..4bb2bfb 100644 --- a/experiENS/urls.py +++ b/experiENS/urls.py @@ -1,19 +1,20 @@ from django.conf import settings -from django.conf.urls import include, url +from django.urls import include, path from django.contrib import admin urlpatterns = [ - url(r'^', include('avisstage.urls', namespace='avisstage')), + path('', include('avisstage.urls')), - url(r'^account/', include('allauth_ens.urls')), + + path("authens/", include("authens.urls")), - url(r'^tinymce/', include('tinymce.urls')), - url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')), - url(r'^admin/', include(admin.site.urls)), + path('tinymce/', include('tinymce.urls')), + path('taggit_autosuggest/', include('taggit_autosuggest.urls')), + path('admin/', admin.site.urls), ] if settings.DEBUG: import debug_toolbar urlpatterns = [ - url(r'^__debug__/', include(debug_toolbar.urls)), + path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns diff --git a/requirements.txt b/requirements.txt index 08eceff..16e58ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,11 @@ -django==1.11.* -django-cas-ng==3.5.* -django-taggit==0.22.* -python-ldap==3.0.* -django-tinymce==2.7.* -django-braces==1.12.* +django==2.2.* +django-taggit==1.3.* +django-tinymce==3.2.* +django-braces==1.14.* django-taggit-autosuggest==0.3.* -pytz==2018.* +pytz==2020.* django-tastypie==0.14.* -lxml==4.2.* -django-elasticsearch-dsl==0.4.* -django-allauth-ens==1.1.* +lxml==4.6.* +django-elasticsearch-dsl==7.1.* +authens +django-simple-email-confirmation==0.*