from datetime import timedelta from authens.models import CASAccount from authens.signals import post_cas_connect from taggit_autosuggest.managers import TaggableManager from tinymce.models import HTMLField as RichTextField from django.contrib.auth.models import User from django.contrib.gis.db import models as geomodels from django.db import models from django.db.models.signals import post_save from django.forms.widgets import DateInput from django.template.defaultfilters import slugify from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property from django.utils.html import strip_tags from .statics import ( DEPARTEMENTS_DEFAUT, NIVEAU_SCOL_DICT, NIVEAU_SCOL_OPTIONS, PAYS_OPTIONS, TYPE_LIEU_DICT, TYPE_LIEU_OPTIONS, TYPE_STAGE_DICT, TYPE_STAGE_OPTIONS, ) from .utils import choices_length, is_email_ens def _default_cas_login(): return (timezone.now() - timedelta(days=365)).date() # # Profil Normalien (extension du modèle User) # class Normalien(models.Model): user = models.OneToOneField( User, related_name="profil", on_delete=models.SET_NULL, null=True ) # Infos spécifiques nom = models.CharField(u"Nom complet", max_length=255, blank=True) promotion = models.CharField(u"Promotion", max_length=40, blank=True) contactez_moi = models.BooleanField( u"Inviter les visiteurs à me contacter", default=True, help_text="Affiche votre adresse e-mail principale sur votre profil public", ) bio = models.TextField(u"À propos de moi", blank=True, default="") last_cas_login = models.DateField(default=_default_cas_login) class Meta: verbose_name = u"Profil élève" verbose_name_plural = u"Profils élèves" def __str__(self): return u"%s (%s)" % (self.nom, self.user.username) # Liste des stages publiés def stages_publics(self): return self.stages.filter(public=True).order_by("-date_debut") def has_nonENS_email(self): return ( self.user.email_address_set.exclude(confirmed_at__isnull=True) .exclude(email__endswith="ens.fr") .exclude(email__endswith="ens.psl.eu") .exists() ) def nom_complet(self): if self.nom.strip(): return self.nom return self.user.username @property def en_scolarite(self): return self.last_cas_login > (timezone.now() - timedelta(days=60)).date() @property def preferred_email(self): return self.user.email # Hook à la création d'un nouvel utilisateur : information de base def create_basic_user_profile(sender, instance, created, **kwargs): if created: profil, created = Normalien.objects.get_or_create(user=instance) if not created and profil.promotion != "": return if "@" in instance.username: profil.promotion = instance.username.split("@")[1] profil.save() post_save.connect(create_basic_user_profile, sender=User) # Hook d'authENS : information du CAS def handle_cas_connection(sender, instance, created, cas_login, attributes, **kwargs): profil, created = Normalien.objects.get_or_create(user=instance) profil.last_cas_login = timezone.now().date() if not created: profil.save() return dirs = attributes.get("homeDirectory", "").split("/") if len(dirs) < 4: print("HomeDirectory invalide", dirs) return year = dirs[2] departement = dirs[3] dep = dict(DEPARTEMENTS_DEFAUT).get(departement.lower(), "") profil.promotion = "%s %s" % (dep, year) profil.nom = attributes.get("name", "") profil.save() post_cas_connect.connect(handle_cas_connection, sender=User) # # Lieu de stage # class Lieu(models.Model): # Général nom = models.CharField(u"Nom de l'institution d'accueil", max_length=250) type_lieu = models.CharField( u"Type de structure d'accueil", default="universite", choices=TYPE_LIEU_OPTIONS, max_length=choices_length(TYPE_LIEU_OPTIONS), ) # Infos géographiques ville = models.CharField(u"Ville", max_length=200) pays = models.CharField( u"Pays", choices=PAYS_OPTIONS, max_length=choices_length(PAYS_OPTIONS) ) # Coordonnées # objects = geomodels.GeoManager() # Requis par GeoDjango coord = geomodels.PointField(u"Coordonnées", geography=True, srid=4326) # Type du lieu en plus joli @property def type_lieu_fancy(self): return TYPE_LIEU_DICT.get(self.type_lieu, ("lieu", False))[0] @property def type_lieu_fem(self): return TYPE_LIEU_DICT.get(self.type_lieu, ("lieu", False))[1] def __str__(self): return u"%s (%s)" % (self.nom, self.ville) class Meta: verbose_name = "Lieu" verbose_name_plural = "Lieux" # # Matières des stages # class StageMatiere(models.Model): nom = models.CharField(u"Nom", max_length=30) slug = models.SlugField() class Meta: verbose_name = "Matière des stages" verbose_name_plural = "Matières des stages" def __str__(self): return self.nom # # Un stage # class Stage(models.Model): # Misc auteur = models.ForeignKey( Normalien, related_name="stages", on_delete=models.SET_NULL, null=True ) public = models.BooleanField(u"Visible publiquement", default=False) date_creation = models.DateTimeField(u"Créé le", default=timezone.now) date_maj = models.DateTimeField(u"Mis à jour le", default=timezone.now) len_avis_stage = models.IntegerField(u"Longueur des avis de stage", default=0) len_avis_lieux = models.IntegerField(u"Longueur des avis de lieu", default=0) # Caractéristiques du stage sujet = models.CharField(u"Sujet", max_length=500) date_debut = models.DateField(u"Date de début", null=True) date_fin = models.DateField(u"Date de fin", null=True) type_stage = models.CharField( u"Type", default="stage", choices=TYPE_STAGE_OPTIONS, max_length=choices_length(TYPE_STAGE_OPTIONS), ) niveau_scol = models.CharField( u"Année de scolarité", default="", choices=NIVEAU_SCOL_OPTIONS, max_length=choices_length(NIVEAU_SCOL_OPTIONS), blank=True, ) thematiques = TaggableManager(u"Thématiques", blank=True) matieres = models.ManyToManyField( StageMatiere, verbose_name=u"Matière(s)", related_name="stages" ) encadrants = models.CharField(u"Encadrant⋅e⋅s", max_length=500, blank=True) structure = models.CharField(u"Structure d'accueil", max_length=500, blank=True) # Avis lieux = models.ManyToManyField( Lieu, related_name="stages", through="AvisLieu", blank=True ) # Affichage des avis ordonnés @property def avis_lieux(self): return self.avislieu_set.order_by("order") # Shortcut pour affichage rapide @property def lieu_principal(self): avis_lieux = self.avis_lieux if len(avis_lieux) == 0: return None return self.avis_lieux[0].lieu # Type du stage en plus joli @property def type_stage_fancy(self): return TYPE_STAGE_DICT.get(self.type_stage, ("stage", False))[0] @property def type_stage_fem(self): return TYPE_STAGE_DICT.get(self.type_stage, ("stage", False))[1] # Niveau scolaire en plus joli @property def niveau_scol_fancy(self): return NIVEAU_SCOL_DICT.get(self.niveau_scol, "") # Optimisation de requêtes @cached_property def all_lieux(self): return self.lieux.all() def get_absolute_url(self): return reverse("avisstage:stage", self) def __str__(self): return u"%s (par %s)" % (self.sujet, self.auteur.user.username) def update_stats(self, save=True): def get_len(obj): length = 0 avis = obj.avis_all for k, av in avis: length += len(av.split()) length += len(obj.chapo.split()) length += len(obj.les_plus.split()) length += len(obj.les_moins.split()) return length if self.avis_stage: self.len_avis_stage = get_len(self.avis_stage) self.len_avis_lieux = 0 for avis in self.avislieu_set.all(): self.len_avis_lieux += get_len(avis) if save: self.save() class Meta: verbose_name = "Stage" # # Les avis # class AvisStage(models.Model): stage = models.OneToOneField( Stage, related_name="avis_stage", on_delete=models.CASCADE ) chapo = models.TextField(u"En quelques mots", blank=True) avis_ambiance = RichTextField(u"L'ambiance de travail", blank=True) avis_sujet = RichTextField(u"La mission", blank=True) avis_admin = RichTextField(u"Formalités et administration", blank=True) avis_prestage = RichTextField(u"Avant le stage", blank=True, default="") les_plus = models.TextField(u"Les plus de cette expérience", blank=True) les_moins = models.TextField(u"Les moins de cette expérience", blank=True) def __str__(self): return u"Avis sur {%s} par %s" % ( self.stage.sujet, self.stage.auteur.user.username, ) # Liste des champs d'avis, couplés à leur nom (pour l'affichage) @property def avis_all(self): fields = ["avis_sujet", "avis_ambiance", "avis_admin", "avis_prestage"] return [ (AvisStage._meta.get_field(field).verbose_name, getattr(self, field, "")) for field in fields ] class AvisLieu(models.Model): stage = models.ForeignKey(Stage, on_delete=models.CASCADE) lieu = models.ForeignKey(Lieu, on_delete=models.CASCADE) order = models.IntegerField("Ordre", default=0) chapo = models.TextField(u"En quelques mots", blank=True) avis_lieustage = RichTextField(u"Les lieux de travail", blank=True) avis_pratique = RichTextField(u"S'installer - conseils pratiques", blank=True) avis_tourisme = RichTextField(u"Dans les parages", blank=True) les_plus = models.TextField(u"Les plus du lieu", blank=True) les_moins = models.TextField(u"Les moins du lieu", blank=True) class Meta: verbose_name = "Avis sur un lieu de stage" verbose_name_plural = "Avis sur un lieu de stage" def __str__(self): return u"Avis sur {%s} par %s" % (self.lieu.nom, self.stage.auteur.user_id) # Liste des champs d'avis, couplés à leur nom (pour l'affichage) @property def avis_all(self): fields = ["avis_lieustage", "avis_pratique", "avis_tourisme"] return [ (AvisLieu._meta.get_field(field).verbose_name, getattr(self, field, "")) for field in fields ]