Merge branch 'maj2021' into 'master'

Mise à jour 2021

Un certain nombre de changements qu'il était temps d'apporter :
- passage à Django 2.2
- basculement de Allauth à AuthENS (=> gestion des adresses mail avec django-simple-email-confirmation)
- statut "en scolarité/archicube" automatique
- débugs divers (carte des lieux, ...)
- mise à jour des dépendances

See merge request klub-dev-ens/experiENS!13
This commit is contained in:
Robin Champenois 2021-02-07 18:23:24 +01:00
commit 9c1092cf8f
36 changed files with 1082 additions and 420 deletions

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -1,7 +1,4 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class AvisstageConfig(AppConfig):
name = 'avisstage'

View file

@ -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))
)

View file

@ -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é'))

File diff suppressed because one or more lines are too long

View file

@ -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 <clipper>@<promo>
# 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'))

View file

@ -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),
),
]

View file

@ -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'),
),
]

View file

@ -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()
if not created and profil.promotion != "":
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", "")
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
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
post_save.connect(create_user_profile, sender=User)
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)

View file

@ -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 {

View file

@ -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;

File diff suppressed because it is too large Load diff

View file

@ -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 &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: 'mapbox.streets',
id: 'mapbox/streets-v11',
accessToken: MAPBOX_API_KEY
});
map.addLayer(layer);

View file

@ -37,9 +37,9 @@
<li><a href="{% url 'avisstage:moderation' %}">Modo</a></li>
{% endif %}
{% if user.is_authenticated %}
<li><a href="{% url "account_logout" %}"><span class="username">{{ user.username }}</span><br/> Déconnexion</a></li>
<li><a href="{% url "authens:logout" %}"><span class="username">{{ user.username }}</span><br/> Déconnexion</a></li>
{% else %}
<li><a href="{% url "account_login" %}">Connexion</a></li>
<li><a href="{% url "authens:login" %}">Connexion</a></li>
{% endif %}
</ul>
</nav>

View file

@ -0,0 +1,19 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Confirmation requise - ExperiENS{% endblock %}
{% block content %}
<h1>Confirmation requise</h1>
<article>
{% if object.confirmed_at %}
<p>L'adresse {{ object.email }} a déjà été confirmée.</p>
{% else %}
<p>Un mail de confirmation vous a été envoyé à l'adresse {{ object.email }} pour la vérifier.</p>
<p>Merci de cliquer sur le lien inclus pour confirmer qu'elle est correcte.</p>
<p>Si vous ne recevez rien, vérifier dans vos indésirables.</p>
{% endif %}
<p><a href="{% url "avisstage:parametres" %}">Retour</a></p>
</article>
{% endblock %}

View file

@ -0,0 +1,29 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block content %}
<h1>Définir un mot de passe</h1>
<form action="" method="post">
{% csrf_token %}
{{ form.non_field_errors }}
<div class="field">
<label>Nom d'utilisateur</label>
<div class="input">
{{ user.username }}
</div>
</div>
{% for field in form %}
{{ field.errors }}
<div class="field">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
<div class="input">
{{ field }}
{% if field.help_text %}
<p class="help_text">{{ field.help_text }}</p>
{% endif %}
</div>
</div>
{% endfor %}
<input type="submit" value="Enregistrer" />
</form>
{% endblock %}

View file

@ -0,0 +1,18 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Supprimer une adresse mail - ExperiENS{% endblock %}
{% block content %}
<h1>Supprimer une adresse mail</h1>
<article>
<section class="profil">
<form action="" method="POST">
{% csrf_token %}
<p>Êtes-vous sûr⋅e de vouloir supprimer l'adresse mail {{ object.email }} ?</p>
<p><a href="{% url "avisstage:parametres" %}">Retour</a> &nbsp; <input type="submit" value="Confirmer la suppression"></p>
</form>
</section>
</article>
{% endblock %}

View file

@ -0,0 +1,76 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Mes paramètres - ExperiENS{% endblock %}
{% block content %}
<h1>Mes paramètres</h1>
<article>
<h2>Adresses e-mail</h2>
<ul class="mes-emails">
{% for email in request.user.email_address_set.all %}
<li>
<span class="adresse">{{ email.email }}
<span class="confirmee">{{ email.confirmed_at|yesno:"&#10003;,&#10007;"|safe }}</span></span>
{% if email.confirmed_at %}
<span class="principale">
{% if email.email == user.email %}
Principale
{% else %}
<form action="{% url "avisstage:emails_principal" email.email %}" method="POST">
{% csrf_token %}
<input type="submit" value="Rendre principale">
</form>
{% endif %}
</span>
{% else %}
<span class="confirmer">
<form action="{% url "avisstage:emails_reconfirme" email.email %}" method="POST">
{% csrf_token %}
<input type="submit" value="Renvoyer le lien de confirmation">
</form>
</span>
{% endif %}
<span class="supprimer">
{% if not email.email == user.email %}
<a href="{% url "avisstage:emails_supprime" email.email %}">Supprimer</a>
{% endif %}
</span>
</li>
{% endfor %}
<li>
<form action="" method="POST">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
{{ field.errors }}
<div class="field">
<div class="input">
{{ field }}
</div>
</div>
{% endfor %}
<input type="submit" value="Ajouter l'adresse">
</form>
</li>
</ul>
</article>
<article>
<h2>Mot de passe</h2>
<section class="profil">
{% if request.user.password and request.user.has_usable_password %}
<p>Un mot de passe interne est déjà défini pour ce compte.</p>
{% else %}
<p>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.</p>
{% endif %}
<form action="{% url "avisstage:mdp_demande" %}" method="POST">
{% csrf_token %}
<input type="submit" value="Définir un nouveau mot de passe" />
</form>
<p>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.</p>
</section>
</article>
{% endblock %}

View file

@ -5,10 +5,10 @@
<script type="text/javascript" src="{% static "js/render.js" %}"></script>
{% endblock %}
{% block title %}Profil de {{ object.nom }} - ExperiENS{% endblock %}
{% block title %}Profil de {{ object.nom_complet }} - ExperiENS{% endblock %}
{% block content %}
<h1>Profil de {{ object.nom }}
<h1>Profil de {{ object.nom_complet }}
{% if object.user == user %}
<a href="{% url "avisstage:profil_edit" %}" class="btn edit-btn">Modifier mes infos</a>
{% endif %}

View file

@ -72,7 +72,7 @@
<h1>{{ object.sujet }}</h1>
<p class="dates"><span class="year">{{ object.date_debut|date:"Y" }}</span><span class="debut">{{ object.date_debut|date:"d/m" }}</span><span class="fin">{{ object.date_fin|date:"d/m" }}</span></p>
</div>
<p><a href="{% url "avisstage:profil" object.auteur.user.username %}">{{ object.auteur.nom }}</a>
<p><a href="{% url "avisstage:profil" object.auteur.user.username %}">{{ object.auteur.nom_complet }}</a>
a fait {{ object.type_stage_fem|yesno:"cette,ce" }} <b>{{ object.type_stage_fancy }}</b>
{% 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 %}.</p>

View file

@ -18,6 +18,13 @@
</div>
</div>
{% endfor %}
<input type="submit" />
<div class="field">
<label>Adresse e-mail</label>
<div class="input">
{{ request.user.email }}
<p class="help_text">Allez dans <a href="{% url "avisstage:parametres" %}">les paramètres de connexion</a> pour modifier votre adresse principale</p>
</div>
</div>
<input type="submit" value="Enregistrer" />
</form>
{% endblock %}

View file

@ -11,9 +11,9 @@
{% if not user.is_authenticated %}
<div class="entrer">
<p><a href="{% url "clipper_login" %}" class="btn">Connexion</a></p>
<p><a href="{% url "authens:login.cas" %}" class="btn">Connexion</a></p>
<p class="helptext">Connexion via le serveur central d'authentification ENS <br />(identifiants clipper)</p>
<p class="archicubes"><a href="{% url "account_login" %}">Accès archicubes</a> <br /><i>Pour continuer à tenir à jour ses fiches, sans voir celles des autres</i></p>
<p class="archicubes"><a href="{% url "authens:login.pwd" %}">Accès archicubes</a> <br /><i>Pour continuer à tenir à jour ses fiches, sans voir celles des autres</i></p>
</div>
{% endif %}

View file

@ -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

View file

@ -0,0 +1 @@
[ExperiENS] Définition du mot de passe

View file

@ -4,38 +4,42 @@
{% block title %}Espace personnel - ExperiENS{% endblock %}
{% block content %}
<h1>Bonjour {{ user.profil.nom }} !</h1>
<h1>Bonjour {{ user.profil.nom_complet }} !</h1>
<article>
<h2>Mon compte</h2>
<section class="profil">
{% if user.profil.en_scolarite %}
<h3 class="scolarite">Statut : En scolarité</h3>
<p>Vous pouvez accéder à l'ensemble du site, et aux fiches de stages.</p>
<p>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.</p>
<p>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 !</p>
{% else %}
<h3 class="scolarite">Statut : Archicube</h3>
<p>Vous ne pouvez plus accéder qu'à vos propres expériences pour les modifier, et tenir à jour votre profil.</p>
<p>Si vous êtes encore en scolarité, merci de vous <a href="{% url "clipper_login" %}?process=connect">reconnecter en passant par le serveur d'authentification de l'ENS</a> pour mettre à jour votre statut.</p>
{% endif %}
<p><i>Le statut est mis à jour automatiquement chaque année selon le mode de connexion que vous utilisez.</i></p>
</section>
<section class="profil">
<h3>Adresses e-mail</h3>
{% if not user.profil.has_nonENS_email %}<p align="center" class="warning">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 !</p>{% endif %}
<p><a href="{% url "account_email" %}">Gérer les adresses e-mail liées à mon compte</a></p>
</section>
<section class="profil">
<h3>Mode de connexion</h3>
{% if user.profil.en_scolarite %}<p>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 }}</p>{% endif %}
{% if not user.password %}<p class="warning" align="center">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 !</p>{% endif %}
<p><a href="{% url "account_change_password" %}">Créer / changer mon mot de passe ExperiENS</a></p>
<section class="two-cols">
<section class="profil">
{% if user.profil.en_scolarite %}
<h3 class="scolarite">Statut : En scolarité</h3>
<p>Vous pouvez accéder à l'ensemble du site, et aux fiches de stages.</p>
<p>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.</p>
<p>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 !</p>
{% else %}
<h3 class="scolarite">Statut : Archicube</h3>
<p>Vous ne pouvez plus accéder qu'à vos propres expériences pour les modifier, et tenir à jour votre profil.</p>
<p>Si vous êtes encore en scolarité, merci de vous <a href="{% url "authens:login.cas" %}">reconnecter en passant par le serveur d'authentification de l'ENS</a> pour mettre à jour votre statut.</p>
{% endif %}
<hr />
<p><i>Le statut est mis à jour automatiquement tous les deux mois selon le mode de connexion que vous utilisez.</i></p>
</section>
<section class="profil">
<h3>Connexion</h3>
<p><b>Adresse e-mail principale :</b><br/> {{ user.email }}</p>
{% if not user.profil.has_nonENS_email %}<p align="center" class="warning">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 !</p>{% endif %}
<hr/>
<p><b>Mot de passe interne :</b> {% if user.password and user.has_usable_password %}Défini{% else %}Non défini{% endif %}</p>
{% if not user.password or not user.has_usable_password %}<p class="warning" align="center">Pensez à définir un mot de passe propre à ExperiENS pour garder l'accès au site quand vous n'aurez plus de compte clipper !</p>{% endif %}
{% if user.profil.en_scolarite %}<p>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 <b>{{ user.username }}</b> et le mot de passe spécifique à ExperiENS que vous aurez défini.</p>{% endif %}
<hr/>
<p><a href="{% url "avisstage:parametres" %}">Gérer mes paramètres de connexion</a></p>
</section>
</section>
</article>
<article>
<h2><a href="{% url "avisstage:profil" user.username %}">Mon profil public</a> <a href="{% url "avisstage:profil_edit" %}" class="edit-btn btn">Modifier mes infos</a></h2>
<h2>Mon profil public <a href="{% url "avisstage:profil" user.username %}" class="btn">Voir</a> <a href="{% url "avisstage:profil_edit" %}" class="edit-btn btn">Modifier mes infos</a></h2>
{% with object=user.profil %}
<section class="profil">
<div class="infos">

View file

@ -1,16 +1,18 @@
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
@ -19,14 +21,16 @@ class ExperiENSTestCase(CASTestCase):
'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()
self.u_vieuxcon = User.objects.create_user('vieuxcon',
'vieuxcon@ens.fr',
'vieuxcon')
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.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()
@ -356,7 +382,8 @@ class ScolariteViewsTest(ExperiENSTestCase):
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')

View file

@ -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<pk>\w+)/$', views.StageView.as_view(), name='stage'),
url(r'^stage/(?P<pk>\w+)/edit/$', views.manage_stage, name='stage_edit'),
url(r'^stage/(?P<pk>\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/<int:pk>/', views.StageView.as_view(), name='stage'),
path('stage/<int:pk>/edit/', views.manage_stage, name='stage_edit'),
path('stage/<int:pk>/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<username>[\w@]+)/$', views.ProfilView.as_view(),
path('lieu/save/', views.save_lieu, name='lieu_ajout'),
path('profil/show/<str:username>/', 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/<str:email>/aconfirmer/',
views.AdresseAConfirmer.as_view(), name="emails_aconfirmer"),
path('profil/emails/<str:email>/supprime/', views.SupprimeAdresse.as_view(),
name="emails_supprime"),
path('profil/emails/<str:email>/reconfirme/',
views.ReConfirmeAdresse.as_view(),
name="emails_reconfirme"),
path('profil/emails/<str:email>/principal/',
views.RendAdressePrincipale.as_view(), name="emails_principal"),
path('profil/emails/confirme/<str:key>/', views.ConfirmeAdresse.as_view(),
name="emails_confirme"),
path('profil/mdp/demande/',
views.EnvoieLienMotDePasse.as_view(), name="mdp_demande"),
path('profil/mdp/<str:uidb64>/<str:token>/',
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)),
]

View file

@ -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")

View file

@ -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

View file

@ -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 @<promo> 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)

View file

@ -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,

View file

@ -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']

View file

@ -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')),
url(r'^tinymce/', include('tinymce.urls')),
url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')),
url(r'^admin/', include(admin.site.urls)),
path("authens/", include("authens.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

View file

@ -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.*