Referentiels/python/ressourcedocx.py

605 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import re
from officiel import *
from modeles import *
from officiel import supprime_accent_espace, get_code_from_nom_using_dict
import ruamel.yaml
from ruamel.yaml.scalarstring import FoldedScalarString as folded
from tools import caracteres_recalcitrants
__LOGGER = logging.getLogger(__name__)
class Docx():
"""Classe de base pour les ressources/saé/exemples du docx"""
__LOGGER = logging.getLogger(__name__)
def __init__(self, nom, brut):
self.nom = nom
self.brut = brut # les données brutes de la ressource/saé
self.semestre = None # le semestre de la ressource/saé (chargé ultérieurement)
self.apprentissages = None # les acs (chargés ultérieurement)
self.mots = None # les mots-clés chargés ultérieurement
def charge_ac(self, apprentissages):
self.apprentissages = apprentissages
def __str__(self):
print(self.nom + " " + self.code)
def nettoie_semestre(self):
"""Pour une ressource, ou une SAE, nettoie le champ semestre"""
if self.semestre:
if "1" in self.semestre:
self.semestre = "S1"
elif "2" in self.semestre:
self.semestre = "S2"
else:
Docx.__LOGGER.warning(f"nettoie_semestre : dans \"{self.nom}, PAS de semestre => rattaché au S2")
self.semestre = "S2"
else:
Docx.__LOGGER.warning(f"nettoie_semestre : dans \"{self.nom}, PAS de semestre => rattaché au S2")
self.semestre = "S2"
def nettoie_acs(self):
"""Nettoie les acs d'une ressource ou d'une saé,
en les remplaçant par leur code pour les 3 compétences"""
if len(self.apprentissages) != 3:
Docx.__LOGGER.warning(f"nettoie_acs : Problème dans le nombre de compétences de {self.nom}")
dico = {}
for comp in range(3):
donnees = self.apprentissages[comp] # chaine de caractères listant les ACS
acs_avec_code = devine_acs_by_code(donnees) # récupère les codes des acs
acs_avec_code = [ac.replace(" ", "") for ac in acs_avec_code] # supprime les espaces inutiles
acs_avec_nom = devine_code_by_nom_from_dict(donnees, DATA_ACS) # récupère les codes en utilisant les noms
acs_avec_nom = [ac.replace(" ", "") for ac in acs_avec_nom] # supprime les espaces inutiles
if acs_avec_code and set(acs_avec_nom).intersection(set(acs_avec_code)) != set(acs_avec_nom):
Docx.__LOGGER.warning(f"Dans {self.nom}, revoir les ACS : {acs_avec_code} vs {acs_avec_nom}")
acs_finaux = sorted(list(set(acs_avec_code + acs_avec_nom)))
dico["RT" + str(comp + 1)] = acs_finaux
self.apprentissages = dico # Mise à jour du champ
def nettoie_mots_cles(self):
mots = self.mots # .encode('utf8', 'ignore').decode('utf8')
mots = mots.replace(".", "").replace(";", ",")
liste_mots = mots.split(",")
liste_mots = [l.strip() for l in liste_mots] # supprime les espaces inutiles
mots = ", ".join(liste_mots)
self.mots = mots
def nettoie_titre(self, data_titres):
"""Nettoie le titre d'une ressource ou d'une SAE en utilisant les titres officiels
fournis dans le yaml (via le dictionnaire DATA_RESSOURCES)"""
def devine_nom(champ):
champ_purge = supprime_accent_espace(champ)
for sem in data_titres:
for code in data_titres[sem]:
nom_purge = supprime_accent_espace(data_titres[sem][code])
if champ_purge.startswith(nom_purge):
return data_titres[sem][code] # le bon nom
old = self.nom
titre = devine_nom(self.nom)
if titre and titre != old:
Docx.__LOGGER.warning(f"nettoie_titre : {old} => titre deviné \"{titre}\"")
self.nom = titre
def dico_to_yaml(self, dico):
output = ruamel.yaml.dump(dico, Dumper=ruamel.yaml.RoundTripDumper,
allow_unicode=True, width=100)
# Purge les lignes vides en trop
lignes = output.split("\n")
lignes_finales = []
for (i, ligne) in enumerate(lignes):
if ligne.rstrip() == "":
if i != len(lignes)-1 and lignes[i+1].rstrip() != "":
lignes_finales.append(ligne) # ajoute la ligne si la suivante n'est pas vide
else:
lignes_finales.append(ligne)
output = "\n".join(lignes_finales)
# Ajoute les espaces manquants dans les listes markdown
# écrite sur plusieurs lignes
lignes = output.split("\n") # pour vérif
lignes_finales = []
avec_marqueur = False
last_marqueur = 0
for (i, ligne) in enumerate(lignes):
if "dAPI" in ligne:
print("ici")
ligne = ligne.replace("\t", " "*2)
if " *" in ligne and avec_marqueur == False:
avec_marqueur = True
last_marqueur = " *"
elif " *" in ligne and avec_marqueur == False:
avec_marqueur = True
last_marqueur = " *"
else: # pas de marqueur
if ligne.strip() == "" or ligne[0] != " ":
avec_marqueur = False # fin du marqueur
elif ligne.strip() != "" and avec_marqueur == True:
if last_marqueur == " *":
ligne = " "*4 + ligne.lstrip()
else:
ligne = " "*6 + ligne.lstrip()
lignes_finales.append(ligne)
output = "\n".join(lignes_finales)
return output
class RessourceDocx(Docx):
"""Classe modélisant les ressources, lorsqu'elles sont extraites du docx"""
__LOGGER = logging.getLogger(__name__)
def charge_informations(self, code, semestre, heures_encadrees, tp, sae, prerequis, description, mots):
self.code = code
self.semestre = semestre # <--
self.heures_encadrees = heures_encadrees
self.tp = tp
self.sae = sae
self.prerequis = prerequis
self.description = description
self.contexte = None
self.contenu = None
self.mots = mots
def nettoie_titre_ressource(self):
"""Nettoie le titre d'une ressource ou d'une SAE en utilisant les titres officiels
fournis dans le yaml (via le dictionnaire DATA_RESSOURCES)"""
old = self.nom
self.nettoie_titre(DATA_RESSOURCES)
titre2 = get_officiel_ressource_name_by_code(self.code)
if titre2 != self.nom:
self.nom = titre2
RessourceDocx.__LOGGER.warning(f"nettoie_titre : {old} => titre d'après PN \"{titre2}\"")
def nettoie_code(self):
"""Recherche le code de la forme RXXX"""
if self.code:
codes = devine_ressources_by_code(self.code)
if len(codes) == 1:
self.code = codes[0]
else:
code_devine = get_code_from_nom_using_dict(self.nom, DATA_RESSOURCES)
if code_devine:
RessourceDocx.__LOGGER.warning(f"nettoie_code : \"{self.nom}\" => code {code_devine}")
self.code = code_devine
else:
self.code = None
if not self.code:
RessourceDocx.__LOGGER.warning(f"nettoie_code : \"{self.nom}\" => code manquant")
def nettoie_prerequis(self):
"""Nettoie les prérequis"""
if self.prerequis:
self.prerequis = nettoie_liste_ressources(self.prerequis)
if not self.prerequis:
self.prerequis = "Aucun"
def nettoie_sae(self):
"""Nettoie le champ SAe d'une ressource en détectant les codes"""
SAE_avec_code = devine_sae_by_code(self.sae)
liste = [l.rstrip() for l in SAE_avec_code]
self.sae = liste
if not self.sae:
RessourceDocx.__LOGGER.warning(f"nettoie_sae: dans {self.nom} pas de SAE (:")
def nettoie_heures(self):
"""Nettoie le champ (horaire) (de la forme 46h ou 33...) pour en extraire la valeur numérique :
le champ peut contenir 2 volumes (heures formation puis heures tp), auquel cas les 2 valeurs sont renvoyées
dans un tuple"""
if self.heures_encadrees: # si les heures encadrées sont renseignées
volumes = nettoie_champ_heure(self.heures_encadrees)
if self.tp:
self.tp = nettoie_champ_heure(self.tp)
if isinstance(volumes, int):
self.heures_encadrees = volumes
elif isinstance(volumes, tuple):
self.heures_encadrees = volumes[0]
if not self.tp:
self.tp = volumes[1]
elif self.tp != volumes[1]:
RessourceDocx.__LOGGER.warning(r"nettoie_heure: ans {self.nom}, pb dans les heures tp/td")
else:
self.heures_encadrees = None
def split_description(self):
"""Découpe le champ description en un contexte+un contenu ; si pas possible """
champs = self.description.split("\n")
champs = [c for c in champs if c] # supprime les lignes vides
indicea = 0 # la ligne mentionnant le contexte
if True in [ligne.startswith("Contexte et ") for ligne in champs]: # la ligne commençant par Contenus
indicea = [ligne.startswith("Contexte et ") for ligne in champs].index(True)
indicec = 0
contexte = []
if True in [ligne.startswith("Contenus") for ligne in champs]: # la ligne commençant par Contenus
indicec = [ligne.startswith("Contenus") for ligne in champs].index(True)
if True in [ligne.startswith("Contexte et ") for ligne in champs]:
contexte = champs[indicea + 1:indicec]
else:
contexte = champs[:indicec]
# suppression des lignes vides
contexte = "\n".join(remove_ligne_vide(contexte))
# suppression des liens
contexte = remove_link(contexte)
if not contexte:
contexte = "Aucun"
contenu = "\n".join(champs[indicec + 1:])
# sauvegarde des champs
self.contexte = contexte
self.contenu = contenu
def nettoie_contenu(self):
"""Partant du contenu détaillé d'une ressource, la transforme
en markdown en générant les listes à puces"""
contenu = self.contenu.replace(" / ", "/")
self.contenu = convert_to_markdown(contenu)
def nettoie_champ(self):
"""Lance le nettoyage des champs"""
self.nettoie_code()
self.nettoie_titre_ressource()
self.nettoie_heures()
self.nettoie_semestre()
self.nettoie_acs()
self.nettoie_sae()
self.nettoie_prerequis()
self.nettoie_mots_cles()
# Remet en forme le descriptif
self.split_description()
self.nettoie_contenu()
def to_yaml(self):
"""Exporte la ressource en yaml"""
dico = {"nom": self.nom,
"code": self.code,
"semestre" : int(self.semestre[1]),
"heures_formation": self.heures_encadrees if self.heures_encadrees else "???",
"heures_tp": self.tp if self.tp else "???",
"acs": self.apprentissages,
"sae": self.sae,
"prerequis": self.prerequis,
"contexte": folded(self.contexte),
"contenu": folded(self.contenu),
"motscles": self.mots if self.mots else ""
}
return self.dico_to_yaml(dico)
def nettoie_champ_heure(champ):
try: # champ contenant uniquement un nbre d'heure
volumes = int(champ)
return volumes
except:
volumes = re.findall("(\d{2}\D|\d{1}\D)", champ)
if len(volumes) == 1:
return int(volumes[0][:-1])
elif len(volumes) == 2:
volumes = sorted(volumes, reverse=True)
return (int(volumes[0][:-1]), int(volumes[1][:-1]))
def nettoie_liste_ressources(contenu):
"""Nettoie un contenu contenant une liste ressources, en extrayant les codes ressources
et en les fournissant les codes extraits dans une liste
"""
R_avec_code = devine_ressources_by_code(contenu)
R_avec_nom = devine_code_by_nom_from_dict(contenu, DATA_RESSOURCES)
liste = R_avec_code + R_avec_nom
liste = [l.strip().replace(",", "").replace(".", "") for l in liste] # supprime les espaces et les ponctuations restantes
return sorted(list(set(liste)))
def devine_acs_by_code(champ):
"""Recherche les codes ressources de la forme ACXXX ou AC0XXX dans champ ;
ramène les codes AC0XXX à 3 chiffres.
"""
codes3 = re.findall(r"(AC[0-9][0-9][0-9]\D)", champ) # de code à 3 chiffres
codes4 = re.findall(r"(AC0[0-9][0-9][0-9])", champ)
codes3 = [c.rstrip() for c in codes3]
codes4 = [c.rstrip() for c in codes4]
codes4 += [ "AC0" + c[-3:] for c in codes3] # ajoute les 0 manquants des acs (codage AC0111)
codes4 = [c.strip() for c in codes4]
return sorted(list(set(codes4)))
def devine_ressources_by_code(champ):
"""Recherche les codes ressources de la forme RXXX dans champ ;
"""
codes1 = re.findall(r"(R\d{3})", champ) # de code à 3 chiffres
codes2 = re.findall(r"(R\d{3}\D)", champ)
codes = codes1 + [c.strip() for c in codes2]
return sorted(list(set(codes)))
def devine_ressources_by_nom(donnees):
"""Partant d'une chaine de caractères, détermine les ressources
présentes dans la donnée, en utilisant les infos officielles de
ressources.yml"""
donnees_purge = supprime_accent_espace(donnees)
codes = []
for sem in DATA_RESSOURCES:
for code in DATA_RESSOURCES[sem]:
nom_purge = supprime_accent_espace(DATA_RESSOURCES[sem][code])
if nom_purge in donnees_purge:
codes += [code]
return sorted(list(set(codes)))
def devine_sae_by_code(donnees):
"""Partant d'une chaine de caractères, détermine les codes des SAE"""
codes = re.findall(r"(SAE\d\d)", donnees)
codes += re.findall(r"(SAÉ\d\d)", donnees)# de code à 3 chiffres
for (i, code) in enumerate(codes):
codes[i] = codes[i].replace("E", "É")
return sorted(list(set(codes)))
def remove_link(contenu):
liens = re.findall("(<a\s.*\">)", contenu)
for m in liens:
contenu = contenu.replace(m, "")
contenu = contenu.replace("</a>", "")
return contenu
def remove_ligne_vide(contenus):
"""Supprime les lignes vides"""
if isinstance(contenus, list):
return [c for c in contenus if c.rstrip()]
else: # contenu = chaine
temp = contenus.split("\n")
temp = [t for t in temp if t.replace("\t", "").rstrip()]
return "\n".join(temp)
def get_marqueur_numerique(contenu):
"""Revoie la liste des marqueurs numériques"""
m = re.findall(r"(\d/|\d\s/)", contenu)
m += re.findall(r"(\d\s\)|\d\))", contenu) # les marqueurs de la forme 1)
m += re.findall(r"(--)\s", contenu)
m += re.findall(r"(--)\t", contenu)
return m
def get_marqueurs(contenu):
"""Renvoie la liste des marqueurs (à 1 caractère) partant d'un contenu - splitable en plusieurs lignes
(éventuellement vide)"""
contenus = [ligne.rstrip() for ligne in contenu.split("\n")] # les contenus
marqueurs = []
for ligne in contenus:
m = re.search(r"(\t)*", ligne) # y a-t-il des tabulations ?
if m.group() != "":
ajout = m.group()
else:
ajout = ""
ligne = ligne.replace("\t","").rstrip() # supprime les tabulations pour rapatrier le marqueur en début de ligne
if ligne: # si la ligne n'est pas vide, cherche le marqueur en début de ligne (si 1 caractère)
if ligne[0] not in string.ascii_letters and ligne[0] != "É" and ligne[0] != "/":
marqueurs += [ajout + ligne[0]] # tous les symboles
marqueurs_finaux = [] # tri les marqueurs en supprimant les doublons et en gardant un ordre (pour détecter les sous listes)
for m in marqueurs:
if m not in marqueurs_finaux:
marqueurs_finaux.append(m)
return marqueurs_finaux
def get_marqueur_from_liste(ligne, marqueurs):
"""Renvoie le marqueur qui marque le début d'une ligne parmi une liste de marqueurs recherchés"""
for m in marqueurs:
if ligne.startswith(m):
return m
def remplace_marqueur_numerique_with_caracteres(contenu):
"""Remplace les marqueurs numériques par des marqueurs > lorsque présents dans un contenu"""
marqueurs_numeriques = get_marqueur_numerique(contenu)
for m in marqueurs_numeriques: # remplace les marqueurs numériques
contenu = contenu.replace(m, ">")
return contenu
def convert_to_markdown(contenu):
"""Convertit un contenu avec des marqueurs en markdown"""
contenu = remove_link(contenu) # supprime les liens
contenu = remplace_marqueur_numerique_with_caracteres(contenu)
marqueurs_finaux = get_marqueurs(contenu)
lignes = contenu.split("\n")
contenus_fin = lignes[:] # copie des ligne
for (i, ligne) in enumerate(lignes):
m = get_marqueur_from_liste(ligne, marqueurs_finaux) # identifie la présence d'un marqueur dans la ligne
if m:
pos = marqueurs_finaux.index(m)
ligne = "\t" * (pos) + "* " + ligne.replace(m, "").replace("\t", "").rstrip()
# corrige les espaces après les marqueurs
champ = re.findall(r"(\*\s+)\w", ligne)
for c in champ:
ligne = ligne.replace(c, "* ")
contenus_fin[i] = ligne
contenu = "\n\n".join(contenus_fin)
return contenu
class SAEDocx(Docx):
"""Classe modélisant un chapeau de SAé relu dans les docx"""
__LOGGER = logging.getLogger(__name__)
def charge_informations(self, code, semestre, heures_encadrees, tp, projet, description, ressources, livrables, mots):
self.code = code
self.semestre = semestre # <--
self.heures_encadrees = heures_encadrees
self.tp = tp
self.projet = projet
self.description = description
self.ressources = ressources
self.livrables = livrables
self.mots = mots
def nettoie_titre_sae(self):
"""Nettoie le titre d'une SAE en utilisant les titres officiels
fournis dans le yaml (via le dictionnaire DATA_RESSOURCES)"""
old = self.nom
self.nettoie_titre(DATA_SAES)
titre2 = get_officiel_sae_name_by_code(self.code)
if titre2 != self.nom:
self.nom = titre2
SAEDocx.__LOGGER.warning(f"nettoie_titre : {old} => titre d'après PN \"{titre2}\"")
def nettoie_code(self):
"""Recherche les codes de la forme SAE|éXX """
if self.code:
codes = devine_sae_by_code(self.code)
if len(codes) == 1:
self.code = codes[0]
else:
code_devine = get_code_from_nom_using_dict(self.nom, DATA_SAES)
if code_devine:
SAEDocx.__LOGGER.warning(f"nettoie_code : \"{self.nom}\" => code {code_devine}")
self.code = code_devine
else:
self.code = None
if not self.code:
SAEDocx.__LOGGER.warning(f"nettoie_code : \"{self.nom}\" => code manquant")
def nettoie_heures_sae(self):
"""Nettoie les champs (horaires) des saes"""
if self.heures_encadrees: # si les heures encadrées sont renseignées
self.heures_encadrees = nettoie_champ_heure(self.heures_encadrees)
else:
SAEDocx.__LOGGER.warning(f"nettoie_heures_sae: dans {self.nom}, manque les heures de formation")
self.heures_encadrees = "???"
if self.tp:
self.tp = nettoie_champ_heure(self.tp)
else:
SAEDocx.__LOGGER.warning(f"nettoie_heures_sae: dans {self.nom}, manque les heures de tp")
self.tp = "???"
if self.projet:
self.projet = nettoie_champ_heure(self.projet)
else:
SAEDocx.__LOGGER.warning(f"nettoie_heures_sae: dans {self.nom}, manque les heures de projet")
self.projet = "???"
try:
if self.heures_encadrees < self.tp:
SAEDocx.__LOGGER.warning(f"nettoie_heures_sae: dans {self.nom}, pb dans les heures formations/tp")
except:
pass
def nettoie_livrables_sae(self):
"""Partant du contenu détaillé d'une ressource, la transforme
en markdown en générant les listes à puces"""
self.livrables = convert_to_markdown(self.livrables)
def nettoie_ressources(self):
"""Nettoie le champ ressource d'une sae en détectant les codes"""
self.ressources = nettoie_liste_ressources(self.ressources)
if not self.ressources:
SAEDocx.__LOGGER.warning(f"nettoie_ressources: dans {self.nom} pas de ressources (:")
def nettoie_description(self):
"""Nettoie le champ description"""
self.description = convert_to_markdown(self.description)
def to_yaml(self):
"""Exporte la ressource en yaml"""
dico = {"titre": self.nom,
"code": self.code,
"semestre": int(self.semestre[1]),
"heures_encadrees": self.heures_encadrees if self.heures_encadrees else "???",
"tp": self.tp if self.tp else "???",
"projet": self.projet if self.projet else "???",
"description": folded(self.description),
"acs": self.apprentissages,
"ressources": self.ressources,
"livrables": folded(self.livrables),
"motscles": self.mots if self.mots else ""
}
return self.dico_to_yaml(dico)
class ExempleSAEDocx(Docx):
"""Classe modélisant les exemples de SAE tel que relu dans les Docx"""
def __init__(self, nom, brut, code):
self.nom = nom.rstrip()
self.brut = brut # les données brutes de la ressource
self.code = code # code de la SAE à laquelle l'exemple est raccroché
# Ajoute le semestre de la SAE
self.semestre = int(get_officiel_sem_sae_by_code(code)[1])
def charge_informations(self, description, formes, problematique, modalite):
self.description = description
self.formes = formes # <--
self.problematique = problematique
self.modalite = modalite
def nettoie_description(self):
"""Nettoie la description d'un exemple de SAE"""
self.description = convert_to_markdown(self.description)
def nettoie_problematique(self):
"""Nettoie la description d'un exemple de SAE"""
if self.problematique:
self.problematique = convert_to_markdown(self.problematique)
else:
self.problematique = ""
def nettoie_modalite(self):
"""Nettoie les modalités (d'évaluation) d'un exemple de SAE"""
if self.modalite:
self.modalite = convert_to_markdown(self.modalite)
else:
self.modalite = f"Les même que les livrables et les productions de la {self.code}"
def nettoie_formes(self):
"""Nettoie les modalités (d'évaluation) d'un exemple de SAE"""
if self.formes:
self.formes = convert_to_markdown(self.formes)
else:
self.formes = ""
def nettoie_champs(self):
"""Déclenche le nettoyage des champs de l'exemple"""
self.nom = self.nom.strip()
self.nettoie_modalite()
self.nettoie_description()
self.nettoie_problematique()
self.nettoie_formes()
def to_yaml(self):
"""Exporte la ressource en yaml"""
dico = {"titre": self.nom,
"code": self.code,
"semestre": self.semestre,
"description": folded(self.description),
"formes": folded(self.formes),
"problematique": folded(self.problematique) if self.problematique !="" else "",
"modalite": folded(self.modalite),
}
return self.dico_to_yaml(dico)
if __name__=="__main__":
# Eléments de test
for sem in DATA_RESSOURCES:
for code in DATA_RESSOURCES[sem]:
nom_data = supprime_accent_espace(DATA_RESSOURCES[sem][code])
print(nom_data)