Merge branch 'thubrecht/format' into 'master'

Thubrecht/format

See merge request klub-dev-ens/experiENS!15
This commit is contained in:
Robin Champenois 2021-07-11 20:52:47 +00:00
commit db1d35fbb6
33 changed files with 2675 additions and 1446 deletions

View file

@ -1,33 +1,41 @@
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 AvisLieu, AvisStage, Lieu, Normalien, Stage, StageMatiere
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)

View file

@ -1,24 +1,24 @@
# coding: utf-8
from tastypie.resources import ModelResource
from tastypie import fields
from tastypie.authentication import SessionAuthentication
from tastypie import fields, utils
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
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:
@ -26,7 +26,7 @@ class LieuResource(ModelResource):
resource_name = "lieu"
fields = ["nom", "ville", "pays", "coord", "type_lieu", "id"]
#login_required
# login_required
authentication = SessionAuthentication()
# Filtres personnalisés
@ -37,15 +37,15 @@ 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
@ -58,13 +58,13 @@ class LieuResource(ModelResource):
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()
@ -74,6 +74,7 @@ class LieuResource(ModelResource):
return bundle
# API sur un stage
class StageResource(ModelResource):
class Meta:
@ -81,7 +82,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,8 +93,8 @@ 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
@ -103,23 +104,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()

View file

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

View file

@ -1,7 +1,8 @@
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):
@wraps(view_func)
@ -9,4 +10,5 @@ def en_scolarite_required(view_func):
if request.user.profil.en_scolarite:
return view_func(request, *args, **kwargs)
return redirect(reverse("avisstage:403-archicubes"))
return _wrapped_view

View file

@ -1,52 +1,60 @@
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 Stage, AvisStage, AvisLieu
from .models import Stage
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()
@ -70,6 +78,6 @@ class StageDocument(Document):
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

View file

@ -1,49 +1,71 @@
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, Stage, User
from .widgets import LatLonField
# Sur-classe utile
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)*(<p>(&nbsp;|[ \n\t]|<br[ /]*>)*</p>( \t\n)*)+?", re.IGNORECASE)
trailing_white = re.compile(r"(( \t\n)*<p>(&nbsp;|[ \n\t]|<br[ /]*>)*</p>)+?( \t\n)*$", re.IGNORECASE)
leading_white = re.compile(
r"^( \t\n)*(<p>(&nbsp;|[ \n\t]|<br[ /]*>)*</p>( \t\n)*)+?", re.IGNORECASE
)
trailing_white = re.compile(
r"(( \t\n)*<p>(&nbsp;|[ \n\t]|<br[ /]*>)*</p>)+?( \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="Date de début", input_formats=["%d/%m/%Y"], widget=date_widget
)
date_fin = forms.DateField(
label="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)"
"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):
@ -54,7 +76,7 @@ class StageForm(forms.ModelForm):
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
@ -65,36 +87,83 @@ class StageForm(forms.ModelForm):
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",
"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",
"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",
}
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",
"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"})
"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",
}
widgets = {"lieu": forms.HiddenInput(attrs={"class": "lieu-hidden"})}
# Création d'un nouveau lieu
class LieuForm(forms.ModelForm):
@ -103,25 +172,31 @@ class LieuForm(forms.ModelForm):
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,7 +206,10 @@ 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
@ -139,11 +217,14 @@ 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))
)

View file

@ -1,43 +1,59 @@
#coding: utf-8
from django.core.management.base import BaseCommand, CommandError
from django.db.models import Count
from avisstage.models import Stage, Lieu
from django.core.management.base import BaseCommand
from avisstage.models import 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")
print("Les modifications ne seront pas appliquées")
min_lieu = options.get('min_lieu', 0)
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))
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(
"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 = " > %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("%s %s" % (pprint, self.style.SUCCESS("-> 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(
"%s %s"
% (pprint, self.style.WARNING("-> À supprimer manuellement"))
)
self.stdout.write(self.style.SUCCESS("Nettoyage des lieux effectué"))

View file

@ -1,18 +1,19 @@
#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 Stage, Lieu
from avisstage.models import Stage
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")
print("Les modifications ne seront pas appliquées")
min_stage = options.get('min_stage', 0)
min_stage = options.get("min_stage", 0)
for stage in Stage.objects.annotate(c=Count("lieux"))\
.filter(c__gte=2, id__gte=min_stage):
for stage in Stage.objects.annotate(c=Count("lieux")).filter(
c__gte=2, id__gte=min_stage
):
lieuset = {}
todel = []
problems = []
@ -52,15 +54,19 @@ 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" % \
(avis.lieu, alen))
print(
" > 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(
"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é"))

View file

@ -1,35 +1,41 @@
#coding: utf-8
from django.core.management.base import BaseCommand, CommandError
from django.db.models import Count
from avisstage.models import Stage, Lieu
from django.core.management.base import BaseCommand
from avisstage.models import 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")
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)" % (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(
"Suppression de %s (id=%d, %d avis)"
% (plieu, plieu.id, plieu.avislieu_set.count())
)
print(
"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("Terminé"))

View file

@ -1,8 +1,10 @@
from django.core.management.base import BaseCommand, CommandError
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils import timezone
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'
@ -11,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(u'Terminé'))
self.stdout.write(self.style.SUCCESS("Terminé"))

File diff suppressed because one or more lines are too long

View file

@ -2,30 +2,37 @@
# 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):
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"
),
),
]

File diff suppressed because one or more lines are too long

View file

@ -1,19 +1,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 +25,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 <clipper>@<promo>
# 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 +53,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 +75,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"))

View file

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

View file

@ -1,32 +1,37 @@
# 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):
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",
),
),
]

View file

@ -1,64 +1,69 @@
from django.db import models
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.contrib.gis.db import models as geomodels
from django.template.defaultfilters import slugify
from django.forms.widgets import DateInput
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 datetime import timedelta
from authens.signals import post_cas_connect
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 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.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
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,
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
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)
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",
default=True, help_text="Affiche votre adresse e-mail principale sur votre profil public")
bio = models.TextField(u"À propos de moi", blank=True, default="")
"Inviter les visiteurs à me contacter",
default=True,
help_text="Affiche votre adresse e-mail principale sur votre profil public",
)
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):
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,6 +82,7 @@ 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:
@ -89,8 +95,10 @@ def create_basic_user_profile(sender, instance, created, **kwargs):
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)
@ -114,33 +122,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("Nom de l'institution d'accueil", max_length=250)
type_lieu = models.CharField(
"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("Ville", max_length=200)
pays = models.CharField(
"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("Coordonnées", geography=True, srid=4326)
# Type du lieu en plus joli
@property
@ -152,18 +160,20 @@ 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"
verbose_name_plural = "Lieux"
#
# Matières des stages
#
class StageMatiere(models.Model):
nom = models.CharField(u"Nom", max_length=30)
nom = models.CharField("Nom", max_length=30)
slug = models.SlugField()
class Meta:
@ -173,49 +183,59 @@ 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)
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)
auteur = models.ForeignKey(
Normalien, related_name="stages", on_delete=models.SET_NULL, null=True
)
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",
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(
"Type",
default="stage",
choices=TYPE_STAGE_OPTIONS,
max_length=choices_length(TYPE_STAGE_OPTIONS),
)
niveau_scol = models.CharField(
"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")
encadrants = models.CharField(u"Encadrant⋅e⋅s", max_length=500, blank=True)
structure = models.CharField(u"Structure d'accueil", max_length=500, blank=True)
thematiques = TaggableManager("Thématiques", blank=True)
matieres = models.ManyToManyField(
StageMatiere, verbose_name="Matière(s)", related_name="stages"
)
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(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 +249,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,10 +265,10 @@ 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)
return "%s (par %s)" % (self.sujet, self.auteur.user.username)
def update_stats(self, save=True):
def get_len(obj):
@ -272,32 +293,40 @@ class Stage(models.Model):
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)
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" % (self.stage.sujet, self.stage.auteur.user.username)
return "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):
@ -305,25 +334,26 @@ 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
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
]

View file

@ -1,330 +1,337 @@
# 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', 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"),
(
"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"),
)
# 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"),
)

View file

@ -1,28 +1,33 @@
# 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()
@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*([?!:])", "\\1\\2", value)
value = re.sub(r"(\w)\s*([,.])", "\\1\\2", value)
value = re.sub(r"([?!:,.])(\w)", "\\1 \\2", value)
return value
@register.filter
def avis_len(value):
if value < 5:
@ -32,6 +37,7 @@ def avis_len(value):
else:
return "long"
@register.simple_tag
def url_replace(request, field, value):
dict_ = request.GET.copy()

View file

@ -1,25 +1,25 @@
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, Stage, StageMatiere, User
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"
@ -33,21 +33,29 @@ class ExperiENSTestCase(TestCase):
)
self.sa_conscrit.save()
self.u_archi = User.objects.create_user('archicube',
'archicube@ens.fr',
'archicube')
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.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 = 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")
@ -55,258 +63,282 @@ class ExperiENSTestCase(TestCase):
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.cstage1.id})
)
self.assertRedirectToLogin(reverse('avisstage:stage',
kwargs={'pk':self.cstage2.id}))
self.assertRedirectToLogin(reverse('avisstage:stage',
kwargs={'pk':self.astage1.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_archi.username}))
self.assertRedirectToLogin(
reverse("avisstage:profil", kwargs={"username": self.u_conscrit.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"})
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"})
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": "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.cstage1.id})
)
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'))
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.cstage1.id})
)
self.assertPageNotFound(reverse('avisstage:stage',
kwargs={'pk':self.cstage2.id}))
self.assertPageNotFound(
reverse("avisstage:stage", kwargs={"pk": self.cstage2.id})
)
testurl = reverse('avisstage:stage',
kwargs={'pk':self.astage1.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"))
self.assert403Archicubes(reverse('avisstage:recherche_resultats'))
self.assert403Archicubes(reverse("avisstage:recherche_resultats"))
self.assert403Archicubes(reverse('avisstage:stage_items'))
self.assert403Archicubes(reverse("avisstage:stage_items"))
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 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"})
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": "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}))
self.assertRedirects(
r, reverse("avisstage:stage", kwargs={"pk": self.astage1.id})
)
testurl = reverse('avisstage:stage_ajout')
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)
@ -337,14 +369,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):
@ -354,14 +386,12 @@ class ScolariteViewsTest(ExperiENSTestCase):
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(
@ -371,15 +401,18 @@ class ScolariteViewsTest(ExperiENSTestCase):
)
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 +422,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}))
self.assertPageNotFound(
reverse("avisstage:stage", kwargs={"pk": self.cstage2.id})
)
testurl = reverse('avisstage:stage',
kwargs={'pk':self.vstage1.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"})
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"})
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
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": "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}))
self.assertRedirects(
r, reverse("avisstage:stage", kwargs={"pk": self.vstage1.id})
)
testurl = reverse('avisstage:stage_ajout')
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)

View file

@ -1,51 +1,69 @@
from django.urls import include, path
from . import views, api
from tastypie.api import Api
v1_api = Api(api_name='v1')
from django.urls import include, path
from . import api, views, views_search
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/<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'),
path('403/archicubes/', views.archicubes_interdits,
name='403-archicubes'),
path('lieu/save/', views.save_lieu, name='lieu_ajout'),
path('profil/show/<str:username>/', 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/<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'),
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/<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"),
path("403/archicubes/", views.archicubes_interdits, name="403-archicubes"),
path("lieu/save/", views.save_lieu, name="lieu_ajout"),
path("profil/show/<str:username>/", 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/<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_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)),
]

View file

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

View file

@ -1,47 +1,53 @@
# coding: utf-8
from django.shortcuts import render, redirect, get_object_or_404
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.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
import math
import random
from collections import Counter, defaultdict
from braces.views import LoginRequiredMixin
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 import forms
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.views import PasswordResetConfirmView
from django.core.mail import send_mail
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,
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
#
# 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,35 +59,41 @@ 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):
@ -95,30 +107,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 +144,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,46 +160,60 @@ 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
@ -192,9 +222,9 @@ def save_lieu(request):
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
@ -203,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
@ -217,51 +248,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):
@ -283,24 +317,25 @@ def publier_stage(request, pk):
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 +343,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,45 +353,76 @@ 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"
@ -369,12 +434,14 @@ class MesAdressesMixin(LoginRequiredMixin):
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,13 +450,17 @@ 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
@ -403,9 +474,11 @@ class MesParametres(LoginRequiredMixin, FormView):
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
@ -417,10 +490,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 +505,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)
except Exception as e:
email = EmailAddress.objects.confirm(
self.kwargs["key"], self.request.user, True
)
except Exception:
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 +531,27 @@ 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 +561,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

View file

@ -1,18 +1,15 @@
# coding: utf-8
import json
import logging
from datetime import date
from django import forms
from django.contrib.auth.decorators import login_required
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 Q, Case, When
from django.http import JsonResponse, HttpResponseBadRequest
from django.shortcuts import render, redirect, get_object_or_404
import json
import logging
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 render
USE_ELASTICSEARCH = getattr(settings, "USE_ELASTICSEARCH", True)
@ -21,36 +18,44 @@ 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")
# 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)
sujet = forms.CharField(label="À propos de", required=False)
contexte = forms.CharField(
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)),
required=False)
niveau_scol = forms.ChoiceField(label="Année d'étude", choices=([('', u'')]
+ list(NIVEAU_SCOL_OPTIONS)),
required=False)
type_stage = forms.ChoiceField(
label="Type de stage",
choices=([("", "")] + list(TYPE_STAGE_OPTIONS)),
required=False,
)
niveau_scol = forms.ChoiceField(
label="Année d'étude",
choices=([("", "")] + 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')
type_lieu = forms.ChoiceField(
label="Type de lieu d'accueil",
choices=([("", "")] + list(TYPE_LIEU_OPTIONS)),
required=False,
)
tri = forms.ChoiceField(
label="Tri par",
choices=[("pertinence", "Pertinence"), ("-date_maj", "Dernière mise à jour")],
required=False,
initial="pertinence",
)
def cherche(**kwargs):
@ -58,9 +63,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 +78,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 +128,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
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 +183,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 +213,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 +249,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})
return JsonResponse(
{"stages": stages, "page": page, "num_pages": paginator.num_pages}
)
template_name = 'avisstage/recherche/resultats.html'
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})

View file

@ -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": ("Entrez une latitude valide."),
"invalid_longitude": ("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

View file

@ -1,6 +1,7 @@
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 @<promo> logic

View file

@ -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
from .secrets import GOOGLE_API_KEY, MAPBOX_API_KEY, SECRET_KEY # noqa
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
@ -24,68 +25,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',
#'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'
"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',
"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 +92,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 +107,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,
},
},
}

View file

@ -1,11 +1,14 @@
from .settings_base import *
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'),
"default": {
"ENGINE": "django.contrib.gis.db.backends.spatialite",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
@ -13,35 +16,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")

View file

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

View file

@ -1,20 +1,18 @@
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')),
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

View file

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

View file

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

10
setup.cfg Normal file
View file

@ -0,0 +1,10 @@
[flake8]
max-line-length = 99
exclude = .git, *.pyc, __pycache__, migrations
extend-ignore = E231, E203, E402
[isort]
profile = black
known_django = django
known_first_party = avisstage
sections = FUTURE,STDLIB,THIRDPARTY,DJANGO,FIRSTPARTY,LOCALFOLDER