diff --git a/app/models.py b/app/models.py deleted file mode 100644 index 0205be7bfc..0000000000 --- a/app/models.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: UTF-8 -* - -"""ScoDoc8 models -""" - -# None, at this point -# see auth.models for user/role related models diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000000..d763012ca8 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,57 @@ +# -*- coding: UTF-8 -* + +"""Modèles base de données ScoDoc +XXX version préliminaire ScoDoc8 #sco8 sans département +""" + +CODE_STR_LEN = 16 # chaine pour les codes +SHORT_STR_LEN = 32 # courtes chaine, eg acronymes +APO_CODE_STR_LEN = 16 # nb de car max d'un code Apogée + +from app.models.absences import Absence, AbsenceNotification, BilletAbsence +from app.models.entreprises import ( + Entreprise, + EntrepriseCorrespondant, + EntrepriseContact, +) +from app.models.etudiants import ( + Identite, + Adresse, + Admission, + ItemSuivi, + ItemSuiviTag, + EtudAnnotation, +) +from app.models.events import Scolog, ScolarNews +from app.models.formations import ( + NotesFormation, + NotesUE, + NotesMatiere, + NotesModule, + NotesTag, +) +from app.models.formsemestre import ( + FormSemestre, + NotesFormsemestreEtape, + FormModalite, + NotesFormsemestreUECoef, + NotesFormsemestreUEComputationExpr, + NotesFormsemestreCustomMenu, + NotesFormsemestreInscription, + NotesModuleImpl, + notes_modules_enseignants, + NotesModuleImplInscription, + NotesEvaluation, + NotesSemSet, + notes_semset_formsemestre, +) +from app.models.groups import Partition, GroupDescr, group_membership +from app.models.notes import ( + ScolarEvent, + ScolarFormsemestreValidation, + ScolarAutorisationInscription, + NotesAppreciations, + NotesNotes, + NotesNotesLog, +) +from app.models.preferences import ScoPreferences diff --git a/app/models/absences.py b/app/models/absences.py new file mode 100644 index 0000000000..a1c1e45ff0 --- /dev/null +++ b/app/models/absences.py @@ -0,0 +1,75 @@ +# -*- coding: UTF-8 -* + +"""Gestion des absences +""" + +from app import db +from app.models import APO_CODE_STR_LEN +from app.models import SHORT_STR_LEN +from app.models import CODE_STR_LEN + + +class Absence(db.Model): + """une absence (sur une demi-journée)""" + + __tablename__ = "absences" + id = db.Column(db.Integer, primary_key=True) + etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True) + jour = db.Column(db.Date) + estabs = db.Column(db.Boolean()) + estjust = db.Column(db.Boolean()) + matin = db.Column(db.Boolean()) + # motif de l'absence: + description = db.Column(db.Text()) + entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + # moduleimpid concerne (optionnel): + moduleimpl_id = db.Column( + db.Integer, + db.ForeignKey("notes_moduleimpl.id"), + ) + # XXX TODO: contrainte ajoutée: vérifier suppression du module + # (mettre à NULL sans supprimer) + + +class AbsenceNotification(db.Model): + """Notification d'absence émise""" + + __tablename__ = "absences_notifications" + + id = db.Column(db.Integer, primary_key=True) + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + notification_date = db.Column( + db.DateTime(timezone=True), server_default=db.func.now() + ) + email = db.Column(db.Text()) + nbabs = db.Column(db.Integer) + nbabsjust = db.Column(db.Integer) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + + +class BilletAbsence(db.Model): + """Billet d'absence (signalement par l'étudiant)""" + + __tablename__ = "billet_absence" + + id = db.Column(db.Integer, primary_key=True) + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + index=True, + ) + abs_begin = db.Column(db.DateTime(timezone=True)) + abs_end = db.Column(db.DateTime(timezone=True)) + # raison de l'absence: + description = db.Column(db.Text()) + # False: new, True: processed + etat = db.Column(db.Boolean(), default=False) + entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + # true si l'absence _pourrait_ etre justifiée + justified = db.Column(db.Boolean(), default=False) diff --git a/app/models/entreprises.py b/app/models/entreprises.py new file mode 100644 index 0000000000..0e114baccb --- /dev/null +++ b/app/models/entreprises.py @@ -0,0 +1,69 @@ +# -*- coding: UTF-8 -* + +"""Gestion des absences +""" + +from app import db +from app.models import APO_CODE_STR_LEN +from app.models import SHORT_STR_LEN +from app.models import CODE_STR_LEN + + +class Entreprise(db.Model): + """une entreprise""" + + __tablename__ = "entreprises" + id = db.Column(db.Integer, primary_key=True) + entreprise_id = db.synonym("id") + + nom = db.Column(db.Text) + adresse = db.Column(db.Text) + ville = db.Column(db.Text) + codepostal = db.Column(db.Text) + pays = db.Column(db.Text) + contact_origine = db.Column(db.Text) + secteur = db.Column(db.Text) + note = db.Column(db.Text) + privee = db.Column(db.Text) + localisation = db.Column(db.Text) + # -1 inconnue, 0, 25, 50, 75, 100: + qualite_relation = db.Column(db.Integer) + plus10salaries = db.Column(db.Boolean()) + date_creation = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + + +class EntrepriseCorrespondant(db.Model): + """Personne contact en entreprise""" + + __tablename__ = "entreprise_correspondant" + id = db.Column(db.Integer, primary_key=True) + entreprise_corresp_id = db.synonym("id") + entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id")) + nom = db.Column(db.Text) + prenom = db.Column(db.Text) + civilite = db.Column(db.Text) + fonction = db.Column(db.Text) + phone1 = db.Column(db.Text) + phone2 = db.Column(db.Text) + mobile = db.Column(db.Text) + mail1 = db.Column(db.Text) + mail2 = db.Column(db.Text) + fax = db.Column(db.Text) + note = db.Column(db.Text) + + +class EntrepriseContact(db.Model): + """Evènement (contact) avec une entreprise""" + + __tablename__ = "entreprise_contact" + id = db.Column(db.Integer, primary_key=True) + entreprise_contact_id = db.synonym("id") + date = db.Column(db.DateTime(timezone=True)) + type_contact = db.Column(db.Text) + entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id")) + entreprise_corresp_id = db.Column( + db.Integer, db.ForeignKey("entreprise_correspondant.id") + ) + etudid = db.Column(db.Integer) # sans contrainte pour garder logs après suppression + description = db.Column(db.Text) + enseignant = db.Column(db.Text) diff --git a/app/models/etudiants.py b/app/models/etudiants.py new file mode 100644 index 0000000000..053749bf40 --- /dev/null +++ b/app/models/etudiants.py @@ -0,0 +1,153 @@ +# -*- coding: UTF-8 -* + +"""Définition d'un étudiant + et données rattachées (adresses, annotations, ...) +""" + +from app import db +from app.models import APO_CODE_STR_LEN +from app.models import SHORT_STR_LEN +from app.models import CODE_STR_LEN + + +class Identite(db.Model): + """étudiant""" + + __tablename__ = "identite" + + id = db.Column(db.Integer, primary_key=True) + etudid = db.synonym("id") + + nom = db.Column(db.Text()) + prenom = db.Column(db.Text()) + nom_usuel = db.Column(db.Text()) + # optionnel (si present, affiché à la place du nom) + civilite = db.Column(db.String(1), nullable=False) + __table_args__ = (db.CheckConstraint("civilite IN ('M', 'F', 'X')"),) + + date_naissance = db.Column(db.Date) + lieu_naissance = db.Column(db.Text()) + dept_naissance = db.Column(db.Text()) + nationalite = db.Column(db.Text()) + statut = db.Column(db.Text()) + boursier = db.Column(db.Boolean()) # True si boursier ('O' en ScoDoc7) + photo_filename = db.Column(db.Text()) + code_nip = db.Column(db.String(CODE_STR_LEN), unique=True) + code_ine = db.Column(db.String(CODE_STR_LEN), unique=True) + + +class Adresse(db.Model): + """Adresse d'un étudiant + (le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule) + """ + + __tablename__ = "adresse" + + id = db.Column(db.Integer, primary_key=True) + adresse_id = db.synonym("id") + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + email = db.Column(db.Text()) # mail institutionnel + emailperso = db.Column(db.Text) # email personnel (exterieur) + domicile = db.Column(db.Text) + codepostaldomicile = db.Column(db.Text) + villedomicile = db.Column(db.Text) + paysdomicile = db.Column(db.Text) + telephone = db.Column(db.Text) + telephonemobile = db.Column(db.Text) + fax = db.Column(db.Text) + typeadresse = db.Column(db.Text, default="domicile", nullable=False) + description = db.Column(db.Text) + + +class Admission(db.Model): + """Informations liées à l'admission d'un étudiant""" + + __tablename__ = "admissions" + + id = db.Column(db.Integer, primary_key=True) + adm_id = db.synonym("id") + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + # Anciens champs de ScoDoc7, à revoir pour être plus générique et souple + # notamment dans le cadre du bac 2021 + # de plus, certaines informations liées à APB ne sont plus disponibles + # avec Parcoursup + annee = db.Column(db.Integer) + bac = db.Column(db.Text) + specialite = db.Column(db.Text) + annee_bac = db.Column(db.Integer) + math = db.Column(db.Text) + physique = db.Column(db.Float) + anglais = db.Column(db.Float) + francais = db.Column(db.Float) + # Rang dans les voeux du candidat (inconnu avec APB et PS) + rang = db.Column(db.Integer) + # Qualité et décision du jury d'admission (ou de l'examinateur) + qualite = db.Column(db.Float) + rapporteur = db.Column(db.Text) + decision = db.Column(db.Text) + score = db.Column(db.Float) + commentaire = db.Column(db.Text) + # Lycée d'origine: + nomlycee = db.Column(db.Text) + villelycee = db.Column(db.Text) + codepostallycee = db.Column(db.Text) + codelycee = db.Column(db.Text) + # 'APB', 'APC-PC', 'CEF', 'Direct', '?' (autre) + type_admission = db.Column(db.Text) + # était boursier dans le cycle precedent (lycee) ? + boursier_prec = db.Column(db.Boolean()) + # classement par le jury d'admission (1 à N), + # global (pas celui d'APB si il y a des groupes) + classement = db.Column(db.Integer) + # code du groupe APB + apb_groupe = db.Column(db.Text) + # classement (1..Ngr) par le jury dans le groupe APB + apb_classement_gr = db.Column(db.Integer) + + +# Suivi scolarité / débouchés +class ItemSuivi(db.Model): + __tablename__ = "itemsuivi" + + id = db.Column(db.Integer, primary_key=True) + itemsuivi_id = db.synonym("id") + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + situation = db.Column(db.Text) + + +class ItemSuiviTag(db.Model): + __tablename__ = "itemsuivi_tags" + id = db.Column(db.Integer, primary_key=True) + tag_id = db.synonym("id") + title = db.Column(db.String(SHORT_STR_LEN), nullable=False, unique=True) + + +# Association tag <-> module +itemsuivi_tags_assoc = db.Table( + "itemsuivi_tags_assoc", + db.Column("tag_id", db.Integer, db.ForeignKey("itemsuivi_tags.id")), + db.Column("itemsuivi_id", db.Integer, db.ForeignKey("itemsuivi.id")), +) +# ON DELETE CASCADE ? + + +class EtudAnnotation(db.Model): + """Annotation sur un étudiant""" + + __tablename__ = "etud_annotations" + + id = db.Column(db.Integer, primary_key=True) + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + etudid = db.Column(db.Integer) # sans contrainte pour garder logs après suppression + authenticated_user = db.Column(db.Text) + comment = db.Column(db.Text) diff --git a/app/models/events.py b/app/models/events.py new file mode 100644 index 0000000000..cd95e492e4 --- /dev/null +++ b/app/models/events.py @@ -0,0 +1,35 @@ +# -*- coding: UTF-8 -* + +"""Evenements et logs divers +""" + +from app import db +from app.models import APO_CODE_STR_LEN +from app.models import SHORT_STR_LEN +from app.models import CODE_STR_LEN + + +class Scolog(db.Model): + """Log des actions (journal modif etudiants)""" + + __tablename__ = "scolog" + + id = db.Column(db.Integer, primary_key=True) + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + etudid = db.Column(db.Integer) # sans contrainte pour garder logs après suppression + authenticated_user = db.Column(db.Text) # login, sans contrainte + # zope_remote_addr suppressed + + +class ScolarNews(db.Model): + """Nouvelles pour page d'accueil""" + + __tablename__ = "scolar_news" + id = db.Column(db.Integer, primary_key=True) + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + authenticated_user = db.Column(db.Text) # login, sans contrainte + # type in 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC' + type = db.Column(db.String(SHORT_STR_LEN)) + object = db.Column(db.Integer) # moduleimpl_id, formation_id, formsemestre_id + text = db.Column(db.Text) + url = db.Column(db.Text) diff --git a/app/models/formations.py b/app/models/formations.py new file mode 100644 index 0000000000..9767c1dbca --- /dev/null +++ b/app/models/formations.py @@ -0,0 +1,117 @@ +"""ScoDoc8 models : Formations (hors BUT) +""" +from typing import Any + +from app import db +from app.models import APO_CODE_STR_LEN +from app.models import SHORT_STR_LEN + + +class NotesFormation(db.Model): + """Programme pédagogique d'une formation""" + + __tablename__ = "notes_formations" + __table_args__ = (db.UniqueConstraint("acronyme", "titre", "version"),) + + id = db.Column(db.Integer, primary_key=True) + formation_id = db.synonym("id") + acronyme = db.Column(db.String(SHORT_STR_LEN), nullable=False) + titre = db.Column(db.Text(), nullable=False) + version = db.Column(db.Integer, default=1) + formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False) + type_parcours = db.Column(db.Integer, default=0) + code_specialite = db.Column(db.String(SHORT_STR_LEN)) + + def __init__(self, **kwargs): + super(NotesFormation, self).__init__(**kwargs) + if self.formation_code is None: + # génère formation_code à la création + self.formation_code = f"FCOD{self.id:03d}" + + +class NotesUE(db.Model): + """Unité d'Enseignement""" + + __tablename__ = "notes_ue" + + id = db.Column(db.Integer, primary_key=True) + ue_id = db.synonym("id") + formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id")) + acronyme = db.Column(db.String(SHORT_STR_LEN), nullable=False) + numero = db.Column(db.Integer) # ordre de présentation + titre = db.Column(db.Text()) + # Type d'UE: 0 normal ("fondamentale"), 1 "sport", 2 "projet et stage (LP)", + # 4 "élective" + type = db.Column(db.Integer, default=0) + ue_code = db.Column(db.String(SHORT_STR_LEN), nullable=False) + ects = db.Column(db.Float) # nombre de credits ECTS + is_external = db.Column(db.Boolean(), nullable=False, default=False) + # id de l'element pedagogique Apogee correspondant: + code_apogee = db.Column(db.String(APO_CODE_STR_LEN)) + # coef UE, utilise seulement si l'option use_ue_coefs est activée: + coefficient = db.Column(db.Float) + + def __init__(self, **kwargs): + super(NotesUE, self).__init__(**kwargs) + if self.ue_code is None: + # génère code à la création + self.ue_code = f"UCOD{self.ue_id:03d}" + + +class NotesMatiere(db.Model): + """Matières: regroupe les modules d'une UE + La matière a peu d'utilité en dehors de la présentation des modules + d'une UE. + """ + + __tablename__ = "notes_matieres" + __table_args__ = (db.UniqueConstraint("ue_id", "titre"),) + + id = db.Column(db.Integer, primary_key=True) + matiere_id = db.synonym("id") + ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id")) + titre = db.Column(db.Text()) + numero = db.Column(db.Integer) # ordre de présentation + + +class NotesModule(db.Model): + """Module""" + + __tablename__ = "notes_modules" + + id = db.Column(db.Integer, primary_key=True) + module_id = db.synonym("id") + titre = db.Column(db.Text()) + abbrev = db.Column(db.Text()) # nom court + code = db.Column(db.String(SHORT_STR_LEN), nullable=False) + heures_cours = db.Column(db.Float) + heures_td = db.Column(db.Float) + heures_tp = db.Column(db.Float) + coefficient = db.Column(db.Float) # coef PPN + ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id")) + formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id")) + matiere_id = db.Column(db.Integer, db.ForeignKey("notes_matieres.id")) + # pas un id mais le numéro du semestre: 1, 2, ... + semestre_id = db.Column(db.Integer, nullable=False, default=1) + numero = db.Column(db.Integer) # ordre de présentation + # id de l'element pedagogique Apogee correspondant: + code_apogee = db.Column(db.String(APO_CODE_STR_LEN)) + module_type = db.Column(db.Integer) # NULL ou 0:defaut, 1: malus (NOTES_MALUS) + + +class NotesTag(db.Model): + """Tag sur un module""" + + __tablename__ = "notes_tags" + + id = db.Column(db.Integer, primary_key=True) + tag_id = db.synonym("id") + title = db.Column(db.String(SHORT_STR_LEN), nullable=False, unique=True) + + +# Association tag <-> module +notes_modules_tags = db.Table( + "notes_modules_tags", + db.Column("tag_id", db.Integer, db.ForeignKey("notes_tags.id")), + db.Column("module_id", db.Integer, db.ForeignKey("notes_modules.id")), +) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py new file mode 100644 index 0000000000..7e89ba7fbd --- /dev/null +++ b/app/models/formsemestre.py @@ -0,0 +1,284 @@ +# -*- coding: UTF-8 -* + +"""ScoDoc8 models +""" +from typing import Any + +from app import db +from app.models import APO_CODE_STR_LEN +from app.models import SHORT_STR_LEN +from app.models import CODE_STR_LEN + + +class FormSemestre(db.Model): + """Mise en oeuvre d'un semestre de formation + was notes_formsemestre + """ + + __tablename__ = "notes_formsemestre" + + id = db.Column(db.Integer, primary_key=True) + formsemestre_id = db.synonym("id") + formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id")) + semestre_id = db.Column(db.Integer, nullable=False, default=1) + titre = db.Column(db.Text()) + date_debut = db.Column(db.Date()) + date_fin = db.Column(db.Date()) + etat = db.Column(db.Boolean(), nullable=False, default=True) # False si verrouillé + modalite = db.Column(db.String(16), db.ForeignKey("form_modalite.modalite")) + # gestion compensation sem DUT: + gestion_compensation = db.Column(db.Boolean(), nullable=False, default=False) + # ne publie pas le bulletin XML: + bul_hide_xml = db.Column(db.Boolean(), nullable=False, default=False) + # semestres decales (pour gestion jurys): + gestion_semestrielle = db.Column(db.Boolean(), nullable=False, default=False) + # couleur fond bulletins HTML: + bul_bgcolor = db.Column(db.String(SHORT_STR_LEN), default="white") + # autorise resp. a modifier semestre: + resp_can_edit = db.Column(db.Boolean(), nullable=False, default=False) + # autorise resp. a modifier slt les enseignants: + resp_can_change_ens = db.Column(db.Boolean(), nullable=False, default=True) + # autorise les ens a creer des evals: + ens_can_edit_eval = db.Column(db.Boolean(), nullable=False, default=False) + # code element semestre Apogee, eg VRTW1 ou V2INCS4,V2INLS4 + elt_sem_apo = db.Column(db.String(APO_CODE_STR_LEN)) + # code element annee Apogee, eg VRT1A ou V2INLA,V2INCA + elt_annee_apo = db.Column(db.String(APO_CODE_STR_LEN)) + + etapes = db.relationship( + "NotesFormsemestreEtape", cascade="all,delete", backref="notes_formsemestre" + ) + + def __init__(self, **kwargs): + super(FormSemestre, self).__init__(**kwargs) + if self.modalite is None: + self.modalite = FormModalite.DEFAULT_MODALITE + + +# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre +notes_formsemestre_responsables = db.Table( + "notes_formsemestre_responsables", + db.Column( + "formsemestre_id", + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ), + db.Column("responsable_id", db.Integer, db.ForeignKey("user.id")), +) + + +class NotesFormsemestreEtape(db.Model): + """Étape Apogée associées au semestre""" + + __tablename__ = "notes_formsemestre_etapes" + id = db.Column(db.Integer, primary_key=True) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + etape_apo = db.Column(db.String(APO_CODE_STR_LEN)) + + +class FormModalite(db.Model): + """Modalités de formation, utilisées pour la présentation + (grouper les semestres, générer des codes, etc.) + """ + + id = db.Column(db.Integer, primary_key=True) + modalite = db.Column(db.String(16), unique=True, index=True) # code + titre = db.Column(db.Text()) # texte explicatif + # numero = ordre de presentation) + numero = db.Column(db.Integer) + + DEFAULT_MODALITE = "FI" + + @staticmethod + def insert_modalites(): + """Create default modalities""" + numero = 0 + for (code, titre) in ( + (FormModalite.DEFAULT_MODALITE, "Formation Initiale"), + ("FAP", "Apprentissage"), + ("FC", "Formation Continue"), + ("DEC", "Formation Décalées"), + ("LIC", "Licence"), + ("CPRO", "Contrats de Professionnalisation"), + ("DIST", "À distance"), + ("ETR", "À l'étranger"), + ("EXT", "Extérieur"), + ("OTHER", "Autres formations"), + ): + modalite = FormModalite.query.filter_by(modalite=code).first() + if modalite is None: + modalite = FormModalite(modalite=code, titre=titre, numero=numero) + db.session.add(modalite) + numero += 1 + db.session.commit() + + +class NotesFormsemestreUECoef(db.Model): + """Coef des UE capitalisees arrivant dans ce semestre""" + + __tablename__ = "notes_formsemestre_uecoef" + __table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id"),) + + id = db.Column(db.Integer, primary_key=True) + formsemestre_uecoef_id = db.synonym("id") + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + ue_id = db.Column( + db.Integer, + db.ForeignKey("notes_ue.id"), + ) + coefficient = db.Column(db.Float, nullable=False) + + +class NotesFormsemestreUEComputationExpr(db.Model): + """Formules utilisateurs pour calcul moyenne UE""" + + __tablename__ = "notes_formsemestre_ue_computation_expr" + __table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id"),) + + id = db.Column(db.Integer, primary_key=True) + notes_formsemestre_ue_computation_expr_id = db.synonym("id") + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + ue_id = db.Column( + db.Integer, + db.ForeignKey("notes_ue.id"), + ) + # formule de calcul moyenne + computation_expr = db.Column(db.Text()) + + +class NotesFormsemestreCustomMenu(db.Model): + """Menu custom associe au semestre""" + + __tablename__ = "notes_formsemestre_custommenu" + + id = db.Column(db.Integer, primary_key=True) + custommenu_id = db.synonym("id") + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + title = db.Column(db.Text()) + url = db.Column(db.Text()) + idx = db.Column(db.Integer, default=0) # rang dans le menu + + +class NotesFormsemestreInscription(db.Model): + """Inscription à un semestre de formation""" + + __tablename__ = "notes_formsemestre_inscription" + __table_args__ = (db.UniqueConstraint("formsemestre_id", "etudid"),) + + id = db.Column(db.Integer, primary_key=True) + formsemestre_inscription_id = db.synonym("id") + + etudid = db.Column(db.Integer, db.ForeignKey("identite.id")) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + # I inscrit, D demission en cours de semestre, DEF si "defaillant" + etat = db.Column(db.String(CODE_STR_LEN)) + # etape apogee d'inscription (experimental 2020) + etape = db.Column(db.String(APO_CODE_STR_LEN)) + + +class NotesModuleImpl(db.Model): + """Mise en oeuvre d'un module pour une annee/semestre""" + + __tablename__ = "notes_moduleimpl" + __table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),) + + id = db.Column(db.Integer, primary_key=True) + moduleimpl_id = db.synonym("id") + module_id = db.Column( + db.Integer, + db.ForeignKey("notes_modules.id"), + ) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id")) + # formule de calcul moyenne: + computation_expr = db.Column(db.Text()) + + +# Enseignants (chargés de TD ou TP) d'un moduleimpl +notes_modules_enseignants = db.Table( + "notes_modules_enseignants", + db.Column( + "moduleimpl_id", + db.Integer, + db.ForeignKey("notes_moduleimpl.id"), + ), + db.Column("ens_id", db.Integer, db.ForeignKey("user.id")), +) +# XXX il manque probablement une relation pour gérer cela + + +class NotesModuleImplInscription(db.Model): + """Inscription à un module (etudiants,moduleimpl)""" + + __tablename__ = "notes_moduleimpl_inscription" + + id = db.Column(db.Integer, primary_key=True) + moduleimpl_inscription_id = db.synonym("id") + db.Column( + "moduleimpl_id", + db.Integer, + db.ForeignKey("notes_moduleimpl.moduleimpl_id"), + ) + etudid = db.Column(db.Integer, db.ForeignKey("identite.id")) + + +class NotesEvaluation(db.Model): + """Evaluation (contrôle, examen, ...)""" + + __tablename__ = "notes_evaluation" + + id = db.Column(db.Integer, primary_key=True) + evaluation_id = db.synonym("id") + jour = db.Column(db.Date) + heure_debut = db.Column(db.Time) + heure_fin = db.Column(db.Time) + description = db.Column(db.Text) + note_max = db.Column(db.Float) + coefficient = db.Column(db.Float) + visibulletin = db.Column(db.Boolean, nullable=False, default=True) + publish_incomplete = db.Column(db.Boolean, nullable=False, default=False) + # type d'evaluation: False normale, True rattrapage: + evaluation_type = db.Column(db.Boolean, nullable=False, default=False) + # ordre de presentation (par défaut, le plus petit numero + # est la plus ancienne eval): + numero = db.Column(db.Integer) + + +class NotesSemSet(db.Model): + """semsets: ensemble de formsemestres pour exports Apogée""" + + __tablename__ = "notes_semset" + + id = db.Column(db.Integer, primary_key=True) + semset_id = db.synonym("id") + title = db.Column(db.Text) + annee_scolaire = db.Column(db.Integer, nullable=True, default=None) + # periode: 0 (année), 1 (Simpair), 2 (Spair) + sem_id = db.Column(db.Integer, nullable=True, default=None) + + +# Association: +notes_semset_formsemestre = db.Table( + "notes_semset_formsemestre", + db.Column("formsemestre_id", db.Integer, db.ForeignKey("notes_formsemestre.id")), + db.Column("semset_id", db.Integer, db.ForeignKey("notes_semset.id")), + db.UniqueConstraint("formsemestre_id", "semset_id"), +) diff --git a/app/models/groups.py b/app/models/groups.py new file mode 100644 index 0000000000..0fd6c756e4 --- /dev/null +++ b/app/models/groups.py @@ -0,0 +1,60 @@ +# -*- coding: UTF-8 -* + +"""Groups & partitions +""" +from typing import Any + +from app import db +from app.models import APO_CODE_STR_LEN +from app.models import SHORT_STR_LEN +from app.models import CODE_STR_LEN + + +class Partition(db.Model): + """Partition: découpage d'une promotion en groupes""" + + __table_args__ = (db.UniqueConstraint("formsemestre_id", "partition_name"),) + + id = db.Column(db.Integer, primary_key=True) + partition_id = db.synonym("id") + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + # "TD", "TP", ... (NULL for 'all') + partition_name = db.Column(db.String(SHORT_STR_LEN)) + # numero = ordre de presentation) + numero = db.Column(db.Integer) + bul_show_rank = db.Column(db.Boolean(), nullable=False, default=False) + show_in_lists = db.Column(db.Boolean(), nullable=False, default=True) + + def __init__(self, **kwargs): + super(Partition, self).__init__(**kwargs) + if self.numero is None: + # génère numero à la création + last_partition = Partition.query.order_by(Partition.numero.desc()).first() + if last_partition: + self.numero = last_partition.numero + 1 + else: + self.numero = 1 + + +class GroupDescr(db.Model): + """Description d'un groupe d'une partition""" + + __tablename__ = "group_descr" + __table_args__ = (db.UniqueConstraint("partition_id", "group_name"),) + + id = db.Column(db.Integer, primary_key=True) + group_id = db.synonym("id") + partition_id = db.Column(db.Integer, db.ForeignKey("partition.id")) + # "A", "C2", ... (NULL for 'all'): + group_name = db.Column(db.String(SHORT_STR_LEN)) + + +group_membership = db.Table( + "group_membership", + db.Column("etudid", db.Integer, db.ForeignKey("identite.id")), + db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")), + db.UniqueConstraint("etudid", "group_id"), +) diff --git a/app/models/notes.py b/app/models/notes.py new file mode 100644 index 0000000000..8abe9cae41 --- /dev/null +++ b/app/models/notes.py @@ -0,0 +1,160 @@ +# -*- coding: UTF-8 -* + +"""Notes, décisions de jury, évènements scolaires +""" + +from app import db +from app.models import APO_CODE_STR_LEN +from app.models import SHORT_STR_LEN +from app.models import CODE_STR_LEN + + +class ScolarEvent(db.Model): + """Evenement dans le parcours scolaire d'un étudiant""" + + __tablename__ = "scolar_events" + id = db.Column(db.Integer, primary_key=True) + event_id = db.synonym("id") + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + ue_id = db.Column( + db.Integer, + db.ForeignKey("notes_ue.id"), + ) + # 'CREATION', 'INSCRIPTION', 'DEMISSION', + # 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM' + # 'ECHEC_SEM' + # 'UTIL_COMPENSATION' + event_type = db.Column(db.String(SHORT_STR_LEN)) + # Semestre compensé par formsemestre_id: + comp_formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + + +class ScolarFormsemestreValidation(db.Model): + """Décisions de jury""" + + __tablename__ = "scolar_formsemestre_validation" + # Assure unicité de la décision: + __table_args__ = (db.UniqueConstraint("etudid", "formsemestre_id", "ue_id"),) + + id = db.Column(db.Integer, primary_key=True) + formsemestre_validation_id = db.synonym("id") + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + ue_id = db.Column( + db.Integer, + db.ForeignKey("notes_ue.id"), + ) + code = db.Column(db.String(CODE_STR_LEN), nullable=False) + # NULL pour les UE, True|False pour les semestres: + assidu = db.Column(db.Boolean) + event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + # NULL sauf si compense un semestre: + compense_formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + moy_ue = db.Column(db.Float) + # (normalement NULL) indice du semestre, utile seulement pour + # UE "antérieures" et si la formation définit des UE utilisées + # dans plusieurs semestres (cas R&T IUTV v2) + semestre_id = db.Column(db.Integer) + # Si UE validée dans le cursus d'un autre etablissement + is_external = db.Column(db.Boolean, default=False) + + +class ScolarAutorisationInscription(db.Model): + """Autorisation d'inscription dans un semestre""" + + __tablename__ = "scolar_autorisation_inscription" + id = db.Column(db.Integer, primary_key=True) + autorisation_inscription_id = db.synonym("id") + + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + formation_code = db.Column(db.String(CODE_STR_LEN), nullable=False) + # semestre ou on peut s'inscrire: + semestre_id = db.Column(db.Integer) + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + origin_formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + + +class NotesAppreciations(db.Model): + """Appréciations sur bulletins""" + + id = db.Column(db.Integer, primary_key=True) + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + index=True, + ) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + author = db.Column(db.Text) # login, sans contrainte + comment = db.Column(db.Text) # texte libre + + +class NotesNotes(db.Model): + """Une note""" + + __tablename__ = "notes_notes" + __table_args__ = (db.UniqueConstraint("etudid", "evaluation_id"),) + id = db.Column(db.Integer, primary_key=True) + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + evaluation_id = db.Column( + db.Integer, db.ForeignKey("notes_evaluation.id"), index=True + ) + value = db.Column(db.Float) + # infos sur saisie de cette note: + comment = db.Column(db.Text) # texte libre + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + uid = db.Column(db.Integer, db.ForeignKey("user.id")) + + +class NotesNotesLog(db.Model): + """Historique des modifs sur notes (anciennes entrees de notes_notes)""" + + __tablename__ = "notes_notes_log" + id = db.Column(db.Integer, primary_key=True) + + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id"), + ) + evaluation_id = db.Column( + db.Integer, + # db.ForeignKey("notes_evaluation.id"), + index=True, + ) + value = db.Column(db.Float) + # infos sur saisie de cette note: + comment = db.Column(db.Text) # texte libre + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + uid = db.Column(db.Integer, db.ForeignKey("user.id")) diff --git a/app/models/preferences.py b/app/models/preferences.py new file mode 100644 index 0000000000..7d4f80b008 --- /dev/null +++ b/app/models/preferences.py @@ -0,0 +1,18 @@ +# -*- coding: UTF-8 -* + +"""Model : preferences +""" + +from app import db + + +class ScoPreferences(db.Model): + """ScoDoc preferences""" + + __tablename__ = "sco_prefs" + id = db.Column(db.Integer, primary_key=True) + pref_id = db.synonym("id") + dept = db.Column(db.String(16), index=True) + name = db.Column(db.String(128), nullable=False) + value = db.Column(db.Text()) + formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id")) diff --git a/app/scodoc/sco_modalites.py b/app/scodoc/sco_modalites.py index b178923687..8fe8354308 100644 --- a/app/scodoc/sco_modalites.py +++ b/app/scodoc/sco_modalites.py @@ -102,7 +102,7 @@ def do_modalite_delete(context, oid, REQUEST=None): _modaliteEditor.delete(cnx, oid) -def do_modalite_edit(context, *args, **kw): +def do_modalite_edit(context, *args, **kw): # unused "edit a modalite" cnx = ndb.GetDBConnexion() # check diff --git a/misc/createtables.sql b/misc/createtables.sql index 1b9333d308..e5ca1ab7b2 100644 --- a/misc/createtables.sql +++ b/misc/createtables.sql @@ -202,34 +202,34 @@ CREATE TABLE etud_annotations ( ) WITH OIDS; -- ------------ Nouvelle gestion des absences ------------ -CREATE SEQUENCE abs_idgen; -CREATE FUNCTION abs_newid( text ) returns text as ' - select $1 || to_char( nextval(''abs_idgen''), ''FM999999999'' ) - as result; - ' language SQL; +-- CREATE SEQUENCE abs_idgen; +-- CREATE FUNCTION abs_newid( text ) returns text as ' +-- select $1 || to_char( nextval(''abs_idgen''), ''FM999999999'' ) +-- as result; +-- ' language SQL; -CREATE TABLE abs_absences ( - absid text default abs_newid('AB') PRIMARY KEY, - etudid character(32), - abs_begin timestamp with time zone, - abs_end timestamp with time zone -) WITH OIDS; +-- CREATE TABLE abs_absences ( +-- absid text default abs_newid('AB') PRIMARY KEY, +-- etudid character(32), +-- abs_begin timestamp with time zone, +-- abs_end timestamp with time zone +-- ) WITH OIDS; -CREATE TABLE abs_presences ( - absid text default abs_newid('PR') PRIMARY KEY, - etudid character(32), - abs_begin timestamp with time zone, - abs_end timestamp with time zone -) WITH OIDS; +-- CREATE TABLE abs_presences ( +-- absid text default abs_newid('PR') PRIMARY KEY, +-- etudid character(32), +-- abs_begin timestamp with time zone, +-- abs_end timestamp with time zone +-- ) WITH OIDS; -CREATE TABLE abs_justifs ( - absid text default abs_newid('JU') PRIMARY KEY, - etudid character(32), - abs_begin timestamp with time zone, - abs_end timestamp with time zone, - category text, - description text -) WITH OIDS; +-- CREATE TABLE abs_justifs ( +-- absid text default abs_newid('JU') PRIMARY KEY, +-- etudid character(32), +-- abs_begin timestamp with time zone, +-- abs_end timestamp with time zone, +-- category text, +-- description text +-- ) WITH OIDS; @@ -600,12 +600,12 @@ CREATE TABLE scolar_events ( etudid text, event_date timestamp default now(), formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id), - ue_id text REFERENCES notes_ue(ue_id), + ue_id text REFERENCES notes_ue(ue_id), event_type text, -- 'CREATION', 'INSCRIPTION', 'DEMISSION', -- 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM' -- 'ECHEC_SEM' -- 'UTIL_COMPENSATION' - comp_formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id) + comp_formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id) -- semestre compense par formsemestre_id ) WITH OIDS; diff --git a/scodoc.py b/scodoc.py index 9f6a610230..ac691a2be2 100755 --- a/scodoc.py +++ b/scodoc.py @@ -19,6 +19,9 @@ from flask.cli import with_appcontext from app import create_app, cli, db from app.auth.models import User, Role, UserRole + +from app import models +from app.models import ScoPreferences, FormSemestre, FormModalite from app.views import notes, scolar, absences import app.utils as utils @@ -35,11 +38,17 @@ def make_shell_context(): from flask_login import login_user, logout_user, current_user admin_role = Role.query.filter_by(name="SuperAdmin").first() - admin = ( - User.query.join(UserRole) - .filter((UserRole.user_id == User.id) & (UserRole.role_id == admin_role.id)) - .first() - ) + if admin_role: + admin = ( + User.query.join(UserRole) + .filter((UserRole.user_id == User.id) & (UserRole.role_id == admin_role.id)) + .first() + ) + else: + click.echo( + "Warning: user database not initialized !\n (use: flask user-db-init)" + ) + admin = None return { "db": db, @@ -58,6 +67,10 @@ def make_shell_context(): "logout_user": logout_user, "admin": admin, "ctx": app.test_request_context(), + # "FormModalite": FormModalite, + # "ScoPreferences": ScoPreferences, + # "FormSemestre": FormSemestre, + "models": models, }