- {{ form.description.label }}
- {{ form.description(cols=80, rows=8) }}
-
{{ form.description.description }}
+
+
{{ form.wip.label }} {{form.wip() }}
+
{{ form.wip.description }}
+ {{ render_string_field(form.date_debut_inscriptions, size=10) }}
+ {{ render_string_field(form.date_fin_inscriptions, size=10) }}
+
+ {{ render_textarea_field(form.description) }}
+ {{ render_string_field(form.responsable) }}
+
+ {{ form.photo_ens.label }}
+
+ {% if formsemestre_description.photo_ens %}
+
+
+ Changer l'image: {{ form.photo_ens() }}
+
+ {% else %}
+
Aucune photo ou illustration chargée.
+ {{ form.photo_ens() }}
+ {% endif %}
+
+ {{ render_string_field(form.campus) }}
+ {{ render_string_field(form.salle, size=32) }}
+ {{ render_string_field(form.horaire) }}
+
- {{ form.responsable.label }}
- {{ form.responsable(size=64) }}
-
{{ form.responsable.description }}
-
-
- {{ form.lieu.label }}
- {{ form.lieu(size=48) }}
-
{{ form.lieu.description }}
-
-
- {{ form.horaire.label }}
- {{ form.horaire(size=64) }}
-
{{ form.horaire.description }}
+
{{ form.dispositif.label }} :
+
{{ form.dispositif }}
+ {% if form.dispositif.description %}
+
{{ form.dispositif.description }}
+ {% endif %}
+ {{ render_string_field(form.public) }}
+ {{ render_textarea_field(form.modalites_mcc, rows=8) }}
+ {{ render_textarea_field(form.prerequis, rows=5) }}
{{ form.image.label }}
@@ -68,7 +110,7 @@ div.image img {
+ alt="Current Image" style="max-width: 400px;">
Changer l'image: {{ form.image() }}
@@ -76,8 +118,8 @@ div.image img {
Aucune image n'est actuellement associée à ce semestre.
{{ form.image() }}
{% endif %}
-
+
{{ form.submit }} {{ form.cancel }}
diff --git a/app/views/notes_formsemestre.py b/app/views/notes_formsemestre.py
index 972d25bc..1bafa62d 100644
--- a/app/views/notes_formsemestre.py
+++ b/app/views/notes_formsemestre.py
@@ -29,6 +29,8 @@ Vues "modernes" des formsemestres
Emmanuel Viennet, 2023
"""
+import datetime
+
from flask import flash, redirect, render_template, url_for
from flask import current_app, g, request
@@ -46,6 +48,7 @@ from app.models import (
Formation,
FormSemestre,
FormSemestreDescription,
+ FORMSEMESTRE_DISPOSITIFS,
ScoDocSiteConfig,
)
from app.scodoc import (
@@ -248,7 +251,7 @@ def edit_formsemestre_description(formsemestre_id: int):
db.session.commit()
formsemestre_description = formsemestre.description
form = edit_description.FormSemestreDescriptionForm(obj=formsemestre_description)
-
+ ok = True
if form.validate_on_submit():
if form.cancel.data: # cancel button
return redirect(
@@ -258,36 +261,75 @@ def edit_formsemestre_description(formsemestre_id: int):
formsemestre_id=formsemestre.id,
)
)
- form_image = form.image
- del form.image
- form.populate_obj(formsemestre_description)
+ # Vérification valeur dispositif
+ if form.dispositif.data not in FORMSEMESTRE_DISPOSITIFS:
+ flash("Dispositif inconnu", "danger")
+ ok = False
- if form_image.data:
- image_data = form_image.data.read()
- max_length = current_app.config.get("MAX_CONTENT_LENGTH")
- if max_length and len(image_data) > max_length:
- flash(
- f"Image too large, max {max_length} bytes",
- "danger",
+ # Vérification dates inscriptions
+ if form.date_debut_inscriptions.data:
+ try:
+ date_debut_inscriptions_dt = datetime.datetime.strptime(
+ form.date_debut_inscriptions.data, scu.DATE_FMT
)
- return redirect(
- url_for(
- "notes.edit_formsemestre_description",
- formsemestre_id=formsemestre.id,
- scodoc_dept=g.scodoc_dept,
- )
+ except ValueError:
+ flash("Date de début des inscriptions invalide", "danger")
+ form.set_error("date début invalide", form.date_debut_inscriptions)
+ ok = False
+ else:
+ date_debut_inscriptions_dt = None
+ if form.date_fin_inscriptions.data:
+ try:
+ date_fin_inscriptions_dt = datetime.datetime.strptime(
+ form.date_fin_inscriptions.data, scu.DATE_FMT
)
- formsemestre_description.image = image_data
+ except ValueError:
+ flash("Date de fin des inscriptions invalide", "danger")
+ form.set_error("date fin invalide", form.date_fin_inscriptions)
+ ok = False
+ else:
+ date_fin_inscriptions_dt = None
+ if ok:
+ # dates converties
+ form.date_debut_inscriptions.data = date_debut_inscriptions_dt
+ form.date_fin_inscriptions.data = date_fin_inscriptions_dt
+ # Affecte tous les champs sauf les images:
+ form_image = form.image
+ del form.image
+ form_photo_ens = form.photo_ens
+ del form.photo_ens
+ form.populate_obj(formsemestre_description)
+ # Affecte les images:
+ for field, form_field in (
+ ("image", form_image),
+ ("photo_ens", form_photo_ens),
+ ):
+ if form_field.data:
+ image_data = form_field.data.read()
+ max_length = current_app.config.get("MAX_CONTENT_LENGTH")
+ if max_length and len(image_data) > max_length:
+ flash(
+ f"Image trop grande ({field}), max {max_length} octets",
+ "danger",
+ )
+ return redirect(
+ url_for(
+ "notes.edit_formsemestre_description",
+ formsemestre_id=formsemestre.id,
+ scodoc_dept=g.scodoc_dept,
+ )
+ )
+ setattr(formsemestre_description, field, image_data)
- db.session.commit()
- flash("Description enregistrée", "success")
- return redirect(
- url_for(
- "notes.formsemestre_status",
- formsemestre_id=formsemestre.id,
- scodoc_dept=g.scodoc_dept,
+ db.session.commit()
+ flash("Description enregistrée", "success")
+ return redirect(
+ url_for(
+ "notes.formsemestre_status",
+ formsemestre_id=formsemestre.id,
+ scodoc_dept=g.scodoc_dept,
+ )
)
- )
return render_template(
"formsemestre/edit_description.j2",
@@ -295,4 +337,5 @@ def edit_formsemestre_description(formsemestre_id: int):
formsemestre=formsemestre,
formsemestre_description=formsemestre_description,
sco=ScoData(formsemestre=formsemestre),
+ title="Modif. description semestre",
)
diff --git a/migrations/versions/2640b7686de6_formsemestre_description.py b/migrations/versions/2640b7686de6_formsemestre_description.py
index c275d3e2..c2533093 100644
--- a/migrations/versions/2640b7686de6_formsemestre_description.py
+++ b/migrations/versions/2640b7686de6_formsemestre_description.py
@@ -21,11 +21,20 @@ def upgrade():
op.create_table(
"notes_formsemestre_description",
sa.Column("id", sa.Integer(), nullable=False),
- sa.Column("image", sa.LargeBinary(), nullable=True),
sa.Column("description", sa.Text(), server_default="", nullable=False),
- sa.Column("responsable", sa.Text(), server_default="", nullable=False),
- sa.Column("lieu", sa.Text(), server_default="", nullable=False),
sa.Column("horaire", sa.Text(), server_default="", nullable=False),
+ sa.Column("date_debut_inscriptions", sa.DateTime(timezone=True), nullable=True),
+ sa.Column("date_fin_inscriptions", sa.DateTime(timezone=True), nullable=True),
+ sa.Column("wip", sa.Boolean(), server_default="false", nullable=False),
+ sa.Column("image", sa.LargeBinary(), nullable=True),
+ sa.Column("campus", sa.Text(), server_default="", nullable=False),
+ sa.Column("salle", sa.Text(), server_default="", nullable=False),
+ sa.Column("dispositif", sa.Integer(), server_default="0", nullable=False),
+ sa.Column("modalites_mcc", sa.Text(), server_default="", nullable=False),
+ sa.Column("photo_ens", sa.LargeBinary(), nullable=True),
+ sa.Column("public", sa.Text(), server_default="", nullable=False),
+ sa.Column("prerequis", sa.Text(), server_default="", nullable=False),
+ sa.Column("responsable", sa.Text(), server_default="", nullable=False),
sa.Column("formsemestre_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["formsemestre_id"], ["notes_formsemestre.id"], ondelete="CASCADE"
diff --git a/tests/unit/test_formsemestre.py b/tests/unit/test_formsemestre.py
index 75ed6793..0e5e2f96 100644
--- a/tests/unit/test_formsemestre.py
+++ b/tests/unit/test_formsemestre.py
@@ -223,11 +223,14 @@ def test_formsemestre_description(test_client):
db.session.commit()
assert formsemestre.description is None
# Association d'une description
- formsemestre.description = FormSemestreDescription(
- description="Description 2",
- responsable="Responsable 2",
- lieu="Lieu 2",
- horaire="Horaire 2",
+ formsemestre.description = FormSemestreDescription.create_from_dict(
+ {
+ "description": "Description",
+ "responsable": "Responsable",
+ "campus": "Sorbonne",
+ "salle": "L214",
+ "horaire": "23h à l'aube",
+ }
)
db.session.add(formsemestre)
db.session.commit()
From e6f86d655b4887800346b2f544a2d6cd3dc69d89 Mon Sep 17 00:00:00 2001
From: ilona
Date: Wed, 14 Aug 2024 10:28:26 +0200
Subject: [PATCH 5/8] API FormSemestreDescription + test
---
app/api/formsemestres.py | 25 ++++++++++++++++
tests/api/test_api_formsemestre.py | 48 ++++++++++++++++++++++++++++++
tests/api/test_api_permissions.py | 2 +-
3 files changed, 74 insertions(+), 1 deletion(-)
diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py
index e46258f2..24a98f33 100644
--- a/app/api/formsemestres.py
+++ b/app/api/formsemestres.py
@@ -33,6 +33,7 @@ from app.models import (
Departement,
Evaluation,
FormSemestre,
+ FormSemestreDescription,
FormSemestreEtape,
FormSemestreInscription,
Identite,
@@ -812,6 +813,30 @@ def formsemestre_get_description(formsemestre_id: int):
return formsemestre.description.to_dict() if formsemestre.description else {}
+@bp.post("/formsemestre//description/edit")
+@api_web_bp.post("/formsemestre//description/edit")
+@login_required
+@scodoc
+@permission_required(Permission.ScoView)
+@as_json
+def formsemestre_edit_description(formsemestre_id: int):
+ """Modifie description externe du formsemestre
+
+ formsemestre_id : l'id du formsemestre
+
+ SAMPLES
+ -------
+ /formsemestre//description/edit;{""description"":""descriptif du semestre"", ""dispositif"":1}
+ """
+ formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
+ args = request.get_json(force=True) # may raise 400 Bad Request
+ if not formsemestre.description:
+ formsemestre.description = FormSemestreDescription()
+ formsemestre.description.from_dict(args)
+ db.session.commit()
+ return formsemestre.description.to_dict()
+
+
@bp.route("/formsemestre//description/image")
@api_web_bp.route("/formsemestre//description/image")
@login_required
diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py
index 23d55ccc..923f4c79 100644
--- a/tests/api/test_api_formsemestre.py
+++ b/tests/api/test_api_formsemestre.py
@@ -28,8 +28,10 @@ from tests.api.setup_test_api import (
API_URL,
CHECK_CERTIFICATE,
GET,
+ POST,
api_headers,
api_admin_headers,
+ set_headers,
)
from tests.api.tools_test_api import (
@@ -780,3 +782,49 @@ def _compare_formsemestre_resultat(res: list[dict], ref: list[dict]):
if "nbabs" in k:
continue
assert res_d[k] == ref_d[k], f"values for key {k} differ."
+
+
+def test_formsemestre_description(api_admin_headers):
+ """
+ Test accès et modification de la description
+ """
+ set_headers(api_admin_headers)
+ formsemestre_id = 1
+ r = GET(f"/formsemestre/{formsemestre_id}")
+ assert "description" not in r
+ r = POST(
+ f"/formsemestre/{formsemestre_id}/description/edit",
+ data={
+ "description": "une description",
+ "horaire": "un horaire",
+ "salle": "une salle",
+ "dispositif": 1,
+ "wip": True,
+ },
+ )
+ assert r["description"] == "une description"
+ assert r["horaire"] == "un horaire"
+ assert r["salle"] == "une salle"
+ assert r["dispositif"] == 1
+ assert r["wip"] is True
+ r = GET(f"/formsemestre/{formsemestre_id}/description")
+ assert r["description"] == "une description"
+ assert r["horaire"] == "un horaire"
+ assert r["salle"] == "une salle"
+ assert r["dispositif"] == 1
+ assert r["wip"] is True
+ r = POST(
+ f"/formsemestre/{formsemestre_id}/description/edit",
+ data={
+ "description": "",
+ "horaire": "",
+ "salle": "",
+ "dispositif": 0,
+ "wip": False,
+ },
+ )
+ assert r["description"] == ""
+ assert r["horaire"] == ""
+ assert r["salle"] == ""
+ assert r["dispositif"] == 0
+ assert r["wip"] is False
diff --git a/tests/api/test_api_permissions.py b/tests/api/test_api_permissions.py
index ae3dad92..f94ce12e 100755
--- a/tests/api/test_api_permissions.py
+++ b/tests/api/test_api_permissions.py
@@ -100,7 +100,7 @@ def test_permissions(api_headers):
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
- assert r.status_code == 200
+ assert r.status_code // 100 == 2 # 2xx success
# Même chose sans le jeton:
for rule in api_rules:
From 4bfd0858a8210578455b99f42e9ac6622ea7607e Mon Sep 17 00:00:00 2001
From: ilona
Date: Wed, 14 Aug 2024 15:39:57 +0200
Subject: [PATCH 6/8] API FormSemestreDescription: images: upload, tests.
---
app/api/formsemestres.py | 21 ++++++++++++++-------
app/views/notes_formsemestre.py | 16 ++++++++++++++++
tests/api/setup_test_api.py | 11 ++++++++---
tests/api/test_api_formsemestre.py | 16 ++++++++++++++++
4 files changed, 54 insertions(+), 10 deletions(-)
diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py
index 24a98f33..afd2e9ba 100644
--- a/app/api/formsemestres.py
+++ b/app/api/formsemestres.py
@@ -13,12 +13,14 @@
FormSemestre
"""
-import mimetypes
+import base64
+import io
from operator import attrgetter, itemgetter
from flask import g, make_response, request
from flask_json import as_json
from flask_login import current_user, login_required
+import PIL
import sqlalchemy as sa
import app
from app import db, log
@@ -820,8 +822,8 @@ def formsemestre_get_description(formsemestre_id: int):
@permission_required(Permission.ScoView)
@as_json
def formsemestre_edit_description(formsemestre_id: int):
- """Modifie description externe du formsemestre
-
+ """Modifie description externe du formsemestre.
+ Les images peuvent êtres passées dans el json, encodées en base64.
formsemestre_id : l'id du formsemestre
SAMPLES
@@ -832,6 +834,10 @@ def formsemestre_edit_description(formsemestre_id: int):
args = request.get_json(force=True) # may raise 400 Bad Request
if not formsemestre.description:
formsemestre.description = FormSemestreDescription()
+ # Decode images (base64)
+ for key in ["image", "photo_ens"]:
+ if key in args:
+ args[key] = base64.b64decode(args[key])
formsemestre.description.from_dict(args)
db.session.commit()
return formsemestre.description.to_dict()
@@ -868,11 +874,12 @@ def formsemestre_get_photo_ens(formsemestre_id: int):
return _image_response(formsemestre.description.photo_ens)
-def _image_response(image_data):
+def _image_response(image_data: bytes):
# Guess the mimetype based on the image data
- mimetype = mimetypes.guess_type("image")[0]
-
- if not mimetype:
+ try:
+ image = PIL.Image.open(io.BytesIO(image_data))
+ mimetype = image.get_format_mimetype()
+ except PIL.UnidentifiedImageError:
# Default to binary stream if mimetype cannot be determined
mimetype = "application/octet-stream"
diff --git a/app/views/notes_formsemestre.py b/app/views/notes_formsemestre.py
index 1bafa62d..e3e25c90 100644
--- a/app/views/notes_formsemestre.py
+++ b/app/views/notes_formsemestre.py
@@ -30,9 +30,11 @@ Emmanuel Viennet, 2023
"""
import datetime
+import io
from flask import flash, redirect, render_template, url_for
from flask import current_app, g, request
+import PIL
from app import db, log
from app.decorators import (
@@ -319,6 +321,20 @@ def edit_formsemestre_description(formsemestre_id: int):
scodoc_dept=g.scodoc_dept,
)
)
+ try:
+ _ = PIL.Image.open(io.BytesIO(image_data))
+ except PIL.UnidentifiedImageError:
+ flash(
+ f"Image invalide ({field}), doit être une image",
+ "danger",
+ )
+ return redirect(
+ url_for(
+ "notes.edit_formsemestre_description",
+ formsemestre_id=formsemestre.id,
+ scodoc_dept=g.scodoc_dept,
+ )
+ )
setattr(formsemestre_description, field, image_data)
db.session.commit()
diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py
index 2ce97eb8..126162e2 100644
--- a/tests/api/setup_test_api.py
+++ b/tests/api/setup_test_api.py
@@ -122,9 +122,14 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False):
if reply.headers.get("Content-Type", None) == "application/json":
return reply.json() # decode la reponse JSON
if reply.headers.get("Content-Type", None) in [
- "image/jpg",
- "image/png",
"application/pdf",
+ "application/vnd.ms-excel",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "image/gif",
+ "image/jpeg",
+ "image/png",
+ "image/webp",
]:
retval = {
"Content-Type": reply.headers.get("Content-Type", None),
@@ -132,7 +137,7 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False):
}
return retval
raise APIError(
- "Unknown returned content {r.headers.get('Content-Type', None} !\n",
+ f"Unknown returned content {reply.headers.get('Content-Type', None)} !\n",
status_code=reply.status_code,
)
diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py
index 923f4c79..1395a6a3 100644
--- a/tests/api/test_api_formsemestre.py
+++ b/tests/api/test_api_formsemestre.py
@@ -16,6 +16,7 @@ Utilisation :
Lancer :
pytest tests/api/test_api_formsemestre.py
"""
+import base64
import json
import requests
from types import NoneType
@@ -813,6 +814,9 @@ def test_formsemestre_description(api_admin_headers):
assert r["salle"] == "une salle"
assert r["dispositif"] == 1
assert r["wip"] is True
+ # La réponse ne contient pas les images, servies à part:
+ assert "image" not in r
+ assert "photo_ens" not in r
r = POST(
f"/formsemestre/{formsemestre_id}/description/edit",
data={
@@ -828,3 +832,15 @@ def test_formsemestre_description(api_admin_headers):
assert r["salle"] == ""
assert r["dispositif"] == 0
assert r["wip"] is False
+ # Upload image
+ with open("tests/ressources/images/papillon.jpg", "rb") as f:
+ img = f.read()
+ img_base64 = base64.b64encode(img).decode("utf-8")
+ r = POST(
+ f"/formsemestre/{formsemestre_id}/description/edit", data={"image": img_base64}
+ )
+ assert r["wip"] is False
+ r = GET(f"/formsemestre/{formsemestre_id}/description/image", raw=True)
+ assert r.status_code == 200
+ assert r.headers.get("Content-Type") == "image/jpeg"
+ assert r.content == img
From 6a48d5bbcf114702929e5a7e1855b99547d877e8 Mon Sep 17 00:00:00 2001
From: ilona
Date: Thu, 22 Aug 2024 16:42:38 +0200
Subject: [PATCH 7/8] =?UTF-8?q?Pr=C3=A9f.=20pour=20envoi=20d=E2=80=99une?=
=?UTF-8?q?=20notification=20mail=20=C3=A0=20chaque=20(de)inscription?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/models/formsemestre.py | 51 ++++++++++++++++++++-
app/scodoc/sco_formsemestre_exterieurs.py | 1 -
app/scodoc/sco_formsemestre_inscriptions.py | 22 +++------
app/scodoc/sco_preferences.py | 21 +++++++--
app/static/css/scodoc.css | 6 +++
5 files changed, 79 insertions(+), 22 deletions(-)
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 34d34aad..9c73cf58 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -23,7 +23,7 @@ from sqlalchemy.sql import text
from sqlalchemy import func
import app.scodoc.sco_utils as scu
-from app import db, log
+from app import db, email, log
from app.auth.models import User
from app import models
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
@@ -1093,9 +1093,58 @@ class FormSemestre(models.ScoDocModel):
msg=f"inscription en semestre {self.titre_annee()}",
commit=True,
)
+ log(
+ f"inscrit_etudiant: {etud.nomprenom} ({etud.id}) au semestre {self.titre_annee()}"
+ )
+ # Notification mail
+ self._notify_inscription(etud)
sco_cache.invalidate_formsemestre(formsemestre_id=self.id)
return inscr
+ def desinscrit_etudiant(self, etud: Identite):
+ "Désinscrit l'étudiant du semestre (et notifie le cas échéant)"
+ inscr_sem = FormSemestreInscription.query.filter_by(
+ etudid=etud.id, formsemestre_id=self.id
+ ).first()
+ if not inscr_sem:
+ raise ScoValueError(
+ f"{etud.nomprenom} ({etud.id}) n'est pas inscrit au semestre !"
+ )
+ db.session.delete(inscr_sem)
+ Scolog.logdb(
+ method="desinscrit_etudiant",
+ etudid=etud.id,
+ msg=f"désinscription semestre {self.titre_annee()}",
+ commit=True,
+ )
+ log(
+ f"desinscrit_etudiant: {etud.nomprenom} ({etud.id}) au semestre {self.titre_annee()}"
+ )
+ self._notify_inscription(etud, action="désinscrit")
+ sco_cache.invalidate_formsemestre(formsemestre_id=self.id)
+
+ def _notify_inscription(self, etud: Identite, action="inscrit") -> None:
+ "Notifie inscription d'un étudiant: envoie un mail selon paramétrage"
+ destinations = (
+ sco_preferences.get_preference("emails_notifications_inscriptions", self.id)
+ or ""
+ )
+ destinations = [x.strip() for x in destinations.split(",")]
+ destinations = [x for x in destinations if x]
+ if not destinations:
+ return
+ txt = f"""{etud.nom_prenom()}
+ s'est {action}{etud.e}
+ en {self.titre_annee()}"""
+ subject = f"""Inscription de {etud.nom_prenom()} en {self.titre_annee()}"""
+ # build mail
+ log(f"_notify_inscription: sending notification to {destinations}")
+ log(f"_notify_inscription: subject: {subject}")
+ log(txt)
+ email.send_email(
+ "[ScoDoc] " + subject, email.get_from_addr(), destinations, txt
+ )
+
def get_partitions_list(
self, with_default=True, only_listed=False
) -> list[Partition]:
diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py
index 4ba6a47a..b4c49c21 100644
--- a/app/scodoc/sco_formsemestre_exterieurs.py
+++ b/app/scodoc/sco_formsemestre_exterieurs.py
@@ -51,7 +51,6 @@ import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc import html_sco_header
-from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_validation
diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py
index f77d1d85..a7650be2 100644
--- a/app/scodoc/sco_formsemestre_inscriptions.py
+++ b/app/scodoc/sco_formsemestre_inscriptions.py
@@ -182,12 +182,11 @@ def do_formsemestre_desinscription(
if check_has_dec_jury:
check_if_has_decision_jury(formsemestre, [etudid])
- insem = do_formsemestre_inscription_list(
- args={"formsemestre_id": formsemestre_id, "etudid": etudid}
- )
- if not insem:
+ inscr_sem = FormSemestreInscription.query.filter_by(
+ etudid=etudid, formsemestre_id=formsemestre_id
+ ).first()
+ if not inscr_sem:
raise ScoValueError(f"{etud.nomprenom} n'est pas inscrit au semestre !")
- insem = insem[0]
# -- desinscription de tous les modules
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
@@ -211,10 +210,8 @@ def do_formsemestre_desinscription(
Partition.formsemestre_remove_etud(formsemestre_id, etud)
# -- désincription du semestre
- do_formsemestre_inscription_delete(
- insem["formsemestre_inscription_id"], formsemestre_id=formsemestre_id
- )
- sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
+ formsemestre.desinscrit_etudiant(etud)
+
# --- Semestre extérieur
if formsemestre.modalite == "EXT":
if 0 == len(formsemestre.inscriptions):
@@ -226,13 +223,6 @@ def do_formsemestre_desinscription(
db.session.commit()
flash(f"Semestre extérieur supprimé: {formsemestre.titre_annee()}")
- Scolog.logdb(
- method="formsemestre_desinscription",
- etudid=etudid,
- msg=f"desinscription semestre {formsemestre_id}",
- commit=True,
- )
-
def do_formsemestre_inscription_with_modules(
formsemestre_id,
diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py
index 6b940076..90d4cc43 100644
--- a/app/scodoc/sco_preferences.py
+++ b/app/scodoc/sco_preferences.py
@@ -369,10 +369,23 @@ class BasePreferences:
"emails_notifications",
{
"initvalue": "",
- "title": "e-mails à qui notifier les opérations",
+ "title": "e-mail(s) à qui notifier les opérations",
"size": 70,
- "explanation": """adresses séparées par des virgules; notifie les opérations
- (saisies de notes, etc).
+ "explanation": """optionnel; adresses séparées par des virgules;
+ notifie les opérations (saisies de notes, etc).
+ """,
+ "category": "general",
+ "only_global": False, # peut être spécifique à un semestre
+ },
+ ),
+ (
+ "emails_notifications_inscriptions",
+ {
+ "initvalue": "",
+ "title": "e-mail(s) à qui notifier les inscriptions d'étudiants",
+ "size": 70,
+ "explanation": """optionnel; adresses séparées par des virgules;
+ notifie les inscriptions/désincriptions de chaque individu.
""",
"category": "general",
"only_global": False, # peut être spécifique à un semestre
@@ -2321,6 +2334,7 @@ class BasePreferences:
+
"""
descr["explanation"] = menu_global
@@ -2385,7 +2399,6 @@ class SemPreferences:
def edit(self, categories=[]):
"""Dialog to edit semestre preferences in given categories"""
from app.scodoc import html_sco_header
- from app.scodoc import sco_formsemestre
if not self.formsemestre_id:
raise ScoValueError(
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 783ac666..5b80c194 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -3320,6 +3320,12 @@ li.tf-msg {
padding-bottom: 5px;
}
+.pref-comment {
+ font-style: italic;
+ font-size: small;
+ color: var(--sco-color-explication);
+}
+
div.formsemestre-warning-box {
background-color: yellow;
border-radius: 4px;
From d8f6fe35e902ee830f29337aa87e2d5f4421673d Mon Sep 17 00:00:00 2001
From: ilona
Date: Fri, 23 Aug 2024 16:33:35 +0200
Subject: [PATCH 8/8] Liste groupe: affichage optionnel de la date
d'inscription
---
app/scodoc/sco_excel.py | 17 ++++++++++++++---
app/scodoc/sco_groups_view.py | 34 +++++++++++++++++++++++++++-------
app/static/js/groups_view.js | 30 +++++++++++++++++++-----------
app/views/scolar.py | 12 +++++++++---
4 files changed, 69 insertions(+), 24 deletions(-)
diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py
index 1ef1df49..8a30e92e 100644
--- a/app/scodoc/sco_excel.py
+++ b/app/scodoc/sco_excel.py
@@ -45,6 +45,7 @@ from openpyxl.worksheet.worksheet import Worksheet
import app.scodoc.sco_utils as scu
from app import log
+from app.models.scolar_event import ScolarEvent
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import notesdb, sco_preferences
@@ -638,11 +639,12 @@ def excel_feuille_listeappel(
lines,
partitions=None,
with_codes=False,
+ with_date_inscription=False,
with_paiement=False,
server_name=None,
edt_params: dict = None,
):
- """generation feuille appel
+ """Génération feuille appel.
edt_params :
- "discipline" : Discipline
@@ -763,7 +765,8 @@ def excel_feuille_listeappel(
cells.append(ws.make_cell("etudid", style3))
cells.append(ws.make_cell("code_nip", style3))
cells.append(ws.make_cell("code_ine", style3))
-
+ if with_date_inscription:
+ cells.append(ws.make_cell("Date inscr.", style3))
# case Groupes
cells.append(ws.make_cell("Groupes", style3))
letter_int += 1
@@ -805,7 +808,15 @@ def excel_feuille_listeappel(
cells.append(ws.make_cell(code_nip, style2t3))
code_ine = t.get("code_ine", "")
cells.append(ws.make_cell(code_ine, style2t3))
-
+ if with_date_inscription:
+ event = ScolarEvent.query.filter_by(
+ etudid=t["etudid"],
+ event_type="INSCRIPTION",
+ formsemestre_id=formsemestre_id,
+ ).first()
+ if event:
+ date_inscription = event.event_date
+ cells.append(ws.make_cell(date_inscription, style2t3))
cells.append(ws.make_cell(style=style2t3))
ws.append_row(cells)
ws.set_row_dimension_height(row_id, 30)
diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py
index d4817ee2..541589bb 100644
--- a/app/scodoc/sco_groups_view.py
+++ b/app/scodoc/sco_groups_view.py
@@ -40,7 +40,7 @@ from flask import url_for, g, render_template, request
from flask_login import current_user
from app import db
-from app.models import FormSemestre, Identite
+from app.models import FormSemestre, Identite, ScolarEvent
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_assiduites as scass
@@ -70,6 +70,7 @@ def groups_lists(
group_ids=(),
fmt="html",
with_codes=0,
+ with_date_inscription=0,
etat=None,
with_paiement=0,
with_archives=0,
@@ -102,6 +103,7 @@ def groups_lists(
groups_infos=groups_infos,
fmt=fmt,
with_codes=with_codes,
+ with_date_inscription=with_date_inscription,
etat=etat,
with_paiement=with_paiement,
with_archives=with_archives,
@@ -121,6 +123,7 @@ def groups_lists(
groups_infos=groups_infos,
fmt=fmt,
with_codes=with_codes,
+ with_date_inscription=with_date_inscription,
etat=etat,
with_paiement=with_paiement,
with_archives=with_archives,
@@ -507,6 +510,7 @@ class DisplayedGroupsInfos:
def groups_table(
groups_infos: DisplayedGroupsInfos = None,
with_codes=0,
+ with_date_inscription=0,
etat=None,
fmt="html",
with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail)
@@ -522,15 +526,16 @@ def groups_table(
can_view_etud_data = int(current_user.has_permission(Permission.ViewEtudData))
with_codes = int(with_codes)
+ with_date_inscription = int(with_date_inscription)
with_paiement = int(with_paiement) and can_view_etud_data
with_archives = int(with_archives) and can_view_etud_data
with_annotations = int(with_annotations) and can_view_etud_data
with_bourse = int(with_bourse) and can_view_etud_data
- base_url_np = groups_infos.base_url + f"&with_codes={with_codes}"
base_url = (
- base_url_np
- + f"""&with_paiement={with_paiement}&with_archives={
+ groups_infos.base_url
+ + f"""&with_codes={with_codes}&with_date_inscription={
+ with_date_inscription}&with_paiement={with_paiement}&with_archives={
with_archives}&with_annotations={with_annotations
}&with_bourse={with_bourse}"""
)
@@ -546,6 +551,7 @@ def groups_table(
"etudid": "etudid",
"code_nip": "code_nip",
"code_ine": "code_ine",
+ "date_inscription": "Date inscription",
"datefinalisationinscription_str": "Finalisation inscr.",
"paiementinscription_str": "Paiement",
"etudarchive": "Fichiers",
@@ -579,9 +585,11 @@ def groups_table(
if with_codes:
columns_ids += ["etape", "etudid", "code_nip", "code_ine"]
+ if with_date_inscription:
+ columns_ids += ["date_inscription"]
if with_paiement:
columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"]
- if with_paiement: # or with_codes:
+ if with_paiement:
sco_portal_apogee.check_paiement_etuds(groups_infos.members)
if with_archives:
from app.scodoc import sco_archives_etud
@@ -597,6 +605,16 @@ def groups_table(
moodle_groupenames = set()
# ajoute liens
for etud_info in groups_infos.members:
+ if with_date_inscription:
+ event = ScolarEvent.query.filter_by(
+ etudid=etud_info["etudid"],
+ event_type="INSCRIPTION",
+ formsemestre_id=groups_infos.formsemestre_id,
+ ).first()
+ if event:
+ etud_info["date_inscription"] = event.event_date.strftime(scu.DATE_FMT)
+ etud_info["_date_inscription_xls"] = event.event_date
+ etud_info["_date_inscription_order"] = event.event_date.isoformat
if etud_info["email"]:
etud_info["_email_target"] = "mailto:" + etud_info["email"]
else:
@@ -612,8 +630,8 @@ def groups_table(
etud_info["_nom_disp_order"] = etud_sort_key(etud_info)
etud_info["_prenom_target"] = fiche_url
- etud_info["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (
- etud_info["etudid"]
+ etud_info["_nom_disp_td_attrs"] = (
+ f"""id="{etud_info['etudid']}" class="etudinfo" """
)
etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non"
if etud_info["etat"] == "D":
@@ -720,6 +738,7 @@ def groups_table(
if groups_infos.members:
options = {
"with_codes": "Affiche codes",
+ "with_date_inscription": "Date inscription",
}
if can_view_etud_data:
options.update(
@@ -824,6 +843,7 @@ def groups_table(
groups_infos.members,
partitions=groups_infos.partitions,
with_codes=with_codes,
+ with_date_inscription=with_date_inscription,
with_paiement=with_paiement,
server_name=request.url_root,
)
diff --git a/app/static/js/groups_view.js b/app/static/js/groups_view.js
index 7f16a580..43eb694b 100644
--- a/app/static/js/groups_view.js
+++ b/app/static/js/groups_view.js
@@ -48,13 +48,12 @@ function change_list_options(selected_options) {
"with_archives",
"with_annotations",
"with_codes",
+ "with_date_inscription",
"with_bourse",
];
for (var i = 0; i < options.length; i++) {
- var option = options[i];
- if ($.inArray(option, selected_options) >= 0) {
- urlParams.set(option, "1");
- }
+ let option = options[i];
+ urlParams.set(option, selected_options.indexOf(option) >= 0 ? "1" : "0");
}
window.location = url.href;
}
@@ -62,23 +61,32 @@ function change_list_options(selected_options) {
// Menu choix groupe:
function toggle_visible_etuds() {
//
- $(".etud_elem").hide();
+ document.querySelectorAll('.etud_elem').forEach(element => {
+ element.style.display = 'none';
+ });
var qargs = "";
- $("#group_ids_sel option:selected").each(function (index, opt) {
+ var selectedOptions = document.querySelectorAll("#group_ids_sel option:checked");
+ var qargs = "";
+ selectedOptions.forEach(function (opt) {
var group_id = opt.value;
- $(".group-" + group_id).show();
+ var groupElements = document.querySelectorAll(".group-" + group_id);
+ groupElements.forEach(function (elem) {
+ elem.style.display = "block";
+ });
qargs += "&group_ids=" + group_id;
});
// Update url saisie tableur:
- var input_eval = $("#formnotes_evaluation_id");
+ let input_eval = document.querySelectorAll("#formnotes_evaluation_id");
if (input_eval.length > 0) {
- var evaluation_id = input_eval[0].value;
- $("#menu_saisie_tableur a").attr(
+ let evaluation_id = input_eval[0].value;
+ let menu_saisie_tableur_a = document.querySelector("#menu_saisie_tableur a");
+ menu_saisie_tableur_a.setAttribute(
"href",
"saisie_notes_tableur?evaluation_id=" + evaluation_id + qargs
);
// lien feuille excel:
- $("#lnk_feuille_saisie").attr(
+ let lnk_feuille_saisie = document.querySelector("#lnk_feuille_saisie");
+ lnk_feuille_saisie.setAttribute(
"href",
"feuille_saisie_notes?evaluation_id=" + evaluation_id + qargs
);
diff --git a/app/views/scolar.py b/app/views/scolar.py
index 1a55f771..31438ae8 100644
--- a/app/views/scolar.py
+++ b/app/views/scolar.py
@@ -471,18 +471,24 @@ def groups_lists(
fmt="html",
# Options pour listes:
with_codes=0,
+ with_date_inscription=0,
etat=None,
- with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail)
- with_archives=0, # ajoute colonne avec noms fichiers archivés
+ with_paiement=0,
+ with_archives=0,
with_annotations=0,
with_bourse=0,
formsemestre_id=None,
):
- "Listes des étudiants des groupes"
+ """Listes des étudiants des groupes.
+ Si with_paiement, ajoute colonnes infos paiement droits et finalisation
+ inscription (lent car interrogation portail).
+ Si with_archives, ajoute colonne avec noms fichiers archivés.
+ """
return sco_groups_view.groups_lists(
group_ids=group_ids,
fmt=fmt,
with_codes=with_codes,
+ with_date_inscription=with_date_inscription,
etat=etat,
with_paiement=with_paiement,
with_archives=with_archives,