forked from ScoDoc/ScoDoc
merge el
This commit is contained in:
commit
6ab027dffe
@ -13,11 +13,14 @@
|
||||
FormSemestre
|
||||
|
||||
"""
|
||||
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
|
||||
@ -32,6 +35,7 @@ from app.models import (
|
||||
Departement,
|
||||
Evaluation,
|
||||
FormSemestre,
|
||||
FormSemestreDescription,
|
||||
FormSemestreEtape,
|
||||
FormSemestreInscription,
|
||||
Identite,
|
||||
@ -790,3 +794,95 @@ def formsemestre_edt(formsemestre_id: int):
|
||||
return sco_edt_cal.formsemestre_edt_dict(
|
||||
formsemestre, group_ids=group_ids, show_modules_titles=show_modules_titles
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/description")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/description")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_get_description(formsemestre_id: int):
|
||||
"""Description externe du formsemestre. Peut être vide.
|
||||
|
||||
formsemestre_id : l'id du formsemestre
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/formsemestre/1/description
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
return formsemestre.description.to_dict() if formsemestre.description else {}
|
||||
|
||||
|
||||
@bp.post("/formsemestre/<int:formsemestre_id>/description/edit")
|
||||
@api_web_bp.post("/formsemestre/<int:formsemestre_id>/description/edit")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_edit_description(formsemestre_id: int):
|
||||
"""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
|
||||
-------
|
||||
/formsemestre/<int:formsemestre_id>/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()
|
||||
# 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()
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/description/image")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/description/image")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formsemestre_get_description_image(formsemestre_id: int):
|
||||
"""Image de la description externe du formsemestre. Peut être vide.
|
||||
|
||||
formsemestre_id : l'id du formsemestre
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if not formsemestre.description or not formsemestre.description.image:
|
||||
return make_response("", 204) # 204 No Content status
|
||||
|
||||
return _image_response(formsemestre.description.image)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/description/photo_ens")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/description/photo_ens")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formsemestre_get_photo_ens(formsemestre_id: int):
|
||||
"""Photo du responsable, ou illustration du formsemestre. Peut être vide."""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if not formsemestre.description or not formsemestre.description.photo_ens:
|
||||
return make_response("", 204) # 204 No Content status
|
||||
|
||||
return _image_response(formsemestre.description.photo_ens)
|
||||
|
||||
|
||||
def _image_response(image_data: bytes):
|
||||
# Guess the mimetype based on the image data
|
||||
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"
|
||||
|
||||
response = make_response(image_data)
|
||||
response.headers["Content-Type"] = mimetype
|
||||
return response
|
||||
|
@ -1 +1,23 @@
|
||||
# empty but required for pylint
|
||||
"""WTF Forms for ScoDoc
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
|
||||
|
||||
class ScoDocForm(FlaskForm):
|
||||
"""Super class for ScoDoc forms
|
||||
(inspired by @iziram)
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Init form, adding a filed for our error messages"
|
||||
super().__init__(*args, **kwargs)
|
||||
self.ok = True
|
||||
self.error_messages: list[str] = [] # used to report our errors
|
||||
|
||||
def set_error(self, err_msg, field=None):
|
||||
"Set error message both in form and field"
|
||||
self.ok = False
|
||||
self.error_messages.append(err_msg)
|
||||
if field:
|
||||
field.errors.append(err_msg)
|
||||
|
112
app/forms/formsemestre/edit_description.py
Normal file
112
app/forms/formsemestre/edit_description.py
Normal file
@ -0,0 +1,112 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Formulaire édition description formsemestre
|
||||
"""
|
||||
from wtforms import (
|
||||
BooleanField,
|
||||
FileField,
|
||||
SelectField,
|
||||
StringField,
|
||||
TextAreaField,
|
||||
SubmitField,
|
||||
)
|
||||
from wtforms.validators import AnyOf, Optional
|
||||
|
||||
from app.forms import ScoDocForm
|
||||
from app.models import FORMSEMESTRE_DISPOSITIFS
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class DateDMYField(StringField):
|
||||
"Champ date JJ/MM/AAAA"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
render_kw = kwargs.pop("render_kw", {})
|
||||
render_kw.update({"class": "datepicker", "size": 10})
|
||||
super().__init__(*args, render_kw=render_kw, **kwargs)
|
||||
|
||||
# note: process_formdata(self, valuelist) ne fonctionne pas
|
||||
# en cas d'erreur de saisie les valeurs ne sont pas ré-affichées.
|
||||
# On vérifie donc les valeurs dans le code de la vue.
|
||||
|
||||
def process_data(self, value):
|
||||
"Process data from model to form"
|
||||
if value:
|
||||
self.data = value.strftime(scu.DATE_FMT)
|
||||
else:
|
||||
self.data = ""
|
||||
|
||||
|
||||
class FormSemestreDescriptionForm(ScoDocForm):
|
||||
"Formulaire édition description formsemestre"
|
||||
description = TextAreaField(
|
||||
"Description",
|
||||
validators=[Optional()],
|
||||
description="""texte libre : informations
|
||||
sur le contenu, les objectifs, les modalités d'évaluation, etc.""",
|
||||
)
|
||||
horaire = StringField(
|
||||
"Horaire", validators=[Optional()], description="ex: les lundis 9h-12h"
|
||||
)
|
||||
date_debut_inscriptions = DateDMYField(
|
||||
"Date de début des inscriptions",
|
||||
description="""date d'ouverture des inscriptions
|
||||
(laisser vide pour autoriser tout le temps)""",
|
||||
render_kw={
|
||||
"id": "date_debut_inscriptions",
|
||||
},
|
||||
)
|
||||
date_fin_inscriptions = DateDMYField(
|
||||
"Date de fin des inscriptions",
|
||||
render_kw={
|
||||
"id": "date_fin_inscriptions",
|
||||
},
|
||||
)
|
||||
image = FileField(
|
||||
"Image", validators=[Optional()], description="Image illustrant cette formation"
|
||||
)
|
||||
campus = StringField(
|
||||
"Campus", validators=[Optional()], description="ex: Villetaneuse"
|
||||
)
|
||||
salle = StringField("Salle", validators=[Optional()], description="ex: salle 123")
|
||||
dispositif = SelectField(
|
||||
"Dispositif",
|
||||
choices=FORMSEMESTRE_DISPOSITIFS.items(),
|
||||
coerce=int,
|
||||
description="modalité de formation",
|
||||
validators=[AnyOf(FORMSEMESTRE_DISPOSITIFS.keys())],
|
||||
)
|
||||
modalites_mcc = TextAreaField(
|
||||
"Modalités de contrôle des connaissances",
|
||||
validators=[Optional()],
|
||||
description="texte libre",
|
||||
)
|
||||
photo_ens = FileField(
|
||||
"Photo de l'enseignant(e)",
|
||||
validators=[Optional()],
|
||||
description="ou autre illustration",
|
||||
)
|
||||
public = StringField(
|
||||
"Public visé", validators=[Optional()], description="ex: débutants"
|
||||
)
|
||||
prerequis = TextAreaField(
|
||||
"Prérequis", validators=[Optional()], description="texte libre"
|
||||
)
|
||||
responsable = StringField(
|
||||
"Responsable",
|
||||
validators=[Optional()],
|
||||
description="""nom de l'enseignant de la formation, ou personne
|
||||
chargée de l'organisation du semestre.""",
|
||||
)
|
||||
|
||||
wip = BooleanField(
|
||||
"Travaux en cours",
|
||||
description="work in progress: si coché, affichera juste le titre du semestre",
|
||||
)
|
||||
|
||||
submit = SubmitField("Enregistrer")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
@ -14,12 +14,13 @@ class _EditModimplsCodesForm(FlaskForm):
|
||||
# construit dynamiquement ci-dessous
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def EditModimplsCodesForm(formsemestre: FormSemestre) -> _EditModimplsCodesForm:
|
||||
"Création d'un formulaire pour éditer les codes"
|
||||
|
||||
# Formulaire dynamique, on créé une classe ad-hoc
|
||||
class F(_EditModimplsCodesForm):
|
||||
pass
|
||||
"class factory"
|
||||
|
||||
def _gen_mod_form(modimpl: ModuleImpl):
|
||||
field = StringField(
|
||||
|
@ -38,7 +38,7 @@ class ScoDocModel(db.Model):
|
||||
__abstract__ = True # declare an abstract class for SQLAlchemy
|
||||
|
||||
def clone(self, not_copying=()):
|
||||
"""Clone, not copying the given attrs
|
||||
"""Clone, not copying the given attrs, and add to session.
|
||||
Attention: la copie n'a pas d'id avant le prochain flush ou commit.
|
||||
"""
|
||||
d = dict(self.__dict__)
|
||||
@ -188,6 +188,10 @@ from app.models.formsemestre import (
|
||||
NotesSemSet,
|
||||
notes_semset_formsemestre,
|
||||
)
|
||||
from app.models.formsemestre_descr import (
|
||||
FormSemestreDescription,
|
||||
FORMSEMESTRE_DISPOSITIFS,
|
||||
)
|
||||
from app.models.moduleimpls import (
|
||||
ModuleImpl,
|
||||
notes_modules_enseignants,
|
||||
|
@ -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
|
||||
@ -36,7 +36,7 @@ from app.models.config import ScoDocSiteConfig
|
||||
from app.models.departements import Departement
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.events import ScolarNews
|
||||
from app.models.events import Scolog, ScolarNews
|
||||
from app.models.formations import Formation
|
||||
from app.models.groups import GroupDescr, Partition
|
||||
from app.models.moduleimpls import (
|
||||
@ -45,9 +45,10 @@ from app.models.moduleimpls import (
|
||||
notes_modules_enseignants,
|
||||
)
|
||||
from app.models.modules import Module
|
||||
from app.models.scolar_event import ScolarEvent
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import codes_cursus, sco_preferences
|
||||
from app.scodoc import codes_cursus, sco_cache, sco_preferences
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric
|
||||
@ -69,6 +70,8 @@ class FormSemestre(models.ScoDocModel):
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||
titre = db.Column(db.Text(), nullable=False)
|
||||
# nb max d'inscriptions (non DEM), null si illimité:
|
||||
capacite_accueil = db.Column(db.Integer, nullable=True)
|
||||
date_debut = db.Column(db.Date(), nullable=False)
|
||||
date_fin = db.Column(db.Date(), nullable=False) # jour inclus
|
||||
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
|
||||
@ -143,6 +146,12 @@ class FormSemestre(models.ScoDocModel):
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
description = db.relationship(
|
||||
"FormSemestreDescription",
|
||||
back_populates="formsemestre",
|
||||
cascade="all, delete-orphan",
|
||||
uselist=False,
|
||||
)
|
||||
etuds = db.relationship(
|
||||
"Identite",
|
||||
secondary="notes_formsemestre_inscription",
|
||||
@ -1013,20 +1022,129 @@ class FormSemestre(models.ScoDocModel):
|
||||
codes |= {x.strip() for x in self.elt_passage_apo.split(",") if x}
|
||||
return codes
|
||||
|
||||
def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
|
||||
def get_inscrits(
|
||||
self, include_demdef=False, order=False, etats: set | None = None
|
||||
) -> list[Identite]:
|
||||
"""Liste des étudiants inscrits à ce semestre
|
||||
Si include_demdef, tous les étudiants, avec les démissionnaires
|
||||
et défaillants.
|
||||
Si etats, seuls les étudiants dans l'un des états indiqués.
|
||||
Si order, tri par clé sort_key
|
||||
"""
|
||||
if include_demdef:
|
||||
etuds = [ins.etud for ins in self.inscriptions]
|
||||
else:
|
||||
elif not etats:
|
||||
etuds = [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
|
||||
else:
|
||||
etuds = [ins.etud for ins in self.inscriptions if ins.etat in etats]
|
||||
if order:
|
||||
etuds.sort(key=lambda e: e.sort_key)
|
||||
return etuds
|
||||
|
||||
def inscrit_etudiant(
|
||||
self,
|
||||
etud: "Identite",
|
||||
etat: str = scu.INSCRIT,
|
||||
etape: str | None = None,
|
||||
method: str | None = None,
|
||||
) -> "FormSemestreInscription":
|
||||
"""Inscrit l'étudiant au semestre, ou renvoie son inscription s'il l'est déjà.
|
||||
Vérifie la capacité d'accueil si indiquée (non null): si le semestre est plein,
|
||||
lève une exception. Génère un évènement et un log étudiant.
|
||||
method: indique origine de l'inscription pour le log étudiant.
|
||||
"""
|
||||
# remplace ancien do_formsemestre_inscription_create()
|
||||
if not self.etat: # check lock
|
||||
raise ScoValueError("inscrit_etudiant: semestre verrouille")
|
||||
inscr = FormSemestreInscription.query.filter_by(
|
||||
formsemestre_id=self.id, etudid=etud.id
|
||||
).first()
|
||||
if inscr is not None:
|
||||
return inscr
|
||||
|
||||
if self.capacite_accueil is not None:
|
||||
# tous sauf démissionnaires:
|
||||
inscriptions = self.get_inscrits(etats={scu.INSCRIT, scu.DEF})
|
||||
if len(inscriptions) >= self.capacite_accueil:
|
||||
raise ScoValueError(
|
||||
f"Semestre {self.titre} complet : {len(self.inscriptions)} inscrits",
|
||||
dest_url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.id,
|
||||
),
|
||||
)
|
||||
|
||||
inscr = FormSemestreInscription(
|
||||
formsemestre_id=self.id, etudid=etud.id, etat=etat, etape=etape
|
||||
)
|
||||
db.session.add(inscr)
|
||||
# Évènement
|
||||
event = ScolarEvent(
|
||||
etudid=etud.id,
|
||||
formsemestre_id=self.id,
|
||||
event_type="INSCRIPTION",
|
||||
)
|
||||
db.session.add(event)
|
||||
# Log etudiant
|
||||
Scolog.logdb(
|
||||
method=method,
|
||||
etudid=etud.id,
|
||||
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]:
|
||||
|
82
app/models/formsemestre_descr.py
Normal file
82
app/models/formsemestre_descr.py
Normal file
@ -0,0 +1,82 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Description d'un formsemestre pour applications tierces.
|
||||
|
||||
Ces informations sont éditables dans ScoDoc et publiés sur l'API
|
||||
pour affichage dans l'application tierce.
|
||||
"""
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
|
||||
|
||||
class FormSemestreDescription(models.ScoDocModel):
|
||||
"""Informations décrivant un "semestre" (session) de formation
|
||||
pour un apprenant.
|
||||
"""
|
||||
|
||||
__tablename__ = "notes_formsemestre_description"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
description = db.Column(db.Text(), nullable=False, default="", server_default="")
|
||||
"description du cours, html autorisé"
|
||||
horaire = db.Column(db.Text(), nullable=False, default="", server_default="")
|
||||
"indication sur l'horaire, texte libre"
|
||||
date_debut_inscriptions = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||
date_fin_inscriptions = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||
|
||||
wip = db.Column(db.Boolean, nullable=False, default=False, server_default="false")
|
||||
"work in progress: si vrai, affichera juste le titre du semestre"
|
||||
|
||||
# Store image data directly in the database:
|
||||
image = db.Column(db.LargeBinary(), nullable=True)
|
||||
campus = db.Column(db.Text(), nullable=False, default="", server_default="")
|
||||
salle = db.Column(db.Text(), nullable=False, default="", server_default="")
|
||||
|
||||
dispositif = db.Column(db.Integer, nullable=False, default=0, server_default="0")
|
||||
"0 présentiel, 1 online, 2 hybride"
|
||||
modalites_mcc = db.Column(db.Text(), nullable=False, default="", server_default="")
|
||||
"modalités de contrôle des connaissances"
|
||||
photo_ens = db.Column(db.LargeBinary(), nullable=True)
|
||||
"photo de l'enseignant(e)"
|
||||
public = db.Column(db.Text(), nullable=False, default="", server_default="")
|
||||
"public visé"
|
||||
prerequis = db.Column(db.Text(), nullable=False, default="", server_default="")
|
||||
"prérequis (texte libre, html autorisé)"
|
||||
responsable = db.Column(db.Text(), nullable=False, default="", server_default="")
|
||||
"responsable du cours (texte libre, html autorisé)"
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
formsemestre = db.relationship(
|
||||
"FormSemestre", back_populates="description", uselist=False
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<FormSemestreDescription {self.id} {self.formsemestre}>"
|
||||
|
||||
def clone(self, not_copying=()) -> "FormSemestreDescription":
|
||||
"""clone instance"""
|
||||
return super().clone(not_copying=not_copying + ("formsemestre_id",))
|
||||
|
||||
def to_dict(self, exclude_images=True) -> dict:
|
||||
"dict, tous les attributs sauf les images"
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
if exclude_images:
|
||||
d.pop("image", None)
|
||||
d.pop("photo_ens", None)
|
||||
return d
|
||||
|
||||
|
||||
FORMSEMESTRE_DISPOSITIFS = {
|
||||
0: "présentiel",
|
||||
1: "en ligne",
|
||||
2: "hybride",
|
||||
}
|
@ -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)
|
||||
|
@ -53,6 +53,7 @@ _formsemestreEditor = ndb.EditableTable(
|
||||
"semestre_id",
|
||||
"formation_id",
|
||||
"titre",
|
||||
"capacite_accueil",
|
||||
"date_debut",
|
||||
"date_fin",
|
||||
"gestion_compensation",
|
||||
|
@ -345,8 +345,6 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
"labels": modalites_titles,
|
||||
},
|
||||
),
|
||||
]
|
||||
modform.append(
|
||||
(
|
||||
"semestre_id",
|
||||
{
|
||||
@ -362,10 +360,21 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
"attributes": ['onchange="change_semestre_id();"'] if is_apc else "",
|
||||
},
|
||||
),
|
||||
)
|
||||
(
|
||||
"capacite_accueil",
|
||||
{
|
||||
"title": "Capacité d'accueil",
|
||||
"size": 4,
|
||||
"explanation": "nombre max d'inscrits (hors démissionnaires). Laisser vide si pas de limite.",
|
||||
"type": "int",
|
||||
"allow_null": True,
|
||||
},
|
||||
),
|
||||
]
|
||||
etapes = sco_portal_apogee.get_etapes_apogee_dept()
|
||||
# Propose les etapes renvoyées par le portail
|
||||
# et ajoute les étapes du semestre qui ne sont pas dans la liste (soit la liste a changé, soit l'étape a été ajoutée manuellement)
|
||||
# et ajoute les étapes du semestre qui ne sont pas dans la liste
|
||||
# (soit la liste a changé, soit l'étape a été ajoutée manuellement)
|
||||
etapes_set = {et[0] for et in etapes}
|
||||
if edit:
|
||||
for etape_vdi in formsemestre.etapes_apo_vdi():
|
||||
@ -503,6 +512,12 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||
}">Modifier les codes Apogée et emploi du temps des modules</a>
|
||||
</p>
|
||||
|
||||
<p><a class="stdlink" href="{url_for("notes.edit_formsemestre_description",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||
}">Éditer la description externe du semestre</a>
|
||||
</p>
|
||||
|
||||
<h3>Sélectionner les modules, leurs responsables et les étudiants
|
||||
à inscrire:</h3>
|
||||
"""
|
||||
@ -838,6 +853,14 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
):
|
||||
msg = '<ul class="tf-msg"><li class="tf-msg">Code étape Apogée manquant</li></ul>'
|
||||
|
||||
# check capacité accueil si indiquée
|
||||
if edit and isinstance(tf[2]["capacite_accueil"], int):
|
||||
new_capacite_accueil = tf[2]["capacite_accueil"]
|
||||
inscriptions = formsemestre.get_inscrits(etats={scu.INSCRIT, scu.DEF})
|
||||
if len(inscriptions) > new_capacite_accueil:
|
||||
msg = f"""<ul class="tf-msg"><li class="tf-msg">Capacité d'accueil insuffisante
|
||||
(il y a {len(inscriptions)} inscrits non démissionaires)</li></ul>"""
|
||||
|
||||
if tf[0] == 0 or msg:
|
||||
return f"""<p>Formation <a class="discretelink" href="{
|
||||
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept,
|
||||
@ -1284,6 +1307,7 @@ def do_formsemestre_clone(
|
||||
clone_partitions=False,
|
||||
):
|
||||
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions.
|
||||
Clone description.
|
||||
New dates, responsable_id
|
||||
"""
|
||||
log(f"do_formsemestre_clone: {orig_formsemestre_id}")
|
||||
@ -1372,10 +1396,14 @@ def do_formsemestre_clone(
|
||||
|
||||
# 5- Copie les parcours
|
||||
formsemestre.parcours = formsemestre_orig.parcours
|
||||
|
||||
# 6- Copy description
|
||||
formsemestre.description = formsemestre_orig.description.clone()
|
||||
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
|
||||
# 6- Copy partitions and groups
|
||||
# 7- Copy partitions and groups
|
||||
if clone_partitions:
|
||||
sco_groups_copy.clone_partitions_and_groups(
|
||||
orig_formsemestre_id, formsemestre.id
|
||||
|
@ -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
|
||||
|
@ -85,43 +85,6 @@ def do_formsemestre_inscription_listinscrits(formsemestre_id):
|
||||
return r
|
||||
|
||||
|
||||
def do_formsemestre_inscription_create(args, method=None):
|
||||
"create a formsemestre_inscription (and sco event)"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
log(f"do_formsemestre_inscription_create: args={args}")
|
||||
sems = sco_formsemestre.do_formsemestre_list(
|
||||
{"formsemestre_id": args["formsemestre_id"]}
|
||||
)
|
||||
if len(sems) != 1:
|
||||
raise ScoValueError(f"code de semestre invalide: {args['formsemestre_id']}")
|
||||
sem = sems[0]
|
||||
# check lock
|
||||
if not sem["etat"]:
|
||||
raise ScoValueError("inscription: semestre verrouille")
|
||||
#
|
||||
r = _formsemestre_inscriptionEditor.create(cnx, args)
|
||||
# Evenement
|
||||
sco_etud.scolar_events_create(
|
||||
cnx,
|
||||
args={
|
||||
"etudid": args["etudid"],
|
||||
"event_date": time.strftime(scu.DATE_FMT),
|
||||
"formsemestre_id": args["formsemestre_id"],
|
||||
"event_type": "INSCRIPTION",
|
||||
},
|
||||
)
|
||||
# Log etudiant
|
||||
Scolog.logdb(
|
||||
method=method,
|
||||
etudid=args["etudid"],
|
||||
msg=f"inscription en semestre {args['formsemestre_id']}",
|
||||
commit=True,
|
||||
)
|
||||
#
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=args["formsemestre_id"])
|
||||
return r
|
||||
|
||||
|
||||
def do_formsemestre_inscription_delete(oid, formsemestre_id=None):
|
||||
"delete formsemestre_inscription"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
@ -219,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)
|
||||
@ -248,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):
|
||||
@ -263,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,
|
||||
@ -283,7 +236,9 @@ def do_formsemestre_inscription_with_modules(
|
||||
"""Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
|
||||
(donc sauf le sport)
|
||||
Si dept_id est spécifié, utilise ce département au lieu du courant.
|
||||
Vérifie la capacité d'accueil.
|
||||
"""
|
||||
etud = Identite.get_etud(etudid)
|
||||
group_ids = group_ids or []
|
||||
if isinstance(group_ids, int):
|
||||
group_ids = [group_ids]
|
||||
@ -294,13 +249,9 @@ def do_formsemestre_inscription_with_modules(
|
||||
if group_id != ""
|
||||
]
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id)
|
||||
# inscription au semestre
|
||||
# Inscription au semestre
|
||||
args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
|
||||
if etat is not None:
|
||||
args["etat"] = etat
|
||||
if etape is not None:
|
||||
args["etape"] = etape
|
||||
do_formsemestre_inscription_create(args, method=method)
|
||||
formsemestre.inscrit_etudiant(etud, etat=etat, etape=etape, method=method)
|
||||
log(
|
||||
f"""do_formsemestre_inscription_with_modules: etudid={
|
||||
etudid} formsemestre_id={formsemestre_id}"""
|
||||
@ -478,7 +429,7 @@ def formsemestre_inscription_with_modules(
|
||||
H.append("<ul>")
|
||||
for s in others:
|
||||
H.append(
|
||||
f"""<li><a href="{
|
||||
f"""<li><a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_desinscription", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=s["formsemestre_id"], etudid=etudid )
|
||||
}" class="stdlink">désinscrire de {s["titreannee"]}
|
||||
@ -486,13 +437,13 @@ def formsemestre_inscription_with_modules(
|
||||
)
|
||||
H.append("</ul>")
|
||||
H.append(
|
||||
f"""<p><a href="{ url_for( "notes.formsemestre_inscription_with_modules",
|
||||
f"""<p><a class="stdlink" href="{
|
||||
url_for( "notes.formsemestre_inscription_with_modules",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id,
|
||||
multiple_ok=1,
|
||||
group_ids=group_ids )
|
||||
}">Continuer quand même l'inscription</a>
|
||||
</p>"""
|
||||
# was sco_groups.make_query_groups(group_ids)
|
||||
)
|
||||
return render_template(
|
||||
"sco_page.j2",
|
||||
|
@ -59,8 +59,6 @@ import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_archives_formsemestre
|
||||
from app.scodoc import sco_assiduites as scass
|
||||
from app.scodoc import sco_bulletins
|
||||
@ -989,8 +987,8 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
|
||||
page_title = page_title or "Modules de "
|
||||
|
||||
H = [
|
||||
f"""<table>
|
||||
<tr><td class="fichetitre2">Formation: </td><td>
|
||||
f"""<table class="formsemestre_status_head">
|
||||
<tr><td class="fichetitre2">Formation : </td><td>
|
||||
<a href="{url_for('notes.ue_table',
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)}"
|
||||
class="discretelink" title="Formation {
|
||||
@ -1013,15 +1011,24 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
|
||||
sem_parcours = formsemestre.get_parcours_apc()
|
||||
H.append(
|
||||
f"""
|
||||
<tr><td class="fichetitre2">Parcours: </td>
|
||||
<tr><td class="fichetitre2">Parcours : </td>
|
||||
<td style="color: blue;">{', '.join(parcours.code for parcours in sem_parcours)}</td>
|
||||
</tr>
|
||||
"""
|
||||
)
|
||||
if formsemestre.capacite_accueil is not None:
|
||||
H.append(
|
||||
f"""
|
||||
<tr><td class="fichetitre2">Capacité d'accueil : </td>
|
||||
<td>{formsemestre.capacite_accueil}</td>
|
||||
</tr>
|
||||
"""
|
||||
)
|
||||
|
||||
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
|
||||
H.append(
|
||||
'<tr><td class="fichetitre2">Évaluations: </td><td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides'
|
||||
"""<tr><td class="fichetitre2">Évaluations : </td>
|
||||
<td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides"""
|
||||
% evals
|
||||
)
|
||||
if evals["last_modif"]:
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
@ -2320,6 +2333,7 @@ class BasePreferences:
|
||||
<option value="create">Spécifier valeur pour ce
|
||||
semestre seulement</option>
|
||||
</select>
|
||||
<span class="pref-comment">{descr["comment"]}</span>
|
||||
"""
|
||||
descr["explanation"] = menu_global
|
||||
|
||||
|
@ -1843,6 +1843,15 @@ div.formsemestre_status {
|
||||
/* EMO_WARNING, "⚠️" */
|
||||
}
|
||||
|
||||
table.formsemestre_status_head {
|
||||
border-collapse: collapse;
|
||||
|
||||
}
|
||||
|
||||
table.formsemestre_status_head tr td:nth-child(2) {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
table.formsemestre_status {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
@ -3310,6 +3319,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;
|
||||
|
@ -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
|
||||
);
|
||||
|
130
app/templates/formsemestre/edit_description.j2
Normal file
130
app/templates/formsemestre/edit_description.j2
Normal file
@ -0,0 +1,130 @@
|
||||
{% extends "sco_page.j2" %}
|
||||
{% import 'wtf.j2' as wtf %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<style>
|
||||
.field_descr {
|
||||
font-style: italic;
|
||||
color: green;
|
||||
}
|
||||
div.field_descr {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.submit {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
gap: 24px;
|
||||
}
|
||||
div.image {
|
||||
margin-left: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
div.image img {
|
||||
border: 1px dashed #b60c0c;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% macro render_string_field(field, size=64) %}
|
||||
<div>
|
||||
<span class="wtf-field">{{ field.label }} :</span>
|
||||
<span class="wtf-field">{{ field(size=size)|safe }}</span>
|
||||
{% for error in field.errors %}
|
||||
<span class="text-danger">{{ error }}</span>
|
||||
{% endfor %}
|
||||
{% if field.description %}
|
||||
<div class="field_descr">{{ field.description }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_textarea_field(field, cols=80, rows=12) %}
|
||||
<div>
|
||||
<div class="wtf-field">{{ field.label }} :</div>
|
||||
<div class="wtf-field">{{ field(cols=cols, rows=rows)|safe }}</div>
|
||||
{% if field.description %}
|
||||
<div class="field_descr">{{ field.description }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="tab-content">
|
||||
<h2>Édition de la description du semestre</h2>
|
||||
|
||||
<div class="help">
|
||||
<p>Les informations saisies ici ne sont pas utilisées par ScoDoc mais
|
||||
mises à disposition des applications tierces comme AutoSco.
|
||||
</p>
|
||||
<p>Tous les champs sont optionnels.</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<span class="wtf-field">{{ form.wip.label }}</span> <span class="wtf-field">{{form.wip() }}</span>
|
||||
<span class="field_descr">{{ form.wip.description }}</span>
|
||||
{{ 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 }}
|
||||
<div class="image">
|
||||
{% if formsemestre_description.photo_ens %}
|
||||
<img src="{{ url_for('apiweb.formsemestre_get_photo_ens',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id) }}"
|
||||
alt="Current Image" style="max-width: 200px;">
|
||||
<div>
|
||||
Changer l'image: {{ form.photo_ens() }}
|
||||
</div>
|
||||
{% else %}
|
||||
<em>Aucune photo ou illustration chargée.</em>
|
||||
{{ form.photo_ens() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{{ render_string_field(form.campus) }}
|
||||
{{ render_string_field(form.salle, size=32) }}
|
||||
{{ render_string_field(form.horaire) }}
|
||||
|
||||
<div>
|
||||
<span class="wtf-field">{{ form.dispositif.label }} :</span>
|
||||
<span class="wtf-field">{{ form.dispositif }}</span>
|
||||
{% if form.dispositif.description %}
|
||||
<div class="field_descr">{{ form.dispositif.description }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ render_string_field(form.public) }}
|
||||
{{ render_textarea_field(form.modalites_mcc, rows=8) }}
|
||||
{{ render_textarea_field(form.prerequis, rows=5) }}
|
||||
|
||||
{{ form.image.label }}
|
||||
<div class="image">
|
||||
{% if formsemestre_description.image %}
|
||||
<img src="{{ url_for('apiweb.formsemestre_get_description_image',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id) }}"
|
||||
alt="Current Image" style="max-width: 400px;">
|
||||
<div>
|
||||
Changer l'image: {{ form.image() }}
|
||||
</div>
|
||||
{% else %}
|
||||
<em>Aucune image n'est actuellement associée à ce semestre.</em>
|
||||
{{ form.image() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="submit">
|
||||
{{ form.submit }} {{ form.cancel }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
@ -25,20 +25,34 @@
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Vues "modernes" des formsemestre
|
||||
Vues "modernes" des formsemestres
|
||||
Emmanuel Viennet, 2023
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import io
|
||||
|
||||
from flask import flash, redirect, render_template, url_for
|
||||
from flask import g, request
|
||||
from flask import current_app, g, request
|
||||
import PIL
|
||||
|
||||
from app import db, log
|
||||
from app.decorators import (
|
||||
scodoc,
|
||||
permission_required,
|
||||
)
|
||||
from app.forms.formsemestre import change_formation, edit_modimpls_codes_apo
|
||||
from app.models import Formation, FormSemestre, ScoDocSiteConfig
|
||||
from app.forms.formsemestre import (
|
||||
change_formation,
|
||||
edit_modimpls_codes_apo,
|
||||
edit_description,
|
||||
)
|
||||
from app.models import (
|
||||
Formation,
|
||||
FormSemestre,
|
||||
FormSemestreDescription,
|
||||
FORMSEMESTRE_DISPOSITIFS,
|
||||
ScoDocSiteConfig,
|
||||
)
|
||||
from app.scodoc import (
|
||||
sco_edt_cal,
|
||||
sco_formations,
|
||||
@ -223,3 +237,121 @@ def formsemestre_edt_help_config(formsemestre_id: int):
|
||||
ScoDocSiteConfig=ScoDocSiteConfig,
|
||||
title="Aide configuration EDT",
|
||||
)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formsemestre_description/<int:formsemestre_id>/edit", methods=["GET", "POST"]
|
||||
)
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormSemestre)
|
||||
def edit_formsemestre_description(formsemestre_id: int):
|
||||
"Edition de la description d'un formsemestre"
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if not formsemestre.description:
|
||||
formsemestre.description = FormSemestreDescription()
|
||||
db.session.add(formsemestre)
|
||||
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(
|
||||
url_for(
|
||||
"notes.formsemestre_editwithmodules",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
)
|
||||
)
|
||||
# Vérification valeur dispositif
|
||||
if form.dispositif.data not in FORMSEMESTRE_DISPOSITIFS:
|
||||
flash("Dispositif inconnu", "danger")
|
||||
ok = False
|
||||
|
||||
# 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
|
||||
)
|
||||
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
|
||||
)
|
||||
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,
|
||||
)
|
||||
)
|
||||
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()
|
||||
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",
|
||||
form=form,
|
||||
formsemestre=formsemestre,
|
||||
formsemestre_description=formsemestre_description,
|
||||
sco=ScoData(formsemestre=formsemestre),
|
||||
title="Modif. description semestre",
|
||||
)
|
||||
|
@ -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,
|
||||
|
51
migrations/versions/2640b7686de6_formsemestre_description.py
Normal file
51
migrations/versions/2640b7686de6_formsemestre_description.py
Normal file
@ -0,0 +1,51 @@
|
||||
"""FormSemestreDescription et capacité d'accueil
|
||||
|
||||
Revision ID: 2640b7686de6
|
||||
Revises: f6cb3d4e44ec
|
||||
Create Date: 2024-08-11 15:44:32.560054
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "2640b7686de6"
|
||||
down_revision = "f6cb3d4e44ec"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"notes_formsemestre_description",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("description", 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"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("capacite_accueil", sa.Integer(), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op:
|
||||
batch_op.drop_column("capacite_accueil")
|
||||
op.drop_table("notes_formsemestre_description")
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -16,6 +16,7 @@ Utilisation :
|
||||
Lancer :
|
||||
pytest tests/api/test_api_formsemestre.py
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import requests
|
||||
from types import NoneType
|
||||
@ -28,8 +29,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 +783,64 @@ 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
|
||||
# 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={
|
||||
"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
|
||||
# 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
|
||||
|
@ -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:
|
||||
|
@ -3,12 +3,12 @@
|
||||
|
||||
""" Test création/accès/clonage formsemestre
|
||||
"""
|
||||
from flask import Response
|
||||
from flask import g, Response
|
||||
import pytest
|
||||
from tests.unit import yaml_setup, call_view
|
||||
|
||||
import app
|
||||
from app.models import Formation, FormSemestre
|
||||
from app import db
|
||||
from app.models import Formation, FormSemestre, FormSemestreDescription
|
||||
from app.scodoc import (
|
||||
sco_archives_formsemestre,
|
||||
sco_cost_formation,
|
||||
@ -35,6 +35,7 @@ from app.scodoc import (
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.views import notes, scolar
|
||||
from config import TestConfig
|
||||
from tests.unit import yaml_setup, call_view
|
||||
|
||||
DEPT = TestConfig.DEPT_TEST
|
||||
|
||||
@ -205,3 +206,39 @@ def test_formsemestre_misc_views(test_client):
|
||||
ans = sco_debouche.report_debouche_date(start_year=2000)
|
||||
ans = sco_cost_formation.formsemestre_estim_cost(formsemestre.id)
|
||||
# pas de test des indicateurs de suivi BUT
|
||||
|
||||
|
||||
def test_formsemestre_description(test_client):
|
||||
"""Test FormSemestreDescription"""
|
||||
app.set_sco_dept(DEPT)
|
||||
#
|
||||
nb_descriptions = FormSemestreDescription.query.count()
|
||||
# Création d'un semestre
|
||||
|
||||
formsemestre = FormSemestre(
|
||||
dept_id=g.scodoc_dept_id,
|
||||
titre="test description",
|
||||
date_debut="2024-08-01",
|
||||
date_fin="2024-08-31",
|
||||
)
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
assert formsemestre.description is None
|
||||
# Association d'une description
|
||||
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()
|
||||
assert formsemestre.description.formsemestre.id == formsemestre.id
|
||||
assert FormSemestreDescription.query.count() == nb_descriptions + 1
|
||||
# Suppression / cascade
|
||||
db.session.delete(formsemestre)
|
||||
db.session.commit()
|
||||
assert FormSemestreDescription.query.count() == nb_descriptions
|
||||
|
Loading…
Reference in New Issue
Block a user