Compare commits

...

21 Commits

Author SHA1 Message Date
Emmanuel Viennet 29eb8c297b Améliore page accueil dept.: formation, cosmétique, export excel 2024-03-21 13:21:25 +01:00
Emmanuel Viennet 38032a8c09 Ré-écriture de la page d'accueil de département. Template. 2024-03-21 12:06:34 +01:00
Emmanuel Viennet 2f2d98954c Maquette: introduit scobox, reprend certaines pages. WIP 2024-03-20 18:13:19 +01:00
Emmanuel Viennet 2e5d94f048 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into iziram-main96 2024-03-19 20:59:28 +01:00
Emmanuel Viennet 1b1b8ebdc4 Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc into iziram-main96 2024-03-19 20:59:13 +01:00
Emmanuel Viennet 9c6db169f3 Restreint accès aux bulletins PDF si formsemestre.bul_hide_xml (sémantique changée) + WIP tests unitaires API 2024-03-19 18:22:02 +01:00
Iziram 8ded16b94f Assiduité : liste_assi : colonne code et titre module closes #865 2024-03-19 16:30:13 +01:00
Iziram 5d10ee467e Assiduité : téléchargement des assiduités 2024-03-19 16:30:08 +01:00
Iziram 7af0dd1e1e Assiduite : signal_assiduites_diff tableau transposer + modifs mineurs 2024-03-18 17:41:17 +01:00
Emmanuel Viennet dece9a82d1 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into iziram-main96 2024-03-18 14:24:27 +01:00
Iziram b74d525c28 Assiduité: signal_assiduites_diff OK 2024-03-15 16:08:41 +01:00
Iziram c617ee321a Assiduité : signal_assiduites_diff suppr titres 2024-03-14 15:39:42 +01:00
Iziram 56ec4ba43d Assiduité : page signal_assiduites_diff WIP 2024-03-13 16:35:56 +01:00
Iziram d14f7e21b7 Assiduité : calendrier utilisation couleur générale (assiduites.css) 2024-03-11 11:39:36 +01:00
Iziram c3cb1da561 Assiduité : refonte signal_assiduites_group 2024-03-11 11:39:06 +01:00
Iziram cce60d432d Assiduité : timeline ajout timepicker 2024-03-11 11:37:58 +01:00
Iziram 4386994f7d Assiduité : bilan_etud suppr bouton suppression + avertissement tableau 2024-03-11 11:37:21 +01:00
Iziram fddfddfa7b Assiduité : minitimeline utilisation couleur assiduite + assiduite_bubble 2024-03-11 11:36:24 +01:00
Iziram 39dca32d2e Assiduité : date_utils suppression scodoc-datetime + ajout time conflit 2024-03-11 11:35:43 +01:00
Iziram e2b9cd3ded Assiduité : suppression assiduite js non utilisé 2024-03-11 11:35:09 +01:00
Iziram be227f4a2f Assiduité : Prompt : blocage scroll + fermeture on success 2024-03-11 11:33:05 +01:00
40 changed files with 2896 additions and 3439 deletions

View File

@ -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")

View File

@ -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)

View File

@ -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 (

View File

@ -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)

View File

@ -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()

View File

@ -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>"""
)

View File

@ -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:

View File

@ -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>
, en bas de page, suivez le lien
</p>
<p>, 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&nbsp;: 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:

View File

@ -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,

View File

@ -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>
"""
)

View File

@ -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]}

View File

@ -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)

View File

@ -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>
"""
)

View File

@ -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 = "&#9888;&#65039;" # warning /!\

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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

View File

@ -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);

View File

@ -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 = (

View File

@ -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 %}

View File

@ -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>

View File

@ -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&nbsp;:</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&nbsp;:</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&nbsp;:
<input type="text" class="datepicker" id="datedeb" name="datedeb">
</label>
<br>
<label for="datefin">
Au&nbsp;:
<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 %}

View File

@ -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.",

View File

@ -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;
}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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&nbsp;: {{grp|safe}}</div>
<div class="infos-button" style="margin-left: 24px;">Date&nbsp;: <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 %}

View File

@ -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">&times;</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 {

View File

@ -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);

View File

@ -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 () {

View File

@ -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
}));
});

View File

@ -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;

View File

@ -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 %}

View File

@ -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}}
&nbsp;
<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&nbsp;: 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 %}

View File

@ -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,
),
)

View File

@ -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)

View File

@ -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",

View File

@ -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):
"""