From dcb53e9c359e9b71d252dd2e712d1391894b0613 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet La suppression sera définitive. Fichier associé le %s à l'étudiant %s La suppression sera définitive. Attention: la suppression d'une formation est irréversible et implique la supression de toutes les UE, matières et modules de la formation !
La suppression sera définitive. Le programme pédagogique ("formation") va être dupliqué pour que vous puissiez le modifier sans affecter les autres semestres. Les autres paramètres (étudiants, notes...) du semestre seront inchangés. Veillez à ne pas abuser de cette possibilité, car créer trop de versions de formations va vous compliquer la gestion (à vous de garder trace des différences et à ne pas vous tromper par la suite...).
@@ -1280,7 +1281,8 @@ def formsemestre_delete2(
"""Delete a formsemestre (confirmation)"""
# Confirmation dialog
if not dialog_confirmed:
- return context.confirmDialog(
+ return scu.confirm_dialog(
+ context,
""" (opération irréversible) Aucun groupe dans cette partition')
+ if sco_groups.can_change_groups(context, REQUEST, formsemestre_id):
+ H.append(
+ ' (créer)'
+ % partition["partition_id"]
+ )
+ H.append(" Les groupes %s de cette partition seront supprimés Afficher les résultats en pourcentages Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC)
@@ -418,7 +419,8 @@ def evaluation_suppress_alln(context, evaluation_id, REQUEST, dialog_confirmed=F
msg = " Confirmer la suppression des %d notes ? Important: il y a déjà des décisions de jury enregistrées, qui seront potentiellement à revoir suite à cette modification ! Attention: %d photos ne sont pas disponibles et ne peuvent pas être exportées. Vous pouvez exporter seulement les photos existantes"""
% (
nb_missing,
- groups_infos.base_url
- + "&dialog_confirmed=1&format=%s" % format,
+ groups_infos.base_url + "&dialog_confirmed=1&format=%s" % format,
),
dest_url="trombino",
OK="Exporter seulement les photos existantes",
@@ -252,7 +252,8 @@ def trombino_copy_photos(context, group_ids=[], REQUEST=None, dialog_confirmed=F
+ footer
)
if not dialog_confirmed:
- return context.confirmDialog(
+ return scu.confirm_dialog(
+ context,
""" Les photos du groupe %s présentes dans ScoDoc seront remplacées par celles du portail (si elles existent). (les photos sont normalement automatiquement copiées lors de leur première utilisation, l'usage de cette fonction n'est nécessaire que si les photos du portail ont été modifiées) Confirmer ? ' + helpmsg + " ZAbsences ScoDoc 8 g.scodoc_dept=%(scodoc_dept)s
+ %s
+ Les cases cochées correspondent à des absences.
+ Les absences saisies ne sont pas justifiées (sauf si un justificatif a été entré
+ par ailleurs).
+ Si vous "décochez" une case, l'absence correspondante sera supprimée.
+ Attention, les modifications sont automatiquement entregistrées au fur et à mesure.
+ Période du %s au %s (nombre de demi-journées)
+Justifs non utilisés: nombre de demi-journées avec justificatif mais sans absences relevées.
+
+Cliquez sur un nom pour afficher le calendrier des absences Aucune absence ! L'étudiant pense pouvoir justifier cette absence. Supprimer ce billet (utiliser en cas d'erreur, par ex. billet en double) Liste de tous les billets en attente %d étudiants ont validé l'UE %s (%s) Si vous supprimez cette UE, ces validations vont être supprimées !Confirmer la suppression de l'archive du %s ?
Confirmer la suppression des fichiers ?
Confirmer la suppression de la formation %(titre)s (%(acronyme)s) ?
Suppression de l'UE %(titre)s (%(acronyme)s))
" % ue,
dest_url="",
REQUEST=REQUEST,
diff --git a/app/scodoc/sco_etape_apogee_view.py b/app/scodoc/sco_etape_apogee_view.py
index 2376e08c..530ca6b5 100644
--- a/app/scodoc/sco_etape_apogee_view.py
+++ b/app/scodoc/sco_etape_apogee_view.py
@@ -215,9 +215,9 @@ def apo_semset_maq_status(
)
if apo_dups:
- url_list = (
- "view_apo_etuds?semset_id=%s&title=Doublons%%20Apogee&nips=%s"
- % (semset_id, "&nips=".join(apo_dups))
+ url_list = "view_apo_etuds?semset_id=%s&title=Doublons%%20Apogee&nips=%s" % (
+ semset_id,
+ "&nips=".join(apo_dups),
)
H.append(
'Confirmer la suppression du fichier étape %s?
Associer à une nouvelle version de formation non verrouillée ?
Vous voulez vraiment supprimer ce semestre ???
Confirmer le %s du semestre ?
" % msg,
helpmsg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées.
Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment
@@ -1462,7 +1465,8 @@ def formsemestre_change_publication_bul(
msg = "non"
else:
msg = ""
- return context.confirmDialog(
+ return scu.confirm_dialog(
+ context,
"Confirmer la %s publication des bulletins ?
" % msg,
helpmsg="""Il est parfois utile de désactiver la diffusion des bulletins,
par exemple pendant la tenue d'un jury ou avant harmonisation des notes.
diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
index a9c63623..2824a3c4 100644
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -725,12 +725,130 @@ def formsemestre_lists(context, formsemestre_id, REQUEST=None):
sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
H = [
context.html_sem_header(REQUEST, "", sem),
- context.make_listes_sem(sem, REQUEST),
+ _make_listes_sem(context, sem, REQUEST),
context.sco_footer(REQUEST),
]
return "\n".join(H)
+# genere liste html pour accès aux groupes de ce semestre
+# XXX #sco8 vérifier si c'est encore utilisé !
+def _make_listes_sem(context, sem, REQUEST=None, with_absences=True):
+ context = context
+ authuser = REQUEST.AUTHENTICATED_USER
+ r = context.ScoURL() # root url
+ # construit l'URL "destination"
+ # (a laquelle on revient apres saisie absences)
+ query_args = cgi.parse_qs(REQUEST.QUERY_STRING)
+ 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 !
+
+ #
+ H = []
+ # pas de menu absences si pas autorise:
+ if with_absences and not authuser.has_permission(ScoAbsChange, context):
+ with_absences = False
+
+ #
+ H.append(
+ 'Listes de %(titre)s (%(mois_debut)s - %(mois_fin)s)
'
+ % sem
+ )
+
+ formsemestre_id = sem["formsemestre_id"]
+
+ # calcule dates 1er jour semaine pour absences
+ try:
+ if with_absences:
+ first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday()
+ FA = [] # formulaire avec menu saisi absences
+ FA.append(
+ '")
+ FormAbs = "\n".join(FA)
+ else:
+ FormAbs = ""
+ except ScoInvalidDateError: # dates incorrectes dans semestres ?
+ FormAbs = ""
+ #
+ H.append(' Tous les étudiants
" % partition)
+ else:
+ H.append("Groupes de %(partition_name)s
" % partition)
+ groups = sco_groups.get_partition_groups(context, partition)
+ if groups:
+ H.append("")
+ for group in groups:
+ n_members = len(
+ sco_groups.get_group_members(context, group["group_id"])
+ )
+ group["url"] = r
+ if group["group_name"]:
+ group["label"] = "groupe %(group_name)s" % group
+ else:
+ group["label"] = "liste"
+ H.append('
")
+ else:
+ H.append('')
+ H.append(
+ """ ")
+ H.append("
+ %(label)s
+
+ (format tableur)
+ Photos
+ """
+ % group
+ )
+ H.append("(%d étudiants) " % n_members)
+
+ if with_absences:
+ H.append(FormAbs % group)
+
+ H.append("Ajouter une partition
'
+ % formsemestre_id
+ )
+
+ H.append("Supprimer la partition "%s" ?
Il n'y a rien à modifier !
""")
H.append(
- context.confirmDialog(
+ scu.confirm_dialog(
+ context,
dest_url="formsemestre_inscr_passage",
add_headers=False,
cancel_url="formsemestre_inscr_passage?formsemestre_id="
diff --git a/app/scodoc/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py
index a2fdc2fe..86c6779c 100644
--- a/app/scodoc/sco_poursuite_dut.py
+++ b/app/scodoc/sco_poursuite_dut.py
@@ -150,13 +150,27 @@ def _flatten_info(info):
return ids
+def _getEtudInfoGroupes(context, group_ids, etat=None):
+ """liste triée d'infos (dict) sur les etudiants du groupe indiqué.
+ Attention: lent, car plusieurs requetes SQL par etudiant !
+ """
+ etuds = []
+ for group_id in group_ids:
+ members = sco_groups.get_group_members(context, group_id, etat=etat)
+ for m in members:
+ etud = context.getEtudInfo(etudid=m["etudid"], filled=True)[0]
+ etuds.append(etud)
+
+ return etuds
+
+
def formsemestre_poursuite_report(
context, formsemestre_id, format="html", REQUEST=None
):
"""Table avec informations "poursuite" """
sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
- etuds = context.getEtudInfoGroupes(
- [sco_groups.get_default_group(context, formsemestre_id)]
+ etuds = _getEtudInfoGroupes(
+ context, [sco_groups.get_default_group(context, formsemestre_id)]
)
infos = []
diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py
index 26ebb865..fdcf1c97 100644
--- a/app/scodoc/sco_report.py
+++ b/app/scodoc/sco_report.py
@@ -88,11 +88,22 @@ def formsemestre_etuds_stats(context, sem, only_primo=False):
bs.append(etud["specialite"])
etud["bac-specialite"] = " ".join(bs)
#
- if (not only_primo) or context.isPrimoEtud(etud, sem):
+ if (not only_primo) or is_primo_etud(context, etud, sem):
etuds.append(etud)
return etuds
+def is_primo_etud(context, etud, sem):
+ """Determine si un (filled) etud a ete inscrit avant ce semestre.
+ Regarde la liste des semestres dans lesquels l'étudiant est inscrit
+ """
+ now = sem["dateord"]
+ for s in etud["sems"]: # le + recent d'abord
+ if s["dateord"] < now:
+ return False
+ return True
+
+
def _categories_and_results(etuds, category, result):
categories = {}
results = {}
@@ -416,7 +427,7 @@ def table_suivi_cohorte(
and (not annee_bac or (annee_bac == str(etud["annee_bac"])))
and (not civilite or (civilite == etud["civilite"]))
and (not statut or (statut == etud["statut"]))
- and (not only_primo or context.isPrimoEtud(etud, sem))
+ and (not only_primo or is_primo_etud(context, etud, sem))
):
orig_set.add(etudid)
# semestres suivants:
@@ -708,14 +719,16 @@ def formsemestre_suivi_cohorte(
return t
base_url = REQUEST.URL0
- burl = (
- "%s?formsemestre_id=%s&bac=%s&bacspecialite=%s&civilite=%s&statut=%s"
- % (base_url, formsemestre_id, bac, bacspecialite, civilite, statut)
+ burl = "%s?formsemestre_id=%s&bac=%s&bacspecialite=%s&civilite=%s&statut=%s" % (
+ base_url,
+ formsemestre_id,
+ bac,
+ bacspecialite,
+ civilite,
+ statut,
)
if percent:
- pplink = (
- '' % burl
- )
+ pplink = '' % burl
else:
pplink = (
'Mettre toutes les notes manquantes de l'évaluation
à la valeur %s ?
Suppression de l'ensemble %(title)s ?
" % s,
dest_url="",
REQUEST=REQUEST,
diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py
index 10ec8921..d9c99493 100644
--- a/app/scodoc/sco_synchro_etuds.py
+++ b/app/scodoc/sco_synchro_etuds.py
@@ -205,7 +205,8 @@ def formsemestre_synchro_etuds(
H.append("""Il n'y a rien à modifier !
""")
H.append(
- context.confirmDialog(
+ scu.confirm_dialog(
+ context,
dest_url="formsemestre_synchro_etuds",
add_headers=False,
cancel_url="formsemestre_synchro_etuds?formsemestre_id="
diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py
index 478d74b4..7d5c34fa 100644
--- a/app/scodoc/sco_trombino.py
+++ b/app/scodoc/sco_trombino.py
@@ -185,12 +185,12 @@ def check_local_photos_availability(context, groups_infos, REQUEST, format=""):
parameters = {"group_ids": groups_infos.group_ids, "format": format}
return (
False,
- context.confirmDialog(
+ scu.confirm_dialog(
+ context,
"""Copier les photos du portail vers ScoDoc ?
Code étudiant (%s) dupliqué !
""" % code_name,
helpmsg="""Le %s %s est déjà utilisé: un seul étudiant peut avoir ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.
"
+ else:
+ if etud["codelycee"]:
+ etud["ilycee"] = format_lycee_from_code(etud["codelycee"])
+ else:
+ etud["ilycee"] = ""
+ rap = ""
+ if etud["rapporteur"] or etud["commentaire"]:
+ rap = "Note du rapporteur"
+ if etud["rapporteur"]:
+ rap += " (%s)" % etud["rapporteur"]
+ rap += ": "
+ if etud["commentaire"]:
+ rap += "%s" % etud["commentaire"]
+ etud["rap"] = rap
+
+ # if etud['boursier_prec']:
+ # pass
+
+ if etud["telephone"]:
+ etud["telephonestr"] = "Tél.: " + format_telephone(etud["telephone"])
+ else:
+ etud["telephonestr"] = ""
+ if etud["telephonemobile"]:
+ etud["telephonemobilestr"] = "Mobile: " + format_telephone(
+ etud["telephonemobile"]
+ )
+ else:
+ etud["telephonemobilestr"] = ""
+ etud["debouche"] = etud["debouche"] or ""
+
+
+def descr_situation_etud(context, etudid, ne=""):
+ """chaine decrivant la situation actuelle de l'etudiant"""
+ cnx = context.GetDBConnexion()
+ cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
+ cursor.execute(
+ "select I.formsemestre_id, I.etat from notes_formsemestre_inscription I, notes_formsemestre S where etudid=%(etudid)s and S.formsemestre_id = I.formsemestre_id and date_debut < now() and date_fin > now() order by S.date_debut desc;",
+ {"etudid": etudid},
+ )
+ r = cursor.dictfetchone()
+ if not r:
+ situation = "non inscrit"
+ else:
+ sem = sco_formsemestre.get_formsemestre(context, r["formsemestre_id"])
+ if r["etat"] == "I":
+ situation = "inscrit%s en %s" % (ne, sem["titremois"])
+ # Cherche la date d'inscription dans scolar_events:
+ events = scolars.scolar_events_list(
+ cnx,
+ args={
+ "etudid": etudid,
+ "formsemestre_id": sem["formsemestre_id"],
+ "event_type": "INSCRIPTION",
+ },
+ )
+ if not events:
+ log(
+ "*** situation inconsistante pour %s (inscrit mais pas d'event)"
+ % etudid
+ )
+ date_ins = "???" # ???
+ else:
+ date_ins = events[0]["event_date"]
+ situation += " le " + str(date_ins)
+ else:
+ situation = "démission de %s" % sem["titremois"]
+ # Cherche la date de demission dans scolar_events:
+ events = scolars.scolar_events_list(
+ cnx,
+ args={
+ "etudid": etudid,
+ "formsemestre_id": sem["formsemestre_id"],
+ "event_type": "DEMISSION",
+ },
+ )
+ if not events:
+ log(
+ "*** situation inconsistante pour %s (demission mais pas d'event)"
+ % etudid
+ )
+ date_dem = "???" # ???
+ else:
+ date_dem = events[0]["event_date"]
+ situation += " le " + str(date_dem)
+ return situation
\ No newline at end of file
diff --git a/app/views/absences.py b/app/views/absences.py
index 2353d645..ba048031 100644
--- a/app/views/absences.py
+++ b/app/views/absences.py
@@ -1,10 +1,62 @@
# -*- 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
+#
+##############################################################################
+
"""
Module absences: issu de ScoDoc7 / ZAbsences.py
Emmanuel Viennet, 2021
+
+Gestion des absences (v4)
+
+Code dérivé de la partie la plus ancienne de ScoDoc, et à revoir.
+
+L'API de plus bas niveau est en gros:
+
+ AnnuleAbsencesDatesNoJust(etudid, dates)
+ CountAbs(etudid, debut, fin, matin=None, moduleimpl_id=None)
+ CountAbsJust(etudid, debut, fin, matin=None, moduleimpl_id=None)
+ ListeAbsJust(etudid, datedebut) [pas de fin ?]
+ ListeAbsNonJust(etudid, datedebut) [pas de fin ?]
+ ListeJustifs(etudid, datedebut, datefin=None, only_no_abs=True)
+
+ ListeAbsJour(date, am=True, pm=True, is_abs=None, is_just=None)
+ ListeAbsNonJustJour(date, am=True, pm=True)
+
"""
+import string
+import re
+import time
+import datetime
+import dateutil
+import dateutil.parser
+import calendar
+import urllib
+import cgi
+import jaxml
+
from flask import g
from flask import current_app
@@ -19,19 +71,1915 @@ from app.auth.models import Permission
from app.views import absences_bp as bp
-context = ScoDoc7Context(globals())
+# ---------------
+from app.scodoc import sco_utils as scu
+from app.scodoc import notesdb
+from app.scodoc.notes_log import log
+from app.scodoc.scolog import logdb
+from app.scodoc.sco_permissions import ScoAbsAddBillet, ScoAbsChange, ScoView
+from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidDateError
+from app.scodoc.TrivialFormulator import TrivialFormulator, TF
+from app.scodoc.gen_tables import GenTable
+from app.scodoc import html_sco_header
+from app.scodoc import scolars
+from app.scodoc import sco_formsemestre
+from app.scodoc import sco_moduleimpl
+from app.scodoc import sco_groups
+from app.scodoc import sco_groups_view
+from app.scodoc import sco_excel
+from app.scodoc import sco_abs_notification, sco_abs_views
+from app.scodoc import sco_compute_moy
+from app.scodoc import sco_abs
+from app.scodoc.sco_abs import ddmmyyyy
+
+CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
-@bp.route("/")
+def _toboolean(x):
+ "convert a value to boolean (ensure backward compat with OLD intranet code)"
+ if type(x) == type(""):
+ x = x.lower()
+ if x and x != "false": # backward compat...
+ return True
+ else:
+ return False
+
+
+# --------------------------------------------------------------------
+#
+# ABSENCES (/ScoDoc/ScoDoc 8 ZAbsences !
- Aucun étudiant !
"
+ + context.sco_footer(REQUEST)
+ )
+
+ base_url = "SignaleAbsenceGrHebdo?datelundi=%s&%s&destination=%s" % (
+ datelundi,
+ groups_infos.groups_query_args,
+ urllib.quote(destination),
+ )
+
+ formsemestre_id = groups_infos.formsemestre_id
+ require_module = context.get_preference("abs_require_module", formsemestre_id)
+ etuds = [
+ context.getEtudInfo(etudid=m["etudid"], filled=True)[0]
+ for m in groups_infos.members
+ ]
+ # Restreint aux inscrits au module sélectionné
+ if moduleimpl_id:
+ mod_inscrits = set(
+ [
+ x["etudid"]
+ for x in sco_moduleimpl.do_moduleimpl_inscription_list(
+ context.Notes, moduleimpl_id=moduleimpl_id
+ )
+ ]
+ )
+ etuds_inscrits_module = [e for e in etuds if e["etudid"] in mod_inscrits]
+ if etuds_inscrits_module:
+ etuds = etuds_inscrits_module
+ else:
+ # Si aucun etudiant n'est inscrit au module choisi...
+ moduleimpl_id = None
+ nt = context.Notes._getNotesCache().get_NotesTable(context.Notes, formsemestre_id)
+ sem = sco_formsemestre.do_formsemestre_list(
+ context, {"formsemestre_id": formsemestre_id}
+ )[0]
+
+ # calcule dates jours de cette semaine
+ # liste de dates iso "yyyy-mm-dd"
+ datessem = [notesdb.DateDMYtoISO(datelundi)]
+ for _ in sco_abs.day_names(context)[1:]:
+ datessem.append(sco_abs.next_iso_day(context, datessem[-1]))
+ #
+ if groups_infos.tous_les_etuds_du_sem:
+ gr_tit = "en"
+ else:
+ if len(groups_infos.group_ids) > 1:
+ p = "des groupes"
+ else:
+ p = "du groupe"
+ gr_tit = p + ' ' + groups_infos.groups_titles + ""
+
+ H = [
+ context.sco_header(
+ page_title="Saisie hebdomadaire des absences",
+ init_qtip=True,
+ javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
+ + [
+ "js/etud_info.js",
+ "js/abs_ajax.js",
+ "js/groups_view.js",
+ ],
+ cssstyles=CSSSTYLES,
+ no_side_bar=1,
+ REQUEST=REQUEST,
+ ),
+ """
+
+ Saisie des absences %s %s,
+ semaine du lundi %s
+ Aucun étudiant !
"
+ + context.sco_footer(REQUEST)
+ )
+ formsemestre_id = groups_infos.formsemestre_id
+ require_module = context.get_preference("abs_require_module", formsemestre_id)
+ etuds = [
+ context.getEtudInfo(etudid=m["etudid"], filled=True)[0]
+ for m in groups_infos.members
+ ]
+ # Restreint aux inscrits au module sélectionné
+ if moduleimpl_id:
+ mod_inscrits = set(
+ [
+ x["etudid"]
+ for x in sco_moduleimpl.do_moduleimpl_inscription_list(
+ context.Notes, moduleimpl_id=moduleimpl_id
+ )
+ ]
+ )
+ etuds = [e for e in etuds if e["etudid"] in mod_inscrits]
+ if not moduleimpl_id:
+ moduleimpl_id = None
+ base_url_noweeks = (
+ "SignaleAbsenceGrSemestre?datedebut=%s&datefin=%s&%s&destination=%s"
+ % (
+ datedebut,
+ datefin,
+ groups_infos.groups_query_args,
+ urllib.quote(destination),
+ )
+ )
+ base_url = base_url_noweeks + "&nbweeks=%s" % nbweeks # sans le moduleimpl_id
+
+ if etuds:
+ nt = context.Notes._getNotesCache().get_NotesTable(
+ context.Notes, formsemestre_id
+ )
+ sem = sco_formsemestre.do_formsemestre_list(
+ context, {"formsemestre_id": formsemestre_id}
+ )[0]
+ work_saturday = sco_abs.is_work_saturday(context)
+ jourdebut = ddmmyyyy(datedebut, work_saturday=work_saturday)
+ jourfin = ddmmyyyy(datefin, work_saturday=work_saturday)
+ today = ddmmyyyy(
+ time.strftime("%d/%m/%Y", time.localtime()),
+ work_saturday=work_saturday,
+ )
+ today.next()
+ if jourfin > today: # ne propose jamais les semaines dans le futur
+ jourfin = today
+ if jourdebut > today:
+ raise ScoValueError("date de début dans le futur (%s) !" % jourdebut)
+ #
+ if not jourdebut.iswork() or jourdebut > jourfin:
+ raise ValueError(
+ "date debut invalide (%s, ouvrable=%d)"
+ % (str(jourdebut), jourdebut.iswork())
+ )
+ # calcule dates
+ dates = [] # ddmmyyyy instances
+ d = ddmmyyyy(datedebut, work_saturday=work_saturday)
+ while d <= jourfin:
+ dates.append(d)
+ d = d.next(7) # avance d'une semaine
+ #
+ msg = "Montrer seulement les 4 dernières semaines"
+ nwl = 4
+ if nbweeks:
+ nbweeks = int(nbweeks)
+ if nbweeks > 0:
+ dates = dates[-nbweeks:]
+ msg = "Montrer toutes les semaines"
+ nwl = 0
+ url_link_semaines = base_url_noweeks + "&nbweeks=%s" % nwl
+ if moduleimpl_id:
+ url_link_semaines += "&moduleimpl_id=" + moduleimpl_id
+ #
+ dates = [x.ISO() for x in dates]
+ dayname = sco_abs.day_names(context)[jourdebut.weekday]
+
+ if groups_infos.tous_les_etuds_du_sem:
+ gr_tit = "en"
+ else:
+ if len(groups_infos.group_ids) > 1:
+ p = "des groupes "
+ else:
+ p = "du groupe "
+ gr_tit = p + '' + groups_infos.groups_titles + ""
+
+ H = [
+ context.sco_header(
+ page_title="Saisie des absences",
+ init_qtip=True,
+ javascripts=["js/etud_info.js", "js/abs_ajax.js"],
+ no_side_bar=1,
+ REQUEST=REQUEST,
+ ),
+ """
+
+ Saisie des absences %s %s,
+ les %s
+
" % (debut, fin),
+ base_url="%s&formsemestre_id=%s&debut=%s&fin=%s"
+ % (groups_infos.base_url, formsemestre_id, debut, fin),
+ filename="etat_abs_"
+ + scu.make_filename(
+ "%s de %s" % (groups_infos.groups_filename, sem["titreannee"])
+ ),
+ caption=title,
+ html_next_section="""
+ou entrez une date pour visualiser les absents un jour donné :
+État des absences le %s
" % date)
+ H.append(
+ """
+
")
+ if nbetud == 0:
+ H.append("
+ """
+ )
+ for etud in groups_infos.members:
+ nbabsam = context.CountAbs(
+ etudid=etud["etudid"], debut=dateiso, fin=dateiso, matin=1
+ )
+ nbabspm = context.CountAbs(
+ etudid=etud["etudid"], debut=dateiso, fin=dateiso, matin=0
+ )
+ if (nbabsam != 0) or (nbabspm != 0):
+ nbetud += 1
+ nbabsjustam = context.CountAbsJust(
+ etudid=etud["etudid"], debut=dateiso, fin=dateiso, matin=1
+ )
+ nbabsjustpm = context.CountAbsJust(
+ etudid=etud["etudid"], debut=dateiso, fin=dateiso, matin=0
+ )
+ H.append(
+ """
+ Matin Après-midi ")
+ H.append(
+ """
+ %(nomprenom)s """
+ % etud
+ ) # """
+ if nbabsam != 0:
+ if nbabsjustam:
+ H.append("Just.")
+ t_nbabsjustam += 1
+ else:
+ H.append("Abs.")
+ t_nbabsam += 1
+ else:
+ H.append("")
+ H.append(' ')
+ if nbabspm != 0:
+ if nbabsjustpm:
+ H.append("Just.")
+ t_nbabsjustam += 1
+ else:
+ H.append("Abs.")
+ t_nbabspm += 1
+ else:
+ H.append("")
+ H.append(" """
+ % (t_nbabsam, t_nbabsjustam, t_nbabspm, t_nbabsjustpm)
+ )
+ H.append("%d abs, %d just. %d abs, %d just. Erreur: vous n'avez pas choisi de date !
+ Continuer"""
+ % REQUEST.HTTP_REFERER
+ )
+
+ return "\n".join(H) + context.sco_footer(REQUEST)
+
+
+# ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail)
+@bp.route("/AddBilletAbsence")
+@permission_required(Permission.ScoAbsAddBillet)
+@scodoc7func(context)
+def AddBilletAbsence(
+ context,
+ begin,
+ end,
+ description,
+ etudid=False,
+ code_nip=None,
+ code_ine=None,
+ justified=True,
+ REQUEST=None,
+ xml_reply=True,
+):
+ """Memorise un "billet"
+ begin et end sont au format ISO (eg "1999-01-08 04:05:06")
+ """
+ t0 = time.time()
+ # check etudid
+ etuds = context.getEtudInfo(
+ etudid=etudid, code_nip=code_nip, REQUEST=REQUEST, filled=True
+ )
+ if not etuds:
+ return scu.log_unknown_etud(context, REQUEST=REQUEST)
+ etud = etuds[0]
+ # check dates
+ begin_date = dateutil.parser.isoparse(begin) # may raises ValueError
+ end_date = dateutil.parser.isoparse(end)
+ if begin_date > end_date:
+ raise ValueError("invalid dates")
+ #
+ justified = int(justified)
+ #
+ cnx = context.GetDBConnexion()
+ billet_id = sco_abs.billet_absence_create(
+ cnx,
+ {
+ "etudid": etud["etudid"],
+ "abs_begin": begin,
+ "abs_end": end,
+ "description": description,
+ "etat": 0,
+ "justified": justified,
+ },
+ )
+ if xml_reply:
+ # Renvoie le nouveau billet en XML
+ if REQUEST:
+ REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
+
+ billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id})
+ tab = context._tableBillets(billets, etud=etud)
+ log("AddBilletAbsence: new billet_id=%s (%gs)" % (billet_id, time.time() - t0))
+ return tab.make_page(context, REQUEST=REQUEST, format="xml")
+ else:
+ return billet_id
+
+
+@bp.route("/AddBilletAbsenceForm")
+@permission_required(Permission.ScoAbsAddBillet)
+@scodoc7func(context)
+def AddBilletAbsenceForm(context, etudid, REQUEST=None):
+ """Formulaire ajout billet (pour tests seulement, le vrai formulaire accessible aux etudiants
+ étant sur le portail étudiant).
+ """
+ etud = context.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
+ H = [
+ context.sco_header(
+ REQUEST, page_title="Billet d'absence de %s" % etud["nomprenom"]
+ )
+ ]
+ tf = TrivialFormulator(
+ REQUEST.URL0,
+ REQUEST.form,
+ (
+ ("etudid", {"input_type": "hidden"}),
+ ("begin", {"input_type": "date"}),
+ ("end", {"input_type": "date"}),
+ (
+ "justified",
+ {"input_type": "boolcheckbox", "default": 0, "title": "Justifiée"},
+ ),
+ ("description", {"input_type": "textarea"}),
+ ),
+ )
+ if tf[0] == 0:
+ return "\n".join(H) + tf[1] + context.sco_footer(REQUEST)
+ elif tf[0] == -1:
+ return REQUEST.RESPONSE.redirect(context.ScoURL())
+ else:
+ e = tf[2]["begin"].split("/")
+ begin = e[2] + "-" + e[1] + "-" + e[0] + " 00:00:00"
+ e = tf[2]["end"].split("/")
+ end = e[2] + "-" + e[1] + "-" + e[0] + " 00:00:00"
+ log(
+ context.AddBilletAbsence(
+ begin,
+ end,
+ tf[2]["description"],
+ etudid=etudid,
+ xml_reply=True,
+ justified=tf[2]["justified"],
+ )
+ )
+ return REQUEST.RESPONSE.redirect("listeBilletsEtud?etudid=" + etudid)
+
+
+def _tableBillets(context, billets, etud=None, title=""):
+ for b in billets:
+ if b["abs_begin"].hour < 12:
+ m = " matin"
+ else:
+ m = " après-midi"
+ b["abs_begin_str"] = b["abs_begin"].strftime("%d/%m/%Y") + m
+ if b["abs_end"].hour < 12:
+ m = " matin"
+ else:
+ m = " après-midi"
+ b["abs_end_str"] = b["abs_end"].strftime("%d/%m/%Y") + m
+ if b["etat"] == 0:
+ if b["justified"] == 0:
+ b["etat_str"] = "à traiter"
+ else:
+ b["etat_str"] = "à justifier"
+ b["_etat_str_target"] = (
+ "ProcessBilletAbsenceForm?billet_id=%s" % b["billet_id"]
+ )
+ if etud:
+ b["_etat_str_target"] += "&etudid=%s" % etud["etudid"]
+ b["_billet_id_target"] = b["_etat_str_target"]
+ else:
+ b["etat_str"] = "ok"
+ if not etud:
+ # ajoute info etudiant
+ e = context.getEtudInfo(etudid=b["etudid"], filled=1)
+ if not e:
+ b["nomprenom"] = "???" # should not occur
+ else:
+ b["nomprenom"] = e[0]["nomprenom"]
+ b["_nomprenom_target"] = "ficheEtud?etudid=%s" % b["etudid"]
+ if etud and not title:
+ title = "Billets d'absence déclarés par %(nomprenom)s" % etud
+ else:
+ title = title
+ columns_ids = ["billet_id"]
+ if not etud:
+ columns_ids += ["nomprenom"]
+ columns_ids += ["abs_begin_str", "abs_end_str", "description", "etat_str"]
+
+ tab = GenTable(
+ titles={
+ "billet_id": "Numéro",
+ "abs_begin_str": "Début",
+ "abs_end_str": "Fin",
+ "description": "Raison de l'absence",
+ "etat_str": "Etat",
+ },
+ columns_ids=columns_ids,
+ page_title=title,
+ html_title="%s
" % title,
+ preferences=context.get_preferences(),
+ rows=billets,
+ html_sortable=True,
+ )
+ return tab
+
+
+@bp.route("/listeBilletsEtud")
+@permission_required(Permission.ScoView)
+@scodoc7func(context)
+def listeBilletsEtud(context, etudid=False, REQUEST=None, format="html"):
+ """Liste billets pour un etudiant"""
+ etuds = context.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)
+ if not etuds:
+ return scu.log_unknown_etud(context, format=format, REQUEST=REQUEST)
+
+ etud = etuds[0]
+ cnx = context.GetDBConnexion()
+ billets = sco_abs.billet_absence_list(cnx, {"etudid": etud["etudid"]})
+ tab = context._tableBillets(billets, etud=etud)
+ return tab.make_page(context, REQUEST=REQUEST, format=format)
+
+
+@bp.route("/XMLgetBilletsEtud")
+@permission_required(Permission.ScoView)
+@scodoc7func(context)
+def XMLgetBilletsEtud(context, etudid=False, REQUEST=None):
+ """Liste billets pour un etudiant"""
+ if not context.get_preference("handle_billets_abs"):
+ return ""
+ t0 = time.time()
+ r = context.listeBilletsEtud(etudid, REQUEST=REQUEST, format="xml")
+ log("XMLgetBilletsEtud (%gs)" % (time.time() - t0))
+ return r
+
+
+@bp.route("/listeBillets")
+@permission_required(Permission.ScoView)
+@scodoc7func(context)
+def listeBillets(context, REQUEST=None):
+ """Page liste des billets non traités et formulaire recherche d'un billet"""
+ cnx = context.GetDBConnexion()
+ billets = sco_abs.billet_absence_list(cnx, {"etat": 0})
+ tab = context._tableBillets(billets)
+ T = tab.html()
+ H = [
+ context.sco_header(REQUEST, page_title="Billet d'absence non traités"),
+ "Billets d'absence en attente de traitement (%d)
" % len(billets),
+ ]
+
+ tf = TrivialFormulator(
+ REQUEST.URL0,
+ REQUEST.form,
+ (("billet_id", {"input_type": "text", "title": "Numéro du billet"}),),
+ submitbutton=False,
+ )
+ if tf[0] == 0:
+ return "\n".join(H) + tf[1] + T + context.sco_footer(REQUEST)
+ else:
+ return REQUEST.RESPONSE.redirect(
+ "ProcessBilletAbsenceForm?billet_id=" + tf[2]["billet_id"]
+ )
+
+
+@bp.route("/deleteBilletAbsence")
+@permission_required(Permission.ScoAbsChange)
+@scodoc7func(context)
+def deleteBilletAbsence(context, billet_id, REQUEST=None, dialog_confirmed=False):
+ """Supprime un billet."""
+ cnx = context.GetDBConnexion()
+ billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id})
+ if not billets:
+ return REQUEST.RESPONSE.redirect(
+ "listeBillets?head_message=Billet%%20%s%%20inexistant !" % billet_id
+ )
+ if not dialog_confirmed:
+ tab = context._tableBillets(billets)
+ return context.confirmDialog(
+ """Supprimer ce billet ?
""" + tab.html(),
+ dest_url="",
+ REQUEST=REQUEST,
+ cancel_url="listeBillets",
+ parameters={"billet_id": billet_id},
+ )
+
+ sco_abs.billet_absence_delete(cnx, billet_id)
+
+ return REQUEST.RESPONSE.redirect("listeBillets?head_message=Billet%20supprimé")
+
+
+def _ProcessBilletAbsence(context, billet, estjust, description, REQUEST):
+ """Traite un billet: ajoute absence(s) et éventuellement justificatifs,
+ et change l'état du billet à 1.
+ NB: actuellement, les heures ne sont utilisées que pour déterminer si matin et/ou après-midi.
+ """
+ cnx = context.GetDBConnexion()
+ if billet["etat"] != 0:
+ log("billet=%s" % billet)
+ log("billet deja traité !")
+ return -1
+ n = 0 # nombre de demi-journées d'absence ajoutées
+ # 1-- ajout des absences (et justifs)
+ datedebut = billet["abs_begin"].strftime("%d/%m/%Y")
+ datefin = billet["abs_end"].strftime("%d/%m/%Y")
+ dates = sco_abs.DateRangeISO(context, datedebut, datefin)
+ # commence après-midi ?
+ if dates and billet["abs_begin"].hour > 11:
+ context._AddAbsence(
+ billet["etudid"], dates[0], 0, estjust, REQUEST, description=description
+ )
+ n += 1
+ dates = dates[1:]
+ # termine matin ?
+ if dates and billet["abs_end"].hour < 12:
+ context._AddAbsence(
+ billet["etudid"],
+ dates[-1],
+ 1,
+ estjust,
+ REQUEST,
+ description=description,
+ )
+ n += 1
+ dates = dates[:-1]
+
+ for jour in dates:
+ context._AddAbsence(
+ billet["etudid"], jour, 0, estjust, REQUEST, description=description
+ )
+ context._AddAbsence(
+ billet["etudid"], jour, 1, estjust, REQUEST, description=description
+ )
+ n += 2
+
+ # 2- change etat du billet
+ sco_abs.billet_absence_edit(cnx, {"billet_id": billet["billet_id"], "etat": 1})
+
+ return n
+
+
+@bp.route("/ProcessBilletAbsenceForm")
+@permission_required(Permission.ScoAbsChange)
+@scodoc7func(context)
+def ProcessBilletAbsenceForm(context, billet_id, REQUEST=None):
+ """Formulaire traitement d'un billet"""
+ cnx = context.GetDBConnexion()
+ billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id})
+ if not billets:
+ return REQUEST.RESPONSE.redirect(
+ "listeBillets?head_message=Billet%%20%s%%20inexistant !" % billet_id
+ )
+ billet = billets[0]
+ etudid = billet["etudid"]
+ etud = context.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
+
+ H = [
+ context.sco_header(
+ REQUEST,
+ page_title="Traitement billet d'absence de %s" % etud["nomprenom"],
+ ),
+ 'Traitement du billet %s : %s
'
+ % (billet_id, etudid, etud["nomprenom"]),
+ ]
+
+ tf = TrivialFormulator(
+ REQUEST.URL0,
+ REQUEST.form,
+ (
+ ("billet_id", {"input_type": "hidden"}),
+ (
+ "etudid",
+ {"input_type": "hidden"},
+ ), # pour centrer l'UI sur l'étudiant
+ (
+ "estjust",
+ {"input_type": "boolcheckbox", "title": "Absences justifiées"},
+ ),
+ ("description", {"input_type": "text", "size": 42, "title": "Raison"}),
+ ),
+ initvalues={
+ "description": billet["description"],
+ "estjust": billet["justified"],
+ "etudid": etudid,
+ },
+ submitlabel="Enregistrer ces absences",
+ )
+ if tf[0] == 0:
+ tab = context._tableBillets([billet], etud=etud)
+ H.append(tab.html())
+ if billet["justified"] == 1:
+ H.append(
+ """
Vérifiez le justificatif avant d'enregistrer.
" + tf[1] + F + context.sco_footer(REQUEST)
+ elif tf[0] == -1:
+ return REQUEST.RESPONSE.redirect(context.ScoURL())
+ else:
+ n = context._ProcessBilletAbsence(
+ billet, tf[2]["estjust"], tf[2]["description"], REQUEST
+ )
+ if tf[2]["estjust"]:
+ j = "justifiées"
+ else:
+ j = "non justifiées"
+ H.append('Billets déclarés par %s
'
+ % (etud["nomprenom"])
+ )
+ billets = sco_abs.billet_absence_list(cnx, {"etudid": etud["etudid"]})
+ tab = context._tableBillets(billets, etud=etud)
+ H.append(tab.html())
+ return "\n".join(H) + context.sco_footer(REQUEST)
+
+
+@bp.route("/XMLgetAbsEtud")
+@permission_required(Permission.ScoView)
+@scodoc7func(context)
+def XMLgetAbsEtud(context, beg_date="", end_date="", REQUEST=None):
+ """returns list of absences in date interval"""
+ t0 = time.time()
+ etud = context.getEtudInfo(REQUEST=REQUEST)[0]
+ exp = re.compile(r"^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$")
+ if not exp.match(beg_date):
+ raise ScoValueError("invalid date: %s" % beg_date)
+ if not exp.match(end_date):
+ raise ScoValueError("invalid date: %s" % end_date)
+
+ Abs = context._ListeAbsDate(etud["etudid"], beg_date, end_date)
+
+ REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
+ doc = jaxml.XML_document(encoding=scu.SCO_ENCODING)
+ doc.absences(etudid=etud["etudid"], beg_date=beg_date, end_date=end_date)
+ doc._push()
+ for a in Abs:
+ if a["estabs"]: # ne donne pas les justifications si pas d'absence
+ doc._push()
+ doc.abs(
+ begin=a["begin"],
+ end=a["end"],
+ description=a["description"],
+ justified=a["estjust"],
+ )
+ doc._pop()
+ doc._pop()
+ log("XMLgetAbsEtud (%gs)" % (time.time() - t0))
+ return repr(doc)
diff --git a/app/views/notes.py b/app/views/notes.py
index f6130045..0d20761e 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -130,8 +130,6 @@ from app.scodoc import notes_table as notes_table
from app.scodoc.notes_table import NOTES_CACHE_INST, CacheNotesTable
import app.scodoc.VERSION as VERSION
-context = ScoDoc7Context(globals())
-
def sco_publish(route, function, permission):
"""Declare a route for a python function,
@@ -687,7 +685,8 @@ def _do_ue_delete(context, ue_id, delete_validations=False, REQUEST=None, force=
cnx, args={"ue_id": ue_id}
)
if validations and not delete_validations and not force:
- return context.confirmDialog(
+ return scu.confirm_dialog(
+ context,
"Destruction du module impossible car il est utilisé dans des semestres existants !
""",
helpmsg="""Il faut d'abord supprimer le semestre. Mais il est peut être préférable de laisser ce programme intact et d'en créer une nouvelle version pour la modifier.""",
dest_url="ue_list",
@@ -2037,7 +2037,8 @@ def formsemestre_desinscription(
car il n'a pas d'autre étudiant inscrit.
Opération non autorisée pour %s" % REQUEST.AUTHENTICATED_USER, dest_url=context.ScoURL(), @@ -3023,7 +3026,8 @@ def formsemestre_validation_etud_manu( ): "Enregistre choix jury pour un étudiant" if not context._can_validate_sem(REQUEST, formsemestre_id): - return context.confirmDialog( + return scu.confirm_dialog( + context, message="
Opération non autorisée pour %s" % REQUEST.AUTHENTICATED_USER, dest_url=context.ScoURL(), @@ -3052,7 +3056,8 @@ def formsemestre_validate_previous_ue( ): "Form. saisie UE validée hors ScoDoc " if not context._can_validate_sem(REQUEST, formsemestre_id): - return context.confirmDialog( + return scu.confirm_dialog( + context, message="
Opération non autorisée pour %s" % REQUEST.AUTHENTICATED_USER, dest_url=context.ScoURL(), @@ -3078,7 +3083,8 @@ def formsemestre_ext_edit_ue_validations( ): "Form. edition UE semestre extérieur" if not context._can_validate_sem(REQUEST, formsemestre_id): - return context.confirmDialog( + return scu.confirm_dialog( + context, message="
Opération non autorisée pour %s" % REQUEST.AUTHENTICATED_USER, dest_url=context.ScoURL(), @@ -3102,7 +3108,8 @@ sco_publish( def etud_ue_suppress_validation(context, etudid, formsemestre_id, ue_id, REQUEST=None): """Suppress a validation (ue_id, etudid) and redirect to formsemestre""" if not context._can_validate_sem(REQUEST, formsemestre_id): - return context.confirmDialog( + return scu.confirm_dialog( + context, message="
Opération non autorisée pour %s" % REQUEST.AUTHENTICATED_USER, dest_url=context.ScoURL(), @@ -3119,7 +3126,8 @@ def etud_ue_suppress_validation(context, etudid, formsemestre_id, ue_id, REQUEST def formsemestre_validation_auto(context, formsemestre_id, REQUEST): "Formulaire saisie automatisee des decisions d'un semestre" if not context._can_validate_sem(REQUEST, formsemestre_id): - return context.confirmDialog( + return scu.confirm_dialog( + context, message="
Opération non autorisée pour %s" % REQUEST.AUTHENTICATED_USER, dest_url=context.ScoURL(), @@ -3137,7 +3145,8 @@ def formsemestre_validation_auto(context, formsemestre_id, REQUEST): def do_formsemestre_validation_auto(context, formsemestre_id, REQUEST): "Formulaire saisie automatisee des decisions d'un semestre" if not context._can_validate_sem(REQUEST, formsemestre_id): - return context.confirmDialog( + return scu.confirm_dialog( + context, message="
Opération non autorisée pour %s" % REQUEST.AUTHENTICATED_USER, dest_url=context.ScoURL(), @@ -3155,7 +3164,8 @@ def do_formsemestre_validation_auto(context, formsemestre_id, REQUEST): def formsemestre_fix_validation_ues(context, formsemestre_id, REQUEST=None): "Verif/reparation codes UE" if not context._can_validate_sem(REQUEST, formsemestre_id): - return context.confirmDialog( + return scu.confirm_dialog( + context, message="
Opération non autorisée pour %s" % REQUEST.AUTHENTICATED_USER, dest_url=context.ScoURL(), @@ -3175,7 +3185,8 @@ def formsemestre_validation_suppress_etud( ): """Suppression des decisions de jury pour un etudiant.""" if not context._can_validate_sem(REQUEST, formsemestre_id): - return context.confirmDialog( + return scu.confirm_dialog( + context, message="
Opération non autorisée pour %s" % REQUEST.AUTHENTICATED_USER, dest_url=context.ScoURL(), @@ -3194,7 +3205,8 @@ def formsemestre_validation_suppress_etud( ) else: existing = "" - return context.confirmDialog( + return scu.confirm_dialog( + context, """
Cette opération est irréversible.
diff --git a/app/views/scolar.py b/app/views/scolar.py index 84842dd9..e045eab2 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -1,13 +1,59 @@ # -*- coding: utf-8 -*- +############################################################################## +# +# ScoDoc +# +# 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 +# +############################################################################## + """ Module scolar: issu de ScoDoc7 / ZScolar.py Emmanuel Viennet, 2021 """ +import sys +import traceback +import time +import string +import glob +import re +import urllib +import urllib2 +import cgi +import xml +import jaxml + +try: + from cStringIO import StringIO +except: + from StringIO import StringIO +from zipfile import ZipFile +import thread +import psycopg2 + from flask import g from flask import current_app +from config import Config +from scodoc_manager import sco_mgr from app.decorators import ( scodoc7func, ScoDoc7Context, @@ -19,19 +65,1999 @@ from app.auth.models import Permission from app.views import scolar_bp as bp -context = ScoDoc7Context(globals()) +from app.scodoc.notes_log import log + +import app.scodoc.sco_utils as scu +import app.scodoc.notesdb as ndb +from app.scodoc.scolog import logdb +from app.scodoc.sco_permissions import ( + ScoAbsChange, + ScoView, + ScoEnsView, + ScoImplement, + ScoChangeFormation, + ScoChangePreferences, + ScoObservateur, + ScoEtudAddAnnotations, + ScoEtudInscrit, + ScoEtudChangeGroups, + ScoEtudChangeAdr, + ScoEditAllEvals, + ScoEditAllNotes, + ScoEditFormationTags, + ScoEditApo, + ScoSuperAdmin, +) +import app.scodoc.sco_permissions +from app.scodoc.sco_exceptions import ( + AccessDenied, + ScoException, + ScoValueError, + ScoInvalidDateError, + ScoLockedFormError, + ScoGenError, + ScoInvalidDept, +) +from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message +import app.scodoc.scolars +import app.scodoc.sco_codes_parcours +import app.scodoc.sco_preferences +import app.scodoc.sco_formations +from app.scodoc.scolars import ( + format_nom, + format_prenom, + format_civilite, + format_lycee, + format_lycee_from_code, +) +from app.scodoc.scolars import format_telephone, format_pays, make_etud_args +import app.scodoc.sco_find_etud +import app.scodoc.sco_photos +import app.scodoc.sco_formsemestre +import app.scodoc.sco_formsemestre_edit +import app.scodoc.sco_news +from app.scodoc.sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC + +import app.scodoc.html_sco_header +import app.scodoc.html_sidebar + +from app.scodoc.gen_tables import GenTable +import app.scodoc.sco_excel +import app.scodoc.imageresize + +import app.scodoc.ImportScolars +import app.scodoc.sco_abs +import app.scodoc.sco_portal_apogee +import app.scodoc.sco_synchro_etuds +import app.scodoc.sco_page_etud +import app.scodoc.sco_groups +import app.scodoc.sco_trombino +import app.scodoc.sco_groups_view +import app.scodoc.sco_trombino_tours +import app.scodoc.sco_parcours_dut +import app.scodoc.sco_report +import app.scodoc.sco_archives_etud +import app.scodoc.sco_debouche +import app.scodoc.sco_groups_edit +import app.scodoc.sco_up_to_date +import app.scodoc.sco_edt_cal +import app.scodoc.sco_dept +import app.scodoc.sco_dump_db + +from app.scodoc.VERSION import SCOVERSION, SCONEWS + + +log.set_log_directory(Config.INSTANCE_HOME + "/log") +log("ScoDoc8 restarting...") + +# -------------------------------------------------------------------- +# +# SCOLARITE (/ScoDoc/ZScolar ScoDoc 8
-g.scodoc_dept=%(scodoc_dept)s
- - - """ % { - "scodoc_dept": g.scodoc_dept, - } +def about(context, REQUEST): + "version info" + H = [ + """© Emmanuel Viennet 1997-2021
+Version %s
+ """ + % (scu.get_scodoc_version()) + ] + H.append( + 'Logiciel libre écrit en Python.
Utilise ReportLab pour générer les documents PDF, et pyExcelerator pour le traitement des documents Excel.
' + ) + H.append("' + % etud + ] + header = context.sco_header( + REQUEST, page_title="Changement adresse de %(nomprenom)s" % etud + ) + + tf = TrivialFormulator( + REQUEST.URL0, + REQUEST.form, + ( + ("adresse_id", {"input_type": "hidden"}), + ("etudid", {"input_type": "hidden"}), + ( + "email", + { + "size": 40, + "title": "e-mail", + "explanation": "adresse institutionnelle", + }, + ), + ( + "emailperso", + { + "size": 40, + "title": "e-mail", + "explanation": "adresse personnelle", + }, + ), + ( + "domicile", + {"size": 65, "explanation": "numéro, rue", "title": "Adresse"}, + ), + ("codepostaldomicile", {"size": 6, "title": "Code postal"}), + ("villedomicile", {"size": 20, "title": "Ville"}), + ("paysdomicile", {"size": 20, "title": "Pays"}), + ("", {"input_type": "separator", "default": " "}), + ("telephone", {"size": 13, "title": "Téléphone"}), + ("telephonemobile", {"size": 13, "title": "Mobile"}), + ), + initvalues=adr, + submitlabel="Valider le formulaire", + ) + dest_url = context.ScoURL() + "/ficheEtud?etudid=" + etudid + if tf[0] == 0: + return header + "\n".join(H) + tf[1] + context.sco_footer(REQUEST) + elif tf[0] == -1: + return REQUEST.RESPONSE.redirect(dest_url) + else: + if adrs: + scolars.adresse_edit(cnx, args=tf[2], context=context) + else: + scolars.adresse_create(cnx, args=tf[2]) + logdb(REQUEST, cnx, method="changeCoordonnees", etudid=etudid) + return REQUEST.RESPONSE.redirect(dest_url) + + +# --- Gestion des groupes: +sco_publish("/affectGroups", sco_groups_edit.affectGroups, Permission.ScoView) + +sco_publish( + "/XMLgetGroupsInPartition", sco_groups.XMLgetGroupsInPartition, Permission.ScoView +) + +sco_publish( + "/formsemestre_partition_list", + sco_groups.formsemestre_partition_list, + Permission.ScoView, +) + +sco_publish("/setGroups", sco_groups.setGroups, Permission.ScoView) + +sco_publish("/createGroup", sco_groups.createGroup, Permission.ScoView) + +sco_publish("/suppressGroup", sco_groups.suppressGroup, Permission.ScoView) + +sco_publish("/group_set_name", sco_groups.group_set_name, Permission.ScoView) + +sco_publish("/group_rename", sco_groups.group_rename, Permission.ScoView) + +sco_publish( + "/groups_auto_repartition", sco_groups.groups_auto_repartition, Permission.ScoView +) + +sco_publish("/editPartitionForm", sco_groups.editPartitionForm, Permission.ScoView) + +sco_publish("/partition_delete", sco_groups.partition_delete, Permission.ScoView) + +sco_publish("/partition_set_attr", sco_groups.partition_set_attr, Permission.ScoView) + +sco_publish("/partition_move", sco_groups.partition_move, Permission.ScoView) + +sco_publish("/partition_set_name", sco_groups.partition_set_name, Permission.ScoView) + +sco_publish("/partition_rename", sco_groups.partition_rename, Permission.ScoView) + +sco_publish("/partition_create", sco_groups.partition_create, Permission.ScoView) + +sco_publish("/etud_info_html", sco_page_etud.etud_info_html, Permission.ScoView) + +# --- Gestion des photos: +sco_publish("/get_photo_image", sco_photos.get_photo_image, Permission.ScoView) + +sco_publish("/etud_photo_html", sco_photos.etud_photo_html, Permission.ScoView) + + +@bp.route("/etud_photo_orig_page") +@permission_required(Permission.ScoView) +@scodoc7func(context) +def etud_photo_orig_page(context, etudid=None, REQUEST=None): + "Page with photo in orig. size" + etud = context.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0] + H = [ + context.sco_header(REQUEST, page_title=etud["nomprenom"]), + "
Photo actuelle (%(photoloc)s): + """ + % etud, + sco_photos.etud_photo_html( + context, etud, title="photo actuelle", REQUEST=REQUEST + ), + """
Le fichier ne doit pas dépasser 500Ko (recadrer l'image, format "portrait" de préférence).
+L'image sera automagiquement réduite pour obtenir une hauteur de 90 pixels.
+ """, + ] + tf = TrivialFormulator( + REQUEST.URL0, + REQUEST.form, + ( + ("etudid", {"default": etudid, "input_type": "hidden"}), + ( + "photofile", + {"input_type": "file", "title": "Fichier image", "size": 20}, + ), + ), + submitlabel="Valider", + cancelbutton="Annuler", + ) + dest_url = context.ScoURL() + "/ficheEtud?etudid=" + etud["etudid"] + if tf[0] == 0: + return ( + "\n".join(H) + + tf[1] + + '' + % etudid + + context.sco_footer(REQUEST) + ) + elif tf[0] == -1: + return REQUEST.RESPONSE.redirect(dest_url) + else: + data = tf[2]["photofile"].read() + status, diag = sco_photos.store_photo(context, etud, data, REQUEST=REQUEST) + if status != 0: + return REQUEST.RESPONSE.redirect(dest_url) + else: + H.append('Erreur:' + diag + "
") + return "\n".join(H) + context.sco_footer(REQUEST) + + +@bp.route("/formSuppressPhoto") +@permission_required(Permission.ScoEtudChangeAdr) +@scodoc7func(context) +def formSuppressPhoto(context, etudid=None, REQUEST=None, dialog_confirmed=False): + """Formulaire suppression photo étudiant""" + etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0] + if not dialog_confirmed: + return scu.confirm_dialog( + context, + "Confirmer la suppression de la photo de %(nomprenom)s ?
" % etud, + dest_url="", + REQUEST=REQUEST, + cancel_url="ficheEtud?etudid=%s" % etudid, + parameters={"etudid": etudid}, + ) + + sco_photos.suppress_photo(context, etud, REQUEST=REQUEST) + + return REQUEST.RESPONSE.redirect( + context.ScoURL() + "/ficheEtud?etudid=" + etud["etudid"] + ) + + +# +@bp.route("/formDem") +@permission_required(Permission.ScoEtudInscrit) +@scodoc7func(context) +def formDem(context, etudid, formsemestre_id, REQUEST): + "Formulaire Démission Etudiant" + return context._formDem_of_Def( + etudid, + formsemestre_id, + REQUEST=REQUEST, + operation_name="Démission", + operation_method="doDemEtudiant", + ) + + +@bp.route("/formDef") +@permission_required(Permission.ScoEtudInscrit) +@scodoc7func(context) +def formDef(context, etudid, formsemestre_id, REQUEST): + "Formulaire Défaillance Etudiant" + return context._formDem_of_Def( + etudid, + formsemestre_id, + REQUEST=REQUEST, + operation_name="Défaillance", + operation_method="doDefEtudiant", + ) + + +def _formDem_of_Def( + context, + etudid, + formsemestre_id, + REQUEST=None, + operation_name="", + operation_method="", +): + "Formulaire démission ou défaillance Etudiant" + etud = context.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0] + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + if sem["etat"] != "1": + raise ScoValueError("Modification impossible: semestre verrouille") + + etud["formsemestre_id"] = formsemestre_id + etud["semtitre"] = sem["titremois"] + etud["nowdmy"] = time.strftime("%d/%m/%Y") + etud["operation_name"] = operation_name + # + header = context.sco_header( + REQUEST, + page_title="%(operation_name)s de %(nomprenom)s (du semestre %(semtitre)s)" + % etud, + ) + H = [ + '' + % etud + ] + H.append( + """
""" + % etud + ) + return header + "\n".join(H) + context.sco_footer(REQUEST) + + +@bp.route("/doDemEtudiant") +@permission_required(Permission.ScoEtudInscrit) +@scodoc7func(context) +def doDemEtudiant(context, etudid, formsemestre_id, event_date=None, REQUEST=None): + "Déclare la démission d'un etudiant dans le semestre" + return context._doDem_or_Def_Etudiant( + etudid, + formsemestre_id, + event_date=event_date, + etat_new="D", + operation_method="demEtudiant", + event_type="DEMISSION", + REQUEST=REQUEST, + ) + + +@bp.route("/doDefEtudiant") +@permission_required(Permission.ScoEtudInscrit) +@scodoc7func(context) +def doDefEtudiant(context, etudid, formsemestre_id, event_date=None, REQUEST=None): + "Déclare la défaillance d'un etudiant dans le semestre" + return context._doDem_or_Def_Etudiant( + etudid, + formsemestre_id, + event_date=event_date, + etat_new=sco_codes_parcours.DEF, + operation_method="defailleEtudiant", + event_type="DEFAILLANCE", + REQUEST=REQUEST, + ) + + +def _doDem_or_Def_Etudiant( + context, + etudid, + formsemestre_id, + event_date=None, + etat_new="D", # 'D' or DEF + operation_method="demEtudiant", + event_type="DEMISSION", + REQUEST=None, +): + "Démission ou défaillance d'un étudiant" + # marque 'D' ou DEF dans l'inscription au semestre et ajoute + # un "evenement" scolarite + cnx = context.GetDBConnexion() + # check lock + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + if sem["etat"] != "1": + raise ScoValueError("Modification impossible: semestre verrouille") + # + ins = context.Notes.do_formsemestre_inscription_list( + {"etudid": etudid, "formsemestre_id": formsemestre_id} + )[0] + if not ins: + raise ScoException("etudiant non inscrit ?!") + ins["etat"] = etat_new + context.Notes.do_formsemestre_inscription_edit( + args=ins, formsemestre_id=formsemestre_id + ) + logdb(REQUEST, cnx, method=operation_method, etudid=etudid) + scolars.scolar_events_create( + cnx, + args={ + "etudid": etudid, + "event_date": event_date, + "formsemestre_id": formsemestre_id, + "event_type": event_type, + }, + ) + if REQUEST: + return REQUEST.RESPONSE.redirect("ficheEtud?etudid=" + etudid) + + +@bp.route("/doCancelDem") +@permission_required(Permission.ScoEtudInscrit) +@scodoc7func(context) +def doCancelDem( + context, etudid, formsemestre_id, dialog_confirmed=False, args=None, REQUEST=None +): + "Annule une démission" + return context._doCancelDem_or_Def( + etudid, + formsemestre_id, + dialog_confirmed=dialog_confirmed, + args=args, + operation_name="démission", + etat_current="D", + etat_new="I", + operation_method="cancelDem", + event_type="DEMISSION", + REQUEST=REQUEST, + ) + + +@bp.route("/doCancelDef") +@permission_required(Permission.ScoEtudInscrit) +@scodoc7func(context) +def doCancelDef( + context, etudid, formsemestre_id, dialog_confirmed=False, args=None, REQUEST=None +): + "Annule la défaillance de l'étudiant" + return context._doCancelDem_or_Def( + etudid, + formsemestre_id, + dialog_confirmed=dialog_confirmed, + args=args, + operation_name="défaillance", + etat_current=sco_codes_parcours.DEF, + etat_new="I", + operation_method="cancelDef", + event_type="DEFAILLANCE", + REQUEST=REQUEST, + ) + + +def _doCancelDem_or_Def( + context, + etudid, + formsemestre_id, + dialog_confirmed=False, + args=None, + operation_name="", # "démission" ou "défaillance" + etat_current="D", + etat_new="I", + operation_method="cancelDem", + event_type="DEMISSION", + REQUEST=None, +): + "Annule une demission ou une défaillance" + # check lock + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + if sem["etat"] != "1": + raise ScoValueError("Modification impossible: semestre verrouille") + # verif + info = context.getEtudInfo(etudid, filled=True)[0] + ok = False + for i in info["ins"]: + if i["formsemestre_id"] == formsemestre_id: + if i["etat"] != etat_current: + raise ScoValueError("etudiant non %s !" % operation_name) + ok = True + break + if not ok: + raise ScoValueError("etudiant non inscrit ???") + if not dialog_confirmed: + return scu.confirm_dialog( + context, + "Confirmer l'annulation de la %s ?
" % operation_name, + dest_url="", + REQUEST=REQUEST, + cancel_url="ficheEtud?etudid=%s" % etudid, + parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, + ) + # + ins = context.Notes.do_formsemestre_inscription_list( + {"etudid": etudid, "formsemestre_id": formsemestre_id} + )[0] + if ins["etat"] != etat_current: + raise ScoException("etudiant non %s !!!" % etat_current) # obviously a bug + ins["etat"] = etat_new + cnx = context.GetDBConnexion() + context.Notes.do_formsemestre_inscription_edit( + args=ins, formsemestre_id=formsemestre_id + ) + logdb(REQUEST, cnx, method=operation_method, etudid=etudid) + cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + cursor.execute( + "delete from scolar_events where etudid=%(etudid)s and formsemestre_id=%(formsemestre_id)s and event_type='" + + event_type + + "'", + {"etudid": etudid, "formsemestre_id": formsemestre_id}, + ) + cnx.commit() + return REQUEST.RESPONSE.redirect("ficheEtud?etudid=%s" % etudid) + + +@bp.route("/etudident_create_form") +@permission_required(Permission.ScoEtudInscrit) +@scodoc7func(context) +def etudident_create_form(context, REQUEST=None): + "formulaire creation individuelle etudiant" + return context._etudident_create_or_edit_form(REQUEST, edit=False) + + +@bp.route("/etudident_edit_form") +@permission_required(Permission.ScoEtudInscrit) +@scodoc7func(context) +def etudident_edit_form(context, REQUEST=None): + "formulaire edition individuelle etudiant" + return context._etudident_create_or_edit_form(REQUEST, edit=True) + + +def _etudident_create_or_edit_form(context, REQUEST, edit): + "Le formulaire HTML" + H = [context.sco_header(REQUEST, init_jquery_ui=True)] + F = context.sco_footer(REQUEST) + etudid = REQUEST.form.get("etudid", None) + cnx = context.GetDBConnexion() + descr = [] + if not edit: + # creation nouvel etudiant + initvalues = {} + submitlabel = "Ajouter cet étudiant" + H.append( + """En général, il est recommandé d'importer les étudiants depuis Apogée. + N'utilisez ce formulaire que pour les cas particuliers ou si votre établissement + n'utilise pas d'autre logiciel de gestion des inscriptions.
+L'étudiant créé ne sera pas inscrit. + Pensez à l'inscrire dans un semestre !
+ """ + ) + else: + # edition donnees d'un etudiant existant + # setup form init values + if not etudid: + raise ValueError("missing etudid parameter") + descr.append(("etudid", {"default": etudid, "input_type": "hidden"})) + H.append( + '%s
+ %s +Pas d'informations d'Apogée
" + A + F + elif tf[0] == -1: + return "\n".join(H) + tf[1] + "
" + A + F + # return '\n'.join(H) + '
" + + A + + F + ) + # log('NbHomonyms=%s' % NbHomonyms) + if not tf[2]["dont_check_homonyms"] and NbHomonyms > 0: + return ( + "\n".join(H) + + tf_error_message( + """Attention: il y a déjà un étudiant portant des noms et prénoms proches. Vous pouvez forcer la présence d'un homonyme en cochant "autoriser les homonymes" en bas du formulaire.""" + ) + + tf[1] + + "
" + + A + + F + ) + + if not edit: + etud = scolars.create_etud(context, cnx, args=tf[2], REQUEST=REQUEST) + etudid = etud["etudid"] + else: + # modif d'un etudiant + scolars.etudident_edit(cnx, tf[2], context=context, REQUEST=REQUEST) + etud = scolars.etudident_list(cnx, {"etudid": etudid})[0] + context.fillEtudsInfo([etud]) + # Inval semesters with this student: + to_inval = [s["formsemestre_id"] for s in etud["sems"]] + if to_inval: + context.Notes._inval_cache( + formsemestre_id_list=to_inval + ) # > etudident_create_or_edit + # + return REQUEST.RESPONSE.redirect("ficheEtud?etudid=" + etudid) + + +@bp.route("/etudident_delete") +@permission_required(Permission.ScoEtudInscrit) +@scodoc7func(context) +def etudident_delete(context, etudid, dialog_confirmed=False, REQUEST=None): + "Delete a student" + cnx = context.GetDBConnexion() + etuds = scolars.etudident_list(cnx, {"etudid": etudid}) + if not etuds: + raise ScoValueError("Etudiant inexistant !") + else: + etud = etuds[0] + context.fillEtudsInfo([etud]) + if not dialog_confirmed: + return scu.confirm_dialog( + context, + """
Prenez le temps de vérifier que vous devez vraiment supprimer cet étudiant !
+Cette opération irréversible efface toute trace de l'étudiant: inscriptions, notes, absences... dans tous les semestres qu'il a fréquenté.
+Dans la plupart des cas, vous avez seulement besoin de le
Vérifier la fiche de %(nomprenom)s +
""" + % etud, + dest_url="", + REQUEST=REQUEST, + cancel_url="ficheEtud?etudid=%s" % etudid, + OK="Supprimer définitivement cet étudiant", + parameters={"etudid": etudid}, + ) + log("etudident_delete: etudid=%(etudid)s nomprenom=%(nomprenom)s" % etud) + # delete in all tables ! + tables = [ + "notes_appreciations", + "scolar_autorisation_inscription", + "scolar_formsemestre_validation", + "scolar_events", + "notes_notes_log", + "notes_notes", + "notes_moduleimpl_inscription", + "notes_formsemestre_inscription", + "group_membership", + "entreprise_contact", + "etud_annotations", + "scolog", + "admissions", + "adresse", + "absences", + "billet_absence", + "identite", + ] + cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + for table in tables: + cursor.execute("delete from %s where etudid=%%(etudid)s" % table, etud) + cnx.commit() + # Inval semestres où il était inscrit: + to_inval = [s["formsemestre_id"] for s in etud["sems"]] + if to_inval: + context.Notes._inval_cache(formsemestre_id_list=to_inval) # > + return REQUEST.RESPONSE.redirect( + context.ScoURL() + r"?head_message=Etudiant%20supprimé" + ) + + +@bp.route("/check_group_apogee") +@permission_required(Permission.ScoEtudInscrit) +@scodoc7func(context) +def check_group_apogee( + context, group_id, REQUEST=None, etat=None, fix=False, fixmail=False +): + """Verification des codes Apogee et mail de tout un groupe. + Si fix == True, change les codes avec Apogée. + + XXX A re-écrire pour API 2: prendre liste dans l'étape et vérifier à partir de cela. + """ + etat = etat or None + members, group, _, sem, _ = sco_groups.get_group_infos(context, group_id, etat=etat) + formsemestre_id = group["formsemestre_id"] + + cnx = context.GetDBConnexion() + H = [ + context.Notes.html_sem_header( + REQUEST, "Etudiants du %s" % (group["group_name"] or "semestre"), sem + ), + 'Nom | Nom usuel | Prénom | NIP (ScoDoc) | Apogée | |
---|---|---|---|---|---|
%s | %s | %s | %s | %s | %s |
Retour au semestre + """ + % ( + REQUEST.URL0, + formsemestre_id, + scu.strnone(group_id), + scu.strnone(etat), + formsemestre_id, + ) + ) + H.append( + """ +
+Retour au semestre + """ + % ( + REQUEST.URL0, + formsemestre_id, + scu.strnone(group_id), + scu.strnone(etat), + formsemestre_id, + ) + ) + + return "\n".join(H) + context.sco_footer(REQUEST) + + +@bp.route("/form_students_import_excel") +@permission_required(Permission.ScoEtudInscrit) +@scodoc7func(context) +def form_students_import_excel(context, REQUEST, formsemestre_id=None): + "formulaire import xls" + if formsemestre_id: + sem = sco_formsemestre.get_formsemestre(context.Notes, formsemestre_id) + dest_url = ( + context.ScoURL() + + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id + ) + else: + sem = None + dest_url = context.ScoURL() + if sem and sem["etat"] != "1": + raise ScoValueError("Modification impossible: semestre verrouille") + H = [ + context.sco_header(REQUEST, page_title="Import etudiants"), + """
A utiliser pour importer de nouveaux étudiants (typiquement au + premier semestre).
+Si les étudiants à inscrire sont déjà dans un autre + semestre, utiliser le menu "Inscriptions (passage des étudiants) + depuis d'autres semestres à partir du semestre destination. +
+Si vous avez un portail Apogée, il est en général préférable d'importer les + étudiants depuis Apogée, via le menu "Synchroniser avec étape Apogée". +
++ L'opération se déroule en deux étapes. Dans un premier temps, + vous téléchargez une feuille Excel type. Vous devez remplir + cette feuille, une ligne décrivant chaque étudiant. Ensuite, + vous indiquez le nom de votre fichier dans la case "Fichier Excel" + ci-dessous, et cliquez sur "Télécharger" pour envoyer au serveur + votre liste. +
+ """, + ] # ' + if sem: + H.append( + """Les étudiants importés seront inscrits dans + le semestre %s
""" + % sem["titremois"] + ) + else: + H.append( + """ +Pour inscrire directement les étudiants dans un semestre de + formation, il suffit d'indiquer le code de ce semestre + (qui doit avoir été créé au préalable). Cliquez ici pour afficher les codes +
+ """ + % (context.ScoURL()) + ) + + H.append("""Le fichier Excel décrivant les étudiants doit comporter les colonnes suivantes. +
Les colonnes peuvent être placées dans n'importe quel ordre, mais +le titre exact (tel que ci-dessous) doit être sur la première ligne. +
++Les champs avec un astérisque (*) doivent être présents (nulls non autorisés). +
+ + ++
Attribut | Type | Description | |
%s | %s | %s | %s |