# -*- 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 datetime import cgi import urllib import time import collections import operator import sco_utils as scu from sco_permissions import ScoEtudInscrit, ScoEtudAddAnnotations, ScoAbsChange from sco_exceptions import ScoValueError import html_sco_header from gen_tables import GenTable import scolars import sco_abs import sco_excel import sco_formsemestre import sco_moduleimpl import sco_groups import sco_trombino import sco_portal_apogee import sco_parcours_dut import sco_report 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( context, 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( context, group_ids, formsemestre_id=formsemestre_id, etat=etat, REQUEST=REQUEST, select_all_when_unspecified=True, ) # Formats spéciaux: download direct if format != "html": return groups_table( context=context, 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(context, REQUEST, javascripts=JAVASCRIPTS, cssstyles=CSSSTYLES, init_qtip=True ) ] # Menu choix groupe H.append("""
""") H.append(form_groups_choice(context, 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( context=context, 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(context, groups_infos, etat=etat, REQUEST=REQUEST), #'

hello

', "
", '
', tab_absences_html(context, groups_infos, etat=etat, REQUEST=REQUEST), "
", ) ) H.append(html_sco_header.sco_footer(context, REQUEST)) return "\n".join(H) def form_groups_choice( context, 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( context, groups_infos.formsemestre_id ) H = [ """
Groupes: """ % (groups_infos.formsemestre_id, default_group_id) ] H.append( menu_groups_choice(context, groups_infos, submit_on_change=submit_on_change) ) if with_selectall_butt: H.append( """""" ) H.append("
") return "\n".join(H) def menu_groups_choice(context, 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( context, groups_infos.formsemestre_id ) if submit_on_change: klass = "submit_on_change" else: klass = "" H = [ """ ") return "\n".join(H) def menu_group_choice(context, 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(context, group_id) formsemestre_id = group["formsemestre_id"] elif not formsemestre_id: raise ValueError("missing formsemestre_id") H = [ """ """ ) return "\n".join(H) class DisplayedGroupsInfos: """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, context, group_ids=[], # groupes specifies dans l'URL formsemestre_id=None, etat=None, select_all_when_unspecified=False, moduleimpl_id=None, # used to find formsemestre when unspecified REQUEST=None, ): # log('DisplayedGroupsInfos %s' % group_ids) if type(group_ids) == str: 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( context, 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(context, formsemestre_id)] else: # selectionne le premier groupe trouvé, s'il y en a un partition = sco_groups.get_partitions_list( context, formsemestre_id, with_default=True )[0] groups = sco_groups.get_partition_groups(context, partition) if groups: group_ids = [groups[0]["group_id"]] else: group_ids = [sco_groups.get_default_group(context, formsemestre_id)] gq = [] for group_id in group_ids: gq.append("group_ids=" + group_id) self.groups_query_args = "&".join(gq) self.base_url = REQUEST.URL0 + "?" + 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( context, 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( context, 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 = self.groups_filename.translate(None, ":/\\") # 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(context, 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( context=None, 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 """ # 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] ) ) 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(context, groups_infos.members) if with_archives: import sco_archives_etud sco_archives_etud.add_archives_info_to_etud_list(context, groups_infos.members) columns_ids += ["etudarchive"] if with_annotations: scolars.add_annotations_to_etud_list(context, groups_infos.members) columns_ids += ["annotations_str"] if groups_infos.formsemestre["semestre_id"] >= 0: moodle_sem_name = "S%d" % groups_infos.formsemestre["semestre_id"] else: moodle_sem_name = "A" # pas de semestre spécifié, que faire ? 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"] = "" etud["_nom_disp_target"] = "ficheEtud?etudid=" + etud["etudid"] etud["_prenom_target"] = "ficheEtud?etudid=" + etud["etudid"] 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 S1-NomgroupeXXX 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( etud["partitions"][partition_id]["group_name"] ) else: # pas de groupes sélectionnés: prend le premier s'il y en a un if etud["partitions"]: for p in etud["partitions"].items(): # partitions is an OrderedDict break moodle_groupename = [p[1]["group_name"]] else: moodle_groupename = ["tous"] 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 = context.get_preferences(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(context, format=format, REQUEST=REQUEST) elif format == "xlsappel": xls = sco_excel.Excel_feuille_listeappel( context, groups_infos.formsemestre, # le 1er semestre, serait à modifier si plusieurs groups_infos.groups_titles, groups_infos.members, partitions=groups_infos.partitions, with_codes=with_codes, with_paiement=with_paiement, server_name=REQUEST.BASE0, ) filename = "liste_%s" % groups_infos.groups_filename + ".xls" return sco_excel.sendExcelFile(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( context, 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 = context.getEtudInfo(m["etudid"], filled=True)[0] m.update(etud) scolars.etud_add_lycee_infos(etud) # et ajoute le parcours Se = sco_parcours_dut.SituationEtudParcours( context.Notes, etud, groups_infos.formsemestre_id ) m["parcours"] = Se.get_parcours_descr() m["codeparcours"], _ = sco_report.get_codeparcoursetud(context.Notes, 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_SimpleTable(titles=titles, lines=L, SheetName=title) filename = title + ".xls" return sco_excel.sendExcelFile(REQUEST, xls, filename) else: raise ValueError("unsupported format") def tab_absences_html(context, groups_infos, etat=None, REQUEST=None): """contenu du tab "absences et feuilles diverses" """ authuser = REQUEST.AUTHENTICATED_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(context, groups_infos, etat=None, REQUEST=None): """contenu du tab "photos" """ if not groups_infos.members: return '

Aucun étudiant !

' return sco_trombino.trombino_html(context, groups_infos, REQUEST=REQUEST) def form_choix_jour_saisie_hebdo( context, groups_infos, moduleimpl_id=None, REQUEST=None ): """Formulaire choix jour semaine pour saisie.""" authuser = REQUEST.AUTHENTICATED_USER if not authuser.has_permission(Permission.ScoAbsChange, context): 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(context, groups_infos, REQUEST=None): authuser = REQUEST.AUTHENTICATED_USER if not authuser.has_permission(Permission.ScoAbsChange, context): return "" # construit l'URL "destination" # (a laquelle on revient apres saisie absences) query_args = cgi.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.URL, urllib.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(context, formsemestre_id=None, REQUEST=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( context, formsemestre_id, with_default=True ) sem = sco_formsemestre.get_formsemestre(context, 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(context, partition_id) members = partitions_etud_groups[partition_id] for etudid in members: etud = context.getEtudInfo(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 = context.get_preferences(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(context, format="csv", REQUEST=REQUEST)