diff --git a/app/api/assiduites.py b/app/api/assiduites.py
index 1abb6479df..2a2dd5e28e 100644
--- a/app/api/assiduites.py
+++ b/app/api/assiduites.py
@@ -1,6 +1,6 @@
##############################################################################
# ScoDoc
-# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
+# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""ScoDoc 9 API : Assiduités
@@ -532,7 +532,7 @@ def assiduite_create(etudid: int = None, nip=None, ine=None):
# On créé l'assiduité
# 200 + obj si réussi
# 404 + message d'erreur si non réussi
- code, obj = _create_one(data, etud)
+ code, obj = create_one_assiduite(data, etud)
if code == 404:
errors.append({"indice": i, "message": obj})
else:
@@ -590,7 +590,7 @@ def assiduites_create():
# route sans département
set_sco_dept(etud.departement.acronym)
- code, obj = _create_one(data, etud)
+ code, obj = create_one_assiduite(data, etud)
if code == 404:
errors.append({"indice": i, "message": obj})
else:
@@ -600,14 +600,14 @@ def assiduites_create():
return {"errors": errors, "success": success}
-def _create_one(
+def create_one_assiduite(
data: dict,
etud: Identite,
) -> tuple[int, object]:
"""
- _create_one Création d'une assiduité à partir d'une représentation JSON
+ create_one_assiduite: création d'une assiduité à partir d'un dict
- Cette fonction vérifie la représentation JSON
+ Cette fonction vérifie les données du dict (qui vient du JSON API ou d'ailleurs)
Puis crée l'assiduité si la représentation est valide.
@@ -761,7 +761,7 @@ def assiduite_delete():
# Pour chaque assiduite_id on essaye de supprimer l'assiduité
for i, assiduite_id in enumerate(assiduites_list):
- # De la même façon que "_create_one"
+ # De la même façon que "create_one_assiduite"
# Ici le code est soit 200 si réussi ou 404 si raté
# Le message est le message d'erreur si erreur
code, msg = _delete_one(assiduite_id)
@@ -1014,7 +1014,9 @@ def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
else assiduite_unique.external_data
)
- if force and not (external_data is not None and external_data.get("module", False) != ""):
+ if force and not (
+ external_data is not None and external_data.get("module", False) != ""
+ ):
errors.append(
"param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul"
)
diff --git a/app/forms/assiduite/ajout_assiduite_etud.py b/app/forms/assiduite/ajout_assiduite_etud.py
new file mode 100644
index 0000000000..70a8b494ce
--- /dev/null
+++ b/app/forms/assiduite/ajout_assiduite_etud.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# ScoDoc
+#
+# Copyright (c) 1999 - 2023 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
+#
+##############################################################################
+
+"""
+Formulaire ajout d'une "assiduité" sur un étudiant
+"""
+
+from flask_wtf import FlaskForm
+from wtforms import (
+ SelectField,
+ StringField,
+ SubmitField,
+ RadioField,
+ TextAreaField,
+ validators,
+)
+
+
+class AjoutAssiduiteEtudForm(FlaskForm):
+ "Formulaire de saisie d'une assiduité pour un étudiant"
+ assi_etat = RadioField(
+ "Signaler:",
+ choices=[("absent", "absence"), ("retard", "retard"), ("present", "présence")],
+ default="absent",
+ validators=[
+ validators.DataRequired("spécifiez le type d'évènement à signaler"),
+ ],
+ )
+ date_debut = StringField(
+ "Date de début",
+ validators=[validators.Length(max=10)],
+ render_kw={
+ "class": "datepicker",
+ "size": 10,
+ "id": "assi_date_debut",
+ },
+ )
+ heure_debut = StringField(
+ "Heure début",
+ default="",
+ validators=[validators.Length(max=5)],
+ render_kw={
+ "class": "timepicker",
+ "size": 5,
+ "id": "assi_heure_debut",
+ },
+ )
+ heure_fin = StringField(
+ "Heure fin",
+ default="",
+ validators=[validators.Length(max=5)],
+ render_kw={
+ "class": "timepicker",
+ "size": 5,
+ "id": "assi_heure_fin",
+ },
+ )
+ date_fin = StringField(
+ "Date de fin (si plusieurs jours)",
+ validators=[validators.Length(max=10)],
+ render_kw={
+ "class": "datepicker",
+ "size": 10,
+ "id": "assi_date_fin",
+ },
+ )
+ modimpl = SelectField(
+ "Module",
+ choices={}, # will be populated dynamically
+ )
+ assi_raison = TextAreaField(
+ "Raison",
+ render_kw={
+ # "name": "assi_raison",
+ "id": "assi_raison",
+ "cols": 75,
+ "rows": 4,
+ "maxlength": 500,
+ },
+ )
+ submit = SubmitField("Enregistrer")
+ cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
diff --git a/app/models/assiduites.py b/app/models/assiduites.py
index 937e11c43c..0330d6bb1b 100644
--- a/app/models/assiduites.py
+++ b/app/models/assiduites.py
@@ -194,7 +194,8 @@ class Assiduite(db.Model):
user_id=user_id,
)
db.session.add(nouv_assiduite)
- log(f"create_assiduite: {etud.id} {nouv_assiduite}")
+ db.session.flush()
+ log(f"create_assiduite: {etud.id} id={nouv_assiduite.id} {nouv_assiduite}")
Scolog.logdb(
method="create_assiduite",
etudid=etud.id,
@@ -308,6 +309,8 @@ class Justificatif(db.Model):
)
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
+ "date de création de l'élément: date de saisie"
+ # pourrait devenir date de dépot au secrétariat, si différente
user_id = db.Column(
db.Integer,
diff --git a/app/models/etudiants.py b/app/models/etudiants.py
index e37d0dab0b..6148331004 100644
--- a/app/models/etudiants.py
+++ b/app/models/etudiants.py
@@ -340,6 +340,42 @@ class Identite(db.Model, models.ScoDocModel):
reverse=True,
)
+ def get_modimpls_by_formsemestre(
+ self, annee_scolaire: int
+ ) -> dict[int, list["ModuleImpl"]]:
+ """Pour chaque semestre de l'année indiquée dans lequel l'étudiant
+ est inscrit à des moduleimpls, liste ceux ci.
+ { formsemestre_id : [ modimpl, ... ] }
+ annee_scolaire est un nombre: eg 2023
+ """
+ date_debut_annee = scu.date_debut_anne_scolaire(annee_scolaire)
+ date_fin_annee = scu.date_fin_anne_scolaire(annee_scolaire)
+ modimpls = (
+ ModuleImpl.query.join(ModuleImplInscription)
+ .join(FormSemestre)
+ .filter(
+ (FormSemestre.date_debut <= date_fin_annee)
+ & (FormSemestre.date_fin >= date_debut_annee)
+ )
+ .join(Identite)
+ .filter_by(id=self.id)
+ )
+ # Tri, par semestre puis par module, suivant le type de formation:
+ formsemestres = sorted(
+ {m.formsemestre for m in modimpls}, key=lambda s: s.sort_key()
+ )
+ modimpls_by_formsemestre = {}
+ for formsemestre in formsemestres:
+ modimpls_sem = [m for m in modimpls if m.formsemestre_id == formsemestre.id]
+ if formsemestre.formation.is_apc():
+ modimpls_sem.sort(key=lambda m: m.module.sort_key_apc())
+ else:
+ modimpls_sem.sort(
+ key=lambda m: (m.module.ue.numero or 0, m.module.numero or 0)
+ )
+ modimpls_by_formsemestre[formsemestre.id] = modimpls_sem
+ return modimpls_by_formsemestre
+
@classmethod
def convert_dict_fields(cls, args: dict) -> dict:
"""Convert fields in the given dict. No other side effect.
@@ -937,3 +973,8 @@ class EtudAnnotation(db.Model):
etudid = db.Column(db.Integer) # sans contrainte (compat ScoDoc 7))
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
comment = db.Column(db.Text)
+
+
+from app.models.formsemestre import FormSemestre
+from app.models.modules import Module
+from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
diff --git a/app/models/groups.py b/app/models/groups.py
index 8b7ed5690d..dd89109bc6 100644
--- a/app/models/groups.py
+++ b/app/models/groups.py
@@ -11,8 +11,9 @@ from operator import attrgetter
from sqlalchemy.exc import IntegrityError
from app import db, log
-from app.models import ScoDocModel, Scolog, GROUPNAME_STR_LEN, SHORT_STR_LEN
+from app.models import ScoDocModel, GROUPNAME_STR_LEN, SHORT_STR_LEN
from app.models.etudiants import Identite
+from app.models.events import Scolog
from app.scodoc import sco_cache
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py
index 0613ec0282..719a824bcc 100755
--- a/app/scodoc/html_sidebar.py
+++ b/app/scodoc/html_sidebar.py
@@ -45,7 +45,7 @@ def sidebar_common():
f"""ScoDoc {SCOVERSION}
Accueil
Attention, le module doit obligatoirement être renseigné.
-Cela vient de la configuration du semestre ou plus largement du département.
-Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.
+Voir configuration du semestre ou du département.
`; const content = document.createElement("div"); @@ -1002,7 +1001,6 @@ function createAssiduiteComplete(assiduite, etudid) { ) { const HTML = `Attention, l'étudiant n'est pas inscrit à ce module.
-Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.
`; const content = document.createElement("div"); @@ -1015,8 +1013,8 @@ function createAssiduiteComplete(assiduite, etudid) { "Duplication: la période rentre en conflit avec une plage enregistrée" ) { const HTML = ` -L'assiduité n'a pas pu être enregistrée car une autre assiduité existe sur la période sélectionnée
-Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.
+L'assiduité n'a pas pu être enregistrée car un autre évènement + existe sur la période sélectionnée
`; const content = document.createElement("div"); @@ -1657,7 +1655,7 @@ function getSingleEtud(etudid) { } function isSingleEtud() { - return location.href.includes("SignaleAssiduiteEtud"); + return location.href.includes("ajout_assiduite_etud"); } function getCurrentAssiduiteModuleImplId() { diff --git a/app/static/libjs/timepicker-1.3.5/jquery.timepicker.min.css b/app/static/libjs/timepicker-1.3.5/jquery.timepicker.min.css new file mode 100644 index 0000000000..51748c92d7 --- /dev/null +++ b/app/static/libjs/timepicker-1.3.5/jquery.timepicker.min.css @@ -0,0 +1 @@ +.ui-timepicker-container{position:absolute;overflow:hidden;box-sizing:border-box}.ui-timepicker,.ui-timepicker-viewport{box-sizing:content-box;height:205px;display:block;margin:0}.ui-timepicker{list-style:none;padding:0 1px;text-align:center}.ui-timepicker-viewport{padding:0;overflow:auto;overflow-x:hidden}.ui-timepicker-standard{font-family:Verdana,Arial,sans-serif;font-size:1.1em;background-color:#FFF;border:1px solid #AAA;color:#222;margin:0;padding:2px}.ui-timepicker-standard a{border:1px solid transparent;color:#222;display:block;padding:.2em .4em;text-decoration:none}.ui-timepicker-standard .ui-state-hover{background-color:#DADADA;border:1px solid #999;font-weight:400;color:#212121}.ui-timepicker-standard .ui-menu-item{margin:0;padding:0}.ui-timepicker-corners,.ui-timepicker-corners .ui-corner-all{-moz-border-radius:4px;-webkit-border-radius:4px;border-radius:4px}.ui-timepicker-hidden{display:none}.ui-timepicker-no-scrollbar .ui-timepicker{border:none}/*# sourceMappingURL=jquery.timepicker.min.css.map */ \ No newline at end of file diff --git a/app/static/libjs/timepicker-1.3.5/jquery.timepicker.min.js b/app/static/libjs/timepicker-1.3.5/jquery.timepicker.min.js new file mode 100644 index 0000000000..4e264e4a9c --- /dev/null +++ b/app/static/libjs/timepicker-1.3.5/jquery.timepicker.min.js @@ -0,0 +1,2 @@ +!function(e){"object"==typeof module&&"object"==typeof module.exports?e(require("jquery"),window,document):"undefined"!=typeof jQuery&&e(jQuery,window,document)}(function(e,t,i,n){!function(){function t(e,t,i){return new Array(i+1-e.length).join(t)+e}function n(){if(1===arguments.length){var t=arguments[0];return"string"==typeof t&&(t=e.fn.timepicker.parseTime(t)),new Date(0,0,0,t.getHours(),t.getMinutes(),t.getSeconds())}return 3===arguments.length?new Date(0,0,0,arguments[0],arguments[1],arguments[2]):2===arguments.length?new Date(0,0,0,arguments[0],arguments[1],0):new Date(0,0,0)}e.TimePicker=function(){var t=this;t.container=e(".ui-timepicker-container"),t.ui=t.container.find(".ui-timepicker"),0===t.container.length&&(t.container=e("").addClass("ui-timepicker-container").addClass("ui-timepicker-hidden ui-helper-hidden").appendTo("body").hide(),t.ui=e("").addClass("ui-timepicker").addClass("ui-widget ui-widget-content ui-menu").addClass("ui-corner-all").appendTo(t.container),t.viewport=e("