diff --git a/app/but/import_refcomp.py b/app/but/import_refcomp.py index 0f97cd958..ae44a34ce 100644 --- a/app/but/import_refcomp.py +++ b/app/but/import_refcomp.py @@ -4,7 +4,6 @@ # See LICENSE ############################################################################## from xml.etree import ElementTree -from typing import TextIO import sqlalchemy diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 5170875d7..654a1ae36 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -70,6 +70,7 @@ class ResultatsSemestre(ResultatsCache): self.etud_moy_gen: pd.Series = None self.etud_moy_gen_ranks = {} self.etud_moy_gen_ranks_int = {} + self.moy_gen_rangs_by_group = None # virtual self.modimpl_inscr_df: pd.DataFrame = None "Inscriptions: row etudid, col modimlpl_id" self.modimpls_results: ModuleImplResults = None @@ -824,17 +825,25 @@ class ResultatsSemestre(ResultatsCache): self.formsemestre.id ) first_partition = True + col_order = 10 for partition in partitions: cid = f"part_{partition['partition_id']}" + rg_cid = cid + "_rg" # rang dans la partition titles[cid] = partition["partition_name"] if first_partition: klass = "partition" else: klass = "partition partition_aux" titles[f"_{cid}_class"] = klass - titles[f"_{cid}_col_order"] = 10 + titles[f"_{cid}_col_order"] = col_order + titles[f"_{rg_cid}_col_order"] = col_order + 1 + col_order += 2 + if partition["bul_show_rank"]: + titles[rg_cid] = f"Rg {partition['partition_name']}" + titles[f"_{rg_cid}_class"] = "partition_rangs" partition_etud_groups = partitions_etud_groups[partition["partition_id"]] for row in rows: + group = None # group (dict) de l'étudiant dans cette partition # dans NotesTableCompat, à revoir etud_etat = self.get_etud_etat(row["etudid"]) if etud_etat == "D": @@ -847,8 +856,17 @@ class ResultatsSemestre(ResultatsCache): group = partition_etud_groups.get(row["etudid"]) gr_name = group["group_name"] if group else "" if gr_name: - row[f"{cid}"] = gr_name + row[cid] = gr_name row[f"_{cid}_class"] = klass + # Rangs dans groupe + if ( + partition["bul_show_rank"] + and (group is not None) + and (group["id"] in self.moy_gen_rangs_by_group) + ): + rang = self.moy_gen_rangs_by_group[group["id"]][0] + row[rg_cid] = rang.get(row["etudid"], "") + first_partition = False def _recap_add_evaluations( diff --git a/app/models/absences.py b/app/models/absences.py index 830d46f9e..405ea6bff 100644 --- a/app/models/absences.py +++ b/app/models/absences.py @@ -11,7 +11,9 @@ class Absence(db.Model): __tablename__ = "absences" id = db.Column(db.Integer, primary_key=True) - etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True) + etudid = db.Column( + db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True + ) jour = db.Column(db.Date) estabs = db.Column(db.Boolean()) estjust = db.Column(db.Boolean()) @@ -50,7 +52,7 @@ class AbsenceNotification(db.Model): id = db.Column(db.Integer, primary_key=True) etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) notification_date = db.Column( db.DateTime(timezone=True), server_default=db.func.now() diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 42ef1778f..6c9504e24 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -436,7 +436,7 @@ class Adresse(db.Model): adresse_id = db.synonym("id") etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) email = db.Column(db.Text()) # mail institutionnel emailperso = db.Column(db.Text) # email personnel (exterieur) @@ -470,7 +470,7 @@ class Admission(db.Model): adm_id = db.synonym("id") etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) # Anciens champs de ScoDoc7, à revoir pour être plus générique et souple # notamment dans le cadre du bac 2021 @@ -540,7 +540,7 @@ class ItemSuivi(db.Model): itemsuivi_id = db.synonym("id") etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) situation = db.Column(db.Text) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 2f1f92593..90461c988 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -323,6 +323,25 @@ class FormSemestre(db.Model): return "" return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape])) + def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]: + """Calcule la liste des regroupements cohérents d'UE impliquant ce + formsemestre. + Pour une année donnée: l'étudiant est inscrit dans ScoDoc soit dans le semestre + impair, soit pair, soit les deux (il est rare mais pas impossible d'avoir une + inscription seulement en semestre pair, par exemple suite à un transfert ou un + arrêt temporaire du cursus). + + 1. Déterminer l'*autre* formsemestre: semestre précédent ou suivant de la même + année, formation compatible (même référentiel de compétence) dans lequel + l'étudiant est inscrit. + + 2. Construire les couples d'UE (regroupements cohérents): apparier les UE qui + ont le même `ApcParcoursNiveauCompetence`. + """ + if not self.formation.is_apc(): + return [] + raise NotImplementedError() # XXX + def responsables_str(self, abbrev_prenom=True) -> str: """chaîne "J. Dupond, X. Martin" ou "Jacques Dupond, Xavier Martin" @@ -614,7 +633,9 @@ class FormSemestreInscription(db.Model): id = db.Column(db.Integer, primary_key=True) formsemestre_inscription_id = db.synonym("id") - etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True) + etudid = db.Column( + db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True + ) formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), @@ -634,11 +655,16 @@ class FormSemestreInscription(db.Model): ) # I inscrit, D demission en cours de semestre, DEF si "defaillant" etat = db.Column(db.String(CODE_STR_LEN), index=True) - # etape apogee d'inscription (experimental 2020) + # Etape Apogée d'inscription (ajout 2020) etape = db.Column(db.String(APO_CODE_STR_LEN)) + # Parcours (pour les BUT) + parcour_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), index=True) + parcour = db.relationship(ApcParcours) def __repr__(self): - return f"<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={self.formsemestre_id} etat={self.etat}>" + return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={ + self.formsemestre_id} etat={self.etat} { + ('parcours='+str(self.parcour)) if self.parcour else ''}>""" class NotesSemSet(db.Model): diff --git a/app/models/groups.py b/app/models/groups.py index 4c64ad543..1d24b60c0 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -106,7 +106,7 @@ class GroupDescr(db.Model): group_membership = db.Table( "group_membership", - db.Column("etudid", db.Integer, db.ForeignKey("identite.id")), + db.Column("etudid", db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE")), db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")), db.UniqueConstraint("etudid", "group_id"), ) @@ -116,5 +116,5 @@ group_membership = db.Table( # __tablename__ = "group_membership" # __table_args__ = (db.UniqueConstraint("etudid", "group_id"),) # id = db.Column(db.Integer, primary_key=True) -# etudid = db.Column(db.Integer, db.ForeignKey("identite.id")) +# etudid = db.Column(db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE")) # group_id = db.Column(db.Integer, db.ForeignKey("group_descr.id")) diff --git a/app/models/notes.py b/app/models/notes.py index 6da4ef5d6..f88f87287 100644 --- a/app/models/notes.py +++ b/app/models/notes.py @@ -17,7 +17,7 @@ class BulAppreciations(db.Model): date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), index=True, ) formsemestre_id = db.Column( @@ -36,7 +36,7 @@ class NotesNotes(db.Model): id = db.Column(db.Integer, primary_key=True) etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) evaluation_id = db.Column( db.Integer, db.ForeignKey("notes_evaluation.id"), index=True @@ -56,7 +56,7 @@ class NotesNotesLog(db.Model): etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) evaluation_id = db.Column( db.Integer, diff --git a/app/models/validations.py b/app/models/validations.py index 0bf487f3a..64bdaef80 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -19,7 +19,7 @@ class ScolarFormSemestreValidation(db.Model): formsemestre_validation_id = db.synonym("id") etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), index=True, ) formsemestre_id = db.Column( @@ -66,7 +66,7 @@ class ScolarAutorisationInscription(db.Model): etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False) # semestre ou on peut s'inscrire: @@ -86,7 +86,7 @@ class ScolarEvent(db.Model): event_id = db.synonym("id") etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) formsemestre_id = db.Column( diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index aabfaddce..64a7da118 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -244,7 +244,11 @@ def formation_edit(formation_id=None, create=False): return ( "\n".join(H) + tf_error_message( - "Valeurs incorrectes: il existe déjà une formation avec même titre, acronyme et version." + f"""Valeurs incorrectes: il existe déjà une formation avec même titre, + acronyme et version. + """ ) + tf[1] + html_sco_header.sco_footer() diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 381261450..04a2cd975 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -352,7 +352,7 @@ def module_edit( title = f"""Création {object_name} dans la formation {formation.acronyme}""" else: - page_title = "Modification du module {module.code or module.titre or ''}" + page_title = f"Modification du module {module.code or module.titre or ''}" title = f"""Modification du module {module.code or ''} {module.titre or ''} (formation {formation.acronyme}, version {formation.version}) """ diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index e377dabe0..8eae60e04 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -1009,7 +1009,7 @@ def edit_partition_form(formsemestre_id=None): """ ) - if formsemestre.formation.is_apc() and "Parcours" not in ( + if formsemestre.formation.is_apc() and scu.PARTITION_PARCOURS not in ( p["partition_name"] for p in partitions ): # propose création partition "Parcours" @@ -1068,9 +1068,7 @@ def partition_set_attr(partition_id, attr, value): partition[attr] = value partitionEditor.edit(cnx, partition) # invalid bulletin cache - sco_cache.invalidate_formsemestre( - pdfonly=True, formsemestre_id=partition["formsemestre_id"] - ) + sco_cache.invalidate_formsemestre(formsemestre_id=partition["formsemestre_id"]) return "enregistré" diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index c1e7dca27..e8c295854 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -111,6 +111,8 @@ MODULE_TYPE_NAMES = { None: "Module", } +PARTITION_PARCOURS = "Parcours" + MALUS_MAX = 20.0 MALUS_MIN = -20.0 diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 36426b508..fd5068e60 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -15,13 +15,23 @@ $(function () { }, { name: "toggle_partitions", - text: "Toutes les partitions", + text: "Montrer groupes", action: function (e, dt, node, config) { let visible = dt.columns(".partition_aux").visible()[0]; dt.columns(".partition_aux").visible(!visible); - dt.buttons('toggle_partitions:name').text(visible ? "Toutes les partitions" : "Cacher les partitions"); + dt.buttons('toggle_partitions:name').text(visible ? "Montrer groupes" : "Cacher les groupes"); } - }]; + }, + { + name: "toggle_partitions_rangs", + text: "Rangs groupes", + action: function (e, dt, node, config) { + let rangs_visible = dt.columns(".partition_rangs").visible()[0]; + dt.columns(".partition_rangs").visible(!rangs_visible); + dt.buttons('toggle_partitions_rangs:name').text(rangs_visible ? "Rangs groupes" : "Cacher rangs groupes"); + } + }, + ]; if (!$('table.table_recap').hasClass("jury")) { buttons.push( $('table.table_recap').hasClass("apc") ? @@ -95,7 +105,7 @@ $(function () { "columnDefs": [ { // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides - targets: ["codes", "identite_detail", "partition_aux", "admission", "col_empty"], + targets: ["codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty"], visible: false, }, { diff --git a/app/templates/config_codes_decisions.html b/app/templates/config_codes_decisions.html index 0c2f32b24..5f92aa8d3 100644 --- a/app/templates/config_codes_decisions.html +++ b/app/templates/config_codes_decisions.html @@ -6,12 +6,12 @@
Ces codes (ADM, AJ, ...) sont utilisés pour représenter les décisions de jury -et les validations de semestres ou d'UE. les valeurs indiquées ici sont utilisées -dans les exports Apogée. -
-
Ne les modifier que si vous savez ce que vous faites ! -
+Ces codes (ADM, AJ, ...) sont utilisés pour représenter les décisions de jury + et les validations de semestres ou d'UE. + Les valeurs indiquées ici sont utilisées dans les exports Apogée. +
+
Ne les modifier que si vous savez ce que vous faites ! +