FormSemestreDescription: informations pour applications tierces: Modèle, API, éditeur.
This commit is contained in:
parent
fc036705e8
commit
525d0446cc
@ -13,6 +13,7 @@
|
||||
FormSemestre
|
||||
|
||||
"""
|
||||
import mimetypes
|
||||
from operator import attrgetter, itemgetter
|
||||
|
||||
from flask import g, make_response, request
|
||||
@ -790,3 +791,49 @@ def formsemestre_edt(formsemestre_id: int):
|
||||
return sco_edt_cal.formsemestre_edt_dict(
|
||||
formsemestre, group_ids=group_ids, show_modules_titles=show_modules_titles
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/description")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/description")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_get_description(formsemestre_id: int):
|
||||
"""Description externe du formsemestre. Peut être vide.
|
||||
|
||||
formsemestre_id : l'id du formsemestre
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/formsemestre/1/description
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
return formsemestre.description.to_dict() if formsemestre.description else {}
|
||||
|
||||
|
||||
@bp.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
|
||||
|
||||
# Guess the mimetype based on the image data
|
||||
image_data = formsemestre.description.image
|
||||
mimetype = mimetypes.guess_type("image")[0]
|
||||
|
||||
if not mimetype:
|
||||
# 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
|
||||
|
35
app/forms/formsemestre/edit_description.py
Normal file
35
app/forms/formsemestre/edit_description.py
Normal file
@ -0,0 +1,35 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Formulaire édition description formsemestre
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, TextAreaField, FileField, SubmitField
|
||||
from wtforms.validators import Optional
|
||||
|
||||
|
||||
class FormSemestreDescriptionForm(FlaskForm):
|
||||
"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"
|
||||
)
|
||||
image = FileField(
|
||||
"Image", validators=[Optional()], description="Image illustrant cette formation"
|
||||
)
|
||||
lieu = StringField("Lieu", validators=[Optional()], description="ex: salle 123")
|
||||
responsable = StringField(
|
||||
"Responsable", validators=[Optional()], description="ex: nom de l'enseignant"
|
||||
)
|
||||
|
||||
submit = SubmitField("Enregistrer")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
@ -14,12 +14,13 @@ class _EditModimplsCodesForm(FlaskForm):
|
||||
# construit dynamiquement ci-dessous
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def EditModimplsCodesForm(formsemestre: FormSemestre) -> _EditModimplsCodesForm:
|
||||
"Création d'un formulaire pour éditer les codes"
|
||||
|
||||
# Formulaire dynamique, on créé une classe ad-hoc
|
||||
class F(_EditModimplsCodesForm):
|
||||
pass
|
||||
"class factory"
|
||||
|
||||
def _gen_mod_form(modimpl: ModuleImpl):
|
||||
field = StringField(
|
||||
|
@ -38,7 +38,7 @@ class ScoDocModel(db.Model):
|
||||
__abstract__ = True # declare an abstract class for SQLAlchemy
|
||||
|
||||
def clone(self, not_copying=()):
|
||||
"""Clone, not copying the given attrs
|
||||
"""Clone, not copying the given attrs, and add to session.
|
||||
Attention: la copie n'a pas d'id avant le prochain flush ou commit.
|
||||
"""
|
||||
d = dict(self.__dict__)
|
||||
@ -188,6 +188,7 @@ from app.models.formsemestre import (
|
||||
NotesSemSet,
|
||||
notes_semset_formsemestre,
|
||||
)
|
||||
from app.models.formsemestre_descr import FormSemestreDescription
|
||||
from app.models.moduleimpls import (
|
||||
ModuleImpl,
|
||||
notes_modules_enseignants,
|
||||
|
@ -143,6 +143,12 @@ class FormSemestre(models.ScoDocModel):
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
description = db.relationship(
|
||||
"FormSemestreDescription",
|
||||
back_populates="formsemestre",
|
||||
cascade="all, delete-orphan",
|
||||
uselist=False,
|
||||
)
|
||||
etuds = db.relationship(
|
||||
"Identite",
|
||||
secondary="notes_formsemestre_inscription",
|
||||
|
71
app/models/formsemestre_descr.py
Normal file
71
app/models/formsemestre_descr.py
Normal file
@ -0,0 +1,71 @@
|
||||
##############################################################################
|
||||
# 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)
|
||||
# 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="")
|
||||
horaire = db.Column(db.Text(), nullable=False, default="", server_default="")
|
||||
|
||||
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 __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}>"
|
||||
|
||||
def clone(self, not_copying=()) -> "FormSemestreDescription":
|
||||
"""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,
|
||||
}
|
@ -508,6 +508,12 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||
}">Modifier les codes Apogée et emploi du temps des modules</a>
|
||||
</p>
|
||||
|
||||
<p><a class="stdlink" href="{url_for("notes.edit_formsemestre_description",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||
}">Éditer la description externe du semestre</a>
|
||||
</p>
|
||||
|
||||
<h3>Sélectionner les modules, leurs responsables et les étudiants
|
||||
à inscrire:</h3>
|
||||
"""
|
||||
@ -1287,6 +1293,7 @@ def do_formsemestre_clone(
|
||||
clone_partitions=False,
|
||||
):
|
||||
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions.
|
||||
Clone description.
|
||||
New dates, responsable_id
|
||||
"""
|
||||
log(f"do_formsemestre_clone: {orig_formsemestre_id}")
|
||||
@ -1375,10 +1382,14 @@ def do_formsemestre_clone(
|
||||
|
||||
# 5- Copie les parcours
|
||||
formsemestre.parcours = formsemestre_orig.parcours
|
||||
|
||||
# 6- Copy description
|
||||
formsemestre.description = formsemestre_orig.description.clone()
|
||||
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
|
||||
# 6- Copy partitions and groups
|
||||
# 7- Copy partitions and groups
|
||||
if clone_partitions:
|
||||
sco_groups_copy.clone_partitions_and_groups(
|
||||
orig_formsemestre_id, formsemestre.id
|
||||
|
88
app/templates/formsemestre/edit_description.j2
Normal file
88
app/templates/formsemestre/edit_description.j2
Normal file
@ -0,0 +1,88 @@
|
||||
{% extends "sco_page.j2" %}
|
||||
{% import 'wtf.j2' as wtf %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<style>
|
||||
.field_descr {
|
||||
font-style: italic;
|
||||
color: green;
|
||||
}
|
||||
.submit {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
gap: 24px;
|
||||
}
|
||||
div.image {
|
||||
margin-left: 32px;
|
||||
}
|
||||
div.image img {
|
||||
border: 1px dashed #b60c0c;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% 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>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() }}
|
||||
<div>
|
||||
{{ form.description.label }}<br>
|
||||
{{ form.description(cols=80, rows=8) }}
|
||||
<div class="field_descr">{{ form.description.description }}</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{{ 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: 200px;">
|
||||
<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,29 @@
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Vues "modernes" des formsemestre
|
||||
Vues "modernes" des formsemestres
|
||||
Emmanuel Viennet, 2023
|
||||
"""
|
||||
|
||||
from flask import flash, redirect, render_template, url_for
|
||||
from flask import g, request
|
||||
from flask import current_app, g, request
|
||||
|
||||
from app import db, log
|
||||
from app.decorators import (
|
||||
scodoc,
|
||||
permission_required,
|
||||
)
|
||||
from app.forms.formsemestre import change_formation, edit_modimpls_codes_apo
|
||||
from app.models import Formation, FormSemestre, ScoDocSiteConfig
|
||||
from app.forms.formsemestre import (
|
||||
change_formation,
|
||||
edit_modimpls_codes_apo,
|
||||
edit_description,
|
||||
)
|
||||
from app.models import (
|
||||
Formation,
|
||||
FormSemestre,
|
||||
FormSemestreDescription,
|
||||
ScoDocSiteConfig,
|
||||
)
|
||||
from app.scodoc import (
|
||||
sco_edt_cal,
|
||||
sco_formations,
|
||||
@ -223,3 +232,67 @@ def formsemestre_edt_help_config(formsemestre_id: int):
|
||||
ScoDocSiteConfig=ScoDocSiteConfig,
|
||||
title="Aide configuration EDT",
|
||||
)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formsemestre_description/<int:formsemestre_id>/edit", methods=["GET", "POST"]
|
||||
)
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormSemestre)
|
||||
def edit_formsemestre_description(formsemestre_id: int):
|
||||
"Edition de la description d'un formsemestre"
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if not formsemestre.description:
|
||||
formsemestre.description = FormSemestreDescription()
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
formsemestre_description = formsemestre.description
|
||||
form = edit_description.FormSemestreDescriptionForm(obj=formsemestre_description)
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
form_image = form.image
|
||||
del form.image
|
||||
form.populate_obj(formsemestre_description)
|
||||
|
||||
if form_image.data:
|
||||
image_data = form_image.data.read()
|
||||
max_length = current_app.config.get("MAX_CONTENT_LENGTH")
|
||||
if max_length and len(image_data) > max_length:
|
||||
flash(
|
||||
f"Image too large, max {max_length} bytes",
|
||||
"danger",
|
||||
)
|
||||
return redirect(
|
||||
url_for(
|
||||
"notes.edit_formsemestre_description",
|
||||
formsemestre_id=formsemestre.id,
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
)
|
||||
)
|
||||
formsemestre_description.image = 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),
|
||||
)
|
||||
|
38
migrations/versions/2640b7686de6_formsemestre_description.py
Normal file
38
migrations/versions/2640b7686de6_formsemestre_description.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""FormSemestreDescription
|
||||
|
||||
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("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("formsemestre_id", sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["formsemestre_id"], ["notes_formsemestre.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table("notes_formsemestre_description")
|
@ -3,12 +3,12 @@
|
||||
|
||||
""" Test création/accès/clonage formsemestre
|
||||
"""
|
||||
from flask import Response
|
||||
from flask import g, Response
|
||||
import pytest
|
||||
from tests.unit import yaml_setup, call_view
|
||||
|
||||
import app
|
||||
from app.models import Formation, FormSemestre
|
||||
from app import db
|
||||
from app.models import Formation, FormSemestre, FormSemestreDescription
|
||||
from app.scodoc import (
|
||||
sco_archives_formsemestre,
|
||||
sco_cost_formation,
|
||||
@ -35,6 +35,7 @@ from app.scodoc import (
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.views import notes, scolar
|
||||
from config import TestConfig
|
||||
from tests.unit import yaml_setup, call_view
|
||||
|
||||
DEPT = TestConfig.DEPT_TEST
|
||||
|
||||
@ -203,3 +204,36 @@ def test_formsemestre_misc_views(test_client):
|
||||
ans = sco_debouche.report_debouche_date(start_year=2000)
|
||||
ans = sco_cost_formation.formsemestre_estim_cost(formsemestre.id)
|
||||
# pas de test des indicateurs de suivi BUT
|
||||
|
||||
|
||||
def test_formsemestre_description(test_client):
|
||||
"""Test FormSemestreDescription"""
|
||||
app.set_sco_dept(DEPT)
|
||||
#
|
||||
nb_descriptions = FormSemestreDescription.query.count()
|
||||
# Création d'un semestre
|
||||
|
||||
formsemestre = FormSemestre(
|
||||
dept_id=g.scodoc_dept_id,
|
||||
titre="test description",
|
||||
date_debut="2024-08-01",
|
||||
date_fin="2024-08-31",
|
||||
)
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
assert formsemestre.description is None
|
||||
# Association d'une description
|
||||
formsemestre.description = FormSemestreDescription(
|
||||
description="Description 2",
|
||||
responsable="Responsable 2",
|
||||
lieu="Lieu 2",
|
||||
horaire="Horaire 2",
|
||||
)
|
||||
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