This commit is contained in:
Tom Hubrecht 2021-02-07 23:15:47 +01:00
parent 26ad68ff69
commit 7cfc85f1fc
31 changed files with 2461 additions and 1269 deletions

View file

@ -5,29 +5,36 @@ from avisstage.models import *
import authens.models as authmod import authens.models as authmod
class NormalienInline(admin.StackedInline): class NormalienInline(admin.StackedInline):
model = Normalien model = Normalien
inline_classes = ("collapse open",) inline_classes = ("collapse open",)
class UserAdmin(UserAdmin): class UserAdmin(UserAdmin):
inlines = (NormalienInline, ) inlines = (NormalienInline,)
class AvisLieuInline(admin.StackedInline): class AvisLieuInline(admin.StackedInline):
model = AvisLieu model = AvisLieu
inline_classes = ("collapse open",) inline_classes = ("collapse open",)
extra = 0 extra = 0
class AvisStageInline(admin.StackedInline): class AvisStageInline(admin.StackedInline):
model = AvisStage model = AvisStage
inline_classes = ("collapse open",) inline_classes = ("collapse open",)
extra = 0 extra = 0
class StageAdmin(admin.ModelAdmin): class StageAdmin(admin.ModelAdmin):
inlines = (AvisLieuInline, AvisStageInline) inlines = (AvisLieuInline, AvisStageInline)
class StageMatiereAdmin(admin.ModelAdmin): class StageMatiereAdmin(admin.ModelAdmin):
model = StageMatiere model = StageMatiere
prepopulated_fields = {"slug": ('nom',)} prepopulated_fields = {"slug": ("nom",)}
admin.site.unregister(User) admin.site.unregister(User)
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)

View file

@ -10,23 +10,25 @@ from django.urls import reverse
from .models import Lieu, Stage, Normalien, StageMatiere from .models import Lieu, Stage, Normalien, StageMatiere
from .utils import approximate_distance from .utils import approximate_distance
class EnScolariteAuthentication(SessionAuthentication): class EnScolariteAuthentication(SessionAuthentication):
def is_authenticated(self, request, **kwargs): def is_authenticated(self, request, **kwargs):
if super().is_authenticated(request, **kwargs): if super().is_authenticated(request, **kwargs):
return request.user.profil.en_scolarite return request.user.profil.en_scolarite
return False return False
# API principale pour les lieux # API principale pour les lieux
class LieuResource(ModelResource): class LieuResource(ModelResource):
#stages = fields.ToManyField("avisstage.api.StageResource", # stages = fields.ToManyField("avisstage.api.StageResource",
# "stages", use_in="detail", full=True) # "stages", use_in="detail", full=True)
class Meta: class Meta:
queryset = Lieu.objects.all() queryset = Lieu.objects.all()
resource_name = "lieu" resource_name = "lieu"
fields = ["nom", "ville", "pays", "coord", "type_lieu", "id"] fields = ["nom", "ville", "pays", "coord", "type_lieu", "id"]
#login_required # login_required
authentication = SessionAuthentication() authentication = SessionAuthentication()
# Filtres personnalisés # Filtres personnalisés
@ -37,43 +39,44 @@ class LieuResource(ModelResource):
# Trouver les lieux à proximités d'un point donné # Trouver les lieux à proximités d'un point donné
if "lng" in filters and "lat" in filters: if "lng" in filters and "lat" in filters:
lat = float(filters['lat']) lat = float(filters["lat"])
lng = float(filters['lng']) lng = float(filters["lng"])
pt = geos.Point((lng,lat), srid=4326) pt = geos.Point((lng, lat), srid=4326)
self.reference_point = pt 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 # Filtrer les lieux qui ont déjà des stages
if "has_stage" in filters: if "has_stage" in filters:
orm_filters['stages__public'] = True orm_filters["stages__public"] = True
return orm_filters return orm_filters
# Custom apply filters pour ajouter le "distinct" # Custom apply filters pour ajouter le "distinct"
def apply_filters(self, request, applicable_filters): def apply_filters(self, request, applicable_filters):
return self.get_object_list(request).filter(**applicable_filters).distinct() return self.get_object_list(request).filter(**applicable_filters).distinct()
# Ajout d'informations # Ajout d'informations
def dehydrate(self, bundle): def dehydrate(self, bundle):
bundle = super(LieuResource, self).dehydrate(bundle) bundle = super(LieuResource, self).dehydrate(bundle)
obj = bundle.obj obj = bundle.obj
bundle.data['coord'] = {'lat': float(obj.coord.y), bundle.data["coord"] = {"lat": float(obj.coord.y), "lng": float(obj.coord.x)}
'lng': float(obj.coord.x)}
# Distance au point recherché # Distance au point recherché
if "lat" in bundle.request.GET and "lng" in bundle.request.GET: if "lat" in bundle.request.GET and "lng" in bundle.request.GET:
bundle.data['distance'] = approximate_distance( bundle.data["distance"] = approximate_distance(
self.reference_point, bundle.obj.coord) self.reference_point, bundle.obj.coord
)
# Autres infos utiles # Autres infos utiles
bundle.data["pays_nom"] = obj.get_pays_display() bundle.data["pays_nom"] = obj.get_pays_display()
bundle.data["type_lieu_nom"] = obj.type_lieu_fancy bundle.data["type_lieu_nom"] = obj.type_lieu_fancy
# TODO use annotate? # TODO use annotate?
bundle.data["num_stages"] = obj.stages.filter(public=True).count() bundle.data["num_stages"] = obj.stages.filter(public=True).count()
return bundle return bundle
# API sur un stage # API sur un stage
class StageResource(ModelResource): class StageResource(ModelResource):
class Meta: class Meta:
@ -81,7 +84,7 @@ class StageResource(ModelResource):
resource_name = "stage" resource_name = "stage"
fields = ["sujet", "date_debut", "date_fin", "matieres", "id"] fields = ["sujet", "date_debut", "date_fin", "matieres", "id"]
#login_required # login_required
authentication = EnScolariteAuthentication() authentication = EnScolariteAuthentication()
# Filtres personnalisés # Filtres personnalisés
@ -92,9 +95,9 @@ class StageResource(ModelResource):
# Récupération des stages à un lieu donné # Récupération des stages à un lieu donné
if "lieux" in filters: if "lieux" in filters:
flieux = map(int, filters['lieux'].split(',')) flieux = map(int, filters["lieux"].split(","))
orm_filters['lieux__id__in'] = flieux orm_filters["lieux__id__in"] = flieux
return orm_filters return orm_filters
# Informations à ajouter # Informations à ajouter
@ -103,23 +106,27 @@ class StageResource(ModelResource):
obj = bundle.obj obj = bundle.obj
# Affichage des manytomany en condensé # Affichage des manytomany en condensé
bundle.data['auteur'] = obj.auteur.nom bundle.data["auteur"] = obj.auteur.nom
bundle.data['thematiques'] = list(obj.thematiques.all().values_list("name", flat=True)) bundle.data["thematiques"] = list(
bundle.data['matieres'] = list(obj.matieres.all().values_list("nom", flat=True)) 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 # 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 return bundle
# Auteurs des fiches (TODO supprimer ?) # Auteurs des fiches (TODO supprimer ?)
class AuteurResource(ModelResource): class AuteurResource(ModelResource):
stages = fields.ToManyField("avisstage.api.StageResource", stages = fields.ToManyField(
"stages", use_in="detail") "avisstage.api.StageResource", "stages", use_in="detail"
)
class Meta: class Meta:
queryset = Normalien.objects.all() queryset = Normalien.objects.all()
resource_name = "profil" resource_name = "profil"
fields = ["id", "nom", "stages"] fields = ["id", "nom", "stages"]
#login_required # login_required
authentication = EnScolariteAuthentication() authentication = EnScolariteAuthentication()

View file

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

View file

@ -3,10 +3,12 @@ from functools import wraps
from django.urls import reverse from django.urls import reverse
from django.shortcuts import redirect from django.shortcuts import redirect
def en_scolarite_required(view_func): def en_scolarite_required(view_func):
@wraps(view_func) @wraps(view_func)
def _wrapped_view(request, *args, **kwargs): def _wrapped_view(request, *args, **kwargs):
if request.user.profil.en_scolarite: if request.user.profil.en_scolarite:
return view_func(request, *args, **kwargs) return view_func(request, *args, **kwargs)
return redirect(reverse("avisstage:403-archicubes")) return redirect(reverse("avisstage:403-archicubes"))
return _wrapped_view return _wrapped_view

View file

@ -6,47 +6,55 @@ from .statics import PAYS_OPTIONS
PAYS_DICT = dict(PAYS_OPTIONS) PAYS_DICT = dict(PAYS_OPTIONS)
stage = Index('stages') stage = Index("stages")
stage.settings( stage.settings(number_of_shards=1, number_of_replicas=0)
number_of_shards=1,
number_of_replicas=0
)
text_analyzer = analyzer( text_analyzer = analyzer(
'default', "default",
tokenizer="standard", tokenizer="standard",
filter=['lowercase', 'asciifolding', filter=[
token_filter("frstop", type="stop", stopwords="_french_"), "lowercase",
token_filter("frsnow", type="snowball", language="French")]) "asciifolding",
token_filter("frstop", type="stop", stopwords="_french_"),
token_filter("frsnow", type="snowball", language="French"),
],
)
stage.analyzer(text_analyzer) stage.analyzer(text_analyzer)
@stage.doc_type @stage.doc_type
class StageDocument(Document): class StageDocument(Document):
lieux = fields.ObjectField(properties={ lieux = fields.ObjectField(
'nom': fields.TextField(), properties={
'ville': fields.TextField(), "nom": fields.TextField(),
'pays': fields.TextField(), "ville": fields.TextField(),
}) "pays": fields.TextField(),
auteur = fields.ObjectField(properties={ }
'nom': fields.TextField(), )
}) auteur = fields.ObjectField(
properties={
"nom": fields.TextField(),
}
)
thematiques = fields.TextField() thematiques = fields.TextField()
matieres = fields.TextField() matieres = fields.TextField()
class Django: class Django:
model = Stage model = Stage
fields = [ fields = [
'sujet', "sujet",
'encadrants', "encadrants",
'type_stage', "type_stage",
'niveau_scol', "niveau_scol",
'structure', "structure",
'date_debut', "date_debut",
'date_fin' "date_fin",
] ]
def prepare_thematiques(self, instance): 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): def prepare_matieres(self, instance):
return ", ".join(instance.matieres.all().values_list("nom", flat=True)).lower() return ", ".join(instance.matieres.all().values_list("nom", flat=True)).lower()
@ -65,11 +73,11 @@ class StageDocument(Document):
def prepare_sujet(self, instance): def prepare_sujet(self, instance):
return instance.sujet.lower() return instance.sujet.lower()
# Hook pour l'affichage des noms de pays # Hook pour l'affichage des noms de pays
def prepare(self, instance): def prepare(self, instance):
data = super(StageDocument, self).prepare(instance) data = super(StageDocument, self).prepare(instance)
for lieu in data['lieux']: for lieu in data["lieux"]:
lieu['pays'] = PAYS_DICT[lieu['pays']].lower() lieu["pays"] = PAYS_DICT[lieu["pays"]].lower()
return data return data

View file

@ -15,32 +15,52 @@ from .widgets import LatLonField
class HTMLTrimmerForm(forms.ModelForm): class HTMLTrimmerForm(forms.ModelForm):
def clean(self): def clean(self):
# Suppression des espaces blanc avant et après le texte pour les champs html # 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) leading_white = re.compile(
trailing_white = re.compile(r"(( \t\n)*<p>(&nbsp;|[ \n\t]|<br[ /]*>)*</p>)+?( \t\n)*$", re.IGNORECASE) 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() cleaned_data = super(HTMLTrimmerForm, self).clean()
for (fname, fval) in cleaned_data.items(): for (fname, fval) in cleaned_data.items():
# Heuristique : les champs commençant par "avis_" sont des champs html # Heuristique : les champs commençant par "avis_" sont des champs html
if fname[:5] == "avis_": 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 return cleaned_data
# Infos sur un stage # Infos sur un stage
class StageForm(forms.ModelForm): class StageForm(forms.ModelForm):
date_widget = forms.DateInput(attrs={"class":"datepicker", date_widget = forms.DateInput(
"placeholder":"JJ/MM/AAAA"}) 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_debut = forms.DateField(
date_fin = forms.DateField(label=u"Date de fin", label=u"Date de début", input_formats=["%d/%m/%Y"], widget=date_widget
input_formats=["%d/%m/%Y"], widget=date_widget) )
date_fin = forms.DateField(
label=u"Date de fin", input_formats=["%d/%m/%Y"], widget=date_widget
)
class Meta: class Meta:
model = Stage 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 = { 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", "thematiques": u"Mettez une virgule pour valider votre thématique si la suggestion ne correspond pas ou si elle n'existe pas encore",
"structure": u"Nom de l'équipe, du laboratoire, de la startup... (si le lieu ne suffit pas)" "structure": u"Nom de l'équipe, du laboratoire, de la startup... (si le lieu ne suffit pas)",
} }
labels = { labels = {
"date_debut": u"Date de début", "date_debut": u"Date de début",
@ -51,27 +71,36 @@ class StageForm(forms.ModelForm):
if "request" in kwargs: if "request" in kwargs:
self.request = kwargs.pop("request") self.request = kwargs.pop("request")
super(StageForm, self).__init__(*args, **kwargs) super(StageForm, self).__init__(*args, **kwargs)
def save(self, commit=True): def save(self, commit=True):
# Lors de la création : attribution à l'utilisateur connecté # 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 self.instance.auteur = self.request.user.profil
# Date de modification # Date de modification
self.instance.date_maj = timezone.now() self.instance.date_maj = timezone.now()
self.instance.update_stats(False) self.instance.update_stats(False)
stage = super(StageForm, self).save(commit=commit) stage = super(StageForm, self).save(commit=commit)
return stage return stage
# Sous-formulaire des avis sur le stage # Sous-formulaire des avis sur le stage
class AvisStageForm(HTMLTrimmerForm): class AvisStageForm(HTMLTrimmerForm):
class Meta: class Meta:
model = AvisStage 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 = { help_texts = {
"chapo": u"\"Trop long, pas lu\" : une accroche résumant ce que vous avez pensé de ce séjour", "chapo": u'"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de ce séjour',
"avis_ambiance": u"Avez-vous passé un bon moment à ce travail ? Étiez-vous assez guidé⋅e ? Aviez-vous un bon contact avec vos encadrant⋅e⋅s ? Y avait-il une bonne ambiance dans l'équipe ?", "avis_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_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_admin": u"Avez-vous commencé votre travail à la date prévue ? Était-ce compliqué d'obtenir les documents nécessaires (visa, contrats, etc) ? L'administration de l'établissement vous a-t-elle aidé⋅e ? Étiez-vous rémunéré⋅e ?",
@ -80,48 +109,62 @@ class AvisStageForm(HTMLTrimmerForm):
"les_moins": u"Ce qui aurait pu être mieux", "les_moins": u"Ce qui aurait pu être mieux",
} }
class AvisLieuForm(HTMLTrimmerForm): class AvisLieuForm(HTMLTrimmerForm):
class Meta: class Meta:
model = AvisLieu 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 = { help_texts = {
"chapo": u"\"Trop long, pas lu\" : une accroche résumant ce que vous avez pensé de cet endroit", "chapo": u'"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de cet endroit',
"avis_lieustage": u"Qu'avez-vous pensé des lieux où vous travailliez ? Les bâtiments étaient-ils modernes ? Était-il agréable d'y travailler ?", "avis_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_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 ?", "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_plus": u"Les meilleures raisons de partir à cet endroit",
"les_moins": u"Ce qui vous a gêné ou manqué là-bas", "les_moins": u"Ce qui vous a gêné ou manqué là-bas",
} }
widgets = { widgets = {"lieu": forms.HiddenInput(attrs={"class": "lieu-hidden"})}
"lieu": forms.HiddenInput(attrs={"class":"lieu-hidden"})
}
# Création d'un nouveau lieu # Création d'un nouveau lieu
class LieuForm(forms.ModelForm): class LieuForm(forms.ModelForm):
coord = LatLonField() coord = LatLonField()
id = forms.IntegerField(widget=forms.widgets.HiddenInput(), required=False) id = forms.IntegerField(widget=forms.widgets.HiddenInput(), required=False)
class Meta: class Meta:
model = Lieu model = Lieu
fields = ['id', 'nom', 'type_lieu', 'ville', 'pays', 'coord'] fields = ["id", "nom", "type_lieu", "ville", "pays", "coord"]
# Widget de feedback # Widget de feedback
class FeedbackForm(forms.Form): class FeedbackForm(forms.Form):
objet = forms.CharField(label="Objet", required=True) 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 # Nouvelle adresse mail
class AdresseEmailForm(forms.Form): class AdresseEmailForm(forms.Form):
def __init__(self, _user, **kwargs): def __init__(self, _user, **kwargs):
self._user = _user self._user = _user
super().__init__(**kwargs) 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): def clean_email(self):
email = self.cleaned_data["email"] email = self.cleaned_data["email"]
if EmailAddress.objects.filter(user=self._user, email=email).exists(): if EmailAddress.objects.filter(user=self._user, email=email).exists():
raise forms.ValidationError( raise forms.ValidationError("Cette adresse est déjà associée à ce compte")
"Cette adresse est déjà associée à ce compte")
return email return email
@ -131,19 +174,25 @@ def _unicode_ci_compare(s1, s2):
recommended algorithm from Unicode Technical Report 36, section recommended algorithm from Unicode Technical Report 36, section
2.11.2(B)(2). 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 # (Ré)initialisation du mot de passe
class ReinitMdpForm(PasswordResetForm): class ReinitMdpForm(PasswordResetForm):
def get_users(self, email): def get_users(self, email):
"""Override default method to allow unusable passwords""" """Override default method to allow unusable passwords"""
email_field_name = User.get_email_field_name() email_field_name = User.get_email_field_name()
active_users = User._default_manager.filter(**{ active_users = User._default_manager.filter(
'%s__iexact' % email_field_name: email, **{
'is_active': True, "%s__iexact" % email_field_name: email,
}) "is_active": True,
}
)
return ( return (
u for u in active_users u
for u in active_users
if _unicode_ci_compare(email, getattr(u, email_field_name)) if _unicode_ci_compare(email, getattr(u, email_field_name))
) )

View file

@ -1,43 +1,60 @@
#coding: utf-8 # coding: utf-8
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db.models import Count from django.db.models import Count
from avisstage.models import Stage, Lieu from avisstage.models import Stage, Lieu
class Command(BaseCommand): class Command(BaseCommand):
help = 'Nettoie les stages à plusieurs lieux identiques' help = "Nettoie les stages à plusieurs lieux identiques"
def add_arguments(self, parser): 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( parser.add_argument(
'--apply', "--apply",
action='store_true', action="store_true",
default=False, default=False,
help='Applies the modifications', help="Applies the modifications",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
rundb = False rundb = False
if options.get('apply', False): if options.get("apply", False):
rundb = True rundb = True
else: else:
print(u"Les modifications ne seront pas appliquées") print(u"Les modifications ne seront pas appliquées")
min_lieu = options.get('min_lieu', 0)
for lieu in Lieu.objects.filter(id__gte=min_lieu).order_by('-id'): min_lieu = options.get("min_lieu", 0)
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: if len(lproches) == 0:
continue continue
print(u"Doublons possibles pour %s (id=%d, %d avis) :" % (lieu, lieu.id, lieu.avislieu_set.count())) print(
u"Doublons possibles pour %s (id=%d, %d avis) :"
% (lieu, lieu.id, lieu.avislieu_set.count())
)
for plieu in lproches: for plieu in lproches:
pprint = u" > %s (id=%d, %d avis)" % (plieu, plieu.id, plieu.avislieu_set.count()) pprint = u" > %s (id=%d, %d avis)" % (
if plieu.nom == lieu.nom and plieu.ville == lieu.ville and plieu.type_lieu == lieu.type_lieu: plieu,
print(u"%s %s" % (pprint, self.style.SUCCESS(u'-> Suppression'))) plieu.id,
plieu.avislieu_set.count(),
)
if (
plieu.nom == lieu.nom
and plieu.ville == lieu.ville
and plieu.type_lieu == lieu.type_lieu
):
print(u"%s %s" % (pprint, self.style.SUCCESS(u"-> Suppression")))
if rundb: if rundb:
for avis in plieu.avislieu_set.all(): for avis in plieu.avislieu_set.all():
avis.lieu = lieu avis.lieu = lieu
avis.save() avis.save()
plieu.delete() plieu.delete()
else: else:
print(u"%s %s" % (pprint, self.style.WARNING(u'-> À supprimer manuellement'))) print(
self.stdout.write(self.style.SUCCESS(u'Nettoyage des lieux effectué')) u"%s %s"
% (pprint, self.style.WARNING(u"-> À supprimer manuellement"))
)
self.stdout.write(self.style.SUCCESS(u"Nettoyage des lieux effectué"))

View file

@ -1,18 +1,19 @@
#coding: utf-8 # coding: utf-8
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db.models import Count from django.db.models import Count
from avisstage.models import Stage, Lieu from avisstage.models import Stage, Lieu
class Command(BaseCommand): class Command(BaseCommand):
help = 'Nettoie les stages à plusieurs lieux identiques' help = "Nettoie les stages à plusieurs lieux identiques"
def add_arguments(self, parser): 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( parser.add_argument(
'--apply', "--apply",
action='store_true', action="store_true",
default=False, default=False,
help='Applies the modifications', help="Applies the modifications",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
@ -27,15 +28,16 @@ class Command(BaseCommand):
return length return length
rundb = False rundb = False
if options.get('apply', False): if options.get("apply", False):
rundb = True rundb = True
else: else:
print(u"Les modifications ne seront pas appliquées") print(u"Les modifications ne seront pas appliquées")
min_stage = options.get('min_stage', 0)
for stage in Stage.objects.annotate(c=Count("lieux"))\ min_stage = options.get("min_stage", 0)
.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 = {} lieuset = {}
todel = [] todel = []
problems = [] problems = []
@ -54,13 +56,18 @@ class Command(BaseCommand):
if len(todel) > 0: if len(todel) > 0:
print(u"Doublons détectés dans %s" % (stage,)) print(u"Doublons détectés dans %s" % (stage,))
for avis, alen in todel: for avis, alen in todel:
print(u" > Suppression de l'avis sur %s de %d mots" % \ print(
(avis.lieu, alen)) u" > Suppression de l'avis sur %s de %d mots"
% (avis.lieu, alen)
)
if rundb: if rundb:
avis.delete() avis.delete()
if len(problems) > 0: if len(problems) > 0:
self.stdout.write(self.style.WARNING(u"Réparation impossible de %s (id=%d)" % (stage, stage.id))) self.stdout.write(
self.style.WARNING(
u"Réparation impossible de %s (id=%d)" % (stage, stage.id)
)
)
for avis, alen in problems: for avis, alen in problems:
print(u" > Avis sur %s de %d mots" % \ print(u" > Avis sur %s de %d mots" % (avis.lieu, alen))
(avis.lieu, alen)) self.stdout.write(self.style.SUCCESS(u"Nettoyage des stages effectué"))
self.stdout.write(self.style.SUCCESS(u'Nettoyage des stages effectué'))

View file

@ -1,35 +1,42 @@
#coding: utf-8 # coding: utf-8
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db.models import Count from django.db.models import Count
from avisstage.models import Stage, Lieu from avisstage.models import Stage, Lieu
class Command(BaseCommand): class Command(BaseCommand):
help = 'Nettoie les stages à plusieurs lieux identiques' help = "Nettoie les stages à plusieurs lieux identiques"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('del_lieu', type=int, help='Lieu à supprimer') 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("repl_lieu", type=int, help="Lieu le remplaçant")
parser.add_argument( parser.add_argument(
'--apply', "--apply",
action='store_true', action="store_true",
default=False, default=False,
help='Applies the modifications', help="Applies the modifications",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
rundb = False rundb = False
if options.get('apply', False): if options.get("apply", False):
rundb = True rundb = True
else: else:
print(u"Les modifications ne seront pas appliquées") print(u"Les modifications ne seront pas appliquées")
plieu = Lieu.objects.get(id=options['del_lieu']) plieu = Lieu.objects.get(id=options["del_lieu"])
lieu = Lieu.objects.get(id=options['repl_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(
print(u"Remplacement par %s (id=%d, %d avis)" % (lieu, lieu.id, lieu.avislieu_set.count())) u"Suppression de %s (id=%d, %d avis)"
% (plieu, plieu.id, plieu.avislieu_set.count())
)
print(
u"Remplacement par %s (id=%d, %d avis)"
% (lieu, lieu.id, lieu.avislieu_set.count())
)
if rundb: if rundb:
for avis in plieu.avislieu_set.all(): for avis in plieu.avislieu_set.all():
avis.lieu = lieu avis.lieu = lieu
avis.save() avis.save()
plieu.delete() plieu.delete()
self.stdout.write(self.style.SUCCESS(u'Terminé')) self.stdout.write(self.style.SUCCESS(u"Terminé"))

View file

@ -4,6 +4,7 @@ from avisstage.models import Normalien
from django.utils import timezone from django.utils import timezone
from datetime import timedelta from datetime import timedelta
class Command(BaseCommand): class Command(BaseCommand):
help = 'Réinitialise les statuts "en scolarité" de tout le monde' help = 'Réinitialise les statuts "en scolarité" de tout le monde'
@ -13,4 +14,4 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
old_conn = timezone.now() - timedelta(days=365) old_conn = timezone.now() - timedelta(days=365)
Normalien.objects.all().update(last_cas_connect=t) Normalien.objects.all().update(last_cas_connect=t)
self.stdout.write(self.style.SUCCESS(u'Terminé')) self.stdout.write(self.style.SUCCESS(u"Terminé"))

File diff suppressed because one or more lines are too long

View file

@ -9,23 +9,29 @@ import tinymce.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('avisstage', '0001_initial'), ("avisstage", "0001_initial"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='avisstage', model_name="avisstage",
name='avis_prestage', name="avis_prestage",
field=tinymce.models.HTMLField(blank=True, default='', verbose_name='Avant le stage'), field=tinymce.models.HTMLField(
blank=True, default="", verbose_name="Avant le stage"
),
), ),
migrations.AddField( migrations.AddField(
model_name='stage', model_name="stage",
name='len_avis_lieux', name="len_avis_lieux",
field=models.IntegerField(default=0, verbose_name='Longueur des avis de lieu'), field=models.IntegerField(
default=0, verbose_name="Longueur des avis de lieu"
),
), ),
migrations.AddField( migrations.AddField(
model_name='stage', model_name="stage",
name='len_avis_stage', name="len_avis_stage",
field=models.IntegerField(default=0, verbose_name='Longueur des avis de 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

@ -2,18 +2,19 @@ from django.apps import apps as global_apps
from django.db import migrations from django.db import migrations
from django.utils import timezone from django.utils import timezone
def forwards(apps, schema_editor): def forwards(apps, schema_editor):
User = apps.get_model('auth', 'User') User = apps.get_model("auth", "User")
try: try:
CASAccount = apps.get_model('authens', 'CASAccount') CASAccount = apps.get_model("authens", "CASAccount")
except LookupError: except LookupError:
return return
try: try:
SocialAccount = apps.get_model('socialaccount', 'SocialAccount') SocialAccount = apps.get_model("socialaccount", "SocialAccount")
OldEmailAddress = apps.get_model('account', 'EmailAddress') OldEmailAddress = apps.get_model("account", "EmailAddress")
except LookupError: except LookupError:
# Allauth not installed # Allauth not installed
# Simply create CAS accounts for every profile # Simply create CAS accounts for every profile
@ -25,29 +26,26 @@ def forwards(apps, schema_editor):
if ldap_info: if ldap_info:
entrance_year = ldap_info["entrance_year"] entrance_year = ldap_info["entrance_year"]
CASAccount.objects.create( CASAccount.objects.create(
user=user, cas_login=user.username, user=user, cas_login=user.username, entrance_year=entrance_year
entrance_year=entrance_year
) )
for user in User.objects.all(): for user in User.objects.all():
migrate_user(user) migrate_user(user)
return return
NewEmailAddress = apps.get_model("simple_email_confirmation", "EmailAddress")
NewEmailAddress = apps.get_model('simple_email_confirmation',
'EmailAddress')
from simple_email_confirmation.models import EmailAddressManager from simple_email_confirmation.models import EmailAddressManager
# Transfer from allauth to authens # Transfer from allauth to authens
# Assumes usernames have the format <clipper>@<promo> # Assumes usernames have the format <clipper>@<promo>
# Assumes no clashing clipper accounts have ever been found # Assumes no clashing clipper accounts have ever been found
oldusers = ( oldusers = User.objects.all().prefetch_related(
User.objects.all().prefetch_related( "emailaddress_set", "socialaccount_set"
"emailaddress_set", "socialaccount_set")
) )
is_ens_mail = lambda mail: ( 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_conns = []
new_mails = [] new_mails = []
@ -56,14 +54,14 @@ def forwards(apps, schema_editor):
addresses = user.emailaddress_set.all() addresses = user.emailaddress_set.all()
for addr in addresses: for addr in addresses:
newaddr = NewEmailAddress( newaddr = NewEmailAddress(
user=user, email=addr.email, user=user,
email=addr.email,
set_at=timezone.now(), set_at=timezone.now(),
confirmed_at=(timezone.now() if addr.verified else None), confirmed_at=(timezone.now() if addr.verified else None),
key=EmailAddressManager().generate_key(), key=EmailAddressManager().generate_key(),
) )
if addr.primary and user.email != addr.email: if addr.primary and user.email != addr.email:
print("Adresse principale inconsistante", print("Adresse principale inconsistante", user.email, addr.email)
user.email, addr.email)
new_mails.append(newaddr) new_mails.append(newaddr)
# Create new CASAccount connexion # Create new CASAccount connexion
@ -78,30 +76,33 @@ def forwards(apps, schema_editor):
print(user.username) print(user.username)
continue continue
entrance_year = saccount.extra_data.get( entrance_year = saccount.extra_data.get(
"entrance_year", user.username.split("@")[1]) "entrance_year", user.username.split("@")[1]
)
try: try:
entrance_year = 2000 + int(entrance_year) entrance_year = 2000 + int(entrance_year)
except ValueError: except ValueError:
print(entrance_year) print(entrance_year)
continue continue
new_conns.append(CASAccount(user=user, cas_login=clipper, new_conns.append(
entrance_year=int(entrance_year))) CASAccount(user=user, cas_login=clipper, entrance_year=int(entrance_year))
)
NewEmailAddress.objects.bulk_create(new_mails) NewEmailAddress.objects.bulk_create(new_mails)
CASAccount.objects.bulk_create(new_conns) CASAccount.objects.bulk_create(new_conns)
class Migration(migrations.Migration): class Migration(migrations.Migration):
operations = [ operations = [
migrations.RunPython(forwards, migrations.RunPython.noop), migrations.RunPython(forwards, migrations.RunPython.noop),
] ]
dependencies = [ dependencies = [
('avisstage', '0003_auto_20210117_1208'), ("avisstage", "0003_auto_20210117_1208"),
('authens', '0002_old_cas_account'), ("authens", "0002_old_cas_account"),
] ]
if global_apps.is_installed('allauth'): if global_apps.is_installed("allauth"):
dependencies.append(('socialaccount', '0003_extra_data_default_dict')) dependencies.append(("socialaccount", "0003_extra_data_default_dict"))
if global_apps.is_installed('simple_email_confirmation'): if global_apps.is_installed("simple_email_confirmation"):
dependencies.append(('simple_email_confirmation', '0001_initial')) dependencies.append(("simple_email_confirmation", "0001_initial"))

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('avisstage', '0004_allauth_to_authens'), ("avisstage", "0004_allauth_to_authens"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='normalien', model_name="normalien",
name='en_scolarite', name="en_scolarite",
field=models.BooleanField(blank=True, default=False), field=models.BooleanField(blank=True, default=False),
), ),
] ]

View file

@ -7,26 +7,30 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('avisstage', '0005_normalien_en_scolarite'), ("avisstage", "0005_normalien_en_scolarite"),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(
model_name='normalien', model_name="normalien",
name='en_scolarite', name="en_scolarite",
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='normalien', model_name="normalien",
name='mail', name="mail",
), ),
migrations.AddField( migrations.AddField(
model_name='normalien', model_name="normalien",
name='last_cas_login', name="last_cas_login",
field=models.DateField(default=avisstage.models._default_cas_login), field=models.DateField(default=avisstage.models._default_cas_login),
), ),
migrations.AlterField( migrations.AlterField(
model_name='normalien', model_name="normalien",
name='contactez_moi', 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'), 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

@ -20,27 +20,39 @@ from datetime import timedelta
from .utils import choices_length, is_email_ens from .utils import choices_length, is_email_ens
from .statics import ( from .statics import (
DEPARTEMENTS_DEFAUT, PAYS_OPTIONS, TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS, TYPE_LIEU_DICT, DEPARTEMENTS_DEFAUT,
TYPE_STAGE_DICT, NIVEAU_SCOL_OPTIONS, NIVEAU_SCOL_DICT PAYS_OPTIONS,
TYPE_LIEU_OPTIONS,
TYPE_STAGE_OPTIONS,
TYPE_LIEU_DICT,
TYPE_STAGE_DICT,
NIVEAU_SCOL_OPTIONS,
NIVEAU_SCOL_DICT,
) )
def _default_cas_login(): 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) # Profil Normalien (extension du modèle User)
# #
class Normalien(models.Model): class Normalien(models.Model):
user = models.OneToOneField(User, related_name="profil", user = models.OneToOneField(
on_delete=models.SET_NULL, null=True) User, related_name="profil", on_delete=models.SET_NULL, null=True
)
# Infos spécifiques # Infos spécifiques
nom = models.CharField(u"Nom complet", max_length=255, blank=True) nom = models.CharField(u"Nom complet", max_length=255, blank=True)
promotion = models.CharField(u"Promotion", max_length=40, blank=True) promotion = models.CharField(u"Promotion", max_length=40, blank=True)
contactez_moi = models.BooleanField( contactez_moi = models.BooleanField(
u"Inviter les visiteurs à me contacter", u"Inviter les visiteurs à me contacter",
default=True, help_text="Affiche votre adresse e-mail principale sur votre profil public") default=True,
help_text="Affiche votre adresse e-mail principale sur votre profil public",
)
bio = models.TextField(u"À propos de moi", blank=True, default="") bio = models.TextField(u"À propos de moi", blank=True, default="")
last_cas_login = models.DateField(default=_default_cas_login) last_cas_login = models.DateField(default=_default_cas_login)
@ -53,12 +65,11 @@ class Normalien(models.Model):
# Liste des stages publiés # Liste des stages publiés
def stages_publics(self): 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): def has_nonENS_email(self):
return ( return (
self.user.email_address_set self.user.email_address_set.exclude(confirmed_at__isnull=True)
.exclude(confirmed_at__isnull=True)
.exclude(email__endswith="ens.fr") .exclude(email__endswith="ens.fr")
.exclude(email__endswith="ens.psl.eu") .exclude(email__endswith="ens.psl.eu")
.exists() .exists()
@ -77,18 +88,20 @@ class Normalien(models.Model):
def preferred_email(self): def preferred_email(self):
return self.user.email return self.user.email
# Hook à la création d'un nouvel utilisateur : information de base # Hook à la création d'un nouvel utilisateur : information de base
def create_basic_user_profile(sender, instance, created, **kwargs): def create_basic_user_profile(sender, instance, created, **kwargs):
if created: if created:
profil, created = Normalien.objects.get_or_create(user=instance) profil, created = Normalien.objects.get_or_create(user=instance)
if not created and profil.promotion != "": if not created and profil.promotion != "":
return return
if "@" in instance.username: if "@" in instance.username:
profil.promotion = instance.username.split("@")[1] profil.promotion = instance.username.split("@")[1]
profil.save() profil.save()
post_save.connect(create_basic_user_profile, sender=User) post_save.connect(create_basic_user_profile, sender=User)
# Hook d'authENS : information du CAS # Hook d'authENS : information du CAS
@ -104,7 +117,7 @@ def handle_cas_connection(sender, instance, created, cas_login, attributes, **kw
if len(dirs) < 4: if len(dirs) < 4:
print("HomeDirectory invalide", dirs) print("HomeDirectory invalide", dirs)
return return
year = dirs[2] year = dirs[2]
departement = dirs[3] departement = dirs[3]
@ -114,33 +127,33 @@ def handle_cas_connection(sender, instance, created, cas_login, attributes, **kw
profil.nom = attributes.get("name", "") profil.nom = attributes.get("name", "")
profil.save() profil.save()
post_cas_connect.connect(handle_cas_connection, sender=User) post_cas_connect.connect(handle_cas_connection, sender=User)
# #
# Lieu de stage # Lieu de stage
# #
class Lieu(models.Model): class Lieu(models.Model):
# Général # Général
nom = models.CharField(u"Nom de l'institution d'accueil", nom = models.CharField(u"Nom de l'institution d'accueil", max_length=250)
max_length=250) type_lieu = models.CharField(
type_lieu = models.CharField(u"Type de structure d'accueil", u"Type de structure d'accueil",
default="universite", default="universite",
choices=TYPE_LIEU_OPTIONS, choices=TYPE_LIEU_OPTIONS,
max_length=choices_length(TYPE_LIEU_OPTIONS)) max_length=choices_length(TYPE_LIEU_OPTIONS),
)
# Infos géographiques # Infos géographiques
ville = models.CharField(u"Ville", ville = models.CharField(u"Ville", max_length=200)
max_length=200) pays = models.CharField(
pays = models.CharField(u"Pays", u"Pays", choices=PAYS_OPTIONS, max_length=choices_length(PAYS_OPTIONS)
choices=PAYS_OPTIONS, )
max_length=choices_length(PAYS_OPTIONS))
# Coordonnées # Coordonnées
#objects = geomodels.GeoManager() # Requis par GeoDjango # objects = geomodels.GeoManager() # Requis par GeoDjango
coord = geomodels.PointField(u"Coordonnées", coord = geomodels.PointField(u"Coordonnées", geography=True, srid=4326)
geography=True,
srid = 4326)
# Type du lieu en plus joli # Type du lieu en plus joli
@property @property
@ -153,19 +166,21 @@ class Lieu(models.Model):
def __str__(self): def __str__(self):
return u"%s (%s)" % (self.nom, self.ville) return u"%s (%s)" % (self.nom, self.ville)
class Meta: class Meta:
verbose_name = "Lieu" verbose_name = "Lieu"
verbose_name_plural = "Lieux" verbose_name_plural = "Lieux"
# #
# Matières des stages # Matières des stages
# #
class StageMatiere(models.Model): class StageMatiere(models.Model):
nom = models.CharField(u"Nom", max_length=30) nom = models.CharField(u"Nom", max_length=30)
slug = models.SlugField() slug = models.SlugField()
class Meta: class Meta:
verbose_name = "Matière des stages" verbose_name = "Matière des stages"
verbose_name_plural = "Matières des stages" verbose_name_plural = "Matières des stages"
@ -173,14 +188,17 @@ class StageMatiere(models.Model):
def __str__(self): def __str__(self):
return self.nom return self.nom
# #
# Un stage # Un stage
# #
class Stage(models.Model): class Stage(models.Model):
# Misc # Misc
auteur = models.ForeignKey(Normalien, related_name="stages", auteur = models.ForeignKey(
on_delete=models.SET_NULL, null=True) Normalien, related_name="stages", on_delete=models.SET_NULL, null=True
)
public = models.BooleanField(u"Visible publiquement", default=False) public = models.BooleanField(u"Visible publiquement", default=False)
date_creation = models.DateTimeField(u"Créé le", default=timezone.now) date_creation = models.DateTimeField(u"Créé le", default=timezone.now)
date_maj = models.DateTimeField(u"Mis à jour le", default=timezone.now) date_maj = models.DateTimeField(u"Mis à jour le", default=timezone.now)
@ -193,29 +211,36 @@ class Stage(models.Model):
date_debut = models.DateField(u"Date de début", null=True) date_debut = models.DateField(u"Date de début", null=True)
date_fin = models.DateField(u"Date de fin", null=True) date_fin = models.DateField(u"Date de fin", null=True)
type_stage = models.CharField(u"Type", type_stage = models.CharField(
default="stage", u"Type",
choices=TYPE_STAGE_OPTIONS, default="stage",
max_length=choices_length(TYPE_STAGE_OPTIONS)) choices=TYPE_STAGE_OPTIONS,
niveau_scol = models.CharField(u"Année de scolarité", max_length=choices_length(TYPE_STAGE_OPTIONS),
default="", )
choices=NIVEAU_SCOL_OPTIONS, niveau_scol = models.CharField(
max_length=choices_length(NIVEAU_SCOL_OPTIONS), u"Année de scolarité",
blank=True) default="",
choices=NIVEAU_SCOL_OPTIONS,
max_length=choices_length(NIVEAU_SCOL_OPTIONS),
blank=True,
)
thematiques = TaggableManager(u"Thématiques", blank=True) thematiques = TaggableManager(u"Thématiques", blank=True)
matieres = models.ManyToManyField(StageMatiere, verbose_name=u"Matière(s)", related_name="stages") matieres = models.ManyToManyField(
StageMatiere, verbose_name=u"Matière(s)", related_name="stages"
)
encadrants = models.CharField(u"Encadrant⋅e⋅s", max_length=500, blank=True) encadrants = models.CharField(u"Encadrant⋅e⋅s", max_length=500, blank=True)
structure = models.CharField(u"Structure d'accueil", max_length=500, blank=True) structure = models.CharField(u"Structure d'accueil", max_length=500, blank=True)
# Avis # Avis
lieux = models.ManyToManyField(Lieu, related_name="stages", lieux = models.ManyToManyField(
through="AvisLieu", blank=True) Lieu, related_name="stages", through="AvisLieu", blank=True
)
# Affichage des avis ordonnés # Affichage des avis ordonnés
@property @property
def avis_lieux(self): def avis_lieux(self):
return self.avislieu_set.order_by('order') return self.avislieu_set.order_by("order")
# Shortcut pour affichage rapide # Shortcut pour affichage rapide
@property @property
@ -229,6 +254,7 @@ class Stage(models.Model):
@property @property
def type_stage_fancy(self): def type_stage_fancy(self):
return TYPE_STAGE_DICT.get(self.type_stage, ("stage", False))[0] return TYPE_STAGE_DICT.get(self.type_stage, ("stage", False))[0]
@property @property
def type_stage_fem(self): def type_stage_fem(self):
return TYPE_STAGE_DICT.get(self.type_stage, ("stage", False))[1] return TYPE_STAGE_DICT.get(self.type_stage, ("stage", False))[1]
@ -244,8 +270,8 @@ class Stage(models.Model):
return self.lieux.all() return self.lieux.all()
def get_absolute_url(self): def get_absolute_url(self):
return reverse('avisstage:stage', self) return reverse("avisstage:stage", self)
def __str__(self): def __str__(self):
return u"%s (par %s)" % (self.sujet, self.auteur.user.username) return u"%s (par %s)" % (self.sujet, self.auteur.user.username)
@ -259,26 +285,29 @@ class Stage(models.Model):
length += len(obj.les_plus.split()) length += len(obj.les_plus.split())
length += len(obj.les_moins.split()) length += len(obj.les_moins.split())
return length return length
if self.avis_stage: if self.avis_stage:
self.len_avis_stage = get_len(self.avis_stage) self.len_avis_stage = get_len(self.avis_stage)
self.len_avis_lieux = 0 self.len_avis_lieux = 0
for avis in self.avislieu_set.all(): for avis in self.avislieu_set.all():
self.len_avis_lieux += get_len(avis) self.len_avis_lieux += get_len(avis)
if save: if save:
self.save() self.save()
class Meta: class Meta:
verbose_name = "Stage" verbose_name = "Stage"
# #
# Les avis # Les avis
# #
class AvisStage(models.Model): class AvisStage(models.Model):
stage = models.OneToOneField(Stage, related_name="avis_stage", stage = models.OneToOneField(
on_delete=models.CASCADE) Stage, related_name="avis_stage", on_delete=models.CASCADE
)
chapo = models.TextField(u"En quelques mots", blank=True) chapo = models.TextField(u"En quelques mots", blank=True)
avis_ambiance = RichTextField(u"L'ambiance de travail", blank=True) avis_ambiance = RichTextField(u"L'ambiance de travail", blank=True)
@ -290,14 +319,19 @@ class AvisStage(models.Model):
les_moins = models.TextField(u"Les moins de cette expérience", blank=True) les_moins = models.TextField(u"Les moins de cette expérience", blank=True)
def __str__(self): def __str__(self):
return u"Avis sur {%s} par %s" % (self.stage.sujet, self.stage.auteur.user.username) return u"Avis sur {%s} par %s" % (
self.stage.sujet,
self.stage.auteur.user.username,
)
# Liste des champs d'avis, couplés à leur nom (pour l'affichage) # Liste des champs d'avis, couplés à leur nom (pour l'affichage)
@property @property
def avis_all(self): def avis_all(self):
fields = ['avis_sujet', 'avis_ambiance', 'avis_admin', 'avis_prestage'] fields = ["avis_sujet", "avis_ambiance", "avis_admin", "avis_prestage"]
return [(AvisStage._meta.get_field(field).verbose_name, return [
getattr(self, field, '')) for field in fields] (AvisStage._meta.get_field(field).verbose_name, getattr(self, field, ""))
for field in fields
]
class AvisLieu(models.Model): class AvisLieu(models.Model):
@ -307,8 +341,7 @@ class AvisLieu(models.Model):
chapo = models.TextField(u"En quelques mots", blank=True) chapo = models.TextField(u"En quelques mots", blank=True)
avis_lieustage = RichTextField(u"Les lieux de travail", blank=True) avis_lieustage = RichTextField(u"Les lieux de travail", blank=True)
avis_pratique = RichTextField(u"S'installer - conseils pratiques", avis_pratique = RichTextField(u"S'installer - conseils pratiques", blank=True)
blank=True)
avis_tourisme = RichTextField(u"Dans les parages", blank=True) avis_tourisme = RichTextField(u"Dans les parages", blank=True)
les_plus = models.TextField(u"Les plus du lieu", blank=True) les_plus = models.TextField(u"Les plus du lieu", blank=True)
@ -324,6 +357,8 @@ class AvisLieu(models.Model):
# Liste des champs d'avis, couplés à leur nom (pour l'affichage) # Liste des champs d'avis, couplés à leur nom (pour l'affichage)
@property @property
def avis_all(self): def avis_all(self):
fields = ['avis_lieustage', 'avis_pratique', 'avis_tourisme'] fields = ["avis_lieustage", "avis_pratique", "avis_tourisme"]
return [(AvisLieu._meta.get_field(field).verbose_name, return [
getattr(self, field, '')) for field in fields] (AvisLieu._meta.get_field(field).verbose_name, getattr(self, field, ""))
for field in fields
]

View file

@ -1,67 +1,76 @@
# coding: utf-8 # coding: utf-8
DEPARTEMENTS_DEFAUT = ( DEPARTEMENTS_DEFAUT = (
('phy', u'Physique'), ("phy", u"Physique"),
('maths', u'Maths'), ("maths", u"Maths"),
('bio', u'Biologie'), ("bio", u"Biologie"),
('chimie', u'Chimie'), ("chimie", u"Chimie"),
('geol', u'Géosciences'), ("geol", u"Géosciences"),
('dec', u'DEC'), ("dec", u"DEC"),
('info', u'Informatique'), ("info", u"Informatique"),
('litt', u'Littéraire'), ("litt", u"Littéraire"),
('guests', u'Pensionnaires étrangers'), ("guests", u"Pensionnaires étrangers"),
('pei', u'PEI'), ("pei", u"PEI"),
) )
TYPE_STAGE_OPTIONS = ( TYPE_STAGE_OPTIONS = (
(u'Recherche :', ( (
('recherche', u"Stage académique"), u"Recherche :",
('recherche_autre', u"Stage non-académique"), (
('sejour_dri', u"Séjour de recherche DRI"), ("recherche", u"Stage académique"),
)), ("recherche_autre", u"Stage non-académique"),
(u'Stage sans visée de recherche :', ( ("sejour_dri", u"Séjour de recherche DRI"),
('pro', u"Stage en entreprise"), ),
('admin', u"Stage en admin./ONG/orga. internationale"), ),
)), (
(u'Enseignement :', ( u"Stage sans visée de recherche :",
('lectorat', u"Lectorat DRI"), (
('autre_teach', u"Autre expérience d'enseignement"), ("pro", u"Stage en entreprise"),
)), ("admin", u"Stage en admin./ONG/orga. internationale"),
('autre', u"Autre"), ),
),
(
u"Enseignement :",
(
("lectorat", u"Lectorat DRI"),
("autre_teach", u"Autre expérience d'enseignement"),
),
),
("autre", u"Autre"),
) )
# Dictionnaire des type de stage (et de leur genre, True=féminin) # Dictionnaire des type de stage (et de leur genre, True=féminin)
TYPE_STAGE_DICT = { TYPE_STAGE_DICT = {
'recherche': (u"stage de recherche académique", False), "recherche": (u"stage de recherche académique", False),
'recherche_autre': (u"stage de recherche non-académique", False), "recherche_autre": (u"stage de recherche non-académique", False),
'sejour_dri': (u"séjour de recherche DRI", False), "sejour_dri": (u"séjour de recherche DRI", False),
'pro': (u"stage en entreprise sans visée de recherche", False), "pro": (u"stage en entreprise sans visée de recherche", False),
'admin': (u"stage en administration, ONG ou organisation internationale", False), "admin": (u"stage en administration, ONG ou organisation internationale", False),
'lectorat': (u"lectorat DRI", False), "lectorat": (u"lectorat DRI", False),
'autre_teach': (u"expérience de recherche", True), "autre_teach": (u"expérience de recherche", True),
'autre': (u"expérience", True), "autre": (u"expérience", True),
} }
TYPE_LIEU_OPTIONS = ( TYPE_LIEU_OPTIONS = (
('universite', u"Université"), ("universite", u"Université"),
('entreprise', u"Entreprise"), ("entreprise", u"Entreprise"),
('centrerecherche', u"Centre de recherche"), ("centrerecherche", u"Centre de recherche"),
('administration', u"Administration"), ("administration", u"Administration"),
('autre', u"Autre"), ("autre", u"Autre"),
) )
# Place du stage dans le cursus # Place du stage dans le cursus
NIVEAU_SCOL_OPTIONS = ( NIVEAU_SCOL_OPTIONS = (
('L3', u"Licence 3"), ("L3", u"Licence 3"),
('M1', u"Master 1"), ("M1", u"Master 1"),
('M2', u"Master 2"), ("M2", u"Master 2"),
('DOC', u"Pré-doctorat"), ("DOC", u"Pré-doctorat"),
('CST', u"Césure"), ("CST", u"Césure"),
('BLA', u"Année blanche"), ("BLA", u"Année blanche"),
('VAC', u"Vacances scolaires"), ("VAC", u"Vacances scolaires"),
('MIT', u"Mi-temps en parallèle des études"), ("MIT", u"Mi-temps en parallèle des études"),
('', u"Autre"), ("", u"Autre"),
) )
NIVEAU_SCOL_DICT = { NIVEAU_SCOL_DICT = {
@ -74,257 +83,257 @@ NIVEAU_SCOL_DICT = {
"VAC": u"pendant des vacances scolaires", "VAC": u"pendant des vacances scolaires",
"MIT": u"à mi-temps en parallèle des études", "MIT": u"à mi-temps en parallèle des études",
} }
# Dictionnaire des noms de lieux (et de leur genre, True=féminin) # Dictionnaire des noms de lieux (et de leur genre, True=féminin)
TYPE_LIEU_DICT = { TYPE_LIEU_DICT = {
'universite': (u"université", True), "universite": (u"université", True),
'entreprise': (u"entreprise", True), "entreprise": (u"entreprise", True),
'centrerecherche': (u"centre de recherche", False), "centrerecherche": (u"centre de recherche", False),
'administration': (u"administration", True), "administration": (u"administration", True),
'autre': (u"lieu", False), "autre": (u"lieu", False),
} }
PAYS_OPTIONS = ( PAYS_OPTIONS = (
("AF", u"Afghanistan"), ("AF", u"Afghanistan"),
("AL", u"Albanie"), ("AL", u"Albanie"),
("AQ", u"Antarctique"), ("AQ", u"Antarctique"),
("DZ", u"Algérie"), ("DZ", u"Algérie"),
("AS", u"Samoa Américaines"), ("AS", u"Samoa Américaines"),
("AD", u"Andorre"), ("AD", u"Andorre"),
("AO", u"Angola"), ("AO", u"Angola"),
("AG", u"Antigua-et-Barbuda"), ("AG", u"Antigua-et-Barbuda"),
("AZ", u"Azerbaïdjan"), ("AZ", u"Azerbaïdjan"),
("AR", u"Argentine"), ("AR", u"Argentine"),
("AU", u"Australie"), ("AU", u"Australie"),
("AT", u"Autriche"), ("AT", u"Autriche"),
("BS", u"Bahamas"), ("BS", u"Bahamas"),
("BH", u"Bahreïn"), ("BH", u"Bahreïn"),
("BD", u"Bangladesh"), ("BD", u"Bangladesh"),
("AM", u"Arménie"), ("AM", u"Arménie"),
("BB", u"Barbade"), ("BB", u"Barbade"),
("BE", u"Belgique"), ("BE", u"Belgique"),
("BM", u"Bermudes"), ("BM", u"Bermudes"),
("BT", u"Bhoutan"), ("BT", u"Bhoutan"),
("BO", u"Bolivie"), ("BO", u"Bolivie"),
("BA", u"Bosnie-Herzégovine"), ("BA", u"Bosnie-Herzégovine"),
("BW", u"Botswana"), ("BW", u"Botswana"),
("BV", u"Île Bouvet"), ("BV", u"Île Bouvet"),
("BR", u"Brésil"), ("BR", u"Brésil"),
("BZ", u"Belize"), ("BZ", u"Belize"),
("IO", u"Territoire Britannique de l'Océan Indien"), ("IO", u"Territoire Britannique de l'Océan Indien"),
("SB", u"Îles Salomon"), ("SB", u"Îles Salomon"),
("VG", u"Îles Vierges Britanniques"), ("VG", u"Îles Vierges Britanniques"),
("BN", u"Brunéi Darussalam"), ("BN", u"Brunéi Darussalam"),
("BG", u"Bulgarie"), ("BG", u"Bulgarie"),
("MM", u"Myanmar"), ("MM", u"Myanmar"),
("BI", u"Burundi"), ("BI", u"Burundi"),
("BY", u"Bélarus"), ("BY", u"Bélarus"),
("KH", u"Cambodge"), ("KH", u"Cambodge"),
("CM", u"Cameroun"), ("CM", u"Cameroun"),
("CA", u"Canada"), ("CA", u"Canada"),
("CV", u"Cap-vert"), ("CV", u"Cap-vert"),
("KY", u"Îles Caïmanes"), ("KY", u"Îles Caïmanes"),
("CF", u"République Centrafricaine"), ("CF", u"République Centrafricaine"),
("LK", u"Sri Lanka"), ("LK", u"Sri Lanka"),
("TD", u"Tchad"), ("TD", u"Tchad"),
("CL", u"Chili"), ("CL", u"Chili"),
("CN", u"Chine"), ("CN", u"Chine"),
("TW", u"Taïwan"), ("TW", u"Taïwan"),
("CX", u"Île Christmas"), ("CX", u"Île Christmas"),
("CC", u"Îles Cocos (Keeling)"), ("CC", u"Îles Cocos (Keeling)"),
("CO", u"Colombie"), ("CO", u"Colombie"),
("KM", u"Comores"), ("KM", u"Comores"),
("YT", u"Mayotte"), ("YT", u"Mayotte"),
("CG", u"République du Congo"), ("CG", u"République du Congo"),
("CD", u"République Démocratique du Congo"), ("CD", u"République Démocratique du Congo"),
("CK", u"Îles Cook"), ("CK", u"Îles Cook"),
("CR", u"Costa Rica"), ("CR", u"Costa Rica"),
("HR", u"Croatie"), ("HR", u"Croatie"),
("CU", u"Cuba"), ("CU", u"Cuba"),
("CY", u"Chypre"), ("CY", u"Chypre"),
("CZ", u"République Tchèque"), ("CZ", u"République Tchèque"),
("BJ", u"Bénin"), ("BJ", u"Bénin"),
("DK", u"Danemark"), ("DK", u"Danemark"),
("DM", u"Dominique"), ("DM", u"Dominique"),
("DO", u"République Dominicaine"), ("DO", u"République Dominicaine"),
("EC", u"Équateur"), ("EC", u"Équateur"),
("SV", u"El Salvador"), ("SV", u"El Salvador"),
("GQ", u"Guinée Équatoriale"), ("GQ", u"Guinée Équatoriale"),
("ET", u"Éthiopie"), ("ET", u"Éthiopie"),
("ER", u"Érythrée"), ("ER", u"Érythrée"),
("EE", u"Estonie"), ("EE", u"Estonie"),
("FO", u"Îles Féroé"), ("FO", u"Îles Féroé"),
("FK", u"Îles (malvinas) Falkland"), ("FK", u"Îles (malvinas) Falkland"),
("GS", u"Géorgie du Sud et les Îles Sandwich du Sud"), ("GS", u"Géorgie du Sud et les Îles Sandwich du Sud"),
("FJ", u"Fidji"), ("FJ", u"Fidji"),
("FI", u"Finlande"), ("FI", u"Finlande"),
("AX", u"Îles Åland"), ("AX", u"Îles Åland"),
("FR", u"France"), ("FR", u"France"),
("GF", u"Guyane Française"), ("GF", u"Guyane Française"),
("PF", u"Polynésie Française"), ("PF", u"Polynésie Française"),
("TF", u"Terres Australes Françaises"), ("TF", u"Terres Australes Françaises"),
("DJ", u"Djibouti"), ("DJ", u"Djibouti"),
("GA", u"Gabon"), ("GA", u"Gabon"),
("GE", u"Géorgie"), ("GE", u"Géorgie"),
("GM", u"Gambie"), ("GM", u"Gambie"),
("PS", u"Territoire Palestinien Occupé"), ("PS", u"Territoire Palestinien Occupé"),
("DE", u"Allemagne"), ("DE", u"Allemagne"),
("GH", u"Ghana"), ("GH", u"Ghana"),
("GI", u"Gibraltar"), ("GI", u"Gibraltar"),
("KI", u"Kiribati"), ("KI", u"Kiribati"),
("GR", u"Grèce"), ("GR", u"Grèce"),
("GL", u"Groenland"), ("GL", u"Groenland"),
("GD", u"Grenade"), ("GD", u"Grenade"),
("GP", u"Guadeloupe"), ("GP", u"Guadeloupe"),
("GU", u"Guam"), ("GU", u"Guam"),
("GT", u"Guatemala"), ("GT", u"Guatemala"),
("GN", u"Guinée"), ("GN", u"Guinée"),
("GY", u"Guyana"), ("GY", u"Guyana"),
("HT", u"Haïti"), ("HT", u"Haïti"),
("HM", u"Îles Heard et Mcdonald"), ("HM", u"Îles Heard et Mcdonald"),
("VA", u"Saint-Siège (état de la Cité du Vatican)"), ("VA", u"Saint-Siège (état de la Cité du Vatican)"),
("HN", u"Honduras"), ("HN", u"Honduras"),
("HK", u"Hong-Kong"), ("HK", u"Hong-Kong"),
("HU", u"Hongrie"), ("HU", u"Hongrie"),
("IS", u"Islande"), ("IS", u"Islande"),
("IN", u"Inde"), ("IN", u"Inde"),
("ID", u"Indonésie"), ("ID", u"Indonésie"),
("IR", u"République Islamique d'Iran"), ("IR", u"République Islamique d'Iran"),
("IQ", u"Iraq"), ("IQ", u"Iraq"),
("IE", u"Irlande"), ("IE", u"Irlande"),
("IL", u"Israël"), ("IL", u"Israël"),
("IT", u"Italie"), ("IT", u"Italie"),
("CI", u"Côte d'Ivoire"), ("CI", u"Côte d'Ivoire"),
("JM", u"Jamaïque"), ("JM", u"Jamaïque"),
("JP", u"Japon"), ("JP", u"Japon"),
("KZ", u"Kazakhstan"), ("KZ", u"Kazakhstan"),
("JO", u"Jordanie"), ("JO", u"Jordanie"),
("KE", u"Kenya"), ("KE", u"Kenya"),
("KP", u"République Populaire Démocratique de Corée"), ("KP", u"République Populaire Démocratique de Corée"),
("KR", u"République de Corée"), ("KR", u"République de Corée"),
("KW", u"Koweït"), ("KW", u"Koweït"),
("KG", u"Kirghizistan"), ("KG", u"Kirghizistan"),
("LA", u"République Démocratique Populaire Lao"), ("LA", u"République Démocratique Populaire Lao"),
("LB", u"Liban"), ("LB", u"Liban"),
("LS", u"Lesotho"), ("LS", u"Lesotho"),
("LV", u"Lettonie"), ("LV", u"Lettonie"),
("LR", u"Libéria"), ("LR", u"Libéria"),
("LY", u"Jamahiriya Arabe Libyenne"), ("LY", u"Jamahiriya Arabe Libyenne"),
("LI", u"Liechtenstein"), ("LI", u"Liechtenstein"),
("LT", u"Lituanie"), ("LT", u"Lituanie"),
("LU", u"Luxembourg"), ("LU", u"Luxembourg"),
("MO", u"Macao"), ("MO", u"Macao"),
("MG", u"Madagascar"), ("MG", u"Madagascar"),
("MW", u"Malawi"), ("MW", u"Malawi"),
("MY", u"Malaisie"), ("MY", u"Malaisie"),
("MV", u"Maldives"), ("MV", u"Maldives"),
("ML", u"Mali"), ("ML", u"Mali"),
("MT", u"Malte"), ("MT", u"Malte"),
("MQ", u"Martinique"), ("MQ", u"Martinique"),
("MR", u"Mauritanie"), ("MR", u"Mauritanie"),
("MU", u"Maurice"), ("MU", u"Maurice"),
("MX", u"Mexique"), ("MX", u"Mexique"),
("MC", u"Monaco"), ("MC", u"Monaco"),
("MN", u"Mongolie"), ("MN", u"Mongolie"),
("MD", u"République de Moldova"), ("MD", u"République de Moldova"),
("MS", u"Montserrat"), ("MS", u"Montserrat"),
("MA", u"Maroc"), ("MA", u"Maroc"),
("MZ", u"Mozambique"), ("MZ", u"Mozambique"),
("OM", u"Oman"), ("OM", u"Oman"),
("NA", u"Namibie"), ("NA", u"Namibie"),
("NR", u"Nauru"), ("NR", u"Nauru"),
("NP", u"Népal"), ("NP", u"Népal"),
("NL", u"Pays-Bas"), ("NL", u"Pays-Bas"),
("AN", u"Antilles Néerlandaises"), ("AN", u"Antilles Néerlandaises"),
("AW", u"Aruba"), ("AW", u"Aruba"),
("NC", u"Nouvelle-Calédonie"), ("NC", u"Nouvelle-Calédonie"),
("VU", u"Vanuatu"), ("VU", u"Vanuatu"),
("NZ", u"Nouvelle-Zélande"), ("NZ", u"Nouvelle-Zélande"),
("NI", u"Nicaragua"), ("NI", u"Nicaragua"),
("NE", u"Niger"), ("NE", u"Niger"),
("NG", u"Nigéria"), ("NG", u"Nigéria"),
("NU", u"Niué"), ("NU", u"Niué"),
("NF", u"Île Norfolk"), ("NF", u"Île Norfolk"),
("NO", u"Norvège"), ("NO", u"Norvège"),
("MP", u"Îles Mariannes du Nord"), ("MP", u"Îles Mariannes du Nord"),
("UM", u"Îles Mineures Éloignées des États-Unis"), ("UM", u"Îles Mineures Éloignées des États-Unis"),
("FM", u"États Fédérés de Micronésie"), ("FM", u"États Fédérés de Micronésie"),
("MH", u"Îles Marshall"), ("MH", u"Îles Marshall"),
("PW", u"Palaos"), ("PW", u"Palaos"),
("PK", u"Pakistan"), ("PK", u"Pakistan"),
("PA", u"Panama"), ("PA", u"Panama"),
("PG", u"Papouasie-Nouvelle-Guinée"), ("PG", u"Papouasie-Nouvelle-Guinée"),
("PY", u"Paraguay"), ("PY", u"Paraguay"),
("PE", u"Pérou"), ("PE", u"Pérou"),
("PH", u"Philippines"), ("PH", u"Philippines"),
("PN", u"Pitcairn"), ("PN", u"Pitcairn"),
("PL", u"Pologne"), ("PL", u"Pologne"),
("PT", u"Portugal"), ("PT", u"Portugal"),
("GW", u"Guinée-Bissau"), ("GW", u"Guinée-Bissau"),
("TL", u"Timor-Leste"), ("TL", u"Timor-Leste"),
("PR", u"Porto Rico"), ("PR", u"Porto Rico"),
("QA", u"Qatar"), ("QA", u"Qatar"),
("RE", u"Réunion"), ("RE", u"Réunion"),
("RO", u"Roumanie"), ("RO", u"Roumanie"),
("RU", u"Fédération de Russie"), ("RU", u"Fédération de Russie"),
("RW", u"Rwanda"), ("RW", u"Rwanda"),
("SH", u"Sainte-Hélène"), ("SH", u"Sainte-Hélène"),
("KN", u"Saint-Kitts-et-Nevis"), ("KN", u"Saint-Kitts-et-Nevis"),
("AI", u"Anguilla"), ("AI", u"Anguilla"),
("LC", u"Sainte-Lucie"), ("LC", u"Sainte-Lucie"),
("PM", u"Saint-Pierre-et-Miquelon"), ("PM", u"Saint-Pierre-et-Miquelon"),
("VC", u"Saint-Vincent-et-les Grenadines"), ("VC", u"Saint-Vincent-et-les Grenadines"),
("SM", u"Saint-Marin"), ("SM", u"Saint-Marin"),
("ST", u"Sao Tomé-et-Principe"), ("ST", u"Sao Tomé-et-Principe"),
("SA", u"Arabie Saoudite"), ("SA", u"Arabie Saoudite"),
("SN", u"Sénégal"), ("SN", u"Sénégal"),
("SC", u"Seychelles"), ("SC", u"Seychelles"),
("SL", u"Sierra Leone"), ("SL", u"Sierra Leone"),
("SG", u"Singapour"), ("SG", u"Singapour"),
("SK", u"Slovaquie"), ("SK", u"Slovaquie"),
("VN", u"Viet Nam"), ("VN", u"Viet Nam"),
("SI", u"Slovénie"), ("SI", u"Slovénie"),
("SO", u"Somalie"), ("SO", u"Somalie"),
("ZA", u"Afrique du Sud"), ("ZA", u"Afrique du Sud"),
("ZW", u"Zimbabwe"), ("ZW", u"Zimbabwe"),
("ES", u"Espagne"), ("ES", u"Espagne"),
("EH", u"Sahara Occidental"), ("EH", u"Sahara Occidental"),
("SD", u"Soudan"), ("SD", u"Soudan"),
("SR", u"Suriname"), ("SR", u"Suriname"),
("SJ", u"Svalbard etÎle Jan Mayen"), ("SJ", u"Svalbard etÎle Jan Mayen"),
("SZ", u"Swaziland"), ("SZ", u"Swaziland"),
("SE", u"Suède"), ("SE", u"Suède"),
("CH", u"Suisse"), ("CH", u"Suisse"),
("SY", u"République Arabe Syrienne"), ("SY", u"République Arabe Syrienne"),
("TJ", u"Tadjikistan"), ("TJ", u"Tadjikistan"),
("TH", u"Thaïlande"), ("TH", u"Thaïlande"),
("TG", u"Togo"), ("TG", u"Togo"),
("TK", u"Tokelau"), ("TK", u"Tokelau"),
("TO", u"Tonga"), ("TO", u"Tonga"),
("TT", u"Trinité-et-Tobago"), ("TT", u"Trinité-et-Tobago"),
("AE", u"Émirats Arabes Unis"), ("AE", u"Émirats Arabes Unis"),
("TN", u"Tunisie"), ("TN", u"Tunisie"),
("TR", u"Turquie"), ("TR", u"Turquie"),
("TM", u"Turkménistan"), ("TM", u"Turkménistan"),
("TC", u"Îles Turks et Caïques"), ("TC", u"Îles Turks et Caïques"),
("TV", u"Tuvalu"), ("TV", u"Tuvalu"),
("UG", u"Ouganda"), ("UG", u"Ouganda"),
("UA", u"Ukraine"), ("UA", u"Ukraine"),
("MK", u"L'ex-République Yougoslave de Macédoine"), ("MK", u"L'ex-République Yougoslave de Macédoine"),
("EG", u"Égypte"), ("EG", u"Égypte"),
("GB", u"Royaume-Uni"), ("GB", u"Royaume-Uni"),
("IM", u"Île de Man"), ("IM", u"Île de Man"),
("TZ", u"République-Unie de Tanzanie"), ("TZ", u"République-Unie de Tanzanie"),
("US", u"États-Unis"), ("US", u"États-Unis"),
("VI", u"Îles Vierges des États-Unis"), ("VI", u"Îles Vierges des États-Unis"),
("BF", u"Burkina Faso"), ("BF", u"Burkina Faso"),
("UY", u"Uruguay"), ("UY", u"Uruguay"),
("UZ", u"Ouzbékistan"), ("UZ", u"Ouzbékistan"),
("VE", u"Venezuela"), ("VE", u"Venezuela"),
("WF", u"Wallis et Futuna"), ("WF", u"Wallis et Futuna"),
("WS", u"Samoa"), ("WS", u"Samoa"),
("YE", u"Yémen"), ("YE", u"Yémen"),
("CS", u"Serbie-et-Monténégro"), ("CS", u"Serbie-et-Monténégro"),
("ZM", u"Zambie"), ("ZM", u"Zambie"),
) )

View file

@ -6,23 +6,27 @@ import re
register = template.Library() register = template.Library()
@register.inclusion_tag('avisstage/templatetags/widget_lieu.html')
@register.inclusion_tag("avisstage/templatetags/widget_lieu.html")
def lieu_widget(): def lieu_widget():
form = LieuForm() form = LieuForm()
return {"form": form} return {"form": form}
@register.inclusion_tag('avisstage/templatetags/widget_feedback.html')
@register.inclusion_tag("avisstage/templatetags/widget_feedback.html")
def feedback_widget(): def feedback_widget():
form = FeedbackForm() form = FeedbackForm()
return {"form": form} return {"form": form}
@register.filter @register.filter
def typonazisme(value): 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)\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)", u"\\1 \\2", value)
return value return value
@register.filter @register.filter
def avis_len(value): def avis_len(value):
if value < 5: if value < 5:
@ -32,6 +36,7 @@ def avis_len(value):
else: else:
return "long" return "long"
@register.simple_tag @register.simple_tag
def url_replace(request, field, value): def url_replace(request, field, value):
dict_ = request.GET.copy() dict_ = request.GET.copy()

View file

@ -12,14 +12,15 @@ from unittest import mock
from .models import User, Normalien, Lieu, Stage, StageMatiere, AvisLieu from .models import User, Normalien, Lieu, Stage, StageMatiere, AvisLieu
class ExperiENSTestCase(TestCase): class ExperiENSTestCase(TestCase):
# Dummy database # Dummy database
def setUp(self): def setUp(self):
self.u_conscrit = User.objects.create_user('conscrit', self.u_conscrit = User.objects.create_user(
'conscrit@ens.fr', "conscrit", "conscrit@ens.fr", "conscrit"
'conscrit') )
self.p_conscrit = self.u_conscrit.profil self.p_conscrit = self.u_conscrit.profil
self.p_conscrit.nom = "Petit conscrit" self.p_conscrit.nom = "Petit conscrit"
self.p_conscrit.promotion = "Serpentard 2020" self.p_conscrit.promotion = "Serpentard 2020"
@ -32,281 +33,313 @@ class ExperiENSTestCase(TestCase):
entrance_year=2020, entrance_year=2020,
) )
self.sa_conscrit.save() self.sa_conscrit.save()
self.u_archi = User.objects.create_user('archicube',
'archicube@ens.fr',
'archicube')
self.p_archi = self.u_archi.profil
self.p_archi.nom="Vieil archicube"
self.p_archi.promotion="Gryffondor 2014"
self.p_archi.bio="Je suis un vieil archicube"
self.lieu1 = Lieu(nom="Beaux-Bâtons", type_lieu="universite", self.u_archi = User.objects.create_user(
ville="Brocéliande", pays="FR", "archicube", "archicube@ens.fr", "archicube"
coord="POINT(-1.63971 48.116382)") )
self.p_archi = self.u_archi.profil
self.p_archi.nom = "Vieil archicube"
self.p_archi.promotion = "Gryffondor 2014"
self.p_archi.bio = "Je suis un vieil archicube"
self.lieu1 = Lieu(
nom="Beaux-Bâtons",
type_lieu="universite",
ville="Brocéliande",
pays="FR",
coord="POINT(-1.63971 48.116382)",
)
self.lieu1.save() self.lieu1.save()
self.lieu2 = Lieu(nom="Durmstrang", type_lieu="universite", self.lieu2 = Lieu(
ville="Edimbourg", pays="GB", nom="Durmstrang",
coord="POINT(56.32153 -1.259715)") type_lieu="universite",
ville="Edimbourg",
pays="GB",
coord="POINT(56.32153 -1.259715)",
)
self.lieu2.save() self.lieu2.save()
self.matiere1 = StageMatiere(nom="Arithmancie", slug="arithmancie") self.matiere1 = StageMatiere(nom="Arithmancie", slug="arithmancie")
self.matiere1.save() self.matiere1.save()
self.matiere2 = StageMatiere(nom="Sortilège", slug="sortilege") self.matiere2 = StageMatiere(nom="Sortilège", slug="sortilege")
self.matiere2.save() self.matiere2.save()
self.cstage1 = Stage(auteur=self.p_conscrit, sujet="Wingardium Leviosa", self.cstage1 = Stage(
date_debut=date(2020, 5, 10), auteur=self.p_conscrit,
date_fin=date(2020, 8, 26), sujet="Wingardium Leviosa",
type_stage="recherche", date_debut=date(2020, 5, 10),
niveau_scol="M1", public=True) date_fin=date(2020, 8, 26),
type_stage="recherche",
niveau_scol="M1",
public=True,
)
self.cstage1.save() self.cstage1.save()
self.cstage1.matieres.add(self.matiere1) self.cstage1.matieres.add(self.matiere1)
alieu1 = AvisLieu(stage=self.cstage1, lieu=self.lieu1, alieu1 = AvisLieu(stage=self.cstage1, lieu=self.lieu1, chapo="Trop bien")
chapo="Trop bien")
alieu1.save() alieu1.save()
self.cstage2 = Stage(auteur=self.p_conscrit, sujet="Avada Kedavra", self.cstage2 = Stage(
date_debut=date(2021, 5, 10), auteur=self.p_conscrit,
date_fin=date(2021, 8, 26), sujet="Avada Kedavra",
type_stage="sejour_dri", date_debut=date(2021, 5, 10),
niveau_scol="M2", public=False) date_fin=date(2021, 8, 26),
type_stage="sejour_dri",
niveau_scol="M2",
public=False,
)
self.cstage2.save() self.cstage2.save()
self.cstage2.matieres.add(self.matiere2) self.cstage2.matieres.add(self.matiere2)
alieu2 = AvisLieu(stage=self.cstage2, lieu=self.lieu2, alieu2 = AvisLieu(stage=self.cstage2, lieu=self.lieu2, chapo="Trop nul")
chapo="Trop nul")
alieu2.save() alieu2.save()
self.astage1 = Stage(
self.astage1 = Stage(auteur=self.p_archi, sujet="Alohomora", auteur=self.p_archi,
date_debut=date(2014, 5, 10), sujet="Alohomora",
date_fin=date(2014, 8, 26), date_debut=date(2014, 5, 10),
type_stage="recherche", date_fin=date(2014, 8, 26),
niveau_scol="M2", public=True) type_stage="recherche",
niveau_scol="M2",
public=True,
)
self.astage1.save() self.astage1.save()
self.astage1.matieres.add(self.matiere2) self.astage1.matieres.add(self.matiere2)
alieu3 = AvisLieu(stage=self.astage1, lieu=self.lieu1, alieu3 = AvisLieu(stage=self.astage1, lieu=self.lieu1, chapo="Trop moyen")
chapo="Trop moyen")
alieu3.save() alieu3.save()
def assertRedirectToLogin(self, testurl): def assertRedirectToLogin(self, testurl):
r = self.client.get(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): def assertPageNotFound(self, testurl):
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 404) self.assertEqual(r.status_code, 404)
""" """
ACCÈS PUBLIC ACCÈS PUBLIC
""" """
class PublicViewsTest(ExperiENSTestCase): class PublicViewsTest(ExperiENSTestCase):
""" """
Vérifie que les fiches de stages ne sont pas visibles hors connexion Vérifie que les fiches de stages ne sont pas visibles hors connexion
""" """
def test_stage_visibility_public(self):
self.assertRedirectToLogin(reverse('avisstage:stage',
kwargs={'pk':self.cstage1.id}))
self.assertRedirectToLogin(reverse('avisstage:stage',
kwargs={'pk':self.cstage2.id}))
self.assertRedirectToLogin(reverse('avisstage:stage',
kwargs={'pk':self.astage1.id}))
def test_stage_visibility_public(self):
self.assertRedirectToLogin(
reverse("avisstage:stage", kwargs={"pk": self.cstage1.id})
)
self.assertRedirectToLogin(
reverse("avisstage:stage", kwargs={"pk": self.cstage2.id})
)
self.assertRedirectToLogin(
reverse("avisstage:stage", kwargs={"pk": self.astage1.id})
)
""" """
Vérifie que les profils de normaliens ne sont pas visibles hors connexion Vérifie que les profils de normaliens ne sont pas visibles hors connexion
""" """
def test_profil_visibility_public(self): def test_profil_visibility_public(self):
self.assertRedirectToLogin(reverse( self.assertRedirectToLogin(
'avisstage:profil', kwargs={'username': self.u_conscrit.username})) reverse("avisstage:profil", kwargs={"username": self.u_conscrit.username})
)
self.assertRedirectToLogin(reverse( self.assertRedirectToLogin(
'avisstage:profil', kwargs={'username': self.u_archi.username})) reverse("avisstage:profil", kwargs={"username": self.u_archi.username})
)
""" """
Vérifie que la recherche n'est pas accessible hors connexion Vérifie que la recherche n'est pas accessible hors connexion
""" """
def test_pages_visibility_public(self): 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 Vérifie que l'API n'est pas accessible hors connexion
""" """
def test_api_visibility_public(self): def test_api_visibility_public(self):
testurl = reverse('avisstage:api_dispatch_list', testurl = reverse(
kwargs={"resource_name": "lieu", "avisstage:api_dispatch_list",
"api_name": "v1"}) kwargs={"resource_name": "lieu", "api_name": "v1"},
r = self.client.get(testurl) )
self.assertEqual(r.status_code, 401) r = self.client.get(testurl)
self.assertEqual(r.status_code, 401)
testurl = reverse('avisstage:api_dispatch_list',
kwargs={"resource_name": "stage", testurl = reverse(
"api_name": "v1"}) "avisstage:api_dispatch_list",
r = self.client.get(testurl) kwargs={"resource_name": "stage", "api_name": "v1"},
self.assertEqual(r.status_code, 401) )
r = self.client.get(testurl)
testurl = reverse('avisstage:api_dispatch_list', self.assertEqual(r.status_code, 401)
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) r = self.client.get(testurl)
self.assertEqual(r.status_code, 401) self.assertEqual(r.status_code, 401)
""" """
Vérifie que les pages d'édition ne sont pas accessible hors connexion Vérifie que les pages d'édition ne sont pas accessible hors connexion
""" """
def test_edit_visibility_public(self): def test_edit_visibility_public(self):
self.assertRedirectToLogin(reverse( self.assertRedirectToLogin(
'avisstage:stage_edit', kwargs={'pk':self.cstage1.id})) reverse("avisstage:stage_edit", kwargs={"pk": self.cstage1.id})
)
self.assertRedirectToLogin(reverse(
'avisstage:stage_edit', kwargs={'pk':self.astage1.id}))
self.assertRedirectToLogin(reverse( self.assertRedirectToLogin(
'avisstage:stage_publication', kwargs={'pk':self.cstage1.id})) reverse("avisstage:stage_edit", kwargs={"pk": self.astage1.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_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 ACCÈS ARCHICUBE
""" """
class ArchicubeViewsTest(ExperiENSTestCase): class ArchicubeViewsTest(ExperiENSTestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
# Connexion with password # Connexion with password
self.client.login(username='archicube', password='archicube') self.client.login(username="archicube", password="archicube")
def assert403Archicubes(self, testurl): def assert403Archicubes(self, testurl):
r = self.client.get(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 Vérifie que les seules fiches de stages visibles sont les siennes
""" """
def test_stage_visibility_archi(self): def test_stage_visibility_archi(self):
self.assertPageNotFound(reverse('avisstage:stage', self.assertPageNotFound(
kwargs={'pk':self.cstage1.id})) 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) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
""" """
Vérifie que le seul profil visible est le sien 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', def test_profil_visibility_archi(self):
kwargs={'username': self.u_archi.username}) 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) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
""" """
Vérifie que la recherche n'est pas accessible Vérifie que la recherche n'est pas accessible
""" """
def test_pages_visibility_archi(self): 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:stage_items')) self.assert403Archicubes(reverse("avisstage:recherche_resultats"))
testurl = reverse('avisstage:feedback') self.assert403Archicubes(reverse("avisstage:stage_items"))
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:feedback")
r = self.client.post(
testurl, {"objet": "Contact", "message": "Ceci est un texte"}
)
self.assertRedirects(r, reverse("avisstage:index"))
testurl = reverse("avisstage:moderation")
r = self.client.get(testurl) 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 Vérifie que la seule API accessible est celle des lieux
""" """
def test_api_visibility_archi(self): def test_api_visibility_archi(self):
testurl = reverse('avisstage:api_dispatch_list', testurl = reverse(
kwargs={"resource_name": "lieu", "avisstage:api_dispatch_list",
"api_name": "v1"}) kwargs={"resource_name": "lieu", "api_name": "v1"},
)
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
testurl = reverse('avisstage:api_dispatch_list', testurl = reverse(
kwargs={"resource_name": "stage", "avisstage:api_dispatch_list",
"api_name": "v1"}) kwargs={"resource_name": "stage", "api_name": "v1"},
r = self.client.get(testurl) )
self.assertEqual(r.status_code, 401) r = self.client.get(testurl)
self.assertEqual(r.status_code, 401)
testurl = reverse('avisstage:api_dispatch_list',
kwargs={"resource_name": "profil", testurl = reverse(
"api_name": "v1"}) "avisstage:api_dispatch_list",
kwargs={"resource_name": "profil", "api_name": "v1"},
)
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 401) self.assertEqual(r.status_code, 401)
""" """
Vérifie que le seul stage modifiable est le sien Vérifie que le seul stage modifiable est le sien
""" """
def test_edit_visibility_archi(self): 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) r = self.client.get(testurl)
self.assertEqual(r.status_code, 403) 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) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
testurl = reverse('avisstage:stage_publication', testurl = reverse("avisstage:stage_publication", kwargs={"pk": self.cstage1.id})
kwargs={'pk':self.cstage1.id})
r = self.client.post(testurl, {"publier": True}) r = self.client.post(testurl, {"publier": True})
self.assertEqual(r.status_code, 403) self.assertEqual(r.status_code, 403)
testurl = reverse('avisstage:stage_publication', testurl = reverse("avisstage:stage_publication", kwargs={"pk": self.astage1.id})
kwargs={'pk':self.astage1.id})
r = self.client.post(testurl, {"publier": True}) r = self.client.post(testurl, {"publier": True})
self.assertRedirects(r, reverse('avisstage:stage', self.assertRedirects(
kwargs={"pk": self.astage1.id})) r, reverse("avisstage:stage", kwargs={"pk": self.astage1.id})
)
testurl = reverse('avisstage:stage_ajout')
testurl = reverse("avisstage:stage_ajout")
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
testurl = reverse('avisstage:profil_edit') testurl = reverse("avisstage:profil_edit")
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
@ -320,7 +353,7 @@ class DeprecatedArchicubeViewsTest(ArchicubeViewsTest):
fake_cas_client = FakeCASClient(cas_login="archicube", entrance_year=2012) fake_cas_client = FakeCASClient(cas_login="archicube", entrance_year=2012)
mock_cas_client.return_value = fake_cas_client mock_cas_client.return_value = fake_cas_client
self.sa_archi = OldCASAccount( self.sa_archi = OldCASAccount(
user=self.u_archi, user=self.u_archi,
cas_login="archicube", cas_login="archicube",
@ -337,14 +370,14 @@ class DeprecatedArchicubeViewsTest(ArchicubeViewsTest):
self.p_archi.save() self.p_archi.save()
# New connexion with password # New connexion with password
self.client.login(username='archicube', password='archicube') self.client.login(username="archicube", password="archicube")
""" """
ACCÈS EN SCOLARITE ACCÈS EN SCOLARITE
""" """
class ScolariteViewsTest(ExperiENSTestCase): class ScolariteViewsTest(ExperiENSTestCase):
@mock.patch("authens.backends.get_cas_client") @mock.patch("authens.backends.get_cas_client")
def setUp(self, mock_cas_client): def setUp(self, mock_cas_client):
@ -352,16 +385,14 @@ class ScolariteViewsTest(ExperiENSTestCase):
fake_cas_client = FakeCASClient(cas_login="vieuxcon", entrance_year=2017) fake_cas_client = FakeCASClient(cas_login="vieuxcon", entrance_year=2017)
mock_cas_client.return_value = fake_cas_client mock_cas_client.return_value = fake_cas_client
self.u_vieuxcon = User.objects.create_user( self.u_vieuxcon = User.objects.create_user(
'vieuxcon', "vieuxcon", "vieuxcon@ens.fr", "vieuxcon"
'vieuxcon@ens.fr',
'vieuxcon'
) )
self.p_vieuxcon = self.u_vieuxcon.profil self.p_vieuxcon = self.u_vieuxcon.profil
self.p_vieuxcon.nom="Vieux con" self.p_vieuxcon.nom = "Vieux con"
self.p_vieuxcon.promotion="Poufsouffle 2017" self.p_vieuxcon.promotion = "Poufsouffle 2017"
self.p_vieuxcon.bio="Je suis un vieux con encore en scolarité" self.p_vieuxcon.bio = "Je suis un vieux con encore en scolarité"
self.p_vieuxcon.save() self.p_vieuxcon.save()
self.sa_vieuxcon = CASAccount( self.sa_vieuxcon = CASAccount(
@ -370,16 +401,19 @@ class ScolariteViewsTest(ExperiENSTestCase):
entrance_year=2017, entrance_year=2017,
) )
self.sa_vieuxcon.save() self.sa_vieuxcon.save()
self.vstage1 = Stage(auteur=self.p_vieuxcon, sujet="Oubliettes", self.vstage1 = Stage(
date_debut=date(2018, 5, 10), auteur=self.p_vieuxcon,
date_fin=date(2018, 8, 26), sujet="Oubliettes",
type_stage="recherche", date_debut=date(2018, 5, 10),
niveau_scol="M1", public=False) date_fin=date(2018, 8, 26),
type_stage="recherche",
niveau_scol="M1",
public=False,
)
self.vstage1.save() self.vstage1.save()
self.vstage1.matieres.add(self.matiere2) self.vstage1.matieres.add(self.matiere2)
alieu1 = AvisLieu(stage=self.vstage1, lieu=self.lieu2, alieu1 = AvisLieu(stage=self.vstage1, lieu=self.lieu2, chapo="Pas si mal")
chapo="Pas si mal")
alieu1.save() alieu1.save()
# Connexion through CAS # Connexion through CAS
@ -389,134 +423,143 @@ class ScolariteViewsTest(ExperiENSTestCase):
Vérifie que les seules fiches de stages visibles sont les siennes ou celles Vérifie que les seules fiches de stages visibles sont les siennes ou celles
publiques publiques
""" """
def test_stage_visibility_scolarite(self): def test_stage_visibility_scolarite(self):
testurl = reverse('avisstage:stage', testurl = reverse("avisstage:stage", kwargs={"pk": self.cstage1.id})
kwargs={'pk':self.cstage1.id})
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertPageNotFound(reverse('avisstage:stage', self.assertPageNotFound(
kwargs={'pk':self.cstage2.id})) 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) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
""" """
Vérifie que tous les profils sont visibles Vérifie que tous les profils sont visibles
""" """
def test_profil_visibility_scolarite(self): def test_profil_visibility_scolarite(self):
testurl = reverse('avisstage:profil', testurl = reverse(
kwargs={'username': self.u_conscrit.username}) "avisstage:profil", kwargs={"username": self.u_conscrit.username}
)
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertContains(r, "Wingardium Leviosa") # Public self.assertContains(r, "Wingardium Leviosa") # Public
self.assertNotContains(r, "Avada Kedavra") # Brouillon self.assertNotContains(r, "Avada Kedavra") # Brouillon
testurl = reverse('avisstage:profil', testurl = reverse(
kwargs={'username': self.u_archi.username}) "avisstage:profil", kwargs={"username": self.u_archi.username}
)
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
testurl = reverse('avisstage:profil', testurl = reverse(
kwargs={'username': self.u_vieuxcon.username}) "avisstage:profil", kwargs={"username": self.u_vieuxcon.username}
)
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
""" """
Vérifie que la recherche et les autres pages sont accessibles Vérifie que la recherche et les autres pages sont accessibles
""" """
def test_pages_visibility_scolarite(self): def test_pages_visibility_scolarite(self):
testurl = reverse('avisstage:recherche') testurl = reverse("avisstage:recherche")
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
testurl = reverse('avisstage:recherche_resultats') testurl = reverse("avisstage:recherche_resultats")
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertContains(r, "Wingardium Leviosa") # Public self.assertContains(r, "Wingardium Leviosa") # Public
self.assertNotContains(r, "Avada Kedavra") # Brouillon self.assertNotContains(r, "Avada Kedavra") # Brouillon
testurl = reverse('avisstage:stage_items') + "?ids=" \ testurl = (
+ ";".join(("%d" % k.id) for k in [self.cstage1, reverse("avisstage:stage_items")
self.cstage2, + "?ids="
self.astage1]) + ";".join(
("%d" % k.id) for k in [self.cstage1, self.cstage2, self.astage1]
)
)
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertContains(r, "Wingardium Leviosa") # Public self.assertContains(r, "Wingardium Leviosa") # Public
self.assertNotContains(r, "Avada Kedavra") # Brouillon self.assertNotContains(r, "Avada Kedavra") # Brouillon
testurl = reverse('avisstage:feedback') testurl = reverse("avisstage:feedback")
r = self.client.post(testurl, {"objet": "Contact", r = self.client.post(
"message": "Ceci est un texte"}) testurl, {"objet": "Contact", "message": "Ceci est un texte"}
self.assertRedirects(r, reverse('avisstage:index')) )
self.assertRedirects(r, reverse("avisstage:index"))
testurl = reverse('avisstage:moderation') testurl = reverse("avisstage:moderation")
r = self.client.get(testurl) 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 Vérifie que toutes les API sont accessibles et qu'elles ne montrent que les
stages publics stages publics
""" """
def test_api_visibility_scolarite(self): def test_api_visibility_scolarite(self):
testurl = reverse('avisstage:api_dispatch_list', testurl = reverse(
kwargs={"resource_name": "lieu", "avisstage:api_dispatch_list",
"api_name": "v1"}) kwargs={"resource_name": "lieu", "api_name": "v1"},
r = self.client.get(testurl) )
self.assertEqual(r.status_code, 200) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
testurl = reverse('avisstage:api_dispatch_list',
kwargs={"resource_name": "stage", testurl = reverse(
"api_name": "v1"}) "avisstage:api_dispatch_list",
r = self.client.get(testurl) kwargs={"resource_name": "stage", "api_name": "v1"},
self.assertEqual(r.status_code, 200) )
self.assertContains(r, "Wingardium Leviosa") # Public r = self.client.get(testurl)
self.assertNotContains(r, "Avada Kedavra") # Brouillon self.assertEqual(r.status_code, 200)
self.assertContains(r, "Wingardium Leviosa") # Public
testurl = reverse('avisstage:api_dispatch_list', self.assertNotContains(r, "Avada Kedavra") # Brouillon
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) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
""" """
Vérifie que le seul stage modifiable est le sien Vérifie que le seul stage modifiable est le sien
""" """
def test_edit_visibility_scolarite(self): 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) r = self.client.get(testurl)
self.assertEqual(r.status_code, 403) 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) r = self.client.get(testurl)
self.assertEqual(r.status_code, 403) 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) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
testurl = reverse('avisstage:stage_publication', testurl = reverse("avisstage:stage_publication", kwargs={"pk": self.cstage1.id})
kwargs={'pk':self.cstage1.id})
r = self.client.post(testurl, {"publier": True}) r = self.client.post(testurl, {"publier": True})
self.assertEqual(r.status_code, 403) self.assertEqual(r.status_code, 403)
testurl = reverse('avisstage:stage_publication', testurl = reverse("avisstage:stage_publication", kwargs={"pk": self.vstage1.id})
kwargs={'pk':self.vstage1.id})
r = self.client.post(testurl, {"publier": True}) r = self.client.post(testurl, {"publier": True})
self.assertRedirects(r, reverse('avisstage:stage', self.assertRedirects(
kwargs={"pk": self.vstage1.id})) r, reverse("avisstage:stage", kwargs={"pk": self.vstage1.id})
)
testurl = reverse('avisstage:stage_ajout')
testurl = reverse("avisstage:stage_ajout")
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
testurl = reverse('avisstage:profil_edit') testurl = reverse("avisstage:profil_edit")
r = self.client.get(testurl) r = self.client.get(testurl)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)

View file

@ -2,50 +2,62 @@ from django.urls import include, path
from . import views, api from . import views, api
from tastypie.api import Api from tastypie.api import Api
v1_api = Api(api_name='v1') v1_api = Api(api_name="v1")
v1_api.register(api.LieuResource()) v1_api.register(api.LieuResource())
v1_api.register(api.StageResource()) v1_api.register(api.StageResource())
v1_api.register(api.AuteurResource()) v1_api.register(api.AuteurResource())
app_name = "avisstage" app_name = "avisstage"
urlpatterns = [ urlpatterns = [
path('', views.index, name='index'), path("", views.index, name="index"),
path('perso/', views.perso, name='perso'), path("perso/", views.perso, name="perso"),
path('faq/', views.faq, name='faq'), path("faq/", views.faq, name="faq"),
path('stage/nouveau/', views.manage_stage, name='stage_ajout'), path("stage/nouveau/", views.manage_stage, name="stage_ajout"),
path('stage/<int:pk>/', views.StageView.as_view(), name='stage'), 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>/edit/", views.manage_stage, name="stage_edit"),
path('stage/<int:pk>/publication/', views.publier_stage, path("stage/<int:pk>/publication/", views.publier_stage, name="stage_publication"),
name='stage_publication'), path("403/archicubes/", views.archicubes_interdits, name="403-archicubes"),
path('403/archicubes/', views.archicubes_interdits, path("lieu/save/", views.save_lieu, name="lieu_ajout"),
name='403-archicubes'), path("profil/show/<str:username>/", views.ProfilView.as_view(), name="profil"),
path("profil/edit/", views.ProfilEdit.as_view(), name="profil_edit"),
path('lieu/save/', views.save_lieu, name='lieu_ajout'), path("profil/parametres/", views.MesParametres.as_view(), name="parametres"),
path('profil/show/<str:username>/', views.ProfilView.as_view(), path(
name='profil'), "profil/emails/<str:email>/aconfirmer/",
path('profil/edit/', views.ProfilEdit.as_view(), name='profil_edit'), views.AdresseAConfirmer.as_view(),
path('profil/parametres/', views.MesParametres.as_view(), name='parametres'), name="emails_aconfirmer",
path('profil/emails/<str:email>/aconfirmer/', ),
views.AdresseAConfirmer.as_view(), name="emails_aconfirmer"), path(
path('profil/emails/<str:email>/supprime/', views.SupprimeAdresse.as_view(), "profil/emails/<str:email>/supprime/",
name="emails_supprime"), views.SupprimeAdresse.as_view(),
path('profil/emails/<str:email>/reconfirme/', name="emails_supprime",
views.ReConfirmeAdresse.as_view(), ),
name="emails_reconfirme"), path(
path('profil/emails/<str:email>/principal/', "profil/emails/<str:email>/reconfirme/",
views.RendAdressePrincipale.as_view(), name="emails_principal"), views.ReConfirmeAdresse.as_view(),
path('profil/emails/confirme/<str:key>/', views.ConfirmeAdresse.as_view(), name="emails_reconfirme",
name="emails_confirme"), ),
path('profil/mdp/demande/', path(
views.EnvoieLienMotDePasse.as_view(), name="mdp_demande"), "profil/emails/<str:email>/principal/",
path('profil/mdp/<str:uidb64>/<str:token>/', views.RendAdressePrincipale.as_view(),
views.DefinirMotDePasse.as_view(), name="mdp_edit"), name="emails_principal",
),
path('recherche/', views.recherche, name='recherche'), path(
path('recherche/resultats/', views.recherche_resultats, "profil/emails/confirme/<str:key>/",
name='recherche_resultats'), views.ConfirmeAdresse.as_view(),
path('recherche/items/', views.stage_items, name='stage_items'), name="emails_confirme",
path('feedback/', views.feedback, name='feedback'), ),
path('moderation/', views.statistiques, name='moderation'), path(
path('api/', include(v1_api.urls)), "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)),
] ]

View file

@ -1,21 +1,25 @@
from functools import reduce from functools import reduce
from math import cos, radians, sqrt 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): def en_scolarite(user):
return user.profil.en_scolarite return user.profil.en_scolarite
def approximate_distance(a, b): def approximate_distance(a, b):
lat_a = radians(a.y) lat_a = radians(a.y)
lat_b = radians(b.y) lat_b = radians(b.y)
dlon = radians(b.x - a.x) dlon = radians(b.x - a.x)
dlon = dlon * cos((lat_a + lat_b)/2) dlon = dlon * cos((lat_a + lat_b) / 2)
dlat = (lat_a - lat_b) dlat = lat_a - lat_b
distance = 6371000 * sqrt(dlon*dlon + dlat*dlat) distance = 6371000 * sqrt(dlon * dlon + dlat * dlat)
return distance return distance
def is_email_ens(mail, none=False): def is_email_ens(mail, none=False):
if mail is None: if mail is None:
return none return none

View file

@ -3,8 +3,14 @@
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.views.generic import ( from django.views.generic import (
DetailView, ListView, UpdateView, CreateView, TemplateView, DeleteView, DetailView,
FormView, View ListView,
UpdateView,
CreateView,
TemplateView,
DeleteView,
FormView,
View,
) )
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django import forms from django import forms
@ -24,8 +30,13 @@ from simple_email_confirmation.models import EmailAddress
from .models import Normalien, Stage, Lieu, AvisLieu, AvisStage from .models import Normalien, Stage, Lieu, AvisLieu, AvisStage
from .forms import ( from .forms import (
StageForm, LieuForm, AvisStageForm, AvisLieuForm, FeedbackForm, AdresseEmailForm, StageForm,
ReinitMdpForm LieuForm,
AvisStageForm,
AvisLieuForm,
FeedbackForm,
AdresseEmailForm,
ReinitMdpForm,
) )
from .utils import en_scolarite from .utils import en_scolarite
@ -40,8 +51,8 @@ import random, math
# Page d'accueil # Page d'accueil
def index(request): def index(request):
num_stages = Stage.objects.filter(public=True).count() num_stages = Stage.objects.filter(public=True).count()
return render(request, 'avisstage/index.html', return render(request, "avisstage/index.html", {"num_stages": num_stages})
{"num_stages": num_stages})
# Espace personnel # Espace personnel
@login_required @login_required
@ -53,40 +64,46 @@ def perso(request):
profil, created = Normalien.objects.get_or_create(user=request.user) profil, created = Normalien.objects.get_or_create(user=request.user)
profil.save() profil.save()
return render(request, 'avisstage/perso.html') return render(request, "avisstage/perso.html")
# 403 Archicubes # 403 Archicubes
@login_required @login_required
def archicubes_interdits(request): def archicubes_interdits(request):
return render(request, 'avisstage/403-archicubes.html') return render(request, "avisstage/403-archicubes.html")
# Profil # Profil
#login_required # login_required
class ProfilView(LoginRequiredMixin, DetailView): class ProfilView(LoginRequiredMixin, DetailView):
model = Normalien model = Normalien
template_name = 'avisstage/detail/profil.html' template_name = "avisstage/detail/profil.html"
# Récupération du profil # Récupération du profil
def get_object(self): def get_object(self):
# Restriction d'accès pour les archicubes # Restriction d'accès pour les archicubes
if (en_scolarite(self.request.user) or if (
self.kwargs.get('username') == self.request.user.username): en_scolarite(self.request.user)
or self.kwargs.get("username") == self.request.user.username
):
return get_object_or_404( return get_object_or_404(
Normalien, user__username=self.kwargs.get('username')) Normalien, user__username=self.kwargs.get("username")
)
else: else:
raise Http404 raise Http404
# Stage # Stage
#login_required # login_required
class StageView(LoginRequiredMixin, DetailView): class StageView(LoginRequiredMixin, DetailView):
model = Stage model = Stage
template_name = 'avisstage/detail/stage.html' template_name = "avisstage/detail/stage.html"
# Restriction aux stages publics ou personnels # Restriction aux stages publics ou personnels
def get_queryset(self): def get_queryset(self):
filtre = Q(auteur__user_id=self.request.user.id) filtre = Q(auteur__user_id=self.request.user.id)
# Restriction d'accès pour les archicubes # Restriction d'accès pour les archicubes
if en_scolarite(self.request.user): if en_scolarite(self.request.user):
filtre |= Q(public=True) filtre |= Q(public=True)
@ -95,30 +112,33 @@ class StageView(LoginRequiredMixin, DetailView):
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*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 return context
# FAQ # FAQ
def faq(request): def faq(request):
return render(request, 'avisstage/faq.html') return render(request, "avisstage/faq.html")
# #
# EDITION # EDITION
# #
# Profil # Profil
#login_required # login_required
class ProfilEdit(LoginRequiredMixin, UpdateView): class ProfilEdit(LoginRequiredMixin, UpdateView):
model = Normalien model = Normalien
fields = ['nom', 'promotion', 'contactez_moi', 'bio'] fields = ["nom", "promotion", "contactez_moi", "bio"]
template_name = 'avisstage/formulaires/profil.html' template_name = "avisstage/formulaires/profil.html"
# Limitation à son propre profil # Limitation à son propre profil
def get_object(self): def get_object(self):
return self.request.user.profil return self.request.user.profil
def get_success_url(self): def get_success_url(self):
return reverse('avisstage:perso') return reverse("avisstage:perso")
# Stage # Stage
@login_required @login_required
@ -129,8 +149,9 @@ def manage_stage(request, pk=None):
stage = Stage(auteur=request.user.profil) stage = Stage(auteur=request.user.profil)
avis_stage = AvisStage(stage=stage) avis_stage = AvisStage(stage=stage)
c_del = False c_del = False
last_creation = Stage.objects.filter(auteur=request.user.profil)\ last_creation = Stage.objects.filter(auteur=request.user.profil).order_by(
.order_by("-date_creation")[:1] "-date_creation"
)[:1]
if len(last_creation) != 0: if len(last_creation) != 0:
last_maj = last_creation[0].date_creation last_maj = last_creation[0].date_creation
else: else:
@ -144,57 +165,71 @@ def manage_stage(request, pk=None):
# Formset pour les avis des lieux # Formset pour les avis des lieux
AvisLieuFormSet = forms.inlineformset_factory( 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": if request.method == "POST":
# Lecture des données # Lecture des données
form = StageForm(request.POST, request=request, instance=stage, prefix="stage") form = StageForm(request.POST, request=request, instance=stage, prefix="stage")
avis_stage_form = AvisStageForm(request.POST, avis_stage_form = AvisStageForm(
instance=avis_stage, prefix="avis") request.POST, instance=avis_stage, prefix="avis"
avis_lieu_formset = AvisLieuFormSet(request.POST, instance=stage, )
prefix="lieux") avis_lieu_formset = AvisLieuFormSet(
request.POST, instance=stage, prefix="lieux"
)
# Validation et enregistrement # Validation et enregistrement
if (form.is_valid() and if (
avis_stage_form.is_valid() and form.is_valid()
avis_lieu_formset.is_valid()): and avis_stage_form.is_valid()
and avis_lieu_formset.is_valid()
):
stage = form.save() stage = form.save()
avis_stage_form.instance.stage = stage avis_stage_form.instance.stage = stage
avis_stage_form.save() avis_stage_form.save()
avis_lieu_formset.save() avis_lieu_formset.save()
#print(request.POST) # print(request.POST)
if "continuer" in request.POST: if "continuer" in request.POST:
if pk is None: 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: else:
return redirect(reverse('avisstage:stage', return redirect(reverse("avisstage:stage", kwargs={"pk": stage.id}))
kwargs={'pk':stage.id}))
else: else:
form = StageForm(instance=stage, prefix="stage") form = StageForm(instance=stage, prefix="stage")
avis_stage_form = AvisStageForm(instance=avis_stage, prefix="avis") avis_stage_form = AvisStageForm(instance=avis_stage, prefix="avis")
avis_lieu_formset = AvisLieuFormSet(instance=stage, prefix="lieux") avis_lieu_formset = AvisLieuFormSet(instance=stage, prefix="lieux")
# Affichage du formulaire # Affichage du formulaire
return render(request, "avisstage/formulaires/stage.html", return render(
{'form': form, 'avis_stage_form': avis_stage_form, request,
'avis_lieu_formset': avis_lieu_formset, "avisstage/formulaires/stage.html",
'creation': pk is None, "last_maj": last_maj, {
'GOOGLE_API_KEY': settings.GOOGLE_API_KEY, "form": form,
'MAPBOX_API_KEY': settings.MAPBOX_API_KEY}) "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 # Ajout d'un lieu de stage
#login_required # login_required
# Stage # Stage
@login_required @login_required
def save_lieu(request): def save_lieu(request):
normalien = request.user.profil normalien = request.user.profil
if request.method == "POST": if request.method == "POST":
pk = request.POST.get("id", None) pk = request.POST.get("id", None)
#print(request.POST) # print(request.POST)
jitter = False jitter = False
if pk is None or pk == '': if pk is None or pk == "":
lieu = Lieu() lieu = Lieu()
else: else:
# Modification du lieu # Modification du lieu
@ -208,7 +243,7 @@ def save_lieu(request):
lieu = Lieu() lieu = Lieu()
# Servira à bouger un peu le lieu # Servira à bouger un peu le lieu
jitter = True jitter = True
# Lecture des données # Lecture des données
form = LieuForm(request.POST, instance=lieu) form = LieuForm(request.POST, instance=lieu)
@ -217,51 +252,54 @@ def save_lieu(request):
lieu = form.save(commit=False) lieu = form.save(commit=False)
if jitter: if jitter:
cdx, cdy = lieu.coord.get_coords() cdx, cdy = lieu.coord.get_coords()
ang = random.random() * 6.29; ang = random.random() * 6.29
rad = (random.random() + 0.5) * 3e-4 rad = (random.random() + 0.5) * 3e-4
cdx += math.cos(ang) * rad; cdx += math.cos(ang) * rad
cdy += math.sin(ang) * rad; cdy += math.sin(ang) * rad
lieu.coord.set_coords((cdx, cdy)) lieu.coord.set_coords((cdx, cdy))
lieu.save() lieu.save()
# Élimination des doublons # Élimination des doublons
if pk is None or pk == "": 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: for olieu in olieux:
if olieu.type_lieu == lieu.type_lieu and \ if (
olieu.ville == lieu.ville and \ olieu.type_lieu == lieu.type_lieu
olieu.pays == lieu.pays: and olieu.ville == lieu.ville
and olieu.pays == lieu.pays
):
return JsonResponse({"success": True, "id": olieu.id}) return JsonResponse({"success": True, "id": olieu.id})
lieu.save() lieu.save()
return JsonResponse({"success": True, "id": lieu.id}) return JsonResponse({"success": True, "id": lieu.id})
else: else:
return JsonResponse({"success": False, return JsonResponse({"success": False, "errors": form.errors})
"errors": form.errors})
else: else:
return JsonResponse({"erreur": "Aucune donnée POST"}) return JsonResponse({"erreur": "Aucune donnée POST"})
class LieuAjout(LoginRequiredMixin, CreateView): class LieuAjout(LoginRequiredMixin, CreateView):
model = Lieu model = Lieu
form_class = LieuForm form_class = LieuForm
template_name = 'avisstage/formulaires/lieu.html' template_name = "avisstage/formulaires/lieu.html"
# Retourne d'un JSON si requête AJAX # Retourne d'un JSON si requête AJAX
def form_valid(self, form): def form_valid(self, form):
if self.request.GET.get("format", "") == "json": if self.request.GET.get("format", "") == "json":
self.object = form.save() self.object = form.save()
return JsonResponse({"success": True, return JsonResponse({"success": True, "id": self.object.id})
"id": self.object.id})
else: else:
super(LieuAjout, self).form_valid(form) super(LieuAjout, self).form_valid(form)
def form_invalid(self, form): def form_invalid(self, form):
if self.request.GET.get("format", "") == "json": if self.request.GET.get("format", "") == "json":
return JsonResponse({"success": False, return JsonResponse({"success": False, "errors": form.errors})
"errors": form.errors})
else: else:
super(LieuAjout, self).form_valid(form) super(LieuAjout, self).form_valid(form)
# Passage d'un stage en mode public # Passage d'un stage en mode public
@login_required @login_required
def publier_stage(request, pk): def publier_stage(request, pk):
@ -280,27 +318,28 @@ def publier_stage(request, pk):
stage.public = False stage.public = False
stage.save() stage.save()
return redirect(reverse("avisstage:stage", kwargs={"pk": pk})) return redirect(reverse("avisstage:stage", kwargs={"pk": pk}))
# #
# FEEDBACK # FEEDBACK
# #
@login_required @login_required
def feedback(request): def feedback(request):
if request.method == "POST": if request.method == "POST":
form = FeedbackForm(request.POST) form = FeedbackForm(request.POST)
if form.is_valid(): if form.is_valid():
objet = form.cleaned_data['objet'] objet = form.cleaned_data["objet"]
header = "[From : %s <%s>]\n" % (request.user, header = "[From : %s <%s>]\n" % (request.user, request.user.email)
request.user.email) message = header + form.cleaned_data["message"]
message = header + form.cleaned_data['message']
send_mail( send_mail(
"[experiENS] "+ objet, "[experiENS] " + objet,
message, message,
request.user.email, request.user.email,
['robin.champenois@ens.fr'], ["robin.champenois@ens.fr"],
fail_silently=False, fail_silently=False,
) )
if request.GET.get("format", None) == "json": if request.GET.get("format", None) == "json":
@ -308,8 +347,7 @@ def feedback(request):
return redirect(reverse("avisstage:index")) return redirect(reverse("avisstage:index"))
else: else:
if request.GET.get("format", None) == "json": if request.GET.get("format", None) == "json":
return JsonResponse({"success": False, return JsonResponse({"success": False, "errors": form.errors})
"errors": form.errors})
else: else:
form = FeedbackForm() form = FeedbackForm()
raise Http404() raise Http404()
@ -319,62 +357,95 @@ def feedback(request):
# STATISTIQUES # STATISTIQUES
# #
@login_required @login_required
@staff_member_required @staff_member_required
def statistiques(request): def statistiques(request):
nstages = Stage.objects.count() nstages = Stage.objects.count()
npubstages = Stage.objects.filter(public=True).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) nbymatiere = defaultdict(dict)
for npm in nbymatiere_raw: 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(): for mat, npm in nbymatiere.items():
npm["matiere"] = mat npm["matiere"] = mat
nbymatiere = sorted(list(nbymatiere.values()), key=lambda npm: -npm.get("publics", 0)) nbymatiere = sorted(
nbylength = [("Vide", Stage.objects.filter(len_avis_stage__lt=5).count(), list(nbymatiere.values()), key=lambda npm: -npm.get("publics", 0)
Stage.objects.filter(len_avis_lieux__lt=5).count()), )
("Court", Stage.objects.filter(len_avis_stage__lt=30, len_avis_stage__gt=4).count(), nbylength = [
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(), "Vide",
Stage.objects.filter(len_avis_lieux__lt=100, len_avis_lieux__gt=29).count()), Stage.objects.filter(len_avis_stage__lt=5).count(),
("Long", Stage.objects.filter(len_avis_stage__gt=99).count(), Stage.objects.filter(len_avis_lieux__lt=5).count(),
Stage.objects.filter(len_avis_lieux__gt=99).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() nusers = Normalien.objects.count()
nauts = Normalien.objects.filter(stages__isnull=False).distinct().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() nlieux = Lieu.objects.filter(stages__isnull=False).distinct().count()
return render(request, 'avisstage/moderation/statistiques.html', return render(
{'num_stages': nstages, request,
'num_stages_pub': npubstages, "avisstage/moderation/statistiques.html",
'num_par_matiere': nbymatiere, {
'num_users': nusers, "num_stages": nstages,
'num_auteurs': nauts, "num_stages_pub": npubstages,
'num_par_auteur': nbyaut, "num_par_matiere": nbymatiere,
'num_lieux_utiles': nlieux, "num_users": nusers,
'num_par_longueur': nbylength, "num_auteurs": nauts,
}) "num_par_auteur": nbyaut,
"num_lieux_utiles": nlieux,
"num_par_longueur": nbylength,
},
)
# #
# Compte # Compte
# #
class MesAdressesMixin(LoginRequiredMixin): class MesAdressesMixin(LoginRequiredMixin):
slug_url_kwarg = "email" slug_url_kwarg = "email"
slug_field = "email" slug_field = "email"
confirmed_only = False confirmed_only = False
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
qs = self.request.user.email_address_set.all() qs = self.request.user.email_address_set.all()
if self.confirmed_only: if self.confirmed_only:
qs = qs.filter(confirmed_at__isnull=False) qs = qs.filter(confirmed_at__isnull=False)
return qs return qs
def _send_confirm_mail(email, request): def _send_confirm_mail(email, request):
confirm_url = request.build_absolute_uri( confirm_url = request.build_absolute_uri(
reverse("avisstage:emails_confirme", kwargs={"key": email.key})) reverse("avisstage:emails_confirme", kwargs={"key": email.key})
)
send_mail( send_mail(
"[ExperiENS] Confirmez votre adresse a-mail", "[ExperiENS] Confirmez votre adresse a-mail",
"""Bonjour, """Bonjour,
Vous venez d'ajouter cette adresse e-mail à votre compte ExperiENS. Vous venez d'ajouter cette adresse e-mail à votre compte ExperiENS.
@ -383,14 +454,18 @@ Pour la vérifier, merci de cliquer sur le lien suivant, ou de copier l'adresse
{confirm_url} {confirm_url}
Cordialement, Cordialement,
L'équipe ExperiENS""".format(confirm_url=confirm_url), L'équipe ExperiENS""".format(
'experiens-nepasrepondre@eleves.ens.fr', confirm_url=confirm_url
),
"experiens-nepasrepondre@eleves.ens.fr",
[email.email], [email.email],
fail_silently=False, fail_silently=False,
) )
return redirect(reverse("avisstage:emails_aconfirmer", return redirect(
kwargs={"email": email.email})) reverse("avisstage:emails_aconfirmer", kwargs={"email": email.email})
)
class MesParametres(LoginRequiredMixin, FormView): class MesParametres(LoginRequiredMixin, FormView):
model = EmailAddress model = EmailAddress
template_name = "avisstage/compte/parametres.html" template_name = "avisstage/compte/parametres.html"
@ -400,16 +475,18 @@ class MesParametres(LoginRequiredMixin, FormView):
kwargs = super().get_form_kwargs(*args, **kwargs) kwargs = super().get_form_kwargs(*args, **kwargs)
kwargs["_user"] = self.request.user kwargs["_user"] = self.request.user
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
new = EmailAddress.objects.create_unconfirmed( 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) return _send_confirm_mail(new, self.request)
class RendAdressePrincipale(MesAdressesMixin, SingleObjectMixin, View): class RendAdressePrincipale(MesAdressesMixin, SingleObjectMixin, View):
model = EmailAddress model = EmailAddress
confirmed_only = True confirmed_only = True
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
if not hasattr(self, "object"): if not hasattr(self, "object"):
self.object = self.get_object() self.object = self.get_object()
@ -417,10 +494,12 @@ class RendAdressePrincipale(MesAdressesMixin, SingleObjectMixin, View):
self.request.user.save() self.request.user.save()
return redirect(reverse("avisstage:parametres")) return redirect(reverse("avisstage:parametres"))
class AdresseAConfirmer(MesAdressesMixin, DetailView): class AdresseAConfirmer(MesAdressesMixin, DetailView):
model = EmailAddress model = EmailAddress
template_name = "avisstage/compte/aconfirmer.html" template_name = "avisstage/compte/aconfirmer.html"
class ReConfirmeAdresse(MesAdressesMixin, DetailView): class ReConfirmeAdresse(MesAdressesMixin, DetailView):
model = EmailAddress model = EmailAddress
@ -430,18 +509,23 @@ class ReConfirmeAdresse(MesAdressesMixin, DetailView):
return _send_confirm_mail(email, self.request) return _send_confirm_mail(email, self.request)
return redirect(reverse("avisstage:parametres")) return redirect(reverse("avisstage:parametres"))
class ConfirmeAdresse(LoginRequiredMixin, View): class ConfirmeAdresse(LoginRequiredMixin, View):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
try: try:
email = EmailAddress.objects.confirm(self.kwargs["key"], email = EmailAddress.objects.confirm(
self.request.user, True) self.kwargs["key"], self.request.user, True
)
except Exception as e: except Exception as e:
raise Http404() raise Http404()
messages.add_message( messages.add_message(
self.request, messages.SUCCESS, self.request,
"L'adresse email {email} a bien été confirmée".format(email=email.email)) messages.SUCCESS,
"L'adresse email {email} a bien été confirmée".format(email=email.email),
)
return redirect(reverse("avisstage:parametres")) return redirect(reverse("avisstage:parametres"))
class SupprimeAdresse(MesAdressesMixin, DeleteView): class SupprimeAdresse(MesAdressesMixin, DeleteView):
model = EmailAddress model = EmailAddress
template_name = "avisstage/compte/email_supprime.html" template_name = "avisstage/compte/email_supprime.html"
@ -451,21 +535,26 @@ class SupprimeAdresse(MesAdressesMixin, DeleteView):
qs = super().get_queryset(*args, **kwargs) qs = super().get_queryset(*args, **kwargs)
return qs.exclude(email=self.request.user.email) return qs.exclude(email=self.request.user.email)
class EnvoieLienMotDePasse(LoginRequiredMixin, View): class EnvoieLienMotDePasse(LoginRequiredMixin, View):
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
form = ReinitMdpForm({"email": self.request.user.email}) form = ReinitMdpForm({"email": self.request.user.email})
form.is_valid() form.is_valid()
form.save( form.save(
email_template_name = 'avisstage/mails/reinit_mdp.html', email_template_name="avisstage/mails/reinit_mdp.html",
from_email = 'experiens-nepasrepondre@eleves.ens.fr', from_email="experiens-nepasrepondre@eleves.ens.fr",
subject_template_name = 'avisstage/mails/reinit_mdp.txt', subject_template_name="avisstage/mails/reinit_mdp.txt",
) )
messages.add_message( messages.add_message(
self.request, messages.INFO, self.request,
"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) 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")) return redirect(reverse("avisstage:parametres"))
class DefinirMotDePasse(PasswordResetConfirmView): class DefinirMotDePasse(PasswordResetConfirmView):
template_name = "avisstage/compte/edit_mdp.html" template_name = "avisstage/compte/edit_mdp.html"
success_url = reverse_lazy("avisstage:perso") success_url = reverse_lazy("avisstage:perso")
@ -475,4 +564,3 @@ class DefinirMotDePasse(PasswordResetConfirmView):
if self.request.user.is_authenticated and user != self.request.user: if self.request.user.is_authenticated and user != self.request.user:
raise Http404("Ce token n'est pas valide pour votre compte") raise Http404("Ce token n'est pas valide pour votre compte")
return user return user

View file

@ -29,28 +29,36 @@ logger = logging.getLogger("recherche")
# Recherche # Recherche
class SearchForm(forms.Form): class SearchForm(forms.Form):
generique = forms.CharField(required=False) generique = forms.CharField(required=False)
sujet = forms.CharField(label=u'À propos de', required=False) sujet = forms.CharField(label=u"À propos de", required=False)
contexte = forms.CharField(label=u'Contexte (lieu, encadrant⋅e⋅s, structure)', contexte = forms.CharField(
required=False) label=u"Contexte (lieu, encadrant⋅e⋅s, structure)", required=False
)
apres_annee = forms.IntegerField(label=u'Après cette année', required=False)
avant_annee = forms.IntegerField(label=u'Avant cette année', required=False)
type_stage = forms.ChoiceField(label="Type de stage", choices=([('', u'')] apres_annee = forms.IntegerField(label=u"Après cette année", required=False)
+ list(TYPE_STAGE_OPTIONS)), avant_annee = forms.IntegerField(label=u"Avant cette année", required=False)
required=False)
niveau_scol = forms.ChoiceField(label="Année d'étude", choices=([('', u'')] type_stage = forms.ChoiceField(
+ list(NIVEAU_SCOL_OPTIONS)), label="Type de stage",
required=False) choices=([("", u"")] + list(TYPE_STAGE_OPTIONS)),
required=False,
type_lieu = forms.ChoiceField(label=u"Type de lieu d'accueil", )
choices=([('', u'')] niveau_scol = forms.ChoiceField(
+ list(TYPE_LIEU_OPTIONS)), label="Année d'étude",
required=False) choices=([("", u"")] + list(NIVEAU_SCOL_OPTIONS)),
tri = forms.ChoiceField(label=u'Tri par', required=False,
choices=[('pertinence', u'Pertinence'), )
('-date_maj',u'Dernière mise à jour')],
required=False, initial='pertinence') type_lieu = forms.ChoiceField(
label=u"Type de lieu d'accueil",
choices=([("", u"")] + list(TYPE_LIEU_OPTIONS)),
required=False,
)
tri = forms.ChoiceField(
label=u"Tri par",
choices=[("pertinence", u"Pertinence"), ("-date_maj", u"Dernière mise à jour")],
required=False,
initial="pertinence",
)
def cherche(**kwargs): def cherche(**kwargs):
@ -58,9 +66,11 @@ def cherche(**kwargs):
use_dsl = False use_dsl = False
def field_relevant(field, test_string=True): def field_relevant(field, test_string=True):
return field in kwargs and \ return (
kwargs[field] is not None and \ field in kwargs
((not test_string) or kwargs[field].strip() != '') and kwargs[field] is not None
and ((not test_string) or kwargs[field].strip() != "")
)
if USE_ELASTICSEARCH: if USE_ELASTICSEARCH:
dsl = StageDocument.search() dsl = StageDocument.search()
@ -71,39 +81,49 @@ def cherche(**kwargs):
# Champ générique : recherche dans tous les champs # Champ générique : recherche dans tous les champs
if field_relevant("generique"): if field_relevant("generique"):
#print("Filtre generique", kwargs['generique']) # print("Filtre generique", kwargs['generique'])
dsl = dsl.query( dsl = dsl.query(
"multi_match", "multi_match",
query = kwargs["generique"], query=kwargs["generique"],
fuzziness = "auto", fuzziness="auto",
fields = [ fields=[
'sujet^3', "sujet^3",
'encadrants', "encadrants",
'type_stage', "type_stage",
'niveau_scol', "niveau_scol",
'structure', "structure",
"lieux.*^2", "lieux.*^2",
"auteur.nom^2", "auteur.nom^2",
"thematiques^2", "thematiques^2",
"matieres" "matieres",
]) ],
)
use_dsl = True use_dsl = True
# Sujet -> Recherche dan les noms de sujets et les thématiques # Sujet -> Recherche dan les noms de sujets et les thématiques
if field_relevant("sujet"): if field_relevant("sujet"):
dsl = dsl.query("multi_match", dsl = dsl.query(
query = kwargs["sujet"], "multi_match",
fields = ['sujet^2', 'thematiques', 'matieres'], query=kwargs["sujet"],
fuzziness = "auto") fields=["sujet^2", "thematiques", "matieres"],
fuzziness="auto",
)
use_dsl = True use_dsl = True
# Contexte -> Encadrants, structure, lieu # Contexte -> Encadrants, structure, lieu
if field_relevant("contexte"): if field_relevant("contexte"):
dsl = dsl.query("multi_match", dsl = dsl.query(
query = kwargs["contexte"], "multi_match",
fields = ['encadrants', 'structure^2', query=kwargs["contexte"],
'lieux.nom', 'lieux.pays', 'lieux.ville'], fields=[
fuzziness = "auto") "encadrants",
"structure^2",
"lieux.nom",
"lieux.pays",
"lieux.ville",
],
fuzziness="auto",
)
use_dsl = True use_dsl = True
else: else:
@ -111,45 +131,48 @@ def cherche(**kwargs):
# recherche en base de données # recherche en base de données
if field_relevant("generique"): if field_relevant("generique"):
generique = kwargs["generique"] generique = kwargs["generique"]
filtres = (Q(sujet__icontains=generique) filtres = (
| Q(thematiques__name__icontains=generique) Q(sujet__icontains=generique)
| Q(matieres__nom__icontains=generique) | Q(thematiques__name__icontains=generique)
| Q(lieux__nom__icontains=generique)) | Q(matieres__nom__icontains=generique)
| Q(lieux__nom__icontains=generique)
)
# Autres champs -> non fonctionnels # Autres champs -> non fonctionnels
if field_relevant("sujet") or field_relevant("contexte"): if field_relevant("sujet") or field_relevant("contexte"):
raise NotImplementedError( raise NotImplementedError(
"ElasticSearch doit être activé pour ce type de recherche") "ElasticSearch doit être activé pour ce type de recherche"
)
# #
# Filtres directs db # Filtres directs db
# #
# Dates # Dates
if field_relevant('avant_annee', False): if field_relevant("avant_annee", False):
dte = date(kwargs['avant_annee']+1, 1, 1) dte = date(kwargs["avant_annee"] + 1, 1, 1)
filtres &= Q(date_fin__lt=dte) filtres &= Q(date_fin__lt=dte)
if field_relevant('apres_annee', False): if field_relevant("apres_annee", False):
dte = date(kwargs['apres_annee'], 1, 1) dte = date(kwargs["apres_annee"], 1, 1)
filtres &= Q(date_debut__gte=dte) filtres &= Q(date_debut__gte=dte)
# Type de stage # Type de stage
if field_relevant('type_stage'): if field_relevant("type_stage"):
filtres &= Q(type_stage=kwargs["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"]) filtres &= Q(niveau_scol=kwargs["niveau_scol"])
# Type de lieu # Type de lieu
if field_relevant('type_lieu'): if field_relevant("type_lieu"):
filtres &= Q(lieux__type_lieu=kwargs["type_lieu"]) filtres &= Q(lieux__type_lieu=kwargs["type_lieu"])
# Tri # Tri
tri = "pertinence" tri = "pertinence"
if field_relevant('tri') and kwargs['tri'] in ['-date_maj']: if field_relevant("tri") and kwargs["tri"] in ["-date_maj"]:
tri = kwargs['tri'] tri = kwargs["tri"]
if not use_dsl: if not use_dsl:
tri = "-date_maj" tri = "-date_maj"
@ -163,28 +186,27 @@ def cherche(**kwargs):
if tri == "pertinence": if tri == "pertinence":
resultat = resultat.order_by( resultat = resultat.order_by(
Case( Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(dsl_res)])
*[When(pk=pk, then=pos) for pos, pk in enumerate(dsl_res)]
)
) )
else: else:
resultat = resultat.order_by(tri) resultat = resultat.order_by(tri)
return resultat, tri return resultat, tri
@login_required @login_required
@en_scolarite_required @en_scolarite_required
def recherche(request): def recherche(request):
form = SearchForm() form = SearchForm()
return render(request, 'avisstage/recherche/recherche.html', return render(request, "avisstage/recherche/recherche.html", {"form": form})
{"form": form})
@login_required @login_required
@en_scolarite_required @en_scolarite_required
def recherche_resultats(request): def recherche_resultats(request):
stages = [] stages = []
tri = '' tri = ""
vue = 'vue-liste' vue = "vue-liste"
lieux = [] lieux = []
stageids = [] stageids = []
if request.method == "GET": if request.method == "GET":
@ -194,17 +216,22 @@ def recherche_resultats(request):
search_args = form.cleaned_data search_args = form.cleaned_data
# Gestion du cache # Gestion du cache
lsearch_args = {key: val for key, val in search_args.items() lsearch_args = {
if val != "" and val is not None} 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) cache_key = json.dumps(lsearch_args, sort_keys=True)
cached = cache.get(cache_key) cached = cache.get(cache_key)
if cached is None: if cached is None:
# Requête effective # Requête effective
stages, tri = cherche(**search_args) stages, tri = cherche(**search_args)
stageids = list(stages.values_list('id', flat=True)) stageids = list(stages.values_list("id", flat=True))
lieux = [[stageid, lieuid] for (stageid, lieuid) lieux = [
in stages.values_list('id', 'lieux') [stageid, lieuid]
if lieuid is not None] for (stageid, lieuid) in stages.values_list("id", "lieux")
if lieuid is not None
]
# Sauvegarde dans le cache # Sauvegarde dans le cache
to_cache = {"stages": stageids, "lieux": lieux, "tri": tri} to_cache = {"stages": stageids, "lieux": lieux, "tri": tri}
@ -225,42 +252,55 @@ def recherche_resultats(request):
stageids = [] stageids = []
if cached is None: if cached is None:
stages = stages[max(0, stageids.start_index()-1): stages = stages[
stageids.end_index()] max(0, stageids.start_index() - 1) : stageids.end_index()
]
else: else:
orderer = Case(*[When(pk=pk, then=pos) orderer = Case(
for pos, pk in enumerate(stageids)]) *[When(pk=pk, then=pos) for pos, pk in enumerate(stageids)]
)
stages = Stage.objects.filter(id__in=stageids).order_by(orderer) stages = Stage.objects.filter(id__in=stageids).order_by(orderer)
stages = stages.prefetch_related('lieux', 'auteur', stages = stages.prefetch_related(
'matieres', 'thematiques') "lieux", "auteur", "matieres", "thematiques"
)
else: else:
form = SearchForm() form = SearchForm()
if stages: if stages:
vue = 'vue-hybride' vue = "vue-hybride"
# Version JSON pour recherche dynamique # Version JSON pour recherche dynamique
if request.GET.get("format") == "json": if request.GET.get("format") == "json":
return JsonResponse({"stages": stages, "page": page, return JsonResponse(
"num_pages": paginator.num_pages}) {"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": if request.GET.get("format") == "raw":
template_name = 'avisstage/recherche/stage_items.html' template_name = "avisstage/recherche/stage_items.html"
return render(request, template_name, return render(
{"form": form, "stages": stages, "paginator": stageids, request,
"tri": tri, "vue": vue, "lieux": lieux, template_name,
"MAPBOX_API_KEY": settings.MAPBOX_API_KEY}) {
"form": form,
"stages": stages,
"paginator": stageids,
"tri": tri,
"vue": vue,
"lieux": lieux,
"MAPBOX_API_KEY": settings.MAPBOX_API_KEY,
},
)
@login_required @login_required
@en_scolarite_required @en_scolarite_required
def stage_items(request): def stage_items(request):
try: 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: except ValueError:
return HttpResponseBadRequest("Paramètre incorrect") return HttpResponseBadRequest("Paramètre incorrect")
stages = Stage.objects.filter(id__in=stageids)\ stages = Stage.objects.filter(id__in=stageids).prefetch_related(
.prefetch_related('lieux', 'auteur', "lieux", "auteur", "matieres", "thematiques"
'matieres', 'thematiques') )
return render(request, 'avisstage/recherche/stage_items.html', return render(request, "avisstage/recherche/stage_items.html", {"stages": stages})
{"stages": stages})

View file

@ -1,14 +1,14 @@
from django import forms from django import forms
from django.core import validators from django.core import validators
class LatLonWidget(forms.MultiWidget): class LatLonWidget(forms.MultiWidget):
""" """
A Widget that splits Point input into two latitude/longitude boxes. A Widget that splits Point input into two latitude/longitude boxes.
""" """
def __init__(self, attrs=None, date_format=None, time_format=None): def __init__(self, attrs=None, date_format=None, time_format=None):
widgets = (forms.HiddenInput(attrs=attrs), widgets = (forms.HiddenInput(attrs=attrs), forms.HiddenInput(attrs=attrs))
forms.HiddenInput(attrs=attrs))
super(LatLonWidget, self).__init__(widgets, attrs) super(LatLonWidget, self).__init__(widgets, attrs)
def decompress(self, value): def decompress(self, value):
@ -23,13 +23,15 @@ class LatLonField(forms.MultiValueField):
srid = 4326 srid = 4326
default_error_messages = { default_error_messages = {
'invalid_latitude' : (u'Entrez une latitude valide.'), "invalid_latitude": (u"Entrez une latitude valide."),
'invalid_longitude' : (u'Entrez une longitude valide.'), "invalid_longitude": (u"Entrez une longitude valide."),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
fields = (forms.FloatField(min_value=-90, max_value=90), fields = (
forms.FloatField(min_value=-180, max_value=180)) forms.FloatField(min_value=-90, max_value=90),
forms.FloatField(min_value=-180, max_value=180),
)
super(LatLonField, self).__init__(fields, *args, **kwargs) super(LatLonField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list): def compress(self, data_list):
@ -37,11 +39,11 @@ class LatLonField(forms.MultiValueField):
# Raise a validation error if latitude or longitude is empty # Raise a validation error if latitude or longitude is empty
# (possible if LatLongField has required=False). # (possible if LatLongField has required=False).
if data_list[0] in validators.EMPTY_VALUES: 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: 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=4326;POINT(1.12345789 1.123456789)
srid_str = 'SRID=%d'%self.srid srid_str = "SRID=%d" % self.srid
point_str = 'POINT(%f %f)'%tuple(reversed(data_list)) point_str = "POINT(%f %f)" % tuple(reversed(data_list))
return ';'.join([srid_str, point_str]) return ";".join([srid_str, point_str])
return None return None

View file

@ -1,9 +1,10 @@
from authens.backends import ENSCASBackend as AuthENSBackend from authens.backends import ENSCASBackend as AuthENSBackend
from authens.utils import parse_entrance_year from authens.utils import parse_entrance_year
class ENSCASBackend(AuthENSBackend): class ENSCASBackend(AuthENSBackend):
# Override AuthENS backend user creation to implement the @<promo> logic # Override AuthENS backend user creation to implement the @<promo> logic
def get_free_username(self, cas_login, attributes): def get_free_username(self, cas_login, attributes):
entrance_year = parse_entrance_year(attributes.get("homeDirectory")) entrance_year = parse_entrance_year(attributes.get("homeDirectory"))
if entrance_year is None: if entrance_year is None:

View file

@ -24,68 +24,65 @@ ALLOWED_HOSTS = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', "django.contrib.admin",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.staticfiles', "django.contrib.staticfiles",
'django.contrib.gis', "django.contrib.gis",
'django.contrib.sites', "django.contrib.sites",
"django_elasticsearch_dsl",
'django_elasticsearch_dsl',
#'allauth', # Uncomment that part when you #'allauth', # Uncomment that part when you
#'allauth.account', # apply migration #'allauth.account', # apply migration
#'allauth.socialaccount', # Allauth -> AuthENS #'allauth.socialaccount', # Allauth -> AuthENS
"simple_email_confirmation",
'simple_email_confirmation', "authens",
'authens', "tastypie",
'tastypie', "braces",
'braces', "tinymce",
'tinymce', "taggit",
'taggit', "taggit_autosuggest",
'taggit_autosuggest', "avisstage",
'avisstage'
] ]
MIDDLEWARE = ( MIDDLEWARE = (
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
) )
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [ "DIRS": [
# insert your TEMPLATE_DIRS here # insert your TEMPLATE_DIRS here
], ],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.i18n', "django.template.context_processors.i18n",
'django.template.context_processors.media', "django.template.context_processors.media",
'django.template.context_processors.static', "django.template.context_processors.static",
'django.template.context_processors.tz', "django.template.context_processors.tz",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.messages.context_processors.messages', "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 # Database
@ -94,9 +91,9 @@ WSGI_APPLICATION = 'experiENS.wsgi.application'
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/ # 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 USE_I18N = True
@ -109,37 +106,37 @@ SITE_ID = 1
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/ # https://docs.djangoproject.com/en/1.7/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = "/static/"
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', "django.contrib.auth.backends.ModelBackend",
'experiENS.auth.ENSCASBackend', "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 AUTHENS_USE_OLDCAS = False
LOGIN_URL = reverse_lazy('authens:login') LOGIN_URL = reverse_lazy("authens:login")
LOGOUT_URL = reverse_lazy('authens:logout') LOGOUT_URL = reverse_lazy("authens:logout")
LOGIN_REDIRECT_URL = reverse_lazy('avisstage:perso') LOGIN_REDIRECT_URL = reverse_lazy("avisstage:perso")
LOGOUT_REDIRECT_URL = reverse_lazy('avisstage:index') LOGOUT_REDIRECT_URL = reverse_lazy("avisstage:index")
LOGGING = { LOGGING = {
'version': 1, "version": 1,
'disable_existing_loggers': False, "disable_existing_loggers": False,
'handlers': { "handlers": {
'file': { "file": {
'level': 'INFO', "level": "INFO",
'class': 'logging.FileHandler', "class": "logging.FileHandler",
'filename': os.path.join(BASE_DIR, 'recherche.log'), "filename": os.path.join(BASE_DIR, "recherche.log"),
}, },
}, },
'loggers': { "loggers": {
'recherche': { "recherche": {
'handlers': ['file'], "handlers": ["file"],
'level': 'INFO', "level": "INFO",
'propagate': True, "propagate": True,
}, },
}, },
} }

View file

@ -3,9 +3,9 @@ from .settings_base import *
DEBUG = True DEBUG = True
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.contrib.gis.db.backends.spatialite', "ENGINE": "django.contrib.gis.db.backends.spatialite",
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
} }
} }
@ -13,35 +13,30 @@ USE_DEBUG_TOOLBAR = False
if USE_DEBUG_TOOLBAR: if USE_DEBUG_TOOLBAR:
INSTALLED_APPS += [ INSTALLED_APPS += [
'debug_toolbar', "debug_toolbar",
] ]
MIDDLEWARE = ( MIDDLEWARE = ("debug_toolbar.middleware.DebugToolbarMiddleware",) + 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/" 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/" STATIC_URL = "/experiens/static/"
ELASTICSEARCH_DSL = { ELASTICSEARCH_DSL = {
'default': { "default": {"hosts": "localhost:9200"},
'hosts': 'localhost:9200'
},
} }
CLIPPER_LDAP_SERVER = 'ldaps://localhost:636' CLIPPER_LDAP_SERVER = "ldaps://localhost:636"
# Changer à True pour développer avec ES # Changer à True pour développer avec ES
USE_ELASTICSEARCH = False USE_ELASTICSEARCH = False
if not USE_ELASTICSEARCH: if not USE_ELASTICSEARCH:
INSTALLED_APPS.remove('django_elasticsearch_dsl') INSTALLED_APPS.remove("django_elasticsearch_dsl")

View file

@ -9,9 +9,7 @@ DEBUG = False
ALLOWED_HOSTS = ["www.eleves.ens.fr"] ALLOWED_HOSTS = ["www.eleves.ens.fr"]
ADMINS = ( ADMINS = (("Robin Champenois", "champeno@clipper.ens.fr"),)
('Robin Champenois', 'champeno@clipper.ens.fr'),
)
ADMIN_LOGINS = [ ADMIN_LOGINS = [
"champeno", "champeno",
@ -21,33 +19,31 @@ SERVER_EMAIL = "experiens@www.eleves.ens.fr"
ROOT_URL = "/experiens/" ROOT_URL = "/experiens/"
WSGI_APPLICATION = 'experiENS.wsgi.application' WSGI_APPLICATION = "experiENS.wsgi.application"
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.contrib.gis.db.backends.postgis', "ENGINE": "django.contrib.gis.db.backends.postgis",
'NAME': 'experiens', "NAME": "experiens",
'USER': 'experiens', "USER": "experiens",
'PASSWORD': '', "PASSWORD": "",
'HOST': '', "HOST": "",
'PORT': '5432', "PORT": "5432",
} }
} }
STATIC_URL = ROOT_URL + 'static/' STATIC_URL = ROOT_URL + "static/"
MEDIA_URL = ROOT_URL + 'media/' 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" EMAIL_HOST = "nef.ens.fr"
ELASTICSEARCH_DSL = { ELASTICSEARCH_DSL = {
'default': { "default": {"hosts": "127.0.0.1:9200"},
'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" DEFAULT_FROM_EMAIL = "experiens-no-reply@www.eleves.ens.fr"

View file

@ -3,18 +3,16 @@ from django.urls import include, path
from django.contrib import admin from django.contrib import admin
urlpatterns = [ urlpatterns = [
path('', include('avisstage.urls')), path("", include("avisstage.urls")),
path("authens/", include("authens.urls")), path("authens/", include("authens.urls")),
path("tinymce/", include("tinymce.urls")),
path('tinymce/', include('tinymce.urls')), path("taggit_autosuggest/", include("taggit_autosuggest.urls")),
path('taggit_autosuggest/', include('taggit_autosuggest.urls')), path("admin/", admin.site.urls),
path('admin/', admin.site.urls),
] ]
if settings.DEBUG: if settings.DEBUG:
import debug_toolbar import debug_toolbar
urlpatterns = [ urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)), path("__debug__/", include(debug_toolbar.urls)),
] + urlpatterns ] + urlpatterns

View file

@ -8,7 +8,9 @@ https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
""" """
import os import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "experiENS.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "experiENS.settings")
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
application = get_wsgi_application() application = get_wsgi_application()