diff --git a/.gitignore b/.gitignore index 6d49cf2c..9a2e411b 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,4 @@ Thumbs.db *.code-workspace +copy diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index 67a52261..c199d0b3 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -482,9 +482,9 @@ class GenTable(object): ses.append_blank_row() # empty line ses.append_single_cell_row(self.origin, style_base) if wb is None: - return ses.generate_standalone() + return ses.generate() else: - ses.generate_embeded() + ses.generate() def text(self): "raw text representation of the table" diff --git a/app/scodoc/sco_archives_etud.py b/app/scodoc/sco_archives_etud.py index 0c88d2de..04b043c5 100644 --- a/app/scodoc/sco_archives_etud.py +++ b/app/scodoc/sco_archives_etud.py @@ -266,9 +266,14 @@ def etudarchive_generate_excel_sample(group_id=None, REQUEST=None): ], extra_cols=["fichier_a_charger"], ) - return sco_excel.send_excel_file( - REQUEST, data, "ImportFichiersEtudiants" + scu.XLSX_SUFFIX + return scu.send_file( + data, + "ImportFichiersEtudiants", + scu.XLSX_SUFFIX, + scu.XLSX_MIMETYPE, + attached=True, ) + # return sco_excel.send_excel_file(REQUEST, data, "ImportFichiersEtudiants" + scu.XLSX_SUFFIX) def etudarchive_import_files_form(group_id, REQUEST=None): diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index b5f9879e..1e70858a 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -35,11 +35,11 @@ from enum import Enum from tempfile import NamedTemporaryFile import openpyxl.utils.datetime +from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL +from openpyxl.comments import Comment from openpyxl import Workbook, load_workbook from openpyxl.cell import WriteOnlyCell from openpyxl.styles import Font, Border, Side, Alignment, PatternFill -from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL -from openpyxl.comments import Comment import app.scodoc.sco_utils as scu from app.scodoc import notesdb @@ -59,24 +59,9 @@ class COLORS(Enum): LIGHT_YELLOW = "FFFFFF99" -def send_excel_file(request, data, filename, mime=scu.XLSX_MIMETYPE): - """publication fichier. - (on ne doit rien avoir émis avant, car ici sont générés les entetes) - """ - filename = ( - scu.unescape_html(scu.suppress_accents(filename)) - .replace("&", "") - .replace(" ", "_") - ) - request.RESPONSE.setHeader("content-type", mime) - request.RESPONSE.setHeader( - "content-disposition", 'attachment; filename="%s"' % filename - ) - return data - - # Un style est enregistré comme un dictionnaire qui précise la valeur d'un attributdans la liste suivante: -# font, border, number_format, fill, .. (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles) +# font, border, number_format, fill,... +# (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles) def xldate_as_datetime(xldate, datemode=0): @@ -86,6 +71,12 @@ def xldate_as_datetime(xldate, datemode=0): return openpyxl.utils.datetime.from_ISO8601(xldate) +def adjust_sheetname(sheet_name): + # Le nom de la feuille ne peut faire plus de 31 caractères. + # si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?) + return sheet_name[:31] + + class ScoExcelBook: """Permet la génération d'un classeur xlsx composé de plusieurs feuilles. usage: @@ -98,13 +89,16 @@ class ScoExcelBook: def __init__(self): self.sheets = [] # list of sheets + self.wb = Workbook(write_only=True) def create_sheet(self, sheet_name="feuille", default_style=None): """Crée une nouvelle feuille dans ce classeur sheet_name -- le nom de la feuille default_style -- le style par défaut """ - sheet = ScoExcelSheet(sheet_name, default_style) + sheet_name = adjust_sheetname(sheet_name) + ws = self.wb.create_sheet(sheet_name) + sheet = ScoExcelSheet(sheet_name, default_style, ws) self.sheets.append(sheet) return sheet @@ -112,12 +106,12 @@ class ScoExcelBook: """génération d'un stream binaire représentant la totalité du classeur. retourne le flux """ - wb = Workbook(write_only=True) for sheet in self.sheets: - sheet.generate(self) - # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream) + sheet.prepare() + # construction d'un flux + # (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream) with NamedTemporaryFile() as tmp: - wb.save(tmp.name) + self.wb.save(tmp.name) tmp.seek(0) return tmp.read() @@ -125,6 +119,7 @@ class ScoExcelBook: def excel_make_style( bold=False, italic=False, + outline=False, color: COLORS = COLORS.BLACK, bgcolor: COLORS = None, halign=None, @@ -145,7 +140,14 @@ def excel_make_style( size -- taille de police """ style = {} - font = Font(name=font_name, bold=bold, italic=italic, color=color.value, size=size) + font = Font( + name=font_name, + bold=bold, + italic=italic, + outline=outline, + color=color.value, + size=size, + ) style["font"] = font if bgcolor: style["fill"] = PatternFill(fill_type="solid", fgColor=bgcolor.value) @@ -182,41 +184,93 @@ class ScoExcelSheet: """ def __init__(self, sheet_name="feuille", default_style=None, wb=None): - """Création de la feuille. - sheet_name -- le nom de la feuille - default_style -- le style par défaut des cellules - wb -- le WorkBook dans laquelle se trouve la feuille. Si wb est None (cas d'un classeur mono-feuille), - un workbook est crée et associé à cette feuille. + """Création de la feuille. sheet_name + -- le nom de la feuille default_style + -- le style par défaut des cellules ws + -- None si la feuille est autonome (dans ce cas ell crée son propre wb), sinon c'est la worksheet + créée par le workbook propriétaire un workbook est crée et associé à cette feuille. """ # Le nom de la feuille ne peut faire plus de 31 caractères. # si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?) - self.sheet_name = sheet_name[ - :31 - ] # if len(sheet_name) > 31: sheet_name = 'Feuille' ? - self.rows = [] # list of list of cells - # self.cells_styles_lico = {} # { (li,co) : style } - # self.cells_styles_li = {} # { li : style } - # self.cells_styles_co = {} # { co : style } + self.sheet_name = adjust_sheetname(sheet_name) if default_style is None: default_style = excel_make_style() self.default_style = default_style - self.wb = wb or Workbook(write_only=True) # Création de workbook si nécessaire - self.ws = self.wb.create_sheet(title=self.sheet_name) + if wb is None: + self.wb = Workbook() + self.ws = self.wb.active + self.ws.title = self.sheet_name + else: + self.wb = None + self.ws = wb + # internal data + self.rows = [] # list of list of cells self.column_dimensions = {} + self.row_dimensions = {} - def set_column_dimension_width(self, cle, value): - """Détermine la largeur d'une colonne. - cle -- identifie la colonne ("A"n "B", ...) - value -- la dimension (unité : 7 pixels comme affiché dans Excel) + def excel_make_composite_style( + self, + alignment=None, + border=None, + fill=None, + number_format=None, + font=None, + ): + style = {} + if font is not None: + style["font"] = font + if alignment is not None: + style["alignment"] = alignment + if border is not None: + style["border"] = border + if fill is not None: + style["fill"] = fill + if number_format is None: + style["number_format"] = FORMAT_GENERAL + else: + style["number_format"] = number_format + return style + + @staticmethod + def i2col(idx): + if idx < 26: # one letter key + return chr(idx + 65) + else: # two letters AA..ZZ + first = (idx // 26) + 66 + second = (idx % 26) + 65 + return "" + chr(first) + chr(second) + + def set_column_dimension_width(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) """ - self.ws.column_dimensions[cle].width = value + 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 type(cle) == str: # accepts set_column_with("D", ...) + self.ws.column_dimensions[cle].width = value + else: + self.ws.column_dimensions[self.i2col(cle)].width = value - def set_column_dimension_hidden(self, cle, value): - """Masque ou affiche une colonne. - cle -- identifie la colonne ("A"n "B", ...) + 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 + """ + if cle is None: + for i, val in enumerate(value, start=1): + self.ws.row_dimensions[i].height = val + # No keys: value is a list of widths + else: + self.ws.row_dimensions[cle].height = value + + def set_row_dimension_hidden(self, cle, value): + """Masque ou affiche une ligne. + cle -- identifie la colonne (1...) value -- boolean (vrai = colonne cachée) """ - self.ws.column_dimensions[cle].hidden = value + self.ws.row_dimensions[cle].hidden = value def make_cell(self, value: any = None, style=None, comment=None): """Construit une cellule. @@ -232,8 +286,12 @@ class ScoExcelSheet: style = self.default_style if "font" in style: cell.font = style["font"] + if "alignment" in style: + cell.alignment = style["alignment"] if "border" in style: cell.border = style["border"] + if "fill" in style: + cell.fill = style["fill"] if "number_format" in style: cell.number_format = style["number_format"] if "fill" in style: @@ -272,73 +330,31 @@ class ScoExcelSheet: """ajoute une ligne déjà construite à la feuille.""" self.rows.append(row) - # def set_style(self, style=None, li=None, co=None): - # if li is not None and co is not None: - # self.cells_styles_lico[(li, co)] = style - # elif li is None: - # self.cells_styles_li[li] = style - # elif co is None: - # self.cells_styles_co[co] = style - # - # def get_cell_style(self, li, co): - # """Get style for specified cell""" - # return ( - # self.cells_styles_lico.get((li, co), None) - # or self.cells_styles_li.get(li, None) - # or self.cells_styles_co.get(co, None) - # or self.default_style - # ) - - def _generate_ws(self): + def prepare(self): """génére un flux décrivant la feuille. Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille) ou pour la génération d'un classeur multi-feuilles """ - for col in self.column_dimensions.keys(): - self.ws.column_dimensions[col] = self.column_dimensions[col] + for row in self.column_dimensions.keys(): + self.ws.column_dimensions[row] = self.column_dimensions[row] + for row in self.row_dimensions.keys(): + self.ws.row_dimensions[row] = self.row_dimensions[row] for row in self.rows: self.ws.append(row) - def generate_standalone(self): + def generate(self): """génération d'un classeur mono-feuille""" - self._generate_ws() + # this method makes sense only if it is a standalone worksheet (else call workbook.generate() + if self.wb is None: # embeded sheet + raise ScoValueError("can't generate a single sheet from a ScoWorkbook") + # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream) + self.prepare() with NamedTemporaryFile() as tmp: self.wb.save(tmp.name) tmp.seek(0) return tmp.read() - def generate_embeded(self): - """generation d'une feuille include dans un classeur multi-feuilles""" - self._generate_ws() - - def gen_workbook(self, wb=None): - """TODO: à remplacer""" - """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 is None: - wb = Workbook() # Création du fichier - sauvegarde = True - else: - sauvegarde = False - ws0 = wb.add_sheet(self.sheet_name) - li = 0 - for row in self.rows: - co = 0 - for c in row: - # safety net: allow only str, int and float - # #py3 #sco8 A revoir lors de la ré-écriture de ce module - # XXX if type(c) not in (IntType, FloatType): - # c = str(c).decode(scu.SCO_ENCODING) - ws0.write(li, co, c, self.get_cell_style(li, co)) - co += 1 - li += 1 - if sauvegarde: - return wb.savetostr() - else: - return None - def excel_simple_table( titles=None, lines=None, sheet_name=b"feuille", titles_styles=None, comments=None @@ -377,7 +393,7 @@ def excel_simple_table( cell_style = text_style cells.append(ws.make_cell(it, cell_style)) ws.append_row(cells) - return ws.generate_standalone() + return ws.generate() def excel_feuille_saisie(e, titreannee, description, lines): @@ -538,7 +554,7 @@ def excel_feuille_saisie(e, titreannee, description, lines): ws.make_cell("cellule vide -> note non modifiée", style_expl), ] ) - return ws.generate_standalone() + return ws.generate() def excel_bytes_to_list(bytes_content): @@ -758,4 +774,4 @@ def excel_feuille_listeappel( cell_2 = ws.make_cell(("Liste éditée le " + dt), style1i) ws.append_row([None, cell_2]) - return ws.generate_standalone() + return ws.generate() diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index 73b2550d..76039bea 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -1,984 +1,988 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2021 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 -# -############################################################################## - -"""Affichage étudiants d'un ou plusieurs groupes - sous forme: de liste html (table exportable), de trombinoscope (exportable en pdf) -""" - -# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code) - -import collections -import datetime -import operator -import urllib -from urllib.parse import parse_qs -import time - - -from flask import url_for, g, request -from flask_login import current_user - -import app.scodoc.sco_utils as scu -from app.scodoc import html_sco_header -from app.scodoc import sco_abs -from app.scodoc import sco_excel -from app.scodoc import sco_formsemestre -from app.scodoc import sco_groups -from app.scodoc import sco_moduleimpl -from app.scodoc import sco_parcours_dut -from app.scodoc import sco_portal_apogee -from app.scodoc import sco_preferences -from app.scodoc import sco_etud -from app.scodoc.gen_tables import GenTable -from app.scodoc.sco_exceptions import ScoValueError -from app.scodoc.sco_permissions import Permission -from six.moves import range - -JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [ - "js/etud_info.js", - "js/groups_view.js", -] - -CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS - - -def groups_view( - group_ids=[], - format="html", - REQUEST=None, - # Options pour listes: - with_codes=0, - etat=None, - with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) - with_archives=0, # ajoute colonne avec noms fichiers archivés - with_annotations=0, - formsemestre_id=None, # utilise si aucun groupe selectionné -): - """Affichage des étudiants des groupes indiqués - group_ids: liste de group_id - format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf - """ - # Informations sur les groupes à afficher: - groups_infos = DisplayedGroupsInfos( - group_ids, - formsemestre_id=formsemestre_id, - etat=etat, - select_all_when_unspecified=True, - ) - # Formats spéciaux: download direct - if format != "html": - return groups_table( - groups_infos=groups_infos, - format=format, - REQUEST=REQUEST, - with_codes=with_codes, - etat=etat, - with_paiement=with_paiement, - with_archives=with_archives, - with_annotations=with_annotations, - ) - - H = [ - html_sco_header.sco_header( - javascripts=JAVASCRIPTS, - cssstyles=CSSSTYLES, - init_qtip=True, - ) - ] - # Menu choix groupe - H.append("""
""") - H.append(form_groups_choice(groups_infos, submit_on_change=True)) - # Note: le formulaire est soumis a chaque modif des groupes - # on pourrait faire comme pour le form de saisie des notes. Il faudrait pour cela: - # - charger tous les etudiants au debut, quels que soient les groupes selectionnés - # - ajouter du JS pour modifier les liens (arguments group_ids) quand le menu change - - # Tabs - # H.extend( ("""toto""",) ) - H.extend( - ( - """ -
- -
-
- """, - groups_table( - groups_infos=groups_infos, - format=format, - REQUEST=REQUEST, - with_codes=with_codes, - etat=etat, - with_paiement=with_paiement, - with_archives=with_archives, - with_annotations=with_annotations, - ), - "
", - """
""", - tab_photos_html(groups_infos, etat=etat, REQUEST=REQUEST), - #'

hello

', - "
", - '
', - tab_absences_html(groups_infos, etat=etat, REQUEST=REQUEST), - "
", - ) - ) - - H.append(html_sco_header.sco_footer()) - return "\n".join(H) - - -def form_groups_choice(groups_infos, with_selectall_butt=False, submit_on_change=False): - """form pour selection groupes - group_ids est la liste des groupes actuellement sélectionnés - et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. - (utilisé pour retrouver le semestre et proposer la liste des autres groupes) - - Si submit_on_change, ajoute une classe "submit_on_change" qui est utilisee en JS - """ - default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) - - H = [ - """
- - - Groupes: - """ - % (groups_infos.formsemestre_id, default_group_id) - ] - - H.append(menu_groups_choice(groups_infos, submit_on_change=submit_on_change)) - - if with_selectall_butt: - H.append( - """""" - ) - H.append("
") - - return "\n".join(H) - - -def menu_groups_choice(groups_infos, submit_on_change=False): - """menu pour selection groupes - group_ids est la liste des groupes actuellement sélectionnés - et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. - (utilisé pour retrouver le semestre et proposer la liste des autres groupes) - """ - default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) - - if submit_on_change: - klass = "submit_on_change" - else: - klass = "" - H = [ - """ ") - return "\n".join(H) - - -def menu_group_choice(group_id=None, formsemestre_id=None): - """Un bête menu pour choisir un seul groupe - group_id est le groupe actuellement sélectionné. - Si aucun groupe selectionné, utilise formsemestre_id pour lister les groupes. - """ - if group_id: - group = sco_groups.get_group(group_id) - formsemestre_id = group["formsemestre_id"] - elif not formsemestre_id: - raise ValueError("missing formsemestre_id") - H = [ - """ - - - """ - ) - return "\n".join(H) - - -class DisplayedGroupsInfos(object): - """Container with attributes describing groups to display in the page - .groups_query_args : 'group_ids=xxx&group_ids=yyy' - .base_url : url de la requete, avec les groupes, sans les autres paramètres - .formsemestre_id : semestre "principal" (en fait celui du 1er groupe de la liste) - .members - .groups_titles - """ - - def __init__( - self, - group_ids=[], # groupes specifies dans l'URL, ou un seul int - formsemestre_id=None, - etat=None, - select_all_when_unspecified=False, - moduleimpl_id=None, # used to find formsemestre when unspecified - ): - if isinstance(group_ids, int): - if group_ids: - group_ids = [group_ids] # cas ou un seul parametre, pas de liste - else: - group_ids = [] - if not formsemestre_id and moduleimpl_id: - mods = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id) - if len(mods) != 1: - raise ValueError("invalid moduleimpl_id") - formsemestre_id = mods[0]["formsemestre_id"] - - if not group_ids: # appel sans groupe (eg page accueil) - if not formsemestre_id: - raise Exception("missing parameter formsemestre_id or group_ids") - if select_all_when_unspecified: - group_ids = [sco_groups.get_default_group(formsemestre_id)] - else: - # selectionne le premier groupe trouvé, s'il y en a un - partition = sco_groups.get_partitions_list( - formsemestre_id, with_default=True - )[0] - groups = sco_groups.get_partition_groups(partition) - if groups: - group_ids = [groups[0]["group_id"]] - else: - group_ids = [sco_groups.get_default_group(formsemestre_id)] - - gq = [] - for group_id in group_ids: - gq.append("group_ids=" + str(group_id)) - self.groups_query_args = "&".join(gq) - self.base_url = request.base_url + "?" + self.groups_query_args - self.group_ids = group_ids - self.groups = [] - groups_titles = [] - self.members = [] - self.tous_les_etuds_du_sem = ( - False # affiche tous les etuds du semestre ? (si un seul semestre) - ) - self.sems = collections.OrderedDict() # formsemestre_id : sem - self.formsemestre = None - self.formsemestre_id = formsemestre_id - self.nbdem = 0 # nombre d'étudiants démissionnaires en tout - sem = None - selected_partitions = set() - for group_id in group_ids: - group_members, group, group_tit, sem, nbdem = sco_groups.get_group_infos( - group_id, etat=etat - ) - self.groups.append(group) - self.nbdem += nbdem - self.sems[sem["formsemestre_id"]] = sem - if not self.formsemestre_id: - self.formsemestre_id = sem["formsemestre_id"] - self.formsemestre = sem - self.members.extend(group_members) - groups_titles.append(group_tit) - if group["group_name"] == None: - self.tous_les_etuds_du_sem = True - else: - # liste les partitions explicitement sélectionnés (= des groupes de group_ids) - selected_partitions.add((group["numero"], group["partition_id"])) - - self.selected_partitions = [ - x[1] for x in sorted(list(selected_partitions)) - ] # -> [ partition_id ] - - if not self.formsemestre: # aucun groupe selectionne - self.formsemestre = sco_formsemestre.get_formsemestre(formsemestre_id) - - self.sortuniq() - - if len(self.sems) > 1: - self.tous_les_etuds_du_sem = False # plusieurs semestres - if self.tous_les_etuds_du_sem: - if sem and sem["semestre_id"] >= 0: - self.groups_titles = "S%d" % sem["semestre_id"] - else: - self.groups_titles = "tous" - self.groups_filename = self.groups_titles - else: - self.groups_titles = ", ".join(groups_titles) - self.groups_filename = "_".join(groups_titles).replace(" ", "_") - # Sanitize filename: - self.groups_filename = scu.make_filename(self.groups_filename) - - # colonnes pour affichages nom des groupes: - # gère le cas où les étudiants appartiennent à des semestres différents - self.partitions = [] # les partitions, sans celle par defaut - for formsemestre_id in self.sems: - for partition in sco_groups.get_partitions_list(formsemestre_id): - if partition["partition_name"]: - self.partitions.append(partition) - - def sortuniq(self): - "Trie les étudiants (de plusieurs groupes) et elimine les doublons" - if (len(self.group_ids) <= 1) or len(self.members) <= 1: - return # on suppose que les etudiants d'un groupe sont deja triés - self.members.sort( - key=operator.itemgetter("nom_disp", "prenom") - ) # tri selon nom_usuel ou nom - to_remove = [] - T = self.members - for i in range(len(T) - 1, 0, -1): - if T[i - 1]["etudid"] == T[i]["etudid"]: - to_remove.append(i) - for i in to_remove: - del T[i] - - def get_form_elem(self): - """html hidden input with groups""" - H = [] - for group_id in self.group_ids: - H.append('' % group_id) - return "\n".join(H) - - -# Ancien ZScolar.group_list renommé ici en group_table -def groups_table( - REQUEST=None, - groups_infos=None, # instance of DisplayedGroupsInfos - with_codes=0, - etat=None, - format="html", - with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) - with_archives=0, # ajoute colonne avec noms fichiers archivés - with_annotations=0, -): - """liste etudiants inscrits dans ce semestre - format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf - Si with_codes, ajoute 4 colonnes avec les codes etudid, NIP, INE et etape - """ - from app.scodoc import sco_report - - # log( - # "enter groups_table %s: %s" - # % (groups_infos.members[0]["nom"], groups_infos.members[0].get("etape", "-")) - # ) - with_codes = int(with_codes) - with_paiement = int(with_paiement) - with_archives = int(with_archives) - with_annotations = int(with_annotations) - - base_url_np = groups_infos.base_url + "&with_codes=%s" % with_codes - base_url = ( - base_url_np - + "&with_paiement=%s&with_archives=%s&with_annotations=%s" - % (with_paiement, with_archives, with_annotations) - ) - # - columns_ids = ["civilite_str", "nom_disp", "prenom"] # colonnes a inclure - titles = { - "civilite_str": "Civ.", - "nom_disp": "Nom", - "prenom": "Prénom", - "email": "Mail", - "emailperso": "Personnel", - "etat": "Etat", - "etudid": "etudid", - "code_nip": "code_nip", - "code_ine": "code_ine", - "datefinalisationinscription_str": "Finalisation inscr.", - "paiementinscription_str": "Paiement", - "etudarchive": "Fichiers", - "annotations_str": "Annotations", - "etape": "Etape", - "semestre_groupe": "Semestre-Groupe", # pour Moodle - } - - # ajoute colonnes pour groupes - columns_ids.extend([p["partition_id"] for p in groups_infos.partitions]) - titles.update( - dict( - [(p["partition_id"], p["partition_name"]) for p in groups_infos.partitions] - ) - ) - partitions_name = { - p["partition_id"]: p["partition_name"] for p in groups_infos.partitions - } - - if format != "html": # ne mentionne l'état que en Excel (style en html) - columns_ids.append("etat") - columns_ids.append("email") - columns_ids.append("emailperso") - - if format == "moodlecsv": - columns_ids = ["email", "semestre_groupe"] - - if with_codes: - columns_ids += ["etape", "etudid", "code_nip", "code_ine"] - if with_paiement: - columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"] - if with_paiement or with_codes: - sco_portal_apogee.check_paiement_etuds(groups_infos.members) - if with_archives: - from app.scodoc import sco_archives_etud - - sco_archives_etud.add_archives_info_to_etud_list(groups_infos.members) - columns_ids += ["etudarchive"] - if with_annotations: - sco_etud.add_annotations_to_etud_list(groups_infos.members) - columns_ids += ["annotations_str"] - moodle_sem_name = groups_infos.formsemestre["session_id"] - moodle_groupenames = set() - # ajoute liens - for etud in groups_infos.members: - if etud["email"]: - etud["_email_target"] = "mailto:" + etud["email"] - else: - etud["_email_target"] = "" - if etud["emailperso"]: - etud["_emailperso_target"] = "mailto:" + etud["emailperso"] - else: - etud["_emailperso_target"] = "" - fiche_url = url_for( - "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] - ) - etud["_nom_disp_target"] = fiche_url - etud["_prenom_target"] = fiche_url - - etud["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"]) - - if etud["etat"] == "D": - etud["_css_row_class"] = "etuddem" - # et groupes: - for partition_id in etud["partitions"]: - etud[partition_id] = etud["partitions"][partition_id]["group_name"] - # Ajoute colonne pour moodle: semestre_groupe, de la forme RT-DUT-FI-S3-2021-PARTITION-GROUPE - moodle_groupename = [] - if groups_infos.selected_partitions: - # il y a des groupes selectionnes, utilise leurs partitions - for partition_id in groups_infos.selected_partitions: - if partition_id in etud["partitions"]: - moodle_groupename.append( - partitions_name[partition_id] - + "-" - + etud["partitions"][partition_id]["group_name"] - ) - else: - # pas de groupes sélectionnés: prend le premier s'il y en a un - moodle_groupename = ["tous"] - if etud["partitions"]: - for p in etud["partitions"].items(): # partitions is an OrderedDict - moodle_groupename = [ - partitions_name[p[0]] + "-" + p[1]["group_name"] - ] - break - - moodle_groupenames |= set(moodle_groupename) - etud["semestre_groupe"] = moodle_sem_name + "-" + "+".join(moodle_groupename) - - if groups_infos.nbdem > 1: - s = "s" - else: - s = "" - - if format == "moodlecsv": - # de la forme S1-[FI][FA]-groupe.csv - if not moodle_groupenames: - moodle_groupenames = {"tous"} - filename = ( - moodle_sem_name - + "-" - + groups_infos.formsemestre["modalite"] - + "-" - + "+".join(sorted(moodle_groupenames)) - ) - else: - filename = "etudiants_%s" % groups_infos.groups_filename - - prefs = sco_preferences.SemPreferences(groups_infos.formsemestre_id) - tab = GenTable( - rows=groups_infos.members, - columns_ids=columns_ids, - titles=titles, - caption="soit %d étudiants inscrits et %d démissionaire%s." - % (len(groups_infos.members) - groups_infos.nbdem, groups_infos.nbdem, s), - base_url=base_url, - filename=filename, - pdf_link=False, # pas d'export pdf - html_sortable=True, - html_class="table_leftalign table_listegroupe", - xml_outer_tag="group_list", - xml_row_tag="etud", - text_fields_separator=prefs["moodle_csv_separator"], - text_with_titles=prefs["moodle_csv_with_headerline"], - preferences=prefs, - ) - # - if format == "html": - amail_inst = [ - x["email"] for x in groups_infos.members if x["email"] and x["etat"] != "D" - ] - amail_perso = [ - x["emailperso"] - for x in groups_infos.members - if x["emailperso"] and x["etat"] != "D" - ] - - if len(groups_infos.members): - if groups_infos.tous_les_etuds_du_sem: - htitle = "Les %d étudiants inscrits" % len(groups_infos.members) - else: - htitle = "Groupe %s (%d étudiants)" % ( - groups_infos.groups_titles, - len(groups_infos.members), - ) - else: - htitle = "Aucun étudiant !" - H = [ - '
' '

', - htitle, - "", - ] - if groups_infos.members: - Of = [] - options = { - "with_paiement": "Paiement inscription", - "with_archives": "Fichiers archivés", - "with_annotations": "Annotations", - "with_codes": "Codes", - } - for option in options: - if locals().get(option, False): - selected = "selected" - else: - selected = "" - Of.append( - """""" - % (option, selected, options[option]) - ) - - H.extend( - [ - """ - - """, - ] - ) - H.append("

") - if groups_infos.members: - H.extend( - [ - tab.html(), - "") - - return "".join(H) + "
" - - elif ( - format == "pdf" - or format == "xml" - or format == "json" - or format == "xls" - or format == "moodlecsv" - ): - if format == "moodlecsv": - format = "csv" - return tab.make_page(format=format) - - elif format == "xlsappel": - xls = sco_excel.excel_feuille_listeappel( - groups_infos.formsemestre, - groups_infos.groups_titles, - groups_infos.members, - partitions=groups_infos.partitions, - with_codes=with_codes, - with_paiement=with_paiement, - server_name=request.url_root, - ) - filename = "liste_%s" % groups_infos.groups_filename + ".xlsx" - return sco_excel.send_excel_file(REQUEST, xls, filename) - elif format == "allxls": - # feuille Excel avec toutes les infos etudiants - if not groups_infos.members: - return "" - keys = [ - "etudid", - "code_nip", - "etat", - "civilite_str", - "nom", - "nom_usuel", - "prenom", - "inscriptionstr", - ] - if with_paiement: - keys.append("paiementinscription") - keys += [ - "email", - "emailperso", - "domicile", - "villedomicile", - "codepostaldomicile", - "paysdomicile", - "telephone", - "telephonemobile", - "fax", - "date_naissance", - "lieu_naissance", - "bac", - "specialite", - "annee_bac", - "nomlycee", - "villelycee", - "codepostallycee", - "codelycee", - "type_admission", - "boursier_prec", - "debouche", - "parcours", - "codeparcours", - ] - titles = keys[:] - other_partitions = sco_groups.get_group_other_partitions(groups_infos.groups[0]) - keys += [p["partition_id"] for p in other_partitions] - titles += [p["partition_name"] for p in other_partitions] - # remplis infos lycee si on a que le code lycée - # et ajoute infos inscription - for m in groups_infos.members: - etud = sco_etud.get_etud_info(m["etudid"], filled=True)[0] - m.update(etud) - sco_etud.etud_add_lycee_infos(etud) - # et ajoute le parcours - Se = sco_parcours_dut.SituationEtudParcours( - etud, groups_infos.formsemestre_id - ) - m["parcours"] = Se.get_parcours_descr() - m["codeparcours"], _ = sco_report.get_codeparcoursetud(etud) - - def dicttakestr(d, keys): - r = [] - for k in keys: - r.append(str(d.get(k, ""))) - return r - - L = [dicttakestr(m, keys) for m in groups_infos.members] - title = "etudiants_%s" % groups_infos.groups_filename - xls = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title) - filename = title + scu.XLSX_SUFFIX - return sco_excel.send_excel_file(REQUEST, xls, filename) - else: - raise ValueError("unsupported format") - - -def tab_absences_html(groups_infos, etat=None, REQUEST=None): - """contenu du tab "absences et feuilles diverses" """ - authuser = current_user - H = ['
'] - if not groups_infos.members: - return "".join(H) + "

Aucun étudiant !

" - H.extend( - [ - "

Absences

", - '", - "

Feuilles

", - '", - ] - ) - - H.append('

Opérations diverses

") - return "".join(H) - - -def tab_photos_html(groups_infos, etat=None, REQUEST=None): - """contenu du tab "photos" """ - from app.scodoc import sco_trombino - - if not groups_infos.members: - return '

Aucun étudiant !

' - - return sco_trombino.trombino_html(groups_infos, REQUEST=REQUEST) - - -def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None, REQUEST=None): - """Formulaire choix jour semaine pour saisie.""" - authuser = current_user - if not authuser.has_permission(Permission.ScoAbsChange): - return "" - sem = groups_infos.formsemestre - first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday() - today_idx = datetime.date.today().weekday() - - FA = [] # formulaire avec menu saisi absences - FA.append( - '
' - ) - FA.append('' % sem) - FA.append(groups_infos.get_form_elem()) - if moduleimpl_id: - FA.append( - '' % moduleimpl_id - ) - FA.append('') - - FA.append( - """""" - ) - FA.append("""") - FA.append("
") - return "\n".join(FA) - - -# Ajout Le Havre -# Formulaire saisie absences semaine -def form_choix_saisie_semaine(groups_infos, REQUEST=None): - authuser = current_user - if not authuser.has_permission(Permission.ScoAbsChange): - return "" - # construit l'URL "destination" - # (a laquelle on revient apres saisie absences) - query_args = parse_qs(request.query_string) - moduleimpl_id = query_args.get("moduleimpl_id", [""])[0] - if "head_message" in query_args: - del query_args["head_message"] - destination = "%s?%s" % ( - request.base_url, - urllib.parse.urlencode(query_args, True), - ) - destination = destination.replace( - "%", "%%" - ) # car ici utilisee dans un format string ! - - DateJour = time.strftime("%d/%m/%Y") - datelundi = sco_abs.ddmmyyyy(DateJour).prev_monday() - FA = [] # formulaire avec menu saisie hebdo des absences - FA.append('
') - FA.append('' % datelundi) - FA.append('' % moduleimpl_id) - FA.append('' % destination) - FA.append(groups_infos.get_form_elem()) - FA.append('') - FA.append("
") - return "\n".join(FA) - - -def export_groups_as_moodle_csv(formsemestre_id=None): - """Export all students/groups, in a CSV format suitable for Moodle - Each (student,group) will be listed on a separate line - jo@univ.fr,S3-A - jo@univ.fr,S3-B1 - if jo belongs to group A in a partition, and B1 in another one. - Caution: if groups in different partitions share the same name, there will be - duplicates... should we prefix the group names with the partition's name ? - """ - if not formsemestre_id: - raise ScoValueError("missing parameter: formsemestre_id") - _, partitions_etud_groups = sco_groups.get_formsemestre_groups( - formsemestre_id, with_default=True - ) - sem = sco_formsemestre.get_formsemestre(formsemestre_id) - moodle_sem_name = sem["session_id"] - - columns_ids = ("email", "semestre_groupe") - T = [] - for partition_id in partitions_etud_groups: - partition = sco_groups.get_partition(partition_id) - members = partitions_etud_groups[partition_id] - for etudid in members: - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - group_name = members[etudid]["group_name"] - elts = [moodle_sem_name] - if partition["partition_name"]: - elts.append(partition["partition_name"]) - if group_name: - elts.append(group_name) - T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)}) - # Make table - prefs = sco_preferences.SemPreferences(formsemestre_id) - tab = GenTable( - rows=T, - columns_ids=("email", "semestre_groupe"), - filename=moodle_sem_name + "-moodle", - titles={x: x for x in columns_ids}, - text_fields_separator=prefs["moodle_csv_separator"], - text_with_titles=prefs["moodle_csv_with_headerline"], - preferences=prefs, - ) - return tab.make_page(format="csv") +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 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 +# +############################################################################## + +"""Affichage étudiants d'un ou plusieurs groupes + sous forme: de liste html (table exportable), de trombinoscope (exportable en pdf) +""" + +# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code) + +import collections +import datetime +import operator +import urllib +from urllib.parse import parse_qs +import time + + +from flask import url_for, g, request +from flask_login import current_user + +import app.scodoc.sco_utils as scu +from app.scodoc import html_sco_header +from app.scodoc import sco_abs +from app.scodoc import sco_excel +from app.scodoc import sco_formsemestre +from app.scodoc import sco_groups +from app.scodoc import sco_moduleimpl +from app.scodoc import sco_parcours_dut +from app.scodoc import sco_portal_apogee +from app.scodoc import sco_preferences +from app.scodoc import sco_etud +from app.scodoc.gen_tables import GenTable +from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.sco_permissions import Permission +from six.moves import range + +JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [ + "js/etud_info.js", + "js/groups_view.js", +] + +CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS + + +def groups_view( + group_ids=[], + format="html", + REQUEST=None, + # Options pour listes: + with_codes=0, + etat=None, + with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) + with_archives=0, # ajoute colonne avec noms fichiers archivés + with_annotations=0, + formsemestre_id=None, # utilise si aucun groupe selectionné +): + """Affichage des étudiants des groupes indiqués + group_ids: liste de group_id + format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf + """ + # Informations sur les groupes à afficher: + groups_infos = DisplayedGroupsInfos( + group_ids, + formsemestre_id=formsemestre_id, + etat=etat, + select_all_when_unspecified=True, + ) + # Formats spéciaux: download direct + if format != "html": + return groups_table( + groups_infos=groups_infos, + format=format, + REQUEST=REQUEST, + with_codes=with_codes, + etat=etat, + with_paiement=with_paiement, + with_archives=with_archives, + with_annotations=with_annotations, + ) + + H = [ + html_sco_header.sco_header( + javascripts=JAVASCRIPTS, + cssstyles=CSSSTYLES, + init_qtip=True, + ) + ] + # Menu choix groupe + H.append("""
""") + H.append(form_groups_choice(groups_infos, submit_on_change=True)) + # Note: le formulaire est soumis a chaque modif des groupes + # on pourrait faire comme pour le form de saisie des notes. Il faudrait pour cela: + # - charger tous les etudiants au debut, quels que soient les groupes selectionnés + # - ajouter du JS pour modifier les liens (arguments group_ids) quand le menu change + + # Tabs + # H.extend( ("""toto""",) ) + H.extend( + ( + """ +
+ +
+
+ """, + groups_table( + groups_infos=groups_infos, + format=format, + REQUEST=REQUEST, + with_codes=with_codes, + etat=etat, + with_paiement=with_paiement, + with_archives=with_archives, + with_annotations=with_annotations, + ), + "
", + """
""", + tab_photos_html(groups_infos, etat=etat, REQUEST=REQUEST), + #'

hello

', + "
", + '
', + tab_absences_html(groups_infos, etat=etat, REQUEST=REQUEST), + "
", + ) + ) + + H.append(html_sco_header.sco_footer()) + return "\n".join(H) + + +def form_groups_choice(groups_infos, with_selectall_butt=False, submit_on_change=False): + """form pour selection groupes + group_ids est la liste des groupes actuellement sélectionnés + et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. + (utilisé pour retrouver le semestre et proposer la liste des autres groupes) + + Si submit_on_change, ajoute une classe "submit_on_change" qui est utilisee en JS + """ + default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) + + H = [ + """
+ + + Groupes: + """ + % (groups_infos.formsemestre_id, default_group_id) + ] + + H.append(menu_groups_choice(groups_infos, submit_on_change=submit_on_change)) + + if with_selectall_butt: + H.append( + """""" + ) + H.append("
") + + return "\n".join(H) + + +def menu_groups_choice(groups_infos, submit_on_change=False): + """menu pour selection groupes + group_ids est la liste des groupes actuellement sélectionnés + et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. + (utilisé pour retrouver le semestre et proposer la liste des autres groupes) + """ + default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) + + if submit_on_change: + klass = "submit_on_change" + else: + klass = "" + H = [ + """ ") + return "\n".join(H) + + +def menu_group_choice(group_id=None, formsemestre_id=None): + """Un bête menu pour choisir un seul groupe + group_id est le groupe actuellement sélectionné. + Si aucun groupe selectionné, utilise formsemestre_id pour lister les groupes. + """ + if group_id: + group = sco_groups.get_group(group_id) + formsemestre_id = group["formsemestre_id"] + elif not formsemestre_id: + raise ValueError("missing formsemestre_id") + H = [ + """ + + + """ + ) + return "\n".join(H) + + +class DisplayedGroupsInfos(object): + """Container with attributes describing groups to display in the page + .groups_query_args : 'group_ids=xxx&group_ids=yyy' + .base_url : url de la requete, avec les groupes, sans les autres paramètres + .formsemestre_id : semestre "principal" (en fait celui du 1er groupe de la liste) + .members + .groups_titles + """ + + def __init__( + self, + group_ids=[], # groupes specifies dans l'URL, ou un seul int + formsemestre_id=None, + etat=None, + select_all_when_unspecified=False, + moduleimpl_id=None, # used to find formsemestre when unspecified + ): + if isinstance(group_ids, int): + if group_ids: + group_ids = [group_ids] # cas ou un seul parametre, pas de liste + else: + group_ids = [] + if not formsemestre_id and moduleimpl_id: + mods = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id) + if len(mods) != 1: + raise ValueError("invalid moduleimpl_id") + formsemestre_id = mods[0]["formsemestre_id"] + + if not group_ids: # appel sans groupe (eg page accueil) + if not formsemestre_id: + raise Exception("missing parameter formsemestre_id or group_ids") + if select_all_when_unspecified: + group_ids = [sco_groups.get_default_group(formsemestre_id)] + else: + # selectionne le premier groupe trouvé, s'il y en a un + partition = sco_groups.get_partitions_list( + formsemestre_id, with_default=True + )[0] + groups = sco_groups.get_partition_groups(partition) + if groups: + group_ids = [groups[0]["group_id"]] + else: + group_ids = [sco_groups.get_default_group(formsemestre_id)] + + gq = [] + for group_id in group_ids: + gq.append("group_ids=" + str(group_id)) + self.groups_query_args = "&".join(gq) + self.base_url = request.base_url + "?" + self.groups_query_args + self.group_ids = group_ids + self.groups = [] + groups_titles = [] + self.members = [] + self.tous_les_etuds_du_sem = ( + False # affiche tous les etuds du semestre ? (si un seul semestre) + ) + self.sems = collections.OrderedDict() # formsemestre_id : sem + self.formsemestre = None + self.formsemestre_id = formsemestre_id + self.nbdem = 0 # nombre d'étudiants démissionnaires en tout + sem = None + selected_partitions = set() + for group_id in group_ids: + group_members, group, group_tit, sem, nbdem = sco_groups.get_group_infos( + group_id, etat=etat + ) + self.groups.append(group) + self.nbdem += nbdem + self.sems[sem["formsemestre_id"]] = sem + if not self.formsemestre_id: + self.formsemestre_id = sem["formsemestre_id"] + self.formsemestre = sem + self.members.extend(group_members) + groups_titles.append(group_tit) + if group["group_name"] == None: + self.tous_les_etuds_du_sem = True + else: + # liste les partitions explicitement sélectionnés (= des groupes de group_ids) + selected_partitions.add((group["numero"], group["partition_id"])) + + self.selected_partitions = [ + x[1] for x in sorted(list(selected_partitions)) + ] # -> [ partition_id ] + + if not self.formsemestre: # aucun groupe selectionne + self.formsemestre = sco_formsemestre.get_formsemestre(formsemestre_id) + + self.sortuniq() + + if len(self.sems) > 1: + self.tous_les_etuds_du_sem = False # plusieurs semestres + if self.tous_les_etuds_du_sem: + if sem and sem["semestre_id"] >= 0: + self.groups_titles = "S%d" % sem["semestre_id"] + else: + self.groups_titles = "tous" + self.groups_filename = self.groups_titles + else: + self.groups_titles = ", ".join(groups_titles) + self.groups_filename = "_".join(groups_titles).replace(" ", "_") + # Sanitize filename: + self.groups_filename = scu.make_filename(self.groups_filename) + + # colonnes pour affichages nom des groupes: + # gère le cas où les étudiants appartiennent à des semestres différents + self.partitions = [] # les partitions, sans celle par defaut + for formsemestre_id in self.sems: + for partition in sco_groups.get_partitions_list(formsemestre_id): + if partition["partition_name"]: + self.partitions.append(partition) + + def sortuniq(self): + "Trie les étudiants (de plusieurs groupes) et elimine les doublons" + if (len(self.group_ids) <= 1) or len(self.members) <= 1: + return # on suppose que les etudiants d'un groupe sont deja triés + self.members.sort( + key=operator.itemgetter("nom_disp", "prenom") + ) # tri selon nom_usuel ou nom + to_remove = [] + T = self.members + for i in range(len(T) - 1, 0, -1): + if T[i - 1]["etudid"] == T[i]["etudid"]: + to_remove.append(i) + for i in to_remove: + del T[i] + + def get_form_elem(self): + """html hidden input with groups""" + H = [] + for group_id in self.group_ids: + H.append('' % group_id) + return "\n".join(H) + + +# Ancien ZScolar.group_list renommé ici en group_table +def groups_table( + REQUEST=None, + groups_infos=None, # instance of DisplayedGroupsInfos + with_codes=0, + etat=None, + format="html", + with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) + with_archives=0, # ajoute colonne avec noms fichiers archivés + with_annotations=0, +): + """liste etudiants inscrits dans ce semestre + format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf + Si with_codes, ajoute 4 colonnes avec les codes etudid, NIP, INE et etape + """ + from app.scodoc import sco_report + + # log( + # "enter groups_table %s: %s" + # % (groups_infos.members[0]["nom"], groups_infos.members[0].get("etape", "-")) + # ) + with_codes = int(with_codes) + with_paiement = int(with_paiement) + with_archives = int(with_archives) + with_annotations = int(with_annotations) + + base_url_np = groups_infos.base_url + "&with_codes=%s" % with_codes + base_url = ( + base_url_np + + "&with_paiement=%s&with_archives=%s&with_annotations=%s" + % (with_paiement, with_archives, with_annotations) + ) + # + columns_ids = ["civilite_str", "nom_disp", "prenom"] # colonnes a inclure + titles = { + "civilite_str": "Civ.", + "nom_disp": "Nom", + "prenom": "Prénom", + "email": "Mail", + "emailperso": "Personnel", + "etat": "Etat", + "etudid": "etudid", + "code_nip": "code_nip", + "code_ine": "code_ine", + "datefinalisationinscription_str": "Finalisation inscr.", + "paiementinscription_str": "Paiement", + "etudarchive": "Fichiers", + "annotations_str": "Annotations", + "etape": "Etape", + "semestre_groupe": "Semestre-Groupe", # pour Moodle + } + + # ajoute colonnes pour groupes + columns_ids.extend([p["partition_id"] for p in groups_infos.partitions]) + titles.update( + dict( + [(p["partition_id"], p["partition_name"]) for p in groups_infos.partitions] + ) + ) + partitions_name = { + p["partition_id"]: p["partition_name"] for p in groups_infos.partitions + } + + if format != "html": # ne mentionne l'état que en Excel (style en html) + columns_ids.append("etat") + columns_ids.append("email") + columns_ids.append("emailperso") + + if format == "moodlecsv": + columns_ids = ["email", "semestre_groupe"] + + if with_codes: + columns_ids += ["etape", "etudid", "code_nip", "code_ine"] + if with_paiement: + columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"] + if with_paiement or with_codes: + sco_portal_apogee.check_paiement_etuds(groups_infos.members) + if with_archives: + from app.scodoc import sco_archives_etud + + sco_archives_etud.add_archives_info_to_etud_list(groups_infos.members) + columns_ids += ["etudarchive"] + if with_annotations: + sco_etud.add_annotations_to_etud_list(groups_infos.members) + columns_ids += ["annotations_str"] + moodle_sem_name = groups_infos.formsemestre["session_id"] + moodle_groupenames = set() + # ajoute liens + for etud in groups_infos.members: + if etud["email"]: + etud["_email_target"] = "mailto:" + etud["email"] + else: + etud["_email_target"] = "" + if etud["emailperso"]: + etud["_emailperso_target"] = "mailto:" + etud["emailperso"] + else: + etud["_emailperso_target"] = "" + fiche_url = url_for( + "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] + ) + etud["_nom_disp_target"] = fiche_url + etud["_prenom_target"] = fiche_url + + etud["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"]) + + if etud["etat"] == "D": + etud["_css_row_class"] = "etuddem" + # et groupes: + for partition_id in etud["partitions"]: + etud[partition_id] = etud["partitions"][partition_id]["group_name"] + # Ajoute colonne pour moodle: semestre_groupe, de la forme RT-DUT-FI-S3-2021-PARTITION-GROUPE + moodle_groupename = [] + if groups_infos.selected_partitions: + # il y a des groupes selectionnes, utilise leurs partitions + for partition_id in groups_infos.selected_partitions: + if partition_id in etud["partitions"]: + moodle_groupename.append( + partitions_name[partition_id] + + "-" + + etud["partitions"][partition_id]["group_name"] + ) + else: + # pas de groupes sélectionnés: prend le premier s'il y en a un + moodle_groupename = ["tous"] + if etud["partitions"]: + for p in etud["partitions"].items(): # partitions is an OrderedDict + moodle_groupename = [ + partitions_name[p[0]] + "-" + p[1]["group_name"] + ] + break + + moodle_groupenames |= set(moodle_groupename) + etud["semestre_groupe"] = moodle_sem_name + "-" + "+".join(moodle_groupename) + + if groups_infos.nbdem > 1: + s = "s" + else: + s = "" + + if format == "moodlecsv": + # de la forme S1-[FI][FA]-groupe.csv + if not moodle_groupenames: + moodle_groupenames = {"tous"} + filename = ( + moodle_sem_name + + "-" + + groups_infos.formsemestre["modalite"] + + "-" + + "+".join(sorted(moodle_groupenames)) + ) + else: + filename = "etudiants_%s" % groups_infos.groups_filename + + prefs = sco_preferences.SemPreferences(groups_infos.formsemestre_id) + tab = GenTable( + rows=groups_infos.members, + columns_ids=columns_ids, + titles=titles, + caption="soit %d étudiants inscrits et %d démissionaire%s." + % (len(groups_infos.members) - groups_infos.nbdem, groups_infos.nbdem, s), + base_url=base_url, + filename=filename, + pdf_link=False, # pas d'export pdf + html_sortable=True, + html_class="table_leftalign table_listegroupe", + xml_outer_tag="group_list", + xml_row_tag="etud", + text_fields_separator=prefs["moodle_csv_separator"], + text_with_titles=prefs["moodle_csv_with_headerline"], + preferences=prefs, + ) + # + if format == "html": + amail_inst = [ + x["email"] for x in groups_infos.members if x["email"] and x["etat"] != "D" + ] + amail_perso = [ + x["emailperso"] + for x in groups_infos.members + if x["emailperso"] and x["etat"] != "D" + ] + + if len(groups_infos.members): + if groups_infos.tous_les_etuds_du_sem: + htitle = "Les %d étudiants inscrits" % len(groups_infos.members) + else: + htitle = "Groupe %s (%d étudiants)" % ( + groups_infos.groups_titles, + len(groups_infos.members), + ) + else: + htitle = "Aucun étudiant !" + H = [ + '
' '

', + htitle, + "", + ] + if groups_infos.members: + Of = [] + options = { + "with_paiement": "Paiement inscription", + "with_archives": "Fichiers archivés", + "with_annotations": "Annotations", + "with_codes": "Codes", + } + for option in options: + if locals().get(option, False): + selected = "selected" + else: + selected = "" + Of.append( + """""" + % (option, selected, options[option]) + ) + + H.extend( + [ + """ + + """, + ] + ) + H.append("

") + if groups_infos.members: + H.extend( + [ + tab.html(), + "") + + return "".join(H) + "
" + + elif ( + format == "pdf" + or format == "xml" + or format == "json" + or format == "xls" + or format == "moodlecsv" + ): + if format == "moodlecsv": + format = "csv" + return tab.make_page(format=format) + + elif format == "xlsappel": + xls = sco_excel.excel_feuille_listeappel( + groups_infos.formsemestre, + groups_infos.groups_titles, + groups_infos.members, + partitions=groups_infos.partitions, + with_codes=with_codes, + with_paiement=with_paiement, + server_name=request.url_root, + ) + filename = "liste_%s" % groups_infos.groups_filename + return scu.send_file( + xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) + elif format == "allxls": + # feuille Excel avec toutes les infos etudiants + if not groups_infos.members: + return "" + keys = [ + "etudid", + "code_nip", + "etat", + "civilite_str", + "nom", + "nom_usuel", + "prenom", + "inscriptionstr", + ] + if with_paiement: + keys.append("paiementinscription") + keys += [ + "email", + "emailperso", + "domicile", + "villedomicile", + "codepostaldomicile", + "paysdomicile", + "telephone", + "telephonemobile", + "fax", + "date_naissance", + "lieu_naissance", + "bac", + "specialite", + "annee_bac", + "nomlycee", + "villelycee", + "codepostallycee", + "codelycee", + "type_admission", + "boursier_prec", + "debouche", + "parcours", + "codeparcours", + ] + titles = keys[:] + other_partitions = sco_groups.get_group_other_partitions(groups_infos.groups[0]) + keys += [p["partition_id"] for p in other_partitions] + titles += [p["partition_name"] for p in other_partitions] + # remplis infos lycee si on a que le code lycée + # et ajoute infos inscription + for m in groups_infos.members: + etud = sco_etud.get_etud_info(m["etudid"], filled=True)[0] + m.update(etud) + sco_etud.etud_add_lycee_infos(etud) + # et ajoute le parcours + Se = sco_parcours_dut.SituationEtudParcours( + etud, groups_infos.formsemestre_id + ) + m["parcours"] = Se.get_parcours_descr() + m["codeparcours"], _ = sco_report.get_codeparcoursetud(etud) + + def dicttakestr(d, keys): + r = [] + for k in keys: + r.append(str(d.get(k, ""))) + return r + + L = [dicttakestr(m, keys) for m in groups_infos.members] + title = "etudiants_%s" % groups_infos.groups_filename + xls = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title) + filename = title + scu.XLSX_SUFFIX + return scu.send_file( + xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) + else: + raise ValueError("unsupported format") + + +def tab_absences_html(groups_infos, etat=None, REQUEST=None): + """contenu du tab "absences et feuilles diverses" """ + authuser = current_user + H = ['
'] + if not groups_infos.members: + return "".join(H) + "

Aucun étudiant !

" + H.extend( + [ + "

Absences

", + '", + "

Feuilles

", + '", + ] + ) + + H.append('

Opérations diverses

") + return "".join(H) + + +def tab_photos_html(groups_infos, etat=None, REQUEST=None): + """contenu du tab "photos" """ + from app.scodoc import sco_trombino + + if not groups_infos.members: + return '

Aucun étudiant !

' + + return sco_trombino.trombino_html(groups_infos, REQUEST=REQUEST) + + +def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None, REQUEST=None): + """Formulaire choix jour semaine pour saisie.""" + authuser = current_user + if not authuser.has_permission(Permission.ScoAbsChange): + return "" + sem = groups_infos.formsemestre + first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday() + today_idx = datetime.date.today().weekday() + + FA = [] # formulaire avec menu saisi absences + FA.append( + '
' + ) + FA.append('' % sem) + FA.append(groups_infos.get_form_elem()) + if moduleimpl_id: + FA.append( + '' % moduleimpl_id + ) + FA.append('') + + FA.append( + """""" + ) + FA.append("""") + FA.append("
") + return "\n".join(FA) + + +# Ajout Le Havre +# Formulaire saisie absences semaine +def form_choix_saisie_semaine(groups_infos, REQUEST=None): + authuser = current_user + if not authuser.has_permission(Permission.ScoAbsChange): + return "" + # construit l'URL "destination" + # (a laquelle on revient apres saisie absences) + query_args = parse_qs(request.query_string) + moduleimpl_id = query_args.get("moduleimpl_id", [""])[0] + if "head_message" in query_args: + del query_args["head_message"] + destination = "%s?%s" % ( + request.base_url, + urllib.parse.urlencode(query_args, True), + ) + destination = destination.replace( + "%", "%%" + ) # car ici utilisee dans un format string ! + + DateJour = time.strftime("%d/%m/%Y") + datelundi = sco_abs.ddmmyyyy(DateJour).prev_monday() + FA = [] # formulaire avec menu saisie hebdo des absences + FA.append('
') + FA.append('' % datelundi) + FA.append('' % moduleimpl_id) + FA.append('' % destination) + FA.append(groups_infos.get_form_elem()) + FA.append('') + FA.append("
") + return "\n".join(FA) + + +def export_groups_as_moodle_csv(formsemestre_id=None): + """Export all students/groups, in a CSV format suitable for Moodle + Each (student,group) will be listed on a separate line + jo@univ.fr,S3-A + jo@univ.fr,S3-B1 + if jo belongs to group A in a partition, and B1 in another one. + Caution: if groups in different partitions share the same name, there will be + duplicates... should we prefix the group names with the partition's name ? + """ + if not formsemestre_id: + raise ScoValueError("missing parameter: formsemestre_id") + _, partitions_etud_groups = sco_groups.get_formsemestre_groups( + formsemestre_id, with_default=True + ) + sem = sco_formsemestre.get_formsemestre(formsemestre_id) + moodle_sem_name = sem["session_id"] + + columns_ids = ("email", "semestre_groupe") + T = [] + for partition_id in partitions_etud_groups: + partition = sco_groups.get_partition(partition_id) + members = partitions_etud_groups[partition_id] + for etudid in members: + etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] + group_name = members[etudid]["group_name"] + elts = [moodle_sem_name] + if partition["partition_name"]: + elts.append(partition["partition_name"]) + if group_name: + elts.append(group_name) + T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)}) + # Make table + prefs = sco_preferences.SemPreferences(formsemestre_id) + tab = GenTable( + rows=T, + columns_ids=("email", "semestre_groupe"), + filename=moodle_sem_name + "-moodle", + titles={x: x for x in columns_ids}, + text_fields_separator=prefs["moodle_csv_separator"], + text_with_titles=prefs["moodle_csv_with_headerline"], + preferences=prefs, + ) + return tab.make_page(format="csv") diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index 4c19f940..5df8976d 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -27,316 +27,312 @@ """ScoDoc: génération feuille émargement et placement -Contribution M. Salomon, UFC / IUT DE BELFORT-MONTBÉLIARD, 2016 +Contribution J.-M. Place 2021 +basée sur une idée de M. Salomon, UFC / IUT DE BELFORT-MONTBÉLIARD, 2016 """ import random import time -import urllib +from copy import copy -import flask -from flask import request +import wtforms.validators +from flask import request, render_template from flask_login import current_user - -from app.scodoc.sco_exceptions import ScoValueError +from flask_wtf import FlaskForm +from openpyxl.styles import PatternFill, Alignment, Border, Side, Font +from wtforms import ( + StringField, + SubmitField, + SelectField, + RadioField, + HiddenField, + SelectMultipleField, +) import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb -from app import log -from app.scodoc import html_sco_header +from app import ScoValueError +from app.scodoc import html_sco_header, sco_preferences from app.scodoc import sco_edit_module from app.scodoc import sco_evaluations from app.scodoc import sco_excel +from app.scodoc.sco_excel import ScoExcelBook, COLORS from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl from app.scodoc import sco_permissions_check -from app.scodoc import sco_preferences -from app.scodoc import sco_saisie_notes +from app.scodoc.gen_tables import GenTable from app.scodoc import sco_etud import sco_version -from app.scodoc.gen_tables import GenTable -from app.scodoc.sco_excel import * # XXX à vérifier -from app.scodoc.TrivialFormulator import TrivialFormulator + +_ = lambda x: x # sans babel +_l = _ + +COORD = "Coordonnées" +SEQ = "Continue" + +TOUS = "Tous" -def do_placement_selectetuds(REQUEST): - """ - Choisi les étudiants et les infos sur la salle pour leur placement. - """ - evaluation_id = int(REQUEST.form["evaluation_id"]) - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) - if not E: - raise ScoValueError("invalid evaluation_id") - E = E[0] - # M = sco_moduleimpl.do_moduleimpl_list( moduleimpl_id=E["moduleimpl_id"])[0] +def _get_group_info(evaluation_id): # groupes groups = sco_groups.do_evaluation_listegroupes(evaluation_id, include_default=True) - grlabs = [g["group_name"] or "tous" for g in groups] # legendes des boutons - grnams = [g["group_id"] for g in groups] # noms des checkbox - no_groups = (len(groups) == 1) and groups[0]["group_name"] is None + has_groups = False + groups_tree = {} + for group in groups: + partition = group["partition_name"] or TOUS + group_id = group["group_id"] + group_name = group["group_name"] or TOUS + if partition not in groups_tree: + groups_tree[partition] = {} + groups_tree[partition][group_name] = group_id + if partition != TOUS: + has_groups = True + nb_groups = len(groups_tree) + return groups_tree, has_groups, nb_groups - # description de l'evaluation - H = [ + +class PlacementForm(FlaskForm): + """Formulaire pour placement des étudiants en Salle""" + + evaluation_id = HiddenField("evaluation_id") + file_format = RadioField( + "Format de fichier", + choices=["pdf", "xls"], + validators=[ + wtforms.validators.DataRequired("indiquez le format du fichier attendu"), + ], + ) + surveillants = StringField("Surveillants", validators=[]) + batiment = StringField("Batiment") + salle = StringField("Salle") + nb_rangs = SelectField( + "nb_rangs", coerce=int, choices=[3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] + ) + etiquetage = RadioField( + "Numérotation", + choices=[SEQ, COORD], + validators=[ + wtforms.validators.DataRequired("indiquez le style de numérotation"), + ], + ) + groups = SelectMultipleField( + "Groupe(s)", + validators=[ + wtforms.validators.DataRequired("indiquez au moins un groupe"), + ], + ) + submit = SubmitField("OK") + + def __init__(self, formdata=None, data=None): + super().__init__(formdata=formdata, data=data) + self.groups_tree = {} + self.has_groups = None + self.group_tree_length = None + self.nb_groups = None + self.set_evaluation_infos(data["evaluation_id"]) + + def set_evaluation_infos(self, evaluation_id): + """Initialise les données du formulaire avec les données de l'évaluation.""" + eval_data = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) + if not eval_data: + raise ScoValueError("invalid evaluation_id") + self.groups_tree, self.has_groups, self.nb_groups = _get_group_info( + evaluation_id + ) + if self.has_groups: + choices = [] + for partition in self.groups_tree: + for groupe in self.groups_tree[partition]: + groupe_id = str(self.groups_tree[partition][groupe]) + choices.append((groupe_id, "%s (%s)" % (str(groupe), partition))) + self.groups.choices = choices + + +class _DistributeurContinu: + """Distribue les places selon un ordre numérique.""" + + def __init__(self): + self.position = 1 + + def suivant(self): + """Retounre la désignation de la place suivante""" + retour = self.position + self.position += 1 + return retour + + +class _Distributeur2D: + """Distribue les places selon des coordonnées sur nb_rangs.""" + + def __init__(self, nb_rangs): + self.nb_rangs = nb_rangs + self.rang = 1 + self.index = 1 + + def suivant(self): + """Retounre la désignation de la place suivante""" + retour = (self.index, self.rang) + self.rang += 1 + if self.rang > self.nb_rangs: + self.rang = 1 + self.index += 1 + return retour + + +def placement_eval_selectetuds(evaluation_id): + """Creation de l'écran de placement""" + form = PlacementForm( + request.form, + data={"evaluation_id": int(evaluation_id), "groups": TOUS}, + ) + if form.validate_on_submit(): + runner = PlacementRunner(form) + if not runner.check_placement(): + return ( + """

Génération du placement impossible pour %s

+

(vérifiez que le semestre n'est pas verrouillé et que vous + avez l'autorisation d'effectuer cette opération)

+

Continuer

+ """ + % runner.__dict__ + ) + return runner.exec_placement() # calcul et generation du fichier + # return flask.redirect(url_for("scodoc.index")) + htmls = [ + html_sco_header.sco_header(init_jquery_ui=True), sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), "

Placement et émargement des étudiants

", + render_template("scodoc/forms/placement.html", form=form), ] - # - descr = [ - ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}), - ( - "placement_method", - { - "input_type": "radio", - "default": "xls", - "allow_null": False, - "allowed_values": ["pdf", "xls"], - "labels": ["fichier pdf", "fichier xls"], - "title": "Format de fichier :", - }, - ), - ("teachers", {"size": 25, "title": "Surveillants :"}), - ("building", {"size": 25, "title": "Batiment :"}), - ("room", {"size": 10, "title": "Salle :"}), - ( - "columns", - { - "input_type": "radio", - "default": "5", - "allow_null": False, - "allowed_values": ["3", "4", "5", "6", "7", "8"], - "labels": [ - "3 colonnes", - "4 colonnes", - "5 colonnes", - "6 colonnes", - "7 colonnes", - "8 colonnes", - ], - "title": "Nombre de colonnes :", - }, - ), - ( - "numbering", - { - "input_type": "radio", - "default": "coordinate", - "allow_null": False, - "allowed_values": ["continuous", "coordinate"], - "labels": ["continue", "coordonnées"], - "title": "Numérotation :", - }, - ), - ] - if no_groups: - submitbuttonattributes = [] - descr += [ - ( - "group_ids", - { - "default": [ - g["group_id"] # pylint: disable=invalid-sequence-index - for g in groups - ], - "input_type": "hidden", - "type": "list", - }, - ) - ] - else: - descr += [ - ( - "group_ids", - { - "input_type": "checkbox", - "title": "Choix groupe(s) d'étudiants :", - "allowed_values": grnams, - "labels": grlabs, - "attributes": ['onchange="gr_change(this);"'], - }, - ) - ] - - if not ("group_ids" in REQUEST.form and REQUEST.form["group_ids"]): - submitbuttonattributes = ['disabled="1"'] - else: - submitbuttonattributes = [] # groupe(s) preselectionnés - H.append( - # JS pour desactiver le bouton OK si aucun groupe selectionné - """ - """ - ) - - tf = TrivialFormulator( - request.base_url, - REQUEST.form, - descr, - cancelbutton="Annuler", - submitbuttonattributes=submitbuttonattributes, - submitlabel="OK", - formid="gr", - ) - if tf[0] == 0: - # H.append( """
- # Choix du groupe et de la localisation - # """) - H.append("""
""") - return "\n".join(H) + "\n" + tf[1] + "\n
" - elif tf[0] == -1: - return flask.redirect( - "%s/Notes/moduleimpl_status?moduleimpl_id=%s" - % (scu.ScoURL(), E["moduleimpl_id"]) - ) - else: - placement_method = tf[2]["placement_method"] - teachers = tf[2]["teachers"] - building = tf[2]["building"] - room = tf[2]["room"] - group_ids = tf[2]["group_ids"] - columns = tf[2]["columns"] - numbering = tf[2]["numbering"] - if columns in ("3", "4", "5", "6", "7", "8"): - gs = [("group_ids%3Alist=" + urllib.parse.quote_plus(x)) for x in group_ids] - query = ( - "evaluation_id=%s&placement_method=%s&teachers=%s&building=%s&room=%s&columns=%s&numbering=%s&" - % ( - evaluation_id, - placement_method, - teachers, - building, - room, - columns, - numbering, - ) - + "&".join(gs) - ) - return flask.redirect(scu.NotesURL() + "/do_placement?" + query) - else: - raise ValueError( - "invalid placement_method (%s)" % tf[2]["placement_method"] - ) + footer = html_sco_header.sco_footer() + return "\n".join(htmls) + "

" + footer -def do_placement(REQUEST): - """ - Choisi le placement - """ - authuser = current_user - authusername = str(authuser) - try: - evaluation_id = int(REQUEST.form["evaluation_id"]) - except: - raise ScoValueError( - "Formulaire incomplet ! Vous avez sans doute attendu trop longtemps, veuillez vous reconnecter. Si le problème persiste, contacter l'administrateur. Merci." - ) - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] +class PlacementRunner: + """Execution de l'action définie par le formulaire""" - # Check access - # (admin, respformation, and responsable_id) - if not sco_permissions_check.can_edit_notes(authuser, E["moduleimpl_id"]): - return ( - "

Génération du placement impossible pour %s

" % authusername - + """

(vérifiez que le semestre n'est pas verrouillé et que vous - avez l'autorisation d'effectuer cette opération)

-

Continuer

- """ - % E["moduleimpl_id"] - ) - cnx = ndb.GetDBConnexion() - # Infos transmises - placement_method = REQUEST.form["placement_method"] - teachers = REQUEST.form["teachers"] - building = REQUEST.form["building"] - room = REQUEST.form["room"] - columns = REQUEST.form["columns"] - numbering = REQUEST.form["numbering"] - - # Construit liste des etudiants - group_ids = REQUEST.form.get("group_ids", []) - groups = sco_groups.listgroups(group_ids) - gr_title_filename = sco_groups.listgroups_filename(groups) - # gr_title = sco_groups.listgroups_abbrev(groups) - - if None in [g["group_name"] for g in groups]: # tous les etudiants - getallstudents = True - gr_title_filename = "tous" - else: - getallstudents = False - etudids = sco_groups.do_evaluation_listeetuds_groups( - evaluation_id, groups, getallstudents=getallstudents, include_dems=True - ) - if not etudids: - return "

Aucun groupe sélectionné !

" - - M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] - Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] - sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) - evalname = "%s-%s" % (Mod["code"], ndb.DateDMYtoISO(E["jour"])) - if E["description"]: - evaltitre = E["description"] - else: - evaltitre = "évaluation du %s" % E["jour"] - - desceval = [] # une liste de liste de chaines: description de l'evaluation - desceval.append(["%s" % sem["titreannee"]]) - desceval.append(["Module : %s - %s" % (Mod["code"], Mod["abbrev"])]) - desceval.append(["Surveillants : %s" % teachers]) - desceval.append(["Batiment : %s - Salle : %s" % (building, room)]) - desceval.append(["Controle : %s (coef. %g)" % (evaltitre, E["coefficient"])]) - - listetud = [] # liste de couples (nom,prenom) - for etudid in etudids: - # infos identite etudiant (xxx sous-optimal: 1/select par etudiant) - ident = sco_etud.etudident_list(cnx, {"etudid": etudid})[0] - # infos inscription - inscr = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( - {"etudid": etudid, "formsemestre_id": M["formsemestre_id"]} + def __init__(self, form): + """Calcul et génération du fichier sur la base des données du formulaire""" + self.evaluation_id = form["evaluation_id"].data + self.etiquetage = form["etiquetage"].data + self.surveillants = form["surveillants"].data + self.batiment = form["batiment"].data + self.salle = form["salle"].data + self.nb_rangs = form["nb_rangs"].data + self.file_format = form["file_format"].data + self.groups_ids = form["groups"].data + self.eval_data = sco_evaluations.do_evaluation_list( + {"evaluation_id": self.evaluation_id} )[0] - if inscr["etat"] != "D": - nom = ident["nom"].upper() - prenom = ident["prenom"].lower().capitalize() - listetud.append((nom, prenom)) - random.shuffle(listetud) - - sem_preferences = sco_preferences.SemPreferences() - space = sem_preferences.get("feuille_placement_emargement") - maxlines = sem_preferences.get("feuille_placement_positions") - - if placement_method == "xls": - filename = f"placement_{evalname}_{gr_title_filename}{scu.XLSX_SUFFIX}" - xls = Excel_feuille_placement( - E, desceval, listetud, columns, space, maxlines, building, room, numbering + self.groups = sco_groups.listgroups(self.groups_ids) + self.gr_title_filename = sco_groups.listgroups_filename(self.groups) + # gr_title = sco_groups.listgroups_abbrev(d['groups']) + self.current_user = current_user + self.moduleimpl_id = self.eval_data["moduleimpl_id"] + self.moduleimpl_data = sco_moduleimpl.do_moduleimpl_list( + moduleimpl_id=self.moduleimpl_id + )[0] + self.module_data = sco_edit_module.do_module_list( + args={"module_id": self.moduleimpl_data["module_id"]} + )[0] + self.sem = sco_formsemestre.get_formsemestre( + self.moduleimpl_data["formsemestre_id"] ) - return sco_excel.send_excel_file(REQUEST, xls, filename) - else: - nbcolumns = int(columns) + self.evalname = "%s-%s" % ( + self.module_data["code"], + ndb.DateDMYtoISO(self.eval_data["jour"]), + ) + if self.eval_data["description"]: + self.evaltitre = self.eval_data["description"] + else: + self.evaltitre = "évaluation du %s" % self.eval_data["jour"] + self.desceval = [ # une liste de chaines: description de l'evaluation + "%s" % self.sem["titreannee"], + "Module : %s - %s" % (self.module_data["code"], self.module_data["abbrev"]), + "Surveillants : %s" % self.surveillants, + "Batiment : %(batiment)s - Salle : %(salle)s" % self.__dict__, + "Controle : %s (coef. %g)" + % (self.evaltitre, self.eval_data["coefficient"]), + ] + self.styles = None + self.plan = None + self.listetud = None - pdf_title = "%s
" % sem["titreannee"] - pdf_title += "Module : %s - %s
" % (Mod["code"], Mod["abbrev"]) - pdf_title += "Surveillants : %s
" % teachers - pdf_title += "Batiment : %s - Salle : %s
" % (building, room) - pdf_title += "Controle : %s (coef. %g)
" % (evaltitre, E["coefficient"]) - pdf_title += "Date : %s - Horaire : %s à %s" % ( - E["jour"], - E["heure_debut"], - E["heure_fin"], + def check_placement(self): + """Vérifie que l'utilisateur courant a le droit d'édition sur les notes""" + # Check access (admin, respformation, and responsable_id) + return sco_permissions_check.can_edit_notes( + self.current_user, self.moduleimpl_id ) - filename = "placement_%s_%s.pdf" % (evalname, gr_title_filename) + def exec_placement(self): + """Excéute l'action liée au formulaire""" + self._repartition() + if self.file_format == "xls": + return self._production_xls() + return self._production_pdf() + + def _repartition(self): + """ + Calcule le placement. retourne une liste de couples ((nom, prenom), position) + """ + # Construit liste des etudiants et les réparti + self.groups = sco_groups.listgroups(self.groups_ids) + self.listetud = self._build_listetud() + self.plan = self._affectation_places() + + def _build_listetud(self): + get_all_students = None in [ + g["group_name"] for g in self.groups + ] # tous les etudiants + etudids = sco_groups.do_evaluation_listeetuds_groups( + self.evaluation_id, + self.groups, + getallstudents=get_all_students, + include_dems=True, + ) + listetud = [] # liste de couples (nom,prenom) + for etudid in etudids: + # infos identite etudiant (xxx sous-optimal: 1/select par etudiant) + ident = sco_etud.etudident_list(ndb.GetDBConnexion(), {"etudid": etudid})[0] + # infos inscription + inscr = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( + { + "etudid": etudid, + "formsemestre_id": self.moduleimpl_data["formsemestre_id"], + } + )[0] + if inscr["etat"] != "D": + nom = ident["nom"].upper() + prenom = ident["prenom"].lower().capitalize() + etudid = ident["etudid"] + listetud.append((nom, prenom, etudid)) + random.shuffle(listetud) + return listetud + + def _affectation_places(self): + plan = [] + if self.etiquetage == SEQ: + distributeur = _DistributeurContinu() + else: + distributeur = _Distributeur2D(self.nb_rangs) + for etud in self.listetud: + plan.append((etud, distributeur.suivant())) + return plan + + def _production_xls(self): + filename = "placement_%s_%s" % (self.evalname, self.gr_title_filename) + xls = self._excel_feuille_placement() + return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE) + + def _production_pdf(self): + pdf_title = "
".join(self.desceval) + pdf_title += ( + "\nDate : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" + % self.eval_data + ) + filename = "placement_%(evalname)s_%(gr_title_filename)s" % self.__dict__ titles = { "nom": "Nom", "prenom": "Prenom", @@ -344,41 +340,24 @@ def do_placement(REQUEST): "ligne": "Ligne", "place": "Place", } - if numbering == "coordinate": + if self.etiquetage == COORD: columns_ids = ["nom", "prenom", "colonne", "ligne"] else: columns_ids = ["nom", "prenom", "place"] - # etudiants - line = 1 - col = 1 - orderetud = [] - for etudid in listetud: - if numbering == "coordinate": - orderetud.append((etudid[0], etudid[1], col, line)) - else: - orderetud.append((etudid[0], etudid[1], col + (line - 1) * nbcolumns)) - - if col == nbcolumns: - col = 0 - line += 1 - col += 1 - rows = [] - orderetud.sort() - for etudid in orderetud: - if numbering == "coordinate": + for etud in sorted(self.plan, key=lambda item: item[0][0]): # sort by name + if self.etiquetage == COORD: rows.append( { - "nom": etudid[0], - "prenom": etudid[1], - "colonne": etudid[2], - "ligne": etudid[3], + "nom": etud[0][0], + "prenom": etud[0][1], + "colonne": etud[1][0], + "ligne": etud[1][1], } ) else: - rows.append({"nom": etudid[0], "prenom": etudid[1], "place": etudid[2]}) - + rows.append({"nom": etud[0][0], "prenom": etud[0][1], "place": etud[1]}) tab = GenTable( titles=titles, columns_ids=columns_ids, @@ -389,416 +368,258 @@ def do_placement(REQUEST): + "", pdf_title=pdf_title, # pdf_shorttitle = '', - preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), - # html_generate_cells=False # la derniere ligne (moyennes) est incomplete + preferences=sco_preferences.SemPreferences( + self.moduleimpl_data["formsemestre_id"] + ), ) - t = tab.make_page(format="pdf", with_html_headers=False) - return t + return tab.make_page(format="pdf", with_html_headers=False) - -def placement_eval_selectetuds(evaluation_id, REQUEST=None): - """Dialogue placement etudiants: choix methode et localisation""" - evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) - if not evals: - raise ScoValueError("invalid evaluation_id") - theeval = evals[0] - - if theeval["description"]: - page_title = 'Placement "%s"' % theeval["description"] - else: - page_title = "Placement des étudiants" - H = [html_sco_header.sco_header(page_title=page_title)] - - formid = "placementfile" - if not REQUEST.form.get("%s-submitted" % formid, False): - # not submitted, choix groupe - r = do_placement_selectetuds(REQUEST) - if r: - H.append(r) - - H.append( - """

Explications

- -""" - ) - H.append(html_sco_header.sco_footer()) - return "\n".join(H) - - -def Excel_feuille_placement( - E, description, listetud, columns, space, maxlines, building, room, numbering -): - """Genere feuille excel pour placement des etudiants. - E: evaluation (dict) - lines: liste de tuples - (etudid, nom, prenom, etat, groupe, val, explanation) - """ - nbcolumns = int(columns) - - wb = Workbook() - - SheetName0 = "Emargement" - ws0 = wb.add_sheet(SheetName0.decode(scu.SCO_ENCODING)) - # ajuste largeurs colonnes (unite inconnue, empirique) - width = 4500 - if nbcolumns > 5: - width = 22500 // nbcolumns - - for col in range(nbcolumns): - ws0.col(col + 1).width = width - ws0.col(0).width = 750 - - SheetName1 = "Positions" - ws1 = wb.add_sheet(SheetName1.decode(scu.SCO_ENCODING)) - if numbering == "coordinate": - ws1.col(0).width = 4000 - ws1.col(1).width = 4500 - ws1.col(2).width = 1500 - ws1.col(3).width = 1500 - - ws1.col(4).width = 500 - - ws1.col(5).width = 4000 - ws1.col(6).width = 4500 - ws1.col(7).width = 1500 - ws1.col(8).width = 1500 - else: - ws1.col(0).width = 4000 - ws1.col(1).width = 4500 - ws1.col(2).width = 3000 - - ws1.col(3).width = 500 - - ws1.col(4).width = 4000 - ws1.col(5).width = 4500 - ws1.col(6).width = 3000 - - # styles - font0 = Font() - font0.name = "Arial" - font0.bold = True - font0.height = 12 * 0x14 - - font1b = Font() - font1b.name = "Arial" - font1b.bold = True - font1b.height = 9 * 0x14 - - font1i = Font() - font1i.name = "Arial" - font1i.height = 10 * 0x14 - font1i.italic = True - - font1o = Font() - font1o.name = "Arial" - font1o.height = 10 * 0x14 - font1o.outline = True - - font2bi = Font() - font2bi.name = "Arial" - font2bi.height = 8 * 0x14 - font2bi.bold = True - font2bi.italic = True - - font2 = Font() - font2.name = "Arial" - font2.height = 10 * 0x14 - - style_titres = XFStyle() - style_titres.font = font0 - - style1t = XFStyle() - style1t.font = font1b - alignment = Alignment() - alignment.horz = Alignment.HORZ_CENTER - alignment.vert = Alignment.VERT_CENTER - style1t.alignment = alignment - borders = Borders() - borders.left = Borders.DOUBLE - borders.top = Borders.DOUBLE - borders.bottom = Borders.NO_LINE - borders.right = Borders.DOUBLE - style1t.borders = borders - - style1m = XFStyle() - style1m.font = font1b - alignment = Alignment() - alignment.horz = Alignment.HORZ_CENTER - alignment.vert = Alignment.VERT_CENTER - style1m.alignment = alignment - borders = Borders() - borders.left = Borders.DOUBLE - borders.top = Borders.NO_LINE - borders.bottom = Borders.THIN - borders.right = Borders.DOUBLE - style1m.borders = borders - - style1bm = XFStyle() - borders = Borders() - borders.left = Borders.DOUBLE - borders.top = Borders.NO_LINE - borders.bottom = Borders.NO_LINE - borders.right = Borders.DOUBLE - style1bm.borders = borders - - style1bb = XFStyle() - style1bb.font = font1o - alignment = Alignment() - alignment.horz = Alignment.HORZ_RIGHT - alignment.vert = Alignment.VERT_BOTTOM - style1bb.alignment = alignment - borders = Borders() - borders.left = Borders.DOUBLE - borders.top = Borders.NO_LINE - borders.bottom = Borders.DOUBLE - borders.right = Borders.DOUBLE - style1bb.borders = borders - - style2b = XFStyle() - style2b.font = font1i - alignment = Alignment() - alignment.horz = Alignment.HORZ_CENTER - alignment.vert = Alignment.VERT_CENTER - style2b.alignment = alignment - borders = Borders() - borders.left = Borders.THIN - borders.top = Borders.THIN - borders.bottom = Borders.THIN - borders.right = Borders.THIN - style2b.borders = borders - - style2bi = XFStyle() - style2bi.font = font2bi - alignment = Alignment() - alignment.horz = Alignment.HORZ_CENTER - alignment.vert = Alignment.VERT_CENTER - style2bi.alignment = alignment - borders = Borders() - borders.left = Borders.THIN - borders.top = Borders.THIN - borders.bottom = Borders.THIN - borders.right = Borders.THIN - style2bi.borders = borders - pattern = Pattern() - pattern.pattern = Pattern.SOLID_PATTERN - pattern._pattern_back_colour = "gray" - style2bi.pattern = pattern - - style2l = XFStyle() - style2l.font = font2 - alignment = Alignment() - alignment.horz = Alignment.HORZ_LEFT - alignment.vert = Alignment.VERT_CENTER - style2l.alignment = alignment - borders = Borders() - borders.left = Borders.THIN - borders.top = Borders.THIN - borders.bottom = Borders.THIN - borders.right = Borders.NO_LINE - style2l.borders = borders - - style2m1 = XFStyle() - style2m1.font = font2 - alignment = Alignment() - alignment.horz = Alignment.HORZ_LEFT - alignment.vert = Alignment.VERT_CENTER - style2m1.alignment = alignment - borders = Borders() - borders.left = Borders.NO_LINE - borders.top = Borders.THIN - borders.bottom = Borders.THIN - borders.right = Borders.NO_LINE - style2m1.borders = borders - - style2m2 = XFStyle() - style2l.font = font2 - alignment = Alignment() - alignment.horz = Alignment.HORZ_RIGHT - alignment.vert = Alignment.VERT_CENTER - style2m2.alignment = alignment - borders = Borders() - borders.left = Borders.NO_LINE - borders.top = Borders.THIN - borders.bottom = Borders.THIN - borders.right = Borders.NO_LINE - style2m2.borders = borders - - style2r = XFStyle() - style2l.font = font2 - alignment = Alignment() - alignment.horz = Alignment.HORZ_RIGHT - alignment.vert = Alignment.VERT_CENTER - style2r.alignment = alignment - borders = Borders() - borders.left = Borders.NO_LINE - borders.top = Borders.THIN - borders.bottom = Borders.THIN - borders.right = Borders.THIN - style2r.borders = borders - - # ligne de titres - li = 0 - line = 0 - dt = time.strftime("%d/%m/%Y a %Hh%M") - ws0.write(li, 0, "Feuille placement etudiants éditée le %s" % dt, style_titres) - ws1.write(li, 0, "Feuille placement etudiants éditée le %s" % dt, style_titres) - for desceval in description: - if line % 2 == 0: - li += 2 + def _one_header(self, worksheet): + cells = [ + worksheet.make_cell("Nom", self.styles["2bi"]), + worksheet.make_cell("Prénom", self.styles["2bi"]), + ] + if self.etiquetage == COORD: + cells.append(worksheet.make_cell("Colonne", self.styles["2bi"])) + cells.append(worksheet.make_cell("Ligne", self.styles["2bi"])) else: - li += 1 - line += 1 - ws0.write(li, 0, desceval[0].decode(scu.SCO_ENCODING), style_titres) - ws1.write(li, 0, desceval[0].decode(scu.SCO_ENCODING), style_titres) - li += 1 - ws0.write( - li, - 0, - "Date : %s - Horaire : %s à %s" % (E["jour"], E["heure_debut"], E["heure_fin"]), - style_titres, - ) - ws1.write( - li, - 0, - "Date : %s - Horaire : %s à %s" % (E["jour"], E["heure_debut"], E["heure_fin"]), - style_titres, - ) - li += 1 + cells.append(worksheet.make_cell("Place", self.styles["2bi"])) + return cells - # entetes colonnes - feuille0 - for col in range(nbcolumns): - ws0.write(li, col + 1, "colonne %s" % (col + 1), style2b) - # entetes colonnes - feuille1 - if numbering == "coordinate": - ws1.write(li, 0, "Nom", style2bi) - ws1.write(li, 1, "Prénom", style2bi) - ws1.write(li, 2, "Colonne", style2bi) - ws1.write(li, 3, "Ligne", style2bi) + def _headers(self, worksheet, nb_listes): + cells = [] + for _ in range(nb_listes): + cells += self._one_header(worksheet) + cells.append(worksheet.make_cell("")) + worksheet.append_row(cells) - ws1.write(li, 5, "Nom", style2bi) - ws1.write(li, 6, "Prénom", style2bi) - ws1.write(li, 7, "Colonne", style2bi) - ws1.write(li, 8, "Ligne", style2bi) - else: - ws1.write(li, 0, "Nom", style2bi) - ws1.write(li, 1, "Prénom", style2bi) - ws1.write(li, 2, "Place", style2bi) + def _make_styles(self, ws0, ws1): + # polices + font0 = Font(name="Calibri", bold=True, size=12) + font1b = copy(font0) + font1b.size = 9 + font1i = Font(name="Arial", italic=True, size=10) + font1o = Font(name="Arial", outline=True, size=10) + font2bi = Font(name="Arial", bold=True, italic=True, size=8) + font2 = Font(name="Arial", size=10) - ws1.write(li, 4, "Nom", style2bi) - ws1.write(li, 5, "Prénom", style2bi) - ws1.write(li, 6, "Place", style2bi) + # bordures + side_double = Side(border_style="double", color=COLORS.BLACK.value) + side_thin = Side(border_style="thin", color=COLORS.BLACK.value) - # etudiants - line = 1 - col = 1 - linetud = [] - orderetud = [] - placementetud = [] - for etudid in listetud: - linetud.append(etudid) - if numbering == "coordinate": - orderetud.append((etudid[0], etudid[1], col, line)) - else: - orderetud.append((etudid[0], etudid[1], col + (line - 1) * nbcolumns)) + # bordures + border1t = Border(left=side_double, top=side_double, right=side_double) + border1bb = Border(left=side_double, bottom=side_double, right=side_double) + border1bm = Border(left=side_double, right=side_double) + border1m = Border(left=side_double, bottom=side_thin, right=side_double) + border2m = Border(top=side_thin, bottom=side_thin) + border2r = Border(top=side_thin, bottom=side_thin, right=side_thin) + border2l = Border(left=side_thin, top=side_thin, bottom=side_thin) + border2b = Border( + left=side_thin, top=side_thin, bottom=side_thin, right=side_thin + ) - if col == nbcolumns: - placementetud.append(linetud) - linetud = [] - col = 0 - line += 1 - col += 1 - if len(linetud) > 0: - placementetud.append(linetud) + # alignements + align_center_center = Alignment(horizontal="center", vertical="center") + align_right_bottom = Alignment(horizontal="right", vertical="bottom") + align_left_center = Alignment(horizontal="left", vertical="center") + align_right_center = Alignment(horizontal="right", vertical="center") - # etudiants - feuille0 - line = 0 - li0 = li - for linetud in placementetud: - li0 += 1 - line += 1 - ws0.write(li0, 0, line, style2b) - col = 1 - for etudid in linetud: - ws0.write(li0, col, (etudid[0]).decode(scu.SCO_ENCODING), style1t) - ws0.write(li0 + 1, col, (etudid[1]).decode(scu.SCO_ENCODING), style1m) - ws0.row(li0 + 2).height = space - if numbering == "coordinate": - ws0.write(li0 + 2, col, " ", style1bb) + # patterns + pattern = PatternFill( + fill_type="solid", fgColor=sco_excel.COLORS.LIGHT_YELLOW.value + ) + + # styles + self.styles = { + "titres": sco_excel.excel_make_style(font_name="Arial", bold=True, size=12), + "1t": ws0.excel_make_composite_style( + font=font0, alignment=align_center_center, border=border1t + ), + "1m": ws0.excel_make_composite_style( + font=font1b, alignment=align_center_center, border=border1m + ), + "1bm": ws0.excel_make_composite_style( + font=font1b, alignment=align_center_center, border=border1bm + ), + "1bb": ws0.excel_make_composite_style( + font=font1o, alignment=align_right_bottom, border=border1bb + ), + "2b": ws1.excel_make_composite_style( + font=font1i, alignment=align_center_center, border=border2b + ), + "2bi": ws1.excel_make_composite_style( + font=font2bi, + alignment=align_center_center, + border=border2b, + fill=pattern, + ), + "2l": ws1.excel_make_composite_style( + font=font2, alignment=align_left_center, border=border2l + ), + "2m1": ws1.excel_make_composite_style( + font=font2, alignment=align_left_center, border=border2m + ), + "2m2": ws1.excel_make_composite_style( + font=font2, alignment=align_right_center, border=border2m + ), + "2r": ws1.excel_make_composite_style( + font=font2, alignment=align_right_center, border=border2r + ), + } + + def _titres(self, worksheet): + datetime = time.strftime("%d/%m/%Y a %Hh%M") + worksheet.append_single_cell_row( + "Feuille placement etudiants éditée le %s" % datetime, self.styles["titres"] + ) + for line, desceval in enumerate(self.desceval): + if line in [1, 4, 7]: + worksheet.append_blank_row() + worksheet.append_single_cell_row(desceval, self.styles["titres"]) + worksheet.append_single_cell_row( + "Date : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" + % self.eval_data, + self.styles["titres"], + ) + + def _feuille0(self, ws0, space): + self._titres(ws0) + # entetes colonnes - feuille0 + cells = [ws0.make_cell()] + for col in range(self.nb_rangs): + cells.append(ws0.make_cell("colonne %s" % (col + 1), self.styles["2b"])) + ws0.append_row(cells) + + # etudiants - feuille0 + place = 1 + col = 0 + rang = 1 + # Chaque rang est affiché sur 3 lignes xlsx (notées A, B, C) + # ligne A: le nom, ligne B: le prénom, ligne C: un espace ou la place + cells_a = [ws0.make_cell(rang, self.styles["2b"])] + cells_b = [ws0.make_cell("", self.styles["2b"])] + cells_c = [ws0.make_cell("", self.styles["2b"])] + row = 13 # première ligne de signature + for linetud in self.plan: + cells_a.append(ws0.make_cell(linetud[0][0], self.styles["1t"])) # nom + cells_b.append(ws0.make_cell(linetud[0][1], self.styles["1m"])) # prenom + if self.etiquetage == COORD: + cell_c = ws0.make_cell("", self.styles["1bb"]) else: - ws0.write( - li0 + 2, col, "place %s" % (col + (line - 1) * nbcolumns), style1bb - ) - # ws0.write(li+3,col, ' ', style1bm ) - # ws0.write(li+4,col, ' ', style1bb ) - - if col == nbcolumns: - col = 0 - li0 += 2 + cell_c = ws0.make_cell("place %s" % place, self.styles["1bb"]) + place = place + 1 + cells_c.append(cell_c) + ws0.set_row_dimension_height(row, space / 25) + row += 3 col += 1 - - # etudiants - feuille1 - if numbering == "coordinate": - coloffset = 5 - else: - coloffset = 4 - line = 0 - li1 = li - nbcol = 0 - col = 0 - orderetud.sort() - for etudid in orderetud: - li1 += 1 - line += 1 - ws1.write(li1, col, (etudid[0]).decode(scu.SCO_ENCODING), style2l) - ws1.write(li1, col + 1, (etudid[1]).decode(scu.SCO_ENCODING), style2m1) - if numbering == "coordinate": - ws1.write(li1, col + 2, etudid[2], style2m2) - ws1.write(li1, col + 3, etudid[3], style2r) - else: - ws1.write(li1, col + 2, etudid[2], style2r) - - if line == maxlines: - line = 0 - li1 = li - nbcol = nbcol + 1 - col = col + coloffset - if nbcol == 2: - li = li + maxlines + 2 - li1 = li - nbcol = 0 + if col == self.nb_rangs: # On a fini la rangée courante + ws0.append_row(cells_a) # on affiche les 3 lignes construites + ws0.append_row(cells_b) + ws0.append_row(cells_c) + cells_a = [ + ws0.make_cell(rang, self.styles["2b"]) + ] # on réinitialise les 3 lignes + cells_b = [ws0.make_cell("", self.styles["2b"])] + cells_c = [ws0.make_cell("", self.styles["2b"])] col = 0 - if numbering == "coordinate": - ws1.write(li, 0, "Nom", style2bi) - ws1.write(li, 1, "Prénom", style2bi) - ws1.write(li, 2, "Colonne", style2bi) - ws1.write(li, 3, "Ligne", style2bi) + rang += 1 + # publication du rang final incomplet + ws0.append_row(cells_a) # Affiche des 3 lignes (dernières lignes incomplètes) + ws0.append_row(cells_b) + ws0.append_row(cells_c) + ws0.set_row_dimension_height(row, space / 25) - ws1.write(li, 5, "Nom", style2bi) - ws1.write(li, 6, "Prénom", style2bi) - ws1.write(li, 7, "Colonne", style2bi) - ws1.write(li, 8, "Ligne", style2bi) - else: - ws1.write(li, 0, "Nom", style2bi) - ws1.write(li, 1, "Prénom", style2bi) - ws1.write(li, 2, "Place", style2bi) + def _feuille1(self, worksheet, maxlines): + # etudiants - feuille1 + # structuration: + # 1 page = maxlistes listes + # 1 liste = 3 ou 4 colonnes(excel) (selon numbering) et (maximum maxlines) lignes + maxlistes = 2 # nombre de listes par page + # computes excel columns widths + if self.etiquetage == COORD: + gabarit = [16, 18, 6, 6, 2] + else: + gabarit = [16, 18, 12, 2] + widths = [] + for _ in range(maxlistes): + widths += gabarit + worksheet.set_column_dimension_width(value=widths) + nb_etu_restant = len(self.listetud) + self._titres(worksheet) + nb_listes = min( + maxlistes, nb_etu_restant // maxlines + 1 + ) # nombre de colonnes dans la page + self._headers(worksheet, nb_listes) + # construction liste alphabétique + # Affichage + lines = [[] for _ in range(maxlines)] + lineno = 0 + col = 0 + for etud in sorted(self.plan, key=lambda e: e[0][0]): # tri alphabétique + # check for skip of list or page + if col > 0: # add a empty cell between lists + lines[lineno].append(worksheet.make_cell()) + lines[lineno].append(worksheet.make_cell(etud[0][0], self.styles["2l"])) + lines[lineno].append(worksheet.make_cell(etud[0][1], self.styles["2m1"])) + if self.etiquetage == COORD: + lines[lineno].append( + worksheet.make_cell(etud[1][1], self.styles["2m2"]) + ) + lines[lineno].append(worksheet.make_cell(etud[1][0], self.styles["2r"])) + else: + lines[lineno].append(worksheet.make_cell(etud[1], self.styles["2r"])) + lineno = lineno + 1 + if lineno >= maxlines: # fin de liste + col = col + 1 + lineno = 0 + if col >= maxlistes: # fin de page + for line_cells in lines: + worksheet.append_row(line_cells) + lines = [[] for _ in range(maxlines)] + col = 0 + worksheet.append_blank_row() + nb_etu_restant -= maxlistes * maxlines + nb_listes = min( + maxlistes, nb_etu_restant // maxlines + 1 + ) # nombre de colonnes dans la page + self._headers(worksheet, nb_listes) + for line_cells in lines: + worksheet.append_row(line_cells) - ws1.write(li, 4, "Nom", style2bi) - ws1.write(li, 5, "Prénom", style2bi) - ws1.write(li, 6, "Place", style2bi) - return wb.savetostr() + def _excel_feuille_placement(self): + """Genere feuille excel pour placement des etudiants. + E: evaluation (dict) + lines: liste de tuples + (etudid, nom, prenom, etat, groupe, val, explanation) + """ + sem_preferences = sco_preferences.SemPreferences() + space = sem_preferences.get("feuille_placement_emargement") + maxlines = sem_preferences.get("feuille_placement_positions") + nb_rangs = int(self.nb_rangs) + column_width_ratio = ( + 1 / 250 + ) # changement d unités entre pyExcelerator et openpyxl + + workbook = ScoExcelBook() + + sheet_name_0 = "Emargement" + ws0 = workbook.create_sheet(sheet_name_0) + # ajuste largeurs colonnes (unite inconnue, empirique) + width = 4500 * column_width_ratio + if nb_rangs > 5: + width = 22500 * column_width_ratio // nb_rangs + + ws0.set_column_dimension_width("A", 750 * column_width_ratio) + for col in range(nb_rangs): + ws0.set_column_dimension_width( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[col + 1 : col + 2], width + ) + + sheet_name_1 = "Positions" + ws1 = workbook.create_sheet(sheet_name_1) + + self._make_styles(ws0, ws1) + self._feuille0(ws0, space) + self._feuille1(ws1, maxlines) + return workbook.generate() diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py index b54880ad..183b4551 100644 --- a/app/scodoc/sco_prepajury.py +++ b/app/scodoc/sco_prepajury.py @@ -325,5 +325,12 @@ def feuille_preparation_jury(formsemestre_id, REQUEST): current_user, ) ) - xls = ws.generate_standalone() - return sco_excel.send_excel_file(REQUEST, xls, f"PrepaJury{sn}{scu.XLSX_SUFFIX}") + xls = ws.generate() + return scu.send_file( + xls, + f"PrepaJury{sn}{scu.XLSX_SUFFIX}", + scu.XLSX_SUFFIX, + scu.XLSX_MIMETYPE, + attached=True, + ) + # return sco_excel.send_excel_file(REQUEST, xls, f"PrepaJury{sn}{scu.XLSX_SUFFIX}") diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index dade9d89..8e6f1bb6 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -829,7 +829,10 @@ def feuille_saisie_notes(evaluation_id, group_ids=[], REQUEST=None): filename = "notes_%s_%s.xlsx" % (evalname, gr_title_filename) xls = sco_excel.excel_feuille_saisie(E, sem["titreannee"], description, lines=L) - return sco_excel.send_excel_file(REQUEST, xls, filename) + return scu.send_file( + xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) + # return sco_excel.send_excel_file(REQUEST, xls, filename) def has_existing_decision(M, E, etudid): diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index e537fb53..bd3f19aa 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -486,7 +486,10 @@ def photos_generate_excel_sample(group_ids=[], REQUEST=None): ], extra_cols=["fichier_photo"], ) - return sco_excel.send_excel_file(REQUEST, data, "ImportPhotos" + scu.XLSX_SUFFIX) + return scu.send_file( + data, "ImportPhotos", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) + # return sco_excel.send_excel_file(REQUEST, data, "ImportPhotos" + scu.XLSX_SUFFIX) def photos_import_files_form(group_ids=[], REQUEST=None): diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index ef9df0da..a537bb96 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1092,13 +1092,11 @@ h2.formsemestre, .gtrcontent h2 { #formnotes td.tf-fieldlabel { border-bottom: 1px dotted #fdcaca; } - -/* Formulaires ScoDoc 9 */ -form.sco-form { - margin-top: 1em; +.wtf-field li { + display: inline; } -div.sco-submit { - margin-top: 2em; +.wtf-field .errors { + color: red ; font-weight: bold; } /* .formsemestre_menubar { diff --git a/app/templates/scodoc/forms/placement.html b/app/templates/scodoc/forms/placement.html new file mode 100644 index 00000000..f2c11b15 --- /dev/null +++ b/app/templates/scodoc/forms/placement.html @@ -0,0 +1,77 @@ +{% import 'bootstrap/wtf.html' as wtf %} + +{% macro render_field(field) %} + + {{ field.label }} + {{ field()|safe }} + {% if field.errors %} + + {% endif %} + + +{% endmacro %} + +
+
+ {{ form.evaluation_id }} + {{ form.csrf_token }} + + + {{ render_field(form.surveillants) }} + {{ render_field(form.batiment) }} + {{ render_field(form.salle) }} + {{ render_field(form.nb_rangs) }} + {{ render_field(form.etiquetage) }} + {% if form.has_groups %} + {{ render_field(form.groups) }} + + {% endif %} + {{ render_field(form.file_format) }} + +
+

+ + + +

+

Explications

+ +
+ + diff --git a/app/views/notes.py b/app/views/notes.py index 5636d6b8..a1120714 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1645,8 +1645,8 @@ sco_publish( "/placement_eval_selectetuds", sco_placement.placement_eval_selectetuds, Permission.ScoEnsView, + methods=["GET", "POST"], ) -sco_publish("/do_placement", sco_placement.do_placement, Permission.ScoEnsView) # --- Saisie des notes sco_publish( diff --git a/app/views/scolar.py b/app/views/scolar.py index 37ac5bd2..b3fd1fe1 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -1937,7 +1937,10 @@ def import_generate_excel_sample(REQUEST, with_codesemestre="1"): data = sco_import_etuds.sco_import_generate_excel_sample( format, with_codesemestre, exclude_cols=["photo_filename"] ) - return sco_excel.send_excel_file(REQUEST, data, "ImportEtudiants" + scu.XLSX_SUFFIX) + return scu.send_file( + data, "ImportEtudiants", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) + # return sco_excel.send_excel_file(REQUEST, data, "ImportEtudiants" + scu.XLSX_SUFFIX) # --- Données admission @@ -1955,9 +1958,10 @@ def import_generate_admission_sample(REQUEST, formsemestre_id): exclude_cols=["nationalite", "foto", "photo_filename"], group_ids=[group["group_id"]], ) - return sco_excel.send_excel_file( - REQUEST, data, "AdmissionEtudiants" + scu.XLSX_SUFFIX + return scu.send_file( + data, "AdmissionEtudiants", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True ) + # return sco_excel.send_excel_file(REQUEST, data, "AdmissionEtudiants" + scu.XLSX_SUFFIX) # --- Données admission depuis fichier excel (version nov 2016) diff --git a/app/views/users.py b/app/views/users.py index 063ad39c..95366c4e 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -65,8 +65,6 @@ from app import log from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_permissions_check import can_handle_passwd from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message -from app.scodoc.sco_excel import send_excel_file -from app.scodoc.sco_import_users import generate_excel_sample from app.views import users_bp as bp @@ -490,9 +488,10 @@ def create_user_form(REQUEST, user_name=None, edit=0, all_roles=1): def import_users_generate_excel_sample(REQUEST): "une feuille excel pour importation utilisateurs" data = sco_import_users.generate_excel_sample() - return sco_excel.send_excel_file( - REQUEST, data, "ImportUtilisateurs" + scu.XLSX_SUFFIX + return scu.send_file( + data, "ImportUtilisateurs", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True ) + # return sco_excel.send_excel_file(REQUEST, data, "ImportUtilisateurs" + scu.XLSX_SUFFIX) @bp.route("/import_users_form", methods=["GET", "POST"])