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 __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.code = None # chargé ultérieurement 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 self.coeffs = None # chargés ultérieurement def charge_ac(self, apprentissages): self.apprentissages = apprentissages def charge_coeffs(self, coeffs): self.coeffs = coeffs 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))) if acs_finaux: 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 if l.strip()] # supprime les espaces inutiles et les lignes vides 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 nettoie_coeffs(self): coeffs_finaux = {} for (comp, chaine) in enumerate(self.coeffs): if "coef" in chaine: # s'il y a un coeff champ = chaine.split(" ") coeff = eval(champ[1]) coeffs_finaux["RT" + str(comp + 1)] = coeff elif "X" in chaine: coeff = 0 coeffs_finaux["RT" + str(comp + 1)] = coeff self.coeffs = coeffs_finaux 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): 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) # Remplace http(s) URLs pour markdown output = re.sub( r"(http(s)?://[\w\d:#@%/;~_?\+-=\\\.&]*)", r"[\1](\1)", output ) # Remplace les guillemets # ne traite pas tous les cas, mais arrange la majorité output = re.sub( r"\"(.*?)\"", r"«\1»", output, flags=re.DOTALL) # On utilise les guillements français (ajout automatique des bons espaces) output = output.replace("«", r"\og ").replace("»", r"\fg{}") 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 not self.prerequis or AUCUN_PREREQUIS.lower() in self.prerequis.lower(): self.prerequis = AUCUN_PREREQUIS else: ressources = nettoie_liste_ressources(self.prerequis) if ressources: self.prerequis = ressources 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""" volumes = None 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 = [] marqueur = False identifiants = ["Contenus", "Objectifs visés"] # Identifiant marquant la ligne des contenus for id in identifiants: presence = [ligne.startswith(id) for ligne in champs] if True in presence and not marqueur: # la ligne commençant par l'identifiant indicec = presence.index(True) marqueur = 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_contexte(self): """Partant du contexte détaillé d'une ressource, la transforme en markdown en générant les listes à puces""" contexte = self.contexte.replace(" / ", "/") self.contexte = convert_to_markdown(contexte) 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() self.nettoie_coeffs() # Remet en forme le descriptif self.split_description() self.nettoie_contexte() self.nettoie_contenu() print(f"{self.code} {self.semestre}") 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 "???", "coeffs": self.coeffs, "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 if "|" not in c] 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("()", contenu) for m in liens: contenu = contenu.replace(m, "") contenu = contenu.replace("", "") 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 if get_marqueurs(contenus): temp = contenus.split("\n") temp = [t for t in temp if t.replace("\t", "").rstrip()] return "\n".join(temp) else: # pas de marqueur => respect des paragraphes contenus = contenus.replace("\n\n", "\\\\\n") 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"(\d\s\))", contenu) 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""" if self.description: self.description = convert_to_markdown(self.description) else: self.description = "" def nettoie_champs(self): """Lance le nettoyage de tous les champs de la saé""" self.nettoie_heures_sae() self.nettoie_semestre() self.nettoie_acs() self.nettoie_ressources() self.nettoie_description() self.nettoie_livrables_sae() self.nettoie_mots_cles() self.nettoie_coeffs() def to_yaml(self): """Exporte la saé 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), "coeffs": self.coeffs, "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)