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

TaskRouter.js

Blame
  • Forked from Quentin Briand / IFI Express TP
    Source project has a limited visibility.
    notes_formsemestre.py 18.43 KiB
    # -*- coding: utf-8 -*-
    
    ##############################################################################
    #
    # ScoDoc
    #
    # Copyright (c) 1999 - 2024 Emmanuel Viennet.  All rights reserved.
    #
    # This program is free software; you can redistribute it and/or modify
    # it under the terms of the GNU General Public License as published by
    # the Free Software Foundation; either version 2 of the License, or
    # (at your option) any later version.
    #
    # This program is distributed in the hope that it will be useful,
    # but WITHOUT ANY WARRANTY; without even the implied warranty of
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    # GNU General Public License for more details.
    #
    # You should have received a copy of the GNU General Public License
    # along with this program; if not, write to the Free Software
    # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
    #
    #   Emmanuel Viennet      emmanuel.viennet@viennet.net
    #
    ##############################################################################
    
    """
    Vues "modernes" des formsemestres
    Emmanuel Viennet, 2023
    """
    
    import datetime
    import io
    import zipfile
    
    from flask import flash, redirect, render_template, url_for
    from flask import current_app, g, request
    import PIL
    
    from app import db, log
    from app.decorators import (
        scodoc,
        permission_required,
    )
    from app.formations import formation_io, formation_versions
    from app.forms.formsemestre import (
        change_formation,
        edit_modimpls_codes_apo,
        edit_description,
    )
    from app.formsemestre import import_from_descr
    from app.models import (
        Formation,
        FormSemestre,
        FormSemestreDescription,
        FORMSEMESTRE_DISPOSITIFS,
        ScoDocSiteConfig,
    )
    from app.scodoc import (
        sco_edt_cal,
        sco_groups_view,
    )
    from app.scodoc.sco_exceptions import ScoValueError
    from app.scodoc.sco_permissions import Permission
    from app.scodoc import sco_utils as scu
    from app.views import notes_bp as bp
    from app.views import ScoData
    
    
    @bp.route(
        "/formsemestre_change_formation/<int:formsemestre_id>", methods=["GET", "POST"]
    )
    @scodoc
    @permission_required(Permission.EditFormSemestre)
    def formsemestre_change_formation(formsemestre_id: int):
        """Propose de changer un formsemestre de formation.
        Cette opération est bien sûr impossible... sauf si les deux formations sont identiques.
        Par exemple, on vient de créer une formation, et on a oublié d'y associé un formsemestre
        existant.
        """
        formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
        formation_dict = formation_io.formation_export_dict(
            formsemestre.formation, export_external_ues=True, ue_reference_style="acronyme"
        )
        formations = [
            formation
            for formation in Formation.query.filter_by(
                dept_id=formsemestre.dept_id, acronyme=formsemestre.formation.acronyme
            )
            if formation.id != formsemestre.formation.id
            and formation_versions.formations_are_equals(
                formation, formation2_dict=formation_dict
            )
        ]
        form = change_formation.gen_formsemestre_change_formation_form(formations)
        if request.method == "POST" and form.validate:
            if not form.cancel.data:
                new_formation_id = form.radio_but.data
                if new_formation_id is None:  # pas de choix radio
                    flash("Pas de formation sélectionnée !")
                    return render_template(
                        "formsemestre/change_formation.j2",
                        form=form,
                        formations=formations,
                        formsemestre=formsemestre,
                        sco=ScoData(formsemestre=formsemestre),
                    )
                else:
                    new_formation: Formation = Formation.query.filter_by(
                        dept_id=g.scodoc_dept_id, formation_id=new_formation_id
                    ).first_or_404()
                    formation_versions.formsemestre_change_formation(
                        formsemestre, new_formation
                    )
                    flash("Formation du semestre modifiée")
            return redirect(
                url_for(
                    "notes.formsemestre_status",
                    scodoc_dept=g.scodoc_dept,
                    formsemestre_id=formsemestre_id,
                )
            )
        # GET
        return render_template(
            "formsemestre/change_formation.j2",
            form=form,
            formations=formations,
            formsemestre=formsemestre,
            sco=ScoData(formsemestre=formsemestre),
        )
    
    
    @bp.route(
        "/formsemestre_edit_modimpls_codes/<int:formsemestre_id>", methods=["GET", "POST"]
    )
    @scodoc
    @permission_required(Permission.EditFormSemestre)
    def formsemestre_edit_modimpls_codes(formsemestre_id: int):
        """Edition des codes Apogée et EDT"""
        formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
        form = edit_modimpls_codes_apo.EditModimplsCodesForm(formsemestre)
    
        if request.method == "POST" and form.validate:
            if not form.cancel.data:
                # record codes
                for modimpl in formsemestre.modimpls_sorted:
                    field_apo = getattr(form, f"modimpl_apo_{modimpl.id}")
                    field_edt = getattr(form, f"modimpl_edt_{modimpl.id}")
                    if field_apo and field_edt:
                        modimpl.code_apogee = field_apo.data.strip() or None
                        modimpl.edt_id = field_edt.data.strip() or None
                        log(f"setting codes for {modimpl}: apo={field_apo} edt={field_edt}")
                        db.session.add(modimpl)
                db.session.commit()
                flash("Codes enregistrés")
            return redirect(
                url_for(
                    "notes.formsemestre_status",
                    scodoc_dept=g.scodoc_dept,
                    formsemestre_id=formsemestre_id,
                )
            )
        # GET
        for modimpl in formsemestre.modimpls_sorted:
            field_apo = getattr(form, f"modimpl_apo_{modimpl.id}")
            field_edt = getattr(form, f"modimpl_edt_{modimpl.id}")
            field_apo.data = modimpl.code_apogee or ""
            field_edt.data = modimpl.edt_id or ""
        return render_template(
            "formsemestre/edit_modimpls_codes.j2",
            form=form,
            formsemestre=formsemestre,
            sco=ScoData(formsemestre=formsemestre),
        )
    
    
    @bp.route("/formsemestre/edt/<int:formsemestre_id>")
    @scodoc
    @permission_required(Permission.ScoView)
    def formsemestre_edt(formsemestre_id: int):
        """Expérimental: affiche emploi du temps du semestre"""
    
        current_date = request.args.get("current_date")
        show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
        view = request.args.get("view", "week")
        views_names = {"day": "Jour", "month": "Mois", "week": "Semaine"}
        if view not in views_names:
            raise ScoValueError("valeur invalide pour le paramètre view")
        formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
        cfg = ScoDocSiteConfig.query.filter_by(name="assi_morning_time").first()
        hour_start = cfg.value.split(":")[0].lstrip(" 0") if cfg else "7"
        cfg = ScoDocSiteConfig.query.filter_by(name="assi_afternoon_time").first()
        hour_end = cfg.value.split(":")[0].lstrip(" 0") if cfg else "18"
        group_ids = request.args.getlist("group_ids", int)
        groups_infos = sco_groups_view.DisplayedGroupsInfos(
            group_ids=group_ids,
            formsemestre_id=formsemestre_id,
            empty_list_select_all=False,
        )
        return render_template(
            "formsemestre/edt.j2",
            current_date=current_date,
            formsemestre=formsemestre,
            hour_start=hour_start,
            hour_end=hour_end,
            form_groups_choice=sco_groups_view.form_groups_choice(
                groups_infos,
                submit_on_change=True,
                default_deselect_others=False,
                with_deselect_butt=True,
            ),
            groups_query_args=groups_infos.groups_query_args,
            sco=ScoData(formsemestre=formsemestre),
            show_modules_titles=show_modules_titles,
            title=f"EDT S{formsemestre.semestre_id} {formsemestre.titre_formation()}",
            view=view,
            views_names=views_names,
        )
    
    
    @bp.route("/formsemestre/edt_help_config/<int:formsemestre_id>")
    @scodoc
    @permission_required(Permission.ScoView)
    def formsemestre_edt_help_config(formsemestre_id: int):
        """Page d'aide à la configuration de l'extraction emplois du temps
        Affiche les identifiants extraits de l'ics et ceux de ScoDoc.
        """
        formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
        edt2group = sco_edt_cal.formsemestre_retreive_groups_from_edt_id(formsemestre)
        events_sco, edt_groups_ids = sco_edt_cal.load_and_convert_ics(formsemestre)
        return render_template(
            "formsemestre/edt_help_config.j2",
            formsemestre=formsemestre,
            edt2group=edt2group,
            edt_groups_ids=edt_groups_ids,
            events_sco=events_sco,
            sco=ScoData(formsemestre=formsemestre),
            ScoDocSiteConfig=ScoDocSiteConfig,
            title="Aide configuration EDT",
        )
    
    
    @bp.route(
        "/formsemestre_description/<int:formsemestre_id>/edit", methods=["GET", "POST"]
    )
    @scodoc
    @permission_required(Permission.EditFormSemestre)
    def edit_formsemestre_description(formsemestre_id: int):
        "Edition de la description d'un formsemestre"
        formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
        if not formsemestre.description:
            formsemestre.description = FormSemestreDescription()
            db.session.add(formsemestre)
            db.session.commit()
        formsemestre_description = formsemestre.description
        form = edit_description.FormSemestreDescriptionForm(obj=formsemestre_description)
        ok = True
        if form.validate_on_submit():
            if form.cancel.data:  # cancel button
                return redirect(
                    url_for(
                        "notes.formsemestre_editwithmodules",
                        scodoc_dept=g.scodoc_dept,
                        formsemestre_id=formsemestre.id,
                    )
                )
            # Vérification valeur dispositif
            if form.dispositif.data not in FORMSEMESTRE_DISPOSITIFS:
                flash("Dispositif inconnu", "danger")
                ok = False
    
            # Vérification dates inscriptions
            if form.date_debut_inscriptions.data:
                try:
                    date_debut_inscriptions_dt = datetime.datetime.strptime(
                        form.date_debut_inscriptions.data, scu.DATE_FMT
                    )
                except ValueError:
                    flash("Date de début des inscriptions invalide", "danger")
                    form.set_error("date début invalide", form.date_debut_inscriptions)
                    ok = False
            else:
                date_debut_inscriptions_dt = None
            if form.date_fin_inscriptions.data:
                try:
                    date_fin_inscriptions_dt = datetime.datetime.strptime(
                        form.date_fin_inscriptions.data, scu.DATE_FMT
                    )
                except ValueError:
                    flash("Date de fin des inscriptions invalide", "danger")
                    form.set_error("date fin invalide", form.date_fin_inscriptions)
                    ok = False
            else:
                date_fin_inscriptions_dt = None
            if ok:
                # dates converties
                form.date_debut_inscriptions.data = date_debut_inscriptions_dt
                form.date_fin_inscriptions.data = date_fin_inscriptions_dt
                # Affecte tous les champs sauf les images:
                form_image = form.image
                del form.image
                form_photo_ens = form.photo_ens
                del form.photo_ens
                form.populate_obj(formsemestre_description)
                # Affecte les images:
                for field, form_field in (
                    ("image", form_image),
                    ("photo_ens", form_photo_ens),
                ):
                    if form_field.data:
                        image_data = form_field.data.read()
                        max_length = current_app.config.get("MAX_CONTENT_LENGTH")
                        if max_length and len(image_data) > max_length:
                            flash(
                                f"Image trop grande ({field}), max {max_length} octets",
                                "danger",
                            )
                            return redirect(
                                url_for(
                                    "notes.edit_formsemestre_description",
                                    formsemestre_id=formsemestre.id,
                                    scodoc_dept=g.scodoc_dept,
                                )
                            )
                        try:
                            _ = PIL.Image.open(io.BytesIO(image_data))
                        except PIL.UnidentifiedImageError:
                            flash(
                                f"Image invalide ({field}), doit être une image",
                                "danger",
                            )
                            return redirect(
                                url_for(
                                    "notes.edit_formsemestre_description",
                                    formsemestre_id=formsemestre.id,
                                    scodoc_dept=g.scodoc_dept,
                                )
                            )
                        setattr(formsemestre_description, field, image_data)
    
                db.session.commit()
                flash("Description enregistrée", "success")
                return redirect(
                    url_for(
                        "notes.formsemestre_status",
                        formsemestre_id=formsemestre.id,
                        scodoc_dept=g.scodoc_dept,
                    )
                )
    
        return render_template(
            "formsemestre/edit_description.j2",
            form=form,
            formsemestre=formsemestre,
            formsemestre_description=formsemestre_description,
            sco=ScoData(formsemestre=formsemestre),
            title="Modif. description semestre",
        )
    
    
    @bp.route("/formsemestres/import_from_descr", methods=["GET", "POST"])
    @scodoc
    @permission_required(Permission.EditFormSemestre)
    @permission_required(Permission.EditFormation)
    def formsemestres_import_from_description():
        """Import de formation/formsemestre à partir d'un excel.
        Un seul module est créé. Utilisé pour EL.
        """
        form = edit_description.FormSemestresImportFromDescrForm()
        if form.validate_on_submit():
            if form.cancel.data:  # cancel button
                return redirect(
                    url_for(
                        "notes.index_html",
                        scodoc_dept=g.scodoc_dept,
                    )
                )
            datafile = request.files[form.fichier.name]
            image_archive_file = request.files[form.image_archive_file.name]
            create_formation = form.create_formation.data
            infos = import_from_descr.read_excel(datafile)
            images = _extract_images_from_zip(image_archive_file)
            _load_images_refs(infos, images)
            for linenum, info in enumerate(infos, start=1):
                info["formation_commentaire"] = (
                    info.get("formation_commentaire")
                    or f"importé de {request.files[form.fichier.name].filename}, ligne {linenum}"
                )
            formsemestres = import_from_descr.create_formsemestres_from_description(
                infos, create_formation=create_formation, images=images
            )
            current_app.logger.info(
                f"formsemestres_import_from_description: {len(formsemestres)} semestres créés"
            )
            flash(f"Importation et création de {len(formsemestres)} semestres")
            return render_template(
                "formsemestre/import_from_description_result.j2",
                formsemestres=formsemestres,
            )
    
        return render_template(
            "formsemestre/import_from_description.j2",
            title="Importation de semestres de formations monomodules",
            form=form,
            fields_description={
                key: import_from_descr.describe_field(key)
                for key in sorted(import_from_descr.FIELDS_BY_KEY)
            },
        )
    
    
    def _extract_images_from_zip(image_archive_file) -> dict[str, bytes]:
        """Read archive file, and build dict: { path : image_data }
        check that image_data is a valid image.
        """
        # Image suffixes supported by PIL
        exts = PIL.Image.registered_extensions()
        supported_extensions = tuple(ex for ex, f in exts.items() if f in PIL.Image.OPEN)
    
        images = {}
        with zipfile.ZipFile(image_archive_file) as archive:
            for file_info in archive.infolist():
                if file_info.is_dir() or file_info.filename.startswith("__"):
                    continue
                if not file_info.filename.lower().endswith(supported_extensions):
                    continue  # ignore non image files
                with archive.open(file_info) as file:
                    image_data = file.read()
                    try:
                        _ = PIL.Image.open(io.BytesIO(image_data))
                        images[file_info.filename] = image_data
                    except PIL.UnidentifiedImageError as exc:
                        current_app.logger.warning(
                            f"Invalid image in archive: {file_info.filename}"
                        )
                        raise ScoValueError(
                            f"Image invalide dans l'archive: {file_info.filename}",
                            dest_url=url_for(
                                "notes.formsemestres_import_from_description",
                                scodoc_dept=g.scodoc_dept,
                            ),
                            dest_label="Reprendre",
                        ) from exc
        return images
    
    
    def _load_images_refs(infos: list[dict], images: dict):
        """Check if all referenced images in excel (infos)
        are present in the zip archive (images) and put them in the infos dicts.
        """
        breakpoint()
        for linenum, info in enumerate(infos, start=1):
            for key in ("descr_image", "descr_photo_ens"):
                info[key] = (
                    info[key].strip() if isinstance(info[key], str) else None
                ) or None
                if info[key]:
                    if info[key] not in images:
                        raise ScoValueError(
                            f"Image référencée en ligne {linenum}, colonne {key} non trouvée dans le zip",
                            dest_url=url_for(
                                "notes.formsemestres_import_from_description",
                                scodoc_dept=g.scodoc_dept,
                            ),
                            dest_label="Reprendre",
                        )
                    info[key] = images[info[key]]
    
    
    @bp.route("/formsemestres/import_from_descr_sample", methods=["GET", "POST"])
    @scodoc
    @permission_required(Permission.ScoView)
    def formsemestres_import_from_description_sample():
        "Renvoie fichier excel à remplir"
        xls = import_from_descr.generate_sample()
        return scu.send_file(
            xls, "ImportSemestres", scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE
        )