From d3f5c3df704c1c197d9e6e524d0e7f352e5586a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 4 Oct 2019 23:42:56 +0200 Subject: [PATCH 01/23] README: add missing mandatory secrets --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bee1ee7..93536c0 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Ensuite, paramétrez les settings : cd experiENS/ echo 'SECRET_KEY="toto"' > secrets.py + echo 'GOOGLE_API_KEY="toto"' >> secrets.py + echo 'MAPBOX_API_KEY="toto"' >> secrets.py ln -s settings_dev.py settings.py cd ../ From 5726ff2692a6b4f47dc4374754da74845e6aba17 Mon Sep 17 00:00:00 2001 From: Evarin Date: Wed, 29 Jan 2020 15:17:57 +0100 Subject: [PATCH 02/23] Hotfix allauth deprecation clipper --- avisstage/allauth_adapter.py | 39 +++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/avisstage/allauth_adapter.py b/avisstage/allauth_adapter.py index 130b71d..1935e47 100644 --- a/avisstage/allauth_adapter.py +++ b/avisstage/allauth_adapter.py @@ -1,5 +1,6 @@ from allauth.account.adapter import DefaultAccountAdapter -from allauth_ens.adapter import LongTermClipperAccountAdapter +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): @@ -18,3 +19,39 @@ class SocialAccountAdapter(LongTermClipperAccountAdapter): # 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() From 18d1d53c45a3f134d2db01ce4a8c72f854b67865 Mon Sep 17 00:00:00 2001 From: Evarin Date: Fri, 1 Jan 2021 19:37:58 +0100 Subject: [PATCH 03/23] =?UTF-8?q?R=C3=A9pare=20la=20carte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- avisstage/static/js/recherche.js | 4 ++-- avisstage/templates/avisstage/detail/stage.html | 7 +++++-- avisstage/templates/avisstage/recherche/resultats.html | 3 +++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/avisstage/static/js/recherche.js b/avisstage/static/js/recherche.js index d466ff5..bc6457f 100644 --- a/avisstage/static/js/recherche.js +++ b/avisstage/static/js/recherche.js @@ -83,10 +83,10 @@ function InterfaceRecherche(STATIC_ROOT, API_LIEU, MAPBOX_API_KEY, ITEMS_URL, li function initCarte() { if (map !== undefined) return; map = L.map("carte").panTo([30, 15]).setZoom(1); - 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/detail/stage.html b/avisstage/templates/avisstage/detail/stage.html index d5b3c65..9ec7b37 100644 --- a/avisstage/templates/avisstage/detail/stage.html +++ b/avisstage/templates/avisstage/detail/stage.html @@ -6,6 +6,9 @@ {% block extra_head %} + + + + + {% endblock %} From e470a2a26892c16d79958e1a2fd89bcd0943d1de Mon Sep 17 00:00:00 2001 From: Robin Champenois Date: Sun, 7 Feb 2021 18:23:24 +0100 Subject: [PATCH 04/23] =?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.* From 1956f381765af2ca14fa166da9d8a3c4cb14b53e Mon Sep 17 00:00:00 2001 From: Robin Champenois Date: Sun, 7 Feb 2021 19:08:29 +0100 Subject: [PATCH 05/23] Useless import in prod settings --- experiENS/settings_prod.py | 1 - 1 file changed, 1 deletion(-) diff --git a/experiENS/settings_prod.py b/experiENS/settings_prod.py index 08306f6..63bfc1c 100644 --- a/experiENS/settings_prod.py +++ b/experiENS/settings_prod.py @@ -1,7 +1,6 @@ from .settings_base import * import os, sys -from django.core.urlresolvers import reverse_lazy PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(PROJECT_DIR) From 3c2f93bccb43888f781183865b5996ff88e9e2c3 Mon Sep 17 00:00:00 2001 From: Robin Champenois Date: Mon, 28 Jun 2021 21:44:47 +0200 Subject: [PATCH 06/23] =?UTF-8?q?M=C3=A0J=20ElasticSearch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- avisstage/documents.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/avisstage/documents.py b/avisstage/documents.py index b2fcf50..6bf946d 100644 --- a/avisstage/documents.py +++ b/avisstage/documents.py @@ -1,4 +1,4 @@ -from django_elasticsearch_dsl import DocType, Index, fields +from django_elasticsearch_dsl import Document, Index, fields from elasticsearch_dsl import analyzer, token_filter, tokenizer from .models import Stage, AvisStage, AvisLieu @@ -21,19 +21,19 @@ text_analyzer = analyzer( stage.analyzer(text_analyzer) @stage.doc_type -class StageDocument(DocType): +class StageDocument(Document): lieux = fields.ObjectField(properties={ - 'nom': fields.StringField(), - 'ville': fields.StringField(), - 'pays': fields.StringField(), + 'nom': fields.TextField(), + 'ville': fields.TextField(), + 'pays': fields.TextField(), }) auteur = fields.ObjectField(properties={ - 'nom': fields.StringField(), + 'nom': fields.TextField(), }) - thematiques = fields.StringField() - matieres = fields.StringField() + thematiques = fields.TextField() + matieres = fields.TextField() - class Meta: + class Django: model = Stage fields = [ 'sujet', From 22b50166871ea837ec1c46ab67941f7bef8bb872 Mon Sep 17 00:00:00 2001 From: Robin Champenois Date: Mon, 28 Jun 2021 22:09:08 +0200 Subject: [PATCH 07/23] Remove standard useless token filter --- avisstage/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avisstage/documents.py b/avisstage/documents.py index 6bf946d..98da0c0 100644 --- a/avisstage/documents.py +++ b/avisstage/documents.py @@ -15,7 +15,7 @@ stage.settings( text_analyzer = analyzer( 'default', tokenizer="standard", - filter=['lowercase', 'standard', 'asciifolding', + filter=['lowercase', 'asciifolding', token_filter("frstop", type="stop", stopwords="_french_"), token_filter("frsnow", type="snowball", language="French")]) stage.analyzer(text_analyzer) From 370447d355108e90a9d7d64ba452c1117a9b10ab Mon Sep 17 00:00:00 2001 From: Robin Champenois Date: Mon, 28 Jun 2021 22:23:00 +0200 Subject: [PATCH 08/23] Fix search? --- avisstage/views_search.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/avisstage/views_search.py b/avisstage/views_search.py index bedc8a1..9d92700 100644 --- a/avisstage/views_search.py +++ b/avisstage/views_search.py @@ -136,11 +136,12 @@ def cherche(**kwargs): # Application + resultat = Stage.objects if USE_ELASTICSEARCH and use_dsl: - filtres &= Q(id__in=[s.meta.id for s in dsl.scan()]) + resultat = dsl.to_queryset(True) #print(filtres) - resultat = Stage.objects.filter(filtres) + resultat = resultat.filter(filtres).distinct() tri = 'pertinence' if not use_dsl: From 46eacc94da07452485fe4348ec1bb9187dd13105 Mon Sep 17 00:00:00 2001 From: Robin Champenois Date: Mon, 28 Jun 2021 23:00:32 +0200 Subject: [PATCH 09/23] Fix search2? --- avisstage/views_search.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/avisstage/views_search.py b/avisstage/views_search.py index 9d92700..0e6a394 100644 --- a/avisstage/views_search.py +++ b/avisstage/views_search.py @@ -73,9 +73,21 @@ def cherche(**kwargs): if field_relevant("generique"): #print("Filtre generique", kwargs['generique']) dsl = dsl.query( - "match", - _all={"query": kwargs["generique"], - "fuzziness": "auto"}) + "multi_match", + query = kwargs["generique"], + fuzziness = "auto", + fields = [ + 'sujet^3', + 'encadrants', + 'type_stage', + 'niveau_scol', + 'structure', + 'date_*', + "lieux.*^2", + "auteur.nom^2", + "thematiques^2", + "matieres" + ]) use_dsl = True # Sujet -> Recherche dan les noms de sujets et les thématiques From c40a91fb67aa139c7637e020f3b23d3fff3499ca Mon Sep 17 00:00:00 2001 From: Robin Champenois Date: Mon, 28 Jun 2021 23:25:16 +0200 Subject: [PATCH 10/23] Better search --- avisstage/views_search.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/avisstage/views_search.py b/avisstage/views_search.py index 0e6a394..df5e75e 100644 --- a/avisstage/views_search.py +++ b/avisstage/views_search.py @@ -82,7 +82,6 @@ def cherche(**kwargs): 'type_stage', 'niveau_scol', 'structure', - 'date_*', "lieux.*^2", "auteur.nom^2", "thematiques^2", @@ -146,22 +145,30 @@ def cherche(**kwargs): if field_relevant('type_lieu'): filtres &= Q(lieux__type_lieu=kwargs["type_lieu"]) - - # Application - resultat = Stage.objects - if USE_ELASTICSEARCH and use_dsl: - resultat = dsl.to_queryset(True) + # Tri + tri = "pertinence" - #print(filtres) - resultat = resultat.filter(filtres).distinct() - tri = 'pertinence' - - if not use_dsl: - kwargs['tri'] = '-date_maj' - if field_relevant('tri') and kwargs['tri'] in ['-date_maj']: tri = kwargs['tri'] - resultat = resultat.order_by(tri) + + if not use_dsl: + tri = "-date_maj" + + # Application + resultat = Stage.objects.filter(filtres).distinct() + + if USE_ELASTICSEARCH and use_dsl: + dls_res = [s.meta.id for s in dsl.scan()[:50]] + resultat = resultat.filter(id__in=dsl_res) + + if tri == "pertinence": + resultat = resultat.order_by( + Case( + *[When(pk=pk, then=pos) for pos, pk in enumerate(pks)] + ) + ) + else: + resultat = resultat.order_by(tri) return resultat, tri From 26ad68ff693984886fdb3ab78f55ae41c7b0fc0a Mon Sep 17 00:00:00 2001 From: Robin Champenois Date: Mon, 28 Jun 2021 23:29:58 +0200 Subject: [PATCH 11/23] Fix search --- avisstage/views_search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/avisstage/views_search.py b/avisstage/views_search.py index df5e75e..c1f6efe 100644 --- a/avisstage/views_search.py +++ b/avisstage/views_search.py @@ -158,13 +158,13 @@ def cherche(**kwargs): resultat = Stage.objects.filter(filtres).distinct() if USE_ELASTICSEARCH and use_dsl: - dls_res = [s.meta.id for s in dsl.scan()[:50]] + dsl_res = [s.meta.id for s in dsl.scan()] resultat = resultat.filter(id__in=dsl_res) if tri == "pertinence": resultat = resultat.order_by( Case( - *[When(pk=pk, then=pos) for pos, pk in enumerate(pks)] + *[When(pk=pk, then=pos) for pos, pk in enumerate(dsl_res)] ) ) else: From 7cfc85f1fc5b5fb9f1a9c8ecb8052e612479200e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 7 Feb 2021 23:15:47 +0100 Subject: [PATCH 12/23] Black --- avisstage/admin.py | 13 +- avisstage/api.py | 69 +- avisstage/apps.py | 3 +- avisstage/decorators.py | 2 + avisstage/documents.py | 68 +- avisstage/forms.py | 125 +++- .../management/commands/nettoie_lieux.py | 51 +- .../management/commands/nettoie_stages.py | 41 +- .../management/commands/supprime_lieu.py | 33 +- .../management/commands/termine_scolarite.py | 3 +- avisstage/migrations/0001_initial.py | 686 ++++++++++++++++-- .../migrations/0002_auto_20171002_2243.py | 26 +- .../migrations/0003_auto_20210117_1208.py | 350 ++++++++- .../migrations/0004_allauth_to_authens.py | 59 +- .../migrations/0005_normalien_en_scolarite.py | 6 +- .../migrations/0006_auto_20210131_1954.py | 24 +- avisstage/models.py | 159 ++-- avisstage/statics.py | 595 +++++++-------- avisstage/templatetags/avisstage_tags.py | 15 +- avisstage/tests.py | 513 +++++++------ avisstage/urls.py | 92 ++- avisstage/utils.py | 14 +- avisstage/views.py | 324 ++++++--- avisstage/views_search.py | 232 +++--- avisstage/widgets.py | 24 +- experiENS/auth.py | 3 +- experiENS/settings_base.py | 125 ++-- experiENS/settings_dev.py | 27 +- experiENS/settings_prod.py | 32 +- experiENS/urls.py | 14 +- experiENS/wsgi.py | 2 + 31 files changed, 2461 insertions(+), 1269 deletions(-) diff --git a/avisstage/admin.py b/avisstage/admin.py index c57f678..52a6c2d 100644 --- a/avisstage/admin.py +++ b/avisstage/admin.py @@ -5,29 +5,36 @@ from avisstage.models import * import authens.models as authmod + class NormalienInline(admin.StackedInline): model = Normalien inline_classes = ("collapse open",) + class UserAdmin(UserAdmin): - inlines = (NormalienInline, ) + inlines = (NormalienInline,) + class AvisLieuInline(admin.StackedInline): model = AvisLieu inline_classes = ("collapse open",) extra = 0 + class AvisStageInline(admin.StackedInline): model = AvisStage inline_classes = ("collapse open",) extra = 0 - + + class StageAdmin(admin.ModelAdmin): inlines = (AvisLieuInline, AvisStageInline) + class StageMatiereAdmin(admin.ModelAdmin): model = StageMatiere - prepopulated_fields = {"slug": ('nom',)} + prepopulated_fields = {"slug": ("nom",)} + admin.site.unregister(User) admin.site.register(User, UserAdmin) diff --git a/avisstage/api.py b/avisstage/api.py index a1e4c0e..61e555f 100644 --- a/avisstage/api.py +++ b/avisstage/api.py @@ -10,23 +10,25 @@ from django.urls import reverse from .models import Lieu, Stage, Normalien, StageMatiere from .utils import approximate_distance + class EnScolariteAuthentication(SessionAuthentication): def is_authenticated(self, request, **kwargs): if super().is_authenticated(request, **kwargs): return request.user.profil.en_scolarite return False + # API principale pour les lieux class LieuResource(ModelResource): - #stages = fields.ToManyField("avisstage.api.StageResource", + # stages = fields.ToManyField("avisstage.api.StageResource", # "stages", use_in="detail", full=True) - + class Meta: queryset = Lieu.objects.all() resource_name = "lieu" fields = ["nom", "ville", "pays", "coord", "type_lieu", "id"] - - #login_required + + # login_required authentication = SessionAuthentication() # Filtres personnalisés @@ -37,43 +39,44 @@ class LieuResource(ModelResource): # Trouver les lieux à proximités d'un point donné if "lng" in filters and "lat" in filters: - lat = float(filters['lat']) - lng = float(filters['lng']) - pt = geos.Point((lng,lat), srid=4326) + lat = float(filters["lat"]) + lng = float(filters["lng"]) + pt = geos.Point((lng, lat), srid=4326) self.reference_point = pt - orm_filters['coord__distance_lte'] = (pt, 10000) + orm_filters["coord__distance_lte"] = (pt, 10000) # Filtrer les lieux qui ont déjà des stages if "has_stage" in filters: - orm_filters['stages__public'] = True - + orm_filters["stages__public"] = True + return orm_filters # Custom apply filters pour ajouter le "distinct" def apply_filters(self, request, applicable_filters): return self.get_object_list(request).filter(**applicable_filters).distinct() - + # Ajout d'informations def dehydrate(self, bundle): bundle = super(LieuResource, self).dehydrate(bundle) - + obj = bundle.obj - bundle.data['coord'] = {'lat': float(obj.coord.y), - 'lng': float(obj.coord.x)} + bundle.data["coord"] = {"lat": float(obj.coord.y), "lng": float(obj.coord.x)} # Distance au point recherché if "lat" in bundle.request.GET and "lng" in bundle.request.GET: - bundle.data['distance'] = approximate_distance( - self.reference_point, bundle.obj.coord) - + bundle.data["distance"] = approximate_distance( + self.reference_point, bundle.obj.coord + ) + # Autres infos utiles bundle.data["pays_nom"] = obj.get_pays_display() bundle.data["type_lieu_nom"] = obj.type_lieu_fancy # TODO use annotate? - bundle.data["num_stages"] = obj.stages.filter(public=True).count() + bundle.data["num_stages"] = obj.stages.filter(public=True).count() return bundle + # API sur un stage class StageResource(ModelResource): class Meta: @@ -81,7 +84,7 @@ class StageResource(ModelResource): resource_name = "stage" fields = ["sujet", "date_debut", "date_fin", "matieres", "id"] - #login_required + # login_required authentication = EnScolariteAuthentication() # Filtres personnalisés @@ -92,9 +95,9 @@ class StageResource(ModelResource): # Récupération des stages à un lieu donné if "lieux" in filters: - flieux = map(int, filters['lieux'].split(',')) - orm_filters['lieux__id__in'] = flieux - + flieux = map(int, filters["lieux"].split(",")) + orm_filters["lieux__id__in"] = flieux + return orm_filters # Informations à ajouter @@ -103,23 +106,27 @@ class StageResource(ModelResource): obj = bundle.obj # Affichage des manytomany en condensé - bundle.data['auteur'] = obj.auteur.nom - bundle.data['thematiques'] = list(obj.thematiques.all().values_list("name", flat=True)) - bundle.data['matieres'] = list(obj.matieres.all().values_list("nom", flat=True)) - + bundle.data["auteur"] = obj.auteur.nom + bundle.data["thematiques"] = list( + obj.thematiques.all().values_list("name", flat=True) + ) + bundle.data["matieres"] = list(obj.matieres.all().values_list("nom", flat=True)) + # Adresse de la fiche de stage - bundle.data['url'] = reverse("avisstage:stage", kwargs={"pk": obj.id}); + bundle.data["url"] = reverse("avisstage:stage", kwargs={"pk": obj.id}) return bundle + # Auteurs des fiches (TODO supprimer ?) class AuteurResource(ModelResource): - stages = fields.ToManyField("avisstage.api.StageResource", - "stages", use_in="detail") - + stages = fields.ToManyField( + "avisstage.api.StageResource", "stages", use_in="detail" + ) + class Meta: queryset = Normalien.objects.all() resource_name = "profil" fields = ["id", "nom", "stages"] - #login_required + # login_required authentication = EnScolariteAuthentication() diff --git a/avisstage/apps.py b/avisstage/apps.py index 4fa2dd9..f046aa9 100644 --- a/avisstage/apps.py +++ b/avisstage/apps.py @@ -1,4 +1,5 @@ from django.apps import AppConfig + class AvisstageConfig(AppConfig): - name = 'avisstage' + name = "avisstage" diff --git a/avisstage/decorators.py b/avisstage/decorators.py index d5f7dd6..8437240 100644 --- a/avisstage/decorators.py +++ b/avisstage/decorators.py @@ -3,10 +3,12 @@ from functools import wraps from django.urls import reverse from django.shortcuts import redirect + def en_scolarite_required(view_func): @wraps(view_func) def _wrapped_view(request, *args, **kwargs): if request.user.profil.en_scolarite: return view_func(request, *args, **kwargs) return redirect(reverse("avisstage:403-archicubes")) + return _wrapped_view diff --git a/avisstage/documents.py b/avisstage/documents.py index 98da0c0..f8fde43 100644 --- a/avisstage/documents.py +++ b/avisstage/documents.py @@ -6,47 +6,55 @@ from .statics import PAYS_OPTIONS PAYS_DICT = dict(PAYS_OPTIONS) -stage = Index('stages') -stage.settings( - number_of_shards=1, - number_of_replicas=0 -) - +stage = Index("stages") +stage.settings(number_of_shards=1, number_of_replicas=0) + text_analyzer = analyzer( - 'default', + "default", tokenizer="standard", - filter=['lowercase', 'asciifolding', - token_filter("frstop", type="stop", stopwords="_french_"), - token_filter("frsnow", type="snowball", language="French")]) + filter=[ + "lowercase", + "asciifolding", + token_filter("frstop", type="stop", stopwords="_french_"), + token_filter("frsnow", type="snowball", language="French"), + ], +) stage.analyzer(text_analyzer) + @stage.doc_type class StageDocument(Document): - lieux = fields.ObjectField(properties={ - 'nom': fields.TextField(), - 'ville': fields.TextField(), - 'pays': fields.TextField(), - }) - auteur = fields.ObjectField(properties={ - 'nom': fields.TextField(), - }) + lieux = fields.ObjectField( + properties={ + "nom": fields.TextField(), + "ville": fields.TextField(), + "pays": fields.TextField(), + } + ) + auteur = fields.ObjectField( + properties={ + "nom": fields.TextField(), + } + ) thematiques = fields.TextField() matieres = fields.TextField() class Django: model = Stage fields = [ - 'sujet', - 'encadrants', - 'type_stage', - 'niveau_scol', - 'structure', - 'date_debut', - 'date_fin' + "sujet", + "encadrants", + "type_stage", + "niveau_scol", + "structure", + "date_debut", + "date_fin", ] def prepare_thematiques(self, instance): - return ", ".join(instance.thematiques.all().values_list("name", flat=True)).lower() + return ", ".join( + instance.thematiques.all().values_list("name", flat=True) + ).lower() def prepare_matieres(self, instance): return ", ".join(instance.matieres.all().values_list("nom", flat=True)).lower() @@ -65,11 +73,11 @@ class StageDocument(Document): def prepare_sujet(self, instance): return instance.sujet.lower() - + # Hook pour l'affichage des noms de pays def prepare(self, instance): data = super(StageDocument, self).prepare(instance) - - for lieu in data['lieux']: - lieu['pays'] = PAYS_DICT[lieu['pays']].lower() + + for lieu in data["lieux"]: + lieu["pays"] = PAYS_DICT[lieu["pays"]].lower() return data diff --git a/avisstage/forms.py b/avisstage/forms.py index 12e88ea..d3da9a4 100644 --- a/avisstage/forms.py +++ b/avisstage/forms.py @@ -15,32 +15,52 @@ from .widgets import LatLonField class HTMLTrimmerForm(forms.ModelForm): def clean(self): # Suppression des espaces blanc avant et après le texte pour les champs html - leading_white = re.compile(r"^( \t\n)*(

    ( |[ \n\t]|)*

    ( \t\n)*)+?", re.IGNORECASE) - trailing_white = re.compile(r"(( \t\n)*

    ( |[ \n\t]|)*

    )+?( \t\n)*$", re.IGNORECASE) + leading_white = re.compile( + r"^( \t\n)*(

    ( |[ \n\t]|)*

    ( \t\n)*)+?", re.IGNORECASE + ) + trailing_white = re.compile( + r"(( \t\n)*

    ( |[ \n\t]|)*

    )+?( \t\n)*$", re.IGNORECASE + ) cleaned_data = super(HTMLTrimmerForm, self).clean() - + for (fname, fval) in cleaned_data.items(): # Heuristique : les champs commençant par "avis_" sont des champs html if fname[:5] == "avis_": - cleaned_data[fname] = leading_white.sub("", trailing_white.sub("", fval)) - + cleaned_data[fname] = leading_white.sub( + "", trailing_white.sub("", fval) + ) + return cleaned_data + # Infos sur un stage class StageForm(forms.ModelForm): - date_widget = forms.DateInput(attrs={"class":"datepicker", - "placeholder":"JJ/MM/AAAA"}) - date_debut = forms.DateField(label=u"Date de début", - input_formats=["%d/%m/%Y"], widget=date_widget) - date_fin = forms.DateField(label=u"Date de fin", - input_formats=["%d/%m/%Y"], widget=date_widget) + date_widget = forms.DateInput( + attrs={"class": "datepicker", "placeholder": "JJ/MM/AAAA"} + ) + date_debut = forms.DateField( + label=u"Date de début", input_formats=["%d/%m/%Y"], widget=date_widget + ) + date_fin = forms.DateField( + label=u"Date de fin", input_formats=["%d/%m/%Y"], widget=date_widget + ) class Meta: model = Stage - fields = ['sujet', 'date_debut', 'date_fin', 'type_stage', 'niveau_scol', 'thematiques', 'matieres', 'structure', 'encadrants'] + fields = [ + "sujet", + "date_debut", + "date_fin", + "type_stage", + "niveau_scol", + "thematiques", + "matieres", + "structure", + "encadrants", + ] help_texts = { "thematiques": u"Mettez une virgule pour valider votre thématique si la suggestion ne correspond pas ou si elle n'existe pas encore", - "structure": u"Nom de l'équipe, du laboratoire, de la startup... (si le lieu ne suffit pas)" + "structure": u"Nom de l'équipe, du laboratoire, de la startup... (si le lieu ne suffit pas)", } labels = { "date_debut": u"Date de début", @@ -51,27 +71,36 @@ class StageForm(forms.ModelForm): if "request" in kwargs: self.request = kwargs.pop("request") super(StageForm, self).__init__(*args, **kwargs) - + def save(self, commit=True): # Lors de la création : attribution à l'utilisateur connecté - if self.instance.id is None and hasattr(self, 'request'): + if self.instance.id is None and hasattr(self, "request"): self.instance.auteur = self.request.user.profil - + # Date de modification self.instance.date_maj = timezone.now() self.instance.update_stats(False) - + stage = super(StageForm, self).save(commit=commit) return stage + # Sous-formulaire des avis sur le stage class AvisStageForm(HTMLTrimmerForm): class Meta: model = AvisStage - fields = ['chapo', 'avis_sujet', 'avis_ambiance', 'avis_admin', 'avis_prestage', 'les_plus', 'les_moins'] + fields = [ + "chapo", + "avis_sujet", + "avis_ambiance", + "avis_admin", + "avis_prestage", + "les_plus", + "les_moins", + ] help_texts = { - "chapo": u"\"Trop long, pas lu\" : une accroche résumant ce que vous avez pensé de ce séjour", + "chapo": u'"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de ce séjour', "avis_ambiance": u"Avez-vous passé un bon moment à ce travail ? Étiez-vous assez guidé⋅e ? Aviez-vous un bon contact avec vos encadrant⋅e⋅s ? Y avait-il une bonne ambiance dans l'équipe ?", "avis_sujet": u"Quelle était votre mission ? Qu'en avez-vous retiré ? Le travail correspondait-il à vos attentes ? Était-ce à votre niveau, trop dur, trop facile ?", "avis_admin": u"Avez-vous commencé votre travail à la date prévue ? Était-ce compliqué d'obtenir les documents nécessaires (visa, contrats, etc) ? L'administration de l'établissement vous a-t-elle aidé⋅e ? Étiez-vous rémunéré⋅e ?", @@ -80,48 +109,62 @@ class AvisStageForm(HTMLTrimmerForm): "les_moins": u"Ce qui aurait pu être mieux", } + class AvisLieuForm(HTMLTrimmerForm): class Meta: model = AvisLieu - fields = ['lieu', 'chapo', 'avis_lieustage', 'avis_pratique', 'avis_tourisme', 'les_plus', 'les_moins'] + fields = [ + "lieu", + "chapo", + "avis_lieustage", + "avis_pratique", + "avis_tourisme", + "les_plus", + "les_moins", + ] help_texts = { - "chapo": u"\"Trop long, pas lu\" : une accroche résumant ce que vous avez pensé de cet endroit", + "chapo": u'"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de cet endroit', "avis_lieustage": u"Qu'avez-vous pensé des lieux où vous travailliez ? Les bâtiments étaient-ils modernes ? Était-il agréable d'y travailler ?", "avis_pratique": u"Avez-vous eu du mal à trouver un logement ? Y-a-t-il des choses que vous avez apprises sur place qu'il vous aurait été utile de savoir avant de partir ?", "avis_tourisme": u"Y-a-t-il des lieux à visiter dans cette zone ? Avez-vous pratiqué des activités sportives ? Est-il facile de faire des rencontres ?", "les_plus": u"Les meilleures raisons de partir à cet endroit", "les_moins": u"Ce qui vous a gêné ou manqué là-bas", } - widgets = { - "lieu": forms.HiddenInput(attrs={"class":"lieu-hidden"}) - } + widgets = {"lieu": forms.HiddenInput(attrs={"class": "lieu-hidden"})} + # Création d'un nouveau lieu class LieuForm(forms.ModelForm): coord = LatLonField() id = forms.IntegerField(widget=forms.widgets.HiddenInput(), required=False) - + class Meta: model = Lieu - fields = ['id', 'nom', 'type_lieu', 'ville', 'pays', 'coord'] + fields = ["id", "nom", "type_lieu", "ville", "pays", "coord"] + # Widget de feedback class FeedbackForm(forms.Form): objet = forms.CharField(label="Objet", required=True) - message = forms.CharField(label="Message", required=True, widget=forms.widgets.Textarea()) - + 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"})) + + 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") + raise forms.ValidationError("Cette adresse est déjà associée à ce compte") return email @@ -131,19 +174,25 @@ def _unicode_ci_compare(s1, s2): recommended algorithm from Unicode Technical Report 36, section 2.11.2(B)(2). """ - return unicodedata.normalize('NFKC', s1).casefold() == unicodedata.normalize('NFKC', s2).casefold() + 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, - }) + active_users = User._default_manager.filter( + **{ + "%s__iexact" % email_field_name: email, + "is_active": True, + } + ) return ( - u for u in active_users + u + for u in active_users if _unicode_ci_compare(email, getattr(u, email_field_name)) ) diff --git a/avisstage/management/commands/nettoie_lieux.py b/avisstage/management/commands/nettoie_lieux.py index abf3195..02daea2 100644 --- a/avisstage/management/commands/nettoie_lieux.py +++ b/avisstage/management/commands/nettoie_lieux.py @@ -1,43 +1,60 @@ -#coding: utf-8 +# coding: utf-8 from django.core.management.base import BaseCommand, CommandError from django.db.models import Count from avisstage.models import Stage, Lieu + class Command(BaseCommand): - help = 'Nettoie les stages à plusieurs lieux identiques' + help = "Nettoie les stages à plusieurs lieux identiques" def add_arguments(self, parser): - parser.add_argument('min_lieu', nargs='?', default=0, type=int) + parser.add_argument("min_lieu", nargs="?", default=0, type=int) parser.add_argument( - '--apply', - action='store_true', + "--apply", + action="store_true", default=False, - help='Applies the modifications', + help="Applies the modifications", ) def handle(self, *args, **options): rundb = False - if options.get('apply', False): + if options.get("apply", False): rundb = True else: print(u"Les modifications ne seront pas appliquées") - - min_lieu = options.get('min_lieu', 0) - for lieu in Lieu.objects.filter(id__gte=min_lieu).order_by('-id'): - lproches = Lieu.objects.filter(id__lt=lieu.id, coord__distance_lte=(lieu.coord, 5)) + min_lieu = options.get("min_lieu", 0) + + for lieu in Lieu.objects.filter(id__gte=min_lieu).order_by("-id"): + lproches = Lieu.objects.filter( + id__lt=lieu.id, coord__distance_lte=(lieu.coord, 5) + ) if len(lproches) == 0: continue - print(u"Doublons possibles pour %s (id=%d, %d avis) :" % (lieu, lieu.id, lieu.avislieu_set.count())) + print( + u"Doublons possibles pour %s (id=%d, %d avis) :" + % (lieu, lieu.id, lieu.avislieu_set.count()) + ) for plieu in lproches: - pprint = u" > %s (id=%d, %d avis)" % (plieu, plieu.id, plieu.avislieu_set.count()) - if plieu.nom == lieu.nom and plieu.ville == lieu.ville and plieu.type_lieu == lieu.type_lieu: - print(u"%s %s" % (pprint, self.style.SUCCESS(u'-> Suppression'))) + pprint = u" > %s (id=%d, %d avis)" % ( + plieu, + plieu.id, + plieu.avislieu_set.count(), + ) + if ( + plieu.nom == lieu.nom + and plieu.ville == lieu.ville + and plieu.type_lieu == lieu.type_lieu + ): + print(u"%s %s" % (pprint, self.style.SUCCESS(u"-> Suppression"))) if rundb: for avis in plieu.avislieu_set.all(): avis.lieu = lieu avis.save() plieu.delete() else: - print(u"%s %s" % (pprint, self.style.WARNING(u'-> À supprimer manuellement'))) - self.stdout.write(self.style.SUCCESS(u'Nettoyage des lieux effectué')) + print( + u"%s %s" + % (pprint, self.style.WARNING(u"-> À supprimer manuellement")) + ) + self.stdout.write(self.style.SUCCESS(u"Nettoyage des lieux effectué")) diff --git a/avisstage/management/commands/nettoie_stages.py b/avisstage/management/commands/nettoie_stages.py index 8d7704e..eec8e9b 100644 --- a/avisstage/management/commands/nettoie_stages.py +++ b/avisstage/management/commands/nettoie_stages.py @@ -1,18 +1,19 @@ -#coding: utf-8 +# coding: utf-8 from django.core.management.base import BaseCommand, CommandError from django.db.models import Count from avisstage.models import Stage, Lieu + class Command(BaseCommand): - help = 'Nettoie les stages à plusieurs lieux identiques' + help = "Nettoie les stages à plusieurs lieux identiques" def add_arguments(self, parser): - parser.add_argument('min_stage', nargs='?', default=0, type=int) + parser.add_argument("min_stage", nargs="?", default=0, type=int) parser.add_argument( - '--apply', - action='store_true', + "--apply", + action="store_true", default=False, - help='Applies the modifications', + help="Applies the modifications", ) def handle(self, *args, **options): @@ -27,15 +28,16 @@ class Command(BaseCommand): return length rundb = False - if options.get('apply', False): + if options.get("apply", False): rundb = True else: print(u"Les modifications ne seront pas appliquées") - - min_stage = options.get('min_stage', 0) - for stage in Stage.objects.annotate(c=Count("lieux"))\ - .filter(c__gte=2, id__gte=min_stage): + min_stage = options.get("min_stage", 0) + + for stage in Stage.objects.annotate(c=Count("lieux")).filter( + c__gte=2, id__gte=min_stage + ): lieuset = {} todel = [] problems = [] @@ -54,13 +56,18 @@ class Command(BaseCommand): if len(todel) > 0: print(u"Doublons détectés dans %s" % (stage,)) for avis, alen in todel: - print(u" > Suppression de l'avis sur %s de %d mots" % \ - (avis.lieu, alen)) + print( + u" > Suppression de l'avis sur %s de %d mots" + % (avis.lieu, alen) + ) if rundb: avis.delete() if len(problems) > 0: - self.stdout.write(self.style.WARNING(u"Réparation impossible de %s (id=%d)" % (stage, stage.id))) + self.stdout.write( + self.style.WARNING( + u"Réparation impossible de %s (id=%d)" % (stage, stage.id) + ) + ) for avis, alen in problems: - print(u" > Avis sur %s de %d mots" % \ - (avis.lieu, alen)) - self.stdout.write(self.style.SUCCESS(u'Nettoyage des stages effectué')) + print(u" > Avis sur %s de %d mots" % (avis.lieu, alen)) + self.stdout.write(self.style.SUCCESS(u"Nettoyage des stages effectué")) diff --git a/avisstage/management/commands/supprime_lieu.py b/avisstage/management/commands/supprime_lieu.py index 3b138b1..dd779a2 100644 --- a/avisstage/management/commands/supprime_lieu.py +++ b/avisstage/management/commands/supprime_lieu.py @@ -1,35 +1,42 @@ -#coding: utf-8 +# coding: utf-8 from django.core.management.base import BaseCommand, CommandError from django.db.models import Count from avisstage.models import Stage, Lieu + class Command(BaseCommand): - help = 'Nettoie les stages à plusieurs lieux identiques' + help = "Nettoie les stages à plusieurs lieux identiques" def add_arguments(self, parser): - parser.add_argument('del_lieu', type=int, help='Lieu à supprimer') - parser.add_argument('repl_lieu', type=int, help='Lieu le remplaçant') + parser.add_argument("del_lieu", type=int, help="Lieu à supprimer") + parser.add_argument("repl_lieu", type=int, help="Lieu le remplaçant") parser.add_argument( - '--apply', - action='store_true', + "--apply", + action="store_true", default=False, - help='Applies the modifications', + help="Applies the modifications", ) def handle(self, *args, **options): rundb = False - if options.get('apply', False): + if options.get("apply", False): rundb = True else: print(u"Les modifications ne seront pas appliquées") - plieu = Lieu.objects.get(id=options['del_lieu']) - lieu = Lieu.objects.get(id=options['repl_lieu']) - print(u"Suppression de %s (id=%d, %d avis)" % (plieu, plieu.id, plieu.avislieu_set.count())) - print(u"Remplacement par %s (id=%d, %d avis)" % (lieu, lieu.id, lieu.avislieu_set.count())) + plieu = Lieu.objects.get(id=options["del_lieu"]) + lieu = Lieu.objects.get(id=options["repl_lieu"]) + print( + u"Suppression de %s (id=%d, %d avis)" + % (plieu, plieu.id, plieu.avislieu_set.count()) + ) + print( + u"Remplacement par %s (id=%d, %d avis)" + % (lieu, lieu.id, lieu.avislieu_set.count()) + ) if rundb: for avis in plieu.avislieu_set.all(): avis.lieu = lieu avis.save() plieu.delete() - self.stdout.write(self.style.SUCCESS(u'Terminé')) + self.stdout.write(self.style.SUCCESS(u"Terminé")) diff --git a/avisstage/management/commands/termine_scolarite.py b/avisstage/management/commands/termine_scolarite.py index 6779604..aaf1e9e 100644 --- a/avisstage/management/commands/termine_scolarite.py +++ b/avisstage/management/commands/termine_scolarite.py @@ -4,6 +4,7 @@ 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' @@ -13,4 +14,4 @@ class Command(BaseCommand): 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é')) + self.stdout.write(self.style.SUCCESS(u"Terminé")) diff --git a/avisstage/migrations/0001_initial.py b/avisstage/migrations/0001_initial.py index a4325e1..a083c8d 100644 --- a/avisstage/migrations/0001_initial.py +++ b/avisstage/migrations/0001_initial.py @@ -16,127 +16,663 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('taggit', '0002_auto_20150616_2121'), + ("taggit", "0002_auto_20150616_2121"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='AvisLieu', + name="AvisLieu", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('order', models.IntegerField(default=0, verbose_name='Ordre')), - ('chapo', models.TextField(blank=True, verbose_name='En quelques mots')), - ('avis_lieustage', tinymce.models.HTMLField(blank=True, verbose_name='Les lieux de travail')), - ('avis_pratique', tinymce.models.HTMLField(blank=True, verbose_name="S'installer - conseils pratiques")), - ('avis_tourisme', tinymce.models.HTMLField(blank=True, verbose_name='Dans les parages')), - ('les_plus', models.TextField(blank=True, verbose_name='Les plus du lieu')), - ('les_moins', models.TextField(blank=True, verbose_name='Les moins du lieu')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("order", models.IntegerField(default=0, verbose_name="Ordre")), + ( + "chapo", + models.TextField(blank=True, verbose_name="En quelques mots"), + ), + ( + "avis_lieustage", + tinymce.models.HTMLField( + blank=True, verbose_name="Les lieux de travail" + ), + ), + ( + "avis_pratique", + tinymce.models.HTMLField( + blank=True, verbose_name="S'installer - conseils pratiques" + ), + ), + ( + "avis_tourisme", + tinymce.models.HTMLField( + blank=True, verbose_name="Dans les parages" + ), + ), + ( + "les_plus", + models.TextField(blank=True, verbose_name="Les plus du lieu"), + ), + ( + "les_moins", + models.TextField(blank=True, verbose_name="Les moins du lieu"), + ), ], options={ - 'verbose_name': 'Avis sur un lieu de stage', - 'verbose_name_plural': 'Avis sur un lieu de stage', + "verbose_name": "Avis sur un lieu de stage", + "verbose_name_plural": "Avis sur un lieu de stage", }, ), migrations.CreateModel( - name='AvisStage', + name="AvisStage", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('chapo', models.TextField(blank=True, verbose_name='En quelques mots')), - ('avis_ambiance', tinymce.models.HTMLField(blank=True, verbose_name="L'ambiance de travail")), - ('avis_sujet', tinymce.models.HTMLField(blank=True, verbose_name='La mission')), - ('avis_admin', tinymce.models.HTMLField(blank=True, verbose_name='Formalit\xe9s et administration')), - ('les_plus', models.TextField(blank=True, verbose_name='Les plus de cette exp\xe9rience')), - ('les_moins', models.TextField(blank=True, verbose_name='Les moins de cette exp\xe9rience')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "chapo", + models.TextField(blank=True, verbose_name="En quelques mots"), + ), + ( + "avis_ambiance", + tinymce.models.HTMLField( + blank=True, verbose_name="L'ambiance de travail" + ), + ), + ( + "avis_sujet", + tinymce.models.HTMLField(blank=True, verbose_name="La mission"), + ), + ( + "avis_admin", + tinymce.models.HTMLField( + blank=True, verbose_name="Formalit\xe9s et administration" + ), + ), + ( + "les_plus", + models.TextField( + blank=True, verbose_name="Les plus de cette exp\xe9rience" + ), + ), + ( + "les_moins", + models.TextField( + blank=True, verbose_name="Les moins de cette exp\xe9rience" + ), + ), ], ), migrations.CreateModel( - name='Lieu', + name="Lieu", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nom', models.CharField(max_length=250, verbose_name="Nom de l'institution d'accueil")), - ('type_lieu', models.CharField(choices=[(b'universite', 'Universit\xe9'), (b'entreprise', 'Entreprise'), (b'centrerecherche', 'Centre de recherche'), (b'administration', 'Administration'), (b'autre', 'Autre')], default='universite', max_length=15, verbose_name="Type de structure d'accueil")), - ('ville', models.CharField(max_length=200, verbose_name='Ville')), - ('pays', models.CharField(choices=[(b'AF', 'Afghanistan'), (b'AL', 'Albanie'), (b'AQ', 'Antarctique'), (b'DZ', 'Alg\xe9rie'), (b'AS', 'Samoa Am\xe9ricaines'), (b'AD', 'Andorre'), (b'AO', 'Angola'), (b'AG', 'Antigua-et-Barbuda'), (b'AZ', 'Azerba\xefdjan'), (b'AR', 'Argentine'), (b'AU', 'Australie'), (b'AT', 'Autriche'), (b'BS', 'Bahamas'), (b'BH', 'Bahre\xefn'), (b'BD', 'Bangladesh'), (b'AM', 'Arm\xe9nie'), (b'BB', 'Barbade'), (b'BE', 'Belgique'), (b'BM', 'Bermudes'), (b'BT', 'Bhoutan'), (b'BO', 'Bolivie'), (b'BA', 'Bosnie-Herz\xe9govine'), (b'BW', 'Botswana'), (b'BV', '\xcele Bouvet'), (b'BR', 'Br\xe9sil'), (b'BZ', 'Belize'), (b'IO', "Territoire Britannique de l'Oc\xe9an Indien"), (b'SB', '\xceles Salomon'), (b'VG', '\xceles Vierges Britanniques'), (b'BN', 'Brun\xe9i Darussalam'), (b'BG', 'Bulgarie'), (b'MM', 'Myanmar'), (b'BI', 'Burundi'), (b'BY', 'B\xe9larus'), (b'KH', 'Cambodge'), (b'CM', 'Cameroun'), (b'CA', 'Canada'), (b'CV', 'Cap-vert'), (b'KY', '\xceles Ca\xefmanes'), (b'CF', 'R\xe9publique Centrafricaine'), (b'LK', 'Sri Lanka'), (b'TD', 'Tchad'), (b'CL', 'Chili'), (b'CN', 'Chine'), (b'TW', 'Ta\xefwan'), (b'CX', '\xcele Christmas'), (b'CC', '\xceles Cocos (Keeling)'), (b'CO', 'Colombie'), (b'KM', 'Comores'), (b'YT', 'Mayotte'), (b'CG', 'R\xe9publique du Congo'), (b'CD', 'R\xe9publique D\xe9mocratique du Congo'), (b'CK', '\xceles Cook'), (b'CR', 'Costa Rica'), (b'HR', 'Croatie'), (b'CU', 'Cuba'), (b'CY', 'Chypre'), (b'CZ', 'R\xe9publique Tch\xe8que'), (b'BJ', 'B\xe9nin'), (b'DK', 'Danemark'), (b'DM', 'Dominique'), (b'DO', 'R\xe9publique Dominicaine'), (b'EC', '\xc9quateur'), (b'SV', 'El Salvador'), (b'GQ', 'Guin\xe9e \xc9quatoriale'), (b'ET', '\xc9thiopie'), (b'ER', '\xc9rythr\xe9e'), (b'EE', 'Estonie'), (b'FO', '\xceles F\xe9ro\xe9'), (b'FK', '\xceles (malvinas) Falkland'), (b'GS', 'G\xe9orgie du Sud et les \xceles Sandwich du Sud'), (b'FJ', 'Fidji'), (b'FI', 'Finlande'), (b'AX', '\xceles \xc5land'), (b'FR', 'France'), (b'GF', 'Guyane Fran\xe7aise'), (b'PF', 'Polyn\xe9sie Fran\xe7aise'), (b'TF', 'Terres Australes Fran\xe7aises'), (b'DJ', 'Djibouti'), (b'GA', 'Gabon'), (b'GE', 'G\xe9orgie'), (b'GM', 'Gambie'), (b'PS', 'Territoire Palestinien Occup\xe9'), (b'DE', 'Allemagne'), (b'GH', 'Ghana'), (b'GI', 'Gibraltar'), (b'KI', 'Kiribati'), (b'GR', 'Gr\xe8ce'), (b'GL', 'Groenland'), (b'GD', 'Grenade'), (b'GP', 'Guadeloupe'), (b'GU', 'Guam'), (b'GT', 'Guatemala'), (b'GN', 'Guin\xe9e'), (b'GY', 'Guyana'), (b'HT', 'Ha\xefti'), (b'HM', '\xceles Heard et Mcdonald'), (b'VA', 'Saint-Si\xe8ge (\xe9tat de la Cit\xe9 du Vatican)'), (b'HN', 'Honduras'), (b'HK', 'Hong-Kong'), (b'HU', 'Hongrie'), (b'IS', 'Islande'), (b'IN', 'Inde'), (b'ID', 'Indon\xe9sie'), (b'IR', "R\xe9publique Islamique d'Iran"), (b'IQ', 'Iraq'), (b'IE', 'Irlande'), (b'IL', 'Isra\xebl'), (b'IT', 'Italie'), (b'CI', "C\xf4te d'Ivoire"), (b'JM', 'Jama\xefque'), (b'JP', 'Japon'), (b'KZ', 'Kazakhstan'), (b'JO', 'Jordanie'), (b'KE', 'Kenya'), (b'KP', 'R\xe9publique Populaire D\xe9mocratique de Cor\xe9e'), (b'KR', 'R\xe9publique de Cor\xe9e'), (b'KW', 'Kowe\xeft'), (b'KG', 'Kirghizistan'), (b'LA', 'R\xe9publique D\xe9mocratique Populaire Lao'), (b'LB', 'Liban'), (b'LS', 'Lesotho'), (b'LV', 'Lettonie'), (b'LR', 'Lib\xe9ria'), (b'LY', 'Jamahiriya Arabe Libyenne'), (b'LI', 'Liechtenstein'), (b'LT', 'Lituanie'), (b'LU', 'Luxembourg'), (b'MO', 'Macao'), (b'MG', 'Madagascar'), (b'MW', 'Malawi'), (b'MY', 'Malaisie'), (b'MV', 'Maldives'), (b'ML', 'Mali'), (b'MT', 'Malte'), (b'MQ', 'Martinique'), (b'MR', 'Mauritanie'), (b'MU', 'Maurice'), (b'MX', 'Mexique'), (b'MC', 'Monaco'), (b'MN', 'Mongolie'), (b'MD', 'R\xe9publique de Moldova'), (b'MS', 'Montserrat'), (b'MA', 'Maroc'), (b'MZ', 'Mozambique'), (b'OM', 'Oman'), (b'NA', 'Namibie'), (b'NR', 'Nauru'), (b'NP', 'N\xe9pal'), (b'NL', 'Pays-Bas'), (b'AN', 'Antilles N\xe9erlandaises'), (b'AW', 'Aruba'), (b'NC', 'Nouvelle-Cal\xe9donie'), (b'VU', 'Vanuatu'), (b'NZ', 'Nouvelle-Z\xe9lande'), (b'NI', 'Nicaragua'), (b'NE', 'Niger'), (b'NG', 'Nig\xe9ria'), (b'NU', 'Niu\xe9'), (b'NF', '\xcele Norfolk'), (b'NO', 'Norv\xe8ge'), (b'MP', '\xceles Mariannes du Nord'), (b'UM', '\xceles Mineures \xc9loign\xe9es des \xc9tats-Unis'), (b'FM', '\xc9tats F\xe9d\xe9r\xe9s de Micron\xe9sie'), (b'MH', '\xceles Marshall'), (b'PW', 'Palaos'), (b'PK', 'Pakistan'), (b'PA', 'Panama'), (b'PG', 'Papouasie-Nouvelle-Guin\xe9e'), (b'PY', 'Paraguay'), (b'PE', 'P\xe9rou'), (b'PH', 'Philippines'), (b'PN', 'Pitcairn'), (b'PL', 'Pologne'), (b'PT', 'Portugal'), (b'GW', 'Guin\xe9e-Bissau'), (b'TL', 'Timor-Leste'), (b'PR', 'Porto Rico'), (b'QA', 'Qatar'), (b'RE', 'R\xe9union'), (b'RO', 'Roumanie'), (b'RU', 'F\xe9d\xe9ration de Russie'), (b'RW', 'Rwanda'), (b'SH', 'Sainte-H\xe9l\xe8ne'), (b'KN', 'Saint-Kitts-et-Nevis'), (b'AI', 'Anguilla'), (b'LC', 'Sainte-Lucie'), (b'PM', 'Saint-Pierre-et-Miquelon'), (b'VC', 'Saint-Vincent-et-les Grenadines'), (b'SM', 'Saint-Marin'), (b'ST', 'Sao Tom\xe9-et-Principe'), (b'SA', 'Arabie Saoudite'), (b'SN', 'S\xe9n\xe9gal'), (b'SC', 'Seychelles'), (b'SL', 'Sierra Leone'), (b'SG', 'Singapour'), (b'SK', 'Slovaquie'), (b'VN', 'Viet Nam'), (b'SI', 'Slov\xe9nie'), (b'SO', 'Somalie'), (b'ZA', 'Afrique du Sud'), (b'ZW', 'Zimbabwe'), (b'ES', 'Espagne'), (b'EH', 'Sahara Occidental'), (b'SD', 'Soudan'), (b'SR', 'Suriname'), (b'SJ', 'Svalbard et\xcele Jan Mayen'), (b'SZ', 'Swaziland'), (b'SE', 'Su\xe8de'), (b'CH', 'Suisse'), (b'SY', 'R\xe9publique Arabe Syrienne'), (b'TJ', 'Tadjikistan'), (b'TH', 'Tha\xeflande'), (b'TG', 'Togo'), (b'TK', 'Tokelau'), (b'TO', 'Tonga'), (b'TT', 'Trinit\xe9-et-Tobago'), (b'AE', '\xc9mirats Arabes Unis'), (b'TN', 'Tunisie'), (b'TR', 'Turquie'), (b'TM', 'Turkm\xe9nistan'), (b'TC', '\xceles Turks et Ca\xefques'), (b'TV', 'Tuvalu'), (b'UG', 'Ouganda'), (b'UA', 'Ukraine'), (b'MK', "L'ex-R\xe9publique Yougoslave de Mac\xe9doine"), (b'EG', '\xc9gypte'), (b'GB', 'Royaume-Uni'), (b'IM', '\xcele de Man'), (b'TZ', 'R\xe9publique-Unie de Tanzanie'), (b'US', '\xc9tats-Unis'), (b'VI', '\xceles Vierges des \xc9tats-Unis'), (b'BF', 'Burkina Faso'), (b'UY', 'Uruguay'), (b'UZ', 'Ouzb\xe9kistan'), (b'VE', 'Venezuela'), (b'WF', 'Wallis et Futuna'), (b'WS', 'Samoa'), (b'YE', 'Y\xe9men'), (b'CS', 'Serbie-et-Mont\xe9n\xe9gro'), (b'ZM', 'Zambie')], max_length=2, verbose_name='Pays')), - ('coord', django.contrib.gis.db.models.fields.PointField(geography=True, srid=4326, verbose_name='Coordonn\xe9es')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "nom", + models.CharField( + max_length=250, verbose_name="Nom de l'institution d'accueil" + ), + ), + ( + "type_lieu", + models.CharField( + choices=[ + (b"universite", "Universit\xe9"), + (b"entreprise", "Entreprise"), + (b"centrerecherche", "Centre de recherche"), + (b"administration", "Administration"), + (b"autre", "Autre"), + ], + default="universite", + max_length=15, + verbose_name="Type de structure d'accueil", + ), + ), + ("ville", models.CharField(max_length=200, verbose_name="Ville")), + ( + "pays", + models.CharField( + choices=[ + (b"AF", "Afghanistan"), + (b"AL", "Albanie"), + (b"AQ", "Antarctique"), + (b"DZ", "Alg\xe9rie"), + (b"AS", "Samoa Am\xe9ricaines"), + (b"AD", "Andorre"), + (b"AO", "Angola"), + (b"AG", "Antigua-et-Barbuda"), + (b"AZ", "Azerba\xefdjan"), + (b"AR", "Argentine"), + (b"AU", "Australie"), + (b"AT", "Autriche"), + (b"BS", "Bahamas"), + (b"BH", "Bahre\xefn"), + (b"BD", "Bangladesh"), + (b"AM", "Arm\xe9nie"), + (b"BB", "Barbade"), + (b"BE", "Belgique"), + (b"BM", "Bermudes"), + (b"BT", "Bhoutan"), + (b"BO", "Bolivie"), + (b"BA", "Bosnie-Herz\xe9govine"), + (b"BW", "Botswana"), + (b"BV", "\xcele Bouvet"), + (b"BR", "Br\xe9sil"), + (b"BZ", "Belize"), + (b"IO", "Territoire Britannique de l'Oc\xe9an Indien"), + (b"SB", "\xceles Salomon"), + (b"VG", "\xceles Vierges Britanniques"), + (b"BN", "Brun\xe9i Darussalam"), + (b"BG", "Bulgarie"), + (b"MM", "Myanmar"), + (b"BI", "Burundi"), + (b"BY", "B\xe9larus"), + (b"KH", "Cambodge"), + (b"CM", "Cameroun"), + (b"CA", "Canada"), + (b"CV", "Cap-vert"), + (b"KY", "\xceles Ca\xefmanes"), + (b"CF", "R\xe9publique Centrafricaine"), + (b"LK", "Sri Lanka"), + (b"TD", "Tchad"), + (b"CL", "Chili"), + (b"CN", "Chine"), + (b"TW", "Ta\xefwan"), + (b"CX", "\xcele Christmas"), + (b"CC", "\xceles Cocos (Keeling)"), + (b"CO", "Colombie"), + (b"KM", "Comores"), + (b"YT", "Mayotte"), + (b"CG", "R\xe9publique du Congo"), + (b"CD", "R\xe9publique D\xe9mocratique du Congo"), + (b"CK", "\xceles Cook"), + (b"CR", "Costa Rica"), + (b"HR", "Croatie"), + (b"CU", "Cuba"), + (b"CY", "Chypre"), + (b"CZ", "R\xe9publique Tch\xe8que"), + (b"BJ", "B\xe9nin"), + (b"DK", "Danemark"), + (b"DM", "Dominique"), + (b"DO", "R\xe9publique Dominicaine"), + (b"EC", "\xc9quateur"), + (b"SV", "El Salvador"), + (b"GQ", "Guin\xe9e \xc9quatoriale"), + (b"ET", "\xc9thiopie"), + (b"ER", "\xc9rythr\xe9e"), + (b"EE", "Estonie"), + (b"FO", "\xceles F\xe9ro\xe9"), + (b"FK", "\xceles (malvinas) Falkland"), + (b"GS", "G\xe9orgie du Sud et les \xceles Sandwich du Sud"), + (b"FJ", "Fidji"), + (b"FI", "Finlande"), + (b"AX", "\xceles \xc5land"), + (b"FR", "France"), + (b"GF", "Guyane Fran\xe7aise"), + (b"PF", "Polyn\xe9sie Fran\xe7aise"), + (b"TF", "Terres Australes Fran\xe7aises"), + (b"DJ", "Djibouti"), + (b"GA", "Gabon"), + (b"GE", "G\xe9orgie"), + (b"GM", "Gambie"), + (b"PS", "Territoire Palestinien Occup\xe9"), + (b"DE", "Allemagne"), + (b"GH", "Ghana"), + (b"GI", "Gibraltar"), + (b"KI", "Kiribati"), + (b"GR", "Gr\xe8ce"), + (b"GL", "Groenland"), + (b"GD", "Grenade"), + (b"GP", "Guadeloupe"), + (b"GU", "Guam"), + (b"GT", "Guatemala"), + (b"GN", "Guin\xe9e"), + (b"GY", "Guyana"), + (b"HT", "Ha\xefti"), + (b"HM", "\xceles Heard et Mcdonald"), + ( + b"VA", + "Saint-Si\xe8ge (\xe9tat de la Cit\xe9 du Vatican)", + ), + (b"HN", "Honduras"), + (b"HK", "Hong-Kong"), + (b"HU", "Hongrie"), + (b"IS", "Islande"), + (b"IN", "Inde"), + (b"ID", "Indon\xe9sie"), + (b"IR", "R\xe9publique Islamique d'Iran"), + (b"IQ", "Iraq"), + (b"IE", "Irlande"), + (b"IL", "Isra\xebl"), + (b"IT", "Italie"), + (b"CI", "C\xf4te d'Ivoire"), + (b"JM", "Jama\xefque"), + (b"JP", "Japon"), + (b"KZ", "Kazakhstan"), + (b"JO", "Jordanie"), + (b"KE", "Kenya"), + ( + b"KP", + "R\xe9publique Populaire D\xe9mocratique de Cor\xe9e", + ), + (b"KR", "R\xe9publique de Cor\xe9e"), + (b"KW", "Kowe\xeft"), + (b"KG", "Kirghizistan"), + (b"LA", "R\xe9publique D\xe9mocratique Populaire Lao"), + (b"LB", "Liban"), + (b"LS", "Lesotho"), + (b"LV", "Lettonie"), + (b"LR", "Lib\xe9ria"), + (b"LY", "Jamahiriya Arabe Libyenne"), + (b"LI", "Liechtenstein"), + (b"LT", "Lituanie"), + (b"LU", "Luxembourg"), + (b"MO", "Macao"), + (b"MG", "Madagascar"), + (b"MW", "Malawi"), + (b"MY", "Malaisie"), + (b"MV", "Maldives"), + (b"ML", "Mali"), + (b"MT", "Malte"), + (b"MQ", "Martinique"), + (b"MR", "Mauritanie"), + (b"MU", "Maurice"), + (b"MX", "Mexique"), + (b"MC", "Monaco"), + (b"MN", "Mongolie"), + (b"MD", "R\xe9publique de Moldova"), + (b"MS", "Montserrat"), + (b"MA", "Maroc"), + (b"MZ", "Mozambique"), + (b"OM", "Oman"), + (b"NA", "Namibie"), + (b"NR", "Nauru"), + (b"NP", "N\xe9pal"), + (b"NL", "Pays-Bas"), + (b"AN", "Antilles N\xe9erlandaises"), + (b"AW", "Aruba"), + (b"NC", "Nouvelle-Cal\xe9donie"), + (b"VU", "Vanuatu"), + (b"NZ", "Nouvelle-Z\xe9lande"), + (b"NI", "Nicaragua"), + (b"NE", "Niger"), + (b"NG", "Nig\xe9ria"), + (b"NU", "Niu\xe9"), + (b"NF", "\xcele Norfolk"), + (b"NO", "Norv\xe8ge"), + (b"MP", "\xceles Mariannes du Nord"), + ( + b"UM", + "\xceles Mineures \xc9loign\xe9es des \xc9tats-Unis", + ), + (b"FM", "\xc9tats F\xe9d\xe9r\xe9s de Micron\xe9sie"), + (b"MH", "\xceles Marshall"), + (b"PW", "Palaos"), + (b"PK", "Pakistan"), + (b"PA", "Panama"), + (b"PG", "Papouasie-Nouvelle-Guin\xe9e"), + (b"PY", "Paraguay"), + (b"PE", "P\xe9rou"), + (b"PH", "Philippines"), + (b"PN", "Pitcairn"), + (b"PL", "Pologne"), + (b"PT", "Portugal"), + (b"GW", "Guin\xe9e-Bissau"), + (b"TL", "Timor-Leste"), + (b"PR", "Porto Rico"), + (b"QA", "Qatar"), + (b"RE", "R\xe9union"), + (b"RO", "Roumanie"), + (b"RU", "F\xe9d\xe9ration de Russie"), + (b"RW", "Rwanda"), + (b"SH", "Sainte-H\xe9l\xe8ne"), + (b"KN", "Saint-Kitts-et-Nevis"), + (b"AI", "Anguilla"), + (b"LC", "Sainte-Lucie"), + (b"PM", "Saint-Pierre-et-Miquelon"), + (b"VC", "Saint-Vincent-et-les Grenadines"), + (b"SM", "Saint-Marin"), + (b"ST", "Sao Tom\xe9-et-Principe"), + (b"SA", "Arabie Saoudite"), + (b"SN", "S\xe9n\xe9gal"), + (b"SC", "Seychelles"), + (b"SL", "Sierra Leone"), + (b"SG", "Singapour"), + (b"SK", "Slovaquie"), + (b"VN", "Viet Nam"), + (b"SI", "Slov\xe9nie"), + (b"SO", "Somalie"), + (b"ZA", "Afrique du Sud"), + (b"ZW", "Zimbabwe"), + (b"ES", "Espagne"), + (b"EH", "Sahara Occidental"), + (b"SD", "Soudan"), + (b"SR", "Suriname"), + (b"SJ", "Svalbard et\xcele Jan Mayen"), + (b"SZ", "Swaziland"), + (b"SE", "Su\xe8de"), + (b"CH", "Suisse"), + (b"SY", "R\xe9publique Arabe Syrienne"), + (b"TJ", "Tadjikistan"), + (b"TH", "Tha\xeflande"), + (b"TG", "Togo"), + (b"TK", "Tokelau"), + (b"TO", "Tonga"), + (b"TT", "Trinit\xe9-et-Tobago"), + (b"AE", "\xc9mirats Arabes Unis"), + (b"TN", "Tunisie"), + (b"TR", "Turquie"), + (b"TM", "Turkm\xe9nistan"), + (b"TC", "\xceles Turks et Ca\xefques"), + (b"TV", "Tuvalu"), + (b"UG", "Ouganda"), + (b"UA", "Ukraine"), + (b"MK", "L'ex-R\xe9publique Yougoslave de Mac\xe9doine"), + (b"EG", "\xc9gypte"), + (b"GB", "Royaume-Uni"), + (b"IM", "\xcele de Man"), + (b"TZ", "R\xe9publique-Unie de Tanzanie"), + (b"US", "\xc9tats-Unis"), + (b"VI", "\xceles Vierges des \xc9tats-Unis"), + (b"BF", "Burkina Faso"), + (b"UY", "Uruguay"), + (b"UZ", "Ouzb\xe9kistan"), + (b"VE", "Venezuela"), + (b"WF", "Wallis et Futuna"), + (b"WS", "Samoa"), + (b"YE", "Y\xe9men"), + (b"CS", "Serbie-et-Mont\xe9n\xe9gro"), + (b"ZM", "Zambie"), + ], + max_length=2, + verbose_name="Pays", + ), + ), + ( + "coord", + django.contrib.gis.db.models.fields.PointField( + geography=True, srid=4326, verbose_name="Coordonn\xe9es" + ), + ), ], options={ - 'verbose_name': 'Lieu', - 'verbose_name_plural': 'Lieux', + "verbose_name": "Lieu", + "verbose_name_plural": "Lieux", }, ), migrations.CreateModel( - name='Normalien', + name="Normalien", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nom', models.CharField(blank=True, max_length=255, verbose_name='Nom complet')), - ('promotion', models.CharField(blank=True, max_length=40, verbose_name='Promotion')), - ('mail', models.EmailField(blank=True, max_length=200, verbose_name='Adresse e-mail permanente')), - ('contactez_moi', models.BooleanField(default=True, verbose_name='Inviter les visiteurs \xe0 me contacter')), - ('bio', models.TextField(blank=True, default='', verbose_name='\xc0 propos de moi')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profil', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "nom", + models.CharField( + blank=True, max_length=255, verbose_name="Nom complet" + ), + ), + ( + "promotion", + models.CharField( + blank=True, max_length=40, verbose_name="Promotion" + ), + ), + ( + "mail", + models.EmailField( + blank=True, + max_length=200, + verbose_name="Adresse e-mail permanente", + ), + ), + ( + "contactez_moi", + models.BooleanField( + default=True, + verbose_name="Inviter les visiteurs \xe0 me contacter", + ), + ), + ( + "bio", + models.TextField( + blank=True, default="", verbose_name="\xc0 propos de moi" + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="profil", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Profil \xe9l\xe8ve', - 'verbose_name_plural': 'Profils \xe9l\xe8ves', + "verbose_name": "Profil \xe9l\xe8ve", + "verbose_name_plural": "Profils \xe9l\xe8ves", }, ), migrations.CreateModel( - name='Stage', + name="Stage", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('public', models.BooleanField(default=False, verbose_name='Visible publiquement')), - ('date_creation', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Cr\xe9\xe9 le')), - ('date_maj', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Mis \xe0 jour le')), - ('sujet', models.CharField(max_length=500, verbose_name='Sujet')), - ('date_debut', models.DateField(null=True, verbose_name='Date de d\xe9but')), - ('date_fin', models.DateField(null=True, verbose_name='Date de fin')), - ('type_stage', models.CharField(choices=[('Recherche :', ((b'recherche', 'Stage acad\xe9mique'), (b'recherche_autre', 'Stage non-acad\xe9mique'), (b'sejour_dri', 'S\xe9jour de recherche DRI'))), ('Stage sans vis\xe9e de recherche :', ((b'pro', 'Stage en entreprise'), (b'admin', 'Stage en admin./ONG/orga. internationale'))), ('Enseignement :', ((b'lectorat', 'Lectorat DRI'), (b'autre_teach', "Autre exp\xe9rience d'enseignement"))), (b'autre', 'Autre')], default='stage', max_length=31, verbose_name='Type')), - ('niveau_scol', models.CharField(blank=True, choices=[(b'L3', 'Licence 3'), (b'M1', 'Master 1'), (b'M2', 'Master 2'), (b'DOC', 'Pr\xe9-doctorat'), (b'CST', 'C\xe9sure'), (b'BLA', 'Ann\xe9e blanche'), (b'VAC', 'Vacances scolaires'), (b'MIT', 'Mi-temps en parall\xe8le des \xe9tudes'), (b'', 'Autre')], default='', max_length=3, verbose_name='Ann\xe9e de scolarit\xe9')), - ('encadrants', models.CharField(blank=True, max_length=500, verbose_name='Encadrant\u22c5e\u22c5s')), - ('structure', models.CharField(blank=True, max_length=500, verbose_name="Structure d'accueil")), - ('auteur', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='avisstage.Normalien')), - ('lieux', models.ManyToManyField(blank=True, related_name='stages', through='avisstage.AvisLieu', to='avisstage.Lieu')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "public", + models.BooleanField( + default=False, verbose_name="Visible publiquement" + ), + ), + ( + "date_creation", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="Cr\xe9\xe9 le" + ), + ), + ( + "date_maj", + models.DateTimeField( + default=django.utils.timezone.now, + verbose_name="Mis \xe0 jour le", + ), + ), + ("sujet", models.CharField(max_length=500, verbose_name="Sujet")), + ( + "date_debut", + models.DateField(null=True, verbose_name="Date de d\xe9but"), + ), + ("date_fin", models.DateField(null=True, verbose_name="Date de fin")), + ( + "type_stage", + models.CharField( + choices=[ + ( + "Recherche :", + ( + (b"recherche", "Stage acad\xe9mique"), + (b"recherche_autre", "Stage non-acad\xe9mique"), + (b"sejour_dri", "S\xe9jour de recherche DRI"), + ), + ), + ( + "Stage sans vis\xe9e de recherche :", + ( + (b"pro", "Stage en entreprise"), + ( + b"admin", + "Stage en admin./ONG/orga. internationale", + ), + ), + ), + ( + "Enseignement :", + ( + (b"lectorat", "Lectorat DRI"), + ( + b"autre_teach", + "Autre exp\xe9rience d'enseignement", + ), + ), + ), + (b"autre", "Autre"), + ], + default="stage", + max_length=31, + verbose_name="Type", + ), + ), + ( + "niveau_scol", + models.CharField( + blank=True, + choices=[ + (b"L3", "Licence 3"), + (b"M1", "Master 1"), + (b"M2", "Master 2"), + (b"DOC", "Pr\xe9-doctorat"), + (b"CST", "C\xe9sure"), + (b"BLA", "Ann\xe9e blanche"), + (b"VAC", "Vacances scolaires"), + (b"MIT", "Mi-temps en parall\xe8le des \xe9tudes"), + (b"", "Autre"), + ], + default="", + max_length=3, + verbose_name="Ann\xe9e de scolarit\xe9", + ), + ), + ( + "encadrants", + models.CharField( + blank=True, + max_length=500, + verbose_name="Encadrant\u22c5e\u22c5s", + ), + ), + ( + "structure", + models.CharField( + blank=True, max_length=500, verbose_name="Structure d'accueil" + ), + ), + ( + "auteur", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="stages", + to="avisstage.Normalien", + ), + ), + ( + "lieux", + models.ManyToManyField( + blank=True, + related_name="stages", + through="avisstage.AvisLieu", + to="avisstage.Lieu", + ), + ), ], options={ - 'verbose_name': 'Stage', + "verbose_name": "Stage", }, ), migrations.CreateModel( - name='StageMatiere', + name="StageMatiere", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nom', models.CharField(max_length=30, verbose_name='Nom')), - ('slug', models.SlugField()), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("nom", models.CharField(max_length=30, verbose_name="Nom")), + ("slug", models.SlugField()), ], options={ - 'verbose_name': 'Mati\xe8re des stages', - 'verbose_name_plural': 'Mati\xe8res des stages', + "verbose_name": "Mati\xe8re des stages", + "verbose_name_plural": "Mati\xe8res des stages", }, ), migrations.AddField( - model_name='stage', - name='matieres', - field=models.ManyToManyField(related_name='stages', to='avisstage.StageMatiere', verbose_name='Mati\xe8re(s)'), + model_name="stage", + name="matieres", + field=models.ManyToManyField( + related_name="stages", + to="avisstage.StageMatiere", + verbose_name="Mati\xe8re(s)", + ), ), migrations.AddField( - model_name='stage', - name='thematiques', - field=taggit_autosuggest.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Th\xe9matiques'), + model_name="stage", + name="thematiques", + field=taggit_autosuggest.managers.TaggableManager( + blank=True, + help_text="A comma-separated list of tags.", + through="taggit.TaggedItem", + to="taggit.Tag", + verbose_name="Th\xe9matiques", + ), ), migrations.AddField( - model_name='avisstage', - name='stage', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='avis_stage', to='avisstage.Stage'), + model_name="avisstage", + name="stage", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="avis_stage", + to="avisstage.Stage", + ), ), migrations.AddField( - model_name='avislieu', - name='lieu', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='avisstage.Lieu'), + model_name="avislieu", + name="lieu", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="avisstage.Lieu" + ), ), migrations.AddField( - model_name='avislieu', - name='stage', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='avisstage.Stage'), + model_name="avislieu", + name="stage", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="avisstage.Stage" + ), ), ] diff --git a/avisstage/migrations/0002_auto_20171002_2243.py b/avisstage/migrations/0002_auto_20171002_2243.py index 59c0ea4..bdfefce 100644 --- a/avisstage/migrations/0002_auto_20171002_2243.py +++ b/avisstage/migrations/0002_auto_20171002_2243.py @@ -9,23 +9,29 @@ import tinymce.models class Migration(migrations.Migration): dependencies = [ - ('avisstage', '0001_initial'), + ("avisstage", "0001_initial"), ] operations = [ migrations.AddField( - model_name='avisstage', - name='avis_prestage', - field=tinymce.models.HTMLField(blank=True, default='', verbose_name='Avant le stage'), + model_name="avisstage", + name="avis_prestage", + field=tinymce.models.HTMLField( + blank=True, default="", verbose_name="Avant le stage" + ), ), migrations.AddField( - model_name='stage', - name='len_avis_lieux', - field=models.IntegerField(default=0, verbose_name='Longueur des avis de lieu'), + model_name="stage", + name="len_avis_lieux", + field=models.IntegerField( + default=0, verbose_name="Longueur des avis de lieu" + ), ), migrations.AddField( - model_name='stage', - name='len_avis_stage', - field=models.IntegerField(default=0, verbose_name='Longueur des avis de stage'), + model_name="stage", + name="len_avis_stage", + field=models.IntegerField( + default=0, verbose_name="Longueur des avis de stage" + ), ), ] diff --git a/avisstage/migrations/0003_auto_20210117_1208.py b/avisstage/migrations/0003_auto_20210117_1208.py index 307760c..4d13ea9 100644 --- a/avisstage/migrations/0003_auto_20210117_1208.py +++ b/avisstage/migrations/0003_auto_20210117_1208.py @@ -8,38 +8,350 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('avisstage', '0002_auto_20171002_2243'), + ("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'), + 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"), + 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), + 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'), + 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é'), + 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'), + 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 index 9de7655..29f8f8b 100644 --- a/avisstage/migrations/0004_allauth_to_authens.py +++ b/avisstage/migrations/0004_allauth_to_authens.py @@ -2,18 +2,19 @@ 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') - + User = apps.get_model("auth", "User") + try: - CASAccount = apps.get_model('authens', 'CASAccount') + CASAccount = apps.get_model("authens", "CASAccount") except LookupError: return try: - SocialAccount = apps.get_model('socialaccount', 'SocialAccount') - OldEmailAddress = apps.get_model('account', 'EmailAddress') + SocialAccount = apps.get_model("socialaccount", "SocialAccount") + OldEmailAddress = apps.get_model("account", "EmailAddress") except LookupError: # Allauth not installed # Simply create CAS accounts for every profile @@ -25,29 +26,26 @@ def forwards(apps, schema_editor): if ldap_info: entrance_year = ldap_info["entrance_year"] CASAccount.objects.create( - user=user, cas_login=user.username, - entrance_year=entrance_year + 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') + 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") + 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"))) + mail is not None and (mail.endswith("ens.fr") or mail.endswith("ens.psl.eu")) + ) new_conns = [] new_mails = [] @@ -56,14 +54,14 @@ def forwards(apps, schema_editor): addresses = user.emailaddress_set.all() for addr in addresses: newaddr = NewEmailAddress( - user=user, email=addr.email, + 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) + print("Adresse principale inconsistante", user.email, addr.email) new_mails.append(newaddr) # Create new CASAccount connexion @@ -78,30 +76,33 @@ def forwards(apps, schema_editor): print(user.username) continue entrance_year = saccount.extra_data.get( - "entrance_year", user.username.split("@")[1]) + "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))) + 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'), + ("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("allauth"): + dependencies.append(("socialaccount", "0003_extra_data_default_dict")) - if global_apps.is_installed('simple_email_confirmation'): - dependencies.append(('simple_email_confirmation', '0001_initial')) + 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 index da85767..9ec2a88 100644 --- a/avisstage/migrations/0005_normalien_en_scolarite.py +++ b/avisstage/migrations/0005_normalien_en_scolarite.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('avisstage', '0004_allauth_to_authens'), + ("avisstage", "0004_allauth_to_authens"), ] operations = [ migrations.AddField( - model_name='normalien', - name='en_scolarite', + 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 index 1ea3ac5..c940e17 100644 --- a/avisstage/migrations/0006_auto_20210131_1954.py +++ b/avisstage/migrations/0006_auto_20210131_1954.py @@ -7,26 +7,30 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('avisstage', '0005_normalien_en_scolarite'), + ("avisstage", "0005_normalien_en_scolarite"), ] operations = [ migrations.RemoveField( - model_name='normalien', - name='en_scolarite', + model_name="normalien", + name="en_scolarite", ), migrations.RemoveField( - model_name='normalien', - name='mail', + model_name="normalien", + name="mail", ), migrations.AddField( - model_name='normalien', - name='last_cas_login', + 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'), + 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 1b63c8d..9439748 100644 --- a/avisstage/models.py +++ b/avisstage/models.py @@ -20,27 +20,39 @@ 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 + 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() + 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", - on_delete=models.SET_NULL, null=True) + 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) contactez_moi = models.BooleanField( u"Inviter les visiteurs à me contacter", - default=True, help_text="Affiche votre adresse e-mail principale sur votre profil public") + 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) @@ -53,12 +65,11 @@ class Normalien(models.Model): # Liste des stages publiés def stages_publics(self): - return self.stages.filter(public=True).order_by('-date_debut') + return self.stages.filter(public=True).order_by("-date_debut") def has_nonENS_email(self): return ( - self.user.email_address_set - .exclude(confirmed_at__isnull=True) + self.user.email_address_set.exclude(confirmed_at__isnull=True) .exclude(email__endswith="ens.fr") .exclude(email__endswith="ens.psl.eu") .exists() @@ -77,18 +88,20 @@ class Normalien(models.Model): def preferred_email(self): return self.user.email + # 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) - + if not created and profil.promotion != "": return - + if "@" in instance.username: profil.promotion = instance.username.split("@")[1] profil.save() + post_save.connect(create_basic_user_profile, sender=User) # Hook d'authENS : information du CAS @@ -104,7 +117,7 @@ def handle_cas_connection(sender, instance, created, cas_login, attributes, **kw if len(dirs) < 4: print("HomeDirectory invalide", dirs) return - + year = dirs[2] departement = dirs[3] @@ -114,33 +127,33 @@ def handle_cas_connection(sender, instance, created, cas_login, attributes, **kw profil.nom = attributes.get("name", "") profil.save() + post_cas_connect.connect(handle_cas_connection, sender=User) # # Lieu de stage # + class Lieu(models.Model): # Général - nom = models.CharField(u"Nom de l'institution d'accueil", - max_length=250) - type_lieu = models.CharField(u"Type de structure d'accueil", - default="universite", - choices=TYPE_LIEU_OPTIONS, - max_length=choices_length(TYPE_LIEU_OPTIONS)) + nom = models.CharField(u"Nom de l'institution d'accueil", max_length=250) + type_lieu = models.CharField( + u"Type de structure d'accueil", + default="universite", + choices=TYPE_LIEU_OPTIONS, + max_length=choices_length(TYPE_LIEU_OPTIONS), + ) # Infos géographiques - ville = models.CharField(u"Ville", - max_length=200) - pays = models.CharField(u"Pays", - choices=PAYS_OPTIONS, - max_length=choices_length(PAYS_OPTIONS)) - + ville = models.CharField(u"Ville", max_length=200) + pays = models.CharField( + u"Pays", choices=PAYS_OPTIONS, max_length=choices_length(PAYS_OPTIONS) + ) + # Coordonnées - #objects = geomodels.GeoManager() # Requis par GeoDjango - coord = geomodels.PointField(u"Coordonnées", - geography=True, - srid = 4326) + # objects = geomodels.GeoManager() # Requis par GeoDjango + coord = geomodels.PointField(u"Coordonnées", geography=True, srid=4326) # Type du lieu en plus joli @property @@ -153,19 +166,21 @@ class Lieu(models.Model): def __str__(self): return u"%s (%s)" % (self.nom, self.ville) - + class Meta: verbose_name = "Lieu" verbose_name_plural = "Lieux" - + + # # Matières des stages # + class StageMatiere(models.Model): nom = models.CharField(u"Nom", max_length=30) slug = models.SlugField() - + class Meta: verbose_name = "Matière des stages" verbose_name_plural = "Matières des stages" @@ -173,14 +188,17 @@ class StageMatiere(models.Model): def __str__(self): return self.nom + # # Un stage # + class Stage(models.Model): # Misc - auteur = models.ForeignKey(Normalien, related_name="stages", - on_delete=models.SET_NULL, null=True) + 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) @@ -193,29 +211,36 @@ class Stage(models.Model): date_debut = models.DateField(u"Date de début", null=True) date_fin = models.DateField(u"Date de fin", null=True) - type_stage = models.CharField(u"Type", - default="stage", - choices=TYPE_STAGE_OPTIONS, - max_length=choices_length(TYPE_STAGE_OPTIONS)) - niveau_scol = models.CharField(u"Année de scolarité", - default="", - choices=NIVEAU_SCOL_OPTIONS, - max_length=choices_length(NIVEAU_SCOL_OPTIONS), - blank=True) + type_stage = models.CharField( + u"Type", + default="stage", + choices=TYPE_STAGE_OPTIONS, + max_length=choices_length(TYPE_STAGE_OPTIONS), + ) + niveau_scol = models.CharField( + u"Année de scolarité", + default="", + choices=NIVEAU_SCOL_OPTIONS, + max_length=choices_length(NIVEAU_SCOL_OPTIONS), + blank=True, + ) thematiques = TaggableManager(u"Thématiques", blank=True) - matieres = models.ManyToManyField(StageMatiere, verbose_name=u"Matière(s)", related_name="stages") + matieres = models.ManyToManyField( + StageMatiere, verbose_name=u"Matière(s)", related_name="stages" + ) encadrants = models.CharField(u"Encadrant⋅e⋅s", max_length=500, blank=True) structure = models.CharField(u"Structure d'accueil", max_length=500, blank=True) # Avis - lieux = models.ManyToManyField(Lieu, related_name="stages", - through="AvisLieu", blank=True) + lieux = models.ManyToManyField( + Lieu, related_name="stages", through="AvisLieu", blank=True + ) # Affichage des avis ordonnés @property def avis_lieux(self): - return self.avislieu_set.order_by('order') + return self.avislieu_set.order_by("order") # Shortcut pour affichage rapide @property @@ -229,6 +254,7 @@ class Stage(models.Model): @property def type_stage_fancy(self): return TYPE_STAGE_DICT.get(self.type_stage, ("stage", False))[0] + @property def type_stage_fem(self): return TYPE_STAGE_DICT.get(self.type_stage, ("stage", False))[1] @@ -244,8 +270,8 @@ class Stage(models.Model): return self.lieux.all() def get_absolute_url(self): - return reverse('avisstage:stage', self) - + return reverse("avisstage:stage", self) + def __str__(self): return u"%s (par %s)" % (self.sujet, self.auteur.user.username) @@ -259,26 +285,29 @@ class Stage(models.Model): length += len(obj.les_plus.split()) length += len(obj.les_moins.split()) return length - + if self.avis_stage: self.len_avis_stage = get_len(self.avis_stage) self.len_avis_lieux = 0 for avis in self.avislieu_set.all(): self.len_avis_lieux += get_len(avis) - + if save: self.save() - + class Meta: verbose_name = "Stage" + # # Les avis # + class AvisStage(models.Model): - stage = models.OneToOneField(Stage, related_name="avis_stage", - on_delete=models.CASCADE) + 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) @@ -290,14 +319,19 @@ class AvisStage(models.Model): les_moins = models.TextField(u"Les moins de cette expérience", blank=True) def __str__(self): - return u"Avis sur {%s} par %s" % (self.stage.sujet, self.stage.auteur.user.username) + return u"Avis sur {%s} par %s" % ( + self.stage.sujet, + self.stage.auteur.user.username, + ) # Liste des champs d'avis, couplés à leur nom (pour l'affichage) @property def avis_all(self): - fields = ['avis_sujet', 'avis_ambiance', 'avis_admin', 'avis_prestage'] - return [(AvisStage._meta.get_field(field).verbose_name, - getattr(self, field, '')) for field in fields] + fields = ["avis_sujet", "avis_ambiance", "avis_admin", "avis_prestage"] + return [ + (AvisStage._meta.get_field(field).verbose_name, getattr(self, field, "")) + for field in fields + ] class AvisLieu(models.Model): @@ -307,8 +341,7 @@ class AvisLieu(models.Model): chapo = models.TextField(u"En quelques mots", blank=True) avis_lieustage = RichTextField(u"Les lieux de travail", blank=True) - avis_pratique = RichTextField(u"S'installer - conseils pratiques", - blank=True) + avis_pratique = RichTextField(u"S'installer - conseils pratiques", blank=True) avis_tourisme = RichTextField(u"Dans les parages", blank=True) les_plus = models.TextField(u"Les plus du lieu", blank=True) @@ -324,6 +357,8 @@ class AvisLieu(models.Model): # Liste des champs d'avis, couplés à leur nom (pour l'affichage) @property def avis_all(self): - fields = ['avis_lieustage', 'avis_pratique', 'avis_tourisme'] - return [(AvisLieu._meta.get_field(field).verbose_name, - getattr(self, field, '')) for field in fields] + fields = ["avis_lieustage", "avis_pratique", "avis_tourisme"] + return [ + (AvisLieu._meta.get_field(field).verbose_name, getattr(self, field, "")) + for field in fields + ] diff --git a/avisstage/statics.py b/avisstage/statics.py index 2e2b569..6671b5c 100644 --- a/avisstage/statics.py +++ b/avisstage/statics.py @@ -1,67 +1,76 @@ # coding: utf-8 DEPARTEMENTS_DEFAUT = ( - ('phy', u'Physique'), - ('maths', u'Maths'), - ('bio', u'Biologie'), - ('chimie', u'Chimie'), - ('geol', u'Géosciences'), - ('dec', u'DEC'), - ('info', u'Informatique'), - ('litt', u'Littéraire'), - ('guests', u'Pensionnaires étrangers'), - ('pei', u'PEI'), + ("phy", u"Physique"), + ("maths", u"Maths"), + ("bio", u"Biologie"), + ("chimie", u"Chimie"), + ("geol", u"Géosciences"), + ("dec", u"DEC"), + ("info", u"Informatique"), + ("litt", u"Littéraire"), + ("guests", u"Pensionnaires étrangers"), + ("pei", u"PEI"), ) TYPE_STAGE_OPTIONS = ( - (u'Recherche :', ( - ('recherche', u"Stage académique"), - ('recherche_autre', u"Stage non-académique"), - ('sejour_dri', u"Séjour de recherche DRI"), - )), - (u'Stage sans visée de recherche :', ( - ('pro', u"Stage en entreprise"), - ('admin', u"Stage en admin./ONG/orga. internationale"), - )), - (u'Enseignement :', ( - ('lectorat', u"Lectorat DRI"), - ('autre_teach', u"Autre expérience d'enseignement"), - )), - ('autre', u"Autre"), + ( + u"Recherche :", + ( + ("recherche", u"Stage académique"), + ("recherche_autre", u"Stage non-académique"), + ("sejour_dri", u"Séjour de recherche DRI"), + ), + ), + ( + u"Stage sans visée de recherche :", + ( + ("pro", u"Stage en entreprise"), + ("admin", u"Stage en admin./ONG/orga. internationale"), + ), + ), + ( + u"Enseignement :", + ( + ("lectorat", u"Lectorat DRI"), + ("autre_teach", u"Autre expérience d'enseignement"), + ), + ), + ("autre", u"Autre"), ) # Dictionnaire des type de stage (et de leur genre, True=féminin) TYPE_STAGE_DICT = { - 'recherche': (u"stage de recherche académique", False), - 'recherche_autre': (u"stage de recherche non-académique", False), - 'sejour_dri': (u"séjour de recherche DRI", False), - 'pro': (u"stage en entreprise sans visée de recherche", False), - 'admin': (u"stage en administration, ONG ou organisation internationale", False), - 'lectorat': (u"lectorat DRI", False), - 'autre_teach': (u"expérience de recherche", True), - 'autre': (u"expérience", True), + "recherche": (u"stage de recherche académique", False), + "recherche_autre": (u"stage de recherche non-académique", False), + "sejour_dri": (u"séjour de recherche DRI", False), + "pro": (u"stage en entreprise sans visée de recherche", False), + "admin": (u"stage en administration, ONG ou organisation internationale", False), + "lectorat": (u"lectorat DRI", False), + "autre_teach": (u"expérience de recherche", True), + "autre": (u"expérience", True), } TYPE_LIEU_OPTIONS = ( - ('universite', u"Université"), - ('entreprise', u"Entreprise"), - ('centrerecherche', u"Centre de recherche"), - ('administration', u"Administration"), - ('autre', u"Autre"), + ("universite", u"Université"), + ("entreprise", u"Entreprise"), + ("centrerecherche", u"Centre de recherche"), + ("administration", u"Administration"), + ("autre", u"Autre"), ) # Place du stage dans le cursus NIVEAU_SCOL_OPTIONS = ( - ('L3', u"Licence 3"), - ('M1', u"Master 1"), - ('M2', u"Master 2"), - ('DOC', u"Pré-doctorat"), - ('CST', u"Césure"), - ('BLA', u"Année blanche"), - ('VAC', u"Vacances scolaires"), - ('MIT', u"Mi-temps en parallèle des études"), - ('', u"Autre"), + ("L3", u"Licence 3"), + ("M1", u"Master 1"), + ("M2", u"Master 2"), + ("DOC", u"Pré-doctorat"), + ("CST", u"Césure"), + ("BLA", u"Année blanche"), + ("VAC", u"Vacances scolaires"), + ("MIT", u"Mi-temps en parallèle des études"), + ("", u"Autre"), ) NIVEAU_SCOL_DICT = { @@ -74,257 +83,257 @@ NIVEAU_SCOL_DICT = { "VAC": u"pendant des vacances scolaires", "MIT": u"à mi-temps en parallèle des études", } - + # Dictionnaire des noms de lieux (et de leur genre, True=féminin) TYPE_LIEU_DICT = { - 'universite': (u"université", True), - 'entreprise': (u"entreprise", True), - 'centrerecherche': (u"centre de recherche", False), - 'administration': (u"administration", True), - 'autre': (u"lieu", False), + "universite": (u"université", True), + "entreprise": (u"entreprise", True), + "centrerecherche": (u"centre de recherche", False), + "administration": (u"administration", True), + "autre": (u"lieu", False), } PAYS_OPTIONS = ( - ("AF", u"Afghanistan"), - ("AL", u"Albanie"), - ("AQ", u"Antarctique"), - ("DZ", u"Algérie"), - ("AS", u"Samoa Américaines"), - ("AD", u"Andorre"), - ("AO", u"Angola"), - ("AG", u"Antigua-et-Barbuda"), - ("AZ", u"Azerbaïdjan"), - ("AR", u"Argentine"), - ("AU", u"Australie"), - ("AT", u"Autriche"), - ("BS", u"Bahamas"), - ("BH", u"Bahreïn"), - ("BD", u"Bangladesh"), - ("AM", u"Arménie"), - ("BB", u"Barbade"), - ("BE", u"Belgique"), - ("BM", u"Bermudes"), - ("BT", u"Bhoutan"), - ("BO", u"Bolivie"), - ("BA", u"Bosnie-Herzégovine"), - ("BW", u"Botswana"), - ("BV", u"Île Bouvet"), - ("BR", u"Brésil"), - ("BZ", u"Belize"), - ("IO", u"Territoire Britannique de l'Océan Indien"), - ("SB", u"Îles Salomon"), - ("VG", u"Îles Vierges Britanniques"), - ("BN", u"Brunéi Darussalam"), - ("BG", u"Bulgarie"), - ("MM", u"Myanmar"), - ("BI", u"Burundi"), - ("BY", u"Bélarus"), - ("KH", u"Cambodge"), - ("CM", u"Cameroun"), - ("CA", u"Canada"), - ("CV", u"Cap-vert"), - ("KY", u"Îles Caïmanes"), - ("CF", u"République Centrafricaine"), - ("LK", u"Sri Lanka"), - ("TD", u"Tchad"), - ("CL", u"Chili"), - ("CN", u"Chine"), - ("TW", u"Taïwan"), - ("CX", u"Île Christmas"), - ("CC", u"Îles Cocos (Keeling)"), - ("CO", u"Colombie"), - ("KM", u"Comores"), - ("YT", u"Mayotte"), - ("CG", u"République du Congo"), - ("CD", u"République Démocratique du Congo"), - ("CK", u"Îles Cook"), - ("CR", u"Costa Rica"), - ("HR", u"Croatie"), - ("CU", u"Cuba"), - ("CY", u"Chypre"), - ("CZ", u"République Tchèque"), - ("BJ", u"Bénin"), - ("DK", u"Danemark"), - ("DM", u"Dominique"), - ("DO", u"République Dominicaine"), - ("EC", u"Équateur"), - ("SV", u"El Salvador"), - ("GQ", u"Guinée Équatoriale"), - ("ET", u"Éthiopie"), - ("ER", u"Érythrée"), - ("EE", u"Estonie"), - ("FO", u"Îles Féroé"), - ("FK", u"Îles (malvinas) Falkland"), - ("GS", u"Géorgie du Sud et les Îles Sandwich du Sud"), - ("FJ", u"Fidji"), - ("FI", u"Finlande"), - ("AX", u"Îles Åland"), - ("FR", u"France"), - ("GF", u"Guyane Française"), - ("PF", u"Polynésie Française"), - ("TF", u"Terres Australes Françaises"), - ("DJ", u"Djibouti"), - ("GA", u"Gabon"), - ("GE", u"Géorgie"), - ("GM", u"Gambie"), - ("PS", u"Territoire Palestinien Occupé"), - ("DE", u"Allemagne"), - ("GH", u"Ghana"), - ("GI", u"Gibraltar"), - ("KI", u"Kiribati"), - ("GR", u"Grèce"), - ("GL", u"Groenland"), - ("GD", u"Grenade"), - ("GP", u"Guadeloupe"), - ("GU", u"Guam"), - ("GT", u"Guatemala"), - ("GN", u"Guinée"), - ("GY", u"Guyana"), - ("HT", u"Haïti"), - ("HM", u"Îles Heard et Mcdonald"), - ("VA", u"Saint-Siège (état de la Cité du Vatican)"), - ("HN", u"Honduras"), - ("HK", u"Hong-Kong"), - ("HU", u"Hongrie"), - ("IS", u"Islande"), - ("IN", u"Inde"), - ("ID", u"Indonésie"), - ("IR", u"République Islamique d'Iran"), - ("IQ", u"Iraq"), - ("IE", u"Irlande"), - ("IL", u"Israël"), - ("IT", u"Italie"), - ("CI", u"Côte d'Ivoire"), - ("JM", u"Jamaïque"), - ("JP", u"Japon"), - ("KZ", u"Kazakhstan"), - ("JO", u"Jordanie"), - ("KE", u"Kenya"), - ("KP", u"République Populaire Démocratique de Corée"), - ("KR", u"République de Corée"), - ("KW", u"Koweït"), - ("KG", u"Kirghizistan"), - ("LA", u"République Démocratique Populaire Lao"), - ("LB", u"Liban"), - ("LS", u"Lesotho"), - ("LV", u"Lettonie"), - ("LR", u"Libéria"), - ("LY", u"Jamahiriya Arabe Libyenne"), - ("LI", u"Liechtenstein"), - ("LT", u"Lituanie"), - ("LU", u"Luxembourg"), - ("MO", u"Macao"), - ("MG", u"Madagascar"), - ("MW", u"Malawi"), - ("MY", u"Malaisie"), - ("MV", u"Maldives"), - ("ML", u"Mali"), - ("MT", u"Malte"), - ("MQ", u"Martinique"), - ("MR", u"Mauritanie"), - ("MU", u"Maurice"), - ("MX", u"Mexique"), - ("MC", u"Monaco"), - ("MN", u"Mongolie"), - ("MD", u"République de Moldova"), - ("MS", u"Montserrat"), - ("MA", u"Maroc"), - ("MZ", u"Mozambique"), - ("OM", u"Oman"), - ("NA", u"Namibie"), - ("NR", u"Nauru"), - ("NP", u"Népal"), - ("NL", u"Pays-Bas"), - ("AN", u"Antilles Néerlandaises"), - ("AW", u"Aruba"), - ("NC", u"Nouvelle-Calédonie"), - ("VU", u"Vanuatu"), - ("NZ", u"Nouvelle-Zélande"), - ("NI", u"Nicaragua"), - ("NE", u"Niger"), - ("NG", u"Nigéria"), - ("NU", u"Niué"), - ("NF", u"Île Norfolk"), - ("NO", u"Norvège"), - ("MP", u"Îles Mariannes du Nord"), - ("UM", u"Îles Mineures Éloignées des États-Unis"), - ("FM", u"États Fédérés de Micronésie"), - ("MH", u"Îles Marshall"), - ("PW", u"Palaos"), - ("PK", u"Pakistan"), - ("PA", u"Panama"), - ("PG", u"Papouasie-Nouvelle-Guinée"), - ("PY", u"Paraguay"), - ("PE", u"Pérou"), - ("PH", u"Philippines"), - ("PN", u"Pitcairn"), - ("PL", u"Pologne"), - ("PT", u"Portugal"), - ("GW", u"Guinée-Bissau"), - ("TL", u"Timor-Leste"), - ("PR", u"Porto Rico"), - ("QA", u"Qatar"), - ("RE", u"Réunion"), - ("RO", u"Roumanie"), - ("RU", u"Fédération de Russie"), - ("RW", u"Rwanda"), - ("SH", u"Sainte-Hélène"), - ("KN", u"Saint-Kitts-et-Nevis"), - ("AI", u"Anguilla"), - ("LC", u"Sainte-Lucie"), - ("PM", u"Saint-Pierre-et-Miquelon"), - ("VC", u"Saint-Vincent-et-les Grenadines"), - ("SM", u"Saint-Marin"), - ("ST", u"Sao Tomé-et-Principe"), - ("SA", u"Arabie Saoudite"), - ("SN", u"Sénégal"), - ("SC", u"Seychelles"), - ("SL", u"Sierra Leone"), - ("SG", u"Singapour"), - ("SK", u"Slovaquie"), - ("VN", u"Viet Nam"), - ("SI", u"Slovénie"), - ("SO", u"Somalie"), - ("ZA", u"Afrique du Sud"), - ("ZW", u"Zimbabwe"), - ("ES", u"Espagne"), - ("EH", u"Sahara Occidental"), - ("SD", u"Soudan"), - ("SR", u"Suriname"), - ("SJ", u"Svalbard etÎle Jan Mayen"), - ("SZ", u"Swaziland"), - ("SE", u"Suède"), - ("CH", u"Suisse"), - ("SY", u"République Arabe Syrienne"), - ("TJ", u"Tadjikistan"), - ("TH", u"Thaïlande"), - ("TG", u"Togo"), - ("TK", u"Tokelau"), - ("TO", u"Tonga"), - ("TT", u"Trinité-et-Tobago"), - ("AE", u"Émirats Arabes Unis"), - ("TN", u"Tunisie"), - ("TR", u"Turquie"), - ("TM", u"Turkménistan"), - ("TC", u"Îles Turks et Caïques"), - ("TV", u"Tuvalu"), - ("UG", u"Ouganda"), - ("UA", u"Ukraine"), - ("MK", u"L'ex-République Yougoslave de Macédoine"), - ("EG", u"Égypte"), - ("GB", u"Royaume-Uni"), - ("IM", u"Île de Man"), - ("TZ", u"République-Unie de Tanzanie"), - ("US", u"États-Unis"), - ("VI", u"Îles Vierges des États-Unis"), - ("BF", u"Burkina Faso"), - ("UY", u"Uruguay"), - ("UZ", u"Ouzbékistan"), - ("VE", u"Venezuela"), - ("WF", u"Wallis et Futuna"), - ("WS", u"Samoa"), - ("YE", u"Yémen"), - ("CS", u"Serbie-et-Monténégro"), - ("ZM", u"Zambie"), + ("AF", u"Afghanistan"), + ("AL", u"Albanie"), + ("AQ", u"Antarctique"), + ("DZ", u"Algérie"), + ("AS", u"Samoa Américaines"), + ("AD", u"Andorre"), + ("AO", u"Angola"), + ("AG", u"Antigua-et-Barbuda"), + ("AZ", u"Azerbaïdjan"), + ("AR", u"Argentine"), + ("AU", u"Australie"), + ("AT", u"Autriche"), + ("BS", u"Bahamas"), + ("BH", u"Bahreïn"), + ("BD", u"Bangladesh"), + ("AM", u"Arménie"), + ("BB", u"Barbade"), + ("BE", u"Belgique"), + ("BM", u"Bermudes"), + ("BT", u"Bhoutan"), + ("BO", u"Bolivie"), + ("BA", u"Bosnie-Herzégovine"), + ("BW", u"Botswana"), + ("BV", u"Île Bouvet"), + ("BR", u"Brésil"), + ("BZ", u"Belize"), + ("IO", u"Territoire Britannique de l'Océan Indien"), + ("SB", u"Îles Salomon"), + ("VG", u"Îles Vierges Britanniques"), + ("BN", u"Brunéi Darussalam"), + ("BG", u"Bulgarie"), + ("MM", u"Myanmar"), + ("BI", u"Burundi"), + ("BY", u"Bélarus"), + ("KH", u"Cambodge"), + ("CM", u"Cameroun"), + ("CA", u"Canada"), + ("CV", u"Cap-vert"), + ("KY", u"Îles Caïmanes"), + ("CF", u"République Centrafricaine"), + ("LK", u"Sri Lanka"), + ("TD", u"Tchad"), + ("CL", u"Chili"), + ("CN", u"Chine"), + ("TW", u"Taïwan"), + ("CX", u"Île Christmas"), + ("CC", u"Îles Cocos (Keeling)"), + ("CO", u"Colombie"), + ("KM", u"Comores"), + ("YT", u"Mayotte"), + ("CG", u"République du Congo"), + ("CD", u"République Démocratique du Congo"), + ("CK", u"Îles Cook"), + ("CR", u"Costa Rica"), + ("HR", u"Croatie"), + ("CU", u"Cuba"), + ("CY", u"Chypre"), + ("CZ", u"République Tchèque"), + ("BJ", u"Bénin"), + ("DK", u"Danemark"), + ("DM", u"Dominique"), + ("DO", u"République Dominicaine"), + ("EC", u"Équateur"), + ("SV", u"El Salvador"), + ("GQ", u"Guinée Équatoriale"), + ("ET", u"Éthiopie"), + ("ER", u"Érythrée"), + ("EE", u"Estonie"), + ("FO", u"Îles Féroé"), + ("FK", u"Îles (malvinas) Falkland"), + ("GS", u"Géorgie du Sud et les Îles Sandwich du Sud"), + ("FJ", u"Fidji"), + ("FI", u"Finlande"), + ("AX", u"Îles Åland"), + ("FR", u"France"), + ("GF", u"Guyane Française"), + ("PF", u"Polynésie Française"), + ("TF", u"Terres Australes Françaises"), + ("DJ", u"Djibouti"), + ("GA", u"Gabon"), + ("GE", u"Géorgie"), + ("GM", u"Gambie"), + ("PS", u"Territoire Palestinien Occupé"), + ("DE", u"Allemagne"), + ("GH", u"Ghana"), + ("GI", u"Gibraltar"), + ("KI", u"Kiribati"), + ("GR", u"Grèce"), + ("GL", u"Groenland"), + ("GD", u"Grenade"), + ("GP", u"Guadeloupe"), + ("GU", u"Guam"), + ("GT", u"Guatemala"), + ("GN", u"Guinée"), + ("GY", u"Guyana"), + ("HT", u"Haïti"), + ("HM", u"Îles Heard et Mcdonald"), + ("VA", u"Saint-Siège (état de la Cité du Vatican)"), + ("HN", u"Honduras"), + ("HK", u"Hong-Kong"), + ("HU", u"Hongrie"), + ("IS", u"Islande"), + ("IN", u"Inde"), + ("ID", u"Indonésie"), + ("IR", u"République Islamique d'Iran"), + ("IQ", u"Iraq"), + ("IE", u"Irlande"), + ("IL", u"Israël"), + ("IT", u"Italie"), + ("CI", u"Côte d'Ivoire"), + ("JM", u"Jamaïque"), + ("JP", u"Japon"), + ("KZ", u"Kazakhstan"), + ("JO", u"Jordanie"), + ("KE", u"Kenya"), + ("KP", u"République Populaire Démocratique de Corée"), + ("KR", u"République de Corée"), + ("KW", u"Koweït"), + ("KG", u"Kirghizistan"), + ("LA", u"République Démocratique Populaire Lao"), + ("LB", u"Liban"), + ("LS", u"Lesotho"), + ("LV", u"Lettonie"), + ("LR", u"Libéria"), + ("LY", u"Jamahiriya Arabe Libyenne"), + ("LI", u"Liechtenstein"), + ("LT", u"Lituanie"), + ("LU", u"Luxembourg"), + ("MO", u"Macao"), + ("MG", u"Madagascar"), + ("MW", u"Malawi"), + ("MY", u"Malaisie"), + ("MV", u"Maldives"), + ("ML", u"Mali"), + ("MT", u"Malte"), + ("MQ", u"Martinique"), + ("MR", u"Mauritanie"), + ("MU", u"Maurice"), + ("MX", u"Mexique"), + ("MC", u"Monaco"), + ("MN", u"Mongolie"), + ("MD", u"République de Moldova"), + ("MS", u"Montserrat"), + ("MA", u"Maroc"), + ("MZ", u"Mozambique"), + ("OM", u"Oman"), + ("NA", u"Namibie"), + ("NR", u"Nauru"), + ("NP", u"Népal"), + ("NL", u"Pays-Bas"), + ("AN", u"Antilles Néerlandaises"), + ("AW", u"Aruba"), + ("NC", u"Nouvelle-Calédonie"), + ("VU", u"Vanuatu"), + ("NZ", u"Nouvelle-Zélande"), + ("NI", u"Nicaragua"), + ("NE", u"Niger"), + ("NG", u"Nigéria"), + ("NU", u"Niué"), + ("NF", u"Île Norfolk"), + ("NO", u"Norvège"), + ("MP", u"Îles Mariannes du Nord"), + ("UM", u"Îles Mineures Éloignées des États-Unis"), + ("FM", u"États Fédérés de Micronésie"), + ("MH", u"Îles Marshall"), + ("PW", u"Palaos"), + ("PK", u"Pakistan"), + ("PA", u"Panama"), + ("PG", u"Papouasie-Nouvelle-Guinée"), + ("PY", u"Paraguay"), + ("PE", u"Pérou"), + ("PH", u"Philippines"), + ("PN", u"Pitcairn"), + ("PL", u"Pologne"), + ("PT", u"Portugal"), + ("GW", u"Guinée-Bissau"), + ("TL", u"Timor-Leste"), + ("PR", u"Porto Rico"), + ("QA", u"Qatar"), + ("RE", u"Réunion"), + ("RO", u"Roumanie"), + ("RU", u"Fédération de Russie"), + ("RW", u"Rwanda"), + ("SH", u"Sainte-Hélène"), + ("KN", u"Saint-Kitts-et-Nevis"), + ("AI", u"Anguilla"), + ("LC", u"Sainte-Lucie"), + ("PM", u"Saint-Pierre-et-Miquelon"), + ("VC", u"Saint-Vincent-et-les Grenadines"), + ("SM", u"Saint-Marin"), + ("ST", u"Sao Tomé-et-Principe"), + ("SA", u"Arabie Saoudite"), + ("SN", u"Sénégal"), + ("SC", u"Seychelles"), + ("SL", u"Sierra Leone"), + ("SG", u"Singapour"), + ("SK", u"Slovaquie"), + ("VN", u"Viet Nam"), + ("SI", u"Slovénie"), + ("SO", u"Somalie"), + ("ZA", u"Afrique du Sud"), + ("ZW", u"Zimbabwe"), + ("ES", u"Espagne"), + ("EH", u"Sahara Occidental"), + ("SD", u"Soudan"), + ("SR", u"Suriname"), + ("SJ", u"Svalbard etÎle Jan Mayen"), + ("SZ", u"Swaziland"), + ("SE", u"Suède"), + ("CH", u"Suisse"), + ("SY", u"République Arabe Syrienne"), + ("TJ", u"Tadjikistan"), + ("TH", u"Thaïlande"), + ("TG", u"Togo"), + ("TK", u"Tokelau"), + ("TO", u"Tonga"), + ("TT", u"Trinité-et-Tobago"), + ("AE", u"Émirats Arabes Unis"), + ("TN", u"Tunisie"), + ("TR", u"Turquie"), + ("TM", u"Turkménistan"), + ("TC", u"Îles Turks et Caïques"), + ("TV", u"Tuvalu"), + ("UG", u"Ouganda"), + ("UA", u"Ukraine"), + ("MK", u"L'ex-République Yougoslave de Macédoine"), + ("EG", u"Égypte"), + ("GB", u"Royaume-Uni"), + ("IM", u"Île de Man"), + ("TZ", u"République-Unie de Tanzanie"), + ("US", u"États-Unis"), + ("VI", u"Îles Vierges des États-Unis"), + ("BF", u"Burkina Faso"), + ("UY", u"Uruguay"), + ("UZ", u"Ouzbékistan"), + ("VE", u"Venezuela"), + ("WF", u"Wallis et Futuna"), + ("WS", u"Samoa"), + ("YE", u"Yémen"), + ("CS", u"Serbie-et-Monténégro"), + ("ZM", u"Zambie"), ) diff --git a/avisstage/templatetags/avisstage_tags.py b/avisstage/templatetags/avisstage_tags.py index 7f9d03c..bba61d5 100644 --- a/avisstage/templatetags/avisstage_tags.py +++ b/avisstage/templatetags/avisstage_tags.py @@ -6,23 +6,27 @@ import re register = template.Library() -@register.inclusion_tag('avisstage/templatetags/widget_lieu.html') + +@register.inclusion_tag("avisstage/templatetags/widget_lieu.html") def lieu_widget(): form = LieuForm() return {"form": form} -@register.inclusion_tag('avisstage/templatetags/widget_feedback.html') + +@register.inclusion_tag("avisstage/templatetags/widget_feedback.html") def feedback_widget(): form = FeedbackForm() return {"form": form} + @register.filter def typonazisme(value): - value = re.sub(r'(\w)\s*([?!:])', u'\\1 \\2', value) - value = re.sub(r'(\w)\s*([,.])', u'\\1\\2', value) - value = re.sub(r'([?!:,.])(\w)', u'\\1 \\2', value) + value = re.sub(r"(\w)\s*([?!:])", u"\\1 \\2", value) + value = re.sub(r"(\w)\s*([,.])", u"\\1\\2", value) + value = re.sub(r"([?!:,.])(\w)", u"\\1 \\2", value) return value + @register.filter def avis_len(value): if value < 5: @@ -32,6 +36,7 @@ def avis_len(value): else: return "long" + @register.simple_tag def url_replace(request, field, value): dict_ = request.GET.copy() diff --git a/avisstage/tests.py b/avisstage/tests.py index 0ea4bc7..2c9b760 100644 --- a/avisstage/tests.py +++ b/avisstage/tests.py @@ -12,14 +12,15 @@ from unittest import mock from .models import User, Normalien, Lieu, Stage, StageMatiere, AvisLieu + class ExperiENSTestCase(TestCase): - + # Dummy database def setUp(self): - self.u_conscrit = User.objects.create_user('conscrit', - 'conscrit@ens.fr', - 'conscrit') + 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 2020" @@ -32,281 +33,313 @@ class ExperiENSTestCase(TestCase): entrance_year=2020, ) self.sa_conscrit.save() - - self.u_archi = User.objects.create_user('archicube', - 'archicube@ens.fr', - 'archicube') - self.p_archi = self.u_archi.profil - self.p_archi.nom="Vieil archicube" - 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", - ville="Brocéliande", pays="FR", - coord="POINT(-1.63971 48.116382)") + self.u_archi = User.objects.create_user( + "archicube", "archicube@ens.fr", "archicube" + ) + self.p_archi = self.u_archi.profil + self.p_archi.nom = "Vieil archicube" + 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", + ville="Brocéliande", + pays="FR", + coord="POINT(-1.63971 48.116382)", + ) self.lieu1.save() - self.lieu2 = Lieu(nom="Durmstrang", type_lieu="universite", - ville="Edimbourg", pays="GB", - coord="POINT(56.32153 -1.259715)") + self.lieu2 = Lieu( + nom="Durmstrang", + type_lieu="universite", + ville="Edimbourg", + pays="GB", + coord="POINT(56.32153 -1.259715)", + ) self.lieu2.save() self.matiere1 = StageMatiere(nom="Arithmancie", slug="arithmancie") self.matiere1.save() self.matiere2 = StageMatiere(nom="Sortilège", slug="sortilege") self.matiere2.save() - - self.cstage1 = Stage(auteur=self.p_conscrit, sujet="Wingardium Leviosa", - date_debut=date(2020, 5, 10), - date_fin=date(2020, 8, 26), - type_stage="recherche", - niveau_scol="M1", public=True) + + self.cstage1 = Stage( + auteur=self.p_conscrit, + sujet="Wingardium Leviosa", + date_debut=date(2020, 5, 10), + date_fin=date(2020, 8, 26), + type_stage="recherche", + niveau_scol="M1", + public=True, + ) self.cstage1.save() self.cstage1.matieres.add(self.matiere1) - alieu1 = AvisLieu(stage=self.cstage1, lieu=self.lieu1, - chapo="Trop bien") + alieu1 = AvisLieu(stage=self.cstage1, lieu=self.lieu1, chapo="Trop bien") alieu1.save() - self.cstage2 = Stage(auteur=self.p_conscrit, sujet="Avada Kedavra", - date_debut=date(2021, 5, 10), - date_fin=date(2021, 8, 26), - type_stage="sejour_dri", - niveau_scol="M2", public=False) + self.cstage2 = Stage( + auteur=self.p_conscrit, + sujet="Avada Kedavra", + date_debut=date(2021, 5, 10), + date_fin=date(2021, 8, 26), + type_stage="sejour_dri", + niveau_scol="M2", + public=False, + ) self.cstage2.save() self.cstage2.matieres.add(self.matiere2) - alieu2 = AvisLieu(stage=self.cstage2, lieu=self.lieu2, - chapo="Trop nul") + alieu2 = AvisLieu(stage=self.cstage2, lieu=self.lieu2, chapo="Trop nul") alieu2.save() - - self.astage1 = Stage(auteur=self.p_archi, sujet="Alohomora", - date_debut=date(2014, 5, 10), - date_fin=date(2014, 8, 26), - type_stage="recherche", - niveau_scol="M2", public=True) + self.astage1 = Stage( + auteur=self.p_archi, + sujet="Alohomora", + date_debut=date(2014, 5, 10), + date_fin=date(2014, 8, 26), + type_stage="recherche", + niveau_scol="M2", + public=True, + ) self.astage1.save() self.astage1.matieres.add(self.matiere2) - alieu3 = AvisLieu(stage=self.astage1, lieu=self.lieu1, - chapo="Trop moyen") + alieu3 = AvisLieu(stage=self.astage1, lieu=self.lieu1, chapo="Trop moyen") alieu3.save() def assertRedirectToLogin(self, testurl): r = self.client.get(testurl) - return self.assertRedirects(r, settings.LOGIN_URL+"?next="+testurl) + return self.assertRedirects(r, settings.LOGIN_URL + "?next=" + testurl) def assertPageNotFound(self, testurl): r = self.client.get(testurl) self.assertEqual(r.status_code, 404) - - - - - """ ACCÈS PUBLIC """ + + class PublicViewsTest(ExperiENSTestCase): """ Vérifie que les fiches de stages ne sont pas visibles hors connexion """ - def test_stage_visibility_public(self): - self.assertRedirectToLogin(reverse('avisstage:stage', - kwargs={'pk':self.cstage1.id})) - - self.assertRedirectToLogin(reverse('avisstage:stage', - kwargs={'pk':self.cstage2.id})) - - self.assertRedirectToLogin(reverse('avisstage:stage', - kwargs={'pk':self.astage1.id})) + def test_stage_visibility_public(self): + self.assertRedirectToLogin( + reverse("avisstage:stage", kwargs={"pk": self.cstage1.id}) + ) + + self.assertRedirectToLogin( + reverse("avisstage:stage", kwargs={"pk": self.cstage2.id}) + ) + + self.assertRedirectToLogin( + reverse("avisstage:stage", kwargs={"pk": self.astage1.id}) + ) """ Vérifie que les profils de normaliens ne sont pas visibles hors connexion """ + def test_profil_visibility_public(self): - self.assertRedirectToLogin(reverse( - 'avisstage:profil', kwargs={'username': self.u_conscrit.username})) + self.assertRedirectToLogin( + reverse("avisstage:profil", kwargs={"username": self.u_conscrit.username}) + ) - self.assertRedirectToLogin(reverse( - 'avisstage:profil', kwargs={'username': self.u_archi.username})) + self.assertRedirectToLogin( + reverse("avisstage:profil", kwargs={"username": self.u_archi.username}) + ) - """ Vérifie que la recherche n'est pas accessible hors connexion """ + def test_pages_visibility_public(self): - self.assertRedirectToLogin(reverse('avisstage:recherche')) + self.assertRedirectToLogin(reverse("avisstage:recherche")) - self.assertRedirectToLogin(reverse('avisstage:recherche_resultats')) + self.assertRedirectToLogin(reverse("avisstage:recherche_resultats")) - self.assertRedirectToLogin(reverse('avisstage:stage_items')) + self.assertRedirectToLogin(reverse("avisstage:stage_items")) - self.assertRedirectToLogin(reverse('avisstage:feedback')) + self.assertRedirectToLogin(reverse("avisstage:feedback")) - self.assertRedirectToLogin(reverse('avisstage:moderation')) + self.assertRedirectToLogin(reverse("avisstage:moderation")) """ Vérifie que l'API n'est pas accessible hors connexion """ + def test_api_visibility_public(self): - testurl = reverse('avisstage:api_dispatch_list', - kwargs={"resource_name": "lieu", - "api_name": "v1"}) - r = self.client.get(testurl) - self.assertEqual(r.status_code, 401) - - testurl = reverse('avisstage:api_dispatch_list', - kwargs={"resource_name": "stage", - "api_name": "v1"}) - r = self.client.get(testurl) - self.assertEqual(r.status_code, 401) - - testurl = reverse('avisstage:api_dispatch_list', - kwargs={"resource_name": "profil", - "api_name": "v1"}) + testurl = reverse( + "avisstage:api_dispatch_list", + kwargs={"resource_name": "lieu", "api_name": "v1"}, + ) + r = self.client.get(testurl) + self.assertEqual(r.status_code, 401) + + testurl = reverse( + "avisstage:api_dispatch_list", + kwargs={"resource_name": "stage", "api_name": "v1"}, + ) + r = self.client.get(testurl) + self.assertEqual(r.status_code, 401) + + testurl = reverse( + "avisstage:api_dispatch_list", + kwargs={"resource_name": "profil", "api_name": "v1"}, + ) r = self.client.get(testurl) self.assertEqual(r.status_code, 401) - """ Vérifie que les pages d'édition ne sont pas accessible hors connexion """ + def test_edit_visibility_public(self): - self.assertRedirectToLogin(reverse( - 'avisstage:stage_edit', kwargs={'pk':self.cstage1.id})) - - self.assertRedirectToLogin(reverse( - 'avisstage:stage_edit', kwargs={'pk':self.astage1.id})) + self.assertRedirectToLogin( + reverse("avisstage:stage_edit", kwargs={"pk": self.cstage1.id}) + ) - self.assertRedirectToLogin(reverse( - 'avisstage:stage_publication', kwargs={'pk':self.cstage1.id})) - - self.assertRedirectToLogin(reverse( - 'avisstage:stage_publication', kwargs={'pk':self.astage1.id})) - - self.assertRedirectToLogin(reverse('avisstage:stage_ajout')) - - self.assertRedirectToLogin(reverse('avisstage:profil_edit')) + self.assertRedirectToLogin( + reverse("avisstage:stage_edit", kwargs={"pk": self.astage1.id}) + ) + self.assertRedirectToLogin( + reverse("avisstage:stage_publication", kwargs={"pk": self.cstage1.id}) + ) + self.assertRedirectToLogin( + reverse("avisstage:stage_publication", kwargs={"pk": self.astage1.id}) + ) + self.assertRedirectToLogin(reverse("avisstage:stage_ajout")) + self.assertRedirectToLogin(reverse("avisstage:profil_edit")) """ ACCÈS ARCHICUBE """ + + class ArchicubeViewsTest(ExperiENSTestCase): def setUp(self): super().setUp() # Connexion with password - self.client.login(username='archicube', password='archicube') + self.client.login(username="archicube", password="archicube") def assert403Archicubes(self, testurl): r = self.client.get(testurl) - return self.assertRedirects(r, reverse('avisstage:403-archicubes')) + return self.assertRedirects(r, reverse("avisstage:403-archicubes")) """ Vérifie que les seules fiches de stages visibles sont les siennes """ + def test_stage_visibility_archi(self): - self.assertPageNotFound(reverse('avisstage:stage', - kwargs={'pk':self.cstage1.id})) - - self.assertPageNotFound(reverse('avisstage:stage', - kwargs={'pk':self.cstage2.id})) - - testurl = reverse('avisstage:stage', - kwargs={'pk':self.astage1.id}) + self.assertPageNotFound( + reverse("avisstage:stage", kwargs={"pk": self.cstage1.id}) + ) + + self.assertPageNotFound( + reverse("avisstage:stage", kwargs={"pk": self.cstage2.id}) + ) + + testurl = reverse("avisstage:stage", kwargs={"pk": self.astage1.id}) r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - """ Vérifie que le seul profil visible est le sien """ - def test_profil_visibility_archi(self): - self.assertPageNotFound(reverse( - 'avisstage:profil', kwargs={'username': self.u_conscrit.username})) - testurl = reverse('avisstage:profil', - kwargs={'username': self.u_archi.username}) + def test_profil_visibility_archi(self): + self.assertPageNotFound( + reverse("avisstage:profil", kwargs={"username": self.u_conscrit.username}) + ) + + testurl = reverse( + "avisstage:profil", kwargs={"username": self.u_archi.username} + ) r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - """ Vérifie que la recherche n'est pas accessible """ + def test_pages_visibility_archi(self): - self.assert403Archicubes(reverse('avisstage:recherche')) - - self.assert403Archicubes(reverse('avisstage:recherche_resultats')) + self.assert403Archicubes(reverse("avisstage:recherche")) - self.assert403Archicubes(reverse('avisstage:stage_items')) + self.assert403Archicubes(reverse("avisstage:recherche_resultats")) - testurl = reverse('avisstage:feedback') - r = self.client.post(testurl, {"objet": "Contact", - "message": "Ceci est un texte"}) - self.assertRedirects(r, reverse('avisstage:index')) + self.assert403Archicubes(reverse("avisstage:stage_items")) - testurl = reverse('avisstage:moderation') + testurl = reverse("avisstage:feedback") + r = self.client.post( + testurl, {"objet": "Contact", "message": "Ceci est un texte"} + ) + self.assertRedirects(r, reverse("avisstage:index")) + + testurl = reverse("avisstage:moderation") r = self.client.get(testurl) - self.assertRedirects(r, reverse('admin:login')+"?next="+testurl) - + self.assertRedirects(r, reverse("admin:login") + "?next=" + testurl) """ Vérifie que la seule API accessible est celle des lieux """ + def test_api_visibility_archi(self): - testurl = reverse('avisstage:api_dispatch_list', - kwargs={"resource_name": "lieu", - "api_name": "v1"}) + testurl = reverse( + "avisstage:api_dispatch_list", + kwargs={"resource_name": "lieu", "api_name": "v1"}, + ) r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - - testurl = reverse('avisstage:api_dispatch_list', - kwargs={"resource_name": "stage", - "api_name": "v1"}) - r = self.client.get(testurl) - self.assertEqual(r.status_code, 401) - - testurl = reverse('avisstage:api_dispatch_list', - kwargs={"resource_name": "profil", - "api_name": "v1"}) + + testurl = reverse( + "avisstage:api_dispatch_list", + kwargs={"resource_name": "stage", "api_name": "v1"}, + ) + r = self.client.get(testurl) + self.assertEqual(r.status_code, 401) + + testurl = reverse( + "avisstage:api_dispatch_list", + kwargs={"resource_name": "profil", "api_name": "v1"}, + ) r = self.client.get(testurl) self.assertEqual(r.status_code, 401) - """ Vérifie que le seul stage modifiable est le sien """ + def test_edit_visibility_archi(self): - testurl = reverse('avisstage:stage_edit', kwargs={'pk':self.cstage1.id}) + testurl = reverse("avisstage:stage_edit", kwargs={"pk": self.cstage1.id}) r = self.client.get(testurl) self.assertEqual(r.status_code, 403) - testurl = reverse('avisstage:stage_edit', kwargs={'pk':self.astage1.id}) + testurl = reverse("avisstage:stage_edit", kwargs={"pk": self.astage1.id}) r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - testurl = reverse('avisstage:stage_publication', - kwargs={'pk':self.cstage1.id}) + testurl = reverse("avisstage:stage_publication", kwargs={"pk": self.cstage1.id}) r = self.client.post(testurl, {"publier": True}) self.assertEqual(r.status_code, 403) - testurl = reverse('avisstage:stage_publication', - kwargs={'pk':self.astage1.id}) + testurl = reverse("avisstage:stage_publication", kwargs={"pk": self.astage1.id}) r = self.client.post(testurl, {"publier": True}) - self.assertRedirects(r, reverse('avisstage:stage', - kwargs={"pk": self.astage1.id})) - - testurl = reverse('avisstage:stage_ajout') + self.assertRedirects( + r, reverse("avisstage:stage", kwargs={"pk": self.astage1.id}) + ) + + testurl = reverse("avisstage:stage_ajout") r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - - testurl = reverse('avisstage:profil_edit') + + testurl = reverse("avisstage:profil_edit") r = self.client.get(testurl) self.assertEqual(r.status_code, 200) @@ -320,7 +353,7 @@ class DeprecatedArchicubeViewsTest(ArchicubeViewsTest): 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", @@ -337,14 +370,14 @@ class DeprecatedArchicubeViewsTest(ArchicubeViewsTest): self.p_archi.save() # New connexion with password - self.client.login(username='archicube', password='archicube') - - + self.client.login(username="archicube", password="archicube") """ ACCÈS EN SCOLARITE """ + + class ScolariteViewsTest(ExperiENSTestCase): @mock.patch("authens.backends.get_cas_client") def setUp(self, mock_cas_client): @@ -352,16 +385,14 @@ class ScolariteViewsTest(ExperiENSTestCase): 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' + "vieuxcon", "vieuxcon@ens.fr", "vieuxcon" ) self.p_vieuxcon = self.u_vieuxcon.profil - self.p_vieuxcon.nom="Vieux con" - self.p_vieuxcon.promotion="Poufsouffle 2017" - self.p_vieuxcon.bio="Je suis un vieux con encore en scolarité" + self.p_vieuxcon.nom = "Vieux con" + 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 = CASAccount( @@ -370,16 +401,19 @@ class ScolariteViewsTest(ExperiENSTestCase): entrance_year=2017, ) self.sa_vieuxcon.save() - - self.vstage1 = Stage(auteur=self.p_vieuxcon, sujet="Oubliettes", - date_debut=date(2018, 5, 10), - date_fin=date(2018, 8, 26), - type_stage="recherche", - niveau_scol="M1", public=False) + + self.vstage1 = Stage( + auteur=self.p_vieuxcon, + sujet="Oubliettes", + date_debut=date(2018, 5, 10), + date_fin=date(2018, 8, 26), + type_stage="recherche", + niveau_scol="M1", + public=False, + ) self.vstage1.save() self.vstage1.matieres.add(self.matiere2) - alieu1 = AvisLieu(stage=self.vstage1, lieu=self.lieu2, - chapo="Pas si mal") + alieu1 = AvisLieu(stage=self.vstage1, lieu=self.lieu2, chapo="Pas si mal") alieu1.save() # Connexion through CAS @@ -389,134 +423,143 @@ class ScolariteViewsTest(ExperiENSTestCase): Vérifie que les seules fiches de stages visibles sont les siennes ou celles publiques """ + def test_stage_visibility_scolarite(self): - testurl = reverse('avisstage:stage', - kwargs={'pk':self.cstage1.id}) + testurl = reverse("avisstage:stage", kwargs={"pk": self.cstage1.id}) r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - self.assertPageNotFound(reverse('avisstage:stage', - kwargs={'pk':self.cstage2.id})) - - testurl = reverse('avisstage:stage', - kwargs={'pk':self.vstage1.id}) + self.assertPageNotFound( + reverse("avisstage:stage", kwargs={"pk": self.cstage2.id}) + ) + + testurl = reverse("avisstage:stage", kwargs={"pk": self.vstage1.id}) r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - """ Vérifie que tous les profils sont visibles """ + def test_profil_visibility_scolarite(self): - testurl = reverse('avisstage:profil', - kwargs={'username': self.u_conscrit.username}) + testurl = reverse( + "avisstage:profil", kwargs={"username": self.u_conscrit.username} + ) r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - self.assertContains(r, "Wingardium Leviosa") # Public - self.assertNotContains(r, "Avada Kedavra") # Brouillon + self.assertContains(r, "Wingardium Leviosa") # Public + self.assertNotContains(r, "Avada Kedavra") # Brouillon - testurl = reverse('avisstage:profil', - kwargs={'username': self.u_archi.username}) + testurl = reverse( + "avisstage:profil", kwargs={"username": self.u_archi.username} + ) r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - testurl = reverse('avisstage:profil', - kwargs={'username': self.u_vieuxcon.username}) + testurl = reverse( + "avisstage:profil", kwargs={"username": self.u_vieuxcon.username} + ) r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - """ Vérifie que la recherche et les autres pages sont accessibles """ + def test_pages_visibility_scolarite(self): - testurl = reverse('avisstage:recherche') + testurl = reverse("avisstage:recherche") r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - testurl = reverse('avisstage:recherche_resultats') + testurl = reverse("avisstage:recherche_resultats") r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - self.assertContains(r, "Wingardium Leviosa") # Public - self.assertNotContains(r, "Avada Kedavra") # Brouillon + self.assertContains(r, "Wingardium Leviosa") # Public + self.assertNotContains(r, "Avada Kedavra") # Brouillon - testurl = reverse('avisstage:stage_items') + "?ids=" \ - + ";".join(("%d" % k.id) for k in [self.cstage1, - self.cstage2, - self.astage1]) + testurl = ( + reverse("avisstage:stage_items") + + "?ids=" + + ";".join( + ("%d" % k.id) for k in [self.cstage1, self.cstage2, self.astage1] + ) + ) r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - self.assertContains(r, "Wingardium Leviosa") # Public - self.assertNotContains(r, "Avada Kedavra") # Brouillon + self.assertContains(r, "Wingardium Leviosa") # Public + self.assertNotContains(r, "Avada Kedavra") # Brouillon - testurl = reverse('avisstage:feedback') - r = self.client.post(testurl, {"objet": "Contact", - "message": "Ceci est un texte"}) - self.assertRedirects(r, reverse('avisstage:index')) + testurl = reverse("avisstage:feedback") + r = self.client.post( + testurl, {"objet": "Contact", "message": "Ceci est un texte"} + ) + self.assertRedirects(r, reverse("avisstage:index")) - testurl = reverse('avisstage:moderation') + testurl = reverse("avisstage:moderation") r = self.client.get(testurl) - self.assertRedirects(r, reverse('admin:login')+"?next="+testurl) - + self.assertRedirects(r, reverse("admin:login") + "?next=" + testurl) """ Vérifie que toutes les API sont accessibles et qu'elles ne montrent que les stages publics """ + def test_api_visibility_scolarite(self): - testurl = reverse('avisstage:api_dispatch_list', - kwargs={"resource_name": "lieu", - "api_name": "v1"}) - r = self.client.get(testurl) - self.assertEqual(r.status_code, 200) - - testurl = reverse('avisstage:api_dispatch_list', - kwargs={"resource_name": "stage", - "api_name": "v1"}) - r = self.client.get(testurl) - self.assertEqual(r.status_code, 200) - self.assertContains(r, "Wingardium Leviosa") # Public - self.assertNotContains(r, "Avada Kedavra") # Brouillon - - testurl = reverse('avisstage:api_dispatch_list', - kwargs={"resource_name": "profil", - "api_name": "v1"}) + testurl = reverse( + "avisstage:api_dispatch_list", + kwargs={"resource_name": "lieu", "api_name": "v1"}, + ) + r = self.client.get(testurl) + self.assertEqual(r.status_code, 200) + + testurl = reverse( + "avisstage:api_dispatch_list", + kwargs={"resource_name": "stage", "api_name": "v1"}, + ) + r = self.client.get(testurl) + self.assertEqual(r.status_code, 200) + self.assertContains(r, "Wingardium Leviosa") # Public + self.assertNotContains(r, "Avada Kedavra") # Brouillon + + testurl = reverse( + "avisstage:api_dispatch_list", + kwargs={"resource_name": "profil", "api_name": "v1"}, + ) r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - """ Vérifie que le seul stage modifiable est le sien """ + def test_edit_visibility_scolarite(self): - testurl = reverse('avisstage:stage_edit', kwargs={'pk':self.cstage1.id}) + testurl = reverse("avisstage:stage_edit", kwargs={"pk": self.cstage1.id}) r = self.client.get(testurl) self.assertEqual(r.status_code, 403) - testurl = reverse('avisstage:stage_edit', kwargs={'pk':self.astage1.id}) + testurl = reverse("avisstage:stage_edit", kwargs={"pk": self.astage1.id}) r = self.client.get(testurl) self.assertEqual(r.status_code, 403) - - testurl = reverse('avisstage:stage_edit', kwargs={'pk':self.vstage1.id}) + + testurl = reverse("avisstage:stage_edit", kwargs={"pk": self.vstage1.id}) r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - testurl = reverse('avisstage:stage_publication', - kwargs={'pk':self.cstage1.id}) + testurl = reverse("avisstage:stage_publication", kwargs={"pk": self.cstage1.id}) r = self.client.post(testurl, {"publier": True}) self.assertEqual(r.status_code, 403) - testurl = reverse('avisstage:stage_publication', - kwargs={'pk':self.vstage1.id}) + testurl = reverse("avisstage:stage_publication", kwargs={"pk": self.vstage1.id}) r = self.client.post(testurl, {"publier": True}) - self.assertRedirects(r, reverse('avisstage:stage', - kwargs={"pk": self.vstage1.id})) - - testurl = reverse('avisstage:stage_ajout') + self.assertRedirects( + r, reverse("avisstage:stage", kwargs={"pk": self.vstage1.id}) + ) + + testurl = reverse("avisstage:stage_ajout") r = self.client.get(testurl) self.assertEqual(r.status_code, 200) - - testurl = reverse('avisstage:profil_edit') + + testurl = reverse("avisstage:profil_edit") r = self.client.get(testurl) self.assertEqual(r.status_code, 200) diff --git a/avisstage/urls.py b/avisstage/urls.py index 93f0978..b7ce528 100644 --- a/avisstage/urls.py +++ b/avisstage/urls.py @@ -2,50 +2,62 @@ from django.urls import include, path from . import views, api from tastypie.api import Api -v1_api = Api(api_name='v1') +v1_api = Api(api_name="v1") v1_api.register(api.LieuResource()) v1_api.register(api.StageResource()) v1_api.register(api.AuteurResource()) app_name = "avisstage" urlpatterns = [ - 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'), - path('403/archicubes/', views.archicubes_interdits, - name='403-archicubes'), - - path('lieu/save/', views.save_lieu, name='lieu_ajout'), - path('profil/show//', views.ProfilView.as_view(), - name='profil'), - 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'), - 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)), + 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"), + path("403/archicubes/", views.archicubes_interdits, name="403-archicubes"), + path("lieu/save/", views.save_lieu, name="lieu_ajout"), + path("profil/show//", views.ProfilView.as_view(), name="profil"), + 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"), + 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 822f8ae..3ae2d28 100644 --- a/avisstage/utils.py +++ b/avisstage/utils.py @@ -1,21 +1,25 @@ from functools import reduce from math import cos, radians, sqrt -def choices_length (choices): - return reduce (lambda m, choice: max (m, len (choice[0])), choices, 0) + +def choices_length(choices): + return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0) + def en_scolarite(user): return user.profil.en_scolarite + def approximate_distance(a, b): lat_a = radians(a.y) lat_b = radians(b.y) dlon = radians(b.x - a.x) - dlon = dlon * cos((lat_a + lat_b)/2) - dlat = (lat_a - lat_b) - distance = 6371000 * sqrt(dlon*dlon + dlat*dlat) + dlon = dlon * cos((lat_a + lat_b) / 2) + 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 diff --git a/avisstage/views.py b/avisstage/views.py index 897b124..57fcaba 100644 --- a/avisstage/views.py +++ b/avisstage/views.py @@ -3,8 +3,14 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.views.generic import ( - DetailView, ListView, UpdateView, CreateView, TemplateView, DeleteView, - FormView, View + DetailView, + ListView, + UpdateView, + CreateView, + TemplateView, + DeleteView, + FormView, + View, ) from django.views.generic.detail import SingleObjectMixin from django import forms @@ -24,8 +30,13 @@ from simple_email_confirmation.models import EmailAddress from .models import Normalien, Stage, Lieu, AvisLieu, AvisStage from .forms import ( - StageForm, LieuForm, AvisStageForm, AvisLieuForm, FeedbackForm, AdresseEmailForm, - ReinitMdpForm + StageForm, + LieuForm, + AvisStageForm, + AvisLieuForm, + FeedbackForm, + AdresseEmailForm, + ReinitMdpForm, ) from .utils import en_scolarite @@ -40,8 +51,8 @@ import random, math # Page d'accueil def index(request): num_stages = Stage.objects.filter(public=True).count() - return render(request, 'avisstage/index.html', - {"num_stages": num_stages}) + return render(request, "avisstage/index.html", {"num_stages": num_stages}) + # Espace personnel @login_required @@ -53,40 +64,46 @@ def perso(request): profil, created = Normalien.objects.get_or_create(user=request.user) profil.save() - return render(request, 'avisstage/perso.html') + return render(request, "avisstage/perso.html") + # 403 Archicubes @login_required def archicubes_interdits(request): - return render(request, 'avisstage/403-archicubes.html') + return render(request, "avisstage/403-archicubes.html") + # Profil -#login_required +# login_required class ProfilView(LoginRequiredMixin, DetailView): model = Normalien - template_name = 'avisstage/detail/profil.html' + template_name = "avisstage/detail/profil.html" # Récupération du profil def get_object(self): - + # Restriction d'accès pour les archicubes - if (en_scolarite(self.request.user) or - self.kwargs.get('username') == self.request.user.username): + if ( + en_scolarite(self.request.user) + or self.kwargs.get("username") == self.request.user.username + ): return get_object_or_404( - Normalien, user__username=self.kwargs.get('username')) + Normalien, user__username=self.kwargs.get("username") + ) else: raise Http404 + # Stage -#login_required +# login_required class StageView(LoginRequiredMixin, DetailView): model = Stage - template_name = 'avisstage/detail/stage.html' + template_name = "avisstage/detail/stage.html" # Restriction aux stages publics ou personnels def get_queryset(self): filtre = Q(auteur__user_id=self.request.user.id) - + # Restriction d'accès pour les archicubes if en_scolarite(self.request.user): filtre |= Q(public=True) @@ -95,30 +112,33 @@ class StageView(LoginRequiredMixin, DetailView): def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - context['MAPBOX_API_KEY'] = settings.MAPBOX_API_KEY + context["MAPBOX_API_KEY"] = settings.MAPBOX_API_KEY return context + # FAQ def faq(request): - return render(request, 'avisstage/faq.html') + return render(request, "avisstage/faq.html") + # # EDITION # # Profil -#login_required +# login_required class ProfilEdit(LoginRequiredMixin, UpdateView): model = Normalien - fields = ['nom', 'promotion', 'contactez_moi', 'bio'] - template_name = 'avisstage/formulaires/profil.html' + fields = ["nom", "promotion", "contactez_moi", "bio"] + template_name = "avisstage/formulaires/profil.html" # Limitation à son propre profil def get_object(self): return self.request.user.profil - + def get_success_url(self): - return reverse('avisstage:perso') + return reverse("avisstage:perso") + # Stage @login_required @@ -129,8 +149,9 @@ def manage_stage(request, pk=None): stage = Stage(auteur=request.user.profil) avis_stage = AvisStage(stage=stage) c_del = False - last_creation = Stage.objects.filter(auteur=request.user.profil)\ - .order_by("-date_creation")[:1] + last_creation = Stage.objects.filter(auteur=request.user.profil).order_by( + "-date_creation" + )[:1] if len(last_creation) != 0: last_maj = last_creation[0].date_creation else: @@ -144,57 +165,71 @@ def manage_stage(request, pk=None): # Formset pour les avis des lieux AvisLieuFormSet = forms.inlineformset_factory( - Stage, AvisLieu, form=AvisLieuForm, can_delete=c_del, extra=0) + Stage, AvisLieu, form=AvisLieuForm, can_delete=c_del, extra=0 + ) if request.method == "POST": # Lecture des données form = StageForm(request.POST, request=request, instance=stage, prefix="stage") - avis_stage_form = AvisStageForm(request.POST, - instance=avis_stage, prefix="avis") - avis_lieu_formset = AvisLieuFormSet(request.POST, instance=stage, - prefix="lieux") + avis_stage_form = AvisStageForm( + request.POST, instance=avis_stage, prefix="avis" + ) + avis_lieu_formset = AvisLieuFormSet( + request.POST, instance=stage, prefix="lieux" + ) # Validation et enregistrement - if (form.is_valid() and - avis_stage_form.is_valid() and - avis_lieu_formset.is_valid()): + if ( + form.is_valid() + and avis_stage_form.is_valid() + and avis_lieu_formset.is_valid() + ): stage = form.save() avis_stage_form.instance.stage = stage avis_stage_form.save() avis_lieu_formset.save() - #print(request.POST) + # print(request.POST) if "continuer" in request.POST: if pk is None: - return redirect(reverse('avisstage:stage_edit',kwargs={'pk':stage.id})) + return redirect( + reverse("avisstage:stage_edit", kwargs={"pk": stage.id}) + ) else: - return redirect(reverse('avisstage:stage', - kwargs={'pk':stage.id})) + return redirect(reverse("avisstage:stage", kwargs={"pk": stage.id})) else: form = StageForm(instance=stage, prefix="stage") avis_stage_form = AvisStageForm(instance=avis_stage, prefix="avis") avis_lieu_formset = AvisLieuFormSet(instance=stage, prefix="lieux") # Affichage du formulaire - return render(request, "avisstage/formulaires/stage.html", - {'form': form, 'avis_stage_form': avis_stage_form, - 'avis_lieu_formset': avis_lieu_formset, - 'creation': pk is None, "last_maj": last_maj, - 'GOOGLE_API_KEY': settings.GOOGLE_API_KEY, - 'MAPBOX_API_KEY': settings.MAPBOX_API_KEY}) + return render( + request, + "avisstage/formulaires/stage.html", + { + "form": form, + "avis_stage_form": avis_stage_form, + "avis_lieu_formset": avis_lieu_formset, + "creation": pk is None, + "last_maj": last_maj, + "GOOGLE_API_KEY": settings.GOOGLE_API_KEY, + "MAPBOX_API_KEY": settings.MAPBOX_API_KEY, + }, + ) + # Ajout d'un lieu de stage -#login_required +# login_required # Stage @login_required def save_lieu(request): normalien = request.user.profil - + if request.method == "POST": pk = request.POST.get("id", None) - #print(request.POST) + # print(request.POST) jitter = False - if pk is None or pk == '': + if pk is None or pk == "": lieu = Lieu() else: # Modification du lieu @@ -208,7 +243,7 @@ def save_lieu(request): lieu = Lieu() # Servira à bouger un peu le lieu jitter = True - + # Lecture des données form = LieuForm(request.POST, instance=lieu) @@ -217,51 +252,54 @@ def save_lieu(request): lieu = form.save(commit=False) if jitter: cdx, cdy = lieu.coord.get_coords() - ang = random.random() * 6.29; + ang = random.random() * 6.29 rad = (random.random() + 0.5) * 3e-4 - cdx += math.cos(ang) * rad; - cdy += math.sin(ang) * rad; + cdx += math.cos(ang) * rad + cdy += math.sin(ang) * rad lieu.coord.set_coords((cdx, cdy)) lieu.save() # Élimination des doublons if pk is None or pk == "": - olieux = Lieu.objects.filter(nom=lieu.nom, coord__distance_lte=(lieu.coord, 10)) + olieux = Lieu.objects.filter( + nom=lieu.nom, coord__distance_lte=(lieu.coord, 10) + ) for olieu in olieux: - if olieu.type_lieu == lieu.type_lieu and \ - olieu.ville == lieu.ville and \ - olieu.pays == lieu.pays: + if ( + olieu.type_lieu == lieu.type_lieu + and olieu.ville == lieu.ville + and olieu.pays == lieu.pays + ): return JsonResponse({"success": True, "id": olieu.id}) - + lieu.save() return JsonResponse({"success": True, "id": lieu.id}) else: - return JsonResponse({"success": False, - "errors": form.errors}) + return JsonResponse({"success": False, "errors": form.errors}) else: return JsonResponse({"erreur": "Aucune donnée POST"}) + class LieuAjout(LoginRequiredMixin, CreateView): model = Lieu form_class = LieuForm - template_name = 'avisstage/formulaires/lieu.html' + template_name = "avisstage/formulaires/lieu.html" # Retourne d'un JSON si requête AJAX def form_valid(self, form): if self.request.GET.get("format", "") == "json": self.object = form.save() - return JsonResponse({"success": True, - "id": self.object.id}) + return JsonResponse({"success": True, "id": self.object.id}) else: super(LieuAjout, self).form_valid(form) def form_invalid(self, form): if self.request.GET.get("format", "") == "json": - return JsonResponse({"success": False, - "errors": form.errors}) + return JsonResponse({"success": False, "errors": form.errors}) else: super(LieuAjout, self).form_valid(form) + # Passage d'un stage en mode public @login_required def publier_stage(request, pk): @@ -280,27 +318,28 @@ def publier_stage(request, pk): stage.public = False stage.save() - + return redirect(reverse("avisstage:stage", kwargs={"pk": pk})) + # # FEEDBACK # + @login_required def feedback(request): if request.method == "POST": form = FeedbackForm(request.POST) if form.is_valid(): - objet = form.cleaned_data['objet'] - header = "[From : %s <%s>]\n" % (request.user, - request.user.email) - message = header + form.cleaned_data['message'] + objet = form.cleaned_data["objet"] + header = "[From : %s <%s>]\n" % (request.user, request.user.email) + message = header + form.cleaned_data["message"] send_mail( - "[experiENS] "+ objet, + "[experiENS] " + objet, message, request.user.email, - ['robin.champenois@ens.fr'], + ["robin.champenois@ens.fr"], fail_silently=False, ) if request.GET.get("format", None) == "json": @@ -308,8 +347,7 @@ def feedback(request): return redirect(reverse("avisstage:index")) else: if request.GET.get("format", None) == "json": - return JsonResponse({"success": False, - "errors": form.errors}) + return JsonResponse({"success": False, "errors": form.errors}) else: form = FeedbackForm() raise Http404() @@ -319,62 +357,95 @@ def feedback(request): # STATISTIQUES # + @login_required @staff_member_required def statistiques(request): nstages = Stage.objects.count() npubstages = Stage.objects.filter(public=True).count() - nbymatiere_raw = Stage.objects.values('matieres__nom', 'public').annotate(scount=Count('matieres__nom')) + nbymatiere_raw = Stage.objects.values("matieres__nom", "public").annotate( + scount=Count("matieres__nom") + ) nbymatiere = defaultdict(dict) for npm in nbymatiere_raw: - nbymatiere[npm["matieres__nom"]]["publics" if npm["public"] else "drafts"] = npm["scount"] + nbymatiere[npm["matieres__nom"]][ + "publics" if npm["public"] else "drafts" + ] = npm["scount"] for mat, npm in nbymatiere.items(): npm["matiere"] = mat - nbymatiere = sorted(list(nbymatiere.values()), key=lambda npm: -npm.get("publics", 0)) - nbylength = [("Vide", Stage.objects.filter(len_avis_stage__lt=5).count(), - Stage.objects.filter(len_avis_lieux__lt=5).count()), - ("Court", Stage.objects.filter(len_avis_stage__lt=30, len_avis_stage__gt=4).count(), - Stage.objects.filter(len_avis_lieux__lt=30, len_avis_lieux__gt=4).count()), - ("Moyen", Stage.objects.filter(len_avis_stage__lt=100, len_avis_stage__gt=29).count(), - Stage.objects.filter(len_avis_lieux__lt=100, len_avis_lieux__gt=29).count()), - ("Long", Stage.objects.filter(len_avis_stage__gt=99).count(), - Stage.objects.filter(len_avis_lieux__gt=99).count())] + nbymatiere = sorted( + list(nbymatiere.values()), key=lambda npm: -npm.get("publics", 0) + ) + nbylength = [ + ( + "Vide", + Stage.objects.filter(len_avis_stage__lt=5).count(), + Stage.objects.filter(len_avis_lieux__lt=5).count(), + ), + ( + "Court", + Stage.objects.filter(len_avis_stage__lt=30, len_avis_stage__gt=4).count(), + Stage.objects.filter(len_avis_lieux__lt=30, len_avis_lieux__gt=4).count(), + ), + ( + "Moyen", + Stage.objects.filter(len_avis_stage__lt=100, len_avis_stage__gt=29).count(), + Stage.objects.filter(len_avis_lieux__lt=100, len_avis_lieux__gt=29).count(), + ), + ( + "Long", + Stage.objects.filter(len_avis_stage__gt=99).count(), + Stage.objects.filter(len_avis_lieux__gt=99).count(), + ), + ] nusers = Normalien.objects.count() nauts = Normalien.objects.filter(stages__isnull=False).distinct().count() - nbyaut = Counter(Normalien.objects.filter(stages__isnull=False).annotate(scount=Count('stages')).values_list('scount', flat="True")).items() + nbyaut = Counter( + Normalien.objects.filter(stages__isnull=False) + .annotate(scount=Count("stages")) + .values_list("scount", flat="True") + ).items() nlieux = Lieu.objects.filter(stages__isnull=False).distinct().count() - return render(request, 'avisstage/moderation/statistiques.html', - {'num_stages': nstages, - 'num_stages_pub': npubstages, - 'num_par_matiere': nbymatiere, - 'num_users': nusers, - 'num_auteurs': nauts, - 'num_par_auteur': nbyaut, - 'num_lieux_utiles': nlieux, - 'num_par_longueur': nbylength, - }) + return render( + request, + "avisstage/moderation/statistiques.html", + { + "num_stages": nstages, + "num_stages_pub": npubstages, + "num_par_matiere": nbymatiere, + "num_users": nusers, + "num_auteurs": nauts, + "num_par_auteur": nbyaut, + "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})) + reverse("avisstage:emails_confirme", kwargs={"key": email.key}) + ) send_mail( "[ExperiENS] Confirmez votre adresse a-mail", -"""Bonjour, + """Bonjour, Vous venez d'ajouter cette adresse e-mail à votre compte ExperiENS. @@ -383,14 +454,18 @@ Pour la vérifier, merci de cliquer sur le lien suivant, ou de copier l'adresse {confirm_url} Cordialement, -L'équipe ExperiENS""".format(confirm_url=confirm_url), - 'experiens-nepasrepondre@eleves.ens.fr', +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})) - + return redirect( + reverse("avisstage:emails_aconfirmer", kwargs={"email": email.email}) + ) + + class MesParametres(LoginRequiredMixin, FormView): model = EmailAddress template_name = "avisstage/compte/parametres.html" @@ -400,16 +475,18 @@ class MesParametres(LoginRequiredMixin, FormView): 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) + 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() @@ -417,10 +494,12 @@ class RendAdressePrincipale(MesAdressesMixin, SingleObjectMixin, View): 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 @@ -430,18 +509,23 @@ class ReConfirmeAdresse(MesAdressesMixin, DetailView): 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) + 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)) + 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" @@ -451,21 +535,26 @@ class SupprimeAdresse(MesAdressesMixin, DeleteView): 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', + 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) + 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") @@ -475,4 +564,3 @@ class DefinirMotDePasse(PasswordResetConfirmView): 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/avisstage/views_search.py b/avisstage/views_search.py index c1f6efe..f8fe183 100644 --- a/avisstage/views_search.py +++ b/avisstage/views_search.py @@ -29,28 +29,36 @@ logger = logging.getLogger("recherche") # Recherche class SearchForm(forms.Form): generique = forms.CharField(required=False) - sujet = forms.CharField(label=u'À propos de', required=False) - contexte = forms.CharField(label=u'Contexte (lieu, encadrant⋅e⋅s, structure)', - required=False) - - apres_annee = forms.IntegerField(label=u'Après cette année', required=False) - avant_annee = forms.IntegerField(label=u'Avant cette année', required=False) + sujet = forms.CharField(label=u"À propos de", required=False) + contexte = forms.CharField( + label=u"Contexte (lieu, encadrant⋅e⋅s, structure)", required=False + ) - type_stage = forms.ChoiceField(label="Type de stage", choices=([('', u'')] - + list(TYPE_STAGE_OPTIONS)), - required=False) - niveau_scol = forms.ChoiceField(label="Année d'étude", choices=([('', u'')] - + list(NIVEAU_SCOL_OPTIONS)), - required=False) - - type_lieu = forms.ChoiceField(label=u"Type de lieu d'accueil", - choices=([('', u'')] - + list(TYPE_LIEU_OPTIONS)), - required=False) - tri = forms.ChoiceField(label=u'Tri par', - choices=[('pertinence', u'Pertinence'), - ('-date_maj',u'Dernière mise à jour')], - required=False, initial='pertinence') + apres_annee = forms.IntegerField(label=u"Après cette année", required=False) + avant_annee = forms.IntegerField(label=u"Avant cette année", required=False) + + type_stage = forms.ChoiceField( + label="Type de stage", + choices=([("", u"")] + list(TYPE_STAGE_OPTIONS)), + required=False, + ) + niveau_scol = forms.ChoiceField( + label="Année d'étude", + choices=([("", u"")] + list(NIVEAU_SCOL_OPTIONS)), + required=False, + ) + + type_lieu = forms.ChoiceField( + label=u"Type de lieu d'accueil", + choices=([("", u"")] + list(TYPE_LIEU_OPTIONS)), + required=False, + ) + tri = forms.ChoiceField( + label=u"Tri par", + choices=[("pertinence", u"Pertinence"), ("-date_maj", u"Dernière mise à jour")], + required=False, + initial="pertinence", + ) def cherche(**kwargs): @@ -58,9 +66,11 @@ def cherche(**kwargs): use_dsl = False def field_relevant(field, test_string=True): - return field in kwargs and \ - kwargs[field] is not None and \ - ((not test_string) or kwargs[field].strip() != '') + return ( + field in kwargs + and kwargs[field] is not None + and ((not test_string) or kwargs[field].strip() != "") + ) if USE_ELASTICSEARCH: dsl = StageDocument.search() @@ -71,39 +81,49 @@ def cherche(**kwargs): # Champ générique : recherche dans tous les champs if field_relevant("generique"): - #print("Filtre generique", kwargs['generique']) + # print("Filtre generique", kwargs['generique']) dsl = dsl.query( "multi_match", - query = kwargs["generique"], - fuzziness = "auto", - fields = [ - 'sujet^3', - 'encadrants', - 'type_stage', - 'niveau_scol', - 'structure', + query=kwargs["generique"], + fuzziness="auto", + fields=[ + "sujet^3", + "encadrants", + "type_stage", + "niveau_scol", + "structure", "lieux.*^2", "auteur.nom^2", "thematiques^2", - "matieres" - ]) + "matieres", + ], + ) use_dsl = True # Sujet -> Recherche dan les noms de sujets et les thématiques if field_relevant("sujet"): - dsl = dsl.query("multi_match", - query = kwargs["sujet"], - fields = ['sujet^2', 'thematiques', 'matieres'], - fuzziness = "auto") + dsl = dsl.query( + "multi_match", + query=kwargs["sujet"], + fields=["sujet^2", "thematiques", "matieres"], + fuzziness="auto", + ) use_dsl = True # Contexte -> Encadrants, structure, lieu if field_relevant("contexte"): - dsl = dsl.query("multi_match", - query = kwargs["contexte"], - fields = ['encadrants', 'structure^2', - 'lieux.nom', 'lieux.pays', 'lieux.ville'], - fuzziness = "auto") + dsl = dsl.query( + "multi_match", + query=kwargs["contexte"], + fields=[ + "encadrants", + "structure^2", + "lieux.nom", + "lieux.pays", + "lieux.ville", + ], + fuzziness="auto", + ) use_dsl = True else: @@ -111,45 +131,48 @@ def cherche(**kwargs): # recherche en base de données if field_relevant("generique"): generique = kwargs["generique"] - filtres = (Q(sujet__icontains=generique) - | Q(thematiques__name__icontains=generique) - | Q(matieres__nom__icontains=generique) - | Q(lieux__nom__icontains=generique)) + filtres = ( + Q(sujet__icontains=generique) + | Q(thematiques__name__icontains=generique) + | Q(matieres__nom__icontains=generique) + | Q(lieux__nom__icontains=generique) + ) - # Autres champs -> non fonctionnels + # Autres champs -> non fonctionnels if field_relevant("sujet") or field_relevant("contexte"): raise NotImplementedError( - "ElasticSearch doit être activé pour ce type de recherche") + "ElasticSearch doit être activé pour ce type de recherche" + ) # # Filtres directs db # - + # Dates - if field_relevant('avant_annee', False): - dte = date(kwargs['avant_annee']+1, 1, 1) + if field_relevant("avant_annee", False): + dte = date(kwargs["avant_annee"] + 1, 1, 1) filtres &= Q(date_fin__lt=dte) - if field_relevant('apres_annee', False): - dte = date(kwargs['apres_annee'], 1, 1) + if field_relevant("apres_annee", False): + dte = date(kwargs["apres_annee"], 1, 1) filtres &= Q(date_debut__gte=dte) # Type de stage - if field_relevant('type_stage'): + if field_relevant("type_stage"): filtres &= Q(type_stage=kwargs["type_stage"]) - - if field_relevant('niveau_scol'): + + if field_relevant("niveau_scol"): filtres &= Q(niveau_scol=kwargs["niveau_scol"]) # Type de lieu - if field_relevant('type_lieu'): + if field_relevant("type_lieu"): filtres &= Q(lieux__type_lieu=kwargs["type_lieu"]) # Tri tri = "pertinence" - if field_relevant('tri') and kwargs['tri'] in ['-date_maj']: - tri = kwargs['tri'] + if field_relevant("tri") and kwargs["tri"] in ["-date_maj"]: + tri = kwargs["tri"] if not use_dsl: tri = "-date_maj" @@ -163,28 +186,27 @@ def cherche(**kwargs): if tri == "pertinence": resultat = resultat.order_by( - Case( - *[When(pk=pk, then=pos) for pos, pk in enumerate(dsl_res)] - ) + Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(dsl_res)]) ) else: resultat = resultat.order_by(tri) return resultat, tri + @login_required @en_scolarite_required def recherche(request): form = SearchForm() - return render(request, 'avisstage/recherche/recherche.html', - {"form": form}) + return render(request, "avisstage/recherche/recherche.html", {"form": form}) + @login_required @en_scolarite_required def recherche_resultats(request): stages = [] - tri = '' - vue = 'vue-liste' + tri = "" + vue = "vue-liste" lieux = [] stageids = [] if request.method == "GET": @@ -194,17 +216,22 @@ def recherche_resultats(request): search_args = form.cleaned_data # Gestion du cache - lsearch_args = {key: val for key, val in search_args.items() - if val != "" and val is not None} + lsearch_args = { + key: val + for key, val in search_args.items() + if val != "" and val is not None + } cache_key = json.dumps(lsearch_args, sort_keys=True) cached = cache.get(cache_key) if cached is None: # Requête effective stages, tri = cherche(**search_args) - stageids = list(stages.values_list('id', flat=True)) - lieux = [[stageid, lieuid] for (stageid, lieuid) - in stages.values_list('id', 'lieux') - if lieuid is not None] + stageids = list(stages.values_list("id", flat=True)) + lieux = [ + [stageid, lieuid] + for (stageid, lieuid) in stages.values_list("id", "lieux") + if lieuid is not None + ] # Sauvegarde dans le cache to_cache = {"stages": stageids, "lieux": lieux, "tri": tri} @@ -225,42 +252,55 @@ def recherche_resultats(request): stageids = [] if cached is None: - stages = stages[max(0, stageids.start_index()-1): - stageids.end_index()] + stages = stages[ + max(0, stageids.start_index() - 1) : stageids.end_index() + ] else: - orderer = Case(*[When(pk=pk, then=pos) - for pos, pk in enumerate(stageids)]) + orderer = Case( + *[When(pk=pk, then=pos) for pos, pk in enumerate(stageids)] + ) stages = Stage.objects.filter(id__in=stageids).order_by(orderer) - stages = stages.prefetch_related('lieux', 'auteur', - 'matieres', 'thematiques') + stages = stages.prefetch_related( + "lieux", "auteur", "matieres", "thematiques" + ) else: form = SearchForm() if stages: - vue = 'vue-hybride' + vue = "vue-hybride" # Version JSON pour recherche dynamique if request.GET.get("format") == "json": - return JsonResponse({"stages": stages, "page": page, - "num_pages": paginator.num_pages}) - - template_name = 'avisstage/recherche/resultats.html' + return JsonResponse( + {"stages": stages, "page": page, "num_pages": paginator.num_pages} + ) + + template_name = "avisstage/recherche/resultats.html" if request.GET.get("format") == "raw": - template_name = 'avisstage/recherche/stage_items.html' - return render(request, template_name, - {"form": form, "stages": stages, "paginator": stageids, - "tri": tri, "vue": vue, "lieux": lieux, - "MAPBOX_API_KEY": settings.MAPBOX_API_KEY}) + template_name = "avisstage/recherche/stage_items.html" + return render( + request, + template_name, + { + "form": form, + "stages": stages, + "paginator": stageids, + "tri": tri, + "vue": vue, + "lieux": lieux, + "MAPBOX_API_KEY": settings.MAPBOX_API_KEY, + }, + ) + @login_required @en_scolarite_required def stage_items(request): try: - stageids = [int(a) for a in request.GET.get("ids", "").split(';')] + stageids = [int(a) for a in request.GET.get("ids", "").split(";")] except ValueError: return HttpResponseBadRequest("Paramètre incorrect") - stages = Stage.objects.filter(id__in=stageids)\ - .prefetch_related('lieux', 'auteur', - 'matieres', 'thematiques') - return render(request, 'avisstage/recherche/stage_items.html', - {"stages": stages}) + stages = Stage.objects.filter(id__in=stageids).prefetch_related( + "lieux", "auteur", "matieres", "thematiques" + ) + return render(request, "avisstage/recherche/stage_items.html", {"stages": stages}) diff --git a/avisstage/widgets.py b/avisstage/widgets.py index 020807f..e3487d6 100644 --- a/avisstage/widgets.py +++ b/avisstage/widgets.py @@ -1,14 +1,14 @@ from django import forms from django.core import validators + class LatLonWidget(forms.MultiWidget): """ A Widget that splits Point input into two latitude/longitude boxes. """ def __init__(self, attrs=None, date_format=None, time_format=None): - widgets = (forms.HiddenInput(attrs=attrs), - forms.HiddenInput(attrs=attrs)) + widgets = (forms.HiddenInput(attrs=attrs), forms.HiddenInput(attrs=attrs)) super(LatLonWidget, self).__init__(widgets, attrs) def decompress(self, value): @@ -23,13 +23,15 @@ class LatLonField(forms.MultiValueField): srid = 4326 default_error_messages = { - 'invalid_latitude' : (u'Entrez une latitude valide.'), - 'invalid_longitude' : (u'Entrez une longitude valide.'), + "invalid_latitude": (u"Entrez une latitude valide."), + "invalid_longitude": (u"Entrez une longitude valide."), } def __init__(self, *args, **kwargs): - fields = (forms.FloatField(min_value=-90, max_value=90), - forms.FloatField(min_value=-180, max_value=180)) + fields = ( + forms.FloatField(min_value=-90, max_value=90), + forms.FloatField(min_value=-180, max_value=180), + ) super(LatLonField, self).__init__(fields, *args, **kwargs) def compress(self, data_list): @@ -37,11 +39,11 @@ class LatLonField(forms.MultiValueField): # Raise a validation error if latitude or longitude is empty # (possible if LatLongField has required=False). if data_list[0] in validators.EMPTY_VALUES: - raise forms.ValidationError(self.error_messages['invalid_latitude']) + raise forms.ValidationError(self.error_messages["invalid_latitude"]) if data_list[1] in validators.EMPTY_VALUES: - raise forms.ValidationError(self.error_messages['invalid_longitude']) + raise forms.ValidationError(self.error_messages["invalid_longitude"]) # SRID=4326;POINT(1.12345789 1.123456789) - srid_str = 'SRID=%d'%self.srid - point_str = 'POINT(%f %f)'%tuple(reversed(data_list)) - return ';'.join([srid_str, point_str]) + srid_str = "SRID=%d" % self.srid + point_str = "POINT(%f %f)" % tuple(reversed(data_list)) + return ";".join([srid_str, point_str]) return None diff --git a/experiENS/auth.py b/experiENS/auth.py index b8f8d41..1c54a8a 100644 --- a/experiENS/auth.py +++ b/experiENS/auth.py @@ -1,9 +1,10 @@ from authens.backends import ENSCASBackend as AuthENSBackend from authens.utils import parse_entrance_year + 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: diff --git a/experiENS/settings_base.py b/experiENS/settings_base.py index 8a2ed36..a1b393a 100644 --- a/experiENS/settings_base.py +++ b/experiENS/settings_base.py @@ -24,68 +24,65 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.gis', - 'django.contrib.sites', - - 'django_elasticsearch_dsl', - + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.gis", + "django.contrib.sites", + "django_elasticsearch_dsl", #'allauth', # Uncomment that part when you #'allauth.account', # apply migration #'allauth.socialaccount', # Allauth -> AuthENS - - 'simple_email_confirmation', - 'authens', - 'tastypie', - 'braces', - 'tinymce', - 'taggit', - 'taggit_autosuggest', - 'avisstage' + "simple_email_confirmation", + "authens", + "tastypie", + "braces", + "tinymce", + "taggit", + "taggit_autosuggest", + "avisstage", ] 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.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ) TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ # insert your TEMPLATE_DIRS here ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.debug', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.template.context_processors.request', - 'django.contrib.messages.context_processors.messages', + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", ], }, }, ] -ROOT_URLCONF = 'experiENS.urls' +ROOT_URLCONF = "experiENS.urls" -WSGI_APPLICATION = 'experiENS.wsgi.application' +WSGI_APPLICATION = "experiENS.wsgi.application" # Database @@ -94,9 +91,9 @@ WSGI_APPLICATION = 'experiENS.wsgi.application' # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ -LANGUAGE_CODE = 'fr' +LANGUAGE_CODE = "fr" -TIME_ZONE = 'Europe/Paris' +TIME_ZONE = "Europe/Paris" USE_I18N = True @@ -109,37 +106,37 @@ SITE_ID = 1 # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'experiENS.auth.ENSCASBackend', + "django.contrib.auth.backends.ModelBackend", + "experiENS.auth.ENSCASBackend", ) -CAS_SERVER_URL = "https://cas.eleves.ens.fr/" #SPI CAS +CAS_SERVER_URL = "https://cas.eleves.ens.fr/" # SPI CAS AUTHENS_USE_OLDCAS = False -LOGIN_URL = reverse_lazy('authens:login') -LOGOUT_URL = reverse_lazy('authens:logout') -LOGIN_REDIRECT_URL = reverse_lazy('avisstage:perso') -LOGOUT_REDIRECT_URL = reverse_lazy('avisstage:index') +LOGIN_URL = reverse_lazy("authens:login") +LOGOUT_URL = reverse_lazy("authens:logout") +LOGIN_REDIRECT_URL = reverse_lazy("avisstage:perso") +LOGOUT_REDIRECT_URL = reverse_lazy("avisstage:index") LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'file': { - 'level': 'INFO', - 'class': 'logging.FileHandler', - 'filename': os.path.join(BASE_DIR, 'recherche.log'), + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "file": { + "level": "INFO", + "class": "logging.FileHandler", + "filename": os.path.join(BASE_DIR, "recherche.log"), }, }, - 'loggers': { - 'recherche': { - 'handlers': ['file'], - 'level': 'INFO', - 'propagate': True, + "loggers": { + "recherche": { + "handlers": ["file"], + "level": "INFO", + "propagate": True, }, }, } diff --git a/experiENS/settings_dev.py b/experiENS/settings_dev.py index 8c0c9b3..acfdc73 100644 --- a/experiENS/settings_dev.py +++ b/experiENS/settings_dev.py @@ -3,9 +3,9 @@ from .settings_base import * DEBUG = True DATABASES = { - 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.spatialite', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.contrib.gis.db.backends.spatialite", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } @@ -13,35 +13,30 @@ USE_DEBUG_TOOLBAR = False if USE_DEBUG_TOOLBAR: INSTALLED_APPS += [ - 'debug_toolbar', + "debug_toolbar", ] - MIDDLEWARE = ( - 'debug_toolbar.middleware.DebugToolbarMiddleware', - ) + MIDDLEWARE + MIDDLEWARE = ("debug_toolbar.middleware.DebugToolbarMiddleware",) + MIDDLEWARE -INTERNAL_IPS = ['127.0.0.1'] +INTERNAL_IPS = ["127.0.0.1"] -SPATIALITE_LIBRARY_PATH = 'mod_spatialite' +SPATIALITE_LIBRARY_PATH = "mod_spatialite" STATIC_ROOT = "/home/evarin/Bureau/experiENS/static/" -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" STATIC_URL = "/experiens/static/" ELASTICSEARCH_DSL = { - 'default': { - 'hosts': 'localhost:9200' - }, + "default": {"hosts": "localhost:9200"}, } -CLIPPER_LDAP_SERVER = 'ldaps://localhost:636' +CLIPPER_LDAP_SERVER = "ldaps://localhost:636" # Changer à True pour développer avec ES USE_ELASTICSEARCH = False if not USE_ELASTICSEARCH: - INSTALLED_APPS.remove('django_elasticsearch_dsl') - + INSTALLED_APPS.remove("django_elasticsearch_dsl") diff --git a/experiENS/settings_prod.py b/experiENS/settings_prod.py index 63bfc1c..46c5ccf 100644 --- a/experiENS/settings_prod.py +++ b/experiENS/settings_prod.py @@ -9,9 +9,7 @@ DEBUG = False ALLOWED_HOSTS = ["www.eleves.ens.fr"] -ADMINS = ( - ('Robin Champenois', 'champeno@clipper.ens.fr'), - ) +ADMINS = (("Robin Champenois", "champeno@clipper.ens.fr"),) ADMIN_LOGINS = [ "champeno", @@ -21,33 +19,31 @@ SERVER_EMAIL = "experiens@www.eleves.ens.fr" ROOT_URL = "/experiens/" -WSGI_APPLICATION = 'experiENS.wsgi.application' +WSGI_APPLICATION = "experiENS.wsgi.application" DATABASES = { - 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': 'experiens', - 'USER': 'experiens', - 'PASSWORD': '', - 'HOST': '', - 'PORT': '5432', + "default": { + "ENGINE": "django.contrib.gis.db.backends.postgis", + "NAME": "experiens", + "USER": "experiens", + "PASSWORD": "", + "HOST": "", + "PORT": "5432", } } -STATIC_URL = ROOT_URL + 'static/' -MEDIA_URL = ROOT_URL + 'media/' +STATIC_URL = ROOT_URL + "static/" +MEDIA_URL = ROOT_URL + "media/" -STATIC_ROOT = os.path.join(BASE_DIR, 'static/') +STATIC_ROOT = os.path.join(BASE_DIR, "static/") EMAIL_HOST = "nef.ens.fr" ELASTICSEARCH_DSL = { - 'default': { - 'hosts': '127.0.0.1:9200' - }, + "default": {"hosts": "127.0.0.1:9200"}, } -CLIPPER_LDAP_SERVER = 'ldaps://ldap.spi.ens.fr:636' +CLIPPER_LDAP_SERVER = "ldaps://ldap.spi.ens.fr:636" DEFAULT_FROM_EMAIL = "experiens-no-reply@www.eleves.ens.fr" diff --git a/experiENS/urls.py b/experiENS/urls.py index 4bb2bfb..bf55008 100644 --- a/experiENS/urls.py +++ b/experiENS/urls.py @@ -3,18 +3,16 @@ from django.urls import include, path from django.contrib import admin urlpatterns = [ - path('', include('avisstage.urls')), - - + path("", include("avisstage.urls")), path("authens/", include("authens.urls")), - - path('tinymce/', include('tinymce.urls')), - path('taggit_autosuggest/', include('taggit_autosuggest.urls')), - path('admin/', 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 = [ - path('__debug__/', include(debug_toolbar.urls)), + path("__debug__/", include(debug_toolbar.urls)), ] + urlpatterns diff --git a/experiENS/wsgi.py b/experiENS/wsgi.py index 8650d72..c9810b7 100644 --- a/experiENS/wsgi.py +++ b/experiENS/wsgi.py @@ -8,7 +8,9 @@ https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ """ import os + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "experiENS.settings") from django.core.wsgi import get_wsgi_application + application = get_wsgi_application() From 5bc518eba6db5ccc04a48613d62a2a4fe702f494 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 7 Feb 2021 23:24:33 +0100 Subject: [PATCH 13/23] Isort --- avisstage/admin.py | 5 +- avisstage/api.py | 6 +- avisstage/decorators.py | 2 +- avisstage/documents.py | 2 +- avisstage/forms.py | 10 +-- .../management/commands/nettoie_lieux.py | 3 +- .../management/commands/nettoie_stages.py | 3 +- .../management/commands/supprime_lieu.py | 3 +- .../management/commands/termine_scolarite.py | 9 +-- avisstage/migrations/0001_initial.py | 11 ++-- .../migrations/0002_auto_20171002_2243.py | 3 +- .../migrations/0003_auto_20210117_1208.py | 2 +- .../migrations/0004_allauth_to_authens.py | 1 - .../migrations/0006_auto_20210131_1954.py | 3 +- avisstage/models.py | 36 +++++------ avisstage/templatetags/avisstage_tags.py | 5 +- avisstage/tests.py | 19 +++--- avisstage/urls.py | 6 +- avisstage/views.py | 64 +++++++++---------- avisstage/views_search.py | 24 ++++--- setup.cfg | 10 +++ 21 files changed, 121 insertions(+), 106 deletions(-) create mode 100644 setup.cfg diff --git a/avisstage/admin.py b/avisstage/admin.py index 52a6c2d..9bf977f 100644 --- a/avisstage/admin.py +++ b/avisstage/admin.py @@ -1,9 +1,10 @@ +import authens.models as authmod + from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User -from avisstage.models import * -import authens.models as authmod +from avisstage.models import * class NormalienInline(admin.StackedInline): diff --git a/avisstage/api.py b/avisstage/api.py index 61e555f..4d53f95 100644 --- a/avisstage/api.py +++ b/avisstage/api.py @@ -1,13 +1,13 @@ # coding: utf-8 -from tastypie.resources import ModelResource -from tastypie.authentication import SessionAuthentication from tastypie import fields, utils +from tastypie.authentication import SessionAuthentication +from tastypie.resources import ModelResource from django.contrib.gis import geos from django.urls import reverse -from .models import Lieu, Stage, Normalien, StageMatiere +from .models import Lieu, Normalien, Stage, StageMatiere from .utils import approximate_distance diff --git a/avisstage/decorators.py b/avisstage/decorators.py index 8437240..5886ddf 100644 --- a/avisstage/decorators.py +++ b/avisstage/decorators.py @@ -1,7 +1,7 @@ from functools import wraps -from django.urls import reverse from django.shortcuts import redirect +from django.urls import reverse def en_scolarite_required(view_func): diff --git a/avisstage/documents.py b/avisstage/documents.py index f8fde43..9191a24 100644 --- a/avisstage/documents.py +++ b/avisstage/documents.py @@ -1,7 +1,7 @@ from django_elasticsearch_dsl import Document, Index, fields from elasticsearch_dsl import analyzer, token_filter, tokenizer -from .models import Stage, AvisStage, AvisLieu +from .models import AvisLieu, AvisStage, Stage from .statics import PAYS_OPTIONS PAYS_DICT = dict(PAYS_OPTIONS) diff --git a/avisstage/forms.py b/avisstage/forms.py index d3da9a4..d778e5c 100644 --- a/avisstage/forms.py +++ b/avisstage/forms.py @@ -1,16 +1,16 @@ +import re import unicodedata +from simple_email_confirmation.models import EmailAddress + 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, User +from .models import AvisLieu, AvisStage, Lieu, Normalien, Stage, User from .widgets import LatLonField + # Sur-classe utile class HTMLTrimmerForm(forms.ModelForm): def clean(self): diff --git a/avisstage/management/commands/nettoie_lieux.py b/avisstage/management/commands/nettoie_lieux.py index 02daea2..a857503 100644 --- a/avisstage/management/commands/nettoie_lieux.py +++ b/avisstage/management/commands/nettoie_lieux.py @@ -1,7 +1,8 @@ # coding: utf-8 from django.core.management.base import BaseCommand, CommandError from django.db.models import Count -from avisstage.models import Stage, Lieu + +from avisstage.models import Lieu, Stage class Command(BaseCommand): diff --git a/avisstage/management/commands/nettoie_stages.py b/avisstage/management/commands/nettoie_stages.py index eec8e9b..72600b9 100644 --- a/avisstage/management/commands/nettoie_stages.py +++ b/avisstage/management/commands/nettoie_stages.py @@ -1,7 +1,8 @@ # coding: utf-8 from django.core.management.base import BaseCommand, CommandError from django.db.models import Count -from avisstage.models import Stage, Lieu + +from avisstage.models import Lieu, Stage class Command(BaseCommand): diff --git a/avisstage/management/commands/supprime_lieu.py b/avisstage/management/commands/supprime_lieu.py index dd779a2..02c54aa 100644 --- a/avisstage/management/commands/supprime_lieu.py +++ b/avisstage/management/commands/supprime_lieu.py @@ -1,7 +1,8 @@ # coding: utf-8 from django.core.management.base import BaseCommand, CommandError from django.db.models import Count -from avisstage.models import Stage, Lieu + +from avisstage.models import Lieu, Stage class Command(BaseCommand): diff --git a/avisstage/management/commands/termine_scolarite.py b/avisstage/management/commands/termine_scolarite.py index aaf1e9e..41f489a 100644 --- a/avisstage/management/commands/termine_scolarite.py +++ b/avisstage/management/commands/termine_scolarite.py @@ -1,9 +1,10 @@ -from django.core.management.base import BaseCommand, CommandError -from avisstage.models import Normalien - -from django.utils import timezone from datetime import timedelta +from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone + +from avisstage.models import Normalien + class Command(BaseCommand): help = 'Réinitialise les statuts "en scolarité" de tout le monde' diff --git a/avisstage/migrations/0001_initial.py b/avisstage/migrations/0001_initial.py index a083c8d..85df322 100644 --- a/avisstage/migrations/0001_initial.py +++ b/avisstage/migrations/0001_initial.py @@ -2,14 +2,15 @@ # Generated by Django 1.11 on 2017-06-20 17:45 from __future__ import unicode_literals -from django.conf import settings -import django.contrib.gis.db.models.fields -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone import taggit_autosuggest.managers import tinymce.models +import django.contrib.gis.db.models.fields +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + class Migration(migrations.Migration): diff --git a/avisstage/migrations/0002_auto_20171002_2243.py b/avisstage/migrations/0002_auto_20171002_2243.py index bdfefce..f2b0136 100644 --- a/avisstage/migrations/0002_auto_20171002_2243.py +++ b/avisstage/migrations/0002_auto_20171002_2243.py @@ -2,9 +2,10 @@ # Generated by Django 1.11.2 on 2017-10-02 20:43 from __future__ import unicode_literals -from django.db import migrations, models import tinymce.models +from django.db import migrations, models + class Migration(migrations.Migration): diff --git a/avisstage/migrations/0003_auto_20210117_1208.py b/avisstage/migrations/0003_auto_20210117_1208.py index 4d13ea9..3b200ac 100644 --- a/avisstage/migrations/0003_auto_20210117_1208.py +++ b/avisstage/migrations/0003_auto_20210117_1208.py @@ -1,8 +1,8 @@ # Generated by Django 2.2.17 on 2021-01-17 11:08 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/avisstage/migrations/0004_allauth_to_authens.py b/avisstage/migrations/0004_allauth_to_authens.py index 29f8f8b..3ab2c60 100644 --- a/avisstage/migrations/0004_allauth_to_authens.py +++ b/avisstage/migrations/0004_allauth_to_authens.py @@ -1,6 +1,5 @@ from django.apps import apps as global_apps from django.db import migrations - from django.utils import timezone diff --git a/avisstage/migrations/0006_auto_20210131_1954.py b/avisstage/migrations/0006_auto_20210131_1954.py index c940e17..f7b7394 100644 --- a/avisstage/migrations/0006_auto_20210131_1954.py +++ b/avisstage/migrations/0006_auto_20210131_1954.py @@ -1,8 +1,9 @@ # Generated by Django 2.2.17 on 2021-01-31 18:54 -import avisstage.models from django.db import migrations, models +import avisstage.models + class Migration(migrations.Migration): diff --git a/avisstage/models.py b/avisstage/models.py index 9439748..c0c7838 100644 --- a/avisstage/models.py +++ b/avisstage/models.py @@ -1,34 +1,32 @@ -from django.db import models -from django.db.models.signals import post_save +from datetime import timedelta + +from authens.models import CASAccount +from authens.signals import post_cas_connect +from taggit_autosuggest.managers import TaggableManager +from tinymce.models import HTMLField as RichTextField + from django.contrib.auth.models import User from django.contrib.gis.db import models as geomodels -from django.template.defaultfilters import slugify +from django.db import models +from django.db.models.signals import post_save from django.forms.widgets import DateInput +from django.template.defaultfilters import slugify from django.urls import reverse - from django.utils import timezone from django.utils.functional import cached_property from django.utils.html import strip_tags -from taggit_autosuggest.managers import TaggableManager -from tinymce.models import HTMLField as RichTextField - -from authens.signals import post_cas_connect -from authens.models import CASAccount - -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, + NIVEAU_SCOL_OPTIONS, + PAYS_OPTIONS, + TYPE_LIEU_DICT, + TYPE_LIEU_OPTIONS, + TYPE_STAGE_DICT, + TYPE_STAGE_OPTIONS, ) +from .utils import choices_length, is_email_ens def _default_cas_login(): diff --git a/avisstage/templatetags/avisstage_tags.py b/avisstage/templatetags/avisstage_tags.py index bba61d5..eb31cc5 100644 --- a/avisstage/templatetags/avisstage_tags.py +++ b/avisstage/templatetags/avisstage_tags.py @@ -1,8 +1,9 @@ # coding: utf-8 +import re + from django import template -from avisstage.forms import LieuForm, FeedbackForm -import re +from avisstage.forms import FeedbackForm, LieuForm register = template.Library() diff --git a/avisstage/tests.py b/avisstage/tests.py index 2c9b760..559b50b 100644 --- a/avisstage/tests.py +++ b/avisstage/tests.py @@ -1,16 +1,15 @@ -from authens.tests.cas_utils import FakeCASClient -from authens.models import CASAccount, OldCASAccount - 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 +from authens.models import CASAccount, OldCASAccount +from authens.tests.cas_utils import FakeCASClient + +from django.conf import settings +from django.test import TestCase +from django.urls import reverse +from django.utils import timezone + +from .models import AvisLieu, Lieu, Normalien, Stage, StageMatiere, User class ExperiENSTestCase(TestCase): diff --git a/avisstage/urls.py b/avisstage/urls.py index b7ce528..97647e7 100644 --- a/avisstage/urls.py +++ b/avisstage/urls.py @@ -1,7 +1,9 @@ -from django.urls import include, path -from . import views, api from tastypie.api import Api +from django.urls import include, path + +from . import api, views + v1_api = Api(api_name="v1") v1_api.register(api.LieuResource()) v1_api.register(api.StageResource()) diff --git a/avisstage/views.py b/avisstage/views.py index 57fcaba..ce26aa2 100644 --- a/avisstage/views.py +++ b/avisstage/views.py @@ -1,49 +1,49 @@ # coding: utf-8 -from django.shortcuts import render, redirect, get_object_or_404 +import math +import random +from collections import Counter, defaultdict + +from braces.views import LoginRequiredMixin +from simple_email_confirmation.models import EmailAddress -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, reverse_lazy from django.conf import settings +from django.contrib import messages 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, - AdresseEmailForm, - ReinitMdpForm, +from django.db.models import Count, Q +from django.http import Http404, HttpResponseForbidden, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse, reverse_lazy +from django.views.generic import ( + CreateView, + DeleteView, + DetailView, + FormView, + ListView, + TemplateView, + UpdateView, + View, ) +from django.views.generic.detail import SingleObjectMixin + +from .forms import ( + AdresseEmailForm, + AvisLieuForm, + AvisStageForm, + FeedbackForm, + LieuForm, + ReinitMdpForm, + StageForm, +) +from .models import AvisLieu, AvisStage, Lieu, Normalien, Stage from .utils import en_scolarite - from .views_search import * -import random, math - # # LECTURE # diff --git a/avisstage/views_search.py b/avisstage/views_search.py index f8fe183..f88b411 100644 --- a/avisstage/views_search.py +++ b/avisstage/views_search.py @@ -1,18 +1,17 @@ # coding: utf-8 -from datetime import date - -from django import forms -from django.contrib.auth.decorators import login_required -from django.conf import settings -from django.core.cache import cache -from django.core.paginator import Paginator -from django.db.models import Q, Case, When -from django.http import JsonResponse, HttpResponseBadRequest -from django.shortcuts import render, redirect, get_object_or_404 - import json import logging +from datetime import date + +from django import forms +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.core.cache import cache +from django.core.paginator import Paginator +from django.db.models import Case, Q, When +from django.http import HttpResponseBadRequest, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render USE_ELASTICSEARCH = getattr(settings, "USE_ELASTICSEARCH", True) @@ -21,8 +20,7 @@ if USE_ELASTICSEARCH: from .decorators import en_scolarite_required from .models import Stage -from .statics import TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS, NIVEAU_SCOL_OPTIONS - +from .statics import NIVEAU_SCOL_OPTIONS, TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS logger = logging.getLogger("recherche") diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..81f23f8 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,10 @@ +[flake8] +max-line-length = 99 +exclude = .git, *.pyc, __pycache__, migrations +extend-ignore = E231, E203 + +[isort] +profile = black +known_django = django +known_first_party = avisstage +sections = FUTURE,STDLIB,THIRDPARTY,DJANGO,FIRSTPARTY,LOCALFOLDER From b53170feae54c37612be8c502182bf4869e80ff9 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 7 Feb 2021 23:50:15 +0100 Subject: [PATCH 14/23] =?UTF-8?q?On=20enl=C3=A8ve=20les=20u=20devant=20les?= =?UTF-8?q?=20strings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- avisstage/forms.py | 32 +- .../management/commands/nettoie_lieux.py | 14 +- .../management/commands/nettoie_stages.py | 12 +- .../management/commands/supprime_lieu.py | 8 +- .../management/commands/termine_scolarite.py | 2 +- avisstage/models.py | 88 +-- avisstage/statics.py | 594 +++++++++--------- avisstage/templatetags/avisstage_tags.py | 6 +- avisstage/views_search.py | 20 +- avisstage/widgets.py | 4 +- 10 files changed, 390 insertions(+), 390 deletions(-) diff --git a/avisstage/forms.py b/avisstage/forms.py index d778e5c..b6b6f13 100644 --- a/avisstage/forms.py +++ b/avisstage/forms.py @@ -39,10 +39,10 @@ class StageForm(forms.ModelForm): attrs={"class": "datepicker", "placeholder": "JJ/MM/AAAA"} ) date_debut = forms.DateField( - label=u"Date de début", input_formats=["%d/%m/%Y"], widget=date_widget + label="Date de début", input_formats=["%d/%m/%Y"], widget=date_widget ) date_fin = forms.DateField( - label=u"Date de fin", input_formats=["%d/%m/%Y"], widget=date_widget + label="Date de fin", input_formats=["%d/%m/%Y"], widget=date_widget ) class Meta: @@ -59,11 +59,11 @@ class StageForm(forms.ModelForm): "encadrants", ] help_texts = { - "thematiques": u"Mettez une virgule pour valider votre thématique si la suggestion ne correspond pas ou si elle n'existe pas encore", - "structure": u"Nom de l'équipe, du laboratoire, de la startup... (si le lieu ne suffit pas)", + "thematiques": "Mettez une virgule pour valider votre thématique si la suggestion ne correspond pas ou si elle n'existe pas encore", + "structure": "Nom de l'équipe, du laboratoire, de la startup... (si le lieu ne suffit pas)", } labels = { - "date_debut": u"Date de début", + "date_debut": "Date de début", } def __init__(self, *args, **kwargs): @@ -101,12 +101,12 @@ class AvisStageForm(HTMLTrimmerForm): ] help_texts = { "chapo": u'"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de ce séjour', - "avis_ambiance": u"Avez-vous passé un bon moment à ce travail ? Étiez-vous assez guidé⋅e ? Aviez-vous un bon contact avec vos encadrant⋅e⋅s ? Y avait-il une bonne ambiance dans l'équipe ?", - "avis_sujet": u"Quelle était votre mission ? Qu'en avez-vous retiré ? Le travail correspondait-il à vos attentes ? Était-ce à votre niveau, trop dur, trop facile ?", - "avis_admin": u"Avez-vous commencé votre travail à la date prévue ? Était-ce compliqué d'obtenir les documents nécessaires (visa, contrats, etc) ? L'administration de l'établissement vous a-t-elle aidé⋅e ? Étiez-vous rémunéré⋅e ?", - "avis_prestage": u"Comment avez-vous trouvé où aller pour cette expérience ? À quel moment avez-vous commencé à chercher ? Avez-vous eu des entretiens pour obtenir votre place ? Avez-vous eu d'autres pistes, pourquoi avez-vous choisi cette option ?", - "les_plus": u"Les principaux points positifs de cette expérience", - "les_moins": u"Ce qui aurait pu être mieux", + "avis_ambiance": "Avez-vous passé un bon moment à ce travail ? Étiez-vous assez guidé⋅e ? Aviez-vous un bon contact avec vos encadrant⋅e⋅s ? Y avait-il une bonne ambiance dans l'équipe ?", + "avis_sujet": "Quelle était votre mission ? Qu'en avez-vous retiré ? Le travail correspondait-il à vos attentes ? Était-ce à votre niveau, trop dur, trop facile ?", + "avis_admin": "Avez-vous commencé votre travail à la date prévue ? Était-ce compliqué d'obtenir les documents nécessaires (visa, contrats, etc) ? L'administration de l'établissement vous a-t-elle aidé⋅e ? Étiez-vous rémunéré⋅e ?", + "avis_prestage": "Comment avez-vous trouvé où aller pour cette expérience ? À quel moment avez-vous commencé à chercher ? Avez-vous eu des entretiens pour obtenir votre place ? Avez-vous eu d'autres pistes, pourquoi avez-vous choisi cette option ?", + "les_plus": "Les principaux points positifs de cette expérience", + "les_moins": "Ce qui aurait pu être mieux", } @@ -124,11 +124,11 @@ class AvisLieuForm(HTMLTrimmerForm): ] help_texts = { "chapo": u'"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de cet endroit', - "avis_lieustage": u"Qu'avez-vous pensé des lieux où vous travailliez ? Les bâtiments étaient-ils modernes ? Était-il agréable d'y travailler ?", - "avis_pratique": u"Avez-vous eu du mal à trouver un logement ? Y-a-t-il des choses que vous avez apprises sur place qu'il vous aurait été utile de savoir avant de partir ?", - "avis_tourisme": u"Y-a-t-il des lieux à visiter dans cette zone ? Avez-vous pratiqué des activités sportives ? Est-il facile de faire des rencontres ?", - "les_plus": u"Les meilleures raisons de partir à cet endroit", - "les_moins": u"Ce qui vous a gêné ou manqué là-bas", + "avis_lieustage": "Qu'avez-vous pensé des lieux où vous travailliez ? Les bâtiments étaient-ils modernes ? Était-il agréable d'y travailler ?", + "avis_pratique": "Avez-vous eu du mal à trouver un logement ? Y-a-t-il des choses que vous avez apprises sur place qu'il vous aurait été utile de savoir avant de partir ?", + "avis_tourisme": "Y-a-t-il des lieux à visiter dans cette zone ? Avez-vous pratiqué des activités sportives ? Est-il facile de faire des rencontres ?", + "les_plus": "Les meilleures raisons de partir à cet endroit", + "les_moins": "Ce qui vous a gêné ou manqué là-bas", } widgets = {"lieu": forms.HiddenInput(attrs={"class": "lieu-hidden"})} diff --git a/avisstage/management/commands/nettoie_lieux.py b/avisstage/management/commands/nettoie_lieux.py index a857503..38cb81c 100644 --- a/avisstage/management/commands/nettoie_lieux.py +++ b/avisstage/management/commands/nettoie_lieux.py @@ -22,7 +22,7 @@ class Command(BaseCommand): if options.get("apply", False): rundb = True else: - print(u"Les modifications ne seront pas appliquées") + print("Les modifications ne seront pas appliquées") min_lieu = options.get("min_lieu", 0) @@ -33,11 +33,11 @@ class Command(BaseCommand): if len(lproches) == 0: continue print( - u"Doublons possibles pour %s (id=%d, %d avis) :" + "Doublons possibles pour %s (id=%d, %d avis) :" % (lieu, lieu.id, lieu.avislieu_set.count()) ) for plieu in lproches: - pprint = u" > %s (id=%d, %d avis)" % ( + pprint = " > %s (id=%d, %d avis)" % ( plieu, plieu.id, plieu.avislieu_set.count(), @@ -47,7 +47,7 @@ class Command(BaseCommand): and plieu.ville == lieu.ville and plieu.type_lieu == lieu.type_lieu ): - print(u"%s %s" % (pprint, self.style.SUCCESS(u"-> Suppression"))) + print("%s %s" % (pprint, self.style.SUCCESS("-> Suppression"))) if rundb: for avis in plieu.avislieu_set.all(): avis.lieu = lieu @@ -55,7 +55,7 @@ class Command(BaseCommand): plieu.delete() else: print( - u"%s %s" - % (pprint, self.style.WARNING(u"-> À supprimer manuellement")) + "%s %s" + % (pprint, self.style.WARNING("-> À supprimer manuellement")) ) - self.stdout.write(self.style.SUCCESS(u"Nettoyage des lieux effectué")) + self.stdout.write(self.style.SUCCESS("Nettoyage des lieux effectué")) diff --git a/avisstage/management/commands/nettoie_stages.py b/avisstage/management/commands/nettoie_stages.py index 72600b9..f6b9f46 100644 --- a/avisstage/management/commands/nettoie_stages.py +++ b/avisstage/management/commands/nettoie_stages.py @@ -32,7 +32,7 @@ class Command(BaseCommand): if options.get("apply", False): rundb = True else: - print(u"Les modifications ne seront pas appliquées") + print("Les modifications ne seront pas appliquées") min_stage = options.get("min_stage", 0) @@ -55,10 +55,10 @@ class Command(BaseCommand): problems += [(avis, alen), lieuset[aid]] lieuset[aid] = (avis, alen) if len(todel) > 0: - print(u"Doublons détectés dans %s" % (stage,)) + print("Doublons détectés dans %s" % (stage,)) for avis, alen in todel: print( - u" > Suppression de l'avis sur %s de %d mots" + " > Suppression de l'avis sur %s de %d mots" % (avis.lieu, alen) ) if rundb: @@ -66,9 +66,9 @@ class Command(BaseCommand): if len(problems) > 0: self.stdout.write( self.style.WARNING( - u"Réparation impossible de %s (id=%d)" % (stage, stage.id) + "Réparation impossible de %s (id=%d)" % (stage, stage.id) ) ) for avis, alen in problems: - print(u" > Avis sur %s de %d mots" % (avis.lieu, alen)) - self.stdout.write(self.style.SUCCESS(u"Nettoyage des stages effectué")) + print(" > Avis sur %s de %d mots" % (avis.lieu, alen)) + self.stdout.write(self.style.SUCCESS("Nettoyage des stages effectué")) diff --git a/avisstage/management/commands/supprime_lieu.py b/avisstage/management/commands/supprime_lieu.py index 02c54aa..0efbafc 100644 --- a/avisstage/management/commands/supprime_lieu.py +++ b/avisstage/management/commands/supprime_lieu.py @@ -23,16 +23,16 @@ class Command(BaseCommand): if options.get("apply", False): rundb = True else: - print(u"Les modifications ne seront pas appliquées") + print("Les modifications ne seront pas appliquées") plieu = Lieu.objects.get(id=options["del_lieu"]) lieu = Lieu.objects.get(id=options["repl_lieu"]) print( - u"Suppression de %s (id=%d, %d avis)" + "Suppression de %s (id=%d, %d avis)" % (plieu, plieu.id, plieu.avislieu_set.count()) ) print( - u"Remplacement par %s (id=%d, %d avis)" + "Remplacement par %s (id=%d, %d avis)" % (lieu, lieu.id, lieu.avislieu_set.count()) ) if rundb: @@ -40,4 +40,4 @@ class Command(BaseCommand): avis.lieu = lieu avis.save() plieu.delete() - self.stdout.write(self.style.SUCCESS(u"Terminé")) + self.stdout.write(self.style.SUCCESS("Terminé")) diff --git a/avisstage/management/commands/termine_scolarite.py b/avisstage/management/commands/termine_scolarite.py index 41f489a..71a15ed 100644 --- a/avisstage/management/commands/termine_scolarite.py +++ b/avisstage/management/commands/termine_scolarite.py @@ -15,4 +15,4 @@ class Command(BaseCommand): 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é")) + self.stdout.write(self.style.SUCCESS("Terminé")) diff --git a/avisstage/models.py b/avisstage/models.py index c0c7838..9502635 100644 --- a/avisstage/models.py +++ b/avisstage/models.py @@ -44,22 +44,22 @@ class Normalien(models.Model): ) # Infos spécifiques - nom = models.CharField(u"Nom complet", max_length=255, blank=True) - promotion = models.CharField(u"Promotion", max_length=40, blank=True) + nom = models.CharField("Nom complet", max_length=255, blank=True) + promotion = models.CharField("Promotion", max_length=40, blank=True) contactez_moi = models.BooleanField( - u"Inviter les visiteurs à me contacter", + "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="") + bio = models.TextField("À propos de moi", blank=True, default="") last_cas_login = models.DateField(default=_default_cas_login) class Meta: - verbose_name = u"Profil élève" - verbose_name_plural = u"Profils élèves" + verbose_name = "Profil élève" + verbose_name_plural = "Profils élèves" def __str__(self): - return u"%s (%s)" % (self.nom, self.user.username) + return "%s (%s)" % (self.nom, self.user.username) # Liste des stages publiés def stages_publics(self): @@ -135,23 +135,23 @@ post_cas_connect.connect(handle_cas_connection, sender=User) class Lieu(models.Model): # Général - nom = models.CharField(u"Nom de l'institution d'accueil", max_length=250) + nom = models.CharField("Nom de l'institution d'accueil", max_length=250) type_lieu = models.CharField( - u"Type de structure d'accueil", + "Type de structure d'accueil", default="universite", choices=TYPE_LIEU_OPTIONS, max_length=choices_length(TYPE_LIEU_OPTIONS), ) # Infos géographiques - ville = models.CharField(u"Ville", max_length=200) + ville = models.CharField("Ville", max_length=200) pays = models.CharField( - u"Pays", choices=PAYS_OPTIONS, max_length=choices_length(PAYS_OPTIONS) + "Pays", choices=PAYS_OPTIONS, max_length=choices_length(PAYS_OPTIONS) ) # Coordonnées # objects = geomodels.GeoManager() # Requis par GeoDjango - coord = geomodels.PointField(u"Coordonnées", geography=True, srid=4326) + coord = geomodels.PointField("Coordonnées", geography=True, srid=4326) # Type du lieu en plus joli @property @@ -163,7 +163,7 @@ class Lieu(models.Model): return TYPE_LIEU_DICT.get(self.type_lieu, ("lieu", False))[1] def __str__(self): - return u"%s (%s)" % (self.nom, self.ville) + return "%s (%s)" % (self.nom, self.ville) class Meta: verbose_name = "Lieu" @@ -176,7 +176,7 @@ class Lieu(models.Model): class StageMatiere(models.Model): - nom = models.CharField(u"Nom", max_length=30) + nom = models.CharField("Nom", max_length=30) slug = models.SlugField() class Meta: @@ -197,38 +197,38 @@ class Stage(models.Model): 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) - len_avis_stage = models.IntegerField(u"Longueur des avis de stage", default=0) - len_avis_lieux = models.IntegerField(u"Longueur des avis de lieu", default=0) + public = models.BooleanField("Visible publiquement", default=False) + date_creation = models.DateTimeField("Créé le", default=timezone.now) + date_maj = models.DateTimeField("Mis à jour le", default=timezone.now) + len_avis_stage = models.IntegerField("Longueur des avis de stage", default=0) + len_avis_lieux = models.IntegerField("Longueur des avis de lieu", default=0) # Caractéristiques du stage - sujet = models.CharField(u"Sujet", max_length=500) + sujet = models.CharField("Sujet", max_length=500) - date_debut = models.DateField(u"Date de début", null=True) - date_fin = models.DateField(u"Date de fin", null=True) + date_debut = models.DateField("Date de début", null=True) + date_fin = models.DateField("Date de fin", null=True) type_stage = models.CharField( - u"Type", + "Type", default="stage", choices=TYPE_STAGE_OPTIONS, max_length=choices_length(TYPE_STAGE_OPTIONS), ) niveau_scol = models.CharField( - u"Année de scolarité", + "Année de scolarité", default="", choices=NIVEAU_SCOL_OPTIONS, max_length=choices_length(NIVEAU_SCOL_OPTIONS), blank=True, ) - thematiques = TaggableManager(u"Thématiques", blank=True) + thematiques = TaggableManager("Thématiques", blank=True) matieres = models.ManyToManyField( - StageMatiere, verbose_name=u"Matière(s)", related_name="stages" + StageMatiere, verbose_name="Matière(s)", related_name="stages" ) - encadrants = models.CharField(u"Encadrant⋅e⋅s", max_length=500, blank=True) - structure = models.CharField(u"Structure d'accueil", max_length=500, blank=True) + encadrants = models.CharField("Encadrant⋅e⋅s", max_length=500, blank=True) + structure = models.CharField("Structure d'accueil", max_length=500, blank=True) # Avis lieux = models.ManyToManyField( @@ -271,7 +271,7 @@ class Stage(models.Model): return reverse("avisstage:stage", self) def __str__(self): - return u"%s (par %s)" % (self.sujet, self.auteur.user.username) + return "%s (par %s)" % (self.sujet, self.auteur.user.username) def update_stats(self, save=True): def get_len(obj): @@ -307,17 +307,17 @@ class AvisStage(models.Model): 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) - avis_sujet = RichTextField(u"La mission", blank=True) - avis_admin = RichTextField(u"Formalités et administration", blank=True) - avis_prestage = RichTextField(u"Avant le stage", blank=True, default="") + chapo = models.TextField("En quelques mots", blank=True) + avis_ambiance = RichTextField("L'ambiance de travail", blank=True) + avis_sujet = RichTextField("La mission", blank=True) + avis_admin = RichTextField("Formalités et administration", blank=True) + avis_prestage = RichTextField("Avant le stage", blank=True, default="") - les_plus = models.TextField(u"Les plus de cette expérience", blank=True) - les_moins = models.TextField(u"Les moins de cette expérience", blank=True) + les_plus = models.TextField("Les plus de cette expérience", blank=True) + les_moins = models.TextField("Les moins de cette expérience", blank=True) def __str__(self): - return u"Avis sur {%s} par %s" % ( + return "Avis sur {%s} par %s" % ( self.stage.sujet, self.stage.auteur.user.username, ) @@ -337,20 +337,20 @@ class AvisLieu(models.Model): lieu = models.ForeignKey(Lieu, on_delete=models.CASCADE) order = models.IntegerField("Ordre", default=0) - chapo = models.TextField(u"En quelques mots", blank=True) - avis_lieustage = RichTextField(u"Les lieux de travail", blank=True) - avis_pratique = RichTextField(u"S'installer - conseils pratiques", blank=True) - avis_tourisme = RichTextField(u"Dans les parages", blank=True) + chapo = models.TextField("En quelques mots", blank=True) + avis_lieustage = RichTextField("Les lieux de travail", blank=True) + avis_pratique = RichTextField("S'installer - conseils pratiques", blank=True) + avis_tourisme = RichTextField("Dans les parages", blank=True) - les_plus = models.TextField(u"Les plus du lieu", blank=True) - les_moins = models.TextField(u"Les moins du lieu", blank=True) + les_plus = models.TextField("Les plus du lieu", blank=True) + les_moins = models.TextField("Les moins du lieu", blank=True) class Meta: verbose_name = "Avis sur un lieu de stage" verbose_name_plural = "Avis sur un lieu de stage" def __str__(self): - return u"Avis sur {%s} par %s" % (self.lieu.nom, self.stage.auteur.user_id) + return "Avis sur {%s} par %s" % (self.lieu.nom, self.stage.auteur.user_id) # Liste des champs d'avis, couplés à leur nom (pour l'affichage) @property diff --git a/avisstage/statics.py b/avisstage/statics.py index 6671b5c..ce5a9fd 100644 --- a/avisstage/statics.py +++ b/avisstage/statics.py @@ -1,339 +1,339 @@ # coding: utf-8 DEPARTEMENTS_DEFAUT = ( - ("phy", u"Physique"), - ("maths", u"Maths"), - ("bio", u"Biologie"), - ("chimie", u"Chimie"), - ("geol", u"Géosciences"), - ("dec", u"DEC"), - ("info", u"Informatique"), - ("litt", u"Littéraire"), - ("guests", u"Pensionnaires étrangers"), - ("pei", u"PEI"), + ("phy", "Physique"), + ("maths", "Maths"), + ("bio", "Biologie"), + ("chimie", "Chimie"), + ("geol", "Géosciences"), + ("dec", "DEC"), + ("info", "Informatique"), + ("litt", "Littéraire"), + ("guests", "Pensionnaires étrangers"), + ("pei", "PEI"), ) TYPE_STAGE_OPTIONS = ( ( - u"Recherche :", + "Recherche :", ( - ("recherche", u"Stage académique"), - ("recherche_autre", u"Stage non-académique"), - ("sejour_dri", u"Séjour de recherche DRI"), + ("recherche", "Stage académique"), + ("recherche_autre", "Stage non-académique"), + ("sejour_dri", "Séjour de recherche DRI"), ), ), ( - u"Stage sans visée de recherche :", + "Stage sans visée de recherche :", ( - ("pro", u"Stage en entreprise"), - ("admin", u"Stage en admin./ONG/orga. internationale"), + ("pro", "Stage en entreprise"), + ("admin", "Stage en admin./ONG/orga. internationale"), ), ), ( - u"Enseignement :", + "Enseignement :", ( - ("lectorat", u"Lectorat DRI"), - ("autre_teach", u"Autre expérience d'enseignement"), + ("lectorat", "Lectorat DRI"), + ("autre_teach", "Autre expérience d'enseignement"), ), ), - ("autre", u"Autre"), + ("autre", "Autre"), ) # Dictionnaire des type de stage (et de leur genre, True=féminin) TYPE_STAGE_DICT = { - "recherche": (u"stage de recherche académique", False), - "recherche_autre": (u"stage de recherche non-académique", False), - "sejour_dri": (u"séjour de recherche DRI", False), - "pro": (u"stage en entreprise sans visée de recherche", False), - "admin": (u"stage en administration, ONG ou organisation internationale", False), - "lectorat": (u"lectorat DRI", False), - "autre_teach": (u"expérience de recherche", True), - "autre": (u"expérience", True), + "recherche": ("stage de recherche académique", False), + "recherche_autre": ("stage de recherche non-académique", False), + "sejour_dri": ("séjour de recherche DRI", False), + "pro": ("stage en entreprise sans visée de recherche", False), + "admin": ("stage en administration, ONG ou organisation internationale", False), + "lectorat": ("lectorat DRI", False), + "autre_teach": ("expérience de recherche", True), + "autre": ("expérience", True), } TYPE_LIEU_OPTIONS = ( - ("universite", u"Université"), - ("entreprise", u"Entreprise"), - ("centrerecherche", u"Centre de recherche"), - ("administration", u"Administration"), - ("autre", u"Autre"), + ("universite", "Université"), + ("entreprise", "Entreprise"), + ("centrerecherche", "Centre de recherche"), + ("administration", "Administration"), + ("autre", "Autre"), ) # Place du stage dans le cursus NIVEAU_SCOL_OPTIONS = ( - ("L3", u"Licence 3"), - ("M1", u"Master 1"), - ("M2", u"Master 2"), - ("DOC", u"Pré-doctorat"), - ("CST", u"Césure"), - ("BLA", u"Année blanche"), - ("VAC", u"Vacances scolaires"), - ("MIT", u"Mi-temps en parallèle des études"), - ("", u"Autre"), + ("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"), ) NIVEAU_SCOL_DICT = { - "L3": u"pendant sa troisième année de Licence", - "M1": u"pendant sa première année de Master", - "M2": u"pendant sa deuxième année de Master", - "DOC": u"pendant son année de pré-doctorat", - "CST": u"pendant une année de césure", - "BLA": u"pendant une année blanche", - "VAC": u"pendant des vacances scolaires", - "MIT": u"à mi-temps en parallèle des études", + "L3": "pendant sa troisième année de Licence", + "M1": "pendant sa première année de Master", + "M2": "pendant sa deuxième année de Master", + "DOC": "pendant son année de pré-doctorat", + "CST": "pendant une année de césure", + "BLA": "pendant une année blanche", + "VAC": "pendant des vacances scolaires", + "MIT": "à mi-temps en parallèle des études", } # Dictionnaire des noms de lieux (et de leur genre, True=féminin) TYPE_LIEU_DICT = { - "universite": (u"université", True), - "entreprise": (u"entreprise", True), - "centrerecherche": (u"centre de recherche", False), - "administration": (u"administration", True), - "autre": (u"lieu", False), + "universite": ("université", True), + "entreprise": ("entreprise", True), + "centrerecherche": ("centre de recherche", False), + "administration": ("administration", True), + "autre": ("lieu", False), } PAYS_OPTIONS = ( - ("AF", u"Afghanistan"), - ("AL", u"Albanie"), - ("AQ", u"Antarctique"), - ("DZ", u"Algérie"), - ("AS", u"Samoa Américaines"), - ("AD", u"Andorre"), - ("AO", u"Angola"), - ("AG", u"Antigua-et-Barbuda"), - ("AZ", u"Azerbaïdjan"), - ("AR", u"Argentine"), - ("AU", u"Australie"), - ("AT", u"Autriche"), - ("BS", u"Bahamas"), - ("BH", u"Bahreïn"), - ("BD", u"Bangladesh"), - ("AM", u"Arménie"), - ("BB", u"Barbade"), - ("BE", u"Belgique"), - ("BM", u"Bermudes"), - ("BT", u"Bhoutan"), - ("BO", u"Bolivie"), - ("BA", u"Bosnie-Herzégovine"), - ("BW", u"Botswana"), - ("BV", u"Île Bouvet"), - ("BR", u"Brésil"), - ("BZ", u"Belize"), - ("IO", u"Territoire Britannique de l'Océan Indien"), - ("SB", u"Îles Salomon"), - ("VG", u"Îles Vierges Britanniques"), - ("BN", u"Brunéi Darussalam"), - ("BG", u"Bulgarie"), - ("MM", u"Myanmar"), - ("BI", u"Burundi"), - ("BY", u"Bélarus"), - ("KH", u"Cambodge"), - ("CM", u"Cameroun"), - ("CA", u"Canada"), - ("CV", u"Cap-vert"), - ("KY", u"Îles Caïmanes"), - ("CF", u"République Centrafricaine"), - ("LK", u"Sri Lanka"), - ("TD", u"Tchad"), - ("CL", u"Chili"), - ("CN", u"Chine"), - ("TW", u"Taïwan"), - ("CX", u"Île Christmas"), - ("CC", u"Îles Cocos (Keeling)"), - ("CO", u"Colombie"), - ("KM", u"Comores"), - ("YT", u"Mayotte"), - ("CG", u"République du Congo"), - ("CD", u"République Démocratique du Congo"), - ("CK", u"Îles Cook"), - ("CR", u"Costa Rica"), - ("HR", u"Croatie"), - ("CU", u"Cuba"), - ("CY", u"Chypre"), - ("CZ", u"République Tchèque"), - ("BJ", u"Bénin"), - ("DK", u"Danemark"), - ("DM", u"Dominique"), - ("DO", u"République Dominicaine"), - ("EC", u"Équateur"), - ("SV", u"El Salvador"), - ("GQ", u"Guinée Équatoriale"), - ("ET", u"Éthiopie"), - ("ER", u"Érythrée"), - ("EE", u"Estonie"), - ("FO", u"Îles Féroé"), - ("FK", u"Îles (malvinas) Falkland"), - ("GS", u"Géorgie du Sud et les Îles Sandwich du Sud"), - ("FJ", u"Fidji"), - ("FI", u"Finlande"), - ("AX", u"Îles Åland"), - ("FR", u"France"), - ("GF", u"Guyane Française"), - ("PF", u"Polynésie Française"), - ("TF", u"Terres Australes Françaises"), - ("DJ", u"Djibouti"), - ("GA", u"Gabon"), - ("GE", u"Géorgie"), - ("GM", u"Gambie"), - ("PS", u"Territoire Palestinien Occupé"), - ("DE", u"Allemagne"), - ("GH", u"Ghana"), - ("GI", u"Gibraltar"), - ("KI", u"Kiribati"), - ("GR", u"Grèce"), - ("GL", u"Groenland"), - ("GD", u"Grenade"), - ("GP", u"Guadeloupe"), - ("GU", u"Guam"), - ("GT", u"Guatemala"), - ("GN", u"Guinée"), - ("GY", u"Guyana"), - ("HT", u"Haïti"), - ("HM", u"Îles Heard et Mcdonald"), - ("VA", u"Saint-Siège (état de la Cité du Vatican)"), - ("HN", u"Honduras"), - ("HK", u"Hong-Kong"), - ("HU", u"Hongrie"), - ("IS", u"Islande"), - ("IN", u"Inde"), - ("ID", u"Indonésie"), - ("IR", u"République Islamique d'Iran"), - ("IQ", u"Iraq"), - ("IE", u"Irlande"), - ("IL", u"Israël"), - ("IT", u"Italie"), - ("CI", u"Côte d'Ivoire"), - ("JM", u"Jamaïque"), - ("JP", u"Japon"), - ("KZ", u"Kazakhstan"), - ("JO", u"Jordanie"), - ("KE", u"Kenya"), - ("KP", u"République Populaire Démocratique de Corée"), - ("KR", u"République de Corée"), - ("KW", u"Koweït"), - ("KG", u"Kirghizistan"), - ("LA", u"République Démocratique Populaire Lao"), - ("LB", u"Liban"), - ("LS", u"Lesotho"), - ("LV", u"Lettonie"), - ("LR", u"Libéria"), - ("LY", u"Jamahiriya Arabe Libyenne"), - ("LI", u"Liechtenstein"), - ("LT", u"Lituanie"), - ("LU", u"Luxembourg"), - ("MO", u"Macao"), - ("MG", u"Madagascar"), - ("MW", u"Malawi"), - ("MY", u"Malaisie"), - ("MV", u"Maldives"), - ("ML", u"Mali"), - ("MT", u"Malte"), - ("MQ", u"Martinique"), - ("MR", u"Mauritanie"), - ("MU", u"Maurice"), - ("MX", u"Mexique"), - ("MC", u"Monaco"), - ("MN", u"Mongolie"), - ("MD", u"République de Moldova"), - ("MS", u"Montserrat"), - ("MA", u"Maroc"), - ("MZ", u"Mozambique"), - ("OM", u"Oman"), - ("NA", u"Namibie"), - ("NR", u"Nauru"), - ("NP", u"Népal"), - ("NL", u"Pays-Bas"), - ("AN", u"Antilles Néerlandaises"), - ("AW", u"Aruba"), - ("NC", u"Nouvelle-Calédonie"), - ("VU", u"Vanuatu"), - ("NZ", u"Nouvelle-Zélande"), - ("NI", u"Nicaragua"), - ("NE", u"Niger"), - ("NG", u"Nigéria"), - ("NU", u"Niué"), - ("NF", u"Île Norfolk"), - ("NO", u"Norvège"), - ("MP", u"Îles Mariannes du Nord"), - ("UM", u"Îles Mineures Éloignées des États-Unis"), - ("FM", u"États Fédérés de Micronésie"), - ("MH", u"Îles Marshall"), - ("PW", u"Palaos"), - ("PK", u"Pakistan"), - ("PA", u"Panama"), - ("PG", u"Papouasie-Nouvelle-Guinée"), - ("PY", u"Paraguay"), - ("PE", u"Pérou"), - ("PH", u"Philippines"), - ("PN", u"Pitcairn"), - ("PL", u"Pologne"), - ("PT", u"Portugal"), - ("GW", u"Guinée-Bissau"), - ("TL", u"Timor-Leste"), - ("PR", u"Porto Rico"), - ("QA", u"Qatar"), - ("RE", u"Réunion"), - ("RO", u"Roumanie"), - ("RU", u"Fédération de Russie"), - ("RW", u"Rwanda"), - ("SH", u"Sainte-Hélène"), - ("KN", u"Saint-Kitts-et-Nevis"), - ("AI", u"Anguilla"), - ("LC", u"Sainte-Lucie"), - ("PM", u"Saint-Pierre-et-Miquelon"), - ("VC", u"Saint-Vincent-et-les Grenadines"), - ("SM", u"Saint-Marin"), - ("ST", u"Sao Tomé-et-Principe"), - ("SA", u"Arabie Saoudite"), - ("SN", u"Sénégal"), - ("SC", u"Seychelles"), - ("SL", u"Sierra Leone"), - ("SG", u"Singapour"), - ("SK", u"Slovaquie"), - ("VN", u"Viet Nam"), - ("SI", u"Slovénie"), - ("SO", u"Somalie"), - ("ZA", u"Afrique du Sud"), - ("ZW", u"Zimbabwe"), - ("ES", u"Espagne"), - ("EH", u"Sahara Occidental"), - ("SD", u"Soudan"), - ("SR", u"Suriname"), - ("SJ", u"Svalbard etÎle Jan Mayen"), - ("SZ", u"Swaziland"), - ("SE", u"Suède"), - ("CH", u"Suisse"), - ("SY", u"République Arabe Syrienne"), - ("TJ", u"Tadjikistan"), - ("TH", u"Thaïlande"), - ("TG", u"Togo"), - ("TK", u"Tokelau"), - ("TO", u"Tonga"), - ("TT", u"Trinité-et-Tobago"), - ("AE", u"Émirats Arabes Unis"), - ("TN", u"Tunisie"), - ("TR", u"Turquie"), - ("TM", u"Turkménistan"), - ("TC", u"Îles Turks et Caïques"), - ("TV", u"Tuvalu"), - ("UG", u"Ouganda"), - ("UA", u"Ukraine"), - ("MK", u"L'ex-République Yougoslave de Macédoine"), - ("EG", u"Égypte"), - ("GB", u"Royaume-Uni"), - ("IM", u"Île de Man"), - ("TZ", u"République-Unie de Tanzanie"), - ("US", u"États-Unis"), - ("VI", u"Îles Vierges des États-Unis"), - ("BF", u"Burkina Faso"), - ("UY", u"Uruguay"), - ("UZ", u"Ouzbékistan"), - ("VE", u"Venezuela"), - ("WF", u"Wallis et Futuna"), - ("WS", u"Samoa"), - ("YE", u"Yémen"), - ("CS", u"Serbie-et-Monténégro"), - ("ZM", u"Zambie"), + ("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"), ) diff --git a/avisstage/templatetags/avisstage_tags.py b/avisstage/templatetags/avisstage_tags.py index eb31cc5..3b966f2 100644 --- a/avisstage/templatetags/avisstage_tags.py +++ b/avisstage/templatetags/avisstage_tags.py @@ -22,9 +22,9 @@ def feedback_widget(): @register.filter def typonazisme(value): - value = re.sub(r"(\w)\s*([?!:])", u"\\1 \\2", value) - value = re.sub(r"(\w)\s*([,.])", u"\\1\\2", value) - value = re.sub(r"([?!:,.])(\w)", u"\\1 \\2", value) + value = re.sub(r"(\w)\s*([?!:])", "\\1 \\2", value) + value = re.sub(r"(\w)\s*([,.])", "\\1\\2", value) + value = re.sub(r"([?!:,.])(\w)", "\\1 \\2", value) return value diff --git a/avisstage/views_search.py b/avisstage/views_search.py index f88b411..94cbcdf 100644 --- a/avisstage/views_search.py +++ b/avisstage/views_search.py @@ -27,33 +27,33 @@ logger = logging.getLogger("recherche") # Recherche class SearchForm(forms.Form): generique = forms.CharField(required=False) - sujet = forms.CharField(label=u"À propos de", required=False) + sujet = forms.CharField(label="À propos de", required=False) contexte = forms.CharField( - label=u"Contexte (lieu, encadrant⋅e⋅s, structure)", required=False + label="Contexte (lieu, encadrant⋅e⋅s, structure)", required=False ) - apres_annee = forms.IntegerField(label=u"Après cette année", required=False) - avant_annee = forms.IntegerField(label=u"Avant cette année", required=False) + apres_annee = forms.IntegerField(label="Après cette année", required=False) + avant_annee = forms.IntegerField(label="Avant cette année", required=False) type_stage = forms.ChoiceField( label="Type de stage", - choices=([("", u"")] + list(TYPE_STAGE_OPTIONS)), + choices=([("", "")] + list(TYPE_STAGE_OPTIONS)), required=False, ) niveau_scol = forms.ChoiceField( label="Année d'étude", - choices=([("", u"")] + list(NIVEAU_SCOL_OPTIONS)), + choices=([("", "")] + list(NIVEAU_SCOL_OPTIONS)), required=False, ) type_lieu = forms.ChoiceField( - label=u"Type de lieu d'accueil", - choices=([("", u"")] + list(TYPE_LIEU_OPTIONS)), + label="Type de lieu d'accueil", + choices=([("", "")] + list(TYPE_LIEU_OPTIONS)), required=False, ) tri = forms.ChoiceField( - label=u"Tri par", - choices=[("pertinence", u"Pertinence"), ("-date_maj", u"Dernière mise à jour")], + label="Tri par", + choices=[("pertinence", "Pertinence"), ("-date_maj", "Dernière mise à jour")], required=False, initial="pertinence", ) diff --git a/avisstage/widgets.py b/avisstage/widgets.py index e3487d6..3b332e9 100644 --- a/avisstage/widgets.py +++ b/avisstage/widgets.py @@ -23,8 +23,8 @@ class LatLonField(forms.MultiValueField): srid = 4326 default_error_messages = { - "invalid_latitude": (u"Entrez une latitude valide."), - "invalid_longitude": (u"Entrez une longitude valide."), + "invalid_latitude": ("Entrez une latitude valide."), + "invalid_longitude": ("Entrez une longitude valide."), } def __init__(self, *args, **kwargs): From 32ba0e61112aa2e76d4540bf3e5b322e847ecffc Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 8 Feb 2021 02:27:37 +0100 Subject: [PATCH 15/23] flake8 --- avisstage/api.py | 6 ++---- avisstage/documents.py | 4 ++-- avisstage/forms.py | 12 +++++++----- avisstage/management/commands/nettoie_lieux.py | 6 ++---- avisstage/management/commands/nettoie_stages.py | 8 +++----- avisstage/management/commands/supprime_lieu.py | 6 ++---- avisstage/management/commands/termine_scolarite.py | 4 ++-- avisstage/models.py | 7 ++----- avisstage/statics.py | 2 -- avisstage/tests.py | 2 +- avisstage/views.py | 9 ++------- avisstage/views_search.py | 7 +++---- experiENS/settings_base.py | 2 +- experiENS/settings_dev.py | 4 +++- experiENS/settings_prod.py | 4 ++-- experiENS/urls.py | 2 +- scripts/initalize_allauth.py | 3 +-- setup.cfg | 2 +- 18 files changed, 37 insertions(+), 53 deletions(-) diff --git a/avisstage/api.py b/avisstage/api.py index 4d53f95..5090895 100644 --- a/avisstage/api.py +++ b/avisstage/api.py @@ -1,13 +1,11 @@ -# coding: utf-8 - -from tastypie import fields, utils +from tastypie import fields from tastypie.authentication import SessionAuthentication from tastypie.resources import ModelResource from django.contrib.gis import geos from django.urls import reverse -from .models import Lieu, Normalien, Stage, StageMatiere +from .models import Lieu, Normalien, Stage from .utils import approximate_distance diff --git a/avisstage/documents.py b/avisstage/documents.py index 9191a24..04a9852 100644 --- a/avisstage/documents.py +++ b/avisstage/documents.py @@ -1,7 +1,7 @@ from django_elasticsearch_dsl import Document, Index, fields -from elasticsearch_dsl import analyzer, token_filter, tokenizer +from elasticsearch_dsl import analyzer, token_filter -from .models import AvisLieu, AvisStage, Stage +from .models import Stage from .statics import PAYS_OPTIONS PAYS_DICT = dict(PAYS_OPTIONS) diff --git a/avisstage/forms.py b/avisstage/forms.py index b6b6f13..5356ae6 100644 --- a/avisstage/forms.py +++ b/avisstage/forms.py @@ -7,7 +7,7 @@ from django import forms from django.contrib.auth.forms import PasswordResetForm from django.utils import timezone -from .models import AvisLieu, AvisStage, Lieu, Normalien, Stage, User +from .models import AvisLieu, AvisStage, Lieu, Stage, User from .widgets import LatLonField @@ -59,8 +59,10 @@ class StageForm(forms.ModelForm): "encadrants", ] help_texts = { - "thematiques": "Mettez une virgule pour valider votre thématique si la suggestion ne correspond pas ou si elle n'existe pas encore", - "structure": "Nom de l'équipe, du laboratoire, de la startup... (si le lieu ne suffit pas)", + "thematiques": "Mettez une virgule pour valider votre thématique si la suggestion ne " + "correspond pas ou si elle n'existe pas encore", + "structure": "Nom de l'équipe, du laboratoire, de la startup... (si le lieu ne suffit " + "pas)", } labels = { "date_debut": "Date de début", @@ -100,7 +102,7 @@ class AvisStageForm(HTMLTrimmerForm): "les_moins", ] help_texts = { - "chapo": u'"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de ce séjour', + "chapo": '"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de ce séjour', "avis_ambiance": "Avez-vous passé un bon moment à ce travail ? Étiez-vous assez guidé⋅e ? Aviez-vous un bon contact avec vos encadrant⋅e⋅s ? Y avait-il une bonne ambiance dans l'équipe ?", "avis_sujet": "Quelle était votre mission ? Qu'en avez-vous retiré ? Le travail correspondait-il à vos attentes ? Était-ce à votre niveau, trop dur, trop facile ?", "avis_admin": "Avez-vous commencé votre travail à la date prévue ? Était-ce compliqué d'obtenir les documents nécessaires (visa, contrats, etc) ? L'administration de l'établissement vous a-t-elle aidé⋅e ? Étiez-vous rémunéré⋅e ?", @@ -123,7 +125,7 @@ class AvisLieuForm(HTMLTrimmerForm): "les_moins", ] help_texts = { - "chapo": u'"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de cet endroit', + "chapo": '"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de cet endroit', "avis_lieustage": "Qu'avez-vous pensé des lieux où vous travailliez ? Les bâtiments étaient-ils modernes ? Était-il agréable d'y travailler ?", "avis_pratique": "Avez-vous eu du mal à trouver un logement ? Y-a-t-il des choses que vous avez apprises sur place qu'il vous aurait été utile de savoir avant de partir ?", "avis_tourisme": "Y-a-t-il des lieux à visiter dans cette zone ? Avez-vous pratiqué des activités sportives ? Est-il facile de faire des rencontres ?", diff --git a/avisstage/management/commands/nettoie_lieux.py b/avisstage/management/commands/nettoie_lieux.py index 38cb81c..8644b81 100644 --- a/avisstage/management/commands/nettoie_lieux.py +++ b/avisstage/management/commands/nettoie_lieux.py @@ -1,8 +1,6 @@ -# coding: utf-8 -from django.core.management.base import BaseCommand, CommandError -from django.db.models import Count +from django.core.management.base import BaseCommand -from avisstage.models import Lieu, Stage +from avisstage.models import Lieu class Command(BaseCommand): diff --git a/avisstage/management/commands/nettoie_stages.py b/avisstage/management/commands/nettoie_stages.py index f6b9f46..18c1418 100644 --- a/avisstage/management/commands/nettoie_stages.py +++ b/avisstage/management/commands/nettoie_stages.py @@ -1,8 +1,7 @@ -# coding: utf-8 -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from django.db.models import Count -from avisstage.models import Lieu, Stage +from avisstage.models import Stage class Command(BaseCommand): @@ -58,8 +57,7 @@ class Command(BaseCommand): print("Doublons détectés dans %s" % (stage,)) for avis, alen in todel: print( - " > Suppression de l'avis sur %s de %d mots" - % (avis.lieu, alen) + " > Suppression de l'avis sur %s de %d mots" % (avis.lieu, alen) ) if rundb: avis.delete() diff --git a/avisstage/management/commands/supprime_lieu.py b/avisstage/management/commands/supprime_lieu.py index 0efbafc..c609a46 100644 --- a/avisstage/management/commands/supprime_lieu.py +++ b/avisstage/management/commands/supprime_lieu.py @@ -1,8 +1,6 @@ -# coding: utf-8 -from django.core.management.base import BaseCommand, CommandError -from django.db.models import Count +from django.core.management.base import BaseCommand -from avisstage.models import Lieu, Stage +from avisstage.models import Lieu class Command(BaseCommand): diff --git a/avisstage/management/commands/termine_scolarite.py b/avisstage/management/commands/termine_scolarite.py index 71a15ed..6dc7ba5 100644 --- a/avisstage/management/commands/termine_scolarite.py +++ b/avisstage/management/commands/termine_scolarite.py @@ -1,6 +1,6 @@ from datetime import timedelta -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from django.utils import timezone from avisstage.models import Normalien @@ -13,6 +13,6 @@ class Command(BaseCommand): return def handle(self, *args, **options): - old_conn = timezone.now() - timedelta(days=365) + t = timezone.now() - timedelta(days=365) Normalien.objects.all().update(last_cas_connect=t) self.stdout.write(self.style.SUCCESS("Terminé")) diff --git a/avisstage/models.py b/avisstage/models.py index 9502635..3d0794a 100644 --- a/avisstage/models.py +++ b/avisstage/models.py @@ -1,6 +1,5 @@ from datetime import timedelta -from authens.models import CASAccount from authens.signals import post_cas_connect from taggit_autosuggest.managers import TaggableManager from tinymce.models import HTMLField as RichTextField @@ -9,12 +8,9 @@ from django.contrib.auth.models import User from django.contrib.gis.db import models as geomodels from django.db import models from django.db.models.signals import post_save -from django.forms.widgets import DateInput -from django.template.defaultfilters import slugify from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property -from django.utils.html import strip_tags from .statics import ( DEPARTEMENTS_DEFAUT, @@ -26,7 +22,7 @@ from .statics import ( TYPE_STAGE_DICT, TYPE_STAGE_OPTIONS, ) -from .utils import choices_length, is_email_ens +from .utils import choices_length def _default_cas_login(): @@ -102,6 +98,7 @@ def create_basic_user_profile(sender, instance, created, **kwargs): 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) diff --git a/avisstage/statics.py b/avisstage/statics.py index ce5a9fd..5344e8b 100644 --- a/avisstage/statics.py +++ b/avisstage/statics.py @@ -1,5 +1,3 @@ -# coding: utf-8 - DEPARTEMENTS_DEFAUT = ( ("phy", "Physique"), ("maths", "Maths"), diff --git a/avisstage/tests.py b/avisstage/tests.py index 559b50b..18657f4 100644 --- a/avisstage/tests.py +++ b/avisstage/tests.py @@ -9,7 +9,7 @@ from django.test import TestCase from django.urls import reverse from django.utils import timezone -from .models import AvisLieu, Lieu, Normalien, Stage, StageMatiere, User +from .models import AvisLieu, Lieu, Stage, StageMatiere, User class ExperiENSTestCase(TestCase): diff --git a/avisstage/views.py b/avisstage/views.py index ce26aa2..2a1a806 100644 --- a/avisstage/views.py +++ b/avisstage/views.py @@ -1,5 +1,3 @@ -# coding: utf-8 - import math import random from collections import Counter, defaultdict @@ -12,7 +10,6 @@ from django.conf import settings from django.contrib import messages 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.core.mail import send_mail from django.db.models import Count, Q @@ -24,8 +21,6 @@ from django.views.generic import ( DeleteView, DetailView, FormView, - ListView, - TemplateView, UpdateView, View, ) @@ -42,12 +37,12 @@ from .forms import ( ) from .models import AvisLieu, AvisStage, Lieu, Normalien, Stage from .utils import en_scolarite -from .views_search import * # # LECTURE # + # Page d'accueil def index(request): num_stages = Stage.objects.filter(public=True).count() @@ -516,7 +511,7 @@ class ConfirmeAdresse(LoginRequiredMixin, View): email = EmailAddress.objects.confirm( self.kwargs["key"], self.request.user, True ) - except Exception as e: + except Exception: raise Http404() messages.add_message( self.request, diff --git a/avisstage/views_search.py b/avisstage/views_search.py index 94cbcdf..e3c3004 100644 --- a/avisstage/views_search.py +++ b/avisstage/views_search.py @@ -1,5 +1,3 @@ -# coding: utf-8 - import json import logging from datetime import date @@ -8,10 +6,10 @@ from django import forms from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.cache import cache -from django.core.paginator import Paginator +from django.core.paginator import InvalidPage, Paginator from django.db.models import Case, Q, When from django.http import HttpResponseBadRequest, JsonResponse -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import render USE_ELASTICSEARCH = getattr(settings, "USE_ELASTICSEARCH", True) @@ -24,6 +22,7 @@ from .statics import NIVEAU_SCOL_OPTIONS, TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS logger = logging.getLogger("recherche") + # Recherche class SearchForm(forms.Form): generique = forms.CharField(required=False) diff --git a/experiENS/settings_base.py b/experiENS/settings_base.py index a1b393a..cce20dd 100644 --- a/experiENS/settings_base.py +++ b/experiENS/settings_base.py @@ -12,7 +12,7 @@ https://docs.djangoproject.com/en/1.7/ref/settings/ import os from django.urls import reverse_lazy -from .secrets import SECRET_KEY, GOOGLE_API_KEY, MAPBOX_API_KEY +from .secrets import SECRET_KEY, GOOGLE_API_KEY, MAPBOX_API_KEY # noqa BASE_DIR = os.path.dirname(os.path.dirname(__file__)) diff --git a/experiENS/settings_dev.py b/experiENS/settings_dev.py index acfdc73..f4604f1 100644 --- a/experiENS/settings_dev.py +++ b/experiENS/settings_dev.py @@ -1,4 +1,6 @@ -from .settings_base import * +import os + +from .settings_base import BASE_DIR, INSTALLED_APPS, MIDDLEWARE DEBUG = True diff --git a/experiENS/settings_prod.py b/experiENS/settings_prod.py index 46c5ccf..721d2bf 100644 --- a/experiENS/settings_prod.py +++ b/experiENS/settings_prod.py @@ -1,6 +1,6 @@ -from .settings_base import * +import os -import os, sys +from .settings_base import * # noqa PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(PROJECT_DIR) diff --git a/experiENS/urls.py b/experiENS/urls.py index bf55008..d7ec561 100644 --- a/experiENS/urls.py +++ b/experiENS/urls.py @@ -1,6 +1,6 @@ from django.conf import settings -from django.urls import include, path from django.contrib import admin +from django.urls import include, path urlpatterns = [ path("", include("avisstage.urls")), diff --git a/scripts/initalize_allauth.py b/scripts/initalize_allauth.py index 8b18e6c..1e0f515 100644 --- a/scripts/initalize_allauth.py +++ b/scripts/initalize_allauth.py @@ -1,12 +1,11 @@ import sys +from collections import defaultdict from allauth.account.models import EmailAddress from allauth.socialaccount.models import SocialAccount from avisstage.models import Normalien -from collections import defaultdict - accounts = SocialAccount.objects.all().prefetch_related("user") profils = Normalien.objects.all() addresses = EmailAddress.objects.all() diff --git a/setup.cfg b/setup.cfg index 81f23f8..767121e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [flake8] max-line-length = 99 exclude = .git, *.pyc, __pycache__, migrations -extend-ignore = E231, E203 +extend-ignore = E231, E203, E402 [isort] profile = black From 732a6a08da927d0b42ac34100ff5228ef427cf2d Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 29 Jun 2021 00:11:18 +0200 Subject: [PATCH 16/23] More updates --- avisstage/admin.py | 2 +- avisstage/documents.py | 2 +- avisstage/forms.py | 48 +++++++++++++++++++++++++++++++------- avisstage/views.py | 10 ++++---- avisstage/views_search.py | 2 +- experiENS/settings_base.py | 9 +++---- 6 files changed, 53 insertions(+), 20 deletions(-) diff --git a/avisstage/admin.py b/avisstage/admin.py index 9bf977f..469f4cd 100644 --- a/avisstage/admin.py +++ b/avisstage/admin.py @@ -4,7 +4,7 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User -from avisstage.models import * +from avisstage.models import AvisLieu, AvisStage, Lieu, Normalien, Stage, StageMatiere class NormalienInline(admin.StackedInline): diff --git a/avisstage/documents.py b/avisstage/documents.py index 04a9852..484b36f 100644 --- a/avisstage/documents.py +++ b/avisstage/documents.py @@ -38,7 +38,7 @@ class StageDocument(Document): ) thematiques = fields.TextField() matieres = fields.TextField() - + class Django: model = Stage fields = [ diff --git a/avisstage/forms.py b/avisstage/forms.py index 5356ae6..37aae3b 100644 --- a/avisstage/forms.py +++ b/avisstage/forms.py @@ -102,11 +102,30 @@ class AvisStageForm(HTMLTrimmerForm): "les_moins", ] help_texts = { - "chapo": '"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de ce séjour', - "avis_ambiance": "Avez-vous passé un bon moment à ce travail ? Étiez-vous assez guidé⋅e ? Aviez-vous un bon contact avec vos encadrant⋅e⋅s ? Y avait-il une bonne ambiance dans l'équipe ?", - "avis_sujet": "Quelle était votre mission ? Qu'en avez-vous retiré ? Le travail correspondait-il à vos attentes ? Était-ce à votre niveau, trop dur, trop facile ?", - "avis_admin": "Avez-vous commencé votre travail à la date prévue ? Était-ce compliqué d'obtenir les documents nécessaires (visa, contrats, etc) ? L'administration de l'établissement vous a-t-elle aidé⋅e ? Étiez-vous rémunéré⋅e ?", - "avis_prestage": "Comment avez-vous trouvé où aller pour cette expérience ? À quel moment avez-vous commencé à chercher ? Avez-vous eu des entretiens pour obtenir votre place ? Avez-vous eu d'autres pistes, pourquoi avez-vous choisi cette option ?", + "chapo": ( + '"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de ce séjour' + ), + "avis_ambiance": ( + "Avez-vous passé un bon moment à ce travail ? Étiez-vous assez guidé·e ? " + "Aviez-vous un bon contact avec vos encadrant·e·s ? Y avait-il une bonne " + "ambiance dans l'équipe ?" + ), + "avis_sujet": ( + "Quelle était votre mission ? Qu'en avez-vous retiré ? Le travail " + "correspondait-il à vos attentes ? Était-ce à votre niveau, trop dur, " + "trop facile ?" + ), + "avis_admin": ( + "Avez-vous commencé votre travail à la date prévue ? Était-ce compliqué " + "d'obtenir les documents nécessaires (visa, contrats, etc) ? L'administration " + "de l'établissement vous a-t-elle aidé·e ? Étiez-vous rémunéré·e ?" + ), + "avis_prestage": ( + "Comment avez-vous trouvé où aller pour cette expérience ? À quel moment " + "avez-vous commencé à chercher ? Avez-vous eu des entretiens pour obtenir " + "votre place ? Avez-vous eu d'autres pistes, pourquoi avez-vous choisi " + "cette option ?" + ), "les_plus": "Les principaux points positifs de cette expérience", "les_moins": "Ce qui aurait pu être mieux", } @@ -125,10 +144,21 @@ class AvisLieuForm(HTMLTrimmerForm): "les_moins", ] help_texts = { - "chapo": '"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de cet endroit', - "avis_lieustage": "Qu'avez-vous pensé des lieux où vous travailliez ? Les bâtiments étaient-ils modernes ? Était-il agréable d'y travailler ?", - "avis_pratique": "Avez-vous eu du mal à trouver un logement ? Y-a-t-il des choses que vous avez apprises sur place qu'il vous aurait été utile de savoir avant de partir ?", - "avis_tourisme": "Y-a-t-il des lieux à visiter dans cette zone ? Avez-vous pratiqué des activités sportives ? Est-il facile de faire des rencontres ?", + "chapo": ( + '"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de cet endroit' + ), + "avis_lieustage": ( + "Qu'avez-vous pensé des lieux où vous travailliez ? Les bâtiments " + "étaient-ils modernes ? Était-il agréable d'y travailler ?" + ), + "avis_pratique": ( + "Avez-vous eu du mal à trouver un logement ? Y-a-t-il des choses que vous avez " + "apprises sur place qu'il vous aurait été utile de savoir avant de partir ?" + ), + "avis_tourisme": ( + "Y-a-t-il des lieux à visiter dans cette zone ? Avez-vous pratiqué " + "des activités sportives ? Est-il facile de faire des rencontres ?" + ), "les_plus": "Les meilleures raisons de partir à cet endroit", "les_moins": "Ce qui vous a gêné ou manqué là-bas", } diff --git a/avisstage/views.py b/avisstage/views.py index 2a1a806..d25635a 100644 --- a/avisstage/views.py +++ b/avisstage/views.py @@ -233,7 +233,8 @@ def save_lieu(request): # On regarde si les stages associés à ce lieu "appartiennent" tous à l'utilisateur not_same_user = lieu.stages.exclude(auteur=normalien).count() - # Si d'autres personnes ont un stage à cet endroit, on crée un nouveau lieu, un peu à côté + # Si d'autres personnes ont un stage à cet endroit, + # on crée un nouveau lieu, un peu à côté if not_same_user > 0: lieu = Lieu() # Servira à bouger un peu le lieu @@ -543,9 +544,10 @@ class EnvoieLienMotDePasse(LoginRequiredMixin, View): 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 - ), + ( + "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")) diff --git a/avisstage/views_search.py b/avisstage/views_search.py index e3c3004..2c62d6b 100644 --- a/avisstage/views_search.py +++ b/avisstage/views_search.py @@ -28,7 +28,7 @@ class SearchForm(forms.Form): generique = forms.CharField(required=False) sujet = forms.CharField(label="À propos de", required=False) contexte = forms.CharField( - label="Contexte (lieu, encadrant⋅e⋅s, structure)", required=False + label="Contexte (lieu, encadrant·e·s, structure)", required=False ) apres_annee = forms.IntegerField(label="Après cette année", required=False) diff --git a/experiENS/settings_base.py b/experiENS/settings_base.py index cce20dd..2aa9fee 100644 --- a/experiENS/settings_base.py +++ b/experiENS/settings_base.py @@ -10,9 +10,10 @@ 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.urls import reverse_lazy -from .secrets import SECRET_KEY, GOOGLE_API_KEY, MAPBOX_API_KEY # noqa +from .secrets import GOOGLE_API_KEY, MAPBOX_API_KEY, SECRET_KEY # noqa BASE_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -33,9 +34,9 @@ INSTALLED_APPS = [ "django.contrib.gis", "django.contrib.sites", "django_elasticsearch_dsl", - #'allauth', # Uncomment that part when you - #'allauth.account', # apply migration - #'allauth.socialaccount', # Allauth -> AuthENS + # 'allauth', # Uncomment that part when you + # 'allauth.account', # apply migration + # 'allauth.socialaccount', # Allauth -> AuthENS "simple_email_confirmation", "authens", "tastypie", From d9111cc8cb4892e830a9f419da1961ecf4828cb2 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 11 Jul 2021 21:18:14 +0200 Subject: [PATCH 17/23] Import settings in dev mode --- experiENS/settings_dev.py | 1 + 1 file changed, 1 insertion(+) diff --git a/experiENS/settings_dev.py b/experiENS/settings_dev.py index f4604f1..e1fa2a4 100644 --- a/experiENS/settings_dev.py +++ b/experiENS/settings_dev.py @@ -1,5 +1,6 @@ import os +from .settings_base import * # noqa from .settings_base import BASE_DIR, INSTALLED_APPS, MIDDLEWARE DEBUG = True From 02f7b3c8c3472294d0e079ae786f988c5b5760c6 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sun, 11 Jul 2021 21:20:10 +0200 Subject: [PATCH 18/23] Use search views --- avisstage/urls.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/avisstage/urls.py b/avisstage/urls.py index 97647e7..c89c6e2 100644 --- a/avisstage/urls.py +++ b/avisstage/urls.py @@ -2,7 +2,7 @@ from tastypie.api import Api from django.urls import include, path -from . import api, views +from . import api, views, views_search v1_api = Api(api_name="v1") v1_api.register(api.LieuResource()) @@ -56,9 +56,13 @@ urlpatterns = [ views.DefinirMotDePasse.as_view(), name="mdp_edit", ), - path("recherche/", views.recherche, name="recherche"), - path("recherche/resultats/", views.recherche_resultats, name="recherche_resultats"), - path("recherche/items/", views.stage_items, name="stage_items"), + path("recherche/", views_search.recherche, name="recherche"), + path( + "recherche/resultats/", + views_search.recherche_resultats, + name="recherche_resultats", + ), + path("recherche/items/", views_search.stage_items, name="stage_items"), path("feedback/", views.feedback, name="feedback"), path("moderation/", views.statistiques, name="moderation"), path("api/", include(v1_api.urls)), From 25f965c750449b5c5e6473739df3d71ccca45e5b Mon Sep 17 00:00:00 2001 From: Robin Champenois Date: Sun, 29 Aug 2021 14:00:27 +0200 Subject: [PATCH 19/23] debug search --- avisstage/views_search.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/avisstage/views_search.py b/avisstage/views_search.py index 2c62d6b..b77c0de 100644 --- a/avisstage/views_search.py +++ b/avisstage/views_search.py @@ -147,11 +147,11 @@ def cherche(**kwargs): # Dates if field_relevant("avant_annee", False): - dte = date(kwargs["avant_annee"] + 1, 1, 1) + dte = date(min(2100, kwargs["avant_annee"]) + 1, 1, 1) filtres &= Q(date_fin__lt=dte) if field_relevant("apres_annee", False): - dte = date(kwargs["apres_annee"], 1, 1) + dte = date(max(2000, kwargs["apres_annee"]), 1, 1) filtres &= Q(date_debut__gte=dte) # Type de stage @@ -187,6 +187,8 @@ def cherche(**kwargs): ) else: resultat = resultat.order_by(tri) + else: + resultat = resultat.order_by(tri) return resultat, tri From 1491956e30887a92a1d8d6b455940c4d5e80fc45 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 23 Oct 2024 14:07:00 +0200 Subject: [PATCH 20/23] chore: Rename experiENS -> app --- {experiENS => app}/__init__.py | 0 {experiENS => app}/auth.py | 0 {experiENS => app}/settings_base.py | 0 {experiENS => app}/settings_dev.py | 0 {experiENS => app}/settings_prod.py | 0 {experiENS => app}/urls.py | 0 {experiENS => app}/wsgi.py | 4 ++-- 7 files changed, 2 insertions(+), 2 deletions(-) rename {experiENS => app}/__init__.py (100%) rename {experiENS => app}/auth.py (100%) rename {experiENS => app}/settings_base.py (100%) rename {experiENS => app}/settings_dev.py (100%) rename {experiENS => app}/settings_prod.py (100%) rename {experiENS => app}/urls.py (100%) rename {experiENS => app}/wsgi.py (82%) diff --git a/experiENS/__init__.py b/app/__init__.py similarity index 100% rename from experiENS/__init__.py rename to app/__init__.py diff --git a/experiENS/auth.py b/app/auth.py similarity index 100% rename from experiENS/auth.py rename to app/auth.py diff --git a/experiENS/settings_base.py b/app/settings_base.py similarity index 100% rename from experiENS/settings_base.py rename to app/settings_base.py diff --git a/experiENS/settings_dev.py b/app/settings_dev.py similarity index 100% rename from experiENS/settings_dev.py rename to app/settings_dev.py diff --git a/experiENS/settings_prod.py b/app/settings_prod.py similarity index 100% rename from experiENS/settings_prod.py rename to app/settings_prod.py diff --git a/experiENS/urls.py b/app/urls.py similarity index 100% rename from experiENS/urls.py rename to app/urls.py diff --git a/experiENS/wsgi.py b/app/wsgi.py similarity index 82% rename from experiENS/wsgi.py rename to app/wsgi.py index c9810b7..f5e0fd5 100644 --- a/experiENS/wsgi.py +++ b/app/wsgi.py @@ -9,8 +9,8 @@ https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "experiENS.settings") - from django.core.wsgi import get_wsgi_application +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") + application = get_wsgi_application() From d7b80ea06ab5d1fd2e360af6b6c5f4e27b435f55 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 23 Oct 2024 20:01:03 +0200 Subject: [PATCH 21/23] feat: Add nix tooling --- .credentials/SECRET_KEY | 1 + .envrc | 1 + .gitignore | 4 +- default.nix | 94 +++++++++++++++++++++++++++++++++++++++++ npins/default.nix | 80 +++++++++++++++++++++++++++++++++++ npins/sources.json | 34 +++++++++++++++ shell.nix | 1 + 7 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 .credentials/SECRET_KEY create mode 100644 .envrc create mode 100644 default.nix create mode 100644 npins/default.nix create mode 100644 npins/sources.json create mode 100644 shell.nix diff --git a/.credentials/SECRET_KEY b/.credentials/SECRET_KEY new file mode 100644 index 0000000..545a6ec --- /dev/null +++ b/.credentials/SECRET_KEY @@ -0,0 +1 @@ +insecure-secret-key diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.gitignore b/.gitignore index e09edb1..f22e893 100644 --- a/.gitignore +++ b/.gitignore @@ -108,6 +108,6 @@ test.py .#* *.sqlite3 .sass-cache -/static/ -settings.py secrets.py +.direnv +.pre-commit-config.yaml diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..27c362f --- /dev/null +++ b/default.nix @@ -0,0 +1,94 @@ +{ + sources ? import ./npins, + pkgs ? import sources.nixpkgs { }, +}: + +let + nix-pkgs = import sources.nix-pkgs { inherit pkgs; }; + + check = (import sources.git-hooks).run { + src = ./.; + + hooks = { + # Python hooks + black = { + enable = true; + stages = [ "pre-push" ]; + }; + + isort = { + enable = true; + stages = [ "pre-push" ]; + }; + + ruff = { + enable = true; + stages = [ "pre-push" ]; + }; + + # Misc Hooks + commitizen.enable = true; + }; + }; + + python3 = pkgs.python3.override { + packageOverrides = _: _: { + inherit (nix-pkgs) + authens + django-braces + django-elasticsearch-dsl + django-simple-email-confirmation + django-taggit-autosuggest + django-tinymce + loadcredential + spatialite + ; + }; + }; +in + +{ + devShell = pkgs.mkShell { + name = "annuaire.dev"; + + packages = [ + (python3.withPackages (ps: [ + ps.authens + ps.django + ps.django-braces + ps.django-elasticsearch-dsl + ps.django-simple-email-confirmation + ps.django-taggit + ps.django-taggit-autosuggest + ps.django-tastypie + ps.django-tinymce + ps.loadcredential + + # Dev packages + ps.django-debug-toolbar + ps.django-stubs + ps.spatialite + ])) + ]; + + env = { + DJANGO_SETTINGS_MODULE = "app.settings"; + + CREDENTIALS_DIRECTORY = builtins.toString ./.credentials; + + EXPERIENS_DEBUG = builtins.toJSON true; + EXPERIENS_STATIC_ROOT = builtins.toString ./.static; + + EXPERIENS_GDAL_LIBRARY_PATH = "${pkgs.gdal}/lib/libgdal.so"; + EXPERIENS_GEOS_LIBRARY_PATH = "${pkgs.geos}/lib/libgeos_c.so"; + }; + + shellHook = '' + ${check.shellHook} + + if [ ! -d .static ]; then + mkdir .static + fi + ''; + }; +} diff --git a/npins/default.nix b/npins/default.nix new file mode 100644 index 0000000..5e7d086 --- /dev/null +++ b/npins/default.nix @@ -0,0 +1,80 @@ +# Generated by npins. Do not modify; will be overwritten regularly +let + data = builtins.fromJSON (builtins.readFile ./sources.json); + version = data.version; + + mkSource = + spec: + assert spec ? type; + let + path = + if spec.type == "Git" then + mkGitSource spec + else if spec.type == "GitRelease" then + mkGitSource spec + else if spec.type == "PyPi" then + mkPyPiSource spec + else if spec.type == "Channel" then + mkChannelSource spec + else + builtins.throw "Unknown source type ${spec.type}"; + in + spec // { outPath = path; }; + + mkGitSource = + { + repository, + revision, + url ? null, + hash, + branch ? null, + ... + }: + assert repository ? type; + # At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository + # In the latter case, there we will always be an url to the tarball + if url != null then + (builtins.fetchTarball { + inherit url; + sha256 = hash; # FIXME: check nix version & use SRI hashes + }) + else + assert repository.type == "Git"; + let + urlToName = + url: rev: + let + matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url; + + short = builtins.substring 0 7 rev; + + appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else ""; + in + "${if matched == null then "source" else builtins.head matched}${appendShort}"; + name = urlToName repository.url revision; + in + builtins.fetchGit { + url = repository.url; + rev = revision; + inherit name; + # hash = hash; + }; + + mkPyPiSource = + { url, hash, ... }: + builtins.fetchurl { + inherit url; + sha256 = hash; + }; + + mkChannelSource = + { url, hash, ... }: + builtins.fetchTarball { + inherit url; + sha256 = hash; + }; +in +if version == 3 then + builtins.mapAttrs (_: mkSource) data.pins +else + throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`" diff --git a/npins/sources.json b/npins/sources.json new file mode 100644 index 0000000..ff2347f --- /dev/null +++ b/npins/sources.json @@ -0,0 +1,34 @@ +{ + "pins": { + "git-hooks": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "cachix", + "repo": "git-hooks.nix" + }, + "branch": "master", + "revision": "3c3e88f0f544d6bb54329832616af7eb971b6be6", + "url": "https://github.com/cachix/git-hooks.nix/archive/3c3e88f0f544d6bb54329832616af7eb971b6be6.tar.gz", + "hash": "04pwjz423iq2nkazkys905gvsm5j39722ngavrnx42b8msr5k555" + }, + "nix-pkgs": { + "type": "Git", + "repository": { + "type": "Git", + "url": "https://git.hubrecht.ovh/hubrecht/nix-pkgs" + }, + "branch": "main", + "revision": "024f0d09d4ff1a62e11f5fdd74f2d00d0a77da5c", + "url": null, + "hash": "0abpyf4pclslg24wmwl3q6y8x5fmhq9winpgkpbb99yw2815j2iz" + }, + "nixpkgs": { + "type": "Channel", + "name": "nixpkgs-unstable", + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre694416.ccc0c2126893/nixexprs.tar.xz", + "hash": "0cn1z4wzps8nfqxzr6l5mbn81adcqy2cy2ic70z13fhzicmxfsbx" + } + }, + "version": 3 +} \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..d6d21cf --- /dev/null +++ b/shell.nix @@ -0,0 +1 @@ +(import ./. { }).devShell From 4d75efe7c5ade80be8104f431ce5bf94969c31cb Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 23 Oct 2024 20:04:20 +0200 Subject: [PATCH 22/23] fix(manage.py): Make executable --- manage.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 manage.py diff --git a/manage.py b/manage.py old mode 100644 new mode 100755 From 0209ad53ca62f47408e0721eab22485cf8816068 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 23 Oct 2024 20:04:53 +0200 Subject: [PATCH 23/23] feat: Merge settings --- app/settings.py | 242 +++++++++++++++++++++++++++++++++++++++++++ app/settings_base.py | 143 ------------------------- app/settings_dev.py | 45 -------- app/settings_prod.py | 49 --------- 4 files changed, 242 insertions(+), 237 deletions(-) create mode 100644 app/settings.py delete mode 100644 app/settings_base.py delete mode 100644 app/settings_dev.py delete mode 100644 app/settings_prod.py diff --git a/app/settings.py b/app/settings.py new file mode 100644 index 0000000..10cc1b9 --- /dev/null +++ b/app/settings.py @@ -0,0 +1,242 @@ +""" +Django settings for the experiENS project +""" + +from pathlib import Path + +from loadcredential import Credentials + +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ + +credentials = Credentials(env_prefix="EXPERIENS_") + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# WARNING: keep the secret key used in production secret! +SECRET_KEY = credentials["SECRET_KEY"] + +# WARNING: don't run with debug turned on in production! +DEBUG = credentials.get_json("DEBUG", False) + +ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", []) + +ADMINS = credentials.get_json("ADMINS", []) + +SITE_ID = 1 + + +### +# ElasticSearch configuration + +USE_ELASTICSEARCH = credentials.get_json("USE_ELASTICSEARCH", False) +ELASTICSEARCH_DSL = credentials.get_json( + "ELASTICSEARCH_DSL", {"default": {"hosts": "127.0.0.1:9200"}} +) + + +### +# Libraries configuration + +GDAL_LIBRARY_PATH = credentials.get("GDAL_LIBRARY_PATH") +GEOS_LIBRARY_PATH = credentials.get("GEOS_LIBRARY_PATH") + + +### +# List the installed applications + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.gis", + "django.contrib.sites", + *(["django_elasticsearch_dsl"] if USE_ELASTICSEARCH else []), + "simple_email_confirmation", + "authens", + "tastypie", + "braces", + "tinymce", + "taggit", + "taggit_autosuggest", + "avisstage", +] + + +### +# List the installed middlewares + +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.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + + +### +# The main url configuration + +ROOT_URLCONF = "app.urls" + + +### +# Template configuration: +# - Django Templating Language is used +# - Application directories can be used + + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + + +### +# Database configuration +# -> https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + +DATABASES = credentials.get_json( + "DATABASES", + { + "default": { + "ENGINE": "django.contrib.gis.db.backends.spatialite", + "NAME": BASE_DIR / "db.sqlite3", + } + }, +) + +CACHES = credentials.get_json( + "CACHES", + default={ + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + }, + }, +) + + +### +# WSGI application configuration + +WSGI_APPLICATION = "app.wsgi.application" + + +### +# Staticfiles configuration + +STATIC_ROOT = credentials["STATIC_ROOT"] +STATIC_URL = "/static/" + +MEDIA_ROOT = credentials.get("MEDIA_ROOT", BASE_DIR / "media") +MEDIA_URL = "/media/" + + +### +# Internationalization configuration +# -> https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "fr-fr" +TIME_ZONE = "Europe/Paris" + +USE_I18N = True +USE_L10N = True +USE_TZ = True + +LANGUAGES = [ + ("fr", _("Français")), +] + + +### +# Authentication configuration + +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "experiENS.auth.ENSCASBackend", +] + +CAS_SERVER_URL = "https://cas.eleves.ens.fr/" # SPI CAS + +AUTHENS_USE_OLDCAS = False + +LOGIN_URL = reverse_lazy("authens:login") +LOGOUT_URL = reverse_lazy("authens:logout") +LOGIN_REDIRECT_URL = reverse_lazy("avisstage:perso") +LOGOUT_REDIRECT_URL = reverse_lazy("avisstage:index") + + +AUTH_PASSWORD_VALIDATORS = [ + {"NAME": f"django.contrib.auth.password_validation.{v}"} + for v in [ + "UserAttributeSimilarityValidator", + "MinimumLengthValidator", + "CommonPasswordValidator", + "NumericPasswordValidator", + ] +] + + +### +# Logging configuration + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "file": { + "level": "INFO", + "class": "logging.FileHandler", + "filename": credentials.get( + "RECHERCHE_LOG_FILE", BASE_DIR / "recherche.log" + ), + }, + }, + "loggers": { + "recherche": { + "handlers": ["file"], + "level": "INFO", + "propagate": True, + }, + }, +} + + +### +# LDAP configuration + +CLIPPER_LDAP_SERVER = credentials.get("CLIPPER_LDAP_SERVER", "ldaps://localhost:636") + + +# Development settings +if DEBUG: + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + INSTALLED_APPS += [ + "debug_toolbar", + ] + + MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware", *MIDDLEWARE] diff --git a/app/settings_base.py b/app/settings_base.py deleted file mode 100644 index 2aa9fee..0000000 --- a/app/settings_base.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -Django settings for experiENS project. - -For more information on this file, see -https://docs.djangoproject.com/en/1.7/topics/settings/ - -For the full list of settings and their values, see -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.urls import reverse_lazy - -from .secrets import GOOGLE_API_KEY, MAPBOX_API_KEY, SECRET_KEY # noqa - -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ - -ALLOWED_HOSTS = [] - -# Application definition - -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "django.contrib.gis", - "django.contrib.sites", - "django_elasticsearch_dsl", - # 'allauth', # Uncomment that part when you - # 'allauth.account', # apply migration - # 'allauth.socialaccount', # Allauth -> AuthENS - "simple_email_confirmation", - "authens", - "tastypie", - "braces", - "tinymce", - "taggit", - "taggit_autosuggest", - "avisstage", -] - -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.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -) - - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [ - # insert your TEMPLATE_DIRS here - ], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.contrib.auth.context_processors.auth", - "django.template.context_processors.debug", - "django.template.context_processors.i18n", - "django.template.context_processors.media", - "django.template.context_processors.static", - "django.template.context_processors.tz", - "django.template.context_processors.request", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - - -ROOT_URLCONF = "experiENS.urls" - -WSGI_APPLICATION = "experiENS.wsgi.application" - - -# Database -# https://docs.djangoproject.com/en/1.7/ref/settings/#databases - -# Internationalization -# https://docs.djangoproject.com/en/1.7/topics/i18n/ - -LANGUAGE_CODE = "fr" - -TIME_ZONE = "Europe/Paris" - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -SITE_ID = 1 - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.7/howto/static-files/ - -STATIC_URL = "/static/" - -AUTHENTICATION_BACKENDS = ( - "django.contrib.auth.backends.ModelBackend", - "experiENS.auth.ENSCASBackend", -) - -CAS_SERVER_URL = "https://cas.eleves.ens.fr/" # SPI CAS - -AUTHENS_USE_OLDCAS = False - -LOGIN_URL = reverse_lazy("authens:login") -LOGOUT_URL = reverse_lazy("authens:logout") -LOGIN_REDIRECT_URL = reverse_lazy("avisstage:perso") -LOGOUT_REDIRECT_URL = reverse_lazy("avisstage:index") - -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "handlers": { - "file": { - "level": "INFO", - "class": "logging.FileHandler", - "filename": os.path.join(BASE_DIR, "recherche.log"), - }, - }, - "loggers": { - "recherche": { - "handlers": ["file"], - "level": "INFO", - "propagate": True, - }, - }, -} diff --git a/app/settings_dev.py b/app/settings_dev.py deleted file mode 100644 index e1fa2a4..0000000 --- a/app/settings_dev.py +++ /dev/null @@ -1,45 +0,0 @@ -import os - -from .settings_base import * # noqa -from .settings_base import BASE_DIR, INSTALLED_APPS, MIDDLEWARE - -DEBUG = True - -DATABASES = { - "default": { - "ENGINE": "django.contrib.gis.db.backends.spatialite", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), - } -} - -USE_DEBUG_TOOLBAR = False - -if USE_DEBUG_TOOLBAR: - INSTALLED_APPS += [ - "debug_toolbar", - ] - - MIDDLEWARE = ("debug_toolbar.middleware.DebugToolbarMiddleware",) + MIDDLEWARE - -INTERNAL_IPS = ["127.0.0.1"] - -SPATIALITE_LIBRARY_PATH = "mod_spatialite" - -STATIC_ROOT = "/home/evarin/Bureau/experiENS/static/" - -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - -STATIC_URL = "/experiens/static/" - -ELASTICSEARCH_DSL = { - "default": {"hosts": "localhost:9200"}, -} - - -CLIPPER_LDAP_SERVER = "ldaps://localhost:636" - -# Changer à True pour développer avec ES -USE_ELASTICSEARCH = False - -if not USE_ELASTICSEARCH: - INSTALLED_APPS.remove("django_elasticsearch_dsl") diff --git a/app/settings_prod.py b/app/settings_prod.py deleted file mode 100644 index 721d2bf..0000000 --- a/app/settings_prod.py +++ /dev/null @@ -1,49 +0,0 @@ -import os - -from .settings_base import * # noqa - -PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) -BASE_DIR = os.path.dirname(PROJECT_DIR) - -DEBUG = False - -ALLOWED_HOSTS = ["www.eleves.ens.fr"] - -ADMINS = (("Robin Champenois", "champeno@clipper.ens.fr"),) - -ADMIN_LOGINS = [ - "champeno", -] - -SERVER_EMAIL = "experiens@www.eleves.ens.fr" - -ROOT_URL = "/experiens/" - -WSGI_APPLICATION = "experiENS.wsgi.application" - -DATABASES = { - "default": { - "ENGINE": "django.contrib.gis.db.backends.postgis", - "NAME": "experiens", - "USER": "experiens", - "PASSWORD": "", - "HOST": "", - "PORT": "5432", - } -} - -STATIC_URL = ROOT_URL + "static/" -MEDIA_URL = ROOT_URL + "media/" - -STATIC_ROOT = os.path.join(BASE_DIR, "static/") - -EMAIL_HOST = "nef.ens.fr" - - -ELASTICSEARCH_DSL = { - "default": {"hosts": "127.0.0.1:9200"}, -} - - -CLIPPER_LDAP_SERVER = "ldaps://ldap.spi.ens.fr:636" -DEFAULT_FROM_EMAIL = "experiens-no-reply@www.eleves.ens.fr"