diff --git a/README.md b/README.md index 828b5ec998..73488544cd 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,27 @@ Installer le bon vieux `pyExcelerator` dans l'environnement: python -m unittest tests.test_users +# Work in Progress + +## Migration ZScolar + +### Méthodes qui ne devraient plus être publiées: +security.declareProtected(ScoView, "get_preferences") + +def get_preferences(context, formsemestre_id=None): + "Get preferences for this instance (a dict-like instance)" + return sco_preferences.sem_preferences(context, formsemestre_id) + +security.declareProtected(ScoView, "get_preference") + +def get_preference(context, name, formsemestre_id=None): + """Returns value of named preference. + All preferences have a sensible default value (see sco_preferences.py), + this function always returns a usable value for all defined preferences names. + """ + return sco_preferences.get_base_preferences(context).get(formsemestre_id, name) + + diff --git a/app/scodoc/ZAbsences.py b/app/scodoc/ZAbsences.py index 91aaf7eb7f..9e7ae81345 100644 --- a/app/scodoc/ZAbsences.py +++ b/app/scodoc/ZAbsences.py @@ -904,15 +904,16 @@ class ZAbsences( 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_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 + base_url = base_url_noweeks + "&nbweeks=%s" % nbweeks # sans le moduleimpl_id if etuds: nt = self.Notes._getNotesCache().get_NotesTable(self.Notes, formsemestre_id) diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index 2650f0e92e..28fdae366c 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -557,7 +557,8 @@ def formsemestre_delete_archive( dest_url = "formsemestre_list_archives?formsemestre_id=%s" % (formsemestre_id) if not dialog_confirmed: - return context.confirmDialog( + return scu.confirm_dialog( + context, """
La suppression sera définitive.
""" % PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"), diff --git a/app/scodoc/sco_archives_etud.py b/app/scodoc/sco_archives_etud.py index 39eca7fdd9..1469a43b67 100644 --- a/app/scodoc/sco_archives_etud.py +++ b/app/scodoc/sco_archives_etud.py @@ -201,7 +201,8 @@ def etud_delete_archive(context, REQUEST, etudid, archive_name, dialog_confirmed archive_id = EtudsArchive.get_id_from_name(context, etudid, archive_name) dest_url = "ficheEtud?etudid=%s" % etudid if not dialog_confirmed: - return context.confirmDialog( + return scu.confirm_dialog( + context, """Fichier associé le %s à l'étudiant %s
La suppression sera définitive.
""" diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index 6d249230f5..b194774402 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -65,7 +65,8 @@ def formation_delete(context, formation_id=None, dialog_confirmed=False, REQUEST H.append('' % context.NotesURL()) else: if not dialog_confirmed: - return context.confirmDialog( + return scu.confirm_dialog( + context, """Attention: la suppression d'une formation est irréversible et implique la supression de toutes les UE, matières et modules de la formation !
diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 452b12a6db..0a977afe70 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -267,7 +267,8 @@ def ue_delete( ue = ue[0] if not dialog_confirmed: - return context.confirmDialog( + return scu.confirm_dialog( + context, "La suppression sera définitive.
""" % (etape_apo,), diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py index 95044dcbe6..12aa05a101 100644 --- a/app/scodoc/sco_exceptions.py +++ b/app/scodoc/sco_exceptions.py @@ -66,6 +66,12 @@ class FormatError(ScoValueError): pass +class ScoInvalidDept(ScoValueError): + """departement invalide""" + + pass + + class ScoConfigurationError(ScoValueError): """Configuration invalid""" diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 2c05a600f5..927a2829e4 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -1129,7 +1129,8 @@ def formsemestre_associate_new_version( % (s["formsemestre_id"], checked, disabled, s["titremois"]) ) - return context.confirmDialog( + return scu.confirm_dialog( + context, """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)
""", dest_url="", REQUEST=REQUEST, @@ -1423,7 +1425,8 @@ def formsemestre_change_lock( msg = "déverrouillage" else: msg = "verrouillage" - return context.confirmDialog( + return scu.confirm_dialog( + context, "+ %(label)s + | + (format tableur) + Photos + | """ + % group + ) + H.append("(%d étudiants) | " % n_members) + + if with_absences: + H.append(FormAbs % group) + + H.append("
Aucun groupe dans cette partition') + if sco_groups.can_change_groups(context, REQUEST, formsemestre_id): + H.append( + ' (créer)' + % partition["partition_id"] + ) + H.append("
") + if sco_groups.can_change_groups(context, REQUEST, formsemestre_id): + H.append( + 'Les groupes %s de cette partition seront supprimés
""" diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index d1fd088559..e1f9ac24c2 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -340,7 +340,8 @@ def formsemestre_inscr_passage( if not a_inscrire and not a_desinscrire: H.append("""Afficher les résultats en pourcentages
' @@ -1028,7 +1041,7 @@ def tsp_etud_list( 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)) ): etuds.append(etud) diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 5309ee9cd0..98fc06bb11 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -349,7 +349,8 @@ def do_evaluation_set_missing( ) # Confirm action if not dialog_confirmed: - return context.confirmDialog( + return scu.confirm_dialog( + context, """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 ?
" % nb_suppress if existing_decisions: msg += """Important: il y a déjà des décisions de jury enregistrées, qui seront potentiellement à revoir suite à cette modification !
""" - return context.confirmDialog( + return scu.confirm_dialog( + context, msg, dest_url="", REQUEST=REQUEST, diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index d0e3aa9b7a..4e09f58f09 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -362,7 +362,8 @@ def do_semset_delete(context, semset_id, dialog_confirmed=False, REQUEST=None): raise ScoValueError("empty semset_id") s = SemSet(context, semset_id=semset_id) if not dialog_confirmed: - return context.confirmDialog( + return scu.confirm_dialog( + context, "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)
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 4a78f77835..927109347f 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -840,7 +840,25 @@ def log_unknown_etud(context, REQUEST=None, format="html"): "unknown student: etudid=%s code_nip=%s code_ine=%s" % (etudid, code_nip, code_ine) ) - return context.ScoErrorResponse("unknown student", format=format, REQUEST=REQUEST) + return _sco_error_response("unknown student", format=format, REQUEST=REQUEST) + + +# XXX #sco8 à tester ou ré-écrire +def _sco_error_response(context, msg, format="html", REQUEST=None): + """Send an error message to the client, in html or xml format.""" + REQUEST.RESPONSE.setStatus(404, reason=msg) + if format == "html" or format == "pdf": + raise ScoValueError(msg) + elif format == "xml": + REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) + doc = jaxml.XML_document(encoding=scu.SCO_ENCODING) + doc.error(msg=msg) + return repr(doc) + elif format == "json": + REQUEST.RESPONSE.setHeader("content-type", scu.JSON_MIMETYPE) + return "undefined" # XXX voir quoi faire en cas d'erreur json + else: + raise ValueError("ScoErrorResponse: invalid format") # XXX @@ -869,3 +887,55 @@ def return_text_if_published(val, REQUEST): if REQUEST and not isinstance(val, STRING_TYPES): return sendJSON(REQUEST, val) return val + + +def confirm_dialog( + context, + message="Confirmer ?
", + OK="OK", + Cancel="Annuler", + dest_url="", + cancel_url="", + target_variable="dialog_confirmed", + parameters={}, + add_headers=True, # complete page + REQUEST=None, # required + helpmsg=None, +): + # dialog de confirmation simple + parameters[target_variable] = 1 + # Attention: la page a pu etre servie en GET avec des parametres + # si on laisse l'url "action" vide, les parametres restent alors que l'on passe en POST... + if not dest_url: + dest_url = REQUEST.URL + # strip remaining parameters from destination url: + dest_url = urllib.splitquery(dest_url)[0] + H = [ + """") + if helpmsg: + H.append('' + helpmsg + "
") + if add_headers and REQUEST: + return context.sco_header(REQUEST) + "\n".join(H) + context.sco_footer(REQUEST) + else: + return "\n".join(H) diff --git a/app/scodoc/scolars.py b/app/scodoc/scolars.py index 05ebade477..db991a285a 100644 --- a/app/scodoc/scolars.py +++ b/app/scodoc/scolars.py @@ -357,7 +357,8 @@ def _check_duplicate_code(cnx, args, code_name, context, edit=True, REQUEST=None dest_url = "" parameters = {} if context: - err_page = context.confirmDialog( + err_page = scu.confirm_dialog( + context, message="""ZAbsences ScoDoc 8
-g.scodoc_dept=%(scodoc_dept)s
- - - """ % { - "scodoc_dept": g.scodoc_dept, +def AnnuleAbsencesDatesNoJust(context, etudid, dates, moduleimpl_id=None, REQUEST=None): + """Supprime les absences aux dates indiquées + mais ne supprime pas les justificatifs. + """ + # log('AnnuleAbsencesDatesNoJust: moduleimpl_id=%s' % moduleimpl_id) + if not dates: + return + date0 = dates[0] + if len(date0.split(":")) == 2: + # am/pm is present + for date in dates: + jour, ampm = date.split(":") + if ampm == "am": + matin = 1 + elif ampm == "pm": + matin = 0 + else: + raise ValueError("invalid ampm !") + context._AnnuleAbsence(etudid, jour, matin, moduleimpl_id, REQUEST) + return + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) + # supr les absences non justifiees + for date in dates: + cursor.execute( + "delete from absences where etudid=%(etudid)s and (not estjust) and jour=%(date)s and moduleimpl_id=%(moduleimpl_id)s", + vars(), + ) + sco_abs.invalidateAbsEtudDate(context, etudid, date) + # s'assure que les justificatifs ne sont pas "absents" + for date in dates: + cursor.execute( + "update absences set estabs=FALSE where etudid=%(etudid)s and jour=%(date)s and moduleimpl_id=%(moduleimpl_id)s", + vars(), + ) + if dates: + date0 = dates[0] + else: + date0 = None + if len(dates) > 1: + date1 = dates[1] + else: + date1 = None + logdb( + REQUEST, + cnx, + "AnnuleAbsencesDatesNoJust", + etudid=etudid, + msg="%s - %s - %s" % (date0, date1, moduleimpl_id), + ) + cnx.commit() + + +def _list_abs_in_range( + context, etudid, debut, fin, matin=None, moduleimpl_id=None, cursor=None +): + """Liste des absences entre deux dates. + + Args: + etudid + debut string iso date ("2020-03-12") + end string iso date ("2020-03-12") + matin None, True, False + moduleimpl_id + """ + if matin != None: + matin = _toboolean(matin) + ismatin = " AND A.MATIN = %(matin)s " + else: + ismatin = "" + if moduleimpl_id: + modul = " AND A.MODULEIMPL_ID = %(moduleimpl_id)s " + else: + modul = "" + if not cursor: + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) + cursor.execute( + """ +SELECT DISTINCT A.JOUR, A.MATIN +FROM ABSENCES A +WHERE A.ETUDID = %(etudid)s + AND A.ESTABS""" + + ismatin + + modul + + """ + AND A.JOUR BETWEEN %(debut)s AND %(fin)s + """, + vars(), + ) + res = cursor.dictfetchall() + return res + + +@bp.route("/CountAbs") +@permission_required(Permission.ScoView) +@scodoc7func(context) +def CountAbs(context, etudid, debut, fin, matin=None, moduleimpl_id=None): + """CountAbs + matin= 1 ou 0. + + Returns: + An integer. + """ + return len( + _list_abs_in_range( + context, etudid, debut, fin, matin=matin, moduleimpl_id=moduleimpl_id + ) + ) + + +@bp.route("/CountAbsJust") +@permission_required(Permission.ScoView) +@scodoc7func(context) +def CountAbsJust(context, etudid, debut, fin, matin=None, moduleimpl_id=None): + "Count just. abs" + if matin != None: + matin = _toboolean(matin) + ismatin = " AND A.MATIN = %(matin)s " + else: + ismatin = "" + if moduleimpl_id: + modul = " AND A.MODULEIMPL_ID = %(moduleimpl_id)s " + else: + modul = "" + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) + cursor.execute( + """SELECT COUNT(*) AS NbAbsJust FROM ( +SELECT DISTINCT A.JOUR, A.MATIN +FROM ABSENCES A, ABSENCES B +WHERE A.ETUDID = %(etudid)s + AND A.ETUDID = B.ETUDID + AND A.JOUR = B.JOUR AND A.MATIN = B.MATIN + AND A.JOUR BETWEEN %(debut)s AND %(fin)s + AND A.ESTABS AND (A.ESTJUST OR B.ESTJUST)""" + + ismatin + + modul + + """ +) AS tmp + """, + vars(), + ) + res = cursor.fetchone()[0] + return res + + +def _ListeAbsDate(context, etudid, beg_date, end_date): + # Liste des absences et justifs entre deux dates + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) + cursor.execute( + """SELECT jour, matin, estabs, estjust, description FROM ABSENCES A + WHERE A.ETUDID = %(etudid)s + AND A.jour >= %(beg_date)s + AND A.jour <= %(end_date)s + """, + vars(), + ) + Abs = cursor.dictfetchall() + # remove duplicates + A = {} # { (jour, matin) : abs } + for a in Abs: + jour, matin = a["jour"], a["matin"] + if (jour, matin) in A: + # garde toujours la description + a["description"] = a["description"] or A[(jour, matin)]["description"] + # et la justif: + a["estjust"] = a["estjust"] or A[(jour, matin)]["estjust"] + a["estabs"] = a["estabs"] or A[(jour, matin)]["estabs"] + A[(jour, matin)] = a + else: + A[(jour, matin)] = a + if A[(jour, matin)]["description"] is None: + A[(jour, matin)]["description"] = "" + # add hours: matin = 8:00 - 12:00, apresmidi = 12:00 - 18:00 + dat = "%04d-%02d-%02d" % (a["jour"].year, a["jour"].month, a["jour"].day) + if a["matin"]: + A[(jour, matin)]["begin"] = dat + " 08:00:00" + A[(jour, matin)]["end"] = dat + " 11:59:59" + else: + A[(jour, matin)]["begin"] = dat + " 12:00:00" + A[(jour, matin)]["end"] = dat + " 17:59:59" + # sort + R = A.values() + R.sort(key=lambda x: (x["begin"])) + return R + + +@bp.route("/ListeAbsJust") +@permission_required(Permission.ScoView) +@scodoc7func(context) +def ListeAbsJust(context, etudid, datedebut): + "Liste des absences justifiees (par ordre chronologique)" + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) + cursor.execute( + """SELECT DISTINCT A.ETUDID, A.JOUR, A.MATIN FROM ABSENCES A, ABSENCES B +WHERE A.ETUDID = %(etudid)s +AND A.ETUDID = B.ETUDID +AND A.JOUR = B.JOUR AND A.MATIN = B.MATIN AND A.JOUR >= %(datedebut)s +AND A.ESTABS AND (A.ESTJUST OR B.ESTJUST) +ORDER BY A.JOUR + """, + vars(), + ) + A = cursor.dictfetchall() + for a in A: + a["description"] = context._GetAbsDescription(a, cursor=cursor) + return A + + +@bp.route("/ListeAbsNonJust") +@permission_required(Permission.ScoView) +@scodoc7func(context) +def ListeAbsNonJust(context, etudid, datedebut): + "Liste des absences NON justifiees (par ordre chronologique)" + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) + cursor.execute( + """SELECT ETUDID, JOUR, MATIN FROM ABSENCES A +WHERE A.ETUDID = %(etudid)s +AND A.estabs +AND A.jour >= %(datedebut)s +EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B +WHERE B.estjust +AND B.ETUDID = %(etudid)s +ORDER BY JOUR + """, + vars(), + ) + A = cursor.dictfetchall() + for a in A: + a["description"] = context._GetAbsDescription(a, cursor=cursor) + return A + + +@bp.route("/ListeJustifs") +@permission_required(Permission.ScoView) +@scodoc7func(context) +def ListeJustifs(context, etudid, datedebut, datefin=None, only_no_abs=False): + """Liste des justificatifs (sans absence relevée) à partir d'une date, + ou, si datefin spécifié, entre deux dates. + Si only_no_abs: seulement les justificatifs correspondant aux jours sans absences relevées. + """ + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) + req = """SELECT DISTINCT ETUDID, JOUR, MATIN FROM ABSENCES A +WHERE A.ETUDID = %(etudid)s +AND A.ESTJUST +AND A.JOUR >= %(datedebut)s""" + if datefin: + req += """AND A.JOUR <= %(datefin)s""" + if only_no_abs: + req += """ +EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B +WHERE B.estabs +AND B.ETUDID = %(etudid)s + """ + cursor.execute(req, vars()) + A = cursor.dictfetchall() + for a in A: + a["description"] = context._GetAbsDescription(a, cursor=cursor) + + return A + + +def _GetAbsDescription(context, a, cursor=None): + "Description associee a l'absence" + if not cursor: + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) + a = a.copy() + # a['jour'] = a['jour'].date() + if a["matin"]: # devrait etre booleen... :-( + a["matin"] = True + else: + a["matin"] = False + cursor.execute( + """select * from absences where etudid=%(etudid)s and jour=%(jour)s and matin=%(matin)s order by entry_date desc""", + a, + ) + A = cursor.dictfetchall() + desc = None + module = "" + for a in A: + if a["description"]: + desc = a["description"] + if a["moduleimpl_id"] and a["moduleimpl_id"] != "NULL": + # Trouver le nom du module + Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( + context.Notes, moduleimpl_id=a["moduleimpl_id"] + ) + if Mlist: + M = Mlist[0] + module += "%s " % M["module"]["code"] + + if desc: + return "(%s) %s" % (desc, module) + if module: + return module + return "" + + +@bp.route("/ListeAbsJour") +@permission_required(Permission.ScoView) +@scodoc7func(context) +def ListeAbsJour(context, date, am=True, pm=True, is_abs=True, is_just=None): + """Liste des absences et/ou justificatifs ce jour. + is_abs: None (peu importe), True, False + is_just: idem + """ + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) + req = """SELECT DISTINCT etudid, jour, matin FROM ABSENCES A +WHERE A.jour = %(date)s +""" + if is_abs != None: + req += " AND A.estabs = %(is_abs)s" + if is_just != None: + req += " AND A.estjust = %(is_just)s" + if not am: + req += " AND NOT matin " + if not pm: + req += " AND matin" + + cursor.execute(req, {"date": date, "is_just": is_just, "is_abs": is_abs}) + A = cursor.dictfetchall() + for a in A: + a["description"] = context._GetAbsDescription(a, cursor=cursor) + return A + + +@bp.route("/ListeAbsNonJustJour") +@permission_required(Permission.ScoView) +@scodoc7func(context) +def ListeAbsNonJustJour(context, date, am=True, pm=True): + "Liste des absences non justifiees ce jour" + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) + reqa = "" + if not am: + reqa += " AND NOT matin " + if not pm: + reqa += " AND matin " + req = ( + """SELECT etudid, jour, matin FROM ABSENCES A +WHERE A.estabs +AND A.jour = %(date)s +""" + + reqa + + """EXCEPT SELECT etudid, jour, matin FROM ABSENCES B +WHERE B.estjust AND B.jour = %(date)s""" + + reqa + ) + + cursor.execute(req, {"date": date}) + A = cursor.dictfetchall() + for a in A: + a["description"] = context._GetAbsDescription(a, cursor=cursor) + return A + + +@bp.route("/doSignaleAbsenceGrSemestre") +@permission_required(Permission.ScoAbsChange) +@scodoc7func(context) +def doSignaleAbsenceGrSemestre( + context, + moduleimpl_id=None, + abslist=[], + dates="", + etudids="", + destination=None, + REQUEST=None, +): + """Enregistre absences aux dates indiquees (abslist et dates). + dates est une liste de dates ISO (séparées par des ','). + Efface les absences aux dates indiquées par dates, + ou bien ajoute celles de abslist. + """ + if etudids: + etudids = etudids.split(",") + else: + etudids = [] + if dates: + dates = dates.split(",") + else: + dates = [] + + # 1- Efface les absences + if dates: + for etudid in etudids: + context.AnnuleAbsencesDatesNoJust(etudid, dates, moduleimpl_id, REQUEST) + return "Absences effacées" + + # 2- Ajoute les absences + if abslist: + context._add_abslist(abslist, REQUEST, moduleimpl_id) + return "Absences ajoutées" + + return "" + + +def _add_abslist(context, abslist, REQUEST, moduleimpl_id=None): + for a in abslist: + etudid, jour, ampm = a.split(":") + if ampm == "am": + matin = 1 + elif ampm == "pm": + matin = 0 + else: + raise ValueError("invalid ampm !") + # ajoute abs si pas deja absent + if context.CountAbs(etudid, jour, jour, matin, moduleimpl_id) == 0: + context._AddAbsence(etudid, jour, matin, 0, REQUEST, "", moduleimpl_id) + + +# --- Misc tools.... ------------------ + + +def _isFarFutur(context, jour): + # check si jour est dans le futur "lointain" + # pour autoriser les saisies dans le futur mais pas a plus de 6 mois + y, m, d = [int(x) for x in jour.split("-")] + j = datetime.date(y, m, d) + # 6 mois ~ 182 jours: + return j - datetime.date.today() > datetime.timedelta(182) + + +# ------------ HTML Interfaces +@bp.route("/SignaleAbsenceGrHebdo") +@permission_required(Permission.ScoAbsChange) +@scodoc7func(context) +def SignaleAbsenceGrHebdo( + context, datelundi, group_ids=[], destination="", moduleimpl_id=None, REQUEST=None +): + "Saisie hebdomadaire des absences" + if not moduleimpl_id: + moduleimpl_id = None + + groups_infos = sco_groups_view.DisplayedGroupsInfos( + context, group_ids, moduleimpl_id=moduleimpl_id, REQUEST=REQUEST + ) + if not groups_infos.members: + return ( + context.sco_header(page_title="Saisie des absences", REQUEST=REQUEST) + + "
+ Saisie des absences %s %s, + semaine du lundi %s+
+
+ """
+ % {"menu_module": menu_module, "url": base_url, "sel": sel}
+ )
+
+ H += context._gen_form_saisie_groupe(
+ etuds, datessem, destination, moduleimpl_id, require_module
+ )
+
+ H.append(context.sco_footer(REQUEST))
+ return "\n".join(H)
+
+
+@bp.route("/SignaleAbsenceGrSemestre")
+@permission_required(Permission.ScoAbsChange)
+@scodoc7func(context)
+def SignaleAbsenceGrSemestre(
+ context,
+ datedebut,
+ datefin,
+ destination="",
+ group_ids=[], # list of groups to display
+ nbweeks=4, # ne montre que les nbweeks dernieres semaines
+ moduleimpl_id=None,
+ REQUEST=None,
+):
+ """Saisie des absences sur une journée sur un semestre (ou intervalle de dates) entier"""
+ groups_infos = sco_groups_view.DisplayedGroupsInfos(
+ context, group_ids, REQUEST=REQUEST
+ )
+ if not groups_infos.members:
+ return (
+ context.sco_header(page_title="Saisie des absences", REQUEST=REQUEST)
+ + "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, + ), + """
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. + + """ + % destination + ) + return H + + +def _TablesAbsEtud( + context, + etudid, + datedebut, + with_evals=True, + format="html", + absjust_only=0, + REQUEST=None, +): + """Tables des absences justifiees et non justifiees d'un étudiant sur l'année en cours""" + absjust = context.ListeAbsJust(etudid=etudid, datedebut=datedebut) + absnonjust = context.ListeAbsNonJust(etudid=etudid, datedebut=datedebut) + # examens ces jours là ? + if with_evals: + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) + for a in absnonjust + absjust: + cursor.execute( + """select eval.* + from notes_evaluation eval, notes_moduleimpl_inscription mi, notes_moduleimpl m + where eval.jour = %(jour)s and eval.moduleimpl_id = m.moduleimpl_id + and mi.moduleimpl_id = m.moduleimpl_id and mi.etudid = %(etudid)s""", + {"jour": a["jour"].strftime("%Y-%m-%d"), "etudid": etudid}, + ) + a["evals"] = cursor.dictfetchall() + cursor.execute( + """SELECT mi.moduleimpl_id + from absences abs, notes_moduleimpl_inscription mi, notes_moduleimpl m + where abs.matin = %(matin)s and abs.jour = %(jour)s and abs.etudid=%(etudid)s and abs.moduleimpl_id=mi.moduleimpl_id and mi.moduleimpl_id=m.moduleimpl_id + and mi.etudid = %(etudid)s""", + { + "matin": bool(a["matin"]), + "jour": a["jour"].strftime("%Y-%m-%d"), + "etudid": etudid, + }, + ) + a["absent"] = cursor.dictfetchall() + + def matin(x): + if x: + return "matin" + else: + return "après-midi" + + def descr_exams(a): + if not a.has_key("evals"): + return "" + ex = [] + for ev in a["evals"]: + mod = sco_moduleimpl.do_moduleimpl_withmodule_list( + context.Notes, moduleimpl_id=ev["moduleimpl_id"] + )[0] + if format == "html": + ex.append( + '%s' + % (mod["moduleimpl_id"], mod["module"]["code"]) + ) + else: + ex.append(mod["module"]["code"]) + if ex: + return ", ".join(ex) + return "" + + def descr_abs(a): + ex = [] + for ev in a.get("absent", []): + mod = sco_moduleimpl.do_moduleimpl_withmodule_list( + context.Notes, moduleimpl_id=ev["moduleimpl_id"] + )[0] + if format == "html": + ex.append( + '%s' + % (mod["moduleimpl_id"], mod["module"]["code"]) + ) + else: + ex.append(mod["module"]["code"]) + if ex: + return ", ".join(ex) + return "" + + # ajoute date formatée et évaluations + for L in (absnonjust, absjust): + for a in L: + if with_evals: + a["exams"] = descr_exams(a) + a["datedmy"] = a["jour"].strftime("%d/%m/%Y") + a["ampm"] = int(a["matin"]) + a["matin"] = matin(a["matin"]) + index = a["description"].find(")") + if index != -1: + a["motif"] = a["description"][1:index] + else: + a["motif"] = "" + a["description"] = descr_abs(a) or "" + + # ajoute lien pour justifier + if format == "html": + for a in absnonjust: + a["justlink"] = "justifier" + a[ + "_justlink_target" + ] = "doJustifAbsence?etudid=%s&datedebut=%s&datefin=%s&demijournee=%s" % ( + etudid, + a["datedmy"], + a["datedmy"], + a["ampm"], + ) + # + titles = { + "datedmy": "Date", + "matin": "", + "exams": "Examens ce jour", + "justlink": "", + "description": "Modules", + "motif": "Motif", + } + columns_ids = ["datedmy", "matin"] + if format in ("json", "xml"): + columns_ids += ["jour", "ampm"] + if with_evals: + columns_ids.append("exams") + + columns_ids.append("description") + columns_ids.append("motif") + if format == "html": + columns_ids.append("justlink") + + return titles, columns_ids, absnonjust, absjust + + +@bp.route("/EtatAbsencesGr") +@permission_required(Permission.ScoView) +@scodoc7func(context) # ported from dtml +def EtatAbsencesGr( + context, + group_ids=[], # list of groups to display + debut="", + fin="", + with_boursier=True, # colonne boursier + format="html", + REQUEST=None, +): + """Liste les absences de groupes""" + datedebut = notesdb.DateDMYtoISO(debut) + datefin = notesdb.DateDMYtoISO(fin) + # Informations sur les groupes à afficher: + groups_infos = sco_groups_view.DisplayedGroupsInfos( + context, group_ids, REQUEST=REQUEST + ) + formsemestre_id = groups_infos.formsemestre_id + sem = groups_infos.formsemestre + + # Construit tableau (etudid, statut, nomprenom, nbJust, nbNonJust, NbTotal) + T = [] + for m in groups_infos.members: + etud = context.getEtudInfo(etudid=m["etudid"], filled=True)[0] + nbabs = context.CountAbs(etudid=etud["etudid"], debut=datedebut, fin=datefin) + nbabsjust = context.CountAbsJust( + etudid=etud["etudid"], debut=datedebut, fin=datefin + ) + nbjustifs_noabs = len( + context.ListeJustifs( + etudid=etud["etudid"], datedebut=datedebut, only_no_abs=True + ) + ) + # retrouve sem dans etud['sems'] + s = None + for s in etud["sems"]: + if s["formsemestre_id"] == formsemestre_id: + break + if not s or s["formsemestre_id"] != formsemestre_id: + raise ValueError( + "EtatAbsencesGr: can't retreive sem" + ) # bug or malicious arg + T.append( + { + "etudid": etud["etudid"], + "etatincursem": s["ins"]["etat"], + "nomprenom": etud["nomprenom"], + "nbabsjust": nbabsjust, + "nbabsnonjust": nbabs - nbabsjust, + "nbabs": nbabs, + "nbjustifs_noabs": nbjustifs_noabs, + "_nomprenom_target": "CalAbs?etudid=%s" % etud["etudid"], + "_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % etud["etudid"], + "boursier": etud["boursier"], + } + ) + if s["ins"]["etat"] == "D": + T[-1]["_css_row_class"] = "etuddem" + T[-1]["nomprenom"] += " (dem)" + columns_ids = [ + "nomprenom", + "nbjustifs_noabs", + "nbabsjust", + "nbabsnonjust", + "nbabs", + ] + if with_boursier: + columns_ids[1:1] = ["boursier"] + if groups_infos.tous_les_etuds_du_sem: + gr_tit = "" + else: + if len(groups_infos.group_ids) > 1: + p = "des groupes" + else: + p = "du groupe" + if format == "html": + h = ' ' + groups_infos.groups_titles + "" + else: + h = groups_infos.groups_titles + gr_tit = p + h + + title = "Etat des absences %s" % gr_tit + if format == "xls" or format == "xml" or format == "json": + columns_ids = ["etudid"] + columns_ids + tab = GenTable( + columns_ids=columns_ids, + rows=T, + preferences=context.get_preferences(formsemestre_id), + titles={ + "etatincursem": "Etat", + "nomprenom": "Nom", + "nbabsjust": "Justifiées", + "nbabsnonjust": "Non justifiées", + "nbabs": "Total", + "nbjustifs_noabs": "Justifs non utilisés", + "boursier": "Bourse", + }, + html_sortable=True, + html_class="table_leftalign", + html_header=context.sco_header( + REQUEST, + page_title=title, + init_qtip=True, + javascripts=["js/etud_info.js"], + ), + html_title=context.Notes.html_sem_header( + REQUEST, "%s" % title, sem, with_page_header=False + ) + + "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
+ou entrez une date pour visualiser les absents un jour donné :
+
+ | Matin | Après-midi |
---|---|---|
+ %(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(" |
%d abs, %d just. | %d abs, %d just. |
Aucune absence !
") + else: + H.append( + """L'étudiant pense pouvoir justifier cette absence.
Vérifiez le justificatif avant d'enregistrer.
Supprimer ce billet (utiliser en cas d'erreur, par ex. billet en double)
""" + % billet_id + ) + F += 'Liste de tous les billets en attente
' + + return "\n".join(H) + "%d étudiants ont validé l'UE %s (%s)
Si vous supprimez cette UE, ces validations vont être supprimées !
" % (len(validations), ue["acronyme"], ue["titre"]), dest_url="", @@ -920,7 +919,8 @@ def do_module_delete(context, oid, REQUEST): # S'il y a des moduleimpls, on ne peut pas detruire le module ! mods = sco_moduleimpl.do_moduleimpl_list(context, module_id=oid) if mods: - err_page = context.confirmDialog( + err_page = scu.confirm_dialog( + context, message="""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 84842dd966..e045eab2b7 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 |