# -*- 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 # ############################################################################## """ScoDoc: génération feuille émargement et placement Contribution M. Salomon, UFC / IUT DE BELFORT-MONTBÉLIARD, 2016 """ import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error import random import time from copy import copy import flask import wtforms.validators from flask import request, render_template from flask_login import current_user from werkzeug import Response from flask_wtf import FlaskForm from wtforms import ( StringField, PasswordField, BooleanField, SubmitField, SelectField, RadioField, HiddenField, SelectMultipleField, validators, ) from wtforms.validators import ValidationError, DataRequired, Email, EqualTo from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc import html_sco_header from app.scodoc import sco_edit_module from app.scodoc import sco_evaluations from app.scodoc import sco_excel 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 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 = _ class PlacementForm(FlaskForm): TOUS = "Tous" 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=[wtforms.validators.DataRequired("Test")] ) 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=["Continue", "Coordonnées"], 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 _set_evaluation_infos(self, evaluation_id): eval_data = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) if not eval_data: raise ScoValueError("invalid evaluation_id") eval_data = eval_data[0] # groupes groups = sco_groups.do_evaluation_listegroupes( evaluation_id, include_default=True ) self.groups_tree = {} self.has_groups = False for group in groups: partition = group["partition_name"] or self.TOUS # TODO check required group_id = group["group_id"] group_name = group["group_name"] or self.TOUS if partition not in self.groups_tree: self.groups_tree[partition] = {} self.groups_tree[partition][group_name] = group_id if partition != self.TOUS: self.has_groups = True self.groups_tree_length = len(self.groups_tree) if self.has_groups: choices = [] for partition in self.groups_tree: for groupe in self.groups_tree[partition]: id = str(self.groups_tree[partition][groupe]) choices.append((id, "%s (%s)" % (str(groupe), partition))) self.groups.choices = choices def placement_eval_selectetuds(evaluation_id, REQUEST=None): form = PlacementForm( request.form, data={"evaluation_id": int(evaluation_id), "groups": PlacementForm.TOUS}, ) form._set_evaluation_infos(evaluation_id) if form.validate_on_submit(): exec_placement(form) return flask.redirect(titi()) H = [html_sco_header.sco_header(init_jquery_ui=True)] H.append(sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)) H.append("

Placement et émargement des étudiants

") H.append(render_template("forms/placement.html", form=form)) H.append( """

Explications

""" ) F = html_sco_header.sco_footer() return "\n".join(H) + "

" + F def do_placement_selectetuds(): """ Choisi les étudiants et les infos sur la salle pour leur placement. """ # M = sco_moduleimpl.do_moduleimpl_list( moduleimpl_id=E["moduleimpl_id"])[0] # description de l'evaluation H = [ sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), "

Placement et émargement des étudiants

", ] # 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.URL0, 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=" + six.moves.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"] ) def do_placement(REQUEST): """ Choisi le placement """ authuser = REQUEST.AUTHENTICATED_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] # 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"]} )[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 ) return sco_excel.send_excel_file(REQUEST, xls, filename) else: nbcolumns = int(columns) 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"], ) filename = "placement_%s_%s.pdf" % (evalname, gr_title_filename) titles = { "nom": "Nom", "prenom": "Prenom", "colonne": "Colonne", "ligne": "Ligne", "place": "Place", } if numbering == "coordinate": 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": rows.append( { "nom": etudid[0], "prenom": etudid[1], "colonne": etudid[2], "ligne": etudid[3], } ) else: rows.append({"nom": etudid[0], "prenom": etudid[1], "place": etudid[2]}) tab = GenTable( titles=titles, columns_ids=columns_ids, rows=rows, filename=filename, origin="Généré par %s le " % sco_version.SCONAME + scu.timedate_human_repr() + "", pdf_title=pdf_title, # pdf_shorttitle = '', preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), # html_generate_cells=False # la derniere ligne (moyennes) est incomplete ) t = tab.make_page(format="pdf", with_html_headers=False, REQUEST=REQUEST) return t def placement_eval_selectetuds_old(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() if r: if isinstance(r, str): H.append(r) elif isinstance(r, Response): H.append(r.get_data().decode("utf-8")) H.append( """

Explications

""" ) H.append(html_sco_header.sco_footer()) return "\n".join(H) def _one_header(ws, numbering, styles): cells = [] if numbering == "coordinate": cells.append(ws.make_cell("Nom", styles["2bi"])) cells.append(ws.make_cell("Prénom", styles["2bi"])) cells.append(ws.make_cell("Colonne", styles["2bi"])) cells.append(ws.make_cell("Ligne", styles["2bi"])) else: cells.append(ws.make_cell("Nom", styles["2bi"])) cells.append(ws.make_cell("Prénom", styles["2bi"])) cells.append(ws.make_cell("Place", styles["2bi"])) return cells def _headers(ws, numbering, styles, nb_listes): cells = [] for _ in range(nb_listes): cells += _one_header(ws, numbering, styles) cells.append(ws.make_cell("")) ws.append_row(cells) def _make_styles(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) # bordures side_double = Side(border_style="double", color=COLORS.BLACK.value) side_thin = Side(border_style="thin", color=COLORS.BLACK.value) # 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) # 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") # patterns pattern = PatternFill( fill_type="solid", fgColor=sco_excel.COLORS.LIGHT_YELLOW.value ) # styles 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 ), } return styles def _init_lines(maxlines): return [ [] for _ in range(maxlines) ] # lines[no_ligne] -> liste des cellules de la ligne (no_lignes de 1..maxlines def _write_lines(ws, lines): for line in lines: ws.append_row(line) def _titres(ws, description, evaluation, building, room, styles): dt = time.strftime("%d/%m/%Y a %Hh%M") ws.append_single_cell_row( "Feuille placement etudiants éditée le %s" % dt, styles["titres"] ) for line, desceval in enumerate(description): if line in [1, 4, 7]: ws.append_blank_row() ws.append_single_cell_row(desceval[0], styles["titres"]) ws.append_single_cell_row( "Date : %s - Horaire : %s à %s" % (evaluation["jour"], evaluation["heure_debut"], evaluation["heure_fin"]), styles["titres"], ) ws.append_single_cell_row( "Date : %s - Horaire : %s à %s" % (evaluation["jour"], evaluation["heure_debut"], evaluation["heure_fin"]), styles["titres"], ) def _feuille0( ws0, description, evaluation, styles, numbering, listetud, nbcolumns, building, room, space, ): _titres(ws0, description, evaluation, building, room, styles) # entetes colonnes - feuille0 cells = [ws0.make_cell()] for col in range(nbcolumns): cells.append(ws0.make_cell("colonne %s" % (col + 1), styles["2b"])) ws0.append_row(cells) # 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)) if col == nbcolumns: placementetud.append(linetud) linetud = [] col = 0 line += 1 col += 1 if len(linetud) > 0: placementetud.append(linetud) # etudiants - feuille0 place = 1 for rang, linetud in enumerate(placementetud, start=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, styles["2b"])] cells_b = [ws0.make_cell("", styles["2b"])] cells_c = [ws0.make_cell("", styles["2b"])] row = 14 # premieère ligne de signature for etudid in linetud: cells_a.append(ws0.make_cell(etudid[0], styles["1t"])) cells_b.append(ws0.make_cell(etudid[1], styles["1m"])) if numbering == "coordinate": cell_c = ws0.make_cell("", styles["1bb"]) else: cell_c = ws0.make_cell("place %s" % place, styles["1bb"]) cells_c.append(cell_c) ws0.set_row_dimension_height(row, space / 25) row += 3 place = place + 1 if col == nbcolumns: ws0.append_row(cells_a) ws0.append_row(cells_b) ws0.append_row(cells_c) cells_a = [ws0.make_cell(rang, styles["2b"])] cells_b = [ws0.make_cell("", styles["2b"])] cells_c = [ws0.make_cell("", styles["2b"])] # publication du rang final incomplet ws0.append_row(cells_a) ws0.append_row(cells_b) ws0.append_row(cells_c) ws0.set_row_dimension_height(row, space / 25) def _compute_ordretud(listetud, nbcolumns, numbering): orderetud = [] line = 1 col = 1 for etudid in listetud: if numbering == "coordinate": orderetud.append((etudid[0], etudid[1], col, line)) else: orderetud.append( (etudid[0], etudid[1], "%s" % (col + (line - 1) * nbcolumns)) ) col += 1 if col > nbcolumns: col = 1 line += 1 orderetud.sort() return orderetud def _next_page(ws): pass def _feuille1( ws, description, evaluation, styles, numbering, maxlines, nbcolumns, building, room, listetud, ): # 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 numbering == "coordinate": gabarit = [16, 18, 6, 6, 2] else: gabarit = [16, 18, 12, 2] widths = [] for _ in range(maxlistes): widths += gabarit ws.set_column_dimension_width(value=widths) nb_etu_restant = len(listetud) _titres(ws, description, evaluation, building, room, styles) nb_listes = min( maxlistes, nb_etu_restant // maxlines + 1 ) # nombre de colonnes dans la page _headers(ws, numbering, styles, nb_listes) # construction liste alphabétique # Affichage lines = _init_lines(maxlines) orderetud = _compute_ordretud(listetud, nbcolumns, numbering) line = 0 col = 0 for etudid in orderetud: # check for skip of list or page if col > 0: # add a empty cell between lists lines[line].append(ws.make_cell()) lines[line].append(ws.make_cell(etudid[0], styles["2l"])) lines[line].append(ws.make_cell(etudid[1], styles["2m1"])) if numbering == "coordinate": lines[line].append(ws.make_cell(etudid[2], styles["2m2"])) lines[line].append(ws.make_cell(etudid[3], styles["2r"])) else: lines[line].append(ws.make_cell(etudid[2], styles["2r"])) line = line + 1 if line >= maxlines: # fin de liste col = col + 1 line = 0 if col >= maxlistes: # fin de page _write_lines(ws, lines) lines = _init_lines(maxlines) col = 0 ws.append_blank_row() nb_etu_restant -= maxlistes * maxlines nb_listes = min( maxlistes, nb_etu_restant // maxlines + 1 ) # nombre de colonnes dans la page _headers(ws, numbering, styles, nb_listes) _write_lines(ws, lines) def _excel_feuille_placement( evaluation, 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) column_width_ratio = 1 / 250 # changement d unités entre pyExcelerator et openpyxl wb = ScoExcelBook() SheetName0 = "Emargement" ws0 = wb.create_sheet(SheetName0) # ajuste largeurs colonnes (unite inconnue, empirique) width = 4500 * column_width_ratio if nbcolumns > 5: width = 22500 * column_width_ratio // nbcolumns ws0.set_column_dimension_width("A", 750 * column_width_ratio) for col in range(nbcolumns): ws0.set_column_dimension_width( "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[col + 1 : col + 2], width ) SheetName1 = "Positions" ws1 = wb.create_sheet(SheetName1) styles = _make_styles(ws0, ws1) _feuille0( ws0, description, evaluation, styles, numbering, listetud, nbcolumns, building, room, space, ) _feuille1( ws1, description, evaluation, styles, numbering, maxlines, nbcolumns, building, room, listetud, ) return wb.generate()