Skip to content
Snippets Groups Projects
Select Git revision
  • 94347657f6d6e1272c0afe88991dd856f7083b0f
  • master default protected
2 results

scodoc.py

Blame
  • Forked from Jean-Marie Place / SCODOC_R6A06
    Source project has a limited visibility.
    formsemestres.py 20.94 KiB
    ##############################################################################
    # ScoDoc
    # Copyright (c) 1999 - 2024 Emmanuel Viennet.  All rights reserved.
    # See LICENSE
    ##############################################################################
    
    """
      ScoDoc 9 API : accès aux formsemestres
    """
    from operator import attrgetter, itemgetter
    
    from flask import g, make_response, request
    from flask_json import as_json
    from flask_login import current_user, login_required
    import sqlalchemy as sa
    import app
    from app import db
    from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
    from app.decorators import scodoc, permission_required
    from app.scodoc.sco_utils import json_error
    from app.comp import res_sem
    from app.comp.moy_mod import ModuleImplResults
    from app.comp.res_compat import NotesTableCompat
    from app.models import (
        Departement,
        Evaluation,
        FormSemestre,
        FormSemestreEtape,
        FormSemestreInscription,
        Identite,
        ModuleImpl,
        NotesNotes,
    )
    from app.models.formsemestre import GROUPS_AUTO_ASSIGNMENT_DATA_MAX
    from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json
    from app.scodoc import sco_edt_cal
    from app.scodoc import sco_groups
    from app.scodoc.sco_permissions import Permission
    from app.scodoc.sco_utils import ModuleType
    import app.scodoc.sco_utils as scu
    from app.tables.recap import TableRecap
    
    
    @bp.route("/formsemestre/<int:formsemestre_id>")
    @api_web_bp.route("/formsemestre/<int:formsemestre_id>")
    @login_required
    @scodoc
    @permission_required(Permission.ScoView)
    @as_json
    def formsemestre_infos(formsemestre_id: int):
        """
        Information sur le formsemestre indiqué.
    
        formsemestre_id : l'id du formsemestre
    
        Exemple de résultat :
            {
              "block_moyennes": false,
              "bul_bgcolor": "white",
              "bul_hide_xml": false,
              "date_debut_iso": "2021-09-01",
              "date_debut": "01/09/2021",
              "date_fin_iso": "2022-08-31",
              "date_fin": "31/08/2022",
              "dept_id": 1,
              "elt_annee_apo": null,
              "elt_sem_apo": null,
              "ens_can_edit_eval": false,
              "etat": true,
              "formation_id": 1,
              "formsemestre_id": 1,
              "gestion_compensation": false,
              "gestion_semestrielle": false,
              "id": 1,
              "modalite": "FI",
              "resp_can_change_ens": true,
              "resp_can_edit": false,
              "responsables": [1, 99], // uids
              "scodoc7_id": null,
              "semestre_id": 1,
              "titre_formation" : "BUT GEA",
              "titre_num": "BUT GEA semestre 1",
              "titre": "BUT GEA",
            }
    
        """
        query = FormSemestre.query.filter_by(id=formsemestre_id)
        if g.scodoc_dept:
            query = query.filter_by(dept_id=g.scodoc_dept_id)
        formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
        return formsemestre.to_dict_api()
    
    
    @bp.route("/formsemestres/query")
    @api_web_bp.route("/formsemestres/query")
    @login_required
    @scodoc
    @permission_required(Permission.ScoView)
    @as_json
    def formsemestres_query():
        """
        Retourne les formsemestres filtrés par
         étape Apogée ou année scolaire ou département (acronyme ou id) ou état ou code étudiant
    
        etape_apo : un code étape apogée
        annee_scolaire : année de début de l'année scolaire
        dept_acronym : acronyme du département (eg "RT")
        dept_id : id du département
        ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
        etat: 0 si verrouillé, 1 sinon
        """
        etape_apo = request.args.get("etape_apo")
        annee_scolaire = request.args.get("annee_scolaire")
        dept_acronym = request.args.get("dept_acronym")
        dept_id = request.args.get("dept_id")
        etat = request.args.get("etat")
        nip = request.args.get("nip")
        ine = request.args.get("ine")
        formsemestres = FormSemestre.query
        if g.scodoc_dept:
            formsemestres = formsemestres.filter_by(dept_id=g.scodoc_dept_id)
        if annee_scolaire is not None:
            try:
                annee_scolaire_int = int(annee_scolaire)
            except ValueError:
                return json_error(API_CLIENT_ERROR, "invalid annee_scolaire: not int")
            debut_annee = scu.date_debut_annee_scolaire(annee_scolaire_int)
            fin_annee = scu.date_fin_annee_scolaire(annee_scolaire_int)
            formsemestres = formsemestres.filter(
                FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee
            )
        if etat is not None:
            try:
                etat = bool(int(etat))
            except ValueError:
                return json_error(404, "invalid etat: integer expected")
            formsemestres = formsemestres.filter_by(etat=etat)
        if dept_acronym is not None:
            formsemestres = formsemestres.join(Departement).filter_by(acronym=dept_acronym)
        if dept_id is not None:
            try:
                dept_id = int(dept_id)
            except ValueError:
                return json_error(404, "invalid dept_id: integer expected")
            formsemestres = formsemestres.filter_by(dept_id=dept_id)
        if etape_apo is not None:
            formsemestres = formsemestres.join(FormSemestreEtape).filter(
                FormSemestreEtape.etape_apo == etape_apo
            )
        inscr_joined = False
        if nip is not None:
            formsemestres = (
                formsemestres.join(FormSemestreInscription)
                .join(Identite)
                .filter_by(code_nip=nip)
            )
            inscr_joined = True
        if ine is not None:
            if not inscr_joined:
                formsemestres = formsemestres.join(FormSemestreInscription).join(Identite)
            formsemestres = formsemestres.filter_by(code_ine=ine)
    
        return [
            formsemestre.to_dict_api()
            for formsemestre in formsemestres.order_by(
                FormSemestre.date_debut.desc(),
                FormSemestre.modalite,
                FormSemestre.semestre_id,
                FormSemestre.titre,
            )
        ]
    
    
    @bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
    @api_web_bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
    @scodoc
    @permission_required(Permission.EditFormSemestre)
    @as_json
    def formsemestre_edit(formsemestre_id: int):
        """Modifie les champs d'un formsemestre."""
        formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
        args = request.get_json(force=True)  # may raise 400 Bad Request
        editable_keys = {
            "semestre_id",
            "titre",
            "date_debut",
            "date_fin",
            "edt_id",
            "etat",
            "modalite",
            "gestion_compensation",
            "bul_hide_xml",
            "block_moyennes",
            "block_moyenne_generale",
            "mode_calcul_moyennes",
            "gestion_semestrielle",
            "bul_bgcolor",
            "resp_can_edit",
            "resp_can_change_ens",
            "ens_can_edit_eval",
            "elt_sem_apo",
            "elt_annee_apo",
        }
        formsemestre.from_dict({k: v for (k, v) in args.items() if k in editable_keys})
        try:
            db.session.commit()
        except sa.exc.StatementError as exc:
            return json_error(404, f"invalid argument(s): {exc.args[0]}")
        return formsemestre.to_dict_api()
    
    
    @bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
    @bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
    @api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
    @api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
    @login_required
    @scodoc
    @permission_required(Permission.ScoView)
    @as_json
    def bulletins(formsemestre_id: int, version: str = "long"):
        """
        Retourne les bulletins d'un formsemestre donné
    
        formsemestre_id : l'id d'un formesemestre
    
        Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin
        """
        query = FormSemestre.query.filter_by(id=formsemestre_id)
        if g.scodoc_dept:
            query = query.filter_by(dept_id=g.scodoc_dept_id)
        formsemestre: FormSemestre = query.first()
        if formsemestre is None:
            return json_error(404, "formsemestre non trouve")
        app.set_sco_dept(formsemestre.departement.acronym)
    
        data = []
        for etu in formsemestre.etuds:
            bul_etu = get_formsemestre_bulletin_etud_json(
                formsemestre, etu, version=version
            )
            data.append(bul_etu.json)
    
        return data
    
    
    @bp.route("/formsemestre/<int:formsemestre_id>/programme")
    @api_web_bp.route("/formsemestre/<int:formsemestre_id>/programme")
    @login_required
    @scodoc
    @permission_required(Permission.ScoView)
    @as_json
    def formsemestre_programme(formsemestre_id: int):
        """
        Retourne la liste des UEs, ressources et SAEs d'un semestre
    
        formsemestre_id : l'id d'un formsemestre
    
        Exemple de résultat :
            {
              "ues": [
                {
                  "type": 0,
                  "formation_id": 1,
                  "ue_code": "UCOD11",
                  "id": 1,
                  "ects": 12.0,
                  "acronyme": "RT1.1",
                  "is_external": false,
                  "numero": 1,
                  "code_apogee": "",
                  "titre": "Administrer les r\u00e9seaux et l\u2019Internet",
                  "coefficient": 0.0,
                  "semestre_idx": 1,
                  "color": "#B80004",
                  "ue_id": 1
                },
                ...
              ],
              "ressources": [
                {
                "ens": [ 10, 18 ],
                "formsemestre_id": 1,
                "id": 15,
                "module": {
                    "abbrev": "Programmer",
                    "code": "SAE15",
                    "code_apogee": "V7GOP",
                    "coefficient": 1.0,
                    "formation_id": 1,
                    "heures_cours": 0.0,
                    "heures_td": 0.0,
                    "heures_tp": 0.0,
                    "id": 15,
                    "matiere_id": 3,
                    "module_id": 15,
                    "module_type": 3,
                    "numero": 50,
                    "semestre_id": 1,
                    "titre": "Programmer en Python",
                    "ue_id": 3
                },
                "module_id": 15,
                "moduleimpl_id": 15,
                "responsable_id": 2
              },
                ...
              ],
              "saes": [
                {
                  ...
                },
                ...
              ],
              "modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ]
            }
        """
        query = FormSemestre.query.filter_by(id=formsemestre_id)
        if g.scodoc_dept:
            query = query.filter_by(dept_id=g.scodoc_dept_id)
        formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
        ues = formsemestre.get_ues()
        m_list = {
            ModuleType.RESSOURCE: [],
            ModuleType.SAE: [],
            ModuleType.STANDARD: [],
            ModuleType.MALUS: [],
        }
        for modimpl in formsemestre.modimpls_sorted:
            d = modimpl.to_dict(convert_objects=True)
            m_list[modimpl.module.module_type].append(d)
        return {
            "ues": [ue.to_dict(convert_objects=True) for ue in ues],
            "ressources": m_list[ModuleType.RESSOURCE],
            "saes": m_list[ModuleType.SAE],
            "modules": m_list[ModuleType.STANDARD],
            "malus": m_list[ModuleType.MALUS],
        }
    
    
    @bp.route(
        "/formsemestre/<int:formsemestre_id>/etudiants",
        defaults={"with_query": False, "long": False},
    )
    @bp.route(
        "/formsemestre/<int:formsemestre_id>/etudiants/long",
        defaults={"with_query": False, "long": True},
    )
    @bp.route(
        "/formsemestre/<int:formsemestre_id>/etudiants/query",
        defaults={"with_query": True, "long": False},
    )
    @bp.route(
        "/formsemestre/<int:formsemestre_id>/etudiants/long/query",
        defaults={"with_query": True, "long": True},
    )
    @api_web_bp.route(
        "/formsemestre/<int:formsemestre_id>/etudiants",
        defaults={"with_query": False, "long": False},
    )
    @api_web_bp.route(
        "/formsemestre/<int:formsemestre_id>/etudiants/long",
        defaults={"with_query": False, "long": True},
    )
    @api_web_bp.route(
        "/formsemestre/<int:formsemestre_id>/etudiants/query",
        defaults={"with_query": True, "long": False},
    )
    @api_web_bp.route(
        "/formsemestre/<int:formsemestre_id>/etudiants/long/query",
        defaults={"with_query": True, "long": True},
    )
    @login_required
    @scodoc
    @permission_required(Permission.ScoView)
    @as_json
    def formsemestre_etudiants(
        formsemestre_id: int, with_query: bool = False, long: bool = False
    ):
        """Étudiants d'un formsemestre."""
        query = FormSemestre.query.filter_by(id=formsemestre_id)
        if g.scodoc_dept:
            query = query.filter_by(dept_id=g.scodoc_dept_id)
        formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
        if with_query:
            etat = request.args.get("etat")
            if etat is not None:
                etat = {
                    "actifs": scu.INSCRIT,
                    "demissionnaires": scu.DEMISSION,
                    "defaillants": scu.DEF,
                }.get(etat, etat)
                inscriptions = [
                    ins for ins in formsemestre.inscriptions if ins.etat == etat
                ]
            else:
                inscriptions = formsemestre.inscriptions
        else:
            inscriptions = formsemestre.inscriptions
    
        if long:
            restrict = not current_user.has_permission(Permission.ViewEtudData)
            etuds = [ins.etud.to_dict_api(restrict=restrict) for ins in inscriptions]
        else:
            etuds = [ins.etud.to_dict_short() for ins in inscriptions]
        # Ajout des groupes de chaque étudiants
        # XXX A REVOIR: trop inefficace !
        for etud in etuds:
            etud["groups"] = sco_groups.get_etud_groups(
                etud["id"], formsemestre_id, exclude_default=True
            )
    
        return sorted(etuds, key=itemgetter("sort_key"))
    
    
    @bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
    @api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
    @login_required
    @scodoc
    @permission_required(Permission.ScoView)
    @as_json
    def etat_evals(formsemestre_id: int):
        """
        Informations sur l'état des évaluations d'un formsemestre.
    
        formsemestre_id : l'id d'un semestre
    
        Exemple de résultat :
        [
          {
            "id": 1, // moduleimpl_id
            "titre": "Initiation aux réseaux informatiques",
            "evaluations": [
              {
                "id": 1,
                "description": null,
                "datetime_epreuve": null,
                "heure_fin": "09:00:00",
                "coefficient": "02.00"
                "is_complete": true,
                "nb_inscrits": 16,
                "nb_manquantes": 0,
                "ABS": 0,
                "ATT": 0,
                "EXC": 0,
                "saisie_notes": {
                  "datetime_debut": "2021-09-11T00:00:00+02:00",
                  "datetime_fin": "2022-08-25T00:00:00+02:00",
                  "datetime_mediane": "2022-03-19T00:00:00+01:00"
                }
              },
              ...
            ]
          },
        ]
        """
        query = FormSemestre.query.filter_by(id=formsemestre_id)
        if g.scodoc_dept:
            query = query.filter_by(dept_id=g.scodoc_dept_id)
        formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
        app.set_sco_dept(formsemestre.departement.acronym)
        nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
    
        result = []
        for modimpl_id in nt.modimpls_results:
            modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
            modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
            modimpl_dict = modimpl.to_dict(convert_objects=True, with_module=False)
    
            list_eval = []
            for evaluation_id in modimpl_results.evaluations_etat:
                eval_etat = modimpl_results.evaluations_etat[evaluation_id]
                evaluation = Evaluation.query.get_or_404(evaluation_id)
                eval_dict = evaluation.to_dict_api()
                eval_dict["etat"] = eval_etat.to_dict()
    
                eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
                eval_dict["nb_notes_manquantes"] = len(
                    modimpl_results.evals_etudids_sans_note[evaluation.id]
                )
                eval_dict["nb_notes_abs"] = sum(
                    modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE
                )
                eval_dict["nb_notes_att"] = eval_etat.nb_attente
                eval_dict["nb_notes_exc"] = sum(
                    modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE
                )
    
                # Récupération de toutes les notes de l'évaluation
                # eval["notes"] = modimpl_results.get_eval_notes_dict(evaluation_id)
    
                notes = NotesNotes.query.filter_by(evaluation_id=evaluation.id).all()
    
                date_debut = None
                date_fin = None
                date_mediane = None
    
                # Si il y a plus d'une note saisie pour l'évaluation
                if len(notes) >= 1:
                    # Tri des notes en fonction de leurs dates
                    notes_sorted = sorted(notes, key=attrgetter("date"))
    
                    date_debut = notes_sorted[0].date
                    date_fin = notes_sorted[-1].date
    
                    # Note médiane
                    date_mediane = notes_sorted[len(notes_sorted) // 2].date
    
                eval_dict["saisie_notes"] = {
                    "datetime_debut": (
                        date_debut.isoformat() if date_debut is not None else None
                    ),
                    "datetime_fin": date_fin.isoformat() if date_fin is not None else None,
                    "datetime_mediane": (
                        date_mediane.isoformat() if date_mediane is not None else None
                    ),
                }
    
                list_eval.append(eval_dict)
    
            modimpl_dict["evaluations"] = list_eval
            result.append(modimpl_dict)
        return result
    
    
    @bp.route("/formsemestre/<int:formsemestre_id>/resultats")
    @api_web_bp.route("/formsemestre/<int:formsemestre_id>/resultats")
    @login_required
    @scodoc
    @permission_required(Permission.ScoView)
    @as_json
    def formsemestre_resultat(formsemestre_id: int):
        """Tableau récapitulatif des résultats
        Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules.
        """
        format_spec = request.args.get("format", None)
        if format_spec is not None and format_spec != "raw":
            return json_error(API_CLIENT_ERROR, "invalid format specification")
        convert_values = format_spec != "raw"
    
        query = FormSemestre.query.filter_by(id=formsemestre_id)
        if g.scodoc_dept:
            query = query.filter_by(dept_id=g.scodoc_dept_id)
        formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
        app.set_sco_dept(formsemestre.departement.acronym)
        res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
        table = TableRecap(
            res, convert_values=convert_values, include_evaluations=False, mode_jury=False
        )
        # Supprime les champs inutiles (mise en forme)
        rows = table.to_list()
        # Ajoute le groupe de chaque partition:
        etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
        for row in rows:
            row["partitions"] = etud_groups.get(row["etudid"], {})
    
        return rows
    
    
    @bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
    @api_web_bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
    @login_required
    @scodoc
    @permission_required(Permission.ScoView)
    @as_json
    def get_groups_auto_assignment(formsemestre_id: int):
        """rend les données"""
        query = FormSemestre.query.filter_by(id=formsemestre_id)
        if g.scodoc_dept:
            query = query.filter_by(dept_id=g.scodoc_dept_id)
        formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
        response = make_response(formsemestre.groups_auto_assignment_data or b"")
        response.headers["Content-Type"] = scu.JSON_MIMETYPE
        return response
    
    
    @bp.route(
        "/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
    )
    @api_web_bp.route(
        "/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
    )
    @login_required
    @scodoc
    @permission_required(Permission.ScoView)
    @as_json
    def save_groups_auto_assignment(formsemestre_id: int):
        """enregistre les données"""
        query = FormSemestre.query.filter_by(id=formsemestre_id)
        if g.scodoc_dept:
            query = query.filter_by(dept_id=g.scodoc_dept_id)
        formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
    
        if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX:
            return json_error(413, "data too large")
        formsemestre.groups_auto_assignment_data = request.data
        db.session.add(formsemestre)
        db.session.commit()
    
    
    @bp.route("/formsemestre/<int:formsemestre_id>/edt")
    @api_web_bp.route("/formsemestre/<int:formsemestre_id>/edt")
    @login_required
    @scodoc
    @permission_required(Permission.ScoView)
    @as_json
    def formsemestre_edt(formsemestre_id: int):
        """l'emploi du temps du semestre.
        Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
    
        group_ids permet de filtrer sur les groupes ScoDoc.
        show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
        """
        query = FormSemestre.query.filter_by(id=formsemestre_id)
        if g.scodoc_dept:
            query = query.filter_by(dept_id=g.scodoc_dept_id)
        formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
        group_ids = request.args.getlist("group_ids", int)
        show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
        return sco_edt_cal.formsemestre_edt_dict(
            formsemestre, group_ids=group_ids, show_modules_titles=show_modules_titles
        )