forked from ScoDoc/ScoDoc
Compare commits
21 Commits
763f60fb3d
...
29eb8c297b
Author | SHA1 | Date |
---|---|---|
Emmanuel Viennet | 29eb8c297b | |
Emmanuel Viennet | 38032a8c09 | |
Emmanuel Viennet | 2f2d98954c | |
Emmanuel Viennet | 2e5d94f048 | |
Emmanuel Viennet | 1b1b8ebdc4 | |
Emmanuel Viennet | 9c6db169f3 | |
Iziram | 8ded16b94f | |
Iziram | 5d10ee467e | |
Iziram | 7af0dd1e1e | |
Emmanuel Viennet | dece9a82d1 | |
Iziram | b74d525c28 | |
Iziram | c617ee321a | |
Iziram | 56ec4ba43d | |
Iziram | d14f7e21b7 | |
Iziram | c3cb1da561 | |
Iziram | cce60d432d | |
Iziram | 4386994f7d | |
Iziram | fddfddfa7b | |
Iziram | 39dca32d2e | |
Iziram | e2b9cd3ded | |
Iziram | be227f4a2f |
|
@ -414,9 +414,16 @@ def bulletin(
|
|||
if version == "pdf":
|
||||
version = "long"
|
||||
pdf = True
|
||||
if version not in scu.BULLETINS_VERSIONS_BUT:
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if version not in (
|
||||
scu.BULLETINS_VERSIONS_BUT
|
||||
if formsemestre.formation.is_apc()
|
||||
else scu.BULLETINS_VERSIONS
|
||||
):
|
||||
return json_error(404, "version invalide")
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
||||
if formsemestre.bul_hide_xml and pdf:
|
||||
return json_error(403, "bulletin non disponible")
|
||||
# note: la version json est réduite si bul_hide_xml
|
||||
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
||||
return json_error(404, "formsemestre inexistant")
|
||||
|
|
|
@ -12,7 +12,7 @@ from operator import attrgetter, itemgetter
|
|||
from flask import g, make_response, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
import sqlalchemy as sa
|
||||
import app
|
||||
from app import db
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
|
@ -171,6 +171,44 @@ def formsemestres_query():
|
|||
]
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormSemestre)
|
||||
@as_json
|
||||
def formsemestre_edit(formsemestre_id: int):
|
||||
"""Modifie les champs d'un formsemestre."""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
editable_keys = {
|
||||
"semestre_id",
|
||||
"titre",
|
||||
"date_debut",
|
||||
"date_fin",
|
||||
"edt_id",
|
||||
"etat",
|
||||
"modalite",
|
||||
"gestion_compensation",
|
||||
"bul_hide_xml",
|
||||
"block_moyennes",
|
||||
"block_moyenne_generale",
|
||||
"mode_calcul_moyennes",
|
||||
"gestion_semestrielle",
|
||||
"bul_bgcolor",
|
||||
"resp_can_edit",
|
||||
"resp_can_change_ens",
|
||||
"ens_can_edit_eval",
|
||||
"elt_sem_apo",
|
||||
"elt_annee_apo",
|
||||
}
|
||||
formsemestre.from_dict({k: v for (k, v) in args.items() if k in editable_keys})
|
||||
try:
|
||||
db.session.commit()
|
||||
except sa.exc.StatementError as exc:
|
||||
return json_error(404, f"invalid argument(s): {exc.args[0]}")
|
||||
return formsemestre.to_dict_api()
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||
|
@ -468,13 +506,13 @@ def etat_evals(formsemestre_id: int):
|
|||
date_mediane = notes_sorted[len(notes_sorted) // 2].date
|
||||
|
||||
eval_dict["saisie_notes"] = {
|
||||
"datetime_debut": date_debut.isoformat()
|
||||
if date_debut is not None
|
||||
else None,
|
||||
"datetime_debut": (
|
||||
date_debut.isoformat() if date_debut is not None else None
|
||||
),
|
||||
"datetime_fin": date_fin.isoformat() if date_fin is not None else None,
|
||||
"datetime_mediane": date_mediane.isoformat()
|
||||
if date_mediane is not None
|
||||
else None,
|
||||
"datetime_mediane": (
|
||||
date_mediane.isoformat() if date_mediane is not None else None
|
||||
),
|
||||
}
|
||||
|
||||
list_eval.append(eval_dict)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
"""Gestion de l'assiduité (assiduités + justificatifs)
|
||||
"""
|
||||
"""Gestion de l'assiduité (assiduités + justificatifs)"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask_login import current_user
|
||||
|
@ -336,13 +336,19 @@ class Assiduite(ScoDocModel):
|
|||
"""
|
||||
return get_formsemestre_from_data(self.to_dict())
|
||||
|
||||
def get_module(self, traduire: bool = False) -> int | str:
|
||||
"TODO documenter"
|
||||
def get_module(self, traduire: bool = False) -> Module | str:
|
||||
"""
|
||||
Retourne le module associé à l'assiduité
|
||||
Si traduire est vrai, retourne le titre du module précédé du code
|
||||
Sinon rentourne l'objet Module ou None
|
||||
"""
|
||||
|
||||
if self.moduleimpl_id is not None:
|
||||
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||
mod: Module = Module.query.get(modimpl.module_id)
|
||||
if traduire:
|
||||
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||
mod: Module = Module.query.get(modimpl.module_id)
|
||||
return f"{mod.code} {mod.titre}"
|
||||
return mod
|
||||
|
||||
elif self.external_data is not None and "module" in self.external_data:
|
||||
return (
|
||||
|
|
|
@ -249,11 +249,12 @@ class ScolarNews(db.Model):
|
|||
news_list = cls.last_news(n=n)
|
||||
if not news_list:
|
||||
return ""
|
||||
dept_news_url = url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
|
||||
H = [
|
||||
f"""<div class="news"><span class="newstitle"><a href="{
|
||||
url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
|
||||
f"""<div class="scobox news"><div class="scobox-title"><a href="{
|
||||
dept_news_url
|
||||
}">Dernières opérations</a>
|
||||
</span><ul class="newslist">"""
|
||||
</div><ul class="newslist">"""
|
||||
]
|
||||
|
||||
for news in news_list:
|
||||
|
@ -261,16 +262,22 @@ class ScolarNews(db.Model):
|
|||
f"""<li class="newslist"><span class="newsdate">{news.formatted_date()}</span><span
|
||||
class="newstext">{news}</span></li>"""
|
||||
)
|
||||
H.append(
|
||||
f"""<li class="newslist">
|
||||
<span class="newstext"><a href="{dept_news_url}" class="stdlink">...</a>
|
||||
</span>
|
||||
</li>"""
|
||||
)
|
||||
|
||||
H.append("</ul>")
|
||||
H.append("</ul></div>")
|
||||
|
||||
# Informations générales
|
||||
H.append(
|
||||
f"""<div><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}">
|
||||
Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>.
|
||||
f"""<div>
|
||||
Pour en savoir plus sur ScoDoc voir
|
||||
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">scodoc.org</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append("</div>")
|
||||
return "\n".join(H)
|
||||
|
|
|
@ -25,6 +25,7 @@ from sqlalchemy import func
|
|||
import app.scodoc.sco_utils as scu
|
||||
from app import db, log
|
||||
from app.auth.models import User
|
||||
from app import models
|
||||
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
|
||||
from app.models.but_refcomp import (
|
||||
ApcParcours,
|
||||
|
@ -54,7 +55,7 @@ from app.scodoc.sco_vdi import ApoEtapeVDI
|
|||
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
||||
|
||||
|
||||
class FormSemestre(db.Model):
|
||||
class FormSemestre(models.ScoDocModel):
|
||||
"""Mise en oeuvre d'un semestre de formation"""
|
||||
|
||||
__tablename__ = "notes_formsemestre"
|
||||
|
@ -84,7 +85,7 @@ class FormSemestre(db.Model):
|
|||
bul_hide_xml = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
"ne publie pas le bulletin XML ou JSON"
|
||||
"ne publie pas le bulletin sur l'API"
|
||||
block_moyennes = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
|
@ -191,7 +192,8 @@ class FormSemestre(db.Model):
|
|||
def get_formsemestre(
|
||||
cls, formsemestre_id: int | str, dept_id: int = None
|
||||
) -> "FormSemestre":
|
||||
"""FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant"""
|
||||
"""FormSemestre ou 404, cherche uniquement dans le département spécifié
|
||||
ou le courant (g.scodoc_dept)"""
|
||||
if not isinstance(formsemestre_id, int):
|
||||
try:
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
|
@ -245,12 +247,13 @@ class FormSemestre(db.Model):
|
|||
|
||||
def to_dict_api(self):
|
||||
"""
|
||||
Un dict avec les informations sur le semestre destiné à l'api
|
||||
Un dict avec les informations sur le semestre destinées à l'api
|
||||
"""
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
d.pop("groups_auto_assignment_data", None)
|
||||
d["annee_scolaire"] = self.annee_scolaire()
|
||||
d["bul_hide_xml"] = self.bul_hide_xml
|
||||
if self.date_debut:
|
||||
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
|
||||
d["date_debut_iso"] = self.date_debut.isoformat()
|
||||
|
|
|
@ -303,13 +303,16 @@ def sco_header(
|
|||
# div pour affichage messages temporaires
|
||||
H.append('<div id="sco_msg" class="head_message"></div>')
|
||||
#
|
||||
H.append('<div class="sco-app-content">')
|
||||
return "".join(H)
|
||||
|
||||
|
||||
def sco_footer():
|
||||
"""Main HTMl pages footer"""
|
||||
return (
|
||||
"""</div><!-- /gtrcontent -->""" + scu.CUSTOM_HTML_FOOTER + """</body></html>"""
|
||||
"""</div></div><!-- /gtrcontent -->"""
|
||||
+ scu.CUSTOM_HTML_FOOTER
|
||||
+ """</body></html>"""
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -114,10 +114,8 @@ def formsemestre_bulletinetud_published_dict(
|
|||
if etudid not in nt.identdict:
|
||||
abort(404, "etudiant non inscrit dans ce semestre")
|
||||
d = {"type": "classic", "version": "0"}
|
||||
if (not sem["bul_hide_xml"]) or force_publishing:
|
||||
published = True
|
||||
else:
|
||||
published = False
|
||||
|
||||
published = (not formsemestre.bul_hide_xml) or force_publishing
|
||||
if xml_nodate:
|
||||
docdate = ""
|
||||
else:
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
|
@ -28,208 +28,158 @@
|
|||
"""Page accueil département (liste des semestres, etc)
|
||||
"""
|
||||
|
||||
from flask import g
|
||||
from flask import url_for
|
||||
from sqlalchemy import desc
|
||||
from flask import g, url_for, render_template
|
||||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
import app
|
||||
from app import log
|
||||
from app.models import ScolarNews
|
||||
from app.models import FormSemestre, ScolarNews
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import html_sco_header
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_modalites
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_users
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
def index_html(showcodes=0, showsemtable=0):
|
||||
def index_html(showcodes=0, showsemtable=0, export_table_formsemestres=False):
|
||||
"Page accueil département (liste des semestres)"
|
||||
showcodes = int(showcodes)
|
||||
showsemtable = int(showsemtable)
|
||||
H = []
|
||||
showsemtable = int(showsemtable) or export_table_formsemestres
|
||||
|
||||
# News:
|
||||
H.append(ScolarNews.scolar_news_summary_html())
|
||||
|
||||
# Avertissement de mise à jour:
|
||||
H.append("""<div id="update_warning"></div>""")
|
||||
|
||||
# Liste de toutes les sessions:
|
||||
sems = sco_formsemestre.do_formsemestre_list()
|
||||
cursems = [] # semestres "courants"
|
||||
othersems = [] # autres (verrouillés)
|
||||
# icon image:
|
||||
groupicon = scu.icontag("groupicon_img", title="Inscrits", border="0")
|
||||
emptygroupicon = scu.icontag(
|
||||
"emptygroupicon_img", title="Pas d'inscrits", border="0"
|
||||
# Liste tous les formsemestres du dept, le plus récent d'abord
|
||||
current_formsemestres = (
|
||||
FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id, etat=True)
|
||||
.filter(FormSemestre.modalite != "EXT")
|
||||
.order_by(desc(FormSemestre.date_debut))
|
||||
)
|
||||
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
||||
# Sélection sur l'etat du semestre
|
||||
for sem in sems:
|
||||
if sem["etat"] and sem["modalite"] != "EXT":
|
||||
sem["lockimg"] = ""
|
||||
cursems.append(sem)
|
||||
else:
|
||||
sem["lockimg"] = lockicon
|
||||
othersems.append(sem)
|
||||
# Responsable de formation:
|
||||
sco_formsemestre.sem_set_responsable_name(sem)
|
||||
|
||||
if showcodes:
|
||||
sem["tmpcode"] = f"<td><tt>{sem['formsemestre_id']}</tt></td>"
|
||||
else:
|
||||
sem["tmpcode"] = ""
|
||||
# Nombre d'inscrits:
|
||||
args = {"formsemestre_id": sem["formsemestre_id"]}
|
||||
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(args=args)
|
||||
nb = len(ins) # nb etudiants
|
||||
sem["nb_inscrits"] = nb
|
||||
if nb > 0:
|
||||
sem["groupicon"] = groupicon
|
||||
else:
|
||||
sem["groupicon"] = emptygroupicon
|
||||
|
||||
# S'il n'y a pas d'utilisateurs dans la base, affiche message
|
||||
if not sco_users.get_users_count(dept=g.scodoc_dept):
|
||||
H.append(
|
||||
"""<h2>Aucun utilisateur défini !</h2><p>Pour définir des utilisateurs
|
||||
<a href="Users">passez par la page Utilisateurs</a>.
|
||||
<br>
|
||||
Définissez au moins un utilisateur avec le rôle AdminXXX
|
||||
(le responsable du département XXX).
|
||||
</p>
|
||||
"""
|
||||
locked_formsemestres = (
|
||||
FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id, etat=False)
|
||||
.filter(FormSemestre.modalite != "EXT")
|
||||
.order_by(desc(FormSemestre.date_debut))
|
||||
)
|
||||
formsemestres = (
|
||||
FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
.filter(FormSemestre.modalite != "EXT")
|
||||
.order_by(desc(FormSemestre.date_debut))
|
||||
)
|
||||
if showsemtable: # table de tous les formsemestres
|
||||
table = _sem_table_gt(
|
||||
formsemestres,
|
||||
showcodes=showcodes,
|
||||
fmt="xlsx" if export_table_formsemestres else "html",
|
||||
)
|
||||
if export_table_formsemestres:
|
||||
return table # cas spécial: on renvoie juste cette table
|
||||
html_table_formsemestres = table.html()
|
||||
else:
|
||||
html_table_formsemestres = None
|
||||
|
||||
# Liste des formsemestres "courants"
|
||||
if cursems:
|
||||
H.append('<h2 class="listesems">Sessions en cours</h2>')
|
||||
H.append(_sem_table(cursems))
|
||||
return render_template(
|
||||
"scolar/index.j2",
|
||||
current_user=current_user,
|
||||
dept_name=sco_preferences.get_preference("DeptName"),
|
||||
formsemestres=formsemestres,
|
||||
html_current_formsemestres=_show_current_formsemestres(
|
||||
current_formsemestres, showcodes
|
||||
),
|
||||
html_table_formsemestres=html_table_formsemestres,
|
||||
locked_formsemestres=locked_formsemestres,
|
||||
nb_locked=locked_formsemestres.count(),
|
||||
nb_user_accounts=sco_users.get_users_count(dept=g.scodoc_dept),
|
||||
page_title=f"ScoDoc {g.scodoc_dept}",
|
||||
Permission=Permission,
|
||||
scolar_news_summary=ScolarNews.scolar_news_summary_html(),
|
||||
showsemtable=showsemtable,
|
||||
sco=ScoData(),
|
||||
)
|
||||
|
||||
|
||||
def _convert_formsemestres_to_dicts(
|
||||
formsemestres: Query, showcodes: bool, fmt: str = "html"
|
||||
) -> list[dict]:
|
||||
""" """
|
||||
if fmt == "html":
|
||||
# icon images:
|
||||
groupicon = scu.icontag("groupicon_img", title="Inscrits", border="0")
|
||||
emptygroupicon = scu.icontag(
|
||||
"emptygroupicon_img", title="Pas d'inscrits", border="0"
|
||||
)
|
||||
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
||||
else:
|
||||
groupicon = "X"
|
||||
emptygroupicon = ""
|
||||
lockicon = "X"
|
||||
# génère liste de dict
|
||||
sems = []
|
||||
for formsemestre in formsemestres:
|
||||
nb_inscrits = len(formsemestre.inscriptions)
|
||||
formation = formsemestre.formation
|
||||
sem = {
|
||||
"anneescolaire": formsemestre.annee_scolaire(),
|
||||
"anneescolaire_str": formsemestre.annee_scolaire_str(),
|
||||
"bul_hide_xml": formsemestre.bul_hide_xml,
|
||||
"dateord": formsemestre.date_debut,
|
||||
"elt_annee_apo": formsemestre.elt_annee_apo,
|
||||
"elt_sem_apo": formsemestre.elt_sem_apo,
|
||||
"etapes_apo_str": formsemestre.etapes_apo_str(),
|
||||
"formation": f"{formation.acronyme} v{formation.version}",
|
||||
"_formation_target": url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation.id,
|
||||
semestre_idx=formsemestre.semestre_id,
|
||||
),
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"groupicon": groupicon if nb_inscrits > 0 else emptygroupicon,
|
||||
"lockimg": lockicon,
|
||||
"modalite": formsemestre.modalite,
|
||||
"mois_debut": formsemestre.mois_debut(),
|
||||
"mois_fin": formsemestre.mois_fin(),
|
||||
"nb_inscrits": nb_inscrits,
|
||||
"responsable_name": formsemestre.responsables_str(),
|
||||
"semestre_id": formsemestre.semestre_id,
|
||||
"session_id": formsemestre.session_id(),
|
||||
"titre_num": formsemestre.titre_num(),
|
||||
"tmpcode": (f"<td><tt>{formsemestre.id}</tt></td>" if showcodes else ""),
|
||||
}
|
||||
sems.append(sem)
|
||||
return sems
|
||||
|
||||
|
||||
def _show_current_formsemestres(formsemestres: Query, showcodes: bool) -> str:
|
||||
"""html div avec les formsemestres courants de la page d'accueil"""
|
||||
|
||||
H = []
|
||||
if formsemestres.count():
|
||||
H.append("""<div class="scobox-title">Sessions en cours</div>""")
|
||||
H.append(_sem_table(_convert_formsemestres_to_dicts(formsemestres, showcodes)))
|
||||
else:
|
||||
# aucun semestre courant: affiche aide
|
||||
H.append(
|
||||
"""<h2 class="listesems">Aucune session en cours !</h2>
|
||||
"""
|
||||
<div class="scobox-title">Aucune session en cours !</div>
|
||||
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Formations</a>,
|
||||
choisissez une formation, puis suivez le lien "<em>UE, modules, semestres</em>".
|
||||
</p><p>
|
||||
Là, en bas de page, suivez le lien
|
||||
</p>
|
||||
<p>Là, en bas de page, suivez le lien
|
||||
"<em>Mettre en place un nouveau semestre de formation...</em>"
|
||||
</p>"""
|
||||
)
|
||||
|
||||
if showsemtable:
|
||||
H.append(
|
||||
f"""<hr>
|
||||
<h2>Semestres de {sco_preferences.get_preference("DeptName")}</h2>
|
||||
"""
|
||||
)
|
||||
H.append(_sem_table_gt(sems, showcodes=showcodes).html())
|
||||
H.append("</table>")
|
||||
if not showsemtable:
|
||||
H.append(
|
||||
f"""<hr>
|
||||
<p><a class="stdlink" href="{url_for('scolar.index_html',
|
||||
scodoc_dept=g.scodoc_dept, showsemtable=1)
|
||||
}">Voir table des semestres (dont {len(othersems)}
|
||||
verrouillé{'s' if len(othersems) else ''})</a>
|
||||
</p>"""
|
||||
)
|
||||
|
||||
H.append(
|
||||
f"""<p>
|
||||
<form action="{url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)}">
|
||||
Chercher étape courante:
|
||||
<input name="etape_apo" type="text" size="8" spellcheck="false"></input>
|
||||
</form>
|
||||
</p>"""
|
||||
)
|
||||
#
|
||||
H.append(
|
||||
"""<hr>
|
||||
<h3>Gestion des étudiants</h3>
|
||||
<ul>
|
||||
"""
|
||||
)
|
||||
if current_user.has_permission(Permission.EtudInscrit):
|
||||
H.append(
|
||||
f"""
|
||||
<li><a class="stdlink" href="{
|
||||
url_for("scolar.etudident_create_form", scodoc_dept=g.scodoc_dept)
|
||||
}">créer <em>un</em> nouvel étudiant</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{
|
||||
url_for("scolar.form_students_import_excel", scodoc_dept=g.scodoc_dept)
|
||||
}">importer de nouveaux étudiants</a>
|
||||
(<em>ne pas utiliser</em> sauf cas particulier : utilisez plutôt le lien dans
|
||||
le tableau de bord semestre si vous souhaitez inscrire les
|
||||
étudiants importés à un semestre)
|
||||
</li>
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
f"""
|
||||
<li><a class="stdlink" href="{
|
||||
url_for("scolar.export_etudiants_courants", scodoc_dept=g.scodoc_dept)
|
||||
}">exporter tableau des étudiants des semestres en cours</a>
|
||||
</li>
|
||||
"""
|
||||
)
|
||||
if current_user.has_permission(
|
||||
Permission.EtudInscrit
|
||||
) and sco_preferences.get_preference("portal_url"):
|
||||
H.append(
|
||||
f"""
|
||||
<li><a class="stdlink" href="{
|
||||
url_for("scolar.formsemestre_import_etud_admission",
|
||||
scodoc_dept=g.scodoc_dept, tous_courants=1)
|
||||
}">resynchroniser les données étudiants des semestres en cours depuis le portail</a>
|
||||
</li>
|
||||
"""
|
||||
)
|
||||
H.append("</ul>")
|
||||
#
|
||||
if current_user.has_permission(Permission.EditApogee):
|
||||
H.append(
|
||||
f"""<hr>
|
||||
<h3>Exports Apogée</h3>
|
||||
<ul>
|
||||
<li><a class="stdlink" href="{url_for('notes.semset_page', scodoc_dept=g.scodoc_dept)
|
||||
}">Années scolaires / exports Apogée</a></li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
#
|
||||
H.append(
|
||||
"""<hr>
|
||||
<h3>Assistance</h3>
|
||||
<ul>
|
||||
<li><a class="stdlink" href="https://scodoc.org/Contact" target="_blank"
|
||||
rel="noopener noreferrer">Contact (Discord)</a></li>
|
||||
<li><a class="stdlink" href="sco_dump_and_send_db">Envoyer données</a></li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
#
|
||||
return (
|
||||
html_sco_header.sco_header(
|
||||
page_title=f"ScoDoc {g.scodoc_dept}", javascripts=["js/scolar_index.js"]
|
||||
)
|
||||
+ "\n".join(H)
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def _sem_table(sems):
|
||||
def _sem_table(sems: list[dict]) -> str:
|
||||
"""Affiche liste des semestres, utilisée pour semestres en cours"""
|
||||
tmpl = """<tr class="%(trclass)s">%(tmpcode)s
|
||||
<td class="semicon">%(lockimg)s <a href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s#groupes">%(groupicon)s</a></td>
|
||||
tmpl = f"""<tr class="%(trclass)s">%(tmpcode)s
|
||||
<td class="semicon">%(lockimg)s <a href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept)}?formsemestre_id=%(formsemestre_id)s#groupes">%(groupicon)s</a></td>
|
||||
<td class="datesem">%(mois_debut)s <a title="%(session_id)s">-</a> %(mois_fin)s</td>
|
||||
<td><a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
|
||||
<td class="titresem"><a class="stdlink" href="{url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept)}?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
|
||||
<span class="respsem">(%(responsable_name)s)</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -251,25 +201,34 @@ def _sem_table(sems):
|
|||
cur_idx = sem["semestre_id"]
|
||||
else:
|
||||
sem["trclass"] = ""
|
||||
sem["notes_url"] = scu.NotesURL()
|
||||
H.append(tmpl % sem)
|
||||
H.append("</table>")
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def _sem_table_gt(sems, showcodes=False):
|
||||
"""Nouvelle version de la table des semestres
|
||||
def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable:
|
||||
"""Table des semestres
|
||||
Utilise une datatables.
|
||||
"""
|
||||
_style_sems(sems)
|
||||
sems = _style_sems(
|
||||
_convert_formsemestres_to_dicts(formsemestres, showcodes, fmt=fmt), fmt=fmt
|
||||
)
|
||||
sems.sort(
|
||||
key=lambda s: (
|
||||
-s["anneescolaire"],
|
||||
s["semestre_id"] if s["semestre_id"] > 0 else -s["semestre_id"] * 1000,
|
||||
s["modalite"],
|
||||
)
|
||||
)
|
||||
columns_ids = (
|
||||
"lockimg",
|
||||
"published",
|
||||
"semestre_id_n",
|
||||
"modalite",
|
||||
#'mois_debut',
|
||||
"dash_mois_fin",
|
||||
"titre_resp",
|
||||
"nb_inscrits",
|
||||
"formation",
|
||||
"etapes_apo_str",
|
||||
"elt_annee_apo",
|
||||
"elt_sem_apo",
|
||||
|
@ -284,7 +243,7 @@ def _sem_table_gt(sems, showcodes=False):
|
|||
titles={
|
||||
"formsemestre_id": "id",
|
||||
"semestre_id_n": "S#",
|
||||
"modalite": "",
|
||||
"modalite": "" if fmt == "html" else "Modalité",
|
||||
"mois_debut": "Début",
|
||||
"dash_mois_fin": "Année",
|
||||
"titre_resp": "Semestre",
|
||||
|
@ -292,6 +251,7 @@ def _sem_table_gt(sems, showcodes=False):
|
|||
"etapes_apo_str": "Étape Apo.",
|
||||
"elt_annee_apo": "Elt. année Apo.",
|
||||
"elt_sem_apo": "Elt. sem. Apo.",
|
||||
"formation": "Formation",
|
||||
},
|
||||
columns_ids=columns_ids,
|
||||
rows=sems,
|
||||
|
@ -311,22 +271,47 @@ def _sem_table_gt(sems, showcodes=False):
|
|||
return tab
|
||||
|
||||
|
||||
def _style_sems(sems):
|
||||
def _style_sems(sems: list[dict], fmt="html") -> list[dict]:
|
||||
"""ajoute quelques attributs de présentation pour la table"""
|
||||
for sem in sems:
|
||||
sem["notes_url"] = scu.NotesURL()
|
||||
sem["_groupicon_target"] = (
|
||||
"%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s"
|
||||
% sem
|
||||
is_h = fmt == "html"
|
||||
if is_h:
|
||||
icon_published = scu.icontag(
|
||||
"eye_img",
|
||||
border="0",
|
||||
title="Bulletins publiés sur la passerelle étudiants",
|
||||
)
|
||||
icon_hidden = scu.icontag(
|
||||
"hide_img",
|
||||
border="0",
|
||||
title="Bulletins NON publiés sur la passerelle étudiants",
|
||||
)
|
||||
else:
|
||||
icon_published = "publié"
|
||||
icon_hidden = "non publié"
|
||||
for sem in sems:
|
||||
status_url = url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=sem["formsemestre_id"],
|
||||
)
|
||||
sem["_groupicon_target"] = status_url
|
||||
sem["_formsemestre_id_class"] = "blacktt"
|
||||
sem["dash_mois_fin"] = '<a title="%(session_id)s"></a> %(anneescolaire)s' % sem
|
||||
sem["dash_mois_fin"] = (
|
||||
(f"""<a title="{sem['session_id']}">{sem['anneescolaire_str']}</a>""")
|
||||
if is_h
|
||||
else sem["anneescolaire_str"]
|
||||
)
|
||||
sem["_dash_mois_fin_class"] = "datesem"
|
||||
sem["titre_resp"] = (
|
||||
"""<a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
|
||||
<span class="respsem">(%(responsable_name)s)</span>"""
|
||||
% sem
|
||||
(
|
||||
f"""<a class="stdlink" href="{status_url}">{sem['titre_num']}</a>
|
||||
<span class="respsem">({sem['responsable_name']})</span>"""
|
||||
)
|
||||
if is_h
|
||||
else f"""{sem['titre_num']} ({sem["responsable_name"]})"""
|
||||
)
|
||||
sem["published"] = icon_hidden if sem["bul_hide_xml"] else icon_published
|
||||
|
||||
sem["_css_row_class"] = "css_S%d css_M%s" % (
|
||||
sem["semestre_id"],
|
||||
sem["modalite"],
|
||||
|
@ -347,6 +332,7 @@ def _style_sems(sems):
|
|||
sem["_elt_sem_apo_td_attrs"] = (
|
||||
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
|
||||
)
|
||||
return sems
|
||||
|
||||
|
||||
def delete_dept(dept_id: int) -> str:
|
||||
|
|
|
@ -494,7 +494,7 @@ def table_formsemestres(
|
|||
):
|
||||
"""Une table presentant des semestres"""
|
||||
for sem in sems:
|
||||
sem_set_responsable_name(sem)
|
||||
sem_set_responsable_name(sem) # TODO utiliser formsemestre.responsables_str()
|
||||
sem["_titre_num_target"] = url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
|
|
|
@ -798,7 +798,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||
'Tous les étudiants'}
|
||||
</div>
|
||||
<div class="sem-groups-partition-titre">{
|
||||
"Gestion de l'assiduité" if not partition_is_empty else ""
|
||||
"Assiduité" if not partition_is_empty else ""
|
||||
}</div>
|
||||
"""
|
||||
)
|
||||
|
@ -824,14 +824,14 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||
</div>
|
||||
<div class="sem-groups-assi">
|
||||
<div>
|
||||
<a class="btn" href="{
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.visu_assi_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
date_debut=formsemestre.date_debut.isoformat(),
|
||||
date_fin=formsemestre.date_fin.isoformat(),
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
<button>Bilan assiduité</button></a>
|
||||
Bilan</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
@ -839,42 +839,42 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||
H.append(
|
||||
f"""
|
||||
<div>
|
||||
<a class="btn" href="{
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.visu_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
jour = datetime.date.today().isoformat(),
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
<button>Visualiser</button></a>
|
||||
Visualiser</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn" href="{
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
jour=datetime.date.today().isoformat(),
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
<button>Saisie journalière</button></a>
|
||||
Saisie journalière</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn" href="{
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_diff",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
<button>Saisie différée</button></a>
|
||||
Saisie différée</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn" href="{
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.bilan_dept",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
<button>Justificatifs en attente</button></a>
|
||||
Justificatifs en attente</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -1210,7 +1210,9 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
|
|||
|
||||
<p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement,
|
||||
<em>dans un semestre hors ScoDoc</em>.</p>
|
||||
<p class="expl"><b>Les UE validées dans ScoDoc sont
|
||||
|
||||
<div class="scobox explanation">
|
||||
<p><b>Les UE validées dans ScoDoc sont
|
||||
automatiquement prises en compte</b>.
|
||||
</p>
|
||||
<p>Cette page est surtout utile pour les étudiants ayant
|
||||
|
@ -1227,11 +1229,12 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
|
|||
l'attribution des ECTS si le code jury est validant (ADM).
|
||||
</p>
|
||||
<p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
|
||||
</div>
|
||||
|
||||
{_get_etud_ue_cap_html(etud, formsemestre)}
|
||||
|
||||
<div class="sco_box">
|
||||
<div class="sco_box_title">
|
||||
<div class="scobox">
|
||||
<div class="scobox-title">
|
||||
Enregistrer une UE antérieure
|
||||
</div>
|
||||
{tf[1]}
|
||||
|
|
|
@ -50,8 +50,8 @@ def list_formsemestres_modalites(sems):
|
|||
return modalites
|
||||
|
||||
|
||||
def group_sems_by_modalite(sems):
|
||||
"""Given the list of fromsemestre, group them by modalite,
|
||||
def group_sems_by_modalite(sems: list[dict]):
|
||||
"""Given the list of formsemestre, group them by modalite,
|
||||
sorted in each one by semestre id and date
|
||||
"""
|
||||
sems_by_mod = collections.defaultdict(list)
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
"""Tableau de bord module
|
||||
"""
|
||||
"""Tableau de bord module"""
|
||||
|
||||
import math
|
||||
import time
|
||||
import datetime
|
||||
|
@ -329,8 +329,6 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
>Saisie Absences journée</a></span>
|
||||
"""
|
||||
)
|
||||
year, week, day = datetime.date.today().isocalendar()
|
||||
semaine: str = f"{year}-W{week}"
|
||||
H.append(
|
||||
f"""
|
||||
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
|
||||
|
@ -338,11 +336,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
"assiduites.signal_assiduites_diff",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
group_ids=group_id,
|
||||
semaine=semaine,
|
||||
formsemestre_id=formsemestre.id,
|
||||
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
|
||||
)}"
|
||||
>Saisie Absences hebdo</a></span>
|
||||
>Saisie Absences Différée</a></span>
|
||||
"""
|
||||
)
|
||||
|
||||
|
|
|
@ -1442,7 +1442,7 @@ def icontag(name, file_format="png", no_size=False, **attrs):
|
|||
|
||||
|
||||
ICON_PDF = icontag("pdficon16x20_img", title="Version PDF")
|
||||
ICON_XLS = icontag("xlsicon_img", title="Version tableur")
|
||||
ICON_XLS = icontag("xlsicon_img", title="Export tableur (xlsx)")
|
||||
|
||||
# HTML emojis
|
||||
EMO_WARNING = "⚠️" # warning /!\
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
--color-justi: #29b990;
|
||||
--color-justi-clair: #48f6ff;
|
||||
--color-justi-attente: yellow;
|
||||
--color-justi-attente-stripe: #29b990; /* pink #fa25cb; */ /* #789dbb;*/
|
||||
--color-justi-attente-stripe: #29b990;
|
||||
/* pink #fa25cb; */
|
||||
/* #789dbb;*/
|
||||
--color-justi-modifie: rgb(255, 230, 0);
|
||||
--color-justi-invalide: #a84476;
|
||||
--color-nonwork: #badfff;
|
||||
|
@ -28,27 +30,23 @@
|
|||
--color-defaut-dark: #444;
|
||||
--color-default-text: #1f1f1f;
|
||||
|
||||
--motif-justi: repeating-linear-gradient(
|
||||
135deg,
|
||||
transparent,
|
||||
transparent 4px,
|
||||
var(--color-justi) 4px,
|
||||
var(--color-justi) 8px
|
||||
);
|
||||
--motif-justi-invalide: repeating-linear-gradient(
|
||||
-135deg,
|
||||
transparent,
|
||||
transparent 4px,
|
||||
var(--color-justi-invalide) 4px,
|
||||
var(--color-justi-invalide) 8px
|
||||
);
|
||||
--motif-justi: repeating-linear-gradient(135deg,
|
||||
transparent,
|
||||
transparent 4px,
|
||||
var(--color-justi) 4px,
|
||||
var(--color-justi) 8px);
|
||||
--motif-justi-invalide: repeating-linear-gradient(-135deg,
|
||||
transparent,
|
||||
transparent 4px,
|
||||
var(--color-justi-invalide) 4px,
|
||||
var(--color-justi-invalide) 8px);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.selectors > * {
|
||||
.selectors>* {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
|
@ -339,6 +337,11 @@
|
|||
background-image: url(../icons/retard.svg);
|
||||
}
|
||||
|
||||
.rbtn.conflit::before {
|
||||
background-color: var(--color-absent);
|
||||
background-image: url(../icons/solveur_conflits.svg);
|
||||
}
|
||||
|
||||
.rbtn:checked:before {
|
||||
outline: 5px solid var(--color-primary);
|
||||
border-radius: 50%;
|
||||
|
@ -405,29 +408,11 @@
|
|||
.assiduite {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
z-index: 10;
|
||||
height: 100px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.assiduite-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.assiduite-id,
|
||||
.assiduite-period,
|
||||
.assiduite-state,
|
||||
.assiduite-user_id {
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.assiduites-container {
|
||||
|
@ -438,7 +423,7 @@
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
.modal-buttons {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
|
@ -449,48 +434,38 @@
|
|||
bottom: 5%;
|
||||
}
|
||||
|
||||
/* Ajout de la classe CSS pour la bordure en pointillés */
|
||||
.assiduite.selected {
|
||||
border: 2px dashed black;
|
||||
}
|
||||
|
||||
.assiduite-special {
|
||||
height: 120px;
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
border: 2px solid #000;
|
||||
background-color: rgba(36, 36, 36, 0.25);
|
||||
background-image: repeating-linear-gradient(
|
||||
135deg,
|
||||
transparent,
|
||||
transparent 5px,
|
||||
rgba(81, 81, 81, 0.61) 5px,
|
||||
rgba(81, 81, 81, 0.61) 10px
|
||||
);
|
||||
border: 5px solid var(--color-primary);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/*<== Info sur l'assiduité sélectionnée ==>*/
|
||||
.modal-assiduite-content {
|
||||
background-color: #fefefe;
|
||||
margin: 5% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: max-content;
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
display: none;
|
||||
.assiduite .assiduite-bubble {
|
||||
top: 5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.modal-assiduite-content.show {
|
||||
display: block;
|
||||
.assiduite-infos {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 5px;
|
||||
top: 0;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-assiduite-content .infos {
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
|
||||
/*<=== Mass Action ==>*/
|
||||
|
@ -500,57 +475,16 @@
|
|||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 2% 0;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mass-selection span {
|
||||
margin: 0 1%;
|
||||
}
|
||||
|
||||
|
||||
.mass-selection .rbtn {
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*<== Loader ==> */
|
||||
|
||||
.loader-container {
|
||||
display: none;
|
||||
/* Cacher le loader par défaut */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
/* Fond semi-transparent pour bloquer les clics */
|
||||
z-index: 9999;
|
||||
/* Placer le loader au-dessus de tout le contenu */
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 6px solid #f3f3f3;
|
||||
border-radius: 50%;
|
||||
border-top: 6px solid var(--color-primary);
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.fieldsplit {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
@ -569,7 +503,7 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
#page-assiduite-content > * {
|
||||
#page-assiduite-content>* {
|
||||
margin: 1.5% 0;
|
||||
}
|
||||
|
||||
|
@ -643,12 +577,6 @@
|
|||
border: solid 1px #333;
|
||||
}
|
||||
|
||||
.assi-liste {
|
||||
border: 1px solid gray;
|
||||
border-radius: 12px;
|
||||
margin-right: 24px;
|
||||
padding: 12px;
|
||||
}
|
||||
#options-tableau label {
|
||||
font-weight: normal;
|
||||
margin-right: 12px;
|
||||
|
@ -657,15 +585,20 @@
|
|||
section.assi-form {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
table.liste_assi td.date {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
table.liste_assi.dataTable tbody td.date-debut {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
table.liste_assi td.actions {
|
||||
white-space: nowrap; /* boutons horizontalement */
|
||||
white-space: nowrap;
|
||||
/* boutons horizontalement */
|
||||
}
|
||||
|
||||
table.liste_assi td.actions a:last-child {
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
@ -673,31 +606,154 @@ table.liste_assi td.actions a:last-child {
|
|||
tr.row-assiduite td {
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
|
||||
table.liste_assi tbody tr td.assi-type {
|
||||
padding-left: 8px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
tr.row-assiduite.absent td.assi-type {
|
||||
background-color: var(--color-absent-clair);
|
||||
}
|
||||
|
||||
tr.row-assiduite.absent.justifiee td.assi-type {
|
||||
background-color: var(--color-absent-justi);
|
||||
}
|
||||
|
||||
tr.row-assiduite.retard td.assi-type {
|
||||
background-color: var(--color-retard);
|
||||
}
|
||||
|
||||
tr.row-assiduite.present td.assi-type {
|
||||
background-color: var(--color-present);
|
||||
}
|
||||
|
||||
tr.row-justificatif.valide td.assi-type {
|
||||
background-color: var(--color-justi);
|
||||
}
|
||||
|
||||
tr.row-justificatif.attente td.assi-type {
|
||||
background-color: var(--color-justi-attente);
|
||||
}
|
||||
|
||||
tr.row-justificatif.modifie td.assi-type {
|
||||
background-color: var(--color-justi-modifie);
|
||||
}
|
||||
|
||||
tr.row-justificatif.non_valide td.assi-type {
|
||||
background-color: var(--color-justi-invalide);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
<== Loader ==>
|
||||
|
||||
*/
|
||||
/* HTML: <div class="loader"></div> */
|
||||
.loader {
|
||||
width: 80px;
|
||||
height: 70px;
|
||||
border: 5px solid #000;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
background:
|
||||
linear-gradient(#fff 0 0) 0 0/8px 20px,
|
||||
linear-gradient(#fff 0 0) 100% 0/8px 20px,
|
||||
radial-gradient(farthest-side, #fff 90%, #0000) 0 5px/8px 8px content-box,
|
||||
#000;
|
||||
background-repeat: no-repeat;
|
||||
animation: l3 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes l3 {
|
||||
25% {
|
||||
background-position: 0 0, 100% 100%, 100% calc(100% - 5px)
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 0 100%, 100% 100%, 0 calc(100% - 5px)
|
||||
}
|
||||
|
||||
75% {
|
||||
background-position: 0 100%, 100% 0, 100% 5px
|
||||
}
|
||||
}
|
||||
|
||||
#loader {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1000;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/**
|
||||
* <== Couleurs ==>
|
||||
*/
|
||||
|
||||
.color.present {
|
||||
background-color: var(--color-present) !important;
|
||||
}
|
||||
|
||||
.color.absent {
|
||||
background-color: var(--color-absent) !important;
|
||||
}
|
||||
|
||||
.color.absent.est_just {
|
||||
background-color: var(--color-absent-justi) !important;
|
||||
}
|
||||
|
||||
.color.retard {
|
||||
background-color: var(--color-retard) !important;
|
||||
}
|
||||
|
||||
.color.retard.est_just {
|
||||
background-color: var(--color-retard-justi) !important;
|
||||
}
|
||||
|
||||
.color.nonwork {
|
||||
background-color: var(--color-nonwork) !important;
|
||||
}
|
||||
|
||||
.color {
|
||||
background-color: var(--color-defaut) !important;
|
||||
}
|
||||
|
||||
.color.est_just.sans_etat::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
background-color: var(--color-justi) !important;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.color.invalide::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
background-color: var(--color-justi-invalide) !important;
|
||||
}
|
||||
|
||||
.color.attente::before,
|
||||
.color.modifie::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
background: repeating-linear-gradient(to bottom,
|
||||
var(--color-justi-attente-stripe) 0px,
|
||||
var(--color-justi-attente-stripe) 4px,
|
||||
var(--color-justi-attente) 4px,
|
||||
var(--color-justi-attente) 7px) !important;
|
||||
}
|
|
@ -94,7 +94,9 @@
|
|||
top: 200%;
|
||||
}
|
||||
|
||||
.mini-timeline-block:hover .assiduite-bubble {
|
||||
.mini-timeline-block:hover .assiduite-bubble,
|
||||
#prevDateAssi:hover .assiduite-bubble,
|
||||
.assiduites-container .assiduite:hover .assiduite-bubble {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
@ -103,6 +105,11 @@
|
|||
max-height: 150px;
|
||||
}
|
||||
|
||||
#prevDateAssi:hover .assiduite-bubble {
|
||||
transform: translateY(55%);
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.assiduite-bubble::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
@ -189,24 +196,4 @@
|
|||
.mini-timeline-block.creneau {
|
||||
outline: 3px solid var(--color-primary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mini-timeline-block.absent {
|
||||
background-color: var(--color-absent) !important;
|
||||
}
|
||||
|
||||
.mini-timeline-block.present {
|
||||
background-color: var(--color-present) !important;
|
||||
}
|
||||
|
||||
.mini-timeline-block.retard {
|
||||
background-color: var(--color-retard) !important;
|
||||
}
|
||||
|
||||
.mini-timeline-block.justified {
|
||||
background-image: var(--motif-justi);
|
||||
}
|
||||
|
||||
.mini-timeline-block.invalid_justified {
|
||||
background-image: var(--motif-justi-invalide);
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
--sco-content-max-width: 1024px;
|
||||
--sco-color-explication: rgb(10, 58, 140);
|
||||
--sco-color-background: rgb(242, 242, 238);
|
||||
--sco-color-box-bg: rgb(243, 240, 228);
|
||||
--sco-color-mod-std: #afafc2;
|
||||
--sco-color-ressources: #f8c844;
|
||||
--sco-color-saes: #c6ffab;
|
||||
|
@ -28,10 +29,6 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
div.container {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
|
@ -43,6 +40,51 @@ h3 {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
details > summary:first-of-type {
|
||||
display: list-item!important;
|
||||
}
|
||||
|
||||
div.container {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
div.sco-app-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
div.scobox {
|
||||
flex: 1 0 0; /* Equal width for all boxes */
|
||||
max-width: var(--sco-content-max-width);
|
||||
/* margin: 5px; Optional: Add margin between boxes */
|
||||
|
||||
background-color: var(--sco-color-box-bg);
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
margin-right: 12px;
|
||||
padding: 8px;
|
||||
border: 1px solid #c5b4b2;
|
||||
border-radius: 8px;
|
||||
}
|
||||
div.scobox.explanation {
|
||||
background-color: var(--sco-color-background);
|
||||
}
|
||||
div.scobox div.scobox-title {
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
div.scobox-buttons {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
div.scobox-buttons input {
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
div.scobox-etud {
|
||||
background-color: var(--sco-color-background);
|
||||
}
|
||||
|
||||
/* customization of multiselect style */
|
||||
.multiselect-container.dropdown-menu {
|
||||
background-color: #e9e9e9;
|
||||
|
@ -554,10 +596,6 @@ table.listesems tr.firstsem td {
|
|||
padding-top: 0.8em;
|
||||
}
|
||||
|
||||
td.datesem {
|
||||
font-size: 80%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
h2.listesems {
|
||||
padding-top: 10px;
|
||||
|
@ -645,60 +683,51 @@ table.semlist tbody tr td.modalite {
|
|||
}
|
||||
|
||||
div#gtrcontent table.semlist tbody tr.css_S-1 td {
|
||||
background-color: rgb(251, 250, 216);
|
||||
background-color: rgb(211, 213, 255);
|
||||
}
|
||||
|
||||
div#gtrcontent table.semlist tbody tr.css_S1 td {
|
||||
background-color: rgb(92%, 95%, 94%);
|
||||
background-color:#e9efef;
|
||||
}
|
||||
|
||||
div#gtrcontent table.semlist tbody tr.css_S2 td {
|
||||
background-color: rgb(214, 223, 236);
|
||||
background-color: #d4ebd7;
|
||||
}
|
||||
|
||||
div#gtrcontent table.semlist tbody tr.css_S3 td {
|
||||
background-color: rgb(167, 216, 201);
|
||||
background-color: #bedebe;
|
||||
}
|
||||
|
||||
div#gtrcontent table.semlist tbody tr.css_S4 td {
|
||||
background-color: rgb(131, 225, 140);
|
||||
background-color: #afd7ad;
|
||||
}
|
||||
div#gtrcontent table.semlist tbody tr.css_S5 td {
|
||||
background-color: #a0cd9a;
|
||||
}
|
||||
div#gtrcontent table.semlist tbody tr.css_S6 td {
|
||||
background-color: #7dcf78;
|
||||
}
|
||||
|
||||
div#gtrcontent table.semlist tbody tr.css_MEXT td {
|
||||
color: #0b6e08;
|
||||
color: #fefcdf;
|
||||
}
|
||||
|
||||
/* ----- Liste des news ----- */
|
||||
|
||||
div.news {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
div.scobox.news {
|
||||
font-size: 10pt;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0px;
|
||||
margin-right: 16px;
|
||||
margin-left: 16px;
|
||||
padding: 0.5em;
|
||||
background-color: rgb(255, 235, 170);
|
||||
-moz-border-radius: 8px;
|
||||
-khtml-border-radius: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
div.news a,
|
||||
div.news a.stdlink {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.news a:hover {
|
||||
color: rgb(153, 51, 51);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
span.newstitle {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul.newslist {
|
||||
padding-left: 1em;
|
||||
padding-bottom: 0em;
|
||||
|
@ -713,6 +742,21 @@ span.newsdate {
|
|||
span.newstext {
|
||||
font-style: normal;
|
||||
}
|
||||
/* div.news {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 10pt;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0px;
|
||||
margin-right: 16px;
|
||||
margin-left: 16px;
|
||||
padding: 0.5em;
|
||||
background-color: rgb(255, 235, 170);
|
||||
-moz-border-radius: 8px;
|
||||
-khtml-border-radius: 8px;
|
||||
border-radius: 8px;
|
||||
} */
|
||||
|
||||
|
||||
|
||||
span.gt_export_icons {
|
||||
margin-left: 1.5em;
|
||||
|
@ -1184,7 +1228,6 @@ a.discretelink:hover {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.expl,
|
||||
.help {
|
||||
max-width: var(--sco-content-max-width);
|
||||
}
|
||||
|
@ -1980,9 +2023,18 @@ ul.ue_inscr_list li.etud {
|
|||
grid-template-columns: 240px auto;
|
||||
}
|
||||
|
||||
.sem-groups-partition .stdlink, .sem-groups-partition .stdlink:visited {
|
||||
color: black;
|
||||
text-decoration-style: dotted;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.sem-groups-list .stdlink, .sem-groups-list .stdlink:visited {
|
||||
color:rgb(0, 0, 192);
|
||||
}
|
||||
|
||||
.sem-groups-list,
|
||||
.sem-groups-assi {
|
||||
background-color: white;
|
||||
background-color: #ebebeb;
|
||||
border-radius: 6px;
|
||||
margin: 4px;
|
||||
}
|
||||
|
@ -4102,22 +4154,18 @@ div.othersemlist input {
|
|||
margin-left: 20px;
|
||||
}
|
||||
|
||||
div#update_warning {
|
||||
div.scobox.update_warning {
|
||||
display: none;
|
||||
border: 1px solid red;
|
||||
background-color: rgb(250, 220, 220);
|
||||
margin: 3ex;
|
||||
padding-left: 1ex;
|
||||
padding-right: 1ex;
|
||||
padding-bottom: 1ex;
|
||||
}
|
||||
|
||||
div#update_warning > div:first-child:before {
|
||||
div.scobox.update_warning > div:first-child:before {
|
||||
content: url(/ScoDoc/static/icons/warning_img.png);
|
||||
vertical-align: -80%;
|
||||
}
|
||||
|
||||
div#update_warning > div:nth-child(2) {
|
||||
div.scobox.update_warning > div:nth-child(2) {
|
||||
font-size: 80%;
|
||||
padding-left: 8ex;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" height="85" width="85" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 482.568 482.568" xml:space="preserve">
|
||||
<g>
|
||||
<g opacity="0.7">
|
||||
<path d="M116.993,203.218c13.4-1.8,26.8,2.8,36.3,12.3l24,24l22.7-22.6l-32.8-32.7c-5.1-5.1-5.1-13.4,0-18.5s13.4-5.1,18.5,0
|
||||
l32.8,32.8l22.7-22.6l-24.1-24.1c-9.5-9.5-14.1-23-12.3-36.3c4-30.4-5.7-62.2-29-85.6c-23.8-23.8-56.4-33.4-87.3-28.8
|
||||
c-4.9,0.7-6.9,6.8-3.4,10.3l30.9,30.9c14.7,14.7,14.7,38.5,0,53.1l-19,19c-14.7,14.7-38.5,14.7-53.1,0l-31-30.9
|
||||
c-3.5-3.5-9.5-1.5-10.3,3.4c-4.6,30.9,5,63.5,28.8,87.3C54.793,197.518,86.593,207.218,116.993,203.218z"/>
|
||||
<path d="M309.193,243.918l-22.7,22.6l134.8,134.8c5.1,5.1,5.1,13.4,0,18.5s-13.4,5.1-18.5,0l-134.8-134.8l-22.7,22.6l138.9,138.9
|
||||
c17.6,17.6,46.1,17.5,63.7-0.1s17.6-46.1,0.1-63.7L309.193,243.918z"/>
|
||||
<path d="M361.293,153.918h59.9l59.9-119.7l-29.9-29.9l-119.8,59.8v59.9l-162.8,162.3l-29.3-29.2l-118,118
|
||||
c-24.6,24.6-24.6,64.4,0,89s64.4,24.6,89,0l118-118l-29.9-29.9L361.293,153.918z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -255,6 +255,13 @@ Object.defineProperty(Date.prototype, "format", {
|
|||
value: function (formatString) {
|
||||
let iso = this.toIsoUtcString();
|
||||
switch (formatString) {
|
||||
case "DD/MM/YYYY":
|
||||
return this.toLocaleString("fr-FR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
timeZone: SCO_TIMEZONE,
|
||||
});
|
||||
case "DD/MM/Y HH:mm":
|
||||
return this.toLocaleString("fr-FR", {
|
||||
day: "2-digit",
|
||||
|
@ -275,6 +282,8 @@ Object.defineProperty(Date.prototype, "format", {
|
|||
hour12: false,
|
||||
timeZone: SCO_TIMEZONE,
|
||||
});
|
||||
case "HH:mm":
|
||||
return iso.slice(11, 16);
|
||||
|
||||
case "YYYY-MM-DDTHH:mm":
|
||||
// slice : YYYY-MM-DDTHH
|
||||
|
@ -407,196 +416,17 @@ class Duration {
|
|||
}
|
||||
}
|
||||
|
||||
class ScoDocDateTimePicker extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
// Définir si le champ est requis
|
||||
this.required = this.hasAttribute("required");
|
||||
|
||||
// Initialiser le shadow DOM
|
||||
const shadow = this.attachShadow({ mode: "open" });
|
||||
|
||||
// Créer l'input pour la date
|
||||
const dateInput = document.createElement("input");
|
||||
dateInput.type = "date";
|
||||
dateInput.id = "date";
|
||||
|
||||
// Créer l'input pour l'heure
|
||||
const timeInput = document.createElement("input");
|
||||
timeInput.type = "time";
|
||||
timeInput.id = "time";
|
||||
timeInput.step = 60;
|
||||
|
||||
// Ajouter les inputs dans le shadow DOM
|
||||
shadow.appendChild(dateInput);
|
||||
shadow.appendChild(timeInput);
|
||||
|
||||
// Gestionnaires d'événements pour la mise à jour de la valeur
|
||||
dateInput.addEventListener("change", () => this.updateValue());
|
||||
timeInput.addEventListener("change", () => this.updateValue());
|
||||
|
||||
// Style CSS pour les inputs
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
input {
|
||||
display: inline-block;
|
||||
}
|
||||
input:invalid {
|
||||
border: 1px solid red;
|
||||
}
|
||||
`;
|
||||
|
||||
// Ajouter le style au shadow DOM
|
||||
shadow.appendChild(style);
|
||||
|
||||
//Si une value est donnée
|
||||
|
||||
let value = this.getAttribute("value");
|
||||
if (value != null) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ["show"]; // Ajoute 'show' à la liste des attributs observés
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Récupérer l'attribut 'name'
|
||||
this.name = this.getAttribute("name");
|
||||
|
||||
// Créer un input caché pour la valeur datetime
|
||||
this.hiddenInput = document.createElement("input");
|
||||
this.hiddenInput.type = "hidden";
|
||||
this.hiddenInput.name = this.name;
|
||||
this.appendChild(this.hiddenInput);
|
||||
|
||||
// Gérer la soumission du formulaire
|
||||
this.closest("form")?.addEventListener("submit", (e) => {
|
||||
if (!this.validate()) {
|
||||
e.preventDefault(); // Empêcher la soumission si non valide
|
||||
this.dispatchEvent(
|
||||
new Event("invalid", { bubbles: true, cancelable: true })
|
||||
);
|
||||
} else {
|
||||
// Mettre à jour la valeur de l'input caché avant la soumission
|
||||
this.hiddenInput.value = this.isValid()
|
||||
? this.valueAsDate.toFakeIso()
|
||||
: "";
|
||||
}
|
||||
});
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (name === "show") {
|
||||
this.updateDisplay(); // Met à jour l'affichage si l'attribut 'show' change
|
||||
}
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
const mode = this.getAttribute("show") || "both";
|
||||
const dateInput = this.shadowRoot.querySelector("#date");
|
||||
const timeInput = this.shadowRoot.querySelector("#time");
|
||||
|
||||
switch (mode) {
|
||||
case "date":
|
||||
dateInput.style.display = "inline-block";
|
||||
timeInput.style.display = "none";
|
||||
break;
|
||||
case "time":
|
||||
dateInput.style.display = "none";
|
||||
timeInput.style.display = "inline-block";
|
||||
break;
|
||||
case "both":
|
||||
default:
|
||||
dateInput.style.display = "inline-block";
|
||||
timeInput.style.display = "inline-block";
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier si la valeur forme une date valide
|
||||
isValid() {
|
||||
return !Number.isNaN(this.valueAsDate.getTime());
|
||||
}
|
||||
|
||||
// Valider l'élément
|
||||
validate() {
|
||||
if (this.required && !this.isValid()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mettre à jour la valeur interne
|
||||
updateValue() {
|
||||
const dateInput = this.shadowRoot.querySelector("#date");
|
||||
const timeInput = this.shadowRoot.querySelector("#time");
|
||||
this._value = `${dateInput.value}T${timeInput.value}`;
|
||||
this.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
|
||||
// Appliquer le style 'invalid' si nécessaire
|
||||
dateInput.classList.toggle("invalid", this.required && !this.isValid());
|
||||
timeInput.classList.toggle("invalid", this.required && !this.isValid());
|
||||
}
|
||||
|
||||
// Getter pour obtenir la valeur actuelle.
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
get valueAsObject() {
|
||||
const dateInput = this.shadowRoot.querySelector("#date");
|
||||
const timeInput = this.shadowRoot.querySelector("#time");
|
||||
return {
|
||||
date: dateInput.value,
|
||||
time: timeInput.value,
|
||||
};
|
||||
}
|
||||
|
||||
// Getter pour obtenir la valeur en tant qu'objet Date.
|
||||
get valueAsDate() {
|
||||
return new Date(this._value);
|
||||
}
|
||||
|
||||
// Setter pour définir la valeur. Sépare la valeur en date et heure et les définit individuellement.
|
||||
set value(val) {
|
||||
let [date, time] = val.split("T");
|
||||
this.shadowRoot.querySelector("#date").value = date;
|
||||
|
||||
time = time.substring(0, 5);
|
||||
|
||||
this.shadowRoot.querySelector("#time").value = time;
|
||||
this._value = val;
|
||||
}
|
||||
|
||||
// Setter pour définir la valeur à partir d'un objet avec les propriétés 'date' et 'time'.
|
||||
set valueAsObject(obj) {
|
||||
const dateInput = this.shadowRoot.querySelector("#date");
|
||||
const timeInput = this.shadowRoot.querySelector("#time");
|
||||
|
||||
if (obj.hasOwnProperty("date")) {
|
||||
dateInput.value = obj.date || ""; // Définit la valeur de l'input de date si elle est fournie
|
||||
}
|
||||
|
||||
if (obj.hasOwnProperty("time")) {
|
||||
timeInput.value = obj.time.substring(0, 5) || ""; // Définit la valeur de l'input d'heure si elle est fournie
|
||||
}
|
||||
|
||||
// Met à jour la valeur interne en fonction des nouvelles valeurs des inputs
|
||||
this.updateValue();
|
||||
}
|
||||
|
||||
// Setter pour définir la valeur à partir d'un objet Date.
|
||||
set valueAsDate(dateVal) {
|
||||
// Formatage de l'objet Date en string et mise à jour de la valeur.
|
||||
this.value = `${dateVal.getFullYear()}-${String(
|
||||
dateVal.getMonth() + 1
|
||||
).padStart(2, "0")}-${String(dateVal.getDate()).padStart(2, "0")}T${String(
|
||||
dateVal.getHours()
|
||||
).padStart(2, "0")}:${String(dateVal.getMinutes()).padStart(2, "0")}`;
|
||||
}
|
||||
/**
|
||||
* Fonction qui vérifie si une période est dans un interval
|
||||
* Objet période / interval
|
||||
* {
|
||||
* deb: Date,
|
||||
* fin: Date,
|
||||
* }
|
||||
* @param {object} period
|
||||
* @param {object} interval
|
||||
* @returns {boolean} Vrai si la période est dans l'interval
|
||||
*/
|
||||
function hasTimeConflict(period, interval) {
|
||||
return period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb);
|
||||
}
|
||||
|
||||
// Définition du nouvel élément personnalisé 'scodoc-datetime'.
|
||||
customElements.define("scodoc-datetime", ScoDocDateTimePicker);
|
||||
|
|
|
@ -12,7 +12,7 @@ from sqlalchemy import desc, literal, union, asc
|
|||
|
||||
from app import db, g
|
||||
from app.auth.models import User
|
||||
from app.models import Assiduite, Identite, Justificatif
|
||||
from app.models import Assiduite, Identite, Justificatif, Module
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
EtatJustificatif,
|
||||
|
@ -534,10 +534,45 @@ class RowAssiJusti(tb.Row):
|
|||
if self.table.options.show_module:
|
||||
if self.ligne["type"] == "assiduite":
|
||||
assi: Assiduite = Assiduite.query.get(self.ligne["obj_id"])
|
||||
mod: str = assi.get_module(True)
|
||||
self.add_cell("module", "Module", mod, data={"order": mod})
|
||||
if self.table.no_pagination:
|
||||
mod: Module = assi.get_module(False)
|
||||
code = mod.code if isinstance(mod, Module) else ""
|
||||
titre = ""
|
||||
if isinstance(mod, Module):
|
||||
titre = mod.titre
|
||||
elif isinstance(mod, str):
|
||||
titre = mod
|
||||
else:
|
||||
titre = "Non Spécifié"
|
||||
|
||||
self.add_cell(
|
||||
"code_module", "Code Module", code, data={"order": code}
|
||||
)
|
||||
self.add_cell(
|
||||
"titre_module",
|
||||
"Titre Module",
|
||||
titre,
|
||||
data={"order": titre},
|
||||
)
|
||||
else:
|
||||
mod: Module = assi.get_module(True)
|
||||
self.add_cell(
|
||||
"module",
|
||||
"Module",
|
||||
mod,
|
||||
data={"order": mod},
|
||||
)
|
||||
else:
|
||||
self.add_cell("module", "Module", "", data={"order": ""})
|
||||
if self.table.no_pagination:
|
||||
self.add_cell("module", "Module", "", data={"order": ""})
|
||||
else:
|
||||
self.add_cell("code_module", "Code Module", "", data={"order": ""})
|
||||
self.add_cell(
|
||||
"titre_module",
|
||||
"Titre Module",
|
||||
"",
|
||||
data={"order": ""},
|
||||
)
|
||||
|
||||
def _utilisateur(self) -> None:
|
||||
utilisateur: User = (
|
||||
|
|
|
@ -110,9 +110,9 @@ div.submit > input {
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<section class="assi-liste">
|
||||
<div class="scobox assi-liste">
|
||||
{{tableau | safe }}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -120,7 +120,6 @@ div.submit > input {
|
|||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
{% include "sco_timepicker.j2" %}
|
||||
{% endblock scripts %}
|
||||
|
|
|
@ -137,9 +137,9 @@ div.submit > input {
|
|||
</form>
|
||||
</section>
|
||||
{% if tableau %}
|
||||
<section class="assi-liste">
|
||||
<div class="scobox assi-liste">
|
||||
{{tableau | safe }}
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
@ -149,7 +149,6 @@ div.submit > input {
|
|||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
{% include "sco_timepicker.j2" %}
|
||||
<script>
|
||||
|
|
|
@ -7,21 +7,62 @@
|
|||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
{% endblock scripts %}
|
||||
|
||||
{% block app_content %}
|
||||
<h2>Traitement de l'assiduité</h2>
|
||||
|
||||
<h1>Traitement de l'assiduité</h1>
|
||||
<p class="help">
|
||||
Pour saisir l'assiduité ou consulter les états, il est recommandé de passer par
|
||||
le semestre concerné (saisie par jour ou saisie différée).
|
||||
</p>
|
||||
<p class="help">Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant,
|
||||
choisissez d'abord la personne concernée :</p>
|
||||
<br>
|
||||
{{search_etud | safe}}
|
||||
<br>
|
||||
{{billets | safe}}
|
||||
|
||||
<br>
|
||||
<div class="scobox scobox-etud">
|
||||
<p class="help">Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant,
|
||||
choisissez d'abord la personne concernée :</p>
|
||||
{{search_etud | safe}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{billets | safe}}
|
||||
<div>
|
||||
|
||||
|
||||
<div class="scobox">
|
||||
<div class="scobox-title">Télécharger tous les enregistrements d'assiduité</div>
|
||||
|
||||
<form action="{{url_for('assiduites.recup_assiduites_plage', scodoc_dept=g.scodoc_dept)}}" method="post">
|
||||
<label for="datedeb">
|
||||
Du :
|
||||
<input type="text" class="datepicker" id="datedeb" name="datedeb">
|
||||
</label>
|
||||
<br>
|
||||
<label for="datefin">
|
||||
Au :
|
||||
<input type="text" class="datepicker" id="datefin" name="datefin">
|
||||
</label>
|
||||
<br>
|
||||
<label for="formsemestre_id">Origine :</label>
|
||||
<select name="formsemestre_id" id="formsemestre_id">
|
||||
<option value="">Tout le département</option>
|
||||
{% for id, titre in formsemestres.items() %}
|
||||
{% if formsemestre_id == id %}
|
||||
<option value="{{id}}" selected>{{titre}}</option>
|
||||
{% else %}
|
||||
<option value="{{id}}">{{titre}}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="scobox-buttons">
|
||||
<input type="submit" value="Télécharger" name="telecharger">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<section class="nonvalide">
|
||||
{{tableau | safe }}
|
||||
<div class="scobox">
|
||||
{{tableau | safe }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock app_content %}
|
|
@ -37,16 +37,10 @@ Bilan assiduité de {{sco.etud.nomprenom}}
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.alerte {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
.scobox.alerte {
|
||||
text-align: center;
|
||||
border-radius: 7px;
|
||||
|
||||
background-color: var(--color-error);
|
||||
|
||||
}
|
||||
|
||||
.alerte.invisible {
|
||||
|
@ -70,11 +64,11 @@ Bilan assiduité de {{sco.etud.nomprenom}}
|
|||
|
||||
<h2>Bilan de l'assiduité de {{sco.etud.html_link_fiche()|safe}}</span></h2>
|
||||
|
||||
<section class="alerte invisible">
|
||||
<div class="scobox alerte invisible">
|
||||
<p>Attention, cet étudiant a trop d'absences</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="stats">
|
||||
<div class="scobox">
|
||||
<!-- Statistiques d'assiduité (nb pres, nb retard, nb absence) + nb justifié -->
|
||||
<h4>Statistiques d'assiduité</h4>
|
||||
<div class="stats-inputs">
|
||||
|
@ -88,17 +82,16 @@ Bilan assiduité de {{sco.etud.nomprenom}}
|
|||
<div class="stats-values">
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="scobox">
|
||||
<section class="nonvalide">
|
||||
<div class="help">Le tableau n'affiche que les assiduités non justifiées
|
||||
et les justificatifs soumis / modifiés
|
||||
</div>
|
||||
{{tableau | safe }}
|
||||
</section>
|
||||
|
||||
<section class="suppr">
|
||||
<h4>Boutons de suppresions (toute suppression est définitive) </h4>
|
||||
<button type="button" onclick="removeAllAssiduites()">Suppression des assiduités</button>
|
||||
<button type="button" onclick="removeAllJustificatifs()">Suppression des justificatifs</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="legende">
|
||||
<h3>Statistiques</h3>
|
||||
|
@ -140,8 +133,7 @@ Bilan assiduité de {{sco.etud.nomprenom}}
|
|||
}
|
||||
|
||||
function getAssiduitesCount(dateDeb, dateFin, action) {
|
||||
const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&etat=absent,retard,present&split`;
|
||||
//Utiliser async_get au lieu de Jquery
|
||||
const url_api = `../../api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&etat=absent,retard,present&split`;
|
||||
async_get(
|
||||
url_api,
|
||||
action,
|
||||
|
@ -211,79 +203,6 @@ Bilan assiduité de {{sco.etud.nomprenom}}
|
|||
getAssiduitesCount(dateDeb, dateFin, showStats);
|
||||
}
|
||||
|
||||
function removeAllAssiduites() {
|
||||
|
||||
openPromptModal(
|
||||
"Suppression de l'assiduité",
|
||||
document.createTextNode(
|
||||
'Souhaitez vous réellement supprimer toutes les informations sur l\'assiduité de cet étudiant ? Cette suppression est irréversible.')
|
||||
,
|
||||
() => {
|
||||
getAllAssiduitesFromEtud(etudid, (data) => {
|
||||
const toRemove = data.map((a) => a.assiduite_id);
|
||||
console.log(toRemove)
|
||||
deleteAssiduites(toRemove);
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
function removeAllJustificatifs() {
|
||||
openPromptModal(
|
||||
"Suppression des justificatifs",
|
||||
document.createTextNode(
|
||||
'Souhaitez vous réelement supprimer tous les justificatifs de cet étudiant ? Cette supression est irréversible.')
|
||||
,
|
||||
() => {
|
||||
getAllJustificatifsFromEtud(etudid, (data) => {
|
||||
const toRemove = data.map((a) => a.justif_id);
|
||||
|
||||
deleteJustificatifs(toRemove);
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Suppression des assiduties
|
||||
*/
|
||||
function deleteAssiduites(assi) {
|
||||
const path = getUrl() + `/api/assiduite/delete`;
|
||||
async_post(
|
||||
path,
|
||||
assi,
|
||||
(data, status) => {
|
||||
//success
|
||||
if (data.success.length > 0) {
|
||||
}
|
||||
location.reload();
|
||||
},
|
||||
(data, status) => {
|
||||
//error
|
||||
console.error(data, status);
|
||||
errorAlert();
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Suppression des justificatifs
|
||||
*/
|
||||
function deleteJustificatifs(justis) {
|
||||
const path = getUrl() + `/api/justificatif/delete`;
|
||||
async_post(
|
||||
path,
|
||||
justis,
|
||||
(data, status) => {
|
||||
//success
|
||||
location.reload();
|
||||
},
|
||||
(data, status) => {
|
||||
//error
|
||||
console.error(data, status);
|
||||
errorAlert();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const metriques = {
|
||||
"heure": "H.",
|
||||
"demi": "1/2 J.",
|
||||
|
|
|
@ -149,33 +149,7 @@ Calendrier de l'assiduité
|
|||
list-style-type: none;
|
||||
}
|
||||
|
||||
.color.present {
|
||||
background-color: var(--color-present) !important;
|
||||
}
|
||||
|
||||
.color.absent {
|
||||
background-color: var(--color-absent) !important;
|
||||
}
|
||||
|
||||
.color.absent.est_just {
|
||||
background-color: var(--color-absent-justi) !important;
|
||||
}
|
||||
|
||||
.color.retard {
|
||||
background-color: var(--color-retard) !important;
|
||||
}
|
||||
|
||||
.color.retard.est_just {
|
||||
background-color: var(--color-retard-justi) !important;
|
||||
}
|
||||
|
||||
.color.nonwork {
|
||||
background-color: var(--color-nonwork) !important;
|
||||
}
|
||||
|
||||
.color {
|
||||
background-color: var(--color-defaut) !important;
|
||||
}
|
||||
|
||||
|
||||
.pageContent {
|
||||
margin-top: 1vh;
|
||||
|
@ -208,38 +182,6 @@ Calendrier de l'assiduité
|
|||
justify-content: start;
|
||||
}
|
||||
|
||||
.color.est_just.sans_etat::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
background-color: var(--color-justi) !important;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.color.invalide::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
background-color: var(--color-justi-invalide) !important;
|
||||
}
|
||||
|
||||
.color.attente::before,
|
||||
.color.modifie::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
background: repeating-linear-gradient(to bottom,
|
||||
var(--color-justi-attente-stripe) 0px,
|
||||
var(--color-justi-attente-stripe) 4px,
|
||||
var(--color-justi-attente) 4px,
|
||||
var(--color-justi-attente) 7px) !important;
|
||||
}
|
||||
|
||||
.demo.invalide {
|
||||
background-color: var(--color-justi-invalide) !important;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ Assiduité de {{etud.nomprenom}}
|
|||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,101 +1,637 @@
|
|||
{#
|
||||
|
||||
- TODO : revoir le fonctionnement de cette page (trop lente / complexe)
|
||||
- Utiliser majoritairement du python
|
||||
#}
|
||||
|
||||
{% extends "sco_page.j2" %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
||||
|
||||
<style>
|
||||
.ui-timepicker-container,#ui-datepicker-div{
|
||||
z-index: 5 !important;
|
||||
}
|
||||
#new_periode,
|
||||
#actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
#actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#actions label{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#fix {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
justify-content: space-between;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
#fix>.box {
|
||||
border: 1px solid #444;
|
||||
border-radius: 0.5em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.timepicker {
|
||||
width: 5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#moduleimpl_select {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
#gtrcontent .pdp {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#gtrcontent[data-pdp="true"] .pdp {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#tableau-periode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: scroll;
|
||||
max-width: var(--sco-content-max-width);
|
||||
}
|
||||
|
||||
#tableau-periode .pdp {
|
||||
width: 5em;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #f9f9f9;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.cell, .header {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.header{
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
width: 256px;
|
||||
}
|
||||
.cell p{
|
||||
text-align: center;
|
||||
}
|
||||
.sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #f9f9f9;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.cell .assiduite-bubble {
|
||||
display: block;
|
||||
top: 0;
|
||||
z-index: 0;
|
||||
width: 100% !important;
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
.assi-btns {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pointer{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ligne{
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
{% endblock styles %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||
{% include "sco_timepicker.j2" %}
|
||||
|
||||
<script>
|
||||
|
||||
/**
|
||||
* Permet d'afficher ou non les photos des étudiants
|
||||
* @param {boolean} checked
|
||||
*/
|
||||
function afficherPDP(checked) {
|
||||
if (checked) {
|
||||
gtrcontent.setAttribute("data-pdp", "true");
|
||||
} else {
|
||||
gtrcontent.removeAttribute("data-pdp");
|
||||
}
|
||||
|
||||
// On sauvegarde le choix dans le localStorage
|
||||
localStorage.setItem("scodoc-signal_assiduites_diff-pdp", `${checked}`);
|
||||
pdp.checked = checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permet d'ajouter une nouvelle période au tableau
|
||||
* Par défaut la période est générèe avec les valeurs des inputs
|
||||
* Si une période est passée en paramètre, alors on utilise ses valeurs
|
||||
* @param {Object} period - La période à ajouter
|
||||
*/
|
||||
async function nouvellePeriode(period = null) {
|
||||
// On récupère l'id de la période
|
||||
let periodId;
|
||||
if (period) {
|
||||
periodId = period.periodId;
|
||||
} else {
|
||||
periodId = currentPeriodId++;
|
||||
}
|
||||
|
||||
// On récupère les valeurs des inputs
|
||||
let date = document.getElementById("date").value;
|
||||
let debut = document.getElementById("debut").value;
|
||||
let fin = document.getElementById("fin").value;
|
||||
let moduleimpl_id = document.getElementById("moduleimpl_select").value;
|
||||
const moduleimpl = await getModuleImpl({ moduleimpl_id: moduleimpl_id });
|
||||
|
||||
// Si une période est passée en paramètre, on utilise ses valeurs
|
||||
if (period) {
|
||||
date = period.date_debut.format("DD/MM/YYYY");
|
||||
debut = period.date_debut.format("HH:mm");
|
||||
fin = period.date_fin.format("HH:mm");
|
||||
moduleimpl_id = period.moduleimpl_id;
|
||||
}else{
|
||||
//Sinon on vérifie qu'on a bien des valeurs
|
||||
const text = document.createTextNode("Veuillez remplir tous les champs pour ajouter une plage.")
|
||||
if (date == "" || debut == "" || fin == "" || moduleimpl_id == "") {
|
||||
openAlertModal(
|
||||
"Erreur",
|
||||
text
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// On ajoute la nouvelle période au tableau
|
||||
let periodeDiv = document.createElement("div");
|
||||
periodeDiv.classList.add("cell", "header");
|
||||
periodeDiv.id = `periode-${periodId}`;
|
||||
|
||||
const periodP = document.createElement("p");
|
||||
periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`;
|
||||
|
||||
// On ajoute le moduleimpl
|
||||
const modP = document.createElement("p");
|
||||
modP.textContent = moduleimpl;
|
||||
|
||||
// On ajoute le bouton pour supprimer la période
|
||||
const close = document.createElement("button");
|
||||
close.textContent = "❌";
|
||||
close.addEventListener("click", () => {
|
||||
// On supprime toutes les cases du tableau correspondant à cette période
|
||||
document
|
||||
.querySelectorAll(
|
||||
`[data-periodeid="${periodeDiv.getAttribute("data-periodeid")}"]`
|
||||
)
|
||||
.forEach((e) => e.remove());
|
||||
// On supprime la période de la Map periodes
|
||||
periodes.delete(Number(periodeDiv.getAttribute("data-periodeid")));
|
||||
});
|
||||
//On ajoute les éléments au DOM
|
||||
periodeDiv.appendChild(periodP);
|
||||
periodeDiv.appendChild(modP);
|
||||
periodeDiv.appendChild(close);
|
||||
periodeDiv.setAttribute("data-periodeid", periodId);
|
||||
document.getElementById("tete-table").appendChild(periodeDiv);
|
||||
|
||||
// On récupère les étudiants (etudids)
|
||||
let etudids = [
|
||||
...document.querySelectorAll(".ligne[data-etudid]"),
|
||||
].map((e) => e.getAttribute("data-etudid"));
|
||||
|
||||
// On génère une date de début et de fin de la période
|
||||
const date_debut = new Date(
|
||||
$("#date").datepicker("getDate").format("YYYY-MM-DD") + "T" + debut
|
||||
);
|
||||
const date_fin = new Date(
|
||||
$("#date").datepicker("getDate").format("YYYY-MM-DD") + "T" + fin
|
||||
);
|
||||
date_debut.add(1, "seconds");
|
||||
|
||||
// Préparation de la requête
|
||||
const url =
|
||||
`../../api/assiduites/group/query?date_debut=${date_debut.toFakeIso()}` +
|
||||
`&date_fin=${date_fin.toFakeIso()}&etudids=${etudids.join(
|
||||
","
|
||||
)}&with_justifs`;
|
||||
|
||||
//Si la période n'existait pas, alors on l'ajoute à la Map
|
||||
if (!period) {
|
||||
periodes.set(periodId, {
|
||||
date_debut: date_debut.clone().add(-1, "seconds"),
|
||||
date_fin: date_fin,
|
||||
moduleimpl_id: moduleimpl_id,
|
||||
periodId: periodId,
|
||||
});
|
||||
}
|
||||
|
||||
// On récupère les incriptions au module
|
||||
const inscriptions = await getInscriptionModule(moduleimpl_id);
|
||||
|
||||
// On récupère les assiduités
|
||||
await fetch(url)
|
||||
// On convertit la réponse en JSON
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
// On traite les données
|
||||
.then((data) => {
|
||||
for (let etudid of etudids) {
|
||||
// On crée une case pour chaque étudiant
|
||||
let cell = document.createElement("div");
|
||||
cell.classList.add("cell");
|
||||
cell.setAttribute("data-etudid", etudid);
|
||||
cell.setAttribute("data-periodeid", periodId);
|
||||
cell.id = `cell-${etudid}-${periodId}`;
|
||||
document.querySelector(`.ligne[data-etudid="${etudid}"]`).appendChild(cell);
|
||||
|
||||
//Vérification inscription au module
|
||||
// Si l'étudiant n'est pas inscrit, on le notifie et on passe à l'étudiant suivant
|
||||
const inscrit =
|
||||
inscriptions == null ? true : inscriptions.find((e) => e == etudid);
|
||||
if (!inscrit) {
|
||||
cell.textContent = "Non inscrit";
|
||||
cell.classList.add("non-inscrit");
|
||||
continue;
|
||||
}
|
||||
|
||||
//Gestion des assiduités déjà existantes
|
||||
const assiduites = data[etudid];
|
||||
// Si l'étudiant n'a pas d'assiduité, on crée les boutons assiduité
|
||||
if (assiduites.length == 0) {
|
||||
|
||||
const assi_btns = document.createElement('div');
|
||||
assi_btns.classList.add('assi-btns');
|
||||
|
||||
["present", "retard", "absent"].forEach((value) => {
|
||||
const cbox = document.createElement("input");
|
||||
cbox.type = "checkbox";
|
||||
cbox.value = value;
|
||||
cbox.name = `rbtn_${etudid}_${periodId}`;
|
||||
cbox.classList.add("rbtn", value);
|
||||
|
||||
// Event pour être sur qu'un seul bouton est coché à la fois
|
||||
cbox.addEventListener("click", (event) => {
|
||||
const parent = event.target.parentElement;
|
||||
parent.querySelectorAll(".rbtn").forEach((ele) => {
|
||||
if (ele.value != value) {
|
||||
ele.checked = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
// Si une valeur par défaut est donnée alors on l'applique
|
||||
cbox.checked = etatDef.value == value;
|
||||
|
||||
assi_btns.appendChild(cbox);
|
||||
});
|
||||
cell.appendChild(assi_btns);
|
||||
} else {
|
||||
// Si une (ou plus) assiduité sont trouvée pour la période
|
||||
// alors on affiche les informations de la première assiduité
|
||||
setupAssiduiteBubble(cell, assiduites[0]);
|
||||
}
|
||||
}
|
||||
})
|
||||
//Si jamais la requête échoue, on affiche un message d'erreur dans la console
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
|
||||
document.getElementById("tableau-periode").classList.remove("hidden");
|
||||
}
|
||||
/**
|
||||
* Permet de récupérer la saisie puis créer les assiduités grâce à l'api
|
||||
*/
|
||||
function sauvegarderAssiduites() {
|
||||
// Initialisation de la liste des assiduités à créer
|
||||
let assiduitesData = [];
|
||||
// Pour chaque période, on récupère les assiduités saisies
|
||||
for (let [periodeId, periode] of periodes.entries()) {
|
||||
// On prend chaque cellule correspondant à la période
|
||||
const cells = document.querySelectorAll(
|
||||
`.cell[data-periodeid="${periodeId}"][data-etudid]`
|
||||
);
|
||||
// Pour chaque cellule, on récupère l'état de l'assiduité
|
||||
cells.forEach((cell) => {
|
||||
const etudid = cell.getAttribute("data-etudid");
|
||||
const etat = cell.querySelector(".rbtn:checked")?.value;
|
||||
// Il est possible que l'état soit null
|
||||
// - Cas où l'étudiant n'est pas inscrit
|
||||
// - Cas où l'étudiant avait déjà une assiduité
|
||||
if (etat) {
|
||||
// On génère un objet "assiduité"
|
||||
/*
|
||||
{
|
||||
etudid: <int>,
|
||||
etat: <string>,
|
||||
date_debut: <string>,
|
||||
date_fin: <string>,
|
||||
moduleimpl_id: <int>,
|
||||
periodId: <int>
|
||||
}
|
||||
*/
|
||||
assiduitesData.push({
|
||||
etudid: etudid,
|
||||
etat: etat,
|
||||
...periode,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Une fois les assiduités générées, on les envoie à l'api
|
||||
async_post(
|
||||
"../../api/assiduites/create",
|
||||
assiduitesData,
|
||||
// Si la requête passe
|
||||
async (data) => {
|
||||
// On supprime toutes les cases du tableau pour le mettre à jour
|
||||
document.querySelectorAll("[data-periodeid]").forEach((e)=>e.remove())
|
||||
|
||||
// On recrée les périodes
|
||||
// (cela permet de redemander les assiduités, donc mettre à jour les cases)
|
||||
for (let periode of periodes.values()) {
|
||||
await nouvellePeriode(periode);
|
||||
}
|
||||
|
||||
// Si il y n'a pas d'erreur, on affiche un message de succès
|
||||
if (data.errors.length == 0) {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = "Les assiduités ont bien été sauvegardées.";
|
||||
openAlertModal(
|
||||
"Sauvegarde des assiduités",
|
||||
span,
|
||||
null,
|
||||
"var(--color-present)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Si il y a des erreurs, on les affiche
|
||||
if (data.errors.length > 0) {
|
||||
// On crée une map pour regrouper les erreurs par période
|
||||
const erreurs = new Map();
|
||||
data.errors.forEach((err) => {
|
||||
// Pour chaque période on créer une liste d'erreurs
|
||||
// format : [message, etudid]
|
||||
const assi = assiduitesData[err.indice];
|
||||
const msg = err.message;
|
||||
const periodErrors = erreurs.get(assi.periodId) || [];
|
||||
|
||||
// Récupération du nom de l'étudiant
|
||||
const etud = document.querySelector(
|
||||
`#head-${assi.etudid} span`
|
||||
).textContent;
|
||||
periodErrors.push([`Erreur pour ${etud} : ${msg}`, assi.etudid]);
|
||||
erreurs.set(assi.periodId, periodErrors);
|
||||
});
|
||||
|
||||
// Création du DOM
|
||||
/*
|
||||
<ul>
|
||||
<li>
|
||||
Période du ... de ... à ...
|
||||
<ul>
|
||||
<li>Erreur pour ...</li>
|
||||
<li>Erreur pour ...</li>
|
||||
</ul>
|
||||
/li>
|
||||
</ul>
|
||||
*/
|
||||
|
||||
const ul = document.createElement("ul");
|
||||
//Pour chaque période on créer un titre "periode du ... de ... à ..."
|
||||
for (let [periodeId, periodErrors] of erreurs.entries()) {
|
||||
const period = periodes.get(periodeId);
|
||||
const li = document.createElement("li");
|
||||
// On affiche la période
|
||||
li.textContent = `Plage du ${period.date_debut.format(
|
||||
"DD/MM/YYYY HH:mm"
|
||||
)} à ${period.date_fin.format("HH:mm")}`;
|
||||
|
||||
// Nous emmène à la période lorsqu'on clique dessus
|
||||
li.addEventListener("click", () => {
|
||||
location.href = `#periode-${periodeId}`;
|
||||
});
|
||||
li.classList.add("pointer");
|
||||
|
||||
// Pour chaque erreur, on créer un élément de liste
|
||||
const ul2 = document.createElement("ul");
|
||||
periodErrors.forEach((err) => {
|
||||
const li2 = document.createElement("li");
|
||||
li2.textContent = err[0];
|
||||
li2.classList.add("pointer");
|
||||
|
||||
// Nous emmène à la case de l'étudiant lorsqu'on clique dessus
|
||||
li2.addEventListener("click", () => {
|
||||
location.href = `#cell-${err[1]}-${periodeId}`;
|
||||
});
|
||||
ul2.appendChild(li2);
|
||||
});
|
||||
li.appendChild(ul2);
|
||||
ul.appendChild(li);
|
||||
}
|
||||
|
||||
openAlertModal(
|
||||
"Erreurs lors de la sauvegarde des assiduités",
|
||||
ul,
|
||||
"Les autres assiduités ont bien été sauvegardées."
|
||||
);
|
||||
}
|
||||
},
|
||||
(e) => {
|
||||
console.error("Erreur lors de la création des assiduités", e);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Mis en place des variables globales
|
||||
let currentPeriodId = 0;
|
||||
const periodes = new Map();
|
||||
const moduleimpls = new Map();
|
||||
const inscriptionsModules = new Map();
|
||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||
|
||||
// Vérification du forçage de module
|
||||
window.forceModule = "{{ forcer_module }}" == "True";
|
||||
if (window.forceModule) {
|
||||
if (moduleimpl_select.value == "") {
|
||||
document.getElementById("forcemodule").style.display = "block";
|
||||
add_periode.disabled = true;
|
||||
}
|
||||
// Désactivation du bouton d'ajout de période si aucun module n'est sélectionné
|
||||
// et affichage du message de forçage de module
|
||||
moduleimpl_select?.addEventListener("change", (e) => {
|
||||
if (e.target.value != "") {
|
||||
document.getElementById("forcemodule").style.display = "none";
|
||||
add_periode.disabled = false;
|
||||
} else {
|
||||
document.getElementById("forcemodule").style.display = "block";
|
||||
add_periode.disabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Fonction exécutée au lancement de la page
|
||||
* - On affiche ou non les photos des étudiants
|
||||
* - On vérifie si la date est un jour travaillé
|
||||
*/
|
||||
async function main() {
|
||||
const checked = localStorage.getItem("scodoc-signal_assiduites_diff-pdp") == "true";
|
||||
afficherPDP(checked);
|
||||
$("#date").on("change", async function (d) {
|
||||
// On vérifie si la date est un jour travaillé
|
||||
dateCouranteEstTravaillee();
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock scripts %}
|
||||
|
||||
{% block title %}
|
||||
{{title}}
|
||||
{{title}}
|
||||
{% endblock title %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
{% include "assiduites/widgets/prompt.j2" %}
|
||||
{% include "assiduites/widgets/conflict.j2" %}
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||
|
||||
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
|
||||
|
||||
<div class="ue_warning">Attention, cette page utilise des couleurs et conventions différentes
|
||||
de celles des autres pages ScoDoc: elle sera prochainement modifée, merci de votre patience.
|
||||
</div>
|
||||
|
||||
<h3>{{sem | safe }}</h3>
|
||||
<div id="fix">
|
||||
<!-- Nouvelle Plage
|
||||
Permet de créer une nouvelle ligne pour une nouvelle Plage
|
||||
(
|
||||
Jour, -> datepicker
|
||||
Heure de début, -> timepicker
|
||||
Heure de fin -> timepicker
|
||||
ModuleImplId -> select (liste des modules tout semestre confondu)
|
||||
)
|
||||
--->
|
||||
|
||||
{{diff | safe}}
|
||||
<div id="new_periode" class="box">
|
||||
<label for="date">
|
||||
Date :
|
||||
<input type="text" name="date" id="date" class="datepicker">
|
||||
</label>
|
||||
<label for="debut">
|
||||
Heure de début :
|
||||
<input type="text" name="debut" id="debut" class="timepicker">
|
||||
</label>
|
||||
<label for="fin">
|
||||
Heure de fin :
|
||||
<input type="text" name="fin" id="fin" class="timepicker">
|
||||
</label>
|
||||
|
||||
<div class="help">
|
||||
<h3>Explication de la saisie différée</h3>
|
||||
<p>Si la colonne n'est pas valide elle sera affichée en rouge, passez le curseur sur la colonne pour afficher
|
||||
le message d'erreur</p>
|
||||
<p>Sélectionner la date de début de la colonne mettra automatiquement la date de fin à la durée d'une séance
|
||||
(préférence de département)</p>
|
||||
<p>Modifier le module alors que des informations d'assiduité sont déjà enregistrées pour la période changera leur
|
||||
module.</p>
|
||||
<p>Il y a 4 boutons sur la colonne permettant d'enregistrer l'information pour tous les étudiants</p>
|
||||
<p>Le dernier des boutons retire l'information présente.</p>
|
||||
<p>Vous pouvez ajouter des colonnes en appuyant sur le bouton + </p>
|
||||
<p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne.
|
||||
</p>
|
||||
<label for="moduleimpl_select">
|
||||
<div id="forcemodule" style="display: none; margin:10px 0px;">
|
||||
Vous devez spécifier le module ! (voir réglage préférence du semestre)
|
||||
</div>
|
||||
Module :
|
||||
{{moduleimpl_select | safe}}
|
||||
</label>
|
||||
|
||||
<button id="add_periode" onclick="nouvellePeriode()">Ajouter une plage</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Boutons d'actions
|
||||
- Sauvegarder
|
||||
- Afficher la photo de profil
|
||||
- Assiduité par défaut (aucune, present, retard, absent)
|
||||
--->
|
||||
<br>
|
||||
<div id="actions" class="flex">
|
||||
<button id="save" onclick="sauvegarderAssiduites()">ENREGISTRER</button>
|
||||
<label for="pdp">
|
||||
Photo de profil :
|
||||
<input type="checkbox" name="pdp" id="pdp" checked onclick="afficherPDP(this.checked)">
|
||||
</label>
|
||||
|
||||
<label for="etatDef">
|
||||
Intialiser les étudiants comme :
|
||||
<select name="etatDef" id="etatDef">
|
||||
<option value="">-</option>
|
||||
<option value="present">présents</option>
|
||||
<option value="retard">en retard</option>
|
||||
<option value="absent">absents</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Tableau à double entrée
|
||||
Colonne : Etudiants (Header = Nom, Prénom, Photo (si actif))
|
||||
Ligne : Période (Header = Jour, Heure de début, Heure de fin, ModuleImplId)
|
||||
Contenu :
|
||||
- bouton assiduité (présent, retard, absent)
|
||||
- Bouton conflit si conflit de période
|
||||
--->
|
||||
|
||||
<div id="tableau-periode" class="grid-table">
|
||||
<!-- Première ligne : Plages -->
|
||||
<div class="ligne" id="tete-table">
|
||||
<div class="cell header sticky">Étudiants</div>
|
||||
{# <div class="cell header" periode-id="X">Plage X</div> #}
|
||||
</div>
|
||||
{# ... #}
|
||||
|
||||
<hr class="hidden" id="separator">
|
||||
|
||||
{% for etud in etudiants %}
|
||||
<div class="ligne" data-etudid="{{etud.etudid}}">
|
||||
<div class="cell etudinfo sticky" id="head-{{etud.etudid}}">
|
||||
<img src="../../api/etudiant/etudid/{{etud.etudid}}/photo?size=small" alt="{{etud.nomprenom}}" class="pdp">
|
||||
<span>{{ etud.nomprenom }}</span>
|
||||
</div>
|
||||
{# <div class="cell" periode-id="X">Assiduité Plage 1</div> #}
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
{% endblock app_content %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
<script>
|
||||
const etudsDefDem = {{ defdem | safe }}
|
||||
|
||||
const timeMorning = "{{ timeMorning | safe}}";
|
||||
const timeNoon = "{{ timeNoon | safe}}";
|
||||
const timeEvening = "{{ timeEvening | safe}}";
|
||||
|
||||
const defaultDates = {{ defaultDates | safe }}
|
||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
[...document.querySelectorAll('.tr[etudid]')].forEach((a) => {
|
||||
try {
|
||||
if (a.getAttribute("etudid") in etudsDefDem) {
|
||||
defdem = etudsDefDem[a.getAttribute("etudid")] == "D" ? "dem" : "def";
|
||||
a.classList.add(defdem);
|
||||
}
|
||||
} catch (_) { }
|
||||
});
|
||||
|
||||
if (defaultDates != null) {
|
||||
defaultDates.forEach((dateString) => {
|
||||
|
||||
d = new Date(dateString);
|
||||
|
||||
if (isNonWorkDay(d, nonWorkDays)) return;
|
||||
|
||||
matin = `${dateString}T${timeMorning}`;
|
||||
midi = `${dateString}T${timeNoon}`;
|
||||
soir = `${dateString}T${timeEvening}`;
|
||||
|
||||
console.log(matin, midi, soir)
|
||||
|
||||
createColumn(matin, midi);
|
||||
createColumn(midi, soir);
|
||||
});
|
||||
|
||||
updateAllCol();
|
||||
} else {
|
||||
createColumn();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock scripts %}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
{% include "sco_timepicker.j2" %}
|
||||
|
||||
<script>
|
||||
|
||||
|
@ -20,33 +21,21 @@
|
|||
function getPeriodValues() {
|
||||
return [0, 23]
|
||||
}
|
||||
{% else %}
|
||||
setupTimeLine(()=>{creerTousLesEtudiants(etuds)})
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||
|
||||
const readOnly = {{ readonly }};
|
||||
|
||||
|
||||
setupDate();
|
||||
updateDate();
|
||||
if (!readOnly) {
|
||||
setupTimeLine(() => {
|
||||
generateAllEtudRow();
|
||||
});
|
||||
}
|
||||
|
||||
window.forceModule = "{{ forcer_module }}"
|
||||
window.forceModule = window.forceModule == "True" ? true : false
|
||||
window.forceModule = "{{ forcer_module }}" == "True"
|
||||
|
||||
const etudsDefDem = {{ defdem | safe }}
|
||||
|
||||
const select = document.getElementById("moduleimpl_select");
|
||||
|
||||
select?.addEventListener('change', (e) => {
|
||||
generateAllEtudRow();
|
||||
});
|
||||
|
||||
if (window.forceModule) {
|
||||
const btn = document.getElementById("validate_selectors");
|
||||
|
||||
|
@ -63,12 +52,51 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("pdp").addEventListener("change", (e) => {
|
||||
creerTousLesEtudiants(etuds);
|
||||
});
|
||||
|
||||
$('#date').on('change', async function(d) {
|
||||
// On vérifie si la date est un jour travaillé
|
||||
dateCouranteEstTravaillee();
|
||||
|
||||
|
||||
|
||||
await recupAssiduites(etuds, $("#date").datepicker("getDate"));
|
||||
creerTousLesEtudiants(etuds);
|
||||
});
|
||||
|
||||
$("#moduleimpl_select").on("change", ()=>{
|
||||
creerTousLesEtudiants(etuds);
|
||||
});
|
||||
|
||||
$("#group_ids_sel").on("change", ()=>{
|
||||
main();
|
||||
})
|
||||
|
||||
const moduleimpls = {};
|
||||
const inscriptionsModules = new Map();
|
||||
let etuds = new Map();
|
||||
|
||||
async function main(){
|
||||
dateCouranteEstTravaillee();
|
||||
etuds = await recupEtuds($('#group_ids_sel').val());
|
||||
if (etuds.size != 0){
|
||||
await recupAssiduites(etuds, $("#date").datepicker("getDate"));
|
||||
}
|
||||
creerTousLesEtudiants(etuds);
|
||||
|
||||
}
|
||||
|
||||
setTimeout(main, 0);
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock scripts %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap-theme.min.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
|
@ -77,14 +105,8 @@
|
|||
|
||||
|
||||
{% block app_content %}
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
|
||||
{{ minitimeline|safe }}
|
||||
<style>
|
||||
#moduleimpl_select {
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
<section id="content">
|
||||
|
||||
<div class="no-display">
|
||||
|
@ -104,15 +126,16 @@
|
|||
<fieldset class="selectors">
|
||||
<div class="infos">
|
||||
<div class="infos-button">Groupes : {{grp|safe}}</div>
|
||||
<div class="infos-button" style="margin-left: 24px;">Date : <span style="margin-left: 8px;"
|
||||
id="datestr"></span>
|
||||
<input type="text" name="tl_date" id="tl_date" value="{{ date }}" onchange="updateDate()">
|
||||
<div>
|
||||
<input type="text" name="date" id="date" class="datepicker" value="{{date}}">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div style="display: {{'none' if readonly == 'true' else 'block'}};">
|
||||
{{timeline|safe}}
|
||||
</div>
|
||||
|
||||
{% if readonly == "false" %}
|
||||
{{timeline|safe}}
|
||||
|
||||
<div style="margin: 1vh 0;">
|
||||
<div id="forcemodule" style="display: none; margin:10px 0px;">
|
||||
|
@ -123,46 +146,40 @@
|
|||
{% else %}
|
||||
{% endif %}
|
||||
|
||||
{% if readonly == "true" %}
|
||||
<button id="validate_selectors" onclick="validateSelectors(this)">
|
||||
Voir l'assiduité
|
||||
</button>
|
||||
{% else %}
|
||||
<button id="validate_selectors" onclick="validateSelectors(this)">
|
||||
Faire la saisie
|
||||
</button>
|
||||
<div>
|
||||
<label for="pdp">
|
||||
<span>Afficher les photos</span>
|
||||
<input type="checkbox" name="pdp" id="pdp">
|
||||
</label>
|
||||
</div>
|
||||
{% if readonly == "false" %}
|
||||
<div class="mass-selection">
|
||||
<span>Mettre tout le monde :</span>
|
||||
<fieldset class="btns_field mass">
|
||||
<input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present"
|
||||
class="rbtn present" onclick="mettreToutLeMonde('present', this)" title="Present">
|
||||
<input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard"
|
||||
class="rbtn retard" onclick="mettreToutLeMonde('retard', this)" title="Retard">
|
||||
<input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent"
|
||||
class="rbtn absent" onclick="mettreToutLeMonde('absent', this)" title="Absent">
|
||||
<input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun"
|
||||
class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Supprimer">
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<div class="etud_holder">
|
||||
<p class="placeholder">
|
||||
</p>
|
||||
</div>
|
||||
<div class="legende">
|
||||
<h3>Explication diverses</h3>
|
||||
<p>
|
||||
Si la période indiquée par la timeline provoque un conflit d'assiduité pour un étudiant sa ligne deviendra
|
||||
rouge.
|
||||
<br>
|
||||
Dans ce cas il faut résoudre manuellement le conflit : cliquez sur un des boutons d'assiduités pour ouvrir
|
||||
le
|
||||
résolveur de conflit.
|
||||
<br>
|
||||
Correspondance des couleurs :
|
||||
</p>
|
||||
<ul>
|
||||
{% include "assiduites/widgets/legende_couleur.j2" %}
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Ajout d'un conteneur pour le loader -->
|
||||
<div class="loader-container" id="loaderContainer">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
{% include "assiduites/widgets/prompt.j2" %}
|
||||
{% include "assiduites/widgets/conflict.j2" %}
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
{% endblock app_content %}
|
||||
{% endblock app_content %}
|
||||
|
|
|
@ -1,118 +1,100 @@
|
|||
<script>
|
||||
/**
|
||||
* Transformation d'une date de début en position sur la timeline
|
||||
* @param {String} start
|
||||
* @returns {String} un déplacement par rapport à la gauche en %
|
||||
*/
|
||||
function getLeftPosition(start) {
|
||||
const startTime = new Date(start);
|
||||
const startMins = (startTime.getHours() - t_start) * 60 + startTime.getMinutes();
|
||||
return (startMins / (t_end * 60 - t_start * 60)) * 100 + "%";
|
||||
}
|
||||
/**
|
||||
* Ajustement de l'espacement vertical entre les assiduités superposées
|
||||
* @param {HTMLElement} container le conteneur des assiduités
|
||||
* @param {String} start la date début de l'assiduité à placer
|
||||
* @param {String} end la date de fin de l'assiduité à placer
|
||||
* @returns {String} La position en px
|
||||
*/
|
||||
function getTopPosition(container, start, end) {
|
||||
const overlaps = (a, b) => {
|
||||
return a.start < b.end && a.end > b.start;
|
||||
};
|
||||
/**
|
||||
* Transformation d'une date de début en position sur la timeline
|
||||
* @param {String} start
|
||||
* @returns {String} un déplacement par rapport à la gauche en %
|
||||
*/
|
||||
function getLeftPosition(start) {
|
||||
const startTime = new Date(start);
|
||||
const startMins =
|
||||
(startTime.getHours() - t_start) * 60 + startTime.getMinutes();
|
||||
return (startMins / (t_end * 60 - t_start * 60)) * 100 + "%";
|
||||
}
|
||||
/**
|
||||
* Ajustement de l'espacement vertical entre les assiduités superposées
|
||||
* @param {HTMLElement} container le conteneur des assiduités
|
||||
* @param {String} start la date début de l'assiduité à placer
|
||||
* @param {String} end la date de fin de l'assiduité à placer
|
||||
* @returns {String} La position en px
|
||||
*/
|
||||
function getTopPosition(container, start, end) {
|
||||
const overlaps = (a, b) => {
|
||||
return a.start < b.end && a.end > b.start;
|
||||
};
|
||||
|
||||
const startTime = new Date(start);
|
||||
const endTime = new Date(end);
|
||||
const assiduiteDuration = { start: startTime, end: endTime };
|
||||
const startTime = new Date(start);
|
||||
const endTime = new Date(end);
|
||||
const assiduiteDuration = { start: startTime, end: endTime };
|
||||
|
||||
let position = 0;
|
||||
let hasOverlap = true;
|
||||
let position = 0;
|
||||
let hasOverlap = true;
|
||||
|
||||
while (hasOverlap) {
|
||||
hasOverlap = false;
|
||||
Array.from(container.children).some((el) => {
|
||||
const elStart = new Date(el.getAttribute("data-start"));
|
||||
const elEnd = new Date(el.getAttribute("data-end"));
|
||||
const elDuration = { start: elStart, end: elEnd };
|
||||
while (hasOverlap) {
|
||||
hasOverlap = false;
|
||||
Array.from(container.children).some((el) => {
|
||||
const elStart = new Date(el.getAttribute("data-start"));
|
||||
const elEnd = new Date(el.getAttribute("data-end"));
|
||||
const elDuration = { start: elStart, end: elEnd };
|
||||
|
||||
if (overlaps(assiduiteDuration, elDuration)) {
|
||||
position += 25; // Pour ajuster l'espacement vertical entre les assiduités superposées
|
||||
hasOverlap = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return position + "px";
|
||||
if (overlaps(assiduiteDuration, elDuration)) {
|
||||
position += 25; // Pour ajuster l'espacement vertical entre les assiduités superposées
|
||||
hasOverlap = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return position + "px";
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule de la largeur de l'assiduité sur la timeline
|
||||
* @param {String} start date iso de début
|
||||
* @param {String} end date iso de fin
|
||||
* @returns {String} la taille en %
|
||||
*/
|
||||
function getWidth(start, end) {
|
||||
const startTime = new Date(start);
|
||||
const endTime = new Date(end);
|
||||
|
||||
const duration = (endTime - startTime) / 1000 / 60;
|
||||
|
||||
const percent = (duration / (t_end * 60 - t_start * 60)) * 100;
|
||||
return percent + "%";
|
||||
}
|
||||
|
||||
function formatDateModal(date) {
|
||||
return new Date(Date.removeUTC(date)).format("DD/MM/Y HH:mm");
|
||||
}
|
||||
|
||||
class ConflitResolver {
|
||||
constructor(assiduitesList, conflictPeriod, interval) {
|
||||
this.list = assiduitesList;
|
||||
this.conflictPeriod = conflictPeriod;
|
||||
this.interval = interval;
|
||||
this.selectedAssiduite = null;
|
||||
|
||||
this.element = undefined;
|
||||
|
||||
this.callbacks = {
|
||||
delete: () => {},
|
||||
split: () => {},
|
||||
edit: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
refresh(assiduitesList, periode) {
|
||||
this.list = assiduitesList;
|
||||
if (periode) {
|
||||
this.conflictPeriod = periode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformation d'un état en couleur
|
||||
* @param {String} state l'état
|
||||
* @returns {String} la couleur correspondant à l'état
|
||||
*/
|
||||
function getColor(state) {
|
||||
switch (state) {
|
||||
case "PRESENT":
|
||||
return "var(--color-present)";
|
||||
case "ABSENT":
|
||||
return "var(--color-absent)";
|
||||
case "RETARD":
|
||||
return "var(--color-retard)";
|
||||
default:
|
||||
return "var(--color-defaut-dark)";
|
||||
}
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule de la largeur de l'assiduité sur la timeline
|
||||
* @param {String} start date iso de début
|
||||
* @param {String} end date iso de fin
|
||||
* @returns {String} la taille en %
|
||||
*/
|
||||
function getWidth(start, end) {
|
||||
const startTime = new Date(start);
|
||||
const endTime = new Date(end);
|
||||
|
||||
const duration = (endTime - startTime) / 1000 / 60;
|
||||
|
||||
const percent = (duration / (t_end * 60 - t_start * 60)) * 100
|
||||
return percent + "%";
|
||||
}
|
||||
|
||||
class ConflitResolver {
|
||||
constructor(assiduitesList, conflictPeriod, interval) {
|
||||
this.list = assiduitesList;
|
||||
this.conflictPeriod = conflictPeriod;
|
||||
this.interval = interval;
|
||||
this.selectedAssiduite = null;
|
||||
|
||||
this.element = undefined;
|
||||
|
||||
this.callbacks = {
|
||||
delete: () => { },
|
||||
split: () => { },
|
||||
edit: () => { },
|
||||
}
|
||||
}
|
||||
|
||||
refresh(assiduitesList, periode) {
|
||||
this.list = assiduitesList;
|
||||
if (periode) {
|
||||
this.conflictPeriod = periode;
|
||||
}
|
||||
|
||||
this.render()
|
||||
|
||||
}
|
||||
|
||||
selectAssiduite() {
|
||||
|
||||
}
|
||||
|
||||
open() {
|
||||
const html = `
|
||||
<div id="myModal" class="modal">
|
||||
open() {
|
||||
const html = `
|
||||
<div id="myModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</span>
|
||||
<h2>Veuillez régler le conflit pour poursuivre</h2>
|
||||
|
@ -122,201 +104,264 @@
|
|||
<div class="assiduites-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<div class="modal-buttons">
|
||||
<button id="finish" class="btnPrompt">Quitter</button>
|
||||
<button id="delete" class="btnPrompt" disabled>Supprimer</button>
|
||||
<button id="split" class="btnPrompt" disabled>Séparer</button>
|
||||
<button id="edit" class="btnPrompt" disabled>Modifier l'état</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-assiduite-content">
|
||||
<h2>Information de l'assiduité sélectionnée</h2>
|
||||
<div class="infos">
|
||||
<p>Assiduite id : <span id="modal-assiduite-id">A</span></p>
|
||||
<p>Etat : <span id="modal-assiduite-etat">B</span></p>
|
||||
<p>Date de début : <span id="modal-assiduite-deb">C</span></p>
|
||||
<p>Date de fin: <span id="modal-assiduite-fin">D</span></p>
|
||||
<p>Module : <span id="modal-assiduite-module">E</span></p>
|
||||
<p><span id="modal-assiduite-user">F</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML("afterbegin", html);
|
||||
this.element = document.getElementById('myModal');
|
||||
this.deleteBtn = document.querySelector('#myModal #delete');
|
||||
this.editBtn = document.querySelector('#myModal #edit');
|
||||
this.splitBtn = document.querySelector('#myModal #split');
|
||||
this.deleteBtn.addEventListener('click', () => { this.deleteAssiduiteModal() });
|
||||
this.editBtn.addEventListener('click', () => { this.editAssiduiteModal() });
|
||||
this.splitBtn.addEventListener('click', () => { this.splitAssiduiteModal() });
|
||||
document.querySelector("#myModal #finish").addEventListener('click', () => { this.close() })
|
||||
document.body.insertAdjacentHTML("afterbegin", html);
|
||||
this.element = document.getElementById("myModal");
|
||||
document.querySelector("#myModal #finish").addEventListener("click", () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
document.querySelector('#myModal .close').addEventListener('click', () => { this.close() })
|
||||
document.querySelector("#myModal .close").addEventListener("click", () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
// fermeture du modal en appuyant sur echap
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.close()
|
||||
}
|
||||
}, { once: true })
|
||||
|
||||
this.render()
|
||||
// fermeture du modal en appuyant sur echap
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
(e) => {
|
||||
if (e.key === "Escape") {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.element) {
|
||||
this.element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.element) {
|
||||
this.element.remove()
|
||||
/**
|
||||
* Génération du modal
|
||||
*/
|
||||
render() {
|
||||
const timeLabels = document.querySelector(".time-labels");
|
||||
const assiduitesContainer = document.querySelector(".assiduites-container");
|
||||
|
||||
timeLabels.innerHTML = "";
|
||||
assiduitesContainer.innerHTML = '<div class="assiduite-special"></div>';
|
||||
|
||||
// Ajout des labels d'heure sur la frise chronologique
|
||||
for (let i = t_start; i <= t_end; i++) {
|
||||
const timeLabel = document.createElement("div");
|
||||
timeLabel.className = "time-label";
|
||||
timeLabel.textContent = numberToTime(i);
|
||||
timeLabels.appendChild(timeLabel);
|
||||
}
|
||||
|
||||
//Placement de la période conflictuelle sur la timeline
|
||||
const specialAssiduiteEl = document.querySelector(".assiduite-special");
|
||||
|
||||
specialAssiduiteEl.style.width = getWidth(
|
||||
this.conflictPeriod.deb,
|
||||
this.conflictPeriod.fin
|
||||
);
|
||||
specialAssiduiteEl.style.left = getLeftPosition(this.conflictPeriod.deb);
|
||||
specialAssiduiteEl.style.top = "0";
|
||||
specialAssiduiteEl.style.zIndex = "0"; // Place l'assiduité spéciale en arrière-plan
|
||||
assiduitesContainer.appendChild(specialAssiduiteEl);
|
||||
|
||||
//Placement des assiduités sur la timeline
|
||||
this.list.forEach((assiduite) => {
|
||||
const period = {
|
||||
deb: new Date(assiduite.date_debut),
|
||||
fin: new Date(assiduite.date_fin),
|
||||
};
|
||||
if (!hasTimeConflict(period, this.interval)) {
|
||||
return;
|
||||
}
|
||||
const el = document.createElement("div");
|
||||
el.classList.add("assiduite", "color", assiduite.etat.toLowerCase());
|
||||
|
||||
el.style.width = getWidth(assiduite.date_debut, assiduite.date_fin);
|
||||
el.style.left = getLeftPosition(assiduite.date_debut);
|
||||
el.style.top = "10px";
|
||||
el.setAttribute("data-id", assiduite.assiduite_id);
|
||||
el.addEventListener("click", () => {});
|
||||
|
||||
// Génération des boutons d'action (supprimer, éditer, diviser)
|
||||
const actionButtons = document.createElement("div");
|
||||
actionButtons.className = "action-buttons";
|
||||
const deleteButton = document.createElement("button");
|
||||
deleteButton.textContent = "🗑️";
|
||||
deleteButton.addEventListener("click", () => {
|
||||
this.supprimerAssiduite(assiduite);
|
||||
});
|
||||
const editButton = document.createElement("button");
|
||||
editButton.textContent = "📝";
|
||||
editButton.addEventListener("click", () => {
|
||||
this.editerAssiduite(assiduite);
|
||||
});
|
||||
const splitButton = document.createElement("button");
|
||||
splitButton.textContent = "✂️";
|
||||
splitButton.addEventListener("click", () => {
|
||||
this.spliterAssiduite(assiduite);
|
||||
});
|
||||
|
||||
actionButtons.appendChild(editButton);
|
||||
actionButtons.appendChild(splitButton);
|
||||
actionButtons.appendChild(deleteButton);
|
||||
el.appendChild(actionButtons);
|
||||
|
||||
setupAssiduiteBubble(el, assiduite);
|
||||
assiduitesContainer.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
supprimerAssiduite(assiduite) {
|
||||
const html = `
|
||||
<p>Êtes-vous sûr de vouloir supprimer cette assiduité ?</p>
|
||||
`;
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
|
||||
openPromptModal(
|
||||
"Suppression de l'assiduité",
|
||||
div,
|
||||
async (closePromptModal) => {
|
||||
await async_post(
|
||||
`../../api/assiduite/delete`,
|
||||
[assiduite.assiduite_id],
|
||||
async (data) => {
|
||||
if (data.success.length > 0) {
|
||||
const etud = etuds.get(Number(assiduite.etudid));
|
||||
await MiseAJourLigneEtud(etud);
|
||||
this.refresh(etud.assiduites, this.conflictPeriod);
|
||||
closePromptModal();
|
||||
} else {
|
||||
console.error(data.errors["0"].message);
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error(
|
||||
"Erreur lors de la suppression de l'assiduité",
|
||||
error
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
() => {},
|
||||
"var(--color-error)"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sélection d'une assiduité sur la timeline
|
||||
* @param {Assiduité} assiduite l'assiduité sélectionnée
|
||||
*/
|
||||
selectAssiduite(assiduite) {
|
||||
// Désélectionner l'assiduité précédemment sélectionnée
|
||||
if (this.selectedAssiduite) {
|
||||
const prevSelectedEl = document.querySelector(
|
||||
`.assiduite[data-id="${this.selectedAssiduite.assiduite_id}"]`
|
||||
);
|
||||
if (prevSelectedEl) {
|
||||
prevSelectedEl.classList.remove("selected");
|
||||
}
|
||||
editerAssiduite(assiduite) {
|
||||
// Select pour choisir l'état de l'assiduité
|
||||
|
||||
const html = `
|
||||
<select id="etat" name="etat">
|
||||
<option disabled>Choisir un état</option>
|
||||
<option value="present">Présent</option>
|
||||
<option value="absent">Absent</option>
|
||||
<option value="retard">Retard</option>
|
||||
</select>
|
||||
`;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
div.style.display = "flex";
|
||||
div.style.justifyContent = "center";
|
||||
|
||||
openPromptModal(
|
||||
"Modifier l'état de l'assiduité",
|
||||
div,
|
||||
async (closePromptModal) => {
|
||||
const etatAssi = etat.value;
|
||||
if (!etat) return true;
|
||||
|
||||
await async_post(
|
||||
`../../api/assiduite/${assiduite.assiduite_id}/edit`,
|
||||
{
|
||||
etat: etatAssi,
|
||||
},
|
||||
async (data) => {
|
||||
const etud = etuds.get(Number(assiduite.etudid));
|
||||
await MiseAJourLigneEtud(etud);
|
||||
this.refresh(etud.assiduites, this.conflictPeriod);
|
||||
closePromptModal();
|
||||
},
|
||||
(error) => {
|
||||
console.error("Erreur lors de la modification de l'assiduité", error);
|
||||
}
|
||||
);
|
||||
|
||||
// Sélectionner la nouvelle assiduité
|
||||
this.selectedAssiduite = assiduite;
|
||||
const selectedEl = document.querySelector(
|
||||
`.assiduite[data-id="${assiduite.assiduite_id}"]`
|
||||
);
|
||||
if (selectedEl) {
|
||||
selectedEl.classList.add("selected");
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
"var(--color-present)"
|
||||
);
|
||||
}
|
||||
|
||||
//Mise à jour de la partie information du modal
|
||||
const selectedModal = document.querySelector(".modal-assiduite-content");
|
||||
spliterAssiduite(assiduite) {
|
||||
// Select pour choisir l'état de l'assiduité
|
||||
const creneau = getPeriodAsDate()
|
||||
creneau.deb = creneau.deb.format().substring(11,16)
|
||||
creneau.fin = creneau.fin.format().substring(11,16)
|
||||
const html = `
|
||||
<p>La période conflictuelle s'étend de ${creneau.deb} à ${creneau.fin}</p>
|
||||
<br>
|
||||
<input type="text" id="promptTime" name="promptTime" class="timepicker"
|
||||
placeholder="Cliquez pour choisir un horaire" required>
|
||||
`;
|
||||
|
||||
selectedModal.classList.add("show");
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
div.style.display = "flex";
|
||||
div.style.justifyContent = "center";
|
||||
div.style.flexDirection = "column";
|
||||
|
||||
document.getElementById("modal-assiduite-id").textContent =
|
||||
assiduite.assiduite_id;
|
||||
document.getElementById(
|
||||
"modal-assiduite-user"
|
||||
).textContent = `saisie le ${formatDateModal(
|
||||
assiduite.entry_date,
|
||||
"à"
|
||||
)} \npar ${assiduite.user_id}`;
|
||||
document.getElementById("modal-assiduite-module").textContent =
|
||||
assiduite.moduleimpl_id;
|
||||
document.getElementById("modal-assiduite-deb").textContent = formatDateModal(
|
||||
assiduite.date_debut
|
||||
);
|
||||
document.getElementById("modal-assiduite-fin").textContent = formatDateModal(
|
||||
assiduite.date_fin
|
||||
);
|
||||
document.getElementById("modal-assiduite-etat").textContent =
|
||||
assiduite.etat.capitalize();
|
||||
openPromptModal(
|
||||
"Séparer l'assiduité",
|
||||
div,
|
||||
async (closePromptModal) => {
|
||||
const separateur = promptTime.value;
|
||||
if (separateur === "") return true;
|
||||
|
||||
//Activation des boutons d'actions de conflit
|
||||
this.deleteBtn.disabled = false;
|
||||
this.splitBtn.disabled = false;
|
||||
this.editBtn.disabled = false;
|
||||
}
|
||||
/**
|
||||
* Suppression de l'assiduité sélectionnée
|
||||
*/
|
||||
deleteAssiduiteModal() {
|
||||
if (!this.selectedAssiduite) return;
|
||||
deleteAssiduite(this.selectedAssiduite.assiduite_id);
|
||||
const assiduiteAvant = {...assiduite};
|
||||
const assiduiteAprès = {...assiduite};
|
||||
|
||||
this.callbacks.delete(this.selectedAssiduite)
|
||||
assiduiteAvant.date_fin = assiduite.date_fin.substring(0,11) + separateur;
|
||||
assiduiteAprès.date_debut = assiduite.date_debut.substring(0,11) + separateur;
|
||||
|
||||
this.refresh(assiduites[this.selectedAssiduite.etudid]);
|
||||
// On supprime l'assiduité actuelle
|
||||
await async_post(
|
||||
"../../api/assiduite/delete",
|
||||
[assiduite.assiduite_id],
|
||||
(data)=>{console.log(data)},
|
||||
()=>{},
|
||||
)
|
||||
|
||||
// Désélection de l'assiduité
|
||||
this.resetSelection();
|
||||
}
|
||||
// On ajoute les deux nouvelles assiduités
|
||||
await async_post(
|
||||
"../../api/assiduites/create",
|
||||
[assiduiteAvant, assiduiteAprès],
|
||||
async (data)=>{
|
||||
console.log(data);
|
||||
const etud = etuds.get(Number(assiduite.etudid));
|
||||
await MiseAJourLigneEtud(etud);
|
||||
this.refresh(etud.assiduites, this.conflictPeriod);
|
||||
closePromptModal();
|
||||
},
|
||||
()=>{},
|
||||
)
|
||||
|
||||
/**
|
||||
* Division d'une assiduité
|
||||
*/
|
||||
splitAssiduiteModal() {
|
||||
//Préparation du prompt
|
||||
const htmlPrompt = `<legend>Entrez l'heure de séparation</legend>
|
||||
<input type="text" id="promptTime" name="appt"required style="position: relative; z-index: 100000;">`;
|
||||
|
||||
const fieldSet = document.createElement("fieldset");
|
||||
fieldSet.classList.add("fieldsplit", "timepicker");
|
||||
fieldSet.innerHTML = htmlPrompt;
|
||||
|
||||
//Callback de division
|
||||
const success = () => {
|
||||
const separatorTime = document.getElementById("promptTime").value;
|
||||
const dateString =
|
||||
getDate().format("YYYY-MM-DD") + `T${separatorTime}`;
|
||||
const separtorDate = new Date(dateString);
|
||||
|
||||
const assiduite_debut = new Date(this.selectedAssiduite.date_debut);
|
||||
const assiduite_fin = new Date(this.selectedAssiduite.date_fin);
|
||||
|
||||
if (
|
||||
separtorDate.isAfter(assiduite_debut) &&
|
||||
separtorDate.isBefore(assiduite_fin)
|
||||
) {
|
||||
const assiduite_avant = {
|
||||
etat: this.selectedAssiduite.etat,
|
||||
date_debut: assiduite_debut.toFakeIso(),
|
||||
date_fin: separtorDate.toFakeIso(),
|
||||
};
|
||||
|
||||
const assiduite_apres = {
|
||||
etat: this.selectedAssiduite.etat,
|
||||
date_debut: separtorDate.toFakeIso(),
|
||||
date_fin: assiduite_fin.toFakeIso(),
|
||||
};
|
||||
|
||||
if (this.selectedAssiduite.moduleimpl_id) {
|
||||
assiduite_apres["moduleimpl_id"] = this.selectedAssiduite.moduleimpl_id;
|
||||
assiduite_avant["moduleimpl_id"] = this.selectedAssiduite.moduleimpl_id;
|
||||
}
|
||||
|
||||
deleteAssiduite(this.selectedAssiduite.assiduite_id);
|
||||
|
||||
const path = getUrl() + `/api/assiduite/${this.selectedAssiduite.etudid}/create`;
|
||||
sync_post(
|
||||
path,
|
||||
[assiduite_avant, assiduite_apres],
|
||||
(data, status) => {
|
||||
//success
|
||||
},
|
||||
(data, status) => {
|
||||
//error
|
||||
console.error(data, status);
|
||||
errorAlert();
|
||||
}
|
||||
);
|
||||
|
||||
this.callbacks.split(this.selectedAssiduite)
|
||||
this.refresh(assiduites[this.selectedAssiduite.etudid]);
|
||||
this.resetSelection();
|
||||
} else {
|
||||
const att = document.createTextNode(
|
||||
"L'heure de séparation doit être compris dans la période de l'assiduité sélectionnée."
|
||||
);
|
||||
|
||||
openAlertModal("Attention", att, "", "var(--color-warning)");
|
||||
}
|
||||
};
|
||||
|
||||
openPromptModal("Séparation de l'assiduité sélectionnée", fieldSet, success, () => { }, "var(--color-present)");
|
||||
// Initialisation du timepicker
|
||||
const deb = this.selectedAssiduite.date_debut.substring(11,16);
|
||||
const fin = this.selectedAssiduite.date_fin.substring(11,16);
|
||||
},
|
||||
() => {},
|
||||
"var(--color-retard)"
|
||||
);
|
||||
|
||||
// Initialisation du timepicker
|
||||
const deb = assiduite.date_debut.substring(11,16);
|
||||
const fin = assiduite.date_fin.substring(11,16);
|
||||
setTimeout(()=>{
|
||||
$('#promptTime').timepicker({
|
||||
timeFormat: 'HH:mm',
|
||||
|
@ -331,150 +376,15 @@
|
|||
});
|
||||
}, 100
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modification d'une assiduité conflictuelle
|
||||
*/
|
||||
editAssiduiteModal() {
|
||||
if (!this.selectedAssiduite) return;
|
||||
|
||||
//Préparation du modal d'édition
|
||||
const htmlPrompt = `<legend>Entrez l'état de l'assiduité :</legend>
|
||||
<select name="promptSelect" id="promptSelect" required>
|
||||
<option value="">Choissez l'état</option>
|
||||
<option value="present">Présent</option>
|
||||
<option value="retard">En Retard</option>
|
||||
<option value="absent">Absent</option>
|
||||
</select>`;
|
||||
|
||||
const fieldSet = document.createElement("fieldset");
|
||||
fieldSet.classList.add("fieldsplit");
|
||||
fieldSet.innerHTML = htmlPrompt;
|
||||
|
||||
//Callback d'action d'édition
|
||||
const success = () => {
|
||||
const newState = document.getElementById("promptSelect").value;
|
||||
if (!["present", "absent", "retard"].includes(newState.toLowerCase())) {
|
||||
const att = document.createTextNode(
|
||||
"L'état doit être 'present', 'absent' ou 'retard'."
|
||||
);
|
||||
openAlertModal("Attention", att, "", "var(--color-warning)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Actualiser l'affichage
|
||||
|
||||
editAssiduite(this.selectedAssiduite.assiduite_id, newState, [this.selectedAssiduite]);
|
||||
this.callbacks.edit(this.selectedAssiduite)
|
||||
this.refresh(assiduites[this.selectedAssiduite.etudid]);
|
||||
|
||||
// Désélection de l'assiduité
|
||||
this.resetSelection();
|
||||
};
|
||||
|
||||
//Affichage du prompt
|
||||
openPromptModal("Modification de l'état de l'assiduité sélectionnée", fieldSet, success, () => { }, "var(--color-present)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Génération du modal
|
||||
*/
|
||||
render() {
|
||||
const timeLabels = document.querySelector(".time-labels");
|
||||
const assiduitesContainer = document.querySelector(".assiduites-container");
|
||||
|
||||
timeLabels.innerHTML = "";
|
||||
assiduitesContainer.innerHTML = '<div class="assiduite-special"></div>';
|
||||
|
||||
// Ajout des labels d'heure sur la frise chronologique
|
||||
for (let i = t_start; i <= t_end; i++) {
|
||||
const timeLabel = document.createElement("div");
|
||||
timeLabel.className = "time-label";
|
||||
timeLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`;
|
||||
timeLabels.appendChild(timeLabel);
|
||||
}
|
||||
|
||||
//Placement de la période conflictuelle sur la timeline
|
||||
const specialAssiduiteEl = document.querySelector(".assiduite-special");
|
||||
|
||||
specialAssiduiteEl.style.width = getWidth(
|
||||
this.conflictPeriod.deb,
|
||||
this.conflictPeriod.fin
|
||||
);
|
||||
specialAssiduiteEl.style.left = getLeftPosition(this.conflictPeriod.deb);
|
||||
specialAssiduiteEl.style.top = "0";
|
||||
specialAssiduiteEl.style.zIndex = "0"; // Place l'assiduité spéciale en arrière-plan
|
||||
assiduitesContainer.appendChild(specialAssiduiteEl);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Placement des assiduités sur la timeline
|
||||
this.list.forEach((assiduite) => {
|
||||
const period = {
|
||||
deb: new Date(assiduite.date_debut),
|
||||
fin: new Date(assiduite.date_fin),
|
||||
};
|
||||
if (!hasTimeConflict(period, this.interval)) {
|
||||
return;
|
||||
}
|
||||
const el = document.createElement("div");
|
||||
el.className = "assiduite";
|
||||
el.style.backgroundColor = getColor(assiduite.etat);
|
||||
|
||||
|
||||
|
||||
el.style.width = getWidth(assiduite.date_debut, assiduite.date_fin);
|
||||
el.style.left = getLeftPosition(assiduite.date_debut);
|
||||
el.style.top = "10px";
|
||||
el.setAttribute("data-id", assiduite.assiduite_id);
|
||||
el.addEventListener("click", () => this.selectAssiduite(assiduite));
|
||||
|
||||
// Ajout des informations dans la visualisation d'une assiduité
|
||||
const infoContainer = document.createElement("div");
|
||||
infoContainer.className = "assiduite-info";
|
||||
|
||||
const idDiv = document.createElement("div");
|
||||
idDiv.className = "assiduite-id";
|
||||
idDiv.textContent = `ID: ${assiduite.assiduite_id}`;
|
||||
infoContainer.appendChild(idDiv);
|
||||
|
||||
const periodDivDeb = document.createElement("div");
|
||||
periodDivDeb.className = "assiduite-period";
|
||||
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
|
||||
infoContainer.appendChild(periodDivDeb);
|
||||
const periodDivFin = document.createElement("div");
|
||||
periodDivFin.className = "assiduite-period";
|
||||
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
|
||||
infoContainer.appendChild(periodDivFin);
|
||||
|
||||
const stateDiv = document.createElement("div");
|
||||
stateDiv.className = "assiduite-state";
|
||||
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
|
||||
infoContainer.appendChild(stateDiv);
|
||||
|
||||
const userIdDiv = document.createElement("div");
|
||||
userIdDiv.className = "assiduite-user_id";
|
||||
userIdDiv.textContent = `saisie le ${formatDateModal(
|
||||
assiduite.entry_date,
|
||||
"à"
|
||||
)} \npar ${assiduite.user_id}`;
|
||||
infoContainer.appendChild(userIdDiv);
|
||||
|
||||
el.appendChild(infoContainer);
|
||||
assiduitesContainer.appendChild(el);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Remise à zéro de la sélection
|
||||
* Désactivation des boutons d'actions de conflit
|
||||
*/
|
||||
resetSelection() {
|
||||
this.selectedAssiduite = null;
|
||||
this.deleteBtn.disabled = true;
|
||||
this.splitBtn.disabled = true;
|
||||
this.editBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.ui-timepicker-container {
|
||||
|
|
|
@ -13,23 +13,18 @@
|
|||
*/
|
||||
function createMiniTimeline(assiduitesArray, day = null) {
|
||||
const array = [...assiduitesArray];
|
||||
const date = day == null ? getDate() : new Date(day);
|
||||
const date = day == null ? $("#date").datepicker("getDate") : new Date(day);
|
||||
const timeline = document.createElement("div");
|
||||
timeline.className = "mini-timeline";
|
||||
if (isSingleEtud()) {
|
||||
timeline.classList.add("single");
|
||||
}
|
||||
const timelineDate = date.startOf("day");
|
||||
const dayStart = timelineDate.clone().add(mt_start, "hours");
|
||||
const dayEnd = timelineDate.clone().add(mt_end, "hours");
|
||||
const dayStart = new Date(`${timelineDate.format("YYYY-MM-DD").substring(0,10)}T${numberToTime(mt_start)}`);
|
||||
const dayEnd = new Date(`${timelineDate.format("YYYY-MM-DD").substring(0,10)}T${numberToTime(mt_end)}`);
|
||||
const dayDuration = new Duration(dayStart, dayEnd).minutes;
|
||||
|
||||
timeline.appendChild(setMiniTick(timelineDate, dayStart, dayDuration));
|
||||
|
||||
|
||||
|
||||
if (day == null) {
|
||||
const tlTimes = getTimeLineTimes();
|
||||
const tlTimes = getPeriodAsDate();
|
||||
array.push({
|
||||
date_debut: tlTimes.deb.format(),
|
||||
date_fin: tlTimes.fin.format(),
|
||||
|
@ -69,56 +64,20 @@
|
|||
fin = Math.min(mt_end, fin);
|
||||
|
||||
if (day == null) setPeriodValues(deb, fin);
|
||||
if (isSingleEtud()) {
|
||||
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
|
||||
updateJustifyBtn();
|
||||
}
|
||||
|
||||
$("#moduleimpl_select").val(getModuleImplId(assiduité))
|
||||
setTimeout(()=>{
|
||||
$("#moduleimpl_select").trigger("change");
|
||||
}, 0)
|
||||
});
|
||||
//ajouter affichage assiduites on over
|
||||
setupAssiduiteBuble(block, assiduité);
|
||||
setupAssiduiteBubble(block, assiduité);
|
||||
}
|
||||
|
||||
const action = (justificatifs) => {
|
||||
if (justificatifs.length > 0) {
|
||||
let j = "invalid_justified";
|
||||
// TODO: ajout couleur justificatif
|
||||
|
||||
justificatifs.forEach((ju) => {
|
||||
if (ju.etat == "VALIDE") {
|
||||
j = "justified";
|
||||
}
|
||||
});
|
||||
|
||||
block.classList.add(j);
|
||||
}
|
||||
};
|
||||
|
||||
if (assiduité.etudid) {
|
||||
getJustificatifFromPeriod(
|
||||
{
|
||||
deb: new Date(assiduité.date_debut),
|
||||
fin: new Date(assiduité.date_fin),
|
||||
},
|
||||
assiduité.etudid,
|
||||
action
|
||||
);
|
||||
}
|
||||
|
||||
switch (assiduité.etat) {
|
||||
case "PRESENT":
|
||||
block.classList.add("present");
|
||||
break;
|
||||
case "RETARD":
|
||||
block.classList.add("retard");
|
||||
break;
|
||||
case "ABSENT":
|
||||
block.classList.add("absent");
|
||||
break;
|
||||
case "CRENEAU":
|
||||
block.classList.add("creneau");
|
||||
break;
|
||||
default:
|
||||
block.style.backgroundColor = "white";
|
||||
}
|
||||
block.classList.add(assiduité.etat.toLowerCase());
|
||||
if(assiduité.etat != "CRENEAU") block.classList.add("color");
|
||||
|
||||
timeline.appendChild(block);
|
||||
});
|
||||
|
@ -126,57 +85,6 @@
|
|||
return timeline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajout de la visualisation des assiduités de la mini timeline
|
||||
* @param {HTMLElement} el l'élément survollé
|
||||
* @param {Assiduité} assiduite l'assiduité représentée par l'élément
|
||||
*/
|
||||
function setupAssiduiteBuble(el, assiduite) {
|
||||
if (!assiduite) return;
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = "assiduite-bubble";
|
||||
bubble.classList.add(assiduite.etat.toLowerCase());
|
||||
|
||||
const idDiv = document.createElement("div");
|
||||
idDiv.className = "assiduite-id";
|
||||
idDiv.textContent = `${getModuleImpl(assiduite)}`;
|
||||
bubble.appendChild(idDiv);
|
||||
|
||||
const periodDivDeb = document.createElement("div");
|
||||
periodDivDeb.className = "assiduite-period";
|
||||
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
|
||||
bubble.appendChild(periodDivDeb);
|
||||
const periodDivFin = document.createElement("div");
|
||||
periodDivFin.className = "assiduite-period";
|
||||
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
|
||||
bubble.appendChild(periodDivFin);
|
||||
|
||||
const stateDiv = document.createElement("div");
|
||||
stateDiv.className = "assiduite-state";
|
||||
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
|
||||
bubble.appendChild(stateDiv);
|
||||
|
||||
const motifDiv = document.createElement("div");
|
||||
stateDiv.className = "assiduite-why";
|
||||
stateDiv.textContent = `Motif: ${assiduite.desc?.capitalize()}`;
|
||||
bubble.appendChild(motifDiv);
|
||||
|
||||
const userIdDiv = document.createElement("div");
|
||||
userIdDiv.className = "assiduite-user_id";
|
||||
userIdDiv.textContent = `saisie le ${formatDateModal(
|
||||
assiduite.entry_date,
|
||||
" à "
|
||||
)}`;
|
||||
|
||||
if (assiduite.user_id != null) {
|
||||
userIdDiv.textContent += `\npar ${assiduite.user_nom_complet}`
|
||||
}
|
||||
bubble.appendChild(userIdDiv);
|
||||
|
||||
el.appendChild(bubble);
|
||||
}
|
||||
|
||||
function setMiniTick(timelineDate, dayStart, dayDuration) {
|
||||
const endDate = timelineDate.clone().startOf("day");
|
||||
endDate.setHours(13, 0);
|
||||
|
|
|
@ -182,6 +182,9 @@
|
|||
promptModal.removeEventListener('click', this)
|
||||
}
|
||||
})
|
||||
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
}
|
||||
|
||||
function promptModalButtonAction(success, cancel) {
|
||||
|
@ -189,7 +192,7 @@
|
|||
succBtn.classList.add("btnPrompt")
|
||||
succBtn.textContent = "Valider"
|
||||
succBtn.addEventListener('click', () => {
|
||||
const retour = success();
|
||||
const retour = success(closePromptModal);
|
||||
if (retour == null || retour == false || retour == undefined) {
|
||||
closePromptModal();
|
||||
}
|
||||
|
@ -207,6 +210,7 @@
|
|||
|
||||
function closePromptModal() {
|
||||
promptModal.classList.remove("is-active")
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
const promptClose = document.querySelector(".promptModal-close");
|
||||
promptClose.onclick = function () {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div>
|
||||
<div class="sco_box_title">{{ titre }}</div>
|
||||
<div class="assi-tableau">
|
||||
<div class="scobox-title">{{ titre }}</div>
|
||||
<div class="options-tableau">
|
||||
{% if afficher_options != false %}
|
||||
<input type="checkbox" id="show_pres" name="show_pres"
|
||||
|
@ -17,7 +17,7 @@
|
|||
<a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a>
|
||||
<br>
|
||||
{% endif %}
|
||||
<label for="nb_ligne_page">Nombre de lignes par page :</label>
|
||||
<label for="nb_ligne_page">Nombre de lignes par page :</label>
|
||||
<select name="nb_ligne_page" id="nb_ligne_page" onchange="updateTableau()">
|
||||
{% for i in [25,50,100,1000] %}
|
||||
{% if i == options.nb_ligne_page %}
|
||||
|
@ -182,14 +182,14 @@
|
|||
// récupération de la colonne à ordonner
|
||||
// il faut avoir une classe `external-type:<NOM COL>`
|
||||
let order_col = e.className.split(" ").find((e)=>e.indexOf("external-type:") != -1);
|
||||
|
||||
|
||||
//Création de la nouvelle url avec le tri
|
||||
const url = new URL(location.href);
|
||||
url.searchParams.set("order", order);
|
||||
url.searchParams.set("order_col", order_col.split(":")[1]);
|
||||
|
||||
location.href = url.href
|
||||
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
<div class="timeline-container">
|
||||
<div class="period" style="left: 0%; width: 20%">
|
||||
<div class="period-handle left"></div>
|
||||
<div class="period-handle right"></div>
|
||||
<div class="period-time">Time</div>
|
||||
<div id="timeline">
|
||||
<div class="inputs">
|
||||
<input type="text" name="deb" id="deb" class="timepicker">
|
||||
<input type="text" name="fin" id="fin" class="timepicker">
|
||||
</div>
|
||||
<div class="timeline-container">
|
||||
<div class="period" style="left: 0%; width: 20%">
|
||||
<div class="period-handle left"></div>
|
||||
<div class="period-handle right"></div>
|
||||
<div class="period-time">Time</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
@ -87,6 +93,12 @@
|
|||
const text = `${deb} - ${fin}`
|
||||
periodTimeLine.querySelector('.period-time').textContent = text;
|
||||
|
||||
//Mise à jour des inputs
|
||||
try{
|
||||
$('#deb').val(deb);
|
||||
$('#fin').val(fin);
|
||||
}catch{}
|
||||
|
||||
}
|
||||
|
||||
function timelineMainEvent(event) {
|
||||
|
@ -176,6 +188,25 @@
|
|||
func_call = callback;
|
||||
timelineContainer.addEventListener("mousedown", (e) => { timelineMainEvent(e) });
|
||||
timelineContainer.addEventListener("touchstart", (e) => { timelineMainEvent(e) });
|
||||
|
||||
const updateFromInputs = ()=>{
|
||||
let deb = $('#deb').val();
|
||||
let fin = $('#fin').val();
|
||||
if (deb != '' && fin != '') {
|
||||
deb = fromTime(deb);
|
||||
fin = fromTime(fin);
|
||||
try {
|
||||
setPeriodValues(deb, fin);
|
||||
} catch {
|
||||
setPeriodValues(...getPeriodValues());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$('#deb').data('TimePicker').options.change = updateFromInputs;
|
||||
$('#fin').data('TimePicker').options.change = updateFromInputs;
|
||||
|
||||
updatePeriodTimeLabel();
|
||||
}
|
||||
|
||||
function adjustPeriodPosition(newLeft, newWidth) {
|
||||
|
@ -204,7 +235,7 @@
|
|||
const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)];
|
||||
|
||||
if (computedValues[0] > t_end || computedValues[1] < t_start) {
|
||||
return [t_start, min(t_end, t_start + period_default)];
|
||||
return [t_start, Math.min(t_end, t_start + period_default)];
|
||||
}
|
||||
|
||||
if (computedValues[1] - computedValues[0] <= tick_delay && computedValues[1] < t_end - tick_delay) {
|
||||
|
@ -262,6 +293,22 @@
|
|||
return hours + minutes / 60
|
||||
}
|
||||
|
||||
function getPeriodAsDate(){
|
||||
let [deb, fin] = getPeriodValues();
|
||||
deb = numberToTime(deb);
|
||||
fin = numberToTime(fin);
|
||||
|
||||
const dateStr = $("#date")
|
||||
.datepicker("getDate")
|
||||
.format("yyyy-mm-dd")
|
||||
.substring(0, 10);
|
||||
|
||||
return {
|
||||
deb: new Date(`${dateStr}T${deb}`),
|
||||
fin: new Date(`${dateStr}T${fin}`)
|
||||
}
|
||||
}
|
||||
|
||||
createTicks();
|
||||
|
||||
setPeriodValues(t_start, t_start + period_default);
|
||||
|
@ -277,6 +324,21 @@
|
|||
|
||||
</script>
|
||||
<style>
|
||||
|
||||
#timeline {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
.timeline-container {
|
||||
width: 75%;
|
||||
margin-left: 25px;
|
||||
|
|
|
@ -33,9 +33,11 @@
|
|||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
{% block app_content %}
|
||||
page vide
|
||||
{% endblock %}
|
||||
<div class="sco-app-content">
|
||||
{% block app_content %}
|
||||
page vide
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
{# page accueil département #}
|
||||
|
||||
{% extends "sco_page.j2" %}
|
||||
|
||||
{% block app_content %}
|
||||
<style>
|
||||
|
||||
table.listesems tr td.datesem {
|
||||
white-space: nowrap;
|
||||
padding-left: 8px;
|
||||
}
|
||||
table.listesems tr td.titresem {
|
||||
padding-left: 6px;
|
||||
}
|
||||
.table-formsemestres-titre {
|
||||
font-weight: bold;
|
||||
font-size: 110%;
|
||||
}
|
||||
table.semlist tr td.datesem {
|
||||
font-size: 80%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table.semlist tr td.semestre_id_n {
|
||||
text-align: center;
|
||||
}
|
||||
table.semlist tr td.nb_inscrits {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
{# News #}
|
||||
{{scolar_news_summary|safe}}
|
||||
|
||||
{# Avertissement de mise à jour: #}
|
||||
<div id="update_warning" class="scobox update_warning"></div>
|
||||
|
||||
{% if nb_user_accounts == 0 %}
|
||||
<h2>Aucun utilisateur défini !</h2>
|
||||
<p>Pour définir des utilisateurs <a href="{{
|
||||
url_for('users.index_html', scodoc_dept=g.scodoc_dept)
|
||||
}}">passez par la page Utilisateurs</a>.<br>
|
||||
Définissez au moins un utilisateur avec le rôle <tt>AdminXXX</tt>
|
||||
(le responsable du département <tt>XXX</tt>).
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{# Les semestres courants (cad non verrouillés) #}
|
||||
<div class="scobox">
|
||||
{{html_current_formsemestres|safe}}
|
||||
</div>
|
||||
|
||||
{# Table de tous les semestres #}
|
||||
|
||||
{% if html_table_formsemestres %}
|
||||
<details open>
|
||||
<summary class="table-formsemestres-titre">
|
||||
Les {{formsemestres.count()}} semestres de {{dept_name}}
|
||||
|
||||
<a href="{{
|
||||
url_for('scolar.export_table_dept_formsemestres', scodoc_dept=g.scodoc_dept)
|
||||
}}">{{scu.ICON_XLS|safe}}</a>
|
||||
</summary>
|
||||
{{ html_table_formsemestres|safe }}
|
||||
</details>
|
||||
{% else %}
|
||||
<p><a class="stdlink" href="{{
|
||||
url_for('scolar.index_html', scodoc_dept=g.scodoc_dept, showsemtable=1)
|
||||
}}">Voir table des {{formsemestres.count()}} semestres
|
||||
{% if nb_locked %}
|
||||
(dont {{nb_locked}} verrouillé{{'s' if nb_locked > 1 else ''}})
|
||||
{%endif%}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Recherche d'un semestre par code Apogée #}
|
||||
<form action="{url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)}">
|
||||
Chercher étape courante:
|
||||
<input name="etape_apo" type="text" size="8" spellcheck="false"></input>
|
||||
</form>
|
||||
|
||||
{# Gestion des étudiants #}
|
||||
<div class="scobox">
|
||||
<div class="scobox-title">Gestion des étudiants</div>
|
||||
<ul>
|
||||
{% if current_user.has_permission(Permission.EtudInscrit) %}
|
||||
<li><a class="stdlink" href="{{
|
||||
url_for('scolar.etudident_create_form', scodoc_dept=g.scodoc_dept)
|
||||
}}">créer <em>un</em> nouvel étudiant</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{{
|
||||
url_for('scolar.form_students_import_excel', scodoc_dept=g.scodoc_dept)
|
||||
}}">importer de nouveaux étudiants</a>
|
||||
(<em>ne pas utiliser</em> sauf cas particulier : utilisez plutôt le lien dans
|
||||
le tableau de bord semestre si vous souhaitez inscrire les
|
||||
étudiants importés à un semestre)
|
||||
</li>
|
||||
{% endif %}
|
||||
<li><a class="stdlink" href="{{
|
||||
url_for('scolar.export_etudiants_courants', scodoc_dept=g.scodoc_dept)
|
||||
}}">exporter tableau des étudiants des semestres en cours</a>
|
||||
</li>
|
||||
{% if current_user.has_permission(Permission.EtudInscrit) and sco.prefs["portal_url"] %}
|
||||
<li><a class="stdlink" href="{{
|
||||
url_for('scolar.formsemestre_import_etud_admission', scodoc_dept=g.scodoc_dept, tous_courants=1)
|
||||
}}">resynchroniser les données étudiants des semestres en cours depuis le portail</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{# Apogée #}
|
||||
{% if current_user.has_permission(Permission.EditApogee) %}
|
||||
<div class="scobox">
|
||||
<div class="scobox-title">Exports Apogée</div>
|
||||
<ul>
|
||||
<li><a class="stdlink" href="{{
|
||||
url_for('notes.semset_page', scodoc_dept=g.scodoc_dept)
|
||||
}}">Années scolaires / exports Apogée</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Assistance #}
|
||||
<div class="scobox">
|
||||
<div class="scobox-title">Assistance</div>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="stdlink" href="https://scodoc.org/Contact" target="_blank"
|
||||
rel="noopener noreferrer">Contact (Discord)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="stdlink" href="sco_dump_and_send_db">Envoyer données</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% endblock app_content %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/scolar_index.js"></script>
|
||||
{% endblock scripts %}
|
|
@ -186,6 +186,12 @@ def bilan_dept():
|
|||
if not table[0]:
|
||||
return table[1]
|
||||
|
||||
# Récupération des formsemestres (pour le menu déroulant)
|
||||
formsemestres: Query = FormSemestre.get_dept_formsemestres_courants(dept)
|
||||
formsemestres_choices: dict[int, str] = {
|
||||
fs.id: fs.titre_annee() for fs in formsemestres
|
||||
}
|
||||
|
||||
# Peuplement du template jinja
|
||||
return render_template(
|
||||
"assiduites/pages/bilan_dept.j2",
|
||||
|
@ -193,6 +199,8 @@ def bilan_dept():
|
|||
search_etud=sco_find_etud.form_search_etud(dest_url="assiduites.bilan_etud"),
|
||||
billets=billets,
|
||||
sco=ScoData(formsemestre=formsemestre),
|
||||
formsemestres=formsemestres_choices,
|
||||
formsemestre_id=None if not formsemestre else formsemestre.id,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1565,6 +1573,85 @@ def _prepare_tableau(
|
|||
)
|
||||
|
||||
|
||||
@bp.route("/recup_assiduites_plage", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.AbsChange)
|
||||
def recup_assiduites_plage():
|
||||
"""
|
||||
Renvoie un fichier excel contenant toutes les assiduités d'une plage
|
||||
La plage est définie par les valeurs "datedeb" et "datefin" du formulaire
|
||||
Par défaut tous les étudiants du département sont concernés
|
||||
Si le champs "formsemestre_id" est présent dans le formulaire et est non vide,
|
||||
seuls les étudiants inscrits dans ce semestre sont concernés.
|
||||
"""
|
||||
|
||||
date_deb: datetime.datetime = request.form.get("datedeb")
|
||||
date_fin: datetime.datetime = request.form.get("datefin")
|
||||
|
||||
# Vérification des dates
|
||||
try:
|
||||
date_deb = datetime.datetime.strptime(date_deb, "%d/%m/%Y")
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("date_debut invalide", dest_url=request.referrer) from exc
|
||||
try:
|
||||
date_fin = datetime.datetime.strptime(date_fin, "%d/%m/%Y")
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("date_fin invalide", dest_url=request.referrer) from exc
|
||||
|
||||
# Récupération des étudiants
|
||||
etuds: Query = []
|
||||
formsemestre_id: str | None = request.form.get("formsemestre_id")
|
||||
|
||||
name: str = ""
|
||||
|
||||
if formsemestre_id is not None and formsemestre_id != "":
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
etuds = formsemestre.etuds
|
||||
name = formsemestre.session_id()
|
||||
else:
|
||||
dept: Departement = Departement.query.get_or_404(g.scodoc_dept_id)
|
||||
etuds = dept.etudiants
|
||||
name = dept.acronym
|
||||
|
||||
# Récupération des assiduités
|
||||
assiduites: Query = Assiduite.query.filter(
|
||||
Assiduite.etudid.in_([etud.id for etud in etuds])
|
||||
)
|
||||
|
||||
# Filtrage des assiduités en fonction des dates données
|
||||
assiduites = scass.filter_by_date(assiduites, Assiduite, date_deb, date_fin)
|
||||
|
||||
table_data: liste_assi.AssiJustifData = liste_assi.AssiJustifData(
|
||||
assiduites_query=assiduites,
|
||||
)
|
||||
|
||||
options: liste_assi.AssiDisplayOptions = liste_assi.AssiDisplayOptions(
|
||||
show_pres=True,
|
||||
show_reta=True,
|
||||
show_module=True,
|
||||
show_etu=True,
|
||||
)
|
||||
|
||||
date_deb_str: str = date_deb.strftime("%d-%m-%Y")
|
||||
date_fin_str: str = date_fin.strftime("%d-%m-%Y")
|
||||
|
||||
filename: str = f"assiduites_{name}_{date_deb_str}_{date_fin_str}"
|
||||
|
||||
tableau: liste_assi.ListeAssiJusti = liste_assi.ListeAssiJusti(
|
||||
table_data,
|
||||
options=options,
|
||||
titre="tableau-dept-" + filename,
|
||||
no_pagination=True,
|
||||
)
|
||||
|
||||
return scu.send_file(
|
||||
tableau.excel(),
|
||||
filename=filename,
|
||||
mime=scu.XLSX_MIMETYPE,
|
||||
suffix=scu.XLSX_SUFFIX,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/tableau_assiduite_actions", methods=["GET", "POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.AbsChange)
|
||||
|
@ -1813,33 +1900,8 @@ def signal_assiduites_diff():
|
|||
formsemestre_id: int = request.args.get("formsemestre_id", -1)
|
||||
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
||||
date: str = request.args.get("jour", datetime.date.today().isoformat())
|
||||
date_deb: str = request.args.get("date_deb")
|
||||
date_fin: str = request.args.get("date_fin")
|
||||
semaine: str = request.args.get("semaine")
|
||||
|
||||
# Dans le cas où on donne une semaine plutot qu'un jour
|
||||
if semaine is not None:
|
||||
# On génère la semaine iso à partir de l'anne scolaire.
|
||||
semaine = (
|
||||
f"{scu.annee_scolaire()}-W{semaine}" if "W" not in semaine else semaine
|
||||
)
|
||||
# On met à jour les dates avec le date de debut et fin de semaine
|
||||
date_deb: datetime.date = datetime.datetime.strptime(
|
||||
semaine + "-1", "%Y-W%W-%w"
|
||||
)
|
||||
date_fin: datetime.date = date_deb + datetime.timedelta(days=6)
|
||||
|
||||
etudiants: list[Identite] = []
|
||||
|
||||
# --- Vérification de la date ---
|
||||
real_date = scu.is_iso_formated(date, True).date()
|
||||
|
||||
if real_date < formsemestre.date_debut:
|
||||
date = formsemestre.date_debut.isoformat()
|
||||
elif real_date > formsemestre.date_fin:
|
||||
date = formsemestre.date_fin.isoformat()
|
||||
|
||||
# Vérification des groupes
|
||||
if group_ids is None:
|
||||
group_ids = []
|
||||
|
@ -1873,28 +1935,26 @@ def signal_assiduites_diff():
|
|||
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
|
||||
)
|
||||
|
||||
moduleimpl_id = request.args.get("moduleimpl_id", -1)
|
||||
try:
|
||||
moduleimpl_id = int(moduleimpl_id)
|
||||
except ValueError:
|
||||
moduleimpl_id = -1
|
||||
|
||||
return render_template(
|
||||
"assiduites/pages/signal_assiduites_diff.j2",
|
||||
defaultDates=_get_days_between_dates(date_deb, date_fin),
|
||||
defdem=_get_etuds_dem_def(formsemestre),
|
||||
diff=_differee(
|
||||
etudiants=etudiants,
|
||||
moduleimpl_select=_module_selector(
|
||||
formsemestre, request.args.get("moduleimpl_id", None)
|
||||
),
|
||||
date=date,
|
||||
periode={
|
||||
"deb": formsemestre.date_debut.isoformat(),
|
||||
"fin": formsemestre.date_fin.isoformat(),
|
||||
},
|
||||
etudiants=etudiants,
|
||||
moduleimpl_select=_module_selector(
|
||||
formsemestre=formsemestre, moduleimpl_id=moduleimpl_id
|
||||
),
|
||||
gr=gr_tit,
|
||||
nonworkdays=_non_work_days(),
|
||||
sco=ScoData(formsemestre=formsemestre),
|
||||
sem=formsemestre.titre_num(),
|
||||
timeEvening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00:00"),
|
||||
timeMorning=ScoDocSiteConfig.get("assi_morning_time", "08:00:00"),
|
||||
timeNoon=ScoDocSiteConfig.get("assi_lunch_time", "13:00:00"),
|
||||
forcer_module=sco_preferences.get_preference(
|
||||
"forcer_module",
|
||||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -349,6 +349,20 @@ def index_html(showcodes=0, showsemtable=0):
|
|||
return sco_dept.index_html(showcodes=showcodes, showsemtable=showsemtable)
|
||||
|
||||
|
||||
@bp.route("/export_table_dept_formsemestres")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def export_table_dept_formsemestres():
|
||||
"""La table de tous les semestres non EXt du département, en excel"""
|
||||
table = sco_dept.index_html(showcodes=True, export_table_formsemestres=True)
|
||||
return scu.send_file(
|
||||
table.excel(),
|
||||
f"semestres_{g.scodoc_dept}",
|
||||
suffix=scu.XLSX_SUFFIX,
|
||||
mime=scu.XLSX_MIMETYPE,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/install_info")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
|
|
|
@ -79,10 +79,11 @@ if pytest:
|
|||
return get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
|
||||
|
||||
|
||||
def GET(path: str, headers: dict = None, errmsg=None, dept=None):
|
||||
"""Get and returns as JSON
|
||||
def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False):
|
||||
"""Get and optionaly returns as JSON
|
||||
Special case for non json result (image or pdf):
|
||||
return Content-Disposition string (inline or attachment)
|
||||
If raw, return a requests.Response
|
||||
"""
|
||||
if dept:
|
||||
url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path
|
||||
|
@ -101,10 +102,11 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None):
|
|||
raise APIError(
|
||||
errmsg or f"""erreur status={reply.status_code} !""", reply.json()
|
||||
)
|
||||
|
||||
if raw:
|
||||
return reply
|
||||
if reply.headers.get("Content-Type", None) == "application/json":
|
||||
return reply.json() # decode la reponse JSON
|
||||
elif reply.headers.get("Content-Type", None) in [
|
||||
if reply.headers.get("Content-Type", None) in [
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"application/pdf",
|
||||
|
|
|
@ -823,16 +823,13 @@ def test_etudiant_bulletin_semestre(api_headers):
|
|||
assert r.content[:4] == b"%PDF"
|
||||
|
||||
######## Bulletin BUT format intermédiaire en pdf #########
|
||||
r = requests.get(
|
||||
API_URL
|
||||
+ "/etudiant/ine/"
|
||||
+ str(INE)
|
||||
+ "/formsemestre/1/bulletin/selectedevals/pdf",
|
||||
r = GET(
|
||||
f"/etudiant/ine/{INE}/formsemestre/1/bulletin/selectedevals/pdf",
|
||||
headers=api_headers,
|
||||
verify=CHECK_CERTIFICATE,
|
||||
timeout=scu.SCO_TEST_API_TIMEOUT,
|
||||
raw=True, # get response, do not convert to json
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.headers.get("Content-Type", None) == "application/pdf"
|
||||
assert r.content[:4] == b"%PDF"
|
||||
|
||||
################### LONG + PDF #####################
|
||||
|
@ -869,37 +866,17 @@ def test_etudiant_bulletin_semestre(api_headers):
|
|||
################### SHORT #####################
|
||||
|
||||
######### Test etudid #########
|
||||
r = requests.get(
|
||||
API_URL + "/etudiant/etudid/" + str(ETUDID) + "/formsemestre/1/bulletin/short",
|
||||
headers=api_headers,
|
||||
verify=CHECK_CERTIFICATE,
|
||||
timeout=scu.SCO_TEST_API_TIMEOUT,
|
||||
bul = GET(
|
||||
f"/etudiant/etudid/{ETUDID}/formsemestre/1/bulletin/short", headers=api_headers
|
||||
)
|
||||
assert r.status_code == 200
|
||||
bul = r.json()
|
||||
assert len(bul) == 14 # HARDCODED
|
||||
|
||||
######### Test code nip #########
|
||||
|
||||
r = requests.get(
|
||||
API_URL + "/etudiant/nip/" + str(NIP) + "/formsemestre/1/bulletin/short",
|
||||
headers=api_headers,
|
||||
verify=CHECK_CERTIFICATE,
|
||||
timeout=scu.SCO_TEST_API_TIMEOUT,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
bul = r.json()
|
||||
bul = GET(f"/etudiant/nip/{NIP}/formsemestre/1/bulletin/short", headers=api_headers)
|
||||
assert len(bul) == 14 # HARDCODED
|
||||
|
||||
######### Test code ine #########
|
||||
r = requests.get(
|
||||
API_URL + "/etudiant/ine/" + str(INE) + "/formsemestre/1/bulletin/short",
|
||||
headers=api_headers,
|
||||
verify=CHECK_CERTIFICATE,
|
||||
timeout=scu.SCO_TEST_API_TIMEOUT,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
bul = r.json()
|
||||
bul = GET(f"/etudiant/ine/{INE}/formsemestre/1/bulletin/short", headers=api_headers)
|
||||
assert len(bul) == 14 # HARDCODED
|
||||
|
||||
################### SHORT + PDF #####################
|
||||
|
@ -941,6 +918,20 @@ def test_etudiant_bulletin_semestre(api_headers):
|
|||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
### -------- Modifie publication bulletins
|
||||
admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
|
||||
formsemestre = POST_JSON(
|
||||
f"/formsemestre/{1}/edit", {"bul_hide_xml": True}, headers=admin_header
|
||||
)
|
||||
assert formsemestre["bul_hide_xml"] is True
|
||||
# La forme utilisée par la passerelle:
|
||||
bul = GET(f"/etudiant/nip/{NIP}/formsemestre/1/bulletin", headers=api_headers)
|
||||
assert len(bul) == 9 # version raccourcie, longueur HARDCODED
|
||||
# TODO forme utilisée par la passerelle pour les PDF
|
||||
# /ScoDoc/api/etudiant/nip/12345/formsemestre/123/bulletin/long/pdf/nosi
|
||||
# TODO voir forme utilisée par ScoDoc en interne:
|
||||
# formsemestre_bulletinetud?formsemestre_id=1263&etudid=16387
|
||||
|
||||
|
||||
def test_etudiant_groups(api_headers):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue