Skip to content
Snippets Groups Projects
Select Git revision
1 result Searching

Module_correlation.Rmd

Blame
  • config.py 16.49 KiB
    # -*- coding: UTF-8 -*
    
    """Model : site config  WORK IN PROGRESS #WIP
    """
    
    import json
    import re
    import urllib.parse
    
    from flask import flash
    from app import current_app, db, log, models
    from app.comp import bonus_spo
    from app.scodoc.sco_exceptions import ScoValueError
    from app.scodoc import sco_utils as scu
    
    from app.scodoc.codes_cursus import (
        ABAN,
        ABL,
        ADC,
        ADJ,
        ADJR,
        ADM,
        ADSUP,
        AJ,
        ATB,
        ATJ,
        ATT,
        CMP,
        DEF,
        DEM,
        EXCLU,
        NAR,
        PASD,
        PAS1NCI,
        RAT,
        RED,
    )
    
    CODES_SCODOC_TO_APO = {
        ABAN: "ABAN",
        ABL: "ABL",
        ADC: "ADMC",
        ADJ: "ADM",
        ADJR: "ADM",
        ADM: "ADM",
        ADSUP: "ADM",
        AJ: "AJ",
        ATB: "AJAC",
        ATJ: "AJAC",
        ATT: "AJAC",
        CMP: "COMP",
        DEF: "NAR",
        DEM: "NAR",
        EXCLU: "EXC",
        NAR: "NAR",
        PASD: "PASD",
        PAS1NCI: "PAS1NCI",
        RAT: "ATT",
        RED: "RED",
        "NOTES_FMT": "%3.2f",
    }
    
    
    def code_scodoc_to_apo_default(code):
        """Conversion code jury ScoDoc en code Apogée
        (codes par défaut, c'est configurable via ScoDocSiteConfig.get_code_apo)
        """
        return CODES_SCODOC_TO_APO.get(code, "DEF")
    
    
    class ScoDocSiteConfig(models.ScoDocModel):
        """Config. d'un site
        Nouveau en ScoDoc 9: va regrouper les paramètres qui dans les versions
        antérieures étaient dans scodoc_config.py
        """
    
        __tablename__ = "scodoc_site_config"
    
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(128), nullable=False, index=True)
        value = db.Column(db.Text())
    
        BONUS_SPORT = "bonus_sport_func_name"
        NAMES = {
            BONUS_SPORT: str,
            "always_require_ine": bool,
            "SCOLAR_FONT": str,
            "SCOLAR_FONT_SIZE": str,
            "SCOLAR_FONT_SIZE_FOOT": str,
            "INSTITUTION_NAME": str,
            "INSTITUTION_ADDRESS": str,
            "INSTITUTION_CITY": str,
            "DEFAULT_PDF_FOOTER_TEMPLATE": str,
            "enable_entreprises": bool,
            "disable_passerelle": bool,  # remplace pref. bul_display_publication
            "month_debut_annee_scolaire": int,
            "month_debut_periode2": int,
            "disable_bul_pdf": bool,
            "user_require_email_institutionnel": bool,
            # CAS
            "cas_enable": bool,
            "cas_force": bool,
            "cas_allow_for_new_users": bool,
            "cas_login_redirect": bool,
            "cas_server": str,
            "cas_login_route": str,
            "cas_logout_route": str,
            "cas_validate_route": str,
            "cas_attribute_id": str,
            "cas_uid_from_mail_regexp": str,
            "cas_uid_use_scodoc": bool,
            "cas_edt_id_from_xml_regexp": str,
            # Assiduité
            "morning_time": str,
            "lunch_time": str,
            "afternoon_time": str,
        }
    
        def __init__(self, name, value):
            self.name = name
            self.value = value
    
        def __repr__(self):
            return f"<{self.__class__.__name__}('{self.name}', '{self.value}')>"
    
        @classmethod
        def get_dict(cls) -> dict:
            "Returns all data as a dict name = value"
            return {
                c.name: cls.NAMES.get(c.name, lambda x: x)(c.value)
                for c in ScoDocSiteConfig.query.all()
            }
    
        @classmethod
        def set_bonus_sport_class(cls, class_name):
            """Record bonus_sport config.
            If class_name not defined, raise NameError
            """
            if class_name not in cls.get_bonus_sport_class_names():
                raise NameError("invalid class name for bonus_sport")
            c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
            if c:
                log("setting to " + class_name)
                c.value = class_name
            else:
                c = ScoDocSiteConfig(cls.BONUS_SPORT, class_name)
            db.session.add(c)
            db.session.commit()
    
        @classmethod
        def get_bonus_sport_class_name(cls):
            """Get configured bonus function name, or None if None."""
            klass = cls.get_bonus_sport_class_from_name()
            if klass is None:
                return ""
            else:
                return klass.name
    
        @classmethod
        def get_bonus_sport_class(cls):
            """Get configured bonus function, or None if None."""
            return cls.get_bonus_sport_class_from_name()
    
        @classmethod
        def get_bonus_sport_class_from_name(cls, class_name=None):
            """returns bonus class with specified name.
            If name not specified, return the configured function.
            None if no bonus function configured.
            If class_name not found in module bonus_sport, returns None
            and flash a warning.
            """
            if not class_name:  # None or ""
                c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
                if c is None:
                    return None
                class_name = c.value
            if class_name == "":  # pas de bonus défini
                return None
            klass = bonus_spo.get_bonus_class_dict().get(class_name)
            if klass is None:
                flash(
                    f"""Fonction de calcul bonus sport inexistante: {class_name}.
                    Changez là ou contactez votre administrateur local."""
                )
            return klass
    
        @classmethod
        def get_bonus_sport_class_names(cls) -> list:
            """List available bonus class names
            (starting with empty string to represent "no bonus function").
            """
            return [""] + sorted(bonus_spo.get_bonus_class_dict().keys())
    
        @classmethod
        def get_bonus_sport_class_list(cls) -> list[tuple]:
            """List available bonus class names
            (starting with empty string to represent "no bonus function").
            """
            d = bonus_spo.get_bonus_class_dict()
            class_list = [(name, d[name].displayed_name) for name in d]
            class_list.sort(key=lambda x: x[1].replace(" du ", " de "))
            return [("", "")] + class_list
    
        @classmethod
        def get_code_apo(cls, code: str) -> str:
            """La représentation d'un code pour les exports Apogée.
            Par exemple, à l'IUT du H., le code ADM est réprésenté par VAL
            Les codes par défaut sont donnés dans sco_apogee_csv.
            """
            cfg = ScoDocSiteConfig.query.filter_by(name=code).first()
            if not cfg:
                code_apo = code_scodoc_to_apo_default(code)
            else:
                code_apo = cfg.value
            return code_apo
    
        @classmethod
        def get_codes_apo_dict(cls) -> dict[str:str]:
            "Un dict avec code jury : code exporté"
            return {code: cls.get_code_apo(code) for code in CODES_SCODOC_TO_APO}
    
        @classmethod
        def set_code_apo(cls, code: str, code_apo: str):
            """Enregistre nouvelle représentation du code"""
            if code_apo != cls.get_code_apo(code):
                cfg = ScoDocSiteConfig.query.filter_by(name=code).first()
                if cfg is None:
                    cfg = ScoDocSiteConfig(code, code_apo)
                else:
                    cfg.value = code_apo
                db.session.add(cfg)
                db.session.commit()
    
        @classmethod
        def is_cas_enabled(cls) -> bool:
            """True si on utilise le CAS"""
            cfg = ScoDocSiteConfig.query.filter_by(name="cas_enable").first()
            return cfg is not None and cfg.value
    
        @classmethod
        def is_cas_forced(cls) -> bool:
            """True si CAS forcé"""
            cfg = ScoDocSiteConfig.query.filter_by(name="cas_force").first()
            return cfg is not None and cfg.value
    
        @classmethod
        def cas_uid_use_scodoc(cls) -> bool:
            """True si cas_uid_use_scodoc"""
            cfg = ScoDocSiteConfig.query.filter_by(name="cas_uid_use_scodoc").first()
            return cfg is not None and cfg.value
    
        @classmethod
        def is_entreprises_enabled(cls) -> bool:
            """True si on doit activer le module entreprise"""
            cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
            return cfg is not None and cfg.value
    
        @classmethod
        def is_passerelle_disabled(cls):
            """True si on doit cacher les fonctions passerelle ("oeil")."""
            cfg = ScoDocSiteConfig.query.filter_by(name="disable_passerelle").first()
            return cfg is not None and cfg.value
    
        @classmethod
        def is_user_require_email_institutionnel_enabled(cls) -> bool:
            """True si impose saisie email_institutionnel"""
            cfg = ScoDocSiteConfig.query.filter_by(
                name="user_require_email_institutionnel"
            ).first()
            return cfg is not None and cfg.value
    
        @classmethod
        def is_bul_pdf_disabled(cls) -> bool:
            """True si on interdit les exports PDF des bulletins"""
            cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first()
            return cfg is not None and cfg.value
    
        @classmethod
        def enable_entreprises(cls, enabled: bool = True) -> bool:
            """Active (ou déactive) le module entreprises. True si changement."""
            return cls.set("enable_entreprises", "on" if enabled else "")
    
        @classmethod
        def disable_passerelle(cls, disabled: bool = True) -> bool:
            """Désactive (ou active) les fonctions liées à la présence d'une passerelle. True si changement."""
            return cls.set("disable_passerelle", "on" if disabled else "")
    
        @classmethod
        def disable_bul_pdf(cls, enabled=True) -> bool:
            """Interdit (ou autorise) les exports PDF. True si changement."""
            return cls.set("disable_bul_pdf", "on" if enabled else "")
    
        @classmethod
        def get(cls, name: str, default: str = "") -> str:
            "Get configuration param; empty string or specified default if unset"
            cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
            if cfg is None:
                return default
            return cls.NAMES.get(name, lambda x: x)(cfg.value or "")
    
        @classmethod
        def set(cls, name: str, value: str) -> bool:
            "Set parameter, returns True if change. Commit session."
            value_str = str(value or "").strip()
            if (ScoDocSiteConfig.query.filter_by(name=name).first() is None) or (
                cls.get(name) or ""
            ) != value_str:
                cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
                if cfg is None:
                    cfg = ScoDocSiteConfig(name=name, value=value_str)
                else:
                    cfg.value = value_str
                current_app.logger.info(
                    f"""ScoDocSiteConfig: recording {cfg.name}='{cfg.value[:32]}{
                        '...' if len(cfg.value)>32 else ''}'"""
                )
                db.session.add(cfg)
                db.session.commit()
                return True
            return False
    
        @classmethod
        def _get_int_field(cls, name: str, default=None) -> int:
            """Valeur d'un champ integer"""
            cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
            if (cfg is None) or cfg.value is None:
                return default
            return int(cfg.value)
    
        @classmethod
        def _set_int_field(
            cls,
            name: str,
            value: int,
            default=None,
            range_values: tuple = (),
        ) -> bool:
            """Set champ integer. True si changement."""
            if value != cls._get_int_field(name, default=default):
                if not isinstance(value, int) or (
                    range_values and (value < range_values[0]) or (value > range_values[1])
                ):
                    raise ValueError("invalid value")
                cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
                if cfg is None:
                    cfg = ScoDocSiteConfig(name=name, value=str(value))
                else:
                    cfg.value = str(value)
                db.session.add(cfg)
                db.session.commit()
                return True
            return False
    
        @classmethod
        def get_month_debut_annee_scolaire(cls) -> int:
            """Mois de début de l'année scolaire."""
            return cls._get_int_field(
                "month_debut_annee_scolaire", scu.MONTH_DEBUT_ANNEE_SCOLAIRE
            )
    
        @classmethod
        def get_month_debut_periode2(cls) -> int:
            """Mois de début de la seconde période (semestre) de l'année.
            Par défaut, 12 (décembre). Sera souvent juillet en hémisphère sud.
            """
            return cls._get_int_field("month_debut_periode2", scu.MONTH_DEBUT_PERIODE2)
    
        @classmethod
        def set_month_debut_annee_scolaire(
            cls, month: int = scu.MONTH_DEBUT_ANNEE_SCOLAIRE
        ) -> bool:
            """Fixe le mois de début des années scolaires.
            True si changement.
            """
            if cls._set_int_field(
                "month_debut_annee_scolaire", month, scu.MONTH_DEBUT_ANNEE_SCOLAIRE, (1, 12)
            ):
                log(f"set_month_debut_annee_scolaire({month})")
                return True
            return False
    
        @classmethod
        def set_month_debut_periode2(cls, month: int = scu.MONTH_DEBUT_PERIODE2) -> bool:
            """Fixe le mois de début des années scolaires.
            True si changement.
            """
            if cls._set_int_field(
                "month_debut_periode2", month, scu.MONTH_DEBUT_PERIODE2, (1, 12)
            ):
                log(f"set_month_debut_periode2({month})")
                return True
            return False
    
        @classmethod
        def get_perso_links(cls) -> list["PersonalizedLink"]:
            "Return links"
            data_links = cls.get("personalized_links")
            if not data_links:
                return []
            try:
                links_dict = json.loads(data_links)
            except json.decoder.JSONDecodeError as exc:
                # Corrupted data ? erase content
                cls.set("personalized_links", "")
                raise ScoValueError(
                    "Attention: liens personnalisés erronés: ils ont été effacés."
                ) from exc
            return [PersonalizedLink(**item) for item in links_dict]
    
        @classmethod
        def set_perso_links(cls, links: list["PersonalizedLink"] = None):
            "Store all links"
            if not links:
                links = []
            links_dict = [link.to_dict() for link in links]
            data_links = json.dumps(links_dict)
            cls.set("personalized_links", data_links)
    
        @classmethod
        def extract_cas_id(cls, email_addr: str) -> str | None:
            "Extract cas_id from mail, using regexp in config. None if not possible."
            exp = cls.get("cas_uid_from_mail_regexp")
            if not exp or not email_addr:
                return None
            try:
                match = re.search(exp, email_addr)
            except re.error:
                log("error extracting CAS id from '{email_addr}' using regexp '{exp}'")
                return None
            if not match:
                log("no match extracting CAS id from '{email_addr}' using regexp '{exp}'")
                return None
            try:
                cas_id = match.group(1)
            except IndexError:
                log(
                    "no group found extracting CAS id from '{email_addr}' using regexp '{exp}'"
                )
                return None
            return cas_id
    
        @classmethod
        def cas_uid_from_mail_regexp_is_valid(cls, exp: str) -> bool:
            "True si l'expression régulière semble valide"
            # check that it compiles
            try:
                pattern = re.compile(exp)
            except re.error:
                return False
            # and returns at least one group on a simple cannonical address
            match = pattern.search("emmanuel@exemple.fr")
            return match is not None and len(match.groups()) > 0
    
        @classmethod
        def cas_edt_id_from_xml_regexp_is_valid(cls, exp: str) -> bool:
            "True si l'expression régulière semble valide"
            # check that it compiles
            try:
                _ = re.compile(exp)
            except re.error:
                return False
            return True
    
        @classmethod
        def assi_get_rounded_time(cls, label: str, default: str) -> float:
            "Donne l'heure stockée dans la config globale sous label, en float arrondi au quart d'heure"
            return _round_time_str_to_quarter(cls.get(label, default))
    
    
    def _round_time_str_to_quarter(string: str) -> float:
        """Prend une heure iso '12:20:23', et la converti en un nombre d'heures
        en arrondissant au quart d'heure: (les secondes sont ignorées)
        "12:20:00" -> 12.25
        "12:29:00" -> 12.25
        "12:30:00" -> 12.5
        """
        parts = [*map(float, string.split(":"))]
        hour = parts[0]
        minutes = round(parts[1] / 60 * 4) / 4
        return hour + minutes
    
    
    class PersonalizedLink:
        def __init__(self, title: str = "", url: str = "", with_args: bool = False):
            self.title = str(title or "")
            self.url = str(url or "")
            self.with_args = bool(with_args)
    
        def get_url(self, params: dict = {}) -> str:
            if not self.with_args:
                return self.url
            query_string = urllib.parse.urlencode(params)
            if "?" in self.url:
                return self.url + "&" + query_string
            return self.url + "?" + query_string
    
        def to_dict(self) -> dict:
            "as dict"
            return {"title": self.title, "url": self.url, "with_args": self.with_args}