diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 4eb738cf2c140a43812b0174c151f302cb5c4c11..d7c41030f14a44daeed715c42b434f8a2f802c3b 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -1,7 +1,6 @@ # -*- coding: UTF-8 -* -"""ScoDoc models: evaluations -""" +"""ScoDoc models: evaluations""" import datetime from operator import attrgetter @@ -548,6 +547,25 @@ class Evaluation(models.ScoDocModel): .all() ) + def get_bounds(self) -> tuple[int, int]: + """Calcule les notes min et max pour une évaluation.""" + note_max = self.note_max or 0.0 + module = self.moduleimpl.module + if module.module_type in ( + scu.ModuleType.STANDARD, + scu.ModuleType.RESSOURCE, + scu.ModuleType.SAE, + ): + if self.evaluation_type == Evaluation.EVALUATION_BONUS: + note_min, note_max = -20, 20 + else: + note_min = scu.NOTES_MIN + elif module.module_type == scu.ModuleType.MALUS: + note_min = -20.0 + else: + raise ValueError("Invalid module type") # bug + return note_min, note_max + class EvaluationUEPoids(models.ScoDocModel): """Poids des évaluations (BUT) diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index eeac44d88533f2e24010eb2967c1b7da973fd962..9e45d0ba770377a92b5cf2693b25c04fc69432fa 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -108,7 +108,7 @@ def _menu_scolarite( and authuser.has_permission(Permission.EtudInscrit) and not locked ) - note_enabled = etat_inscription == "I" and ( + note_enabled = etat_inscription == scu.INSCRIT and ( etat_inscription != scu.DEMISSION and ( authuser.has_permission(Permission.EditAllNotes) @@ -171,7 +171,7 @@ def _menu_scolarite( "enabled": authuser.has_permission(Permission.EtudInscrit), }, { - "title": "Gérer les notes", + "title": "Editer toutes les notes", "endpoint": "notes.form_saisie_notes_par_etu", "args": {"etu_id": etudid, "semestre_id": formsemestre.id}, "enabled": note_enabled, diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index ea31e95380446ac4ad6ddfc9783a45590fb32291..0e65b1dca4c1bf90e65f5e68b3d069f52dbc3fd7 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -112,26 +112,6 @@ def convert_note_from_string( return note_value, invalid -def get_bounds(evaluation: Evaluation) -> tuple[int, int]: - """Calcule les notes min et max pour une évaluation.""" - note_max = evaluation.note_max or 0.0 - module: Module = evaluation.moduleimpl.module - if module.module_type in ( - scu.ModuleType.STANDARD, - scu.ModuleType.RESSOURCE, - scu.ModuleType.SAE, - ): - if evaluation.evaluation_type == Evaluation.EVALUATION_BONUS: - note_min, note_max = -20, 20 - else: - note_min = scu.NOTES_MIN - elif module.module_type == ModuleType.MALUS: - note_min = -20.0 - else: - raise ValueError("Invalid module type") # bug - return note_min, note_max - - def check_notes( notes: list[(int, float | str)], evaluation: Evaluation ) -> tuple[list[tuple[int, float]], list[int], list[int], list[int], list[int]]: @@ -149,7 +129,7 @@ def check_notes( etudids_non_inscrits : etudid non inscrits à ce module (ne considère pas l'inscr. au semestre) """ - note_min, note_max = get_bounds(evaluation) + note_min, note_max = evaluation.get_bounds() # Vérifie inscription au module (même DEM/DEF) etudids_inscrits_mod = { i.etudid for i in evaluation.moduleimpl.query_inscriptions().all() @@ -779,30 +759,28 @@ def saisie_notes(evaluation: Evaluation, group_ids: list[int] | tuple[int] = ()) # US 1030 - DEB - - class DataSection: """Agrège les données sur un ensemble de module (Ressources, SAEs, Standard, Malus). Nécessaire pour garantir l'ordre de présentation pour les formations en apc. data_modmpl: la liste des modules de la section. """ - def __init__(self, module_type): + def __init__(self, module_type: ModuleType): self.name = scu.MODULE_TYPE_NAMES[module_type] self.data_modimpl = [] - def add_modimpl(self, modimpl): + def add_modimpl(self, modimpl: ModuleImpl): self.data_modimpl.append(modimpl) class DataModimpl: - """Aggrèges les données sur un module pour un étudiants donné. - # récupére les notes sur les années précédentes pour aider à la saisie des cursus adaptés (cursus) - # (sportifs, étalement sur plusieurs années, etc.). - data_evals: lal iste des évaluations du module. + """Aggrège les données sur un module pour un étudiant donné. + # TODO ?: récupére les notes sur les années précédentes pour aider à la saisie + # des cursus adaptés (cursus) (sportifs, étalement sur plusieurs années, etc.). + data_evals: la iste des évaluations du module. """ - def __init__(self, etudiant: Identite, modimpl): + def __init__(self, etudiant: Identite, modimpl: ModuleImpl): self.etudiant = etudiant self.data_evals = [] self.modimpl = modimpl @@ -823,55 +801,111 @@ class DataModimpl: # if ins.formsemestre.formation_id == formation_id: # self.cursus[ins.formsemestre.id] = ins.formsemestre - def add_eval(self, data_eval): + def add_eval(self, data_eval: DataSection): self.data_evals.append(data_eval) class DataEval: """Récupère les données sur une évaluation appliquée à un étudiant""" - def __init__(self, etudid, evaluation): + def __init__(self, etudid: int, evaluation: Evaluation): self.etudid = etudid self.evaluation = evaluation self.evaluation_id = evaluation.id - self.note_min, self.note_max = get_bounds(evaluation) - self.data_debut = evaluation.date_fin - self.date_fin = evaluation.date_fin self.description = evaluation.description - self.ue_poids = evaluation.get_ue_poids_dict() - self.evaluation_type = evaluation.evaluation_type - self.note = NotesNotes.query.filter_by( - evaluation_id=self.evaluation.id, etudid=self.etudid - ).first() - self.note_value = "" if self.note is None else self.note.value - self.history = f"""<span id="formnotes_hist_{self.evaluation_id}">{ - get_note_history_menu(evaluation.id, etudid) - }</span>""" - - def forminput(self): + self.note_min, self.note_max = evaluation.get_bounds() + self.evaluation_type: EvaluationType = evaluation.evaluation_type + self.note = get_evaluation_etud_note(evaluation, etudid) + # self.data_debut = evaluation.date_debut + # self.date_fin = evaluation.date_fin + # self.ue_poids = evaluation.get_ue_poids_dict() + self.note_value = ( + "" + if self.note is None + else scu.fmt_note(self.note.value, fixed_precision_str=False) + ) + + def titre(self) -> str: + """Retourne la chaine Html pour le titre d'une évaluation""" + eval_type: str = { + 0: "", # EVALUATION_NORMAL: + 1: " [rattrapage]", # EVALUATION_RATTRAPAGE + 2: " [deuxième session]", # EVALUATION_SESSION2 + 3: " [bonus]", # EVALUATION_BONUS + }[self.evaluation_type] + return f""" + <a href="{self.link_to_eval()}"> + {self.description or "évaluation sans titre"}{eval_type} + </a> + """ + + def forminput(self) -> str: return f"""<input type="text" size="5" id="formnotes_note_{self.evaluation_id}" class="note" onkeypress="return enter_focus_next(this, event);" note_min="{self.note_min}" note_max="{self.note_max}" evaluation_id="{self.evaluation_id}" original-value="{self.note_value}" value="{self.note_value}" />""" + def link_to_eval(self) -> str: + """Retourne l'url' vers une évaluation""" + return url_for( + "notes.evaluation_listenotes", + scodoc_dept=g.scodoc_dept, + evaluation_id=self.evaluation.id, + ) + + def history(self): + return f"""<span id="formnotes_hist_{self.evaluation_id}"> + {get_note_history_menu(self.evaluation.id, self.etudid)}</span>""" + + +class DataForm: + """ + Class qui décrit les données d'une page de saisie par étudiant: + decision_jury: bool = indique si une décision de jury a été prise pour cet étudiant + data_sections: dict[ModuleTYpe, DataSection] = pour chaque section (ressource, SAE, Malus, Standard), + la liste de ses modimpl concernés + """ + + def __init__(self, etudiant: Identite, semestre: FormSemestre, decision_jury: bool): + self.etudiant = etudiant + self.semestre = semestre + self.decision_jury = decision_jury + self.data_sections = {} + + def add_section(self, data_section: DataSection): + self.data_sections.append(data_section) -def get_data(semestre: FormSemestre, etudiant: Identite) -> dict: - """Récupère les infos pour un étudiant (cf ticket 1030)""" + def link_to_etudiant(self) -> str: + link: str = url_for( + "notes.formsemestre_bulletinetud", + scodoc_dept=g.scodoc_dept, + formsemestre_id=self.semestre.id, + etudid=self.etudiant.etudid, + ) + return f""" + <a href="{link}">{self.etudiant.nomprenom}</a> + """ + + +def get_data(semestre: FormSemestre, etudiant: Identite) -> DataForm: + """Récupère les infos pour une page (donc un étudiant (cf ticket 1030))""" res = res_sem.load_formsemestre_results(semestre) - data = { - "decision_jury": res.etud_has_decision(etudiant.etudid, include_rcues=False), - "data_sections": {}, - } + data = DataForm( + etudiant, semestre, res.etud_has_decision(etudiant.etudid, include_rcues=False) + ) if semestre.formation.is_apc(): for module_type in ModuleType: - data["data_sections"][module_type] = DataSection(module_type) + data.data_sections[module_type] = DataSection(module_type) + else: + for module_type in [ ModuleType.STANDARD, ModuleType.MALUS]: + data.data_sections[module_type] = DataSection(module_type) for modimpl in semestre.modimpls_sorted: module_type = modimpl.module.module_type if res.modimpl_inscr_df[modimpl.id][etudiant.id]: data_modimpl = DataModimpl(etudiant, modimpl) for evaluation in modimpl.evaluations: data_modimpl.add_eval(DataEval(etudiant.etudid, evaluation)) - data["data_sections"][module_type].add_modimpl(data_modimpl) + data.data_sections[module_type].add_modimpl(data_modimpl) return data @@ -891,38 +925,44 @@ def saisie_notes_par_etu(semestre: FormSemestre, etudiant: Identite): inscription = ins # assert ins is not None if ins is None: - raise ValueError("Tentative d accès à une inscription inexistante") # bug - if ins.etat != "I": + raise ScoValueError("Tentative d accès à une inscription inexistante") # bug + if ins.etat != scu.INSCRIT: raise ValueError( "Etudiant démissionnaire ou défaillant" ) # ne devrait pas être accessible. bug "Formulaire de saisie de note centré sur l'étudiant" data = get_data(semestre, etudiant) + if semestre.formation.is_apc(): + module_type_order = [ + ModuleType.RESSOURCE, + ModuleType.SAE, + ModuleType.STANDARD, + ModuleType.MALUS, + ] + else: + module_type_order = [ + ModuleType.STANDARD, + ModuleType.MALUS, + ] return render_template( "etud/saisie_notes_par_etu.j2", title="Saisie notes par étudiant", javascripts=["js/saisie_notes.js", "js/saisie_notes_par_etu.js"], sco=ScoData(formsemestre=semestre), - semestre=semestre, - etudiant=etudiant, data=data, readonly=readonly, get_note_history_menu=get_note_history_menu, - module_type_order=[ - ModuleType.RESSOURCE, - ModuleType.SAE, - ModuleType.STANDARD, - ModuleType.MALUS, - ], + module_type_order=module_type_order, ) # US 1030 - FIN -def get_evaluation_etud_note(evaluation: Evaluation, etudid: int): - a = Notes.query.filter_by(id=evaluation.id, etudid=etudid).first() - pass +def get_evaluation_etud_note(evaluation: Evaluation, etudid: int) -> float | str | None: + return NotesNotes.query.filter_by( + evaluation_id=evaluation.id, etudid=etudid + ).first() def get_sorted_etuds_notes( diff --git a/app/static/js/saisie_notes.js b/app/static/js/saisie_notes.js index 553c30016f7c4322a93da1c394244003d411dd94..b79ffffcc8f75b51499b169852ac280815817d15 100644 --- a/app/static/js/saisie_notes.js +++ b/app/static/js/saisie_notes.js @@ -111,10 +111,20 @@ function get_field_id(elt) { // la forme des id est ????_#### (où #### est return elt.id.split('_').pop(); // formnotes_note_#### ou eval_#### } +function get_hist_field(field_id) { + return document.getElementById(hist_prefix + field_id); +} + +function get_note_field(field_id) { + return document.getElementById(note_prefix + field_id); +} + function write_on_blur(elt) { if (elt.getAttribute("data-modified") === "true") { - valid_note.call(elt); - elt.setAttribute("data-modified", "false"); // Reset the modified flag + if (elt.value !== elt.getAttribute("data-last-saved-value")) { + valid_note.call(elt); + elt.setAttribute("data-modified", "false"); // Reset the modified flag + } } } diff --git a/app/static/js/saisie_notes_par_etu.js b/app/static/js/saisie_notes_par_etu.js index 5c907152298ad5b625a02b99a8bd76395c6f5635..da7dfe0b5f9a1b8513afc911c5726d5e839ea691 100644 --- a/app/static/js/saisie_notes_par_etu.js +++ b/app/static/js/saisie_notes_par_etu.js @@ -21,14 +21,6 @@ function get_evaluation_id(field_id) { return parseInt(note_field.getAttribute("evaluation_id")); } -function get_note_field(field_id) { - return document.getElementById(note_prefix + field_id); -} - -function get_hist_field(field_id) { - return document.getElementById(hist_prefix + field_id); -} - function get_etudid(field_id) { return parseInt(form_etudid); } diff --git a/app/static/js/saisie_notes_par_eval.js b/app/static/js/saisie_notes_par_eval.js index d4e226661f6df7e0aad961a0ad769bf0ee5ff567..4c188e35c26069c2e5ba0dba30a05b2eb4c30532 100644 --- a/app/static/js/saisie_notes_par_eval.js +++ b/app/static/js/saisie_notes_par_eval.js @@ -12,14 +12,6 @@ function get_note_max(field_id) { return note_max; } -function get_note_field(field_id) { - return document.getElementById(note_prefix + field_id); -} - -function get_hist_field(field_id) { - return document.getElementById(hist_prefix + field_id); -} - function get_etudid(field_id) { return parseInt(get_note_field(field_id).getAttribute("data-etudid")); } @@ -29,9 +21,7 @@ document.addEventListener("DOMContentLoaded", function () { noteInputs.forEach(function (input) { console.log(get_field_id(input)); input.addEventListener("input", function() { - if (this.value !== this.getAttribute("data-last-saved-value")) { - this.setAttribute("data-modified", "true"); - } + this.setAttribute("data-modified", "true"); }); input.addEventListener("blur", input.addEventListener("blur", function(event){write_on_blur(event.currentTarget)} )); }); diff --git a/app/templates/etud/saisie_notes_par_etu.j2 b/app/templates/etud/saisie_notes_par_etu.j2 index b4f55ba97d8be2ea0b1a7dc8b7bfeae470e0f391..b0c6d6948cfb6bc228862096c462a73f1cfd2b04 100644 --- a/app/templates/etud/saisie_notes_par_etu.j2 +++ b/app/templates/etud/saisie_notes_par_etu.j2 @@ -34,8 +34,8 @@ {% block app_content %} <div id="saisie_notes_par_etu" class="table-container"> <span class="titre">{{title}}</span> - <h3> Saisie des notes de l'étudiant : {{etudiant.nom}} {{etudiant.prenom}} </h3> - {% if data["decision_jury"] %} + <h3> Saisie des notes de l'étudiant : {{data.link_to_etudiant() | safe }} </h3> + {% if data.decision_jury %} <div class="warning"> <ul class="tf-msg"> <li class="tf-msg">Attention: il y a déjà des <b>décisions de jury</b> enregistrées pour @@ -44,38 +44,38 @@ </div> {% endif %} <div id="formnotes"> - <input type="hidden" id="etudiant_id" value="{{etudiant.id}}" /> - <input type="hidden" id="formnotes_formsemestre_id" value="{{semestre.id}}" /> - {% if data["decision_jury"] %} + <input type="hidden" id="etudiant_id" value="{{data.etudiant.id}}" /> + <input type="hidden" id="formnotes_formsemestre_id" value="{{data.semestre.id}}" /> + {% if data.decision_jury %} {% endif %} <div class="label">commentaire</div> <div class="long_input"> <input type="text" id="formnotes_comment" name="comment" class="input" size="44" onkeypress="return enter_focus_next(this, event);"> </div> {% for module_type in module_type_order %} - {% if data["data_sections"][module_type].data_modimpl %} - <div class="section">{{ data["data_sections"][module_type].name }}</div> - {% for data_modimpl in data["data_sections"][module_type].data_modimpl %} + {% if data.data_sections[module_type].data_modimpl %} + <div class="section">{{ data.data_sections[module_type].name }}</div> + {% for data_modimpl in data.data_sections[module_type].data_modimpl %} <div class="module">{{data_modimpl.link | safe}}</div> {% for data_eval in data_modimpl.data_evals %} - <div class="label">{{ data_eval.description }} - <input type="hidden" id="date_debut_{{ data_eval.evaluation_id }}" value="{{ data_eval.date_debut }}"> - <input type="hidden" id="date_fin_{{ data_eval.evaluation_id }}" value="{{ data_eval.date_fin }}"> - <input type="hidden" id="evaluation_type_{{ data_eval.evaluation_id }}" value="{{ data_eval.evaluation_type }}"> - {% for ue in data_eval.ue_poids %} - <div id="poids_{{ data_eval.evaluation_id }}_{{ ue }}"> - <input type="hidden" class="ue" value="{{ ue }}"> - <input type="hidden" class="evaluation" value="{{ data_eval.evaluation_id }}"> - <input type="hidden" value="{{ data_eval.ue_poids[ue] }}"> - </div> - {% endfor %} - </input> + <div class="label"> + {{ data_eval.titre() | safe }} +{# <input type="hidden" id="date_debut_{{ data_eval.evaluation_id }}" value="{{ data_eval.date_debut }}">#} +{# <input type="hidden" id="date_fin_{{ data_eval.evaluation_id }}" value="{{ data_eval.date_fin }}">#} +{# {% for ue in data_eval.ue_poids %}#} +{# <div id="poids_{{ data_eval.evaluation_id }}_{{ ue }}">#} +{# <input type="hidden" class="ue" value="{{ ue }}">#} +{# <input type="hidden" class="evaluation" value="{{ data_eval.evaluation_id }}">#} +{# <input type="hidden" value="{{ data_eval.ue_poids[ue] }}">#} +{# </div>#} +{# {% endfor %}#} +{# </input>#} </div> <div class="input"> {{ data_eval.forminput() | safe }} </div> <div class="history"> - {{ data_eval.history | safe }} + {{ data_eval.history() | safe }} </div> {% endfor %} {% endfor %} @@ -87,7 +87,7 @@ style="pointer-events: unset; color: unset; text-decoration: unset; cursor: unset;">Terminer</a> - {% if data["decision_jury"] %} + {% if data.decision_jury %} <span id="jurylink" class="jurylink"></span> {% endif %} </div>