Version alpha groups auto assign #640
87
app/but/bulletin_but_court.py
Normal file
87
app/but/bulletin_but_court.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Génération bulletin BUT synthétique en une page
|
||||
|
||||
On génère du HTML. Il sera si possible traduit en PDF par weasyprint.
|
||||
|
||||
Le HTML est lui même généré à partir d'un template Jinja.
|
||||
|
||||
## Données
|
||||
|
||||
Ces données sont des objets passés au template.
|
||||
|
||||
- `etud: Identite` : l'étudiant
|
||||
- `formsemestre: FormSemestre` : le formsemestre d'où est émis ce bulletin
|
||||
- `bulletins_sem: BulletinBUT` les données bulletins pour tous les étudiants
|
||||
- `bul: dict` : le bulletin (dict, même structure que le json publié)
|
||||
- `cursus: EtudCursusBUT`: infos sur le cursus BUT (niveaux validés etc)
|
||||
- `decision_ues: dict`: `{ acronyme_ue : { 'code' : 'ADM' }}` accès aux décisions
|
||||
de jury d'UE
|
||||
- `ects_total` : nombre d'ECTS validées dans ce cursus
|
||||
- `ue_validation_by_niveau : dict` : les validations d'UE de chaque niveau du cursus
|
||||
"""
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from flask import render_template, url_for
|
||||
from flask import g, request
|
||||
|
||||
from app.but.bulletin_but import BulletinBUT
|
||||
from app.but import cursus_but, validations_view
|
||||
from app.decorators import (
|
||||
scodoc,
|
||||
permission_required,
|
||||
)
|
||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.views import notes_bp as bp
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
@bp.route("/bulletin_but/<int:formsemestre_id>/<int:etudid>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def bulletin_but(formsemestre_id: int, etudid: int = None):
|
||||
"""Page HTML affichant le bulletin BUT simplifié"""
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
formsemestre: FormSemestre = (
|
||||
FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
.join(FormSemestreInscription)
|
||||
.filter_by(etudid=etudid)
|
||||
.first_or_404()
|
||||
)
|
||||
bulletins_sem = BulletinBUT(formsemestre)
|
||||
bul = bulletins_sem.bulletin_etud(etud, formsemestre) # dict
|
||||
decision_ues = {x["acronyme"]: x for x in bul["semestre"]["decision_ue"]}
|
||||
cursus = cursus_but.EtudCursusBUT(etud, formsemestre.formation)
|
||||
refcomp = formsemestre.formation.referentiel_competence
|
||||
if refcomp is None:
|
||||
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
|
||||
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
|
||||
refcomp, etud
|
||||
)
|
||||
ects_total = sum((v.ects() for v in ue_validation_by_niveau.values()))
|
||||
|
||||
logo = find_logo(logoname="header", dept_id=g.scodoc_dept_id)
|
||||
|
||||
return render_template(
|
||||
"but/bulletin_court_page.j2",
|
||||
bul=bul,
|
||||
bulletins_sem=bulletins_sem,
|
||||
cursus=cursus,
|
||||
datetime=datetime,
|
||||
decision_ues=decision_ues,
|
||||
ects_total=ects_total,
|
||||
etud=etud,
|
||||
formsemestre=formsemestre,
|
||||
logo=logo,
|
||||
sco=ScoData(formsemestre=formsemestre, etud=etud),
|
||||
time=time,
|
||||
ue_validation_by_niveau=ue_validation_by_niveau,
|
||||
)
|
|
@ -6,7 +6,7 @@ from PIL import Image as PILImage
|
|||
|
||||
def ImageScale(img_file, maxx, maxy):
|
||||
im = PILImage.open(img_file)
|
||||
im.thumbnail((maxx, maxy), PILImage.ANTIALIAS)
|
||||
im.thumbnail((maxx, maxy), PILImage.LANCZOS)
|
||||
out_file_str = io.BytesIO()
|
||||
im.save(out_file_str, im.format)
|
||||
out_file_str.seek(0)
|
||||
|
@ -20,7 +20,7 @@ def ImageScaleH(img_file, W=None, H=90):
|
|||
if W is None:
|
||||
# keep aspect
|
||||
W = int((im.size[0] * H) / float(im.size[1]))
|
||||
im.thumbnail((W, H), PILImage.ANTIALIAS)
|
||||
im.thumbnail((W, H), PILImage.LANCZOS)
|
||||
out_file_str = io.BytesIO()
|
||||
im.save(out_file_str, im.format)
|
||||
out_file_str.seek(0)
|
||||
|
|
|
@ -426,6 +426,7 @@ def dict_decision_jury(
|
|||
etud: Identite, formsemestre: FormSemestre, with_decisions: bool = False
|
||||
) -> dict:
|
||||
"""dict avec decision pour bulletins json
|
||||
- autorisation_inscription
|
||||
- decision : décision semestre
|
||||
- decision_ue : list des décisions UE
|
||||
- situation
|
||||
|
@ -511,7 +512,10 @@ def dict_decision_jury(
|
|||
d["autorisation_inscription"] = []
|
||||
for aut in decision["autorisations"]:
|
||||
d["autorisation_inscription"].append(
|
||||
dict(semestre_id=aut["semestre_id"])
|
||||
dict(
|
||||
semestre_id=aut["semestre_id"],
|
||||
date=aut["date"].isoformat() if aut["date"] else None,
|
||||
)
|
||||
)
|
||||
else:
|
||||
d["decision"] = dict(code="", etat="DEM")
|
||||
|
|
|
@ -33,7 +33,6 @@ avec `ext` membre de LOGOS_IMAGES_ALLOWED_TYPES (= jpg, png)
|
|||
SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos
|
||||
"""
|
||||
import glob
|
||||
import imghdr
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
|
@ -41,6 +40,7 @@ from pathlib import Path
|
|||
|
||||
from flask import current_app, url_for
|
||||
from PIL import Image as PILImage
|
||||
import puremagic
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app import log
|
||||
|
@ -51,99 +51,6 @@ from app.scodoc.sco_exceptions import ScoValueError
|
|||
GLOBAL = "_" # category for server level logos
|
||||
|
||||
|
||||
def find_logo(logoname, dept_id=None, strict=False, prefix=scu.LOGO_FILE_PREFIX):
|
||||
"""
|
||||
"Recherche un logo 'name' existant.
|
||||
Deux strategies:
|
||||
si strict:
|
||||
reherche uniquement dans le département puis si non trouvé au niveau global
|
||||
sinon
|
||||
On recherche en local au dept d'abord puis si pas trouvé recherche globale
|
||||
quelque soit la stratégie, retourne None si pas trouvé
|
||||
:param logoname: le nom recherche
|
||||
:param dept_id: l'id du département dans lequel se fait la recherche (None si global)
|
||||
:param strict: stratégie de recherche (strict = False => dept ou global)
|
||||
:param prefix: le prefix utilisé (parmi scu.LOGO_FILE_PREFIX / scu.BACKGROUND_FILE_PREFIX)
|
||||
:return: un objet Logo désignant le fichier image trouvé (ou None)
|
||||
"""
|
||||
logo = Logo(logoname, dept_id, prefix).select()
|
||||
if logo is None and not strict:
|
||||
logo = Logo(logoname=logoname, dept_id=None, prefix=prefix).select()
|
||||
return logo
|
||||
|
||||
|
||||
def delete_logo(name, dept_id=None):
|
||||
"""Delete all files matching logo (dept_id, name) (including all allowed extensions)
|
||||
Args:
|
||||
name: The name of the logo
|
||||
dept_id: the dept_id (if local). Use None to destroy globals logos
|
||||
"""
|
||||
logo = find_logo(logoname=name, dept_id=dept_id)
|
||||
while logo is not None:
|
||||
os.unlink(logo.select().filepath)
|
||||
logo = find_logo(logoname=name, dept_id=dept_id)
|
||||
|
||||
|
||||
def write_logo(stream, name, dept_id=None):
|
||||
"""Crée le fichier logo sur le serveur.
|
||||
Le suffixe du fichier (parmi LOGO_IMAGES_ALLOWED_TYPES) est déduit du contenu du stream"""
|
||||
Logo(logoname=name, dept_id=dept_id).create(stream)
|
||||
|
||||
|
||||
def rename_logo(old_name, new_name, dept_id):
|
||||
logo = find_logo(old_name, dept_id, True)
|
||||
logo.rename(new_name)
|
||||
|
||||
|
||||
def list_logos():
|
||||
"""Crée l'inventaire de tous les logos existants.
|
||||
L'inventaire se présente comme un dictionnaire de dictionnaire de Logo:
|
||||
[None][name] pour les logos globaux
|
||||
[dept_id][name] pour les logos propres à un département (attention id numérique du dept)
|
||||
Les départements sans logos sont absents du résultat
|
||||
"""
|
||||
inventory = {None: _list_dept_logos()} # logos globaux (header / footer)
|
||||
for dept in Departement.query.filter_by(visible=True).all():
|
||||
logos_dept = _list_dept_logos(dept_id=dept.id)
|
||||
if logos_dept:
|
||||
inventory[dept.id] = _list_dept_logos(dept.id)
|
||||
return inventory
|
||||
|
||||
|
||||
def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
|
||||
"""Inventorie toutes les images existantes pour un niveau (GLOBAL ou un département).
|
||||
retourne un dictionnaire de Logo [logoname] -> Logo
|
||||
les noms des fichiers concernés doivent être de la forme: <rep>/<prefix><name>.<suffixe>
|
||||
<rep> : répertoire de recherche (déduit du dept_id)
|
||||
<prefix>: le prefix (LOGO_FILE_PREFIX pour les logos)
|
||||
<suffix>: un des suffixes autorisés
|
||||
:param dept_id: l'id du departement concerné (si None -> global)
|
||||
:param prefix: le préfixe utilisé
|
||||
:return: le résultat de la recherche ou None si aucune image trouvée
|
||||
"""
|
||||
allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES)
|
||||
# parse filename 'logo_<logoname>.<ext> . be carefull: logoname may include '.'
|
||||
filename_parser = re.compile(f"{prefix}(([^.]*.)+)({allowed_ext})")
|
||||
logos = {}
|
||||
path_dir = Path(scu.SCODOC_LOGOS_DIR)
|
||||
if dept_id:
|
||||
path_dir = Path(
|
||||
os.path.sep.join(
|
||||
[scu.SCODOC_LOGOS_DIR, scu.LOGOS_DIR_PREFIX + str(dept_id)]
|
||||
)
|
||||
)
|
||||
if path_dir.exists():
|
||||
for entry in path_dir.iterdir():
|
||||
if os.access(path_dir.joinpath(entry).absolute(), os.R_OK):
|
||||
result = filename_parser.match(entry.name)
|
||||
if result:
|
||||
logoname = result.group(1)[
|
||||
:-1
|
||||
] # retreive logoname from filename (less final dot)
|
||||
logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select()
|
||||
return logos if len(logos.keys()) > 0 else None
|
||||
|
||||
|
||||
class Logo:
|
||||
"""Responsable des opérations (select, create), du calcul des chemins et url
|
||||
ainsi que de la récupération des informations sur un logo.
|
||||
|
@ -212,7 +119,7 @@ class Logo:
|
|||
def create(self, stream):
|
||||
img_type = guess_image_type(stream)
|
||||
if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES:
|
||||
raise ScoValueError("type d'image invalide")
|
||||
raise ScoValueError(f"type d'image invalide ({img_type})")
|
||||
self._set_format(img_type)
|
||||
self._ensure_directory_exists()
|
||||
filename = self.basepath + "." + self.suffix
|
||||
|
@ -310,14 +217,118 @@ class Logo:
|
|||
)
|
||||
old_path.rename(new_path)
|
||||
|
||||
def html(self) -> str:
|
||||
"élément HTML img affichant ce logo"
|
||||
return f"""<img class="sco_logo" src="{self.get_url()}" alt="Logo {self.logoname}">"""
|
||||
|
||||
|
||||
def find_logo(
|
||||
logoname: str,
|
||||
dept_id: int | None = None,
|
||||
strict: bool = False,
|
||||
prefix: str = scu.LOGO_FILE_PREFIX,
|
||||
) -> Logo | None:
|
||||
"""
|
||||
"Recherche un logo 'name' existant.
|
||||
Deux strategies:
|
||||
si strict:
|
||||
recherche uniquement dans le département puis si non trouvé au niveau global
|
||||
sinon
|
||||
On recherche en local au dept d'abord puis si pas trouvé recherche globale
|
||||
quelque soit la stratégie, retourne None si pas trouvé
|
||||
:param logoname: le nom recherche
|
||||
:param dept_id: l'id du département dans lequel se fait la recherche (None si global)
|
||||
:param strict: stratégie de recherche (strict = False => dept ou global)
|
||||
:param prefix: le prefix utilisé (parmi scu.LOGO_FILE_PREFIX / scu.BACKGROUND_FILE_PREFIX)
|
||||
:return: un objet Logo désignant le fichier image trouvé (ou None)
|
||||
"""
|
||||
logo = Logo(logoname, dept_id, prefix).select()
|
||||
if logo is None and not strict:
|
||||
logo = Logo(logoname=logoname, dept_id=None, prefix=prefix).select()
|
||||
return logo
|
||||
|
||||
|
||||
def delete_logo(name, dept_id=None):
|
||||
"""Delete all files matching logo (dept_id, name) (including all allowed extensions)
|
||||
Args:
|
||||
name: The name of the logo
|
||||
dept_id: the dept_id (if local). Use None to destroy globals logos
|
||||
"""
|
||||
logo = find_logo(logoname=name, dept_id=dept_id)
|
||||
while logo is not None:
|
||||
os.unlink(logo.select().filepath)
|
||||
logo = find_logo(logoname=name, dept_id=dept_id)
|
||||
|
||||
|
||||
def write_logo(stream, name, dept_id=None):
|
||||
"""Crée le fichier logo sur le serveur.
|
||||
Le suffixe du fichier (parmi LOGO_IMAGES_ALLOWED_TYPES) est déduit du contenu du stream
|
||||
"""
|
||||
Logo(logoname=name, dept_id=dept_id).create(stream)
|
||||
|
||||
|
||||
def rename_logo(old_name, new_name, dept_id):
|
||||
logo = find_logo(old_name, dept_id, True)
|
||||
logo.rename(new_name)
|
||||
|
||||
|
||||
def list_logos():
|
||||
"""Crée l'inventaire de tous les logos existants.
|
||||
L'inventaire se présente comme un dictionnaire de dictionnaire de Logo:
|
||||
[None][name] pour les logos globaux
|
||||
[dept_id][name] pour les logos propres à un département (attention id numérique du dept)
|
||||
Les départements sans logos sont absents du résultat
|
||||
"""
|
||||
inventory = {None: _list_dept_logos()} # logos globaux (header / footer)
|
||||
for dept in Departement.query.filter_by(visible=True).all():
|
||||
logos_dept = _list_dept_logos(dept_id=dept.id)
|
||||
if logos_dept:
|
||||
inventory[dept.id] = _list_dept_logos(dept.id)
|
||||
return inventory
|
||||
|
||||
|
||||
def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
|
||||
"""Inventorie toutes les images existantes pour un niveau (GLOBAL ou un département).
|
||||
retourne un dictionnaire de Logo [logoname] -> Logo
|
||||
les noms des fichiers concernés doivent être de la forme: <rep>/<prefix><name>.<suffixe>
|
||||
<rep> : répertoire de recherche (déduit du dept_id)
|
||||
<prefix>: le prefix (LOGO_FILE_PREFIX pour les logos)
|
||||
<suffix>: un des suffixes autorisés
|
||||
:param dept_id: l'id du departement concerné (si None -> global)
|
||||
:param prefix: le préfixe utilisé
|
||||
:return: le résultat de la recherche ou None si aucune image trouvée
|
||||
"""
|
||||
allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES)
|
||||
# parse filename 'logo_<logoname>.<ext> . be carefull: logoname may include '.'
|
||||
filename_parser = re.compile(f"{prefix}(([^.]*.)+)({allowed_ext})")
|
||||
logos = {}
|
||||
path_dir = Path(scu.SCODOC_LOGOS_DIR)
|
||||
if dept_id:
|
||||
path_dir = Path(
|
||||
os.path.sep.join(
|
||||
[scu.SCODOC_LOGOS_DIR, scu.LOGOS_DIR_PREFIX + str(dept_id)]
|
||||
)
|
||||
)
|
||||
if path_dir.exists():
|
||||
for entry in path_dir.iterdir():
|
||||
if os.access(path_dir.joinpath(entry).absolute(), os.R_OK):
|
||||
result = filename_parser.match(entry.name)
|
||||
if result:
|
||||
logoname = result.group(1)[
|
||||
:-1
|
||||
] # retreive logoname from filename (less final dot)
|
||||
logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select()
|
||||
return logos if len(logos.keys()) > 0 else None
|
||||
|
||||
|
||||
def guess_image_type(stream) -> str:
|
||||
"guess image type from header in stream"
|
||||
header = stream.read(512)
|
||||
stream.seek(0)
|
||||
fmt = imghdr.what(None, header)
|
||||
if not fmt:
|
||||
ext = puremagic.from_stream(stream)
|
||||
if not ext or not ext.startswith("."):
|
||||
return None
|
||||
fmt = ext[1:] # remove leading .
|
||||
if fmt == "jfif":
|
||||
fmt = "jpg"
|
||||
return fmt if fmt != "jpeg" else "jpg"
|
||||
|
||||
|
||||
|
|
|
@ -338,7 +338,7 @@ def scale_height(img, W=None, H=REDUCED_HEIGHT):
|
|||
if W is None:
|
||||
# keep aspect
|
||||
W = int((img.size[0] * H) / img.size[1])
|
||||
img.thumbnail((W, H), PILImage.ANTIALIAS)
|
||||
img.thumbnail((W, H), PILImage.LANCZOS)
|
||||
return img
|
||||
|
||||
|
||||
|
|
124
app/static/css/bulletin_court.css
Normal file
124
app/static/css/bulletin_court.css
Normal file
|
@ -0,0 +1,124 @@
|
|||
@media print{
|
||||
body{
|
||||
width: 21cm;
|
||||
height: 29.7cm;
|
||||
}
|
||||
}
|
||||
|
||||
div.but_bul_court {
|
||||
width: 17cm;
|
||||
display: grid;
|
||||
grid-template-columns: 6cm 11cm;
|
||||
font-size: 11pt;
|
||||
}
|
||||
|
||||
#infos_etudiant {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
border-radius: 3mm;
|
||||
border: 1px solid black;
|
||||
background-color: white;
|
||||
padding: 5mm;
|
||||
}
|
||||
.nom {
|
||||
font-weight: bold;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
|
||||
#logo {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
#logo img {
|
||||
text-align: right;
|
||||
height: 3cm;
|
||||
}
|
||||
|
||||
div.but_bul_court table {
|
||||
border-collapse: collapse;
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
div.but_bul_court table th,
|
||||
div.but_bul_court table td {
|
||||
background-color: white;
|
||||
border: 1px solid black; /* Thin black border between cells */
|
||||
padding: 2px 4px 2px 4px; /* Padding inside the cells */
|
||||
}
|
||||
|
||||
table td.col_ue {
|
||||
width: 18mm;
|
||||
}
|
||||
|
||||
#ues {
|
||||
grid-row: 2;
|
||||
grid-column: 1/3;
|
||||
justify-self: end;
|
||||
margin-top: 5mm;
|
||||
margin-bottom: 5mm;
|
||||
}
|
||||
|
||||
#ues tr.titre_table th {
|
||||
background-color: rgb(183,235,255);
|
||||
padding: 2mm;
|
||||
}
|
||||
|
||||
tr.titres_ues td, tr.jury td {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table.resultats_modules {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#ressources {
|
||||
grid-row: 3;
|
||||
grid-column: 1/3;
|
||||
margin-bottom: 5mm;
|
||||
width: 100%;
|
||||
}
|
||||
#ressources tr.titres_ues td:first-child {
|
||||
background-color: rgb(255, 192, 0);
|
||||
}
|
||||
#saes {
|
||||
grid-row: 4;
|
||||
grid-column: 1/3;
|
||||
margin-bottom: 5mm;
|
||||
width: 100%;
|
||||
}
|
||||
#saes tr.titres_ues td:first-child {
|
||||
background-color: rgb(176, 255, 99);
|
||||
}
|
||||
|
||||
#row_situation {
|
||||
grid-row: 5;
|
||||
grid-column: 1/3;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
#cursus_etud, #situation {
|
||||
grid-row: 1;
|
||||
}
|
||||
#situation {
|
||||
background-color: white;
|
||||
justify-self: end;
|
||||
margin-left: 1cm;
|
||||
border-radius: 3mm;
|
||||
border: 1px solid black;
|
||||
padding: 5mm;
|
||||
}
|
||||
|
||||
#footer {
|
||||
grid-row: 6;
|
||||
grid-column: 1/3;
|
||||
margin-top: 5mm;
|
||||
font-size: 9pt;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.but_bul_court .cursus_but {
|
||||
margin-left: 0px;
|
||||
}
|
162
app/templates/but/bulletin_court_page.j2
Normal file
162
app/templates/but/bulletin_court_page.j2
Normal file
|
@ -0,0 +1,162 @@
|
|||
{% extends "sco_page.j2" %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link href="{{scu.STATIC_DIR}}/css/jury_but.css" rel="stylesheet" type="text/css" />
|
||||
<link href="{{scu.STATIC_DIR}}/css/cursus_but.css" rel="stylesheet" type="text/css" />
|
||||
<link href="{{scu.STATIC_DIR}}/css/bulletin_court.css" rel="stylesheet" type="text/css" />
|
||||
{% endblock %}
|
||||
|
||||
{% macro table_modules(mod_type, title) -%}
|
||||
<table class="resultats_modules">
|
||||
<thead>
|
||||
<tr class="titre_table">
|
||||
<th colspan="2"></th>
|
||||
<th colspan="{{ bul.ues|length }}">Unités d'enseignement</th>
|
||||
</tr>
|
||||
<tr class="titres_ues">
|
||||
<td colspan="2">{{title}}</td>
|
||||
{% for ue in bul.ues %}
|
||||
<td class="col_ue">{{ue}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for mod in bul[mod_type] %}
|
||||
<tr>
|
||||
<td>{{mod}}</td>
|
||||
<td>{{bul[mod_type][mod].titre}}</td>
|
||||
{% for ue in bul.ues %}
|
||||
<td>{{
|
||||
bul.ues[ue][mod_type][mod].moyenne
|
||||
if mod in bul.ues[ue][mod_type] else ""
|
||||
}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endmacro %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="but_bul_court">
|
||||
<div id="infos_etudiant">
|
||||
<div class="nom">{{etud.nomprenom}}</div>
|
||||
<div class="formation">BUT {{formsemestre.formation.referentiel_competence.specialite}}</div>
|
||||
{% if formsemestre.etuds_inscriptions[etud.id].parcour %}
|
||||
<div class="parcours">Parcours {{formsemestre.etuds_inscriptions[etud.id].parcour.code}}</div>
|
||||
{% endif %}
|
||||
<div class="annee_scolaire">Année {{formsemestre.annee_scolaire_str()}}</div>
|
||||
<div class="semestre">Semestre {{formsemestre.semestre_id}}</div>
|
||||
</div>
|
||||
|
||||
<div id="logo">
|
||||
{% if logo %}
|
||||
{{logo.html()|safe}}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="ues">
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="titre_table">
|
||||
<th colspan="{{ 1 + bul.ues|length }}">Unités d'enseignement du semestre {{formsemestre.semestre_id}}</th>
|
||||
</tr>
|
||||
<tr class="titres_ues">
|
||||
<td></td>
|
||||
{% for ue in bul.ues %}
|
||||
<td class="col_ue">{{ue}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Moyenne</td>
|
||||
{% for ue in bul.ues %}
|
||||
<td class="col_ue">{{bul.ues[ue].moyenne.value}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bonus</td>
|
||||
{% for ue in bul.ues %}
|
||||
<td class="col_ue">{{bul.ues[ue].bonus if bul.ues[ue].bonus != "00.00" else ""}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Malus</td>
|
||||
{% for ue in bul.ues %}
|
||||
<td class="col_ue">{{bul.ues[ue].malus if bul.ues[ue].malus != "00.00" else ""}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rang</td>
|
||||
{% for ue in bul.ues %}
|
||||
<td class="col_ue">{{bul.ues[ue].moyenne.rang}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Effectif</td>
|
||||
{% for ue in bul.ues %}
|
||||
<td class="col_ue">{{bul.ues[ue].moyenne.total}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ECTS</td>
|
||||
{% for ue in bul.ues %}
|
||||
<td class="col_ue">{{bul.ues[ue].moyenne.ects}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr class="jury">
|
||||
<td>Jury</td>
|
||||
{% for ue in bul.ues %}
|
||||
<td class="col_ue">{{decision_ues[ue].code}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="ressources">
|
||||
{{ table_modules("ressources", "Ressources") }}
|
||||
</div>
|
||||
|
||||
<div id="saes">
|
||||
{{ table_modules("saes", "Situations d'Apprentissage et d'Évaluation (SAÉ)") }}
|
||||
</div>
|
||||
|
||||
<div id="row_situation">
|
||||
<div id="cursus_etud">
|
||||
{% include "but/cursus_etud.j2" %}
|
||||
</div>
|
||||
|
||||
<div id="situation">
|
||||
<div>ECTS acquis : {{ects_total}}</div>
|
||||
<div class="descr_jury">
|
||||
{% if bul.semestre.decision_annee %}
|
||||
Jury tenu le {{
|
||||
datetime.datetime.fromisoformat(bul.semestre.decision_annee.date).strftime("%d/%m/%Y à %H:%M")
|
||||
}},
|
||||
année BUT {{bul.semestre.decision_annee.code}}.
|
||||
{% endif %}
|
||||
{% set virg = joiner(", ") %}
|
||||
{% for aut in bul.semestre.autorisation_inscription -%}
|
||||
{% if loop.first %}
|
||||
Autorisé à s'inscrire en
|
||||
{% endif %}
|
||||
{{- virg() }}S{{aut.semestre_id -}}
|
||||
{%- if loop.last -%}
|
||||
.
|
||||
{%- endif -%}
|
||||
{%- endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
Bulletin généré par ScoDoc le {{time.strftime("%d/%m/%Y à %Hh%M")}}. Explication des codes sur
|
||||
<a href="https://scodoc.org/CodesJuryBUT">https://scodoc.org/CodesJuryBUT</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,7 +1,7 @@
|
|||
{# -*- mode: jinja-html -*- #}
|
||||
{# Description un semestre (barre de menu et infos) #}
|
||||
<!-- formsemestre_header -->
|
||||
<div class="formsemestre_page_title">
|
||||
<div class="formsemestre_page_title noprint">
|
||||
<div class="infos">
|
||||
<span class="semtitle"><a class="stdlink" title="{{sco.sem.session_id()}}" href="{{
|
||||
url_for('notes.formsemestre_status',
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{# Element HTML decrivant un semestre (barre de menu et infos) #}
|
||||
{# was formsemestre_page_title #}
|
||||
|
||||
<div class="formsemestre_page_title">
|
||||
<div class="formsemestre_page_title noprint">
|
||||
<div class="infos">
|
||||
<span class="semtitle"><a class="stdlink" title="{{formsemestre.session_id()}}" href="{{url_for('notes.formsemestre_status',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)}}">{{formsemestre.titre}}</a>
|
||||
|
|
|
@ -35,7 +35,7 @@ from operator import itemgetter
|
|||
import time
|
||||
|
||||
import flask
|
||||
from flask import abort, flash, redirect, render_template, url_for
|
||||
from flask import flash, redirect, render_template, url_for
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
|
@ -44,6 +44,7 @@ from app import models
|
|||
from app.auth.models import User
|
||||
from app.but import (
|
||||
apc_edit_ue,
|
||||
bulletin_but_court,
|
||||
cursus_but,
|
||||
jury_edit_manual,
|
||||
jury_but,
|
||||
|
@ -58,7 +59,6 @@ from app.comp import jury, res_sem
|
|||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import (
|
||||
Formation,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarNews,
|
||||
Scolog,
|
||||
|
|
|
@ -59,6 +59,7 @@ Pillow==10.0.0
|
|||
platformdirs==3.10.0
|
||||
pluggy==1.2.0
|
||||
psycopg2==2.9.6
|
||||
puremagic==1.15
|
||||
py==1.11.0
|
||||
pycparser==2.21
|
||||
pydot==1.4.2
|
||||
|
|
|
@ -3,5 +3,5 @@ Version: x.y.z
|
|||
Architecture: amd64
|
||||
Maintainer: Emmanuel Viennet <emmanuel@viennet.net>
|
||||
Description: ScoDoc 9
|
||||
Un logiciel pour le suivi de la scolarité universitaire.
|
||||
Depends: adduser, curl, gcc, graphviz, graphviz-dev, libpq-dev, postfix|exim4, cracklib-runtime, libcrack2-dev, python3-dev, python3-venv, python3-pip, python3-wheel, nginx, postgresql, libpq-dev, redis, ufw
|
||||
Un logiciel pour le suivi de la scolarité universitaire.
|
||||
Depends: adduser, curl, gcc, graphviz, graphviz-dev, libpq-dev, postfix|exim4, cracklib-runtime, libcrack2-dev, libpango-1.0-0, pango1.0-tools, python3-dev, python3-venv, python3-pip, python3-wheel, nginx, postgresql, libpq-dev, redis, ufw
|
||||
|
|
Loading…
Reference in New Issue
Block a user