forked from ScoDoc/ScoDoc
Assiduites : Front End
This commit is contained in:
parent
506074c48e
commit
fd63877e4d
4
app/__init__.py
Normal file → Executable file
4
app/__init__.py
Normal file → Executable file
@ -322,6 +322,7 @@ def create_app(config_class=DevConfig):
|
||||
from app.views import notes_bp
|
||||
from app.views import users_bp
|
||||
from app.views import absences_bp
|
||||
from app.views import assiduites_bp
|
||||
from app.api import api_bp
|
||||
from app.api import api_web_bp
|
||||
|
||||
@ -340,6 +341,9 @@ def create_app(config_class=DevConfig):
|
||||
app.register_blueprint(
|
||||
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
|
||||
)
|
||||
app.register_blueprint(
|
||||
assiduites_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Assiduites"
|
||||
)
|
||||
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
|
||||
app.register_blueprint(api_web_bp, url_prefix="/ScoDoc/<scodoc_dept>/api")
|
||||
|
||||
|
2
app/scodoc/html_sidebar.py
Normal file → Executable file
2
app/scodoc/html_sidebar.py
Normal file → Executable file
@ -126,7 +126,7 @@ def sidebar(etudid: int = None):
|
||||
if current_user.has_permission(Permission.ScoAbsChange):
|
||||
H.append(
|
||||
f"""
|
||||
<li><a href="{ url_for('absences.SignaleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
|
||||
<li><a href="{ url_for('assiduites.signal_assiduites_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
|
||||
<li><a href="{ url_for('absences.JustifAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Justifier</a></li>
|
||||
<li><a href="{ url_for('absences.AnnuleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Supprimer</a></li>
|
||||
"""
|
||||
|
41
app/scodoc/sco_abs.py
Normal file → Executable file
41
app/scodoc/sco_abs.py
Normal file → Executable file
@ -42,6 +42,8 @@ from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_preferences
|
||||
from app.models import Assiduite, Justificatif
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
# --- Misc tools.... ------------------
|
||||
@ -1052,6 +1054,45 @@ def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
||||
return r
|
||||
|
||||
|
||||
def get_assiduites_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
||||
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||
tuple (nb abs, nb abs justifiées)
|
||||
Utilise un cache.
|
||||
"""
|
||||
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso + "_assiduites"
|
||||
r = sco_cache.AbsSemEtudCache.get(key)
|
||||
if not r:
|
||||
|
||||
date_debut: datetime.datetime = scu.is_iso_formated(date_debut_iso, True)
|
||||
date_fin: datetime.datetime = scu.is_iso_formated(date_fin_iso, True)
|
||||
|
||||
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
||||
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid)
|
||||
|
||||
assiduites = scass.filter_by_date(assiduites, Assiduite, date_debut, date_fin)
|
||||
justificatifs = scass.filter_by_date(
|
||||
justificatifs, Justificatif, date_debut, date_fin
|
||||
)
|
||||
|
||||
calculator: scass.CountCalculator = scass.CountCalculator()
|
||||
calculator.compute_assiduites(assiduites)
|
||||
nb_abs: dict = calculator.to_dict()["demi"]
|
||||
|
||||
abs_just: list[Assiduite] = scass.get_all_justified(
|
||||
etudid, date_debut, date_fin
|
||||
)
|
||||
|
||||
calculator.reset()
|
||||
calculator.compute_assiduites(abs_just)
|
||||
nb_abs_just: dict = calculator.to_dict()["demi"]
|
||||
|
||||
r = (nb_abs, nb_abs_just)
|
||||
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||
if not ans:
|
||||
log("warning: get_assiduites_count failed to cache")
|
||||
return r
|
||||
|
||||
|
||||
def invalidate_abs_count(etudid, sem):
|
||||
"""Invalidate (clear) cached counts"""
|
||||
date_debut = sem["date_debut_iso"]
|
||||
|
8
app/scodoc/sco_formsemestre_status.py
Normal file → Executable file
8
app/scodoc/sco_formsemestre_status.py
Normal file → Executable file
@ -829,9 +829,9 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
||||
</td>
|
||||
<td>
|
||||
<form action="{url_for(
|
||||
"absences.SignaleAbsenceGrSemestre", scodoc_dept=g.scodoc_dept
|
||||
"assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept
|
||||
)}" method="get">
|
||||
<input type="hidden" name="datefin" value="{
|
||||
<input type="hidden" name="date" value="{
|
||||
formsemestre.date_fin.strftime("%d/%m/%Y")}"/>
|
||||
<input type="hidden" name="group_ids" value="%(group_id)s"/>
|
||||
<input type="hidden" name="destination" value="{destination}"/>
|
||||
@ -848,8 +848,8 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
||||
</select>
|
||||
|
||||
<a href="{
|
||||
url_for("absences.choix_semaine", scodoc_dept=g.scodoc_dept)
|
||||
}?group_id=%(group_id)s">saisie par semaine</a>
|
||||
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
|
||||
}?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}">saisie par semaine</a>
|
||||
</form></td>
|
||||
"""
|
||||
else:
|
||||
|
4
app/scodoc/sco_photos.py
Normal file → Executable file
4
app/scodoc/sco_photos.py
Normal file → Executable file
@ -148,11 +148,11 @@ def get_photo_image(etudid=None, size="small"):
|
||||
filename = photo_pathname(etud.photo_filename, size=size)
|
||||
if not filename:
|
||||
filename = UNKNOWN_IMAGE_PATH
|
||||
r = _http_jpeg_file(filename)
|
||||
r = build_image_response(filename)
|
||||
return r
|
||||
|
||||
|
||||
def _http_jpeg_file(filename):
|
||||
def build_image_response(filename):
|
||||
"""returns an image as a Flask response"""
|
||||
st = os.stat(filename)
|
||||
last_modified = st.st_mtime # float timestamp
|
||||
|
475
app/static/css/assiduites.css
Normal file
475
app/static/css/assiduites.css
Normal file
@ -0,0 +1,475 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.selectors>* {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.selectors:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.no-display {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* === Gestion de la timeline === */
|
||||
|
||||
#tl_date {
|
||||
visibility: hidden;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
left: 15%;
|
||||
}
|
||||
|
||||
|
||||
.infos {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
#datestr {
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
border: 1px #444 solid;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#tl_slider {
|
||||
width: 90%;
|
||||
cursor: grab;
|
||||
|
||||
/* visibility: hidden; */
|
||||
}
|
||||
|
||||
#datestr,
|
||||
#time {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.ui-slider-handle.tl_handle {
|
||||
background: none;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
visibility: visible;
|
||||
background-position: top;
|
||||
background-size: cover;
|
||||
border: none;
|
||||
top: -180%;
|
||||
cursor: grab;
|
||||
|
||||
}
|
||||
|
||||
#l_handle {
|
||||
background-image: url(../icons/l_handle.svg);
|
||||
}
|
||||
|
||||
#r_handle {
|
||||
background-image: url(../icons/r_handle.svg);
|
||||
}
|
||||
|
||||
.ui-slider-range.ui-widget-header.ui-corner-all {
|
||||
background-color: #F9C768;
|
||||
background-image: none;
|
||||
opacity: 0.50;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
||||
/* === Gestion des etuds row === */
|
||||
|
||||
.etud_holder {
|
||||
margin-top: 5vh;
|
||||
}
|
||||
|
||||
.etud_row {
|
||||
display: grid;
|
||||
grid-template-columns: 10% 20% 30% 30%;
|
||||
gap: 3.33%;
|
||||
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
width: 80%;
|
||||
height: 50px;
|
||||
|
||||
margin: 0.5% 0;
|
||||
|
||||
box-shadow: -1px 12px 5px -8px rgba(68, 68, 68, 0.61);
|
||||
-webkit-box-shadow: -1px 12px 5px -8px rgba(68, 68, 68, 0.61);
|
||||
-moz-box-shadow: -1px 12px 5px -8px rgba(68, 68, 68, 0.61);
|
||||
}
|
||||
|
||||
.etud_row * {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
height: 50px;
|
||||
|
||||
}
|
||||
|
||||
/* --- Index --- */
|
||||
.etud_row .index_field {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
/* --- Nom étud --- */
|
||||
.etud_row .name_field {
|
||||
grid-column: 2;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.etud_row .name_field .name_set {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin: 0 5%;
|
||||
}
|
||||
|
||||
.etud_row .name_field .name_set * {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.etud_row .name_field .name_set h4 {
|
||||
font-size: small;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.etud_row .name_field .name_set h5 {
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
/* --- Barre assiduités --- */
|
||||
.etud_row .assiduites_bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
grid-column: 3;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.etud_row .assiduites_bar .filler {
|
||||
height: 5px;
|
||||
width: 90%;
|
||||
|
||||
background-color: white;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.etud_row .assiduites_bar #prevDateAssi {
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
|
||||
background-color: white;
|
||||
border: 1px solid #444;
|
||||
margin: 0px 8px;
|
||||
}
|
||||
|
||||
.etud_row .assiduites_bar #prevDateAssi.single {
|
||||
height: 9px;
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
.etud_row.conflit {
|
||||
background-color: #ff000061;
|
||||
}
|
||||
|
||||
.etud_row .assiduites_bar .absent {
|
||||
background-color: #ec5c49 !important;
|
||||
}
|
||||
|
||||
.etud_row .assiduites_bar .present {
|
||||
background-color: #37f05f !important;
|
||||
}
|
||||
|
||||
.etud_row .assiduites_bar .retard {
|
||||
background-color: #ecb52a !important;
|
||||
}
|
||||
|
||||
|
||||
/* --- Boutons assiduités --- */
|
||||
.etud_row .btns_field {
|
||||
grid-column: 4;
|
||||
}
|
||||
|
||||
.btns_field:disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.etud_row .btns_field * {
|
||||
margin: 0 5%;
|
||||
cursor: pointer;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.rbtn {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
}
|
||||
|
||||
.rbtn::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.rbtn.present::before {
|
||||
background-image: url(../icons/present.svg);
|
||||
}
|
||||
|
||||
.rbtn.absent::before {
|
||||
background-image: url(../icons/absent.svg);
|
||||
}
|
||||
|
||||
.rbtn.retard::before {
|
||||
background-image: url(../icons/retard.svg);
|
||||
}
|
||||
|
||||
.rbtn:checked:before {
|
||||
outline: 3px solid #7059FF;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.rbtn:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/*<== Modal conflit ==>*/
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 500;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 5% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
height: 30%;
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.close {
|
||||
color: #111;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Ajout de styles pour la frise chronologique */
|
||||
.modal-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.time-labels,
|
||||
.assiduites-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.assiduites-container {
|
||||
min-height: 20px;
|
||||
height: calc(50% - 60px);
|
||||
/* Augmentation de la hauteur du conteneur des assiduités */
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.action-buttons {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
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-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;
|
||||
height: 30%;
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.modal-assiduite-content.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal-assiduite-content .infos {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
/*<=== Mass Action ==>*/
|
||||
|
||||
.mass-selection {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 2% 0;
|
||||
}
|
||||
|
||||
.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 #3498db;
|
||||
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;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fieldsplit legend {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#page-assiduite-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#page-assiduite-content>* {
|
||||
margin: 1.5% 0;
|
||||
}
|
11
app/static/icons/absent.svg
Executable file
11
app/static/icons/absent.svg
Executable file
@ -0,0 +1,11 @@
|
||||
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="85" height="85" rx="15" fill="#F1A69C"/>
|
||||
<g opacity="0.5" clip-path="url(#clip0_120_4425)">
|
||||
<path d="M67.2116 70L43 45.707L18.7885 70L15.0809 66.3043L39.305 41.9995L15.0809 17.6939L18.7885 14L43 38.2922L67.2116 14L70.9191 17.6939L46.695 41.9995L70.9191 66.3043L67.2116 70Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_120_4425">
|
||||
<rect width="56" height="56" fill="white" transform="matrix(1 0 0 -1 15 70)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 547 B |
13
app/static/icons/present.svg
Executable file
13
app/static/icons/present.svg
Executable file
@ -0,0 +1,13 @@
|
||||
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="85" height="85" rx="15" fill="#9CF1AF"/>
|
||||
<g clip-path="url(#clip0_120_4405)">
|
||||
<g opacity="0.5">
|
||||
<path d="M70.7713 27.5875L36.0497 62.3091C35.7438 62.6149 35.2487 62.6149 34.9435 62.3091L15.2286 42.5935C14.9235 42.2891 14.9235 41.7939 15.2286 41.488L20.0191 36.6976C20.3249 36.3924 20.8201 36.3924 21.1252 36.6976L35.4973 51.069L64.8754 21.6909C65.1819 21.3858 65.6757 21.3858 65.9815 21.6909L70.7713 26.4814C71.0771 26.7865 71.0771 27.281 70.7713 27.5875Z" fill="black"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_120_4405">
|
||||
<rect width="56" height="56" fill="white" transform="translate(15 14)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 729 B |
12
app/static/icons/retard.svg
Executable file
12
app/static/icons/retard.svg
Executable file
@ -0,0 +1,12 @@
|
||||
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="85" height="85" rx="15" fill="#F1D99C"/>
|
||||
<g opacity="0.5" clip-path="url(#clip0_120_4407)">
|
||||
<path d="M55.2901 49.1836L44.1475 41.3918V28C44.1475 27.3688 43.6311 26.8524 43 26.8524C42.3688 26.8524 41.8524 27.3688 41.8524 28V42C41.8524 42.3787 42.036 42.7229 42.3459 42.941L53.9819 51.077C54.177 51.2147 54.4065 51.2836 54.636 51.2836C54.9918 51.2836 55.3475 51.1115 55.577 50.7787C55.9327 50.2623 55.8065 49.5508 55.2901 49.1836Z" fill="black"/>
|
||||
<path d="M62.7836 22.2164C57.482 16.9148 50.459 14 43 14C35.541 14 28.518 16.9148 23.2164 22.2164C17.9148 27.518 15 34.541 15 42C15 49.459 17.9148 56.482 23.2164 61.7836C28.518 67.0852 35.541 70 43 70C50.459 70 57.482 67.0852 62.7836 61.7836C68.0852 56.482 71 49.459 71 42C71 34.541 68.0852 27.518 62.7836 22.2164ZM44.1475 67.682V63C44.1475 62.3689 43.6311 61.8525 43 61.8525C42.3689 61.8525 41.8525 62.3689 41.8525 63V67.682C28.5869 67.0967 17.9033 56.4131 17.318 43.1475H22C22.6311 43.1475 23.1475 42.6311 23.1475 42C23.1475 41.3689 22.6311 40.8525 22 40.8525H17.318C17.9033 27.5869 28.5869 16.9033 41.8525 16.318V21C41.8525 21.6311 42.3689 22.1475 43 22.1475C43.6311 22.1475 44.1475 21.6311 44.1475 21V16.318C57.4131 16.9033 68.0967 27.5869 68.682 40.8525H64C63.3689 40.8525 62.8525 41.3689 62.8525 42C62.8525 42.6311 63.3689 43.1475 64 43.1475H68.682C68.0967 56.4131 57.4131 67.0967 44.1475 67.682Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_120_4407">
|
||||
<rect width="56" height="56" fill="white" transform="translate(15 14)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
1792
app/static/js/assiduites.js
Normal file
1792
app/static/js/assiduites.js
Normal file
File diff suppressed because it is too large
Load Diff
1597
app/static/libjs/moment-timezone.js
Normal file
1597
app/static/libjs/moment-timezone.js
Normal file
File diff suppressed because it is too large
Load Diff
3309
app/static/libjs/moment.new.min.js
vendored
Normal file
3309
app/static/libjs/moment.new.min.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
156
app/templates/assiduites/alert.j2
Normal file
156
app/templates/assiduites/alert.j2
Normal file
@ -0,0 +1,156 @@
|
||||
{% block alertmodal %}
|
||||
<div id="alertModal" class="alertmodal">
|
||||
|
||||
<!-- alertModal content -->
|
||||
<div class="alertmodal-content">
|
||||
<div class="alertmodal-header">
|
||||
<span class="alertmodal-close">×</span>
|
||||
<h2 class="alertmodal-title">alertModal Header</h2>
|
||||
</div>
|
||||
<div class="alertmodal-body">
|
||||
<p>Some text in the alertModal Body</p>
|
||||
<p>Some other text...</p>
|
||||
</div>
|
||||
<div class="alertmodal-footer">
|
||||
<h3>alertModal Footer</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<style>
|
||||
/* The alertModal (background) */
|
||||
.alertmodal {
|
||||
display: none;
|
||||
/* Hidden by default */
|
||||
position: fixed;
|
||||
/* Stay in place */
|
||||
z-index: 50;
|
||||
/* Sit on top */
|
||||
padding-top: 100px;
|
||||
/* Location of the box */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
/* Full width */
|
||||
height: 100%;
|
||||
/* Full height */
|
||||
overflow: auto;
|
||||
/* Enable scroll if needed */
|
||||
background-color: rgb(0, 0, 0);
|
||||
/* Fallback color */
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
/* Black w/ opacity */
|
||||
}
|
||||
|
||||
/* alertModal Content */
|
||||
.alertmodal-content {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-color: #fefefe;
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
border: 1px solid #888;
|
||||
width: 45%;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
-webkit-animation-name: animatetop;
|
||||
-webkit-animation-duration: 0.4s;
|
||||
animation-name: animatetop;
|
||||
animation-duration: 0.4s
|
||||
}
|
||||
|
||||
/* Add Animation */
|
||||
@-webkit-keyframes animatetop {
|
||||
from {
|
||||
top: -300px;
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
to {
|
||||
top: 0;
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animatetop {
|
||||
from {
|
||||
top: -300px;
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
to {
|
||||
top: 0;
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
/* The Close Button */
|
||||
.alertmodal-close {
|
||||
color: white;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alertmodal-close:hover,
|
||||
.alertmodal-close:focus {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.alertmodal-header {
|
||||
padding: 2px 16px;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.alertmodal-body {
|
||||
padding: 2px 16px;
|
||||
}
|
||||
|
||||
.alertmodal-footer {
|
||||
padding: 2px 16px;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.alertmodal.is-active {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const alertmodal = document.getElementById("alertModal");
|
||||
function openAlertModal(titre, contenu, footer, color = "crimson") {
|
||||
alertmodal.classList.add('is-active');
|
||||
|
||||
alertmodal.querySelector('.alertmodal-title').textContent = titre;
|
||||
alertmodal.querySelector('.alertmodal-body').innerHTML = ""
|
||||
alertmodal.querySelector('.alertmodal-body').appendChild(contenu);
|
||||
alertmodal.querySelector('.alertmodal-footer').textContent = footer;
|
||||
|
||||
const banners = Array.from(alertmodal.querySelectorAll('.alertmodal-footer,.alertmodal-header'))
|
||||
banners.forEach((ban) => {
|
||||
ban.style.backgroundColor = color;
|
||||
})
|
||||
}
|
||||
function closeAlertModal() {
|
||||
alertmodal.classList.remove("is-active")
|
||||
}
|
||||
const alertClose = document.querySelector(".alertmodal-close");
|
||||
alertClose.onclick = function () {
|
||||
closeAlertModal()
|
||||
}
|
||||
window.onclick = function (event) {
|
||||
if (event.target == alertmodal) {
|
||||
alertmodal.classList.remove('is-active');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock alertmodal %}
|
105
app/templates/assiduites/minitimeline.j2
Normal file
105
app/templates/assiduites/minitimeline.j2
Normal file
@ -0,0 +1,105 @@
|
||||
<div class="assiduite-bubble">
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.assiduite-bubble {
|
||||
position: fixed;
|
||||
display: none;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
border: 3px solid #ccc;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.assiduite-bubble.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.assiduite-bubble::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #f9f9f9 transparent;
|
||||
}
|
||||
|
||||
.assiduite-bubble::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #ccc transparent;
|
||||
}
|
||||
|
||||
.assiduite-id,
|
||||
.assiduite-period,
|
||||
.assiduite-state,
|
||||
.assiduite-user_id {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.assiduite-bubble.absent {
|
||||
border-color: #ec5c49 !important;
|
||||
}
|
||||
|
||||
.assiduite-bubble.present {
|
||||
border-color: #37f05f !important;
|
||||
}
|
||||
|
||||
.assiduite-bubble.retard {
|
||||
border-color: #ecb52a !important;
|
||||
}
|
||||
|
||||
.mini-timeline {
|
||||
height: 7px;
|
||||
width: 90%;
|
||||
border: 1px solid black;
|
||||
position: relative;
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mini-timeline.single {
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.mini-timeline-block {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#page-assiduite-content .mini-timeline-block {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mini_tick {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: -16px;
|
||||
left: 50%;
|
||||
|
||||
}
|
||||
|
||||
.mini_tick::after {
|
||||
display: block;
|
||||
content: "|";
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
113
app/templates/assiduites/moduleimpl_dynamic_selector.j2
Normal file
113
app/templates/assiduites/moduleimpl_dynamic_selector.j2
Normal file
@ -0,0 +1,113 @@
|
||||
<label for="moduleimpl_select">
|
||||
Module
|
||||
<select id="moduleimpl_select">
|
||||
<option value="" selected> Non spécifié </option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
function getEtudFormSemestres() {
|
||||
let semestre = {};
|
||||
sync_get(getUrl() + `/api/etudiant/etudid/${etudid}/formsemestres`, (data) => {
|
||||
semestre = data;
|
||||
});
|
||||
return semestre;
|
||||
}
|
||||
|
||||
function filterFormSemestres(semestres) {
|
||||
const date = new moment.tz(
|
||||
document.querySelector("#tl_date").value,
|
||||
TIMEZONE
|
||||
);
|
||||
|
||||
semestres = semestres.filter((fm) => {
|
||||
return date.isBetween(fm.date_debut_iso, fm.date_fin_iso)
|
||||
})
|
||||
|
||||
return semestres;
|
||||
}
|
||||
|
||||
function getFormSemestreProgramme(fm_id) {
|
||||
let semestre = {};
|
||||
sync_get(getUrl() + `/api/formsemestre/${fm_id}/programme`, (data) => {
|
||||
semestre = data;
|
||||
});
|
||||
return semestre;
|
||||
}
|
||||
|
||||
function getModulesImplByFormsemestre(semestres) {
|
||||
const map = new Map();
|
||||
|
||||
semestres.forEach((fm) => {
|
||||
const array = [];
|
||||
|
||||
const fm_p = getFormSemestreProgramme(fm.formsemestre_id);
|
||||
["ressources", "saes", "modules"].forEach((r) => {
|
||||
if (r in fm_p) {
|
||||
fm_p[r].forEach((o) => {
|
||||
array.push(getModuleInfos(o))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
map.set(fm.titre_num, array)
|
||||
})
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function getModuleInfos(obj) {
|
||||
return {
|
||||
moduleimpl_id: obj.moduleimpl_id,
|
||||
titre: obj.module.titre,
|
||||
code: obj.module.code,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function populateSelect(sems, selected) {
|
||||
const select = document.getElementById('moduleimpl_select');
|
||||
select.innerHTML = `<option value="" selected> Non spécifié </option>`
|
||||
|
||||
sems.forEach((mods, label) => {
|
||||
const optGrp = document.createElement('optgroup');
|
||||
optGrp.label = label
|
||||
|
||||
mods.forEach((obj) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = obj.moduleimpl_id;
|
||||
opt.textContent = `${obj.code} ${obj.titre}`
|
||||
if (obj.moduleimpl_id == selected) {
|
||||
opt.setAttribute('selected', 'true');
|
||||
}
|
||||
|
||||
optGrp.appendChild(opt);
|
||||
})
|
||||
select.appendChild(optGrp);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function updateSelect(moduleimpl_id) {
|
||||
let sem = getEtudFormSemestres()
|
||||
sem = filterFormSemestres(sem)
|
||||
const mod = getModulesImplByFormsemestre(sem)
|
||||
populateSelect(mod, moduleimpl_id);
|
||||
}
|
||||
|
||||
function updateSelectedSelect(moduleimpl_id) {
|
||||
document.getElementById('moduleimpl_select').value = moduleimpl_id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#moduleimpl_select {
|
||||
width: 125px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
14
app/templates/assiduites/moduleimpl_selector.j2
Normal file
14
app/templates/assiduites/moduleimpl_selector.j2
Normal file
@ -0,0 +1,14 @@
|
||||
<select name="moduleimpl_select" id="moduleimpl_select">
|
||||
|
||||
<option value="" {{selected}}> Non spécifié </option>
|
||||
|
||||
{% for mod in modules %}
|
||||
{% if mod.moduleimpl_id == moduleimpl_id %}
|
||||
<option value="{{mod.moduleimpl_id}}" selected> {{mod.name}} </option>
|
||||
{% else %}
|
||||
<option value="{{mod.moduleimpl_id}}"> {{mod.name}} </option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
</select>
|
211
app/templates/assiduites/prompt.j2
Normal file
211
app/templates/assiduites/prompt.j2
Normal file
@ -0,0 +1,211 @@
|
||||
{% block promptModal %}
|
||||
<div id="promptModal" class="promptModal">
|
||||
|
||||
<!-- promptModal content -->
|
||||
<div class="promptModal-content">
|
||||
<div class="promptModal-header">
|
||||
<span class="promptModal-close">×</span>
|
||||
<h2 class="promptModal-title">promptModal Header</h2>
|
||||
</div>
|
||||
<div class="promptModal-body">
|
||||
<p>Some text in the promptModal Body</p>
|
||||
<p>Some other text...</p>
|
||||
</div>
|
||||
<div class="promptModal-footer">
|
||||
<h3>promptModal Footer</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<style>
|
||||
/* The promptModal (background) */
|
||||
.promptModal {
|
||||
display: none;
|
||||
/* Hidden by default */
|
||||
position: fixed;
|
||||
/* Stay in place */
|
||||
z-index: 50;
|
||||
/* Sit on top */
|
||||
padding-top: 100px;
|
||||
/* Location of the box */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
/* Full width */
|
||||
height: 100%;
|
||||
/* Full height */
|
||||
overflow: auto;
|
||||
/* Enable scroll if needed */
|
||||
background-color: rgb(0, 0, 0);
|
||||
/* Fallback color */
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
/* Black w/ opacity */
|
||||
}
|
||||
|
||||
/* promptModal Content */
|
||||
.promptModal-content {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-color: #fefefe;
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
border: 1px solid #888;
|
||||
width: 45%;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
-webkit-animation-name: animatetop;
|
||||
-webkit-animation-duration: 0.4s;
|
||||
animation-name: animatetop;
|
||||
animation-duration: 0.4s
|
||||
}
|
||||
|
||||
/* Add Animation */
|
||||
@-webkit-keyframes animatetop {
|
||||
from {
|
||||
top: -300px;
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
to {
|
||||
top: 0;
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animatetop {
|
||||
from {
|
||||
top: -300px;
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
to {
|
||||
top: 0;
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
/* The Close Button */
|
||||
.promptModal-close {
|
||||
color: white;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.promptModal-close:hover,
|
||||
.promptModal-close:focus {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.promptModal-header {
|
||||
padding: 2px 16px;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.promptModal-body {
|
||||
padding: 2px 16px;
|
||||
}
|
||||
|
||||
.promptModal-footer {
|
||||
padding: 2px 16px;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.promptModal.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btnPrompt {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
background-color: #6c757d;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btnPrompt:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
.btnPrompt:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.btnPrompt:disabled {
|
||||
opacity: 0.7;
|
||||
background-color: whitesmoke;
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, rgba(81, 81, 81, 0.61) 5px, rgba(81, 81, 81, 0.61) 10px)
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const promptModal = document.getElementById("promptModal");
|
||||
function openPromptModal(titre, contenu, success, cancel = () => { }, color = "crimson") {
|
||||
promptModal.classList.add('is-active');
|
||||
|
||||
promptModal.querySelector('.promptModal-title').textContent = titre;
|
||||
promptModal.querySelector('.promptModal-body').innerHTML = ""
|
||||
promptModal.querySelector('.promptModal-body').appendChild(contenu);
|
||||
|
||||
promptModal.querySelector('.promptModal-footer').innerHTML = ""
|
||||
promptModalButtonAction(success, cancel).forEach((btnPrompt) => {
|
||||
promptModal.querySelector('.promptModal-footer').appendChild(btnPrompt)
|
||||
})
|
||||
|
||||
|
||||
const banners = Array.from(promptModal.querySelectorAll('.promptModal-footer,.promptModal-header'))
|
||||
banners.forEach((ban) => {
|
||||
ban.style.backgroundColor = color;
|
||||
})
|
||||
}
|
||||
|
||||
function promptModalButtonAction(success, cancel) {
|
||||
const succBtn = document.createElement('button')
|
||||
succBtn.classList.add("btnPrompt")
|
||||
succBtn.textContent = "Valider"
|
||||
succBtn.addEventListener('click', () => {
|
||||
success();
|
||||
closePromptModal();
|
||||
})
|
||||
const cancelBtn = document.createElement('button')
|
||||
cancelBtn.classList.add("btnPrompt")
|
||||
cancelBtn.textContent = "Annuler"
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
cancel();
|
||||
closePromptModal();
|
||||
})
|
||||
|
||||
return [succBtn, cancelBtn]
|
||||
}
|
||||
|
||||
function closePromptModal() {
|
||||
promptModal.classList.remove("is-active")
|
||||
}
|
||||
const promptClose = document.querySelector(".promptModal-close");
|
||||
promptClose.onclick = function () {
|
||||
closePromptModal()
|
||||
}
|
||||
window.onclick = function (event) {
|
||||
if (event.target == promptModal) {
|
||||
promptModal.classList.remove('is-active');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock promptModal %}
|
99
app/templates/assiduites/signal_assiduites_etud.j2
Normal file
99
app/templates/assiduites/signal_assiduites_etud.j2
Normal file
@ -0,0 +1,99 @@
|
||||
{# -*- mode: jinja-html -*- #}
|
||||
<div id="myModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</span>
|
||||
<h2>Veuillez régler le conflit pour poursuivre</h2>
|
||||
<!-- Ajout de la frise chronologique -->
|
||||
<div class="modal-timeline">
|
||||
<div class="time-labels"></div>
|
||||
<div class="assiduites-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button id="delete" class="btnPrompt" disabled>Supprimer</button>
|
||||
<button id="split" class="btnPrompt" disabled>Séparer</button>
|
||||
<button id="edit" class="btnPrompt" disabled>Modifier</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>
|
||||
{% include "assiduites/toast.j2" %}
|
||||
<div id="page-assiduite-content">
|
||||
{% block content %}
|
||||
<h2>Signalement de l'assiduité de <span class="rouge">{{sco.etud.nomprenom}}</span></h2>
|
||||
|
||||
<div class="infos">
|
||||
Date: <span id="datestr"></span>
|
||||
<input type="date" name="tl_date" id="tl_date" value="{{ date }}" onchange="updateDate()">
|
||||
</div>
|
||||
|
||||
{% include "assiduites/timeline.j2" %}
|
||||
|
||||
|
||||
<div>
|
||||
{% include "assiduites/moduleimpl_dynamic_selector.j2" %}
|
||||
<button class="btn" onclick="justify()" disabled id="justif-rapide">Justifier</button>
|
||||
</div>
|
||||
|
||||
<div class="btn_group">
|
||||
<button class="btn" onclick="setPeriodValues(8,18)">Journée</button>
|
||||
<button class="btn" onclick="setPeriodValues(8,13)">Matin</button>
|
||||
<button class="btn" onclick="setPeriodValues(13,18)">Après-midi</button>
|
||||
</div>
|
||||
|
||||
<div class="etud_holder">
|
||||
<div id="etud_row_{{sco.etud.id}}">
|
||||
<div class="index"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Ajout d'un conteneur pour le loader -->
|
||||
<div class="loader-container" id="loaderContainer">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
{% include "assiduites/alert.j2" %}
|
||||
{% include "assiduites/prompt.j2" %}
|
||||
|
||||
<script>
|
||||
const etudid = {{ sco.etud.id }};
|
||||
setupDate(() => {
|
||||
actualizeEtud(etudid);
|
||||
updateSelect()
|
||||
});
|
||||
|
||||
updateDate();
|
||||
|
||||
getSingleEtud({{ sco.etud.id }});
|
||||
actualizeEtud({{ sco.etud.id }});
|
||||
updateSelect()
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style>
|
||||
.rouge {
|
||||
color: crimson;
|
||||
}
|
||||
|
||||
.justifie {
|
||||
background-color: rgb(104, 104, 252);
|
||||
color: whitesmoke;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
</div>
|
76
app/templates/assiduites/signal_assiduites_group.j2
Normal file
76
app/templates/assiduites/signal_assiduites_group.j2
Normal file
@ -0,0 +1,76 @@
|
||||
{% include "assiduites/toast.j2" %}
|
||||
<section id="content">
|
||||
|
||||
<div class="no-display">
|
||||
|
||||
<span class="formsemestre_id">{{formsemestre_id}}</span>
|
||||
<span id="formsemestre_date_debut">{{formsemestre_date_debut}}</span>
|
||||
<span id="formsemestre_date_fin">{{formsemestre_date_fin}}</span>
|
||||
|
||||
</div>
|
||||
|
||||
<h2>
|
||||
Saisie des assiduités {{gr_tit|safe}} {{sem}}
|
||||
</h2>
|
||||
|
||||
<fieldset class="selectors">
|
||||
<div>Groupes : {{grp|safe}}</div>
|
||||
|
||||
<div>Modules :{{moduleimpl_select|safe}}</div>
|
||||
|
||||
<div class="infos">
|
||||
Date: <span id="datestr"></span>
|
||||
<input type="date" name="tl_date" id="tl_date" value="{{ date }}" onchange="updateDate()">
|
||||
</div>
|
||||
|
||||
<button id="validate_selectors" onclick="validateSelectors()">
|
||||
Valider
|
||||
</button>
|
||||
</fieldset>
|
||||
|
||||
{{timeline|safe}}
|
||||
|
||||
<div class="etud_holder">
|
||||
</div>
|
||||
<div id="myModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</span>
|
||||
<h2>Veuillez régler le conflit pour poursuivre</h2>
|
||||
<!-- Ajout de la frise chronologique -->
|
||||
<div class="modal-timeline">
|
||||
<div class="time-labels"></div>
|
||||
<div class="assiduites-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button id="delete" class="btnPrompt" disabled>Supprimer</button>
|
||||
<button id="split" class="btnPrompt" disabled>Séparer</button>
|
||||
<button id="edit" class="btnPrompt" disabled>Modifier</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>
|
||||
<!-- Ajout d'un conteneur pour le loader -->
|
||||
<div class="loader-container" id="loaderContainer">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
{% include "assiduites/alert.j2" %}
|
||||
{% include "assiduites/prompt.j2" %}
|
||||
|
||||
<script>
|
||||
updateDate();
|
||||
setupDate();
|
||||
</script>
|
||||
</section>
|
212
app/templates/assiduites/timeline.j2
Normal file
212
app/templates/assiduites/timeline.j2
Normal file
@ -0,0 +1,212 @@
|
||||
<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>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
|
||||
const timelineContainer = document.querySelector(".timeline-container");
|
||||
const periodTimeLine = document.querySelector(".period");
|
||||
|
||||
function createTicks() {
|
||||
for (let i = 8; i <= 18; i++) {
|
||||
const hourTick = document.createElement("div");
|
||||
hourTick.classList.add("tick", "hour");
|
||||
hourTick.style.left = `${((i - 8) / 10) * 100}%`;
|
||||
timelineContainer.appendChild(hourTick);
|
||||
|
||||
const tickLabel = document.createElement("div");
|
||||
tickLabel.classList.add("tick-label");
|
||||
tickLabel.style.left = `${((i - 8) / 10) * 100}%`;
|
||||
tickLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`;
|
||||
timelineContainer.appendChild(tickLabel);
|
||||
|
||||
if (i < 18) {
|
||||
for (let j = 1; j < 4; j++) {
|
||||
const quarterTick = document.createElement("div");
|
||||
quarterTick.classList.add("tick", "quarter");
|
||||
quarterTick.style.left = `${((i - 8 + j / 4) / 10) * 100}%`;
|
||||
timelineContainer.appendChild(quarterTick);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function snapToQuarter(value) {
|
||||
return Math.round(value * 4) / 4;
|
||||
}
|
||||
|
||||
timelineContainer.addEventListener("mousedown", (event) => {
|
||||
const startX = event.clientX;
|
||||
|
||||
if (event.target === periodTimeLine) {
|
||||
const startLeft = parseFloat(periodTimeLine.style.left);
|
||||
|
||||
const onMouseMove = (moveEvent) => {
|
||||
const deltaX = moveEvent.clientX - startX;
|
||||
const containerWidth = timelineContainer.clientWidth;
|
||||
const newLeft = startLeft + (deltaX / containerWidth) * 100;
|
||||
|
||||
adjustPeriodPosition(newLeft, parseFloat(periodTimeLine.style.width));
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener(
|
||||
"mouseup",
|
||||
() => {
|
||||
generateAllEtudRow();
|
||||
snapHandlesToQuarters()
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
} else if (event.target.classList.contains("period-handle")) {
|
||||
const startWidth = parseFloat(periodTimeLine.style.width);
|
||||
const startLeft = parseFloat(periodTimeLine.style.left);
|
||||
const isLeftHandle = event.target.classList.contains("left");
|
||||
|
||||
const onMouseMove = (moveEvent) => {
|
||||
const deltaX = moveEvent.clientX - startX;
|
||||
const containerWidth = timelineContainer.clientWidth;
|
||||
const newWidth =
|
||||
startWidth + ((isLeftHandle ? -deltaX : deltaX) / containerWidth) * 100;
|
||||
|
||||
if (isLeftHandle) {
|
||||
const newLeft = startLeft + (deltaX / containerWidth) * 100;
|
||||
adjustPeriodPosition(newLeft, newWidth);
|
||||
} else {
|
||||
adjustPeriodPosition(parseFloat(periodTimeLine.style.left), newWidth);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener(
|
||||
"mouseup",
|
||||
() => {
|
||||
snapHandlesToQuarters()
|
||||
generateAllEtudRow();
|
||||
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function adjustPeriodPosition(newLeft, newWidth) {
|
||||
|
||||
const snappedLeft = snapToQuarter(newLeft);
|
||||
const snappedWidth = snapToQuarter(newWidth);
|
||||
const minLeft = 0;
|
||||
const maxLeft = 100 - snappedWidth;
|
||||
|
||||
const clampedLeft = Math.min(Math.max(snappedLeft, minLeft), maxLeft);
|
||||
|
||||
periodTimeLine.style.left = `${clampedLeft}%`;
|
||||
periodTimeLine.style.width = `${snappedWidth}%`;
|
||||
}
|
||||
|
||||
function getPeriodValues() {
|
||||
const leftPercentage = parseFloat(periodTimeLine.style.left);
|
||||
const widthPercentage = parseFloat(periodTimeLine.style.width);
|
||||
|
||||
const startHour = (leftPercentage / 100) * 10 + 8;
|
||||
const endHour = ((leftPercentage + widthPercentage) / 100) * 10 + 8;
|
||||
|
||||
const startValue = Math.round(startHour * 4) / 4;
|
||||
const endValue = Math.round(endHour * 4) / 4;
|
||||
|
||||
return [startValue, endValue]
|
||||
}
|
||||
|
||||
function setPeriodValues(deb, fin) {
|
||||
let leftPercentage = (deb - 8) / 10 * 100
|
||||
let widthPercentage = (fin - deb) / 10 * 100
|
||||
periodTimeLine.style.left = `${leftPercentage}%`
|
||||
periodTimeLine.style.width = `${widthPercentage}%`
|
||||
|
||||
snapHandlesToQuarters()
|
||||
generateAllEtudRow();
|
||||
}
|
||||
|
||||
function snapHandlesToQuarters() {
|
||||
const periodValues = getPeriodValues();
|
||||
let lef = Math.min((periodValues[0] - 8) * 10, 97.5)
|
||||
if (lef < 0) {
|
||||
lef = 0;
|
||||
}
|
||||
const left = `${lef}%`
|
||||
let wid = Math.max((periodValues[1] - periodValues[0]) * 10, 2.5)
|
||||
if (wid > 100) {
|
||||
wid = 100;
|
||||
}
|
||||
const width = `${wid}%`
|
||||
periodTimeLine.style.left = left;
|
||||
periodTimeLine.style.width = width;
|
||||
}
|
||||
|
||||
createTicks();
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.timeline-container {
|
||||
width: 75%;
|
||||
margin-left: 5%;
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
position: relative;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* ... */
|
||||
.tick {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.tick.hour {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tick.quarter {
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
.tick-label {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
transform: translateY(100%) translateX(-50%);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
|
||||
.period {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 183, 255, 0.5);
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.period-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 8px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.period-handle.right {
|
||||
right: 0;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
</style>
|
88
app/templates/assiduites/toast.j2
Normal file
88
app/templates/assiduites/toast.j2
Normal file
@ -0,0 +1,88 @@
|
||||
<div class="toast-holder">
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toast-holder {
|
||||
position: fixed;
|
||||
right: 1vw;
|
||||
top: 5vh;
|
||||
height: 80vh;
|
||||
width: 20vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
}
|
||||
|
||||
.toast {
|
||||
margin: 0.5vh 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
padding: 7px;
|
||||
z-index: 250;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
}
|
||||
|
||||
.fadeOut {
|
||||
animation: fadeOut 0.5s ease-in;
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
function generateToast(content, color = "#12d3a5", ttl = 5) {
|
||||
const toast = document.createElement('div')
|
||||
toast.classList.add('toast', 'fadeIn')
|
||||
|
||||
const toastContent = document.createElement('div')
|
||||
toastContent.classList.add('toast-content')
|
||||
toastContent.appendChild(content)
|
||||
|
||||
toast.style.backgroundColor = color;
|
||||
|
||||
setTimeout(() => { toast.classList.replace('fadeIn', 'fadeOut') }, Math.max(0, ttl * 1000 - 500))
|
||||
setTimeout(() => { toast.remove() }, Math.max(0, ttl * 1000))
|
||||
toast.appendChild(toastContent)
|
||||
return toast
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
2
app/templates/sidebar.j2
Normal file → Executable file
2
app/templates/sidebar.j2
Normal file → Executable file
@ -60,7 +60,7 @@
|
||||
{% endif %}
|
||||
<ul>
|
||||
{% if current_user.has_permission(sco.Permission.ScoAbsChange) %}
|
||||
<li><a href="{{ url_for('absences.SignaleAbsenceEtud', scodoc_dept=g.scodoc_dept,
|
||||
<li><a href="{{ url_for('assiduites.signal_assiduites_etud', scodoc_dept=g.scodoc_dept,
|
||||
etudid=sco.etud.id) }}">Ajouter</a></li>
|
||||
<li><a href="{{ url_for('absences.JustifAbsenceEtud', scodoc_dept=g.scodoc_dept,
|
||||
etudid=sco.etud.id) }}">Justifier</a></li>
|
||||
|
@ -23,6 +23,7 @@ scolar_bp = Blueprint("scolar", __name__)
|
||||
notes_bp = Blueprint("notes", __name__)
|
||||
users_bp = Blueprint("users", __name__)
|
||||
absences_bp = Blueprint("absences", __name__)
|
||||
assiduites_bp = Blueprint("assiduites", __name__)
|
||||
|
||||
|
||||
# Cette fonction est bien appelée avant toutes les requêtes
|
||||
@ -107,6 +108,7 @@ class ScoData:
|
||||
|
||||
from app.views import (
|
||||
absences,
|
||||
assiduites,
|
||||
but_formation,
|
||||
notes_formsemestre,
|
||||
notes,
|
||||
|
377
app/views/assiduites.py
Normal file
377
app/views/assiduites.py
Normal file
@ -0,0 +1,377 @@
|
||||
import datetime
|
||||
|
||||
from flask import g, request, render_template
|
||||
|
||||
from flask import abort, url_for
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.decorators import (
|
||||
scodoc,
|
||||
permission_required,
|
||||
)
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.views import assiduites_bp as bp
|
||||
from app.views import ScoData
|
||||
|
||||
# ---------------
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_find_etud
|
||||
from flask_login import current_user
|
||||
|
||||
|
||||
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
||||
|
||||
# --- UTILS ---
|
||||
|
||||
|
||||
class HTMLElement:
|
||||
""""""
|
||||
|
||||
|
||||
class HTMLElement:
|
||||
"""Représentation d'un HTMLElement version Python"""
|
||||
|
||||
def __init__(self, tag: str, *attr, **kattr) -> None:
|
||||
self.tag: str = tag
|
||||
self.children: list[HTMLElement] = []
|
||||
self.self_close: bool = kattr.get("self_close", False)
|
||||
self.text_content: str = kattr.get("text_content", "")
|
||||
self.key_attributes: dict[str, any] = kattr
|
||||
self.attributes: list[str] = list(attr)
|
||||
|
||||
def add(self, *child: HTMLElement) -> None:
|
||||
"""add child element to self"""
|
||||
for kid in child:
|
||||
self.children.append(kid)
|
||||
|
||||
def remove(self, child: HTMLElement) -> None:
|
||||
"""Remove child element from self"""
|
||||
if child in self.children:
|
||||
self.children.remove(child)
|
||||
|
||||
def __str__(self) -> str:
|
||||
attr: list[str] = self.attributes
|
||||
|
||||
for att, val in self.key_attributes.items():
|
||||
if att in ("self_close", "text_content"):
|
||||
continue
|
||||
|
||||
if att != "cls":
|
||||
attr.append(f'{att}="{val}"')
|
||||
else:
|
||||
attr.append(f'class="{val}"')
|
||||
|
||||
if not self.self_close:
|
||||
head: str = f"<{self.tag} {' '.join(attr)}>{self.text_content}"
|
||||
body: str = "\n".join(map(str, self.children))
|
||||
foot: str = f"</{self.tag}>"
|
||||
return head + body + foot
|
||||
return f"<{self.tag} {' '.join(attr)}/>"
|
||||
|
||||
def __add__(self, other: str):
|
||||
return str(self) + other
|
||||
|
||||
def __radd__(self, other: str):
|
||||
return other + str(self)
|
||||
|
||||
|
||||
class HTMLStringElement(HTMLElement):
|
||||
"""Utilisation d'une chaine de caracètres pour représenter un element"""
|
||||
|
||||
def __init__(self, text: str) -> None:
|
||||
self.text: str = text
|
||||
HTMLElement.__init__(self, "textnode")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.text
|
||||
|
||||
|
||||
class HTMLBuilder:
|
||||
def __init__(self, *content: HTMLElement or str) -> None:
|
||||
self.content: list[HTMLElement or str] = list(content)
|
||||
|
||||
def add(self, *element: HTMLElement or str):
|
||||
self.content.extend(element)
|
||||
|
||||
def remove(self, element: HTMLElement or str):
|
||||
if element in self.content:
|
||||
self.content.remove(element)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "\n".join(map(str, self.content))
|
||||
|
||||
def build(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
#
|
||||
# Assiduités (/ScoDoc/<dept>/Scolarite/Assiduites/...)
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@bp.route("/index_html")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def index_html():
|
||||
"""Gestionnaire assiduités, page principale"""
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Saisie des assiduités",
|
||||
cssstyles=["css/calabs.css"],
|
||||
javascripts=["js/calabs.js"],
|
||||
),
|
||||
"""<h2>Traitement des assiduités</h2>
|
||||
<p class="help">
|
||||
Pour saisir des assiduités ou consulter les états, il est recommandé par passer par
|
||||
le semestre concerné (saisie par jours nommés ou par semaines).
|
||||
</p>
|
||||
""",
|
||||
]
|
||||
H.append(
|
||||
"""<p class="help">Pour signaler, annuler ou justifier une assiduité pour un seul étudiant,
|
||||
choisissez d'abord concerné:</p>"""
|
||||
)
|
||||
H.append(sco_find_etud.form_search_etud())
|
||||
if current_user.has_permission(
|
||||
Permission.ScoAbsChange
|
||||
) and sco_preferences.get_preference("handle_billets_abs"):
|
||||
H.append(
|
||||
f"""
|
||||
<h2 style="margin-top: 30px;">Billets d'absence</h2>
|
||||
<ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
|
||||
}">Traitement des billets d'absence en attente</a>
|
||||
</li></ul>
|
||||
"""
|
||||
)
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
@bp.route("/SignaleAssiduiteEtud")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoAbsChange)
|
||||
def signal_assiduites_etud():
|
||||
"""
|
||||
signal_assiduites_etud Saisie de l'assiduité d'un étudiant
|
||||
|
||||
Args:
|
||||
etudid (int): l'identifiant de l'étudiant
|
||||
|
||||
Returns:
|
||||
str: l'html généré
|
||||
"""
|
||||
|
||||
etudid = request.args.get("etudid", -1)
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
if etud.dept_id != g.scodoc_dept_id:
|
||||
abort(404, "étudiant inexistant dans ce département")
|
||||
|
||||
header: str = html_sco_header.sco_header(
|
||||
page_title="Saisie Assiduités",
|
||||
init_qtip=True,
|
||||
javascripts=[
|
||||
"js/assiduites.js",
|
||||
"libjs/moment.new.min.js",
|
||||
"libjs/moment-timezone.js",
|
||||
],
|
||||
cssstyles=CSSSTYLES
|
||||
+ [
|
||||
"css/assiduites.css",
|
||||
],
|
||||
)
|
||||
|
||||
return HTMLBuilder(
|
||||
header,
|
||||
render_template("assiduites/minitimeline.j2"),
|
||||
render_template(
|
||||
"assiduites/signal_assiduites_etud.j2",
|
||||
sco=ScoData(etud),
|
||||
date=datetime.date.today().isoformat(),
|
||||
),
|
||||
).build()
|
||||
|
||||
|
||||
@bp.route("/SignalAssiduiteGr")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoAbsChange)
|
||||
def signal_assiduites_group():
|
||||
"""
|
||||
signal_assiduites_group Saisie des assiduités des groupes pour le jour donnée
|
||||
|
||||
Returns:
|
||||
str: l'html généré
|
||||
"""
|
||||
formsemestre_id: int = request.args.get("formsemestre_id", -1)
|
||||
moduleimpl_id: int = request.args.get("moduleimpl_id")
|
||||
group_ids: list[int] = request.args.get("group_ids", None)
|
||||
|
||||
formsemestre_id: int = request.args.get("formsemestre_id", -1)
|
||||
moduleimpl_id: int = request.args.get("moduleimpl_id")
|
||||
date: str = request.args.get("jour", datetime.date.today().isoformat())
|
||||
group_ids: list[int] = request.args.get("group_ids", None)
|
||||
|
||||
if group_ids is None:
|
||||
group_ids = []
|
||||
else:
|
||||
group_ids = group_ids.split(",")
|
||||
map(str, group_ids)
|
||||
|
||||
# Vérification du moduleimpl_id
|
||||
try:
|
||||
moduleimpl_id = int(moduleimpl_id)
|
||||
except (TypeError, ValueError):
|
||||
moduleimpl_id = None
|
||||
# Vérification du formsemestre_id
|
||||
try:
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
except (TypeError, ValueError):
|
||||
formsemestre_id = None
|
||||
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||
group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
|
||||
)
|
||||
|
||||
# Aucun étudiant WIP
|
||||
if not groups_infos.members:
|
||||
return (
|
||||
html_sco_header.sco_header(page_title="Saisie journalière des Assiduités")
|
||||
+ "<h3>Aucun étudiant ! </h3>"
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
|
||||
# --- URL DEFAULT ---
|
||||
|
||||
base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}"
|
||||
|
||||
# --- Filtrage par formsemestre ---
|
||||
formsemestre_id = groups_infos.formsemestre_id
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if formsemestre.dept_id != g.scodoc_dept_id:
|
||||
abort(404, "groupes inexistants dans ce département")
|
||||
|
||||
require_module = sco_preferences.get_preference(
|
||||
"abs_require_module", formsemestre_id
|
||||
)
|
||||
|
||||
etuds = [
|
||||
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
||||
for m in groups_infos.members
|
||||
]
|
||||
|
||||
# --- Restriction en fonction du moduleimpl_id ---
|
||||
if moduleimpl_id:
|
||||
mod_inscrits = {
|
||||
x["etudid"]
|
||||
for x in sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
moduleimpl_id=moduleimpl_id
|
||||
)
|
||||
}
|
||||
etuds_inscrits_module = [e for e in etuds if e["etudid"] in mod_inscrits]
|
||||
if etuds_inscrits_module:
|
||||
etuds = etuds_inscrits_module
|
||||
else:
|
||||
# Si aucun etudiant n'est inscrit au module choisi...
|
||||
moduleimpl_id = None
|
||||
|
||||
# --- Génération de l'HTML ---
|
||||
sem = formsemestre.to_dict()
|
||||
|
||||
if groups_infos.tous_les_etuds_du_sem:
|
||||
gr_tit = "en"
|
||||
else:
|
||||
if len(groups_infos.group_ids) > 1:
|
||||
grp = "des groupes"
|
||||
else:
|
||||
grp = "du groupe"
|
||||
gr_tit = (
|
||||
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
|
||||
)
|
||||
|
||||
header: str = html_sco_header.sco_header(
|
||||
page_title="Saisie journalière des assiduités",
|
||||
init_qtip=True,
|
||||
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
|
||||
+ [
|
||||
# Voir fonctionnement JS
|
||||
"js/etud_info.js",
|
||||
"js/abs_ajax.js",
|
||||
"js/groups_view.js",
|
||||
"js/assiduites.js",
|
||||
"libjs/moment.new.min.js",
|
||||
"libjs/moment-timezone.js",
|
||||
],
|
||||
cssstyles=CSSSTYLES
|
||||
+ [
|
||||
"css/assiduites.css",
|
||||
],
|
||||
no_side_bar=1,
|
||||
)
|
||||
|
||||
return HTMLBuilder(
|
||||
header,
|
||||
render_template("assiduites/minitimeline.j2"),
|
||||
render_template(
|
||||
"assiduites/signal_assiduites_group.j2",
|
||||
gr_tit=gr_tit,
|
||||
sem=sem["titre_num"],
|
||||
date=date,
|
||||
formsemestre_id=formsemestre_id,
|
||||
grp=sco_groups_view.menu_groups_choice(groups_infos),
|
||||
moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
|
||||
timeline=_timeline(),
|
||||
formsemestre_date_debut=str(formsemestre.date_debut),
|
||||
formsemestre_date_fin=str(formsemestre.date_fin),
|
||||
),
|
||||
html_sco_header.sco_footer(),
|
||||
).build()
|
||||
|
||||
|
||||
def _module_selector(
|
||||
formsemestre: FormSemestre, moduleimpl_id: int = None
|
||||
) -> HTMLElement:
|
||||
"""
|
||||
_module_selector Génère un HTMLSelectElement à partir des moduleimpl du formsemestre
|
||||
|
||||
Args:
|
||||
formsemestre (FormSemestre): Le formsemestre d'où les moduleimpls seront pris.
|
||||
|
||||
Returns:
|
||||
str: La représentation str d'un HTMLSelectElement
|
||||
"""
|
||||
|
||||
ntc: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
modimpls_list: list[dict] = []
|
||||
ues = ntc.get_ues_stat_dict()
|
||||
for ue in ues:
|
||||
modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"])
|
||||
|
||||
selected = moduleimpl_id is not None
|
||||
|
||||
modules = []
|
||||
|
||||
for modimpl in modimpls_list:
|
||||
modname: str = (
|
||||
(modimpl["module"]["code"] or "")
|
||||
+ " "
|
||||
+ (modimpl["module"]["abbrev"] or modimpl["module"]["titre"] or "")
|
||||
)
|
||||
modules.append({"moduleimpl_id": modimpl["moduleimpl_id"], "name": modname})
|
||||
|
||||
return render_template(
|
||||
"assiduites/moduleimpl_selector.j2", selected=selected, modules=modules
|
||||
)
|
||||
|
||||
|
||||
def _timeline() -> HTMLElement:
|
||||
return render_template("assiduites/timeline.j2")
|
Loading…
Reference in New Issue
Block a user