forked from ScoDoc/ScoDoc
FormSemestreDescription: champs pour spécifs EL. Formulaire saisie.
This commit is contained in:
parent
3971145abd
commit
513fb3d46d
@ -826,8 +826,25 @@ def formsemestre_get_description_image(formsemestre_id: int):
|
||||
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):
|
||||
# Guess the mimetype based on the image data
|
||||
image_data = formsemestre.description.image
|
||||
mimetype = mimetypes.guess_type("image")[0]
|
||||
|
||||
if not mimetype:
|
||||
|
@ -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)
|
||||
|
@ -6,13 +6,42 @@
|
||||
|
||||
"""Formulaire édition description formsemestre
|
||||
"""
|
||||
from wtforms import (
|
||||
BooleanField,
|
||||
FileField,
|
||||
SelectField,
|
||||
StringField,
|
||||
TextAreaField,
|
||||
SubmitField,
|
||||
)
|
||||
from wtforms.validators import AnyOf, Optional
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, TextAreaField, FileField, SubmitField
|
||||
from wtforms.validators import Optional
|
||||
from app.forms import ScoDocForm
|
||||
from app.models import FORMSEMESTRE_DISPOSITIFS
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class FormSemestreDescriptionForm(FlaskForm):
|
||||
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",
|
||||
@ -23,12 +52,60 @@ class FormSemestreDescriptionForm(FlaskForm):
|
||||
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"
|
||||
)
|
||||
lieu = StringField("Lieu", validators=[Optional()], description="ex: salle 123")
|
||||
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="ex: nom de l'enseignant"
|
||||
"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")
|
||||
|
@ -188,7 +188,10 @@ from app.models.formsemestre import (
|
||||
NotesSemSet,
|
||||
notes_semset_formsemestre,
|
||||
)
|
||||
from app.models.formsemestre_descr import FormSemestreDescription
|
||||
from app.models.formsemestre_descr import (
|
||||
FormSemestreDescription,
|
||||
FORMSEMESTRE_DISPOSITIFS,
|
||||
)
|
||||
from app.models.moduleimpls import (
|
||||
ModuleImpl,
|
||||
notes_modules_enseignants,
|
||||
|
@ -22,15 +22,33 @@ class FormSemestreDescription(models.ScoDocModel):
|
||||
__tablename__ = "notes_formsemestre_description"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
# Storing image data directly in the database:
|
||||
image = db.Column(db.LargeBinary(), nullable=True)
|
||||
description = db.Column(
|
||||
db.Text(), nullable=False, default="", server_default=""
|
||||
) # HTML allowed
|
||||
responsable = db.Column(db.Text(), nullable=False, default="", server_default="")
|
||||
lieu = db.Column(db.Text(), nullable=False, default="", server_default="")
|
||||
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"),
|
||||
@ -40,20 +58,6 @@ class FormSemestreDescription(models.ScoDocModel):
|
||||
"FormSemestre", back_populates="description", uselist=False
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
image=None,
|
||||
description="",
|
||||
responsable="",
|
||||
lieu="",
|
||||
horaire="",
|
||||
):
|
||||
self.description = description
|
||||
self.horaire = horaire
|
||||
self.image = image
|
||||
self.lieu = lieu
|
||||
self.responsable = responsable
|
||||
|
||||
def __repr__(self):
|
||||
return f"<FormSemestreDescription {self.id} {self.formsemestre}>"
|
||||
|
||||
@ -61,11 +65,18 @@ class FormSemestreDescription(models.ScoDocModel):
|
||||
"""clone instance"""
|
||||
return super().clone(not_copying=not_copying + ("formsemestre_id",))
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"formsemestre_id": self.formsemestre_id,
|
||||
"description": self.description,
|
||||
"responsable": self.responsable,
|
||||
"lieu": self.lieu,
|
||||
"horaire": self.horaire,
|
||||
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",
|
||||
}
|
||||
|
@ -8,6 +8,9 @@
|
||||
font-style: italic;
|
||||
color: green;
|
||||
}
|
||||
div.field_descr {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.submit {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
@ -16,6 +19,7 @@
|
||||
}
|
||||
div.image {
|
||||
margin-left: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
div.image img {
|
||||
border: 1px dashed #b60c0c;
|
||||
@ -23,6 +27,29 @@ div.image img {
|
||||
</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">
|
||||
@ -32,35 +59,50 @@ div.image img {
|
||||
<p>Les informations saisies ici ne sont pas utilisées par ScoDoc mais
|
||||
mises à disposition des applications tierces comme AutoSco.
|
||||
</p>
|
||||
<p>La description du semestre est un </p>
|
||||
<p>Il est possible d'ajouter une image pour illustrer le semestre.</p>
|
||||
<p>Le responsable est généralement l'enseignant en charge de la formation, ou
|
||||
la personne qui s'occupant de l'organisation du semestre.</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>
|
||||
{{ form.description.label }}<br>
|
||||
{{ form.description(cols=80, rows=8) }}
|
||||
<div class="field_descr">{{ form.description.description }}</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>
|
||||
{{ form.responsable.label }}<br>
|
||||
{{ form.responsable(size=64) }}<br>
|
||||
<div class="field_descr">{{ form.responsable.description }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ form.lieu.label }}<br>
|
||||
{{ form.lieu(size=48) }}
|
||||
<div class="field_descr">{{ form.lieu.description }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ form.horaire.label }}<br>
|
||||
{{ form.horaire(size=64) }}<br>
|
||||
<div class="field_descr">{{ form.horaire.description }}</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">
|
||||
@ -68,7 +110,7 @@ div.image img {
|
||||
<img src="{{ url_for('apiweb.formsemestre_get_description_image',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id) }}"
|
||||
alt="Current Image" style="max-width: 200px;">
|
||||
alt="Current Image" style="max-width: 400px;">
|
||||
<div>
|
||||
Changer l'image: {{ form.image() }}
|
||||
</div>
|
||||
@ -76,8 +118,8 @@ div.image img {
|
||||
<em>Aucune image n'est actuellement associée à ce semestre.</em>
|
||||
{{ form.image() }}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="submit">
|
||||
{{ form.submit }} {{ form.cancel }}
|
||||
</div>
|
||||
|
@ -29,6 +29,8 @@ Vues "modernes" des formsemestres
|
||||
Emmanuel Viennet, 2023
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from flask import flash, redirect, render_template, url_for
|
||||
from flask import current_app, g, request
|
||||
|
||||
@ -46,6 +48,7 @@ from app.models import (
|
||||
Formation,
|
||||
FormSemestre,
|
||||
FormSemestreDescription,
|
||||
FORMSEMESTRE_DISPOSITIFS,
|
||||
ScoDocSiteConfig,
|
||||
)
|
||||
from app.scodoc import (
|
||||
@ -248,7 +251,7 @@ def edit_formsemestre_description(formsemestre_id: int):
|
||||
db.session.commit()
|
||||
formsemestre_description = formsemestre.description
|
||||
form = edit_description.FormSemestreDescriptionForm(obj=formsemestre_description)
|
||||
|
||||
ok = True
|
||||
if form.validate_on_submit():
|
||||
if form.cancel.data: # cancel button
|
||||
return redirect(
|
||||
@ -258,16 +261,55 @@ def edit_formsemestre_description(formsemestre_id: int):
|
||||
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)
|
||||
|
||||
if form_image.data:
|
||||
image_data = form_image.data.read()
|
||||
# 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 too large, max {max_length} bytes",
|
||||
f"Image trop grande ({field}), max {max_length} octets",
|
||||
"danger",
|
||||
)
|
||||
return redirect(
|
||||
@ -277,7 +319,7 @@ def edit_formsemestre_description(formsemestre_id: int):
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
)
|
||||
)
|
||||
formsemestre_description.image = image_data
|
||||
setattr(formsemestre_description, field, image_data)
|
||||
|
||||
db.session.commit()
|
||||
flash("Description enregistrée", "success")
|
||||
@ -295,4 +337,5 @@ def edit_formsemestre_description(formsemestre_id: int):
|
||||
formsemestre=formsemestre,
|
||||
formsemestre_description=formsemestre_description,
|
||||
sco=ScoData(formsemestre=formsemestre),
|
||||
title="Modif. description semestre",
|
||||
)
|
||||
|
@ -21,11 +21,20 @@ def upgrade():
|
||||
op.create_table(
|
||||
"notes_formsemestre_description",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("image", sa.LargeBinary(), nullable=True),
|
||||
sa.Column("description", sa.Text(), server_default="", nullable=False),
|
||||
sa.Column("responsable", sa.Text(), server_default="", nullable=False),
|
||||
sa.Column("lieu", sa.Text(), server_default="", nullable=False),
|
||||
sa.Column("horaire", sa.Text(), server_default="", nullable=False),
|
||||
sa.Column("date_debut_inscriptions", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("date_fin_inscriptions", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("wip", sa.Boolean(), server_default="false", nullable=False),
|
||||
sa.Column("image", sa.LargeBinary(), nullable=True),
|
||||
sa.Column("campus", sa.Text(), server_default="", nullable=False),
|
||||
sa.Column("salle", sa.Text(), server_default="", nullable=False),
|
||||
sa.Column("dispositif", sa.Integer(), server_default="0", nullable=False),
|
||||
sa.Column("modalites_mcc", sa.Text(), server_default="", nullable=False),
|
||||
sa.Column("photo_ens", sa.LargeBinary(), nullable=True),
|
||||
sa.Column("public", sa.Text(), server_default="", nullable=False),
|
||||
sa.Column("prerequis", sa.Text(), server_default="", nullable=False),
|
||||
sa.Column("responsable", sa.Text(), server_default="", nullable=False),
|
||||
sa.Column("formsemestre_id", sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["formsemestre_id"], ["notes_formsemestre.id"], ondelete="CASCADE"
|
||||
|
@ -223,11 +223,14 @@ def test_formsemestre_description(test_client):
|
||||
db.session.commit()
|
||||
assert formsemestre.description is None
|
||||
# Association d'une description
|
||||
formsemestre.description = FormSemestreDescription(
|
||||
description="Description 2",
|
||||
responsable="Responsable 2",
|
||||
lieu="Lieu 2",
|
||||
horaire="Horaire 2",
|
||||
formsemestre.description = FormSemestreDescription.create_from_dict(
|
||||
{
|
||||
"description": "Description",
|
||||
"responsable": "Responsable",
|
||||
"campus": "Sorbonne",
|
||||
"salle": "L214",
|
||||
"horaire": "23h à l'aube",
|
||||
}
|
||||
)
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
|
Loading…
Reference in New Issue
Block a user