# -*- 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_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 = [
context.sco_header(
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.append(context.sco_footer(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 = [
"""")
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 = context.Notes.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(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 = ["nom_disp", "prenom"] # colonnes a inclure
titles = {
"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
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=",", # pour csvmoodle
preferences=context.get_preferences(groups_infos.formsemestre_id),
)
#
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 = [
'
")
if groups_infos.members:
H.extend(
[
tab.html(),
"
"
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",
"sexe",
"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"], decisions_jury = 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) + "
')
# Lien pour verif codes INE/NIP
# (pour tous les etudiants du semestre)
group_id = sco_groups.get_default_group(context, groups_infos.formsemestre_id)
if authuser.has_permission(ScoEtudInscrit, context):
H.append(
'
")
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(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(
'")
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(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 saisi hebdo des absences
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, 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"]
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
tab = GenTable(
rows=T,
columns_ids=("email", "semestre_groupe"),
filename=moodle_sem_name + "-moodle",
text_fields_separator=",",
preferences=context.get_preferences(formsemestre_id),
)
return tab.make_page(context, format="csv", REQUEST=REQUEST)