forked from ScoDoc/ScoDoc
merge el
This commit is contained in:
commit
6ab027dffe
@ -13,11 +13,14 @@
|
|||||||
FormSemestre
|
FormSemestre
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
from operator import attrgetter, itemgetter
|
from operator import attrgetter, itemgetter
|
||||||
|
|
||||||
from flask import g, make_response, request
|
from flask import g, make_response, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
import PIL
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import app
|
import app
|
||||||
from app import db, log
|
from app import db, log
|
||||||
@ -32,6 +35,7 @@ from app.models import (
|
|||||||
Departement,
|
Departement,
|
||||||
Evaluation,
|
Evaluation,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
|
FormSemestreDescription,
|
||||||
FormSemestreEtape,
|
FormSemestreEtape,
|
||||||
FormSemestreInscription,
|
FormSemestreInscription,
|
||||||
Identite,
|
Identite,
|
||||||
@ -790,3 +794,95 @@ def formsemestre_edt(formsemestre_id: int):
|
|||||||
return sco_edt_cal.formsemestre_edt_dict(
|
return sco_edt_cal.formsemestre_edt_dict(
|
||||||
formsemestre, group_ids=group_ids, show_modules_titles=show_modules_titles
|
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
|
# construit dynamiquement ci-dessous
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
def EditModimplsCodesForm(formsemestre: FormSemestre) -> _EditModimplsCodesForm:
|
def EditModimplsCodesForm(formsemestre: FormSemestre) -> _EditModimplsCodesForm:
|
||||||
"Création d'un formulaire pour éditer les codes"
|
"Création d'un formulaire pour éditer les codes"
|
||||||
|
|
||||||
# Formulaire dynamique, on créé une classe ad-hoc
|
# Formulaire dynamique, on créé une classe ad-hoc
|
||||||
class F(_EditModimplsCodesForm):
|
class F(_EditModimplsCodesForm):
|
||||||
pass
|
"class factory"
|
||||||
|
|
||||||
def _gen_mod_form(modimpl: ModuleImpl):
|
def _gen_mod_form(modimpl: ModuleImpl):
|
||||||
field = StringField(
|
field = StringField(
|
||||||
|
@ -38,7 +38,7 @@ class ScoDocModel(db.Model):
|
|||||||
__abstract__ = True # declare an abstract class for SQLAlchemy
|
__abstract__ = True # declare an abstract class for SQLAlchemy
|
||||||
|
|
||||||
def clone(self, not_copying=()):
|
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.
|
Attention: la copie n'a pas d'id avant le prochain flush ou commit.
|
||||||
"""
|
"""
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
@ -188,6 +188,10 @@ from app.models.formsemestre import (
|
|||||||
NotesSemSet,
|
NotesSemSet,
|
||||||
notes_semset_formsemestre,
|
notes_semset_formsemestre,
|
||||||
)
|
)
|
||||||
|
from app.models.formsemestre_descr import (
|
||||||
|
FormSemestreDescription,
|
||||||
|
FORMSEMESTRE_DISPOSITIFS,
|
||||||
|
)
|
||||||
from app.models.moduleimpls import (
|
from app.models.moduleimpls import (
|
||||||
ModuleImpl,
|
ModuleImpl,
|
||||||
notes_modules_enseignants,
|
notes_modules_enseignants,
|
||||||
|
@ -23,7 +23,7 @@ from sqlalchemy.sql import text
|
|||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
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.auth.models import User
|
||||||
from app import models
|
from app import models
|
||||||
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
|
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.departements import Departement
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.evaluations import Evaluation
|
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.formations import Formation
|
||||||
from app.models.groups import GroupDescr, Partition
|
from app.models.groups import GroupDescr, Partition
|
||||||
from app.models.moduleimpls import (
|
from app.models.moduleimpls import (
|
||||||
@ -45,9 +45,10 @@ from app.models.moduleimpls import (
|
|||||||
notes_modules_enseignants,
|
notes_modules_enseignants,
|
||||||
)
|
)
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
|
from app.models.scolar_event import ScolarEvent
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.models.validations import ScolarFormSemestreValidation
|
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_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric
|
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"))
|
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||||
titre = db.Column(db.Text(), nullable=False)
|
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_debut = db.Column(db.Date(), nullable=False)
|
||||||
date_fin = db.Column(db.Date(), nullable=False) # jour inclus
|
date_fin = db.Column(db.Date(), nullable=False) # jour inclus
|
||||||
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
|
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
|
||||||
@ -143,6 +146,12 @@ class FormSemestre(models.ScoDocModel):
|
|||||||
lazy="dynamic",
|
lazy="dynamic",
|
||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
)
|
)
|
||||||
|
description = db.relationship(
|
||||||
|
"FormSemestreDescription",
|
||||||
|
back_populates="formsemestre",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
uselist=False,
|
||||||
|
)
|
||||||
etuds = db.relationship(
|
etuds = db.relationship(
|
||||||
"Identite",
|
"Identite",
|
||||||
secondary="notes_formsemestre_inscription",
|
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}
|
codes |= {x.strip() for x in self.elt_passage_apo.split(",") if x}
|
||||||
return codes
|
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
|
"""Liste des étudiants inscrits à ce semestre
|
||||||
Si include_demdef, tous les étudiants, avec les démissionnaires
|
Si include_demdef, tous les étudiants, avec les démissionnaires
|
||||||
et défaillants.
|
et défaillants.
|
||||||
|
Si etats, seuls les étudiants dans l'un des états indiqués.
|
||||||
Si order, tri par clé sort_key
|
Si order, tri par clé sort_key
|
||||||
"""
|
"""
|
||||||
if include_demdef:
|
if include_demdef:
|
||||||
etuds = [ins.etud for ins in self.inscriptions]
|
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]
|
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:
|
if order:
|
||||||
etuds.sort(key=lambda e: e.sort_key)
|
etuds.sort(key=lambda e: e.sort_key)
|
||||||
return etuds
|
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(
|
def get_partitions_list(
|
||||||
self, with_default=True, only_listed=False
|
self, with_default=True, only_listed=False
|
||||||
) -> list[Partition]:
|
) -> 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
|
import app.scodoc.sco_utils as scu
|
||||||
from app import log
|
from app import log
|
||||||
|
from app.models.scolar_event import ScolarEvent
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc import notesdb, sco_preferences
|
from app.scodoc import notesdb, sco_preferences
|
||||||
|
|
||||||
@ -638,11 +639,12 @@ def excel_feuille_listeappel(
|
|||||||
lines,
|
lines,
|
||||||
partitions=None,
|
partitions=None,
|
||||||
with_codes=False,
|
with_codes=False,
|
||||||
|
with_date_inscription=False,
|
||||||
with_paiement=False,
|
with_paiement=False,
|
||||||
server_name=None,
|
server_name=None,
|
||||||
edt_params: dict = None,
|
edt_params: dict = None,
|
||||||
):
|
):
|
||||||
"""generation feuille appel
|
"""Génération feuille appel.
|
||||||
|
|
||||||
edt_params :
|
edt_params :
|
||||||
- "discipline" : Discipline
|
- "discipline" : Discipline
|
||||||
@ -763,7 +765,8 @@ def excel_feuille_listeappel(
|
|||||||
cells.append(ws.make_cell("etudid", style3))
|
cells.append(ws.make_cell("etudid", style3))
|
||||||
cells.append(ws.make_cell("code_nip", style3))
|
cells.append(ws.make_cell("code_nip", style3))
|
||||||
cells.append(ws.make_cell("code_ine", style3))
|
cells.append(ws.make_cell("code_ine", style3))
|
||||||
|
if with_date_inscription:
|
||||||
|
cells.append(ws.make_cell("Date inscr.", style3))
|
||||||
# case Groupes
|
# case Groupes
|
||||||
cells.append(ws.make_cell("Groupes", style3))
|
cells.append(ws.make_cell("Groupes", style3))
|
||||||
letter_int += 1
|
letter_int += 1
|
||||||
@ -805,7 +808,15 @@ def excel_feuille_listeappel(
|
|||||||
cells.append(ws.make_cell(code_nip, style2t3))
|
cells.append(ws.make_cell(code_nip, style2t3))
|
||||||
code_ine = t.get("code_ine", "")
|
code_ine = t.get("code_ine", "")
|
||||||
cells.append(ws.make_cell(code_ine, style2t3))
|
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))
|
cells.append(ws.make_cell(style=style2t3))
|
||||||
ws.append_row(cells)
|
ws.append_row(cells)
|
||||||
ws.set_row_dimension_height(row_id, 30)
|
ws.set_row_dimension_height(row_id, 30)
|
||||||
|
@ -53,6 +53,7 @@ _formsemestreEditor = ndb.EditableTable(
|
|||||||
"semestre_id",
|
"semestre_id",
|
||||||
"formation_id",
|
"formation_id",
|
||||||
"titre",
|
"titre",
|
||||||
|
"capacite_accueil",
|
||||||
"date_debut",
|
"date_debut",
|
||||||
"date_fin",
|
"date_fin",
|
||||||
"gestion_compensation",
|
"gestion_compensation",
|
||||||
|
@ -345,8 +345,6 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
|||||||
"labels": modalites_titles,
|
"labels": modalites_titles,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
|
||||||
modform.append(
|
|
||||||
(
|
(
|
||||||
"semestre_id",
|
"semestre_id",
|
||||||
{
|
{
|
||||||
@ -362,10 +360,21 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
|||||||
"attributes": ['onchange="change_semestre_id();"'] if is_apc else "",
|
"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()
|
etapes = sco_portal_apogee.get_etapes_apogee_dept()
|
||||||
# Propose les etapes renvoyées par le portail
|
# 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}
|
etapes_set = {et[0] for et in etapes}
|
||||||
if edit:
|
if edit:
|
||||||
for etape_vdi in formsemestre.etapes_apo_vdi():
|
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)
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||||
}">Modifier les codes Apogée et emploi du temps des modules</a>
|
}">Modifier les codes Apogée et emploi du temps des modules</a>
|
||||||
</p>
|
</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
|
<h3>Sélectionner les modules, leurs responsables et les étudiants
|
||||||
à inscrire:</h3>
|
à 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>'
|
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:
|
if tf[0] == 0 or msg:
|
||||||
return f"""<p>Formation <a class="discretelink" href="{
|
return f"""<p>Formation <a class="discretelink" href="{
|
||||||
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept,
|
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept,
|
||||||
@ -1284,6 +1307,7 @@ def do_formsemestre_clone(
|
|||||||
clone_partitions=False,
|
clone_partitions=False,
|
||||||
):
|
):
|
||||||
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions.
|
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions.
|
||||||
|
Clone description.
|
||||||
New dates, responsable_id
|
New dates, responsable_id
|
||||||
"""
|
"""
|
||||||
log(f"do_formsemestre_clone: {orig_formsemestre_id}")
|
log(f"do_formsemestre_clone: {orig_formsemestre_id}")
|
||||||
@ -1372,10 +1396,14 @@ def do_formsemestre_clone(
|
|||||||
|
|
||||||
# 5- Copie les parcours
|
# 5- Copie les parcours
|
||||||
formsemestre.parcours = formsemestre_orig.parcours
|
formsemestre.parcours = formsemestre_orig.parcours
|
||||||
|
|
||||||
|
# 6- Copy description
|
||||||
|
formsemestre.description = formsemestre_orig.description.clone()
|
||||||
|
|
||||||
db.session.add(formsemestre)
|
db.session.add(formsemestre)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# 6- Copy partitions and groups
|
# 7- Copy partitions and groups
|
||||||
if clone_partitions:
|
if clone_partitions:
|
||||||
sco_groups_copy.clone_partitions_and_groups(
|
sco_groups_copy.clone_partitions_and_groups(
|
||||||
orig_formsemestre_id, formsemestre.id
|
orig_formsemestre_id, formsemestre.id
|
||||||
|
@ -51,7 +51,6 @@ import app.scodoc.sco_utils as scu
|
|||||||
from app import log
|
from app import log
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||||
from app.scodoc import html_sco_header
|
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
|
||||||
from app.scodoc import sco_formsemestre_inscriptions
|
from app.scodoc import sco_formsemestre_inscriptions
|
||||||
from app.scodoc import sco_formsemestre_validation
|
from app.scodoc import sco_formsemestre_validation
|
||||||
|
@ -85,43 +85,6 @@ def do_formsemestre_inscription_listinscrits(formsemestre_id):
|
|||||||
return r
|
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):
|
def do_formsemestre_inscription_delete(oid, formsemestre_id=None):
|
||||||
"delete formsemestre_inscription"
|
"delete formsemestre_inscription"
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
@ -219,12 +182,11 @@ def do_formsemestre_desinscription(
|
|||||||
if check_has_dec_jury:
|
if check_has_dec_jury:
|
||||||
check_if_has_decision_jury(formsemestre, [etudid])
|
check_if_has_decision_jury(formsemestre, [etudid])
|
||||||
|
|
||||||
insem = do_formsemestre_inscription_list(
|
inscr_sem = FormSemestreInscription.query.filter_by(
|
||||||
args={"formsemestre_id": formsemestre_id, "etudid": etudid}
|
etudid=etudid, formsemestre_id=formsemestre_id
|
||||||
)
|
).first()
|
||||||
if not insem:
|
if not inscr_sem:
|
||||||
raise ScoValueError(f"{etud.nomprenom} n'est pas inscrit au semestre !")
|
raise ScoValueError(f"{etud.nomprenom} n'est pas inscrit au semestre !")
|
||||||
insem = insem[0]
|
|
||||||
# -- desinscription de tous les modules
|
# -- desinscription de tous les modules
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||||
@ -248,10 +210,8 @@ def do_formsemestre_desinscription(
|
|||||||
Partition.formsemestre_remove_etud(formsemestre_id, etud)
|
Partition.formsemestre_remove_etud(formsemestre_id, etud)
|
||||||
|
|
||||||
# -- désincription du semestre
|
# -- désincription du semestre
|
||||||
do_formsemestre_inscription_delete(
|
formsemestre.desinscrit_etudiant(etud)
|
||||||
insem["formsemestre_inscription_id"], formsemestre_id=formsemestre_id
|
|
||||||
)
|
|
||||||
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
|
|
||||||
# --- Semestre extérieur
|
# --- Semestre extérieur
|
||||||
if formsemestre.modalite == "EXT":
|
if formsemestre.modalite == "EXT":
|
||||||
if 0 == len(formsemestre.inscriptions):
|
if 0 == len(formsemestre.inscriptions):
|
||||||
@ -263,13 +223,6 @@ def do_formsemestre_desinscription(
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(f"Semestre extérieur supprimé: {formsemestre.titre_annee()}")
|
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(
|
def do_formsemestre_inscription_with_modules(
|
||||||
formsemestre_id,
|
formsemestre_id,
|
||||||
@ -283,7 +236,9 @@ def do_formsemestre_inscription_with_modules(
|
|||||||
"""Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
|
"""Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
|
||||||
(donc sauf le sport)
|
(donc sauf le sport)
|
||||||
Si dept_id est spécifié, utilise ce département au lieu du courant.
|
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 []
|
group_ids = group_ids or []
|
||||||
if isinstance(group_ids, int):
|
if isinstance(group_ids, int):
|
||||||
group_ids = [group_ids]
|
group_ids = [group_ids]
|
||||||
@ -294,13 +249,9 @@ def do_formsemestre_inscription_with_modules(
|
|||||||
if group_id != ""
|
if group_id != ""
|
||||||
]
|
]
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id)
|
||||||
# inscription au semestre
|
# Inscription au semestre
|
||||||
args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
|
args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
|
||||||
if etat is not None:
|
formsemestre.inscrit_etudiant(etud, etat=etat, etape=etape, method=method)
|
||||||
args["etat"] = etat
|
|
||||||
if etape is not None:
|
|
||||||
args["etape"] = etape
|
|
||||||
do_formsemestre_inscription_create(args, method=method)
|
|
||||||
log(
|
log(
|
||||||
f"""do_formsemestre_inscription_with_modules: etudid={
|
f"""do_formsemestre_inscription_with_modules: etudid={
|
||||||
etudid} formsemestre_id={formsemestre_id}"""
|
etudid} formsemestre_id={formsemestre_id}"""
|
||||||
@ -478,7 +429,7 @@ def formsemestre_inscription_with_modules(
|
|||||||
H.append("<ul>")
|
H.append("<ul>")
|
||||||
for s in others:
|
for s in others:
|
||||||
H.append(
|
H.append(
|
||||||
f"""<li><a href="{
|
f"""<li><a class="stdlink" href="{
|
||||||
url_for("notes.formsemestre_desinscription", scodoc_dept=g.scodoc_dept,
|
url_for("notes.formsemestre_desinscription", scodoc_dept=g.scodoc_dept,
|
||||||
formsemestre_id=s["formsemestre_id"], etudid=etudid )
|
formsemestre_id=s["formsemestre_id"], etudid=etudid )
|
||||||
}" class="stdlink">désinscrire de {s["titreannee"]}
|
}" class="stdlink">désinscrire de {s["titreannee"]}
|
||||||
@ -486,13 +437,13 @@ def formsemestre_inscription_with_modules(
|
|||||||
)
|
)
|
||||||
H.append("</ul>")
|
H.append("</ul>")
|
||||||
H.append(
|
H.append(
|
||||||
f"""<p><a href="{ url_for( "notes.formsemestre_inscription_with_modules",
|
f"""<p><a class="stdlink" href="{
|
||||||
scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id,
|
url_for( "notes.formsemestre_inscription_with_modules",
|
||||||
multiple_ok=1,
|
scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id,
|
||||||
group_ids=group_ids )
|
multiple_ok=1,
|
||||||
|
group_ids=group_ids )
|
||||||
}">Continuer quand même l'inscription</a>
|
}">Continuer quand même l'inscription</a>
|
||||||
</p>"""
|
</p>"""
|
||||||
# was sco_groups.make_query_groups(group_ids)
|
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"sco_page.j2",
|
"sco_page.j2",
|
||||||
|
@ -59,8 +59,6 @@ import app.scodoc.sco_utils as scu
|
|||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
from app.scodoc import codes_cursus
|
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_archives_formsemestre
|
||||||
from app.scodoc import sco_assiduites as scass
|
from app.scodoc import sco_assiduites as scass
|
||||||
from app.scodoc import sco_bulletins
|
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 "
|
page_title = page_title or "Modules de "
|
||||||
|
|
||||||
H = [
|
H = [
|
||||||
f"""<table>
|
f"""<table class="formsemestre_status_head">
|
||||||
<tr><td class="fichetitre2">Formation: </td><td>
|
<tr><td class="fichetitre2">Formation : </td><td>
|
||||||
<a href="{url_for('notes.ue_table',
|
<a href="{url_for('notes.ue_table',
|
||||||
scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)}"
|
scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)}"
|
||||||
class="discretelink" title="Formation {
|
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()
|
sem_parcours = formsemestre.get_parcours_apc()
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
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>
|
<td style="color: blue;">{', '.join(parcours.code for parcours in sem_parcours)}</td>
|
||||||
</tr>
|
</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)
|
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
|
||||||
H.append(
|
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
|
% evals
|
||||||
)
|
)
|
||||||
if evals["last_modif"]:
|
if evals["last_modif"]:
|
||||||
|
@ -40,7 +40,7 @@ from flask import url_for, g, render_template, request
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import FormSemestre, Identite
|
from app.models import FormSemestre, Identite, ScolarEvent
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_assiduites as scass
|
from app.scodoc import sco_assiduites as scass
|
||||||
@ -70,6 +70,7 @@ def groups_lists(
|
|||||||
group_ids=(),
|
group_ids=(),
|
||||||
fmt="html",
|
fmt="html",
|
||||||
with_codes=0,
|
with_codes=0,
|
||||||
|
with_date_inscription=0,
|
||||||
etat=None,
|
etat=None,
|
||||||
with_paiement=0,
|
with_paiement=0,
|
||||||
with_archives=0,
|
with_archives=0,
|
||||||
@ -102,6 +103,7 @@ def groups_lists(
|
|||||||
groups_infos=groups_infos,
|
groups_infos=groups_infos,
|
||||||
fmt=fmt,
|
fmt=fmt,
|
||||||
with_codes=with_codes,
|
with_codes=with_codes,
|
||||||
|
with_date_inscription=with_date_inscription,
|
||||||
etat=etat,
|
etat=etat,
|
||||||
with_paiement=with_paiement,
|
with_paiement=with_paiement,
|
||||||
with_archives=with_archives,
|
with_archives=with_archives,
|
||||||
@ -121,6 +123,7 @@ def groups_lists(
|
|||||||
groups_infos=groups_infos,
|
groups_infos=groups_infos,
|
||||||
fmt=fmt,
|
fmt=fmt,
|
||||||
with_codes=with_codes,
|
with_codes=with_codes,
|
||||||
|
with_date_inscription=with_date_inscription,
|
||||||
etat=etat,
|
etat=etat,
|
||||||
with_paiement=with_paiement,
|
with_paiement=with_paiement,
|
||||||
with_archives=with_archives,
|
with_archives=with_archives,
|
||||||
@ -507,6 +510,7 @@ class DisplayedGroupsInfos:
|
|||||||
def groups_table(
|
def groups_table(
|
||||||
groups_infos: DisplayedGroupsInfos = None,
|
groups_infos: DisplayedGroupsInfos = None,
|
||||||
with_codes=0,
|
with_codes=0,
|
||||||
|
with_date_inscription=0,
|
||||||
etat=None,
|
etat=None,
|
||||||
fmt="html",
|
fmt="html",
|
||||||
with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail)
|
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))
|
can_view_etud_data = int(current_user.has_permission(Permission.ViewEtudData))
|
||||||
with_codes = int(with_codes)
|
with_codes = int(with_codes)
|
||||||
|
with_date_inscription = int(with_date_inscription)
|
||||||
with_paiement = int(with_paiement) and can_view_etud_data
|
with_paiement = int(with_paiement) and can_view_etud_data
|
||||||
with_archives = int(with_archives) 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_annotations = int(with_annotations) and can_view_etud_data
|
||||||
with_bourse = int(with_bourse) 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 = (
|
||||||
base_url_np
|
groups_infos.base_url
|
||||||
+ f"""&with_paiement={with_paiement}&with_archives={
|
+ f"""&with_codes={with_codes}&with_date_inscription={
|
||||||
|
with_date_inscription}&with_paiement={with_paiement}&with_archives={
|
||||||
with_archives}&with_annotations={with_annotations
|
with_archives}&with_annotations={with_annotations
|
||||||
}&with_bourse={with_bourse}"""
|
}&with_bourse={with_bourse}"""
|
||||||
)
|
)
|
||||||
@ -546,6 +551,7 @@ def groups_table(
|
|||||||
"etudid": "etudid",
|
"etudid": "etudid",
|
||||||
"code_nip": "code_nip",
|
"code_nip": "code_nip",
|
||||||
"code_ine": "code_ine",
|
"code_ine": "code_ine",
|
||||||
|
"date_inscription": "Date inscription",
|
||||||
"datefinalisationinscription_str": "Finalisation inscr.",
|
"datefinalisationinscription_str": "Finalisation inscr.",
|
||||||
"paiementinscription_str": "Paiement",
|
"paiementinscription_str": "Paiement",
|
||||||
"etudarchive": "Fichiers",
|
"etudarchive": "Fichiers",
|
||||||
@ -579,9 +585,11 @@ def groups_table(
|
|||||||
|
|
||||||
if with_codes:
|
if with_codes:
|
||||||
columns_ids += ["etape", "etudid", "code_nip", "code_ine"]
|
columns_ids += ["etape", "etudid", "code_nip", "code_ine"]
|
||||||
|
if with_date_inscription:
|
||||||
|
columns_ids += ["date_inscription"]
|
||||||
if with_paiement:
|
if with_paiement:
|
||||||
columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"]
|
columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"]
|
||||||
if with_paiement: # or with_codes:
|
if with_paiement:
|
||||||
sco_portal_apogee.check_paiement_etuds(groups_infos.members)
|
sco_portal_apogee.check_paiement_etuds(groups_infos.members)
|
||||||
if with_archives:
|
if with_archives:
|
||||||
from app.scodoc import sco_archives_etud
|
from app.scodoc import sco_archives_etud
|
||||||
@ -597,6 +605,16 @@ def groups_table(
|
|||||||
moodle_groupenames = set()
|
moodle_groupenames = set()
|
||||||
# ajoute liens
|
# ajoute liens
|
||||||
for etud_info in groups_infos.members:
|
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"]:
|
if etud_info["email"]:
|
||||||
etud_info["_email_target"] = "mailto:" + etud_info["email"]
|
etud_info["_email_target"] = "mailto:" + etud_info["email"]
|
||||||
else:
|
else:
|
||||||
@ -612,8 +630,8 @@ def groups_table(
|
|||||||
etud_info["_nom_disp_order"] = etud_sort_key(etud_info)
|
etud_info["_nom_disp_order"] = etud_sort_key(etud_info)
|
||||||
etud_info["_prenom_target"] = fiche_url
|
etud_info["_prenom_target"] = fiche_url
|
||||||
|
|
||||||
etud_info["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (
|
etud_info["_nom_disp_td_attrs"] = (
|
||||||
etud_info["etudid"]
|
f"""id="{etud_info['etudid']}" class="etudinfo" """
|
||||||
)
|
)
|
||||||
etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non"
|
etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non"
|
||||||
if etud_info["etat"] == "D":
|
if etud_info["etat"] == "D":
|
||||||
@ -720,6 +738,7 @@ def groups_table(
|
|||||||
if groups_infos.members:
|
if groups_infos.members:
|
||||||
options = {
|
options = {
|
||||||
"with_codes": "Affiche codes",
|
"with_codes": "Affiche codes",
|
||||||
|
"with_date_inscription": "Date inscription",
|
||||||
}
|
}
|
||||||
if can_view_etud_data:
|
if can_view_etud_data:
|
||||||
options.update(
|
options.update(
|
||||||
@ -824,6 +843,7 @@ def groups_table(
|
|||||||
groups_infos.members,
|
groups_infos.members,
|
||||||
partitions=groups_infos.partitions,
|
partitions=groups_infos.partitions,
|
||||||
with_codes=with_codes,
|
with_codes=with_codes,
|
||||||
|
with_date_inscription=with_date_inscription,
|
||||||
with_paiement=with_paiement,
|
with_paiement=with_paiement,
|
||||||
server_name=request.url_root,
|
server_name=request.url_root,
|
||||||
)
|
)
|
||||||
|
@ -369,10 +369,23 @@ class BasePreferences:
|
|||||||
"emails_notifications",
|
"emails_notifications",
|
||||||
{
|
{
|
||||||
"initvalue": "",
|
"initvalue": "",
|
||||||
"title": "e-mails à qui notifier les opérations",
|
"title": "e-mail(s) à qui notifier les opérations",
|
||||||
"size": 70,
|
"size": 70,
|
||||||
"explanation": """adresses séparées par des virgules; notifie les opérations
|
"explanation": """optionnel; adresses séparées par des virgules;
|
||||||
(saisies de notes, etc).
|
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",
|
"category": "general",
|
||||||
"only_global": False, # peut être spécifique à un semestre
|
"only_global": False, # peut être spécifique à un semestre
|
||||||
@ -2320,6 +2333,7 @@ class BasePreferences:
|
|||||||
<option value="create">Spécifier valeur pour ce
|
<option value="create">Spécifier valeur pour ce
|
||||||
semestre seulement</option>
|
semestre seulement</option>
|
||||||
</select>
|
</select>
|
||||||
|
<span class="pref-comment">{descr["comment"]}</span>
|
||||||
"""
|
"""
|
||||||
descr["explanation"] = menu_global
|
descr["explanation"] = menu_global
|
||||||
|
|
||||||
|
@ -1843,6 +1843,15 @@ div.formsemestre_status {
|
|||||||
/* EMO_WARNING, "⚠️" */
|
/* EMO_WARNING, "⚠️" */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.formsemestre_status_head {
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
table.formsemestre_status_head tr td:nth-child(2) {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
table.formsemestre_status {
|
table.formsemestre_status {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
@ -3310,6 +3319,12 @@ li.tf-msg {
|
|||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pref-comment {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: small;
|
||||||
|
color: var(--sco-color-explication);
|
||||||
|
}
|
||||||
|
|
||||||
div.formsemestre-warning-box {
|
div.formsemestre-warning-box {
|
||||||
background-color: yellow;
|
background-color: yellow;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -48,13 +48,12 @@ function change_list_options(selected_options) {
|
|||||||
"with_archives",
|
"with_archives",
|
||||||
"with_annotations",
|
"with_annotations",
|
||||||
"with_codes",
|
"with_codes",
|
||||||
|
"with_date_inscription",
|
||||||
"with_bourse",
|
"with_bourse",
|
||||||
];
|
];
|
||||||
for (var i = 0; i < options.length; i++) {
|
for (var i = 0; i < options.length; i++) {
|
||||||
var option = options[i];
|
let option = options[i];
|
||||||
if ($.inArray(option, selected_options) >= 0) {
|
urlParams.set(option, selected_options.indexOf(option) >= 0 ? "1" : "0");
|
||||||
urlParams.set(option, "1");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
window.location = url.href;
|
window.location = url.href;
|
||||||
}
|
}
|
||||||
@ -62,23 +61,32 @@ function change_list_options(selected_options) {
|
|||||||
// Menu choix groupe:
|
// Menu choix groupe:
|
||||||
function toggle_visible_etuds() {
|
function toggle_visible_etuds() {
|
||||||
//
|
//
|
||||||
$(".etud_elem").hide();
|
document.querySelectorAll('.etud_elem').forEach(element => {
|
||||||
|
element.style.display = 'none';
|
||||||
|
});
|
||||||
var qargs = "";
|
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;
|
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;
|
qargs += "&group_ids=" + group_id;
|
||||||
});
|
});
|
||||||
// Update url saisie tableur:
|
// Update url saisie tableur:
|
||||||
var input_eval = $("#formnotes_evaluation_id");
|
let input_eval = document.querySelectorAll("#formnotes_evaluation_id");
|
||||||
if (input_eval.length > 0) {
|
if (input_eval.length > 0) {
|
||||||
var evaluation_id = input_eval[0].value;
|
let evaluation_id = input_eval[0].value;
|
||||||
$("#menu_saisie_tableur a").attr(
|
let menu_saisie_tableur_a = document.querySelector("#menu_saisie_tableur a");
|
||||||
|
menu_saisie_tableur_a.setAttribute(
|
||||||
"href",
|
"href",
|
||||||
"saisie_notes_tableur?evaluation_id=" + evaluation_id + qargs
|
"saisie_notes_tableur?evaluation_id=" + evaluation_id + qargs
|
||||||
);
|
);
|
||||||
// lien feuille excel:
|
// lien feuille excel:
|
||||||
$("#lnk_feuille_saisie").attr(
|
let lnk_feuille_saisie = document.querySelector("#lnk_feuille_saisie");
|
||||||
|
lnk_feuille_saisie.setAttribute(
|
||||||
"href",
|
"href",
|
||||||
"feuille_saisie_notes?evaluation_id=" + evaluation_id + qargs
|
"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
|
Emmanuel Viennet, 2023
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import io
|
||||||
|
|
||||||
from flask import flash, redirect, render_template, url_for
|
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 import db, log
|
||||||
from app.decorators import (
|
from app.decorators import (
|
||||||
scodoc,
|
scodoc,
|
||||||
permission_required,
|
permission_required,
|
||||||
)
|
)
|
||||||
from app.forms.formsemestre import change_formation, edit_modimpls_codes_apo
|
from app.forms.formsemestre import (
|
||||||
from app.models import Formation, FormSemestre, ScoDocSiteConfig
|
change_formation,
|
||||||
|
edit_modimpls_codes_apo,
|
||||||
|
edit_description,
|
||||||
|
)
|
||||||
|
from app.models import (
|
||||||
|
Formation,
|
||||||
|
FormSemestre,
|
||||||
|
FormSemestreDescription,
|
||||||
|
FORMSEMESTRE_DISPOSITIFS,
|
||||||
|
ScoDocSiteConfig,
|
||||||
|
)
|
||||||
from app.scodoc import (
|
from app.scodoc import (
|
||||||
sco_edt_cal,
|
sco_edt_cal,
|
||||||
sco_formations,
|
sco_formations,
|
||||||
@ -223,3 +237,121 @@ def formsemestre_edt_help_config(formsemestre_id: int):
|
|||||||
ScoDocSiteConfig=ScoDocSiteConfig,
|
ScoDocSiteConfig=ScoDocSiteConfig,
|
||||||
title="Aide configuration EDT",
|
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",
|
fmt="html",
|
||||||
# Options pour listes:
|
# Options pour listes:
|
||||||
with_codes=0,
|
with_codes=0,
|
||||||
|
with_date_inscription=0,
|
||||||
etat=None,
|
etat=None,
|
||||||
with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail)
|
with_paiement=0,
|
||||||
with_archives=0, # ajoute colonne avec noms fichiers archivés
|
with_archives=0,
|
||||||
with_annotations=0,
|
with_annotations=0,
|
||||||
with_bourse=0,
|
with_bourse=0,
|
||||||
formsemestre_id=None,
|
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(
|
return sco_groups_view.groups_lists(
|
||||||
group_ids=group_ids,
|
group_ids=group_ids,
|
||||||
fmt=fmt,
|
fmt=fmt,
|
||||||
with_codes=with_codes,
|
with_codes=with_codes,
|
||||||
|
with_date_inscription=with_date_inscription,
|
||||||
etat=etat,
|
etat=etat,
|
||||||
with_paiement=with_paiement,
|
with_paiement=with_paiement,
|
||||||
with_archives=with_archives,
|
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":
|
if reply.headers.get("Content-Type", None) == "application/json":
|
||||||
return reply.json() # decode la reponse JSON
|
return reply.json() # decode la reponse JSON
|
||||||
if reply.headers.get("Content-Type", None) in [
|
if reply.headers.get("Content-Type", None) in [
|
||||||
"image/jpg",
|
|
||||||
"image/png",
|
|
||||||
"application/pdf",
|
"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 = {
|
retval = {
|
||||||
"Content-Type": reply.headers.get("Content-Type", None),
|
"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
|
return retval
|
||||||
raise APIError(
|
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,
|
status_code=reply.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ Utilisation :
|
|||||||
Lancer :
|
Lancer :
|
||||||
pytest tests/api/test_api_formsemestre.py
|
pytest tests/api/test_api_formsemestre.py
|
||||||
"""
|
"""
|
||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
from types import NoneType
|
from types import NoneType
|
||||||
@ -28,8 +29,10 @@ from tests.api.setup_test_api import (
|
|||||||
API_URL,
|
API_URL,
|
||||||
CHECK_CERTIFICATE,
|
CHECK_CERTIFICATE,
|
||||||
GET,
|
GET,
|
||||||
|
POST,
|
||||||
api_headers,
|
api_headers,
|
||||||
api_admin_headers,
|
api_admin_headers,
|
||||||
|
set_headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.api.tools_test_api import (
|
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:
|
if "nbabs" in k:
|
||||||
continue
|
continue
|
||||||
assert res_d[k] == ref_d[k], f"values for key {k} differ."
|
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,
|
verify=CHECK_CERTIFICATE,
|
||||||
timeout=scu.SCO_TEST_API_TIMEOUT,
|
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:
|
# Même chose sans le jeton:
|
||||||
for rule in api_rules:
|
for rule in api_rules:
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
|
|
||||||
""" Test création/accès/clonage formsemestre
|
""" Test création/accès/clonage formsemestre
|
||||||
"""
|
"""
|
||||||
from flask import Response
|
from flask import g, Response
|
||||||
import pytest
|
import pytest
|
||||||
from tests.unit import yaml_setup, call_view
|
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app.models import Formation, FormSemestre
|
from app import db
|
||||||
|
from app.models import Formation, FormSemestre, FormSemestreDescription
|
||||||
from app.scodoc import (
|
from app.scodoc import (
|
||||||
sco_archives_formsemestre,
|
sco_archives_formsemestre,
|
||||||
sco_cost_formation,
|
sco_cost_formation,
|
||||||
@ -35,6 +35,7 @@ from app.scodoc import (
|
|||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.views import notes, scolar
|
from app.views import notes, scolar
|
||||||
from config import TestConfig
|
from config import TestConfig
|
||||||
|
from tests.unit import yaml_setup, call_view
|
||||||
|
|
||||||
DEPT = TestConfig.DEPT_TEST
|
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_debouche.report_debouche_date(start_year=2000)
|
||||||
ans = sco_cost_formation.formsemestre_estim_cost(formsemestre.id)
|
ans = sco_cost_formation.formsemestre_estim_cost(formsemestre.id)
|
||||||
# pas de test des indicateurs de suivi BUT
|
# 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