diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 5d1b2d729..98e0eb6f0 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -247,7 +247,7 @@ class ScoExcelSheet: if idx < 26: # one letter key return chr(idx + 65) else: # two letters AA..ZZ - first = (idx // 26) + 66 + first = (idx // 26) + 64 second = (idx % 26) + 65 return "" + chr(first) + chr(second) @@ -265,6 +265,25 @@ class ScoExcelSheet: else: self.ws.column_dimensions[self.i2col(cle)].width = value + def set_column_dimension_hidden(self, cle=None, value=21): + """Détermine la largeur d'une colonne. cle -- identifie la colonne ("A" "B", ... ou 0, 1, 2, ...) si None, + value donne la liste des largeurs de colonnes depuis A, B, C, ... value -- la dimension (unité : 7 pixels + comme affiché dans Excel) + """ + if cle is None: + for i, val in enumerate(value): + self.ws.column_dimensions[self.i2col(i)].width = val + # No keys: value is a list of widths + elif isinstance(cle, str): # accepts set_column_with("D", ...) + self.ws.column_dimensions[cle].hidden = value + else: + self.ws.column_dimensions[self.i2col(cle)].hidden = value + + def merge(self, start_row, end_row, start_column, end_column): + self.ws.merged_cells.ranges.append( + f"{self.i2col(start_column)}{start_row}:{self.i2col(end_column)}{end_row}" + ) + def set_row_dimension_height(self, cle=None, value=21): """Détermine la hauteur d'une ligne. cle -- identifie la ligne (1, 2, ...) si None, value donne la liste des hauteurs de colonnes depuis 1, 2, 3, ... value -- la dimension @@ -337,14 +356,15 @@ class ScoExcelSheet: return cell - def make_row(self, values: list, style=None, comments=None) -> list: - "build a row" - # TODO make possible differents styles in a row + def make_row(self, values: list, style=None, comments=None): + # styles: a list of style(s) instead of a unique style (in a row) if comments is None: comments = [None] * len(values) + if not isinstance(style, list): + style = [style] * len(values) return [ - self.make_cell(value, style, comment) - for value, comment in zip(values, comments) + self.make_cell(value, a_style, comment) + for value, a_style, comment in zip(values, style, comments) ] def append_single_cell_row(self, value: any, style=None): diff --git a/app/scodoc/sco_excel_add.py b/app/scodoc/sco_excel_add.py new file mode 100644 index 000000000..36927551f --- /dev/null +++ b/app/scodoc/sco_excel_add.py @@ -0,0 +1,221 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2019 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 +# +############################################################################## + + +""" Excel file handling +""" + +# from sco_utils import * +# from sco_excel import * +import time, datetime + +from openpyxl.styles import Font, Alignment, PatternFill, Side, Border + +from app.scodoc.sco_excel import COLORS + + +def make_font( + bold=False, + italic=False, + font_name=None, + font_size=None, + color=COLORS.BLACK.value, + style=None, + uline=False, +): + font = None + if bold or italic or font_name or font_size: + font = Font() + if bold: + font.bold = bold + if uline: + font.underline = Font.UNDERLINE_SINGLE + if italic: + font.italic = italic + font.name = font_name if font_name else "Arial" + if font_size: + font.height = 20 * font_size + if color: + font.color = color + if font and style: + style["font"] = font + return font + + +def make_alignment(halign=None, valign=None, orientation=None, style=None): + alignment = None + if halign or valign or orientation: + alignment = Alignment() + if halign: + alignment.horz = halign + if valign: + alignment.vert = valign + if orientation: + alignment.rota = orientation + if alignment and style: + breakpoint() + style["alignment"] = alignment + return alignment + + +def make_numfmt(fmt, style=None): + if fmt and style: + style["num_format_str"] = fmt + + +def make_pattern(bgcolor, style=None): + pattern = PatternFill(fill_type="solid", fgColor=bgcolor) + if pattern and style: + style["fill"] = pattern + return pattern + + +Sides = { + "none": Side(border_style=None), + "thin": Side(border_style="thin"), + "medium": Side(border_style="medium"), + "thick": Side(border_style="thick"), + "filet": Side(border_style="hair"), +} + + +def make_borders(left=None, top=None, bottom=None, right=None, style=None): + border = None + if left or right or top or bottom: + border = Border() + if left: + border.left = Sides[left] + if right: + border.right = Sides[right] + if top: + border.top = Sides[top] + if bottom: + border.bottom = Sides[bottom] + if border and style: + style["border"] = border + return border + + +# +# +# def Excel_MakeStyle( +# bold=False, +# italic=False, +# color="black", +# bgcolor=None, +# halign=None, +# valign=None, +# orientation=None, +# font_name=None, +# font_size=None, +# ): +# style = XFStyle() +# make_font(bold, italic, font_name, font_size, color, style=style) +# make_alignment(halign, valign, orientation, style=style) +# make_pattern(bgcolor, style=style) +# return style +# +# +# class ScoExcelSheetExt(ScoExcelSheet): +# def __init__(self, sheet_name="feuille", default_style=None): +# ScoExcelSheet.__init__(self, sheet_name, default_style) +# self.cols_width = {} +# self.row_height = {} +# self.merged = [] +# self.panes_frozen = False +# self.horz_split_pos = 0 +# self.vert_split_pos = 0 +# +# def set_panes(self, panes_frozen=True, horz_split_pos=0, vert_split_pos=0): +# self.panes_frozen = panes_frozen +# self.horz_split_pos = horz_split_pos +# self.vert_split_pos = vert_split_pos +# +# def set_cols_width(self, cols_width): +# self.cols_width = cols_width +# +# def set_row_height(self, row_height): +# self.row_height = row_height +# +# def add_merged(self, first_row, last_row, first_col, last_col, value, style): +# self.merged.append((first_row, last_row, first_col, last_col, value, style)) +# +# def gen_workbook(self, wb=None): +# """Generates and returns a workbook from stored data. +# If wb, add a sheet (tab) to the existing workbook (in this case, returns None). +# """ +# if wb == None: +# wb = Workbook() # Création du fichier +# sauvegarde = True +# else: +# sauvegarde = False +# ws0 = wb.add_sheet(self.sheet_name.decode(SCO_ENCODING)) +# li = 0 +# for l in self.cells: +# co = 0 +# for c in l: +# # safety net: allow only str, int and float +# if type(c) == LongType: +# c = int(c) # assume all ScoDoc longs fits in int ! +# elif type(c) not in (IntType, FloatType): +# c = str(c).decode(SCO_ENCODING) +# +# ws0.write(li, co, c, self.get_cell_style(li, co)) +# co += 1 +# li += 1 +# for first_row, last_row, first_col, last_col, value, style in self.merged: +# ws0.write_merge( +# first_row, +# last_row, +# first_col, +# last_col, +# str(value).decode(SCO_ENCODING), +# style, +# ) +# for col_no in range(len(self.cols_width)): +# width = self.cols_width[col_no] +# ws0.col(col_no).width = abs(width) * 110 / 3 +# if width < 0: +# ws0.col(col_no).hidden = True +# row_styles = {} +# for row_no in range(len(self.row_height)): +# height = self.row_height[row_no] +# if height not in row_styles: +# fnt = Font() +# fnt.height = height * 12 +# row_styles[height] = XFStyle() +# row_styles[height].font = fnt +# ws0.row(row_no).set_style(row_styles[height]) # Excel way +# ws0.row(row_no).height = height * 19 / 2 # Libre-Office compatibilité +# if self.panes_frozen: +# ws0.panes_frozen = self.panes_frozen +# ws0.horz_split_pos = self.vert_split_pos +# ws0.vert_split_pos = self.horz_split_pos +# if sauvegarde: +# return wb.savetostr() +# else: +# return None diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index aea7eb0a7..af1dffb60 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -408,6 +408,11 @@ def formsemestre_status_menubar(sem): "endpoint": "notes.feuille_preparation_jury", "args": {"formsemestre_id": formsemestre_id}, }, + { + "title": "Générer feuille préparation Jury DUT", + "endpoint": "notes.feuille_preparation_jury_dut", + "args": {"formsemestre_id": formsemestre_id}, + }, { "title": "Saisie des décisions du jury", "endpoint": "notes.formsemestre_saisie_jury", diff --git a/app/scodoc/sco_prepajury_iuta.py b/app/scodoc/sco_prepajury_iuta.py new file mode 100644 index 000000000..ba0ef7bab --- /dev/null +++ b/app/scodoc/sco_prepajury_iuta.py @@ -0,0 +1,1178 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2019 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 +# +############################################################################## + +"""Feuille excel pour preparation des jurys +""" +from openpyxl.styles import Font, Alignment, Border, Side, PatternFill, Color + +import app.scodoc.sco_utils as scu +from app.comp import res_sem +from app.scodoc import sco_cache, sco_codes_parcours +from app.scodoc import sco_formsemestre +from app.scodoc import sco_groups +from app.scodoc import sco_preferences +from app.scodoc import sco_etud +from app.scodoc import sco_parcours_dut +from app.scodoc import sco_prepajury_formats +from app.scodoc.sco_excel import ScoExcelSheet, excel_make_style, COLORS +from app.scodoc.sco_portal_apogee import query_apogee_portal +from app.scodoc.sco_prepajury_formats import CellFormat, format_note, Formatter +from app.scodoc import sco_abs +from app.scodoc import sco_excel_add +from app import log +from app.models import FormSemestre, Identite + +filtre_decision = ["", "RAT"] +NBSEM = 4 + + +class SemDescriptor: + def __init__(self, nt): + self.acros = {} # ue_code_s : ( numéro, acronyme, titre) + self.codes = [] + self.s_idx = "" + self.s_sx = "" + self.nt = nt + sid = nt.sem["semestre_id"] + if sid > 0: + self.s_idx = sid + self.s_sx = SEMESTRES_SX[sid] + for ue in nt.ues: + if ue.type == 0: + ue_code = ue.ue_code # code indentifiant l'UE + self.codes.append(ue_code) + self.acros[ue_code] = (ue.numero, ue.acronyme, ue.titre) + + def sort(self): + # prev_ue_codes.sort(lambda x, y, prev_ue_acro=prev_ue_acro: cmp(prev_ue_acro[x], prev_ue_acro[y])) + self.codes.sort(key=lambda x: self.acros[x]) + + def widths(self): + widths = [] + widths += [32 for _ in self.codes] + widths += [-32 for _ in range(NBUE - len(self.codes))] + widths += [40, 40] # moyenne et décision + return widths + + def titles(self): + titles = [] + titles += [self.acros[x][1] for x in self.codes] + titles += [ + "" for _ in range(NBUE - len(self.codes)) + ] # Compléte pour avoir NBUE colonnes par semestre + titles += ["Moy %s" % self.s_sx] + titles += ["Décision %s" % self.s_sx] + return titles + + def add_bloc_ue(self, worksheet): + for code in self.codes: + worksheet.append( + [" %-8s %s" % (self.acros[code][1], self.acros[code][2])] + ) + + +# Constantes liées aux decisions sur un semestre +CLASS_ATB = ["ATB", "ATJ", "NAR"] +CLASS_ATT = ["ATT", "AJ"] +CLASS_ADM = ["ADM", "ADM+", "ADC", "ADJ"] + + +def classe(code): + if code in CLASS_ATB: + return CLASS_ATB + if code in CLASS_ATT: + return CLASS_ATT + if code in CLASS_ADM: + return CLASS_ADM + return None + + +class Inscription: + def __init__(self, etudid, sem): + self.etudid = etudid + self.sem = sem + self.s_idx = sem["semestre_id"] + self.sx = SEMESTRES_SX[self.s_idx] + formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) + self.nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + # > get_ues, get_etud_ue_status, get_etud_moy_gen, get_etud_decision_sem + decision = self.nt.get_etud_decision_sem(etudid) + self.code = "" + if decision: + self.code = decision["code"] + if decision["compense_formsemestre_id"]: + self.code += "+" # indique qu'il a servi a compenser + self.moy = self.nt.get_etud_moy_gen(etudid) + self.moy_ue = {} + self.capitalized = {} + for ue in self.nt.ues: + ue_status = self.nt.get_etud_ue_status(etudid, ue.id) + ue_code = ue.ue_code # code identifiant l'UE + self.moy_ue[ue_code] = ue_status["moy"] + self.capitalized[ue_code] = ue_status["was_capitalized"] + + # + # def __repr__(self): + # return ( + # "[%s,%s]:\n" % (self.etudid, self.sx) + # + "ues: %s\n" % self.moy_ue + # + " %s (%s)\n" % (self.moy, self.code) + # ) + # + def display(self, worksheet, descripteur): + for ue_code in descripteur.codes: + if ue_code in self.moy_ue: + worksheet.append(fmt(self.moy_ue[ue_code])) + else: + worksheet.append("-") + for _ in range(NBUE - len(descripteur.codes)): + worksheet.append("") + worksheet.append(fmt(self.moy)) + worksheet.append(self.code) + + # def gabarit(self, descripteur, record): + # gbs = [] + # for ue_code, gb in descripteur.codes, record.gabarit_note: + # if ue_code in self.moy_ue: + # if self.capitalized[ue_code]: + # gb += format_note(FMT_CAP_UE, self.moy_ue[ue_code], gb) + # else: + # gb += format_note(FMT_UE, self.moy_ue[ue_code], gb) + # else: + # gb = 0 + # gbs.append(gb) + # for _ in range(NBUE - len(descripteur.codes)): + # gbs.append(0) + # gbs.append(format_note(FMT_MOY, self.moy), record.gabarit_moy) + # gbs.append(0) + # return gbs + + def compensable(self): + for ue in self.nt.ues: + if ue.type == 0: + moy_ue = self.moy_ue[ue.ue_code] + # if ( + # moy_ue != "NA" + # ): # PATCH COVID-19: une UE neutralisée n'empéche pas la validation d'un semestre + if moy_ue is None or isinstance(moy_ue, str): + return False + if moy_ue < 7.999: + return False + return True + + +# # Calcule la classe du semstre actuel à partir des notes +# # def reglementaire(self, record): +# # for co, moy_ue in enumerate(self.moy_ue.values(), start=0): +# # if moy_ue != "NA": # PATCH COVID-19: une UE neutralisée n'empéche pas la validation d'un semestre +# # if type(moy_ue) != FloatType and type(moy_ue) != IntType: +# # record.gabarit_note[co] |= EMPHASE_SEM +# # return CLASS_ATB +# # if moy_ue < 7.999: +# # record.gabarit_note[co] |= EMPHASE_SEM + SEUIL_8 +# # return CLASS_ATB +# # if type(self.moy) != FloatType and type(self.moy) != IntType: +# # record.gabarit_note[5] |= EMPHASE_SEM + SEUIL_10 +# # return CLASS_ATB +# # if self.moy < 9.999: +# # record.gabarit_note[5] |= EMPHASE_SEM + SEUIL_10 +# # return CLASS_ATT +# # return CLASS_ADM +# +# # def ues_acquises(self, descripteur): +# # liste = [] +# # for ue_code in descripteur.codes: +# # if ( +# # type(self.moy_ue[ue_code]) == FloatType +# # and self.moy_ue[ue_code] >= 9.999 +# # ): +# # liste.append(descripteur.acros[ue_code][1]) +# # return ",".join(liste) if len(liste) > 0 else "" + + +def fmt(x): + """reduit les notes a deux chiffres""" + x = scu.fmt_note(x, keep_numeric=False) + try: + return float(x) + except ValueError: + return x + + +# Calcul la signature (checksum) d'une valeur (chaine de car) +# = ' ' + valeur binaire de la chaine modulo 95 (valeur calculée entre 32 et 127) +def mod95(valeur): + clef = 0 + for car in str(valeur): + clef = (clef * 256 + ord(car)) % 95 + return chr(clef + 32) + + +def entete_semestres(s, descripteurs): + titles = [] + if s in descripteurs: + titles += descripteurs[s].titles() + else: + titles += ["", "", "", "", "", ""] + return titles + + +# Constantes liées aux rangs des semestres +NBSEM = 4 # valeur par défaut +SEMESTRES_IDX = range(1, NBSEM + 1) # valeur par défaut +SEMESTRES_SX = dict((sx, "S%s" % sx) for sx in SEMESTRES_IDX) # valeur par défaut +NBUE = 4 # 4 UE par semestre +SEUIL_ABS = 8 +CATEGORIES = [ + "GEN", + "APOGEE", + "SCODOC", + "S1", + "S2", + "S3", + "S4", + "ABS", + "REGL", + "PREJURY", + "JURY", + "NOTE", + "CLEF", +] +CATEGORIE_DESC = { + "GEN": {"nom": "", "items": ["rang", "nip"], "colour": "CCFFFF"}, + "APOGEE": { + "nom": "Données Apogée", + "items": [ + "Nom", + "Prénom", + "Et. étranger", + "ville", + "pays", + "né le", + "Bac", + "Etape", + "Boursier", + "Impayé", + "Réorient.", + ], + "colour": "00CCFF", + }, + "SCODOC": { + "nom": "Données Scodoc", + "items": ["ID", "Civ.", "Nom", "Prénom", "Parcours", "Groupe"], + "colour": "00FFFF", + }, + "S1": {"nom": "S1", "items": ["-", "-", "-", "-", "-", "-"], "colour": "F0F0F0"}, + "S2": {"nom": "S2", "items": ["-", "-", "-", "-", "-", "-"], "colour": "FFCC99"}, + "S3": {"nom": "S3", "items": ["-", "-", "-", "-", "-", "-"], "colour": "FFCC99"}, + "S4": {"nom": "S4", "items": ["-", "-", "-", "-", "-", "-"], "colour": "FFCC99"}, + "ABS": {"nom": "Abs.", "items": ["Total", "Non just."], "colour": "F0F0F0"}, + "REGL": { + "nom": "Réglementaire", + "items": [ + "Moy. comp.", + "Semestre", + "Proposition Sx", + "Sem. comp.", + "Compensation", + "UE acquises", + ], + "colour": "CCFFCC", + }, + "PREJURY": { + "nom": "Commission", + "items": [ + "Préconisation SX", + "Autre sem. (Sc)", + "Préconisation Sc", + "UE acquises", + ], + "colour": "339966", + }, + "JURY": { + "nom": "Jury", + "items": ["Décision SX", "Autre sem. (Sc)", "Décision Sc", "UE acquises"], + "colour": "99CC00", + }, + "NOTE": {"nom": "Remarques", "items": [""], "colour": "F0F0F0"}, + "CLEF": {"nom": "Clef", "items": ["-"], "colour": "F0F0F0"}, +} + +COMPATIBLES = { + 40: [38, 42, 45, 47, 49], + 42: [38, 45, 47], + 46: [33, 38, 42, 44, 45, 47], + 60: [61], + 61: [60, 83], + 225: [218, 229, 239, 242, 244, 247], + 249: [218, 229, 244, 247], + 252: [178, 201, 225, 229, 239, 242, 244, 247, 343], + 253: [150, 182, 218, 237, 238, 342], + 254: [146, 150, 229, 238], + 255: [150, 182, 216, 218, 237, 238], + 256: [150, 182, 215, 218, 237, 238], + 264: [258, 263], + 332: [301], + 338: [336], + 301: [323, 332], + 258: [263, 264], + 528: [523, 532], + 216: [237, 238, 150, 182, 218], + 215: [150, 237, 238, 182, 218], + 146: [244, 238, 247, 168, 254, 218, 150], + 144: [182, 237, 150, 238, 218], + 236: [150, 242, 252, 225, 216, 229, 201, 247, 239, 178, 244], + 229: [150, 239, 247, 218, 146, 242, 201, 178], +} + + +def recursive_add(compats, form): + if form not in compats: + compats.append(form) + if form in COMPATIBLES: + for succ in COMPATIBLES[form]: + recursive_add(compats, succ) + + +def est_compatible(from_form, to_form): + if from_form == to_form: + return True + # Calcule la cloture transitive + compats = [] + recursive_add(compats, from_form) + return to_form in compats + + +def update_libelles(act_sem, descripteurs): + sx = SEMESTRES_SX[act_sem] + for s in SEMESTRES_IDX: + sn = SEMESTRES_SX[s] + CATEGORIE_DESC[sn]["items"] = entete_semestres(s, descripteurs) + CATEGORIE_DESC[sn]["colour"] = "FFFF99" if s == act_sem else "E0E0E0" + CATEGORIE_DESC["REGL"]["items"] = [ + "Moy. comp.", + "Proposition %s" % sx, + "Sem. comp.", + "Compensation", + "UE acquises", + ] + CATEGORIE_DESC["PREJURY"]["items"] = [ + "Préconisation %s" % sx, + "Autre sem. (Sc)", + "Préconisation Sc", + "UE acquises", + ] + CATEGORIE_DESC["JURY"]["items"] = [ + "Décision %s" % sx, + "Autre sem. (Sc)", + "Décision Sc", + "UE acquises", + ] + + +def get_item(info, field): + return info[field] if field in info else "-" + + +def skip_n_lines(worksheet, nblines): + for _ in range(nblines): + worksheet.append_blank_row() + + +def display_donnees_apogee(infos): + if len(infos) == 1: + info = infos[0] + return [ + get_item(info, item) + for item in [ + "nom", + "prenom", + "Etudiant étranger", + "lieu_naissance", + "country", + "naissance", + "bac", + "etape", + "bourse", + "paiementinscription", + "Abandon/Réorientation", + ] + ] + else: + return ["-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"] + + +def display_donnees_scodoc(etudid, etud, record): + return [ + etudid, + etud["civilite"], + sco_etud.format_nom(etud["nom"]), + sco_etud.format_prenom(etud["prenom"]), + record.parcours, + record.groupesTd, + ] + + +def generate_styles(): + return { + "bold": excel_make_style(bold=True), + "center": excel_make_style(halign="center"), + "boldcenter": excel_make_style(bold=True, halign="center"), + "moy": excel_make_style( + bold=True, halign="center", bgcolor=COLORS.LIGHT_YELLOW + ), + "note": excel_make_style(halign="right"), + "note_bold": excel_make_style(halign="right", bold=True), + } + + +# def bloc_append_footer(l, REQUEST): +# l.append( +# [ +# "Préparé par %s le %s sur %s pour %s" +# % ( +# VERSION.SCONAME, +# time.strftime("%d/%m/%Y à %H:%M"), +# REQUEST.BASE0, +# REQUEST.AUTHENTICATED_USER, +# ) +# ] +# ) +# +# +formatteur = Formatter() + + +def formattage_table(worksheet, act_sem, descripteurs, rangmax): + cols_width = [] + cols_width += [22, -79] # Etudiant + cols_width += [-121, -130, -56, -89, -56, -56, -56, -56, -56, -56, -56] + cols_width += [-49, -34, 95, 80, 116, 26] # scodoc + for s in SEMESTRES_IDX: + if s in descripteurs and s in [act_sem - 1, act_sem, act_sem + 1]: + cols_width += descripteurs[s].widths() + else: + cols_width += [-32, -32, -32, -32, -37, -40] + cols_width += [32, 32] # absences + cols_width += [32, 40, 32, 38, 62] # réglementaire + cols_width += [40, 32, 40, 62] # préjury + cols_width += [-40, -32, -40, -62] # jury (hidden) + cols_width += [200] # Remarques + cols_width += [0] # clef + for i, w in enumerate(cols_width): + if w > 0: + worksheet.set_column_dimension_width(i, w / 7) + else: + worksheet.set_column_dimension_width(i, -w / 7) + worksheet.set_column_dimension_hidden(i, True) + worksheet.set_row_dimension_height(1, 35) + worksheet.set_row_dimension_height(2, 28) + worksheet.set_row_dimension_height(3, 39) + worksheet.set_row_dimension_height(4, 110) + + +imports = {} + + +def compute_sems(etud, formation_id, formation_titre, descripteurs, Se): + inscriptions = {} + rsems = Se.sems[:] # copy + # breakpoint() + # rsems.reverse() + if formation_id not in imports: + imports[formation_id] = {} + for ( + sem + ) in rsems: # parcours les semestres de l'étudiant du plus ancien au plus récent + if not sem["formation_id"] == formation_id: + titre = sem["titreannee"] + import_id = sem["formation_id"] + ok = est_compatible(formation_id, import_id) + if import_id not in imports[formation_id]: + imports[formation_id][import_id] = (titre, ok) + if est_compatible(formation_id, sem["formation_id"]): + s_idx = sem["semestre_id"] + inscriptions[s_idx] = Inscription(etud.id, sem) + if s_idx not in descripteurs: + descripteurs[s_idx] = SemDescriptor(inscriptions[s_idx].nt) + for form in imports[formation_id]: + (titre, ok) = imports[formation_id][form] + if not ok: + log( + "@### Dépendance manquante: @ %10s %5s: %5s %5s (%40s)" + % ( + sco_preferences.get_preference("DeptName"), + formation_id, + form, + titre, + ok, + ) + ) + return inscriptions + + +def format_legende(worksheet, first_line, line_count, style_titre, style_item): + worksheet.set_style(style=style_titre, li=first_line, co=0) + for line in range(first_line + 1, first_line + line_count): + worksheet.set_style(style=style_item, li=line, co=0) + + +# def bloc_append_legendes(worksheet, line_no, descripteurs, REQUEST): +# style_titre = sco_excel_add.Excel_MakeStyle( +# font_name="Courier New", font_size=10, bold=True +# ) +# style_item = sco_excel_add.Excel_MakeStyle(font_name="Courier New", font_size=10) +# +# line_count = bloc_append_legende_ues(worksheet, descripteurs) +# format_legende(worksheet, line_no, line_count, style_titre, style_item) +# line_no += line_count +# +# line_count = bloc_append_legende_iuta(worksheet) +# format_legende(worksheet, line_no, line_count, style_titre, style_item) +# line_no += line_count +# +# line_count = bloc_append_legende_scodoc(worksheet) +# format_legende(worksheet, line_no, line_count, style_titre, style_item) +# line_no += line_count +# +# bloc_append_footer(worksheet, REQUEST) + + +def bloc_append_entete(worksheet, sem, descripteurs, act_sem): + worksheet.append_row( + worksheet.make_row( + ["Feuille préparation Jury %s" % scu.unescape_html(sem["titreannee"])], + style=excel_make_style(font_name="Arial", size=14, bold=True), + ) + ) + worksheet.append_blank_row() + for line in generate_header(worksheet): + worksheet.append_row(line) + # worksheet.ws.merge_cells(start_row=3, start_column=1, end_row=3, end_column=2) + + +# def format_categorie(worksheet, col_no, categorie): +# upper = excel_make_style( +# bold=True, +# halign="center", +# valign="center", +# font_name="calibri", +# size=11, +# bgcolor=categorie["colour"], +# ) +# sco_excel_add.make_borders( +# style=upper, left="thin", top="medium", bottom="thin", right="thin" +# ) +# lower = copy.copy(upper) +# make_font(style=lower, font_name="Calibri", font_size=10, bold=True) +# sco_excel_add.make_alignment( +# style=lower, halign="center", valign="center", orientation="clockwise" +# ) +# lower_left = copy.copy(lower) +# sco_excel_add.make_alignment( +# style=lower_left, halign="center", valign="center", orientation="clockwise" +# ) +# lower_right = copy.copy(lower) +# sco_excel_add.make_alignment( +# style=lower_right, halign="center", valign="center", orientation="clockwise" +# ) +# +# sco_excel_add.make_font(style=upper, font_size=14) +# sco_excel_add.make_borders( +# style=lower, left="thin", top="thin", bottom="thin", right="thin" +# ) +# sco_excel_add.make_borders( +# style=lower_left, left="thin", top="thin", bottom="thin", right="thin" +# ) +# sco_excel_add.make_borders( +# style=lower_right, left="thin", top="thin", bottom="thin", right="thin" +# ) +# left_col = col_no +# right_col = col_no + len(categorie["items"]) +# worksheet.add_merged(2, 2, left_col, right_col - 1, categorie["nom"], upper) +# worksheet.set_style(lower_left, li=3, co=left_col) +# for c in range(left_col + 1, right_col - 1): +# worksheet.set_style(lower, li=3, co=c) +# worksheet.set_style(lower_right, li=3, co=right_col - 1) +# return right_col + + +def generate_categorie(worksheet, categorie): + title = categorie["nom"] + items_list = categorie["items"] + nb_center_cell = len(items_list) - 2 + + # fond + pattern = PatternFill(fill_type="solid", fgColor=categorie["colour"]) + + # bordures + side_thin = Side(border_style="thin", color=COLORS.BLACK.value) + side_hair = Side(border_style="hair", color=COLORS.BLACK.value) + + font_upper = Font(name="Arial", size=14, bold=True) + font_lower = Font(name="Calibri", size=10, bold=True) + align_upper = Alignment(horizontal="center", vertical="center") + align_lower = Alignment(horizontal="center", vertical="center", textRotation=90) + upper_left = worksheet.excel_make_composite_style( + fill=pattern, + font=font_upper, + alignment=align_upper, + border=Border(top=side_thin, right=side_hair, left=side_thin, bottom=side_hair), + ) + upper = worksheet.excel_make_composite_style( + font=font_upper, + fill=pattern, + alignment=align_upper, + border=Border(top=side_thin, right=side_hair, left=side_hair, bottom=side_hair), + ) + upper_right = worksheet.excel_make_composite_style( + font=font_upper, + alignment=align_upper, + fill=pattern, + border=Border(top=side_thin, right=side_thin, left=side_hair, bottom=side_hair), + ) + lower_left = worksheet.excel_make_composite_style( + font=font_lower, + alignment=align_lower, + fill=pattern, + border=Border(top=side_hair, right=side_hair, left=side_thin, bottom=side_thin), + ) + lower = worksheet.excel_make_composite_style( + font=font_lower, + alignment=align_lower, + fill=pattern, + border=Border(top=side_hair, right=side_hair, left=side_hair, bottom=side_thin), + ) + lower_right = worksheet.excel_make_composite_style( + font=font_lower, + alignment=align_lower, + fill=pattern, + border=Border(top=side_hair, right=side_thin, left=side_hair, bottom=side_thin), + ) + + upper_values = [] + upper_values += [title] + ["" for _ in range(1, len(items_list))] + upper_styles = [upper_left] + [upper] * nb_center_cell + [upper_right] + upper_cells = worksheet.make_row(upper_values, upper_styles) + + lower_values = [] + lower_values += items_list + lower_styles = [lower_left] + [lower] * nb_center_cell + [lower_right] + lower_cells = worksheet.make_row(lower_values, lower_styles) + + return upper_cells, lower_cells + + +def merge_categories(worksheet): + row = 3 + left = 0 + for categorie in CATEGORIES: + right = left + len(CATEGORIE_DESC[categorie]["items"]) - 1 + worksheet.merge(start_row=row, end_row=row, start_column=left, end_column=right) + left = right + 1 + + +def generate_header(worksheet): + uppers = [] + lowers = [] + for categorie in CATEGORIES: + upper, lower = generate_categorie(worksheet, CATEGORIE_DESC[categorie]) + uppers += upper + lowers += lower + return [uppers, lowers] + + +def bloc_append_legende_scodoc(worksheet): + codes = sco_codes_parcours.CODES_EXPL.keys() + codes.sort() + worksheet.append_row(worksheet.make_row(["Explication des codes (scodoc)"])) + for code in codes: + worksheet.append_row( + worksheet.make_row( + [" %-8s %s" % (code, sco_codes_parcours.CODES_EXPL[code])] + ) + ) + worksheet.append( + [ + " %-8s %s" + % ("ADM+", "indique que le semestre a déjà servi à en compenser un autre") + ] + ) + # UE : Correspondances acronyme et titre complet + worksheet.append([""]) + return len(codes) + 2 + + +# def bloc_append_legende_iuta(worksheet): +# worksheet.append(["Explication des codes (Jury)"]) +# worksheet.append([" %-8s %s" % ("ADM", "Admis")]) +# worksheet.append([" %-8s %s" % ("ADJ", "Admis par décision de jury")]) +# worksheet.append( +# [ +# " %-8s %s" +# % ( +# "ADC", +# "Admis par compensation (indiquer le n° du semestre utilisé : ADC2", +# ) +# ] +# ) +# worksheet.append( +# [ +# " %-8s %s" +# % ( +# "AJ", +# "Ajourné (non validé), peut devenir ADJ ou ADC, préconisé si réorientation", +# ) +# ] +# ) +# worksheet.append( +# [" %-8s %s" % ("RAT", "Attente de notes (équivalent du Report")] +# ) +# worksheet.append([" %-8s %s" % ("NAR", "Non autorisé à redoubler (exclu)")]) +# worksheet.append( +# [ +# " %-8s %s" +# % ("AP", "Autorisé à poursuivre (préciser dans quel semestre: AP2)") +# ] +# ) +# worksheet.append([]) +# worksheet.append([" %s" % "ATT et ATB disparaissent (remplacés par AJ)"]) +# worksheet.append( +# [" %s" % "Défaillant ou abandon : mettre NAR (exclu) ou AJ selon le cas"] +# ) +# worksheet.append([""]) +# return 12 +# +# +# def bloc_append_legende_ues(worksheet, descripteurs): +# # UE : Correspondances acronyme et titre complet +# worksheet.append([""]) +# worksheet.append(["Titre des UE"]) +# line_count = 1 +# for d in descripteurs: +# descripteurs[d].add_bloc_ue(worksheet) +# line_count += len(descripteurs[d].codes) + 1 +# worksheet.append([""]) +# return line_count + + +def format_line(worksheet, rang, gabarit): + styles = [] + for co, gb in enumerate(gabarit, start=0): + gb += formatteur.borders(rang, co) + if ((rang + 2) / 3) % 2 == 0: + gb |= CellFormat.GRAYED + styles.append(formatteur.get_style(gb)) + return styles + + +# def pane(worksheet): +# worksheet.set_panes(panes_frozen=True, horz_split_pos=16, vert_split_pos=4) +# +# +lignes = {} # etudid : Ligne + + +class Ligne: + def __init__(self, etudid): + self.etudid = etudid + self.regl = None + self.regl2 = None + self.regl2_sem = None + self.ues_acq = None + self.moy_inter = None + self.historique = {} + self.parcours = None + self.groupesTD = None + self.nbabs = None + self.nbabsnonjust = None + self.gabarit_abs = [0, 0] + self.gabarit_note = [] + self.gabarit_moy = CellFormat.FMT_MOY + self.gabarit_comp = CellFormat.FMT_MOY + + def calcul_compensation(self, act_sem, compensateur, compensed): + if self.historique[compensateur].code == "ADM+": + return False + if not self.historique[compensateur].compensable(): + return False + if not self.historique[compensed].compensable(): + return False + try: + moyenne = ( + self.historique[compensateur].moy + self.historique[compensed].moy + ) / 2.0 + except TypeError: + return False + self.moy_inter = moyenne + if moyenne < 9.999: + self.gabarit_comp += format_note(moyenne, CellFormat.FMT_MOY, 10.00) + return False + self.gabarit_comp += format_note( + moyenne, CellFormat.FMT_MOY + CellFormat.EMPHASE_COMP, 10.00 + ) + return True + + def other_compense_act(self, other, act_sem): + self.regl = "ADC%s" % other + # self.regl2 = "ADM+" # "ADM+" + # self.regl2_sem = "S%s" % other + + def act_compense_oth(self, act_sem, other): + self.regl = "ADM" # "ADM+" + self.regl2 = "ADC%s" % act_sem + self.regl2_sem = "S%s" % other + + def test_ue_acq(self, act_inscription, descripteur, act_sem): + base = (act_sem - 1) * 6 + liste = [] + for ue_code in descripteur.codes: + moyenne = act_inscription.moy_ue[ue_code] + if isinstance(moyenne, float) and moyenne >= 9.999: + liste.append(descripteur.acros[ue_code][1]) + self.gabarit_note[base] |= format_note( + moyenne, CellFormat.EMPHASE_UEACQ, 10.00 + ) + else: + self.gabarit_note[base] |= format_note(moyenne, 0, 10.00) + base += 1 + self.ues_acq = ",".join(liste) if len(liste) > 0 else "" + + def init_gabarit(self, descripteurs): + for s in SEMESTRES_IDX: + if s in self.historique: + for ue_code in descripteurs[s].codes: + if ue_code in self.historique[s].moy_ue: + if self.historique[s].capitalized[ue_code]: + self.gabarit_note += [CellFormat.FMT_CAP_UE] + else: + self.gabarit_note += [CellFormat.FMT_UE] + else: + self.gabarit_note += [0] + for _ in range(NBUE - len(descripteurs[s].codes)): + self.gabarit_note += [0] + self.gabarit_note += [CellFormat.FMT_MOY, 0] + else: + self.gabarit_note += [0, 0, 0, 0, 0, 0] + + def test_atb(self, inscription, base, descripteur): + flag = False + offset = 0 + for ue in descripteur.codes: + moy_ue = inscription.moy_ue[ue] + if ( + moy_ue != "NA" + ): # PATCH COVID-19: une UE neutralisée n'empéche pas la validation d'un semestre + if not isinstance(moy_ue, (int, float)): + self.gabarit_note[base + offset] |= format_note( + moy_ue, CellFormat.EMPHASE_ATB, None + ) + flag = True + if moy_ue < 7.995: + self.gabarit_note[base + offset] |= format_note( + moy_ue, CellFormat.EMPHASE_ATB, 8.00 + ) + flag = True + offset += 1 + if not isinstance(inscription.moy, (float, int)): + self.gabarit_note[base + 4] |= format_note( + inscription.moy, CellFormat.EMPHASE_ATB, None + ) + flag = True + else: + self.gabarit_note[base + 4] |= format_note(inscription.moy, 0, 10.00) + return flag + + def test_att(self, moyenne, base): + if not isinstance(moyenne, (int, float)) or moyenne < 9.995: + self.gabarit_note[base + 4] |= format_note( + moyenne, CellFormat.EMPHASE_ATT, 10.00 + ) + return CLASS_ATT + return False + + def reglementaire(self, act_sem, descripteur): + inscription = self.historique[act_sem] + base = 6 * (act_sem - 1) + self.gabarit_note[base + 4] |= format_note( + inscription.moy, CellFormat.FMT_MOY, 10.0 + ) + if self.test_atb(inscription, base, descripteur): + return CLASS_ATB + if self.test_att(inscription.moy, base): + return CLASS_ATT + return CLASS_ADM + + +def feuille_preparation_jury_dut(formsemestre_id): + "Feuille excel pour preparation des jurys DUT" + + # Structure de données + # Globales + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + etuds: Identite = nt.get_inscrits(order_by="moy") # tri par moy gen + sem = sco_formsemestre.get_formsemestre(formsemestre_id) + + etud_groups = sco_groups.formsemestre_get_etud_groupnames(formsemestre_id) + main_partition_id = sco_groups.formsemestre_get_main_partition(formsemestre_id)[ + "partition_id" + ] + dept_name = scu.unescape_html( + sco_preferences.get_preference("DeptName", formsemestre_id) + ) + + # définition des semestres courant(act_sem), précédent (prev_sem) et suivant (next_sem) + act_sem = nt.sem["semestre_id"] + prev_sem = act_sem - 1 if act_sem > 1 else None + next_sem = act_sem + 1 if act_sem < NBSEM else None + + # Données par rang de semestre (S1, .. S4) + descripteurs = {} # sx : SemDescripteur + + # Par étudiant + autorisations = {} + assidu = {} + # Récupération des données + for etud in etuds: + # On récupère les infos de l'étudiant courant + # info = sco_etud.get_etud_info(etudid=etudid, filled=True) + # if not info: + # continue # should not occur... + # etud = info[0] + # On récupère les semestres de l'étudiant courant + etudid = etud.id + Se = sco_parcours_dut.SituationEtudParcours( + etud.to_dict_scodoc7(), formsemestre_id + ) + historique = compute_sems( + etud, sem["formation_id"], sem["titreannee"], descripteurs, Se + ) + + # Ne traite que les étudiants pour lesquels une décision doit être prise + if historique[act_sem].code in filtre_decision: + record = Ligne(etudid) + record.historique = historique + lignes[etudid] = record + record.init_gabarit(descripteurs) + act_inscription = record.historique[act_sem] + + # Calcul des classes des semestres impliqués + act_class = record.reglementaire( + act_sem, descripteurs[act_sem] + ) # act_inscription.reglementaire(record) + if prev_sem in record.historique: + prev_class = classe(record.historique[prev_sem].code) + else: + prev_class = None + if next_sem in record.historique: + next_class = classe(record.historique[next_sem].code) + else: + next_class = None + + # Algorithme de calcul de la décision 'réglementaire' + if sem["ins"]["etat"] == "D" or (historique[act_sem].code == "DEF"): + # Etudiant démissionnaire -> 'AJ' ou 'DEF' ? + record.regl = "AJ" + elif act_class == CLASS_ADM: + if ( + prev_class == CLASS_ATT + ): # compensation éventuelle du prev_sem par act_sem + if record.calcul_compensation(act_sem, act_sem, prev_sem): + record.act_compense_oth(act_sem, prev_sem) + else: + record.regl = "AJ" + # record.test_ue_acq(act_inscription, descripteurs[act_sem], act_sem) + record.gabarit_note[ + (act_sem - 1) * 6 - 1 + ] |= CellFormat.EMPHASE_PREC + elif prev_class == CLASS_ADM: + if ( + next_class == CLASS_ATT + ): # compensation éventuelle du next_sem par act_sem + if record.calcul_compensation(act_sem, act_sem, next_sem): + record.act_compense_oth(act_sem, next_sem) + else: + record.regl = "ADM" + else: + record.regl = "ADM" + elif prev_class == CLASS_ATB: + record.regl = "AJ" + else: + record.regl = "ADM" + elif act_class == CLASS_ATT: + if prev_class == CLASS_ATT: + record.regl = "ATT" + elif ( + prev_class == CLASS_ADM + ): # compensation éventuelle du act_sem par prev_sem + if record.calcul_compensation(act_sem, prev_sem, act_sem): + record.other_compense_act(prev_sem, act_sem) + elif ( + next_class == CLASS_ADM + ): # compensation éventuelle du act_sem par next_sem + if record.calcul_compensation(act_sem, next_sem, act_sem): + record.other_compense_act(next_sem, act_sem) + else: + record.regl = "ATT" + else: + record.regl = "ATT" + elif prev_class == CLASS_ATB: + record.regl = "ATT" + else: + record.regl = "ATT" + elif act_class == CLASS_ATB: + record.regl = "ATB" + else: # should not happen + pass + + if record.regl in {"AJ", "ATT", "ATB"}: + record.test_ue_acq(act_inscription, descripteurs[act_sem], act_sem) + decision = record.historique[act_sem].nt.get_etud_decision_sem(etudid) + if decision: + assidu[etudid] = {0: "Non", 1: "Oui"}.get(decision["assidu"], "") + aut_list = sco_parcours_dut.formsemestre_get_autorisation_inscription( + etudid, formsemestre_id + ) + autorisations[etudid] = ", ".join( + ["S%s" % x["semestre_id"] for x in aut_list] + ) + # parcours: + record.parcours = Se.get_parcours_descr() + # groupe principal (td) + record.groupestd = "" + for s in Se.etud["sems"]: + if s["formsemestre_id"] == formsemestre_id: + record.groupesTd = etud_groups.get(etudid, {}).get( + main_partition_id, "" + ) + # absences: + record.nbabs, record.nbabsjust = sco_abs.get_abs_count(etudid, sem) + record.nbabsnonjust = record.nbabs - record.nbabsjust + record.gabarit_abs = ( + [0, 0] + if record.nbabsnonjust <= SEUIL_ABS + else [0, CellFormat.EMPHASE_ABS] + ) + else: + log("Etudiant ignoré: %s [%s]" % (etudid, historique[act_sem].code)) + + update_libelles(act_sem, descripteurs) + formatteur.set_default_format(excel_make_style(font_name="Calibri", size=9)) + formatteur.set_borders_default( + left="none", right="none", top="filet", bottom="filet" + ) + formatteur.last_rank = len(lignes) + formatteur.set_categories(CATEGORIES, CATEGORIE_DESC) + + # Tri des codes des UE des semestres: + for descripteur in descripteurs.values(): + descripteur.sort() + + worksheet = ScoExcelSheet(sheet_name="Preparation_Jury %s" % SEMESTRES_SX[act_sem]) + bloc_append_entete(worksheet, sem, descripteurs, act_sem) + + # Mise en forme finale + rang = 1 # numero etudiant + for etud in etuds: + etudid = etud.id + if etudid in lignes: + record = lignes[etudid] + if record.historique[act_sem].code in filtre_decision: + # récupération des données + etud = nt.identdict[etudid] + nip = etud["code_nip"] + infos = query_apogee_portal(nip=etud["code_nip"]) + # calculs de validation + + # affichage des données de l'étudiant + line = [str(rang), etud["code_nip"]] + line += display_donnees_apogee(infos) + line += display_donnees_scodoc(etudid, etud, record) + + co = len(line) + gabarit = [0 for _ in range(len(line))] + + for s in SEMESTRES_IDX: + if s in record.historique: + inscription = record.historique[s] + inscription.display(line, descripteurs[s]) + co += 6 + else: + line += ["", "", "", "", "", ""] + gabarit += record.gabarit_note + + line.append(fmt(str(record.nbabs))) + line.append(fmt(str(record.nbabsnonjust))) + if record.nbabsnonjust < 8: + gabarit += [0, 0] + else: + gabarit += [0, CellFormat.EMPHASE_ABS] + + if record.moy_inter is not None: + line.append(fmt(record.moy_inter)) + gabarit.append(record.gabarit_comp) + else: + line.append("") + gabarit.append(0) + + # line.append('S%s' % act_sem) + # gabarit.append(0) + + for _ in range(2): # Reglementaire et préjury + line.append(record.regl if record.regl is not None else "") + line.append( + record.regl2_sem if record.regl2_sem is not None else "" + ) + line.append(record.regl2 if record.regl2 is not None else "") + line.append(record.ues_acq if record.ues_acq is not None else "") + gabarit += [0, 0, 0, 0] + + # Jury et remarques + line += ["", "", "", "", ""] + gabarit += [0, 0, 0, 0, 0] + # clef + clef = [mod95(line[i]) for i in range(1, 50)] + line.append("".join(clef)) + gabarit += [0] + styles = format_line(worksheet, rang, gabarit) + worksheet.append_row(worksheet.make_row(line, styles)) + rang += 1 + + skip_n_lines(worksheet, 8) + # bloc_append_legendes(worksheet, rang + 4, descripteurs) + formattage_table(worksheet, act_sem, descripteurs, rang) + # pane(worksheet) + + merge_categories(worksheet) + xls = worksheet.generate() + return scu.send_file( + xls, + "PrepaJury %s %s.xls" % (dept_name, SEMESTRES_SX[act_sem]), + scu.XLSX_SUFFIX, + scu.XLSX_MIMETYPE, + ) diff --git a/app/views/notes.py b/app/views/notes.py index 005e7963a..dbfc373b1 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -127,6 +127,7 @@ from app.scodoc import sco_placement from app.scodoc import sco_poursuite_dut from app.scodoc import sco_preferences from app.scodoc import sco_prepajury +from app.scodoc import sco_prepajury_iuta from app.scodoc import sco_pvjury from app.scodoc import sco_recapcomplet from app.scodoc import sco_report @@ -2687,6 +2688,11 @@ sco_publish( sco_prepajury.feuille_preparation_jury, Permission.ScoView, ) +sco_publish( + "/feuille_preparation_jury_dut", + sco_prepajury_iuta.feuille_preparation_jury_dut, + Permission.ScoView, +) sco_publish( "/formsemestre_archive", sco_archives.formsemestre_archive,