Merge branch 'master' into feuille_jury

This commit is contained in:
Jean-Marie Place 2023-06-09 07:28:01 +02:00
commit d31d24388f
50 changed files with 1853 additions and 639 deletions

View File

@ -8,16 +8,17 @@
API : accès aux étudiants
"""
from datetime import datetime
from operator import attrgetter
from flask import g, request
from flask_json import as_json
from flask_login import current_user
from flask_login import login_required
from sqlalchemy import desc, or_
from sqlalchemy import desc, func, or_
from sqlalchemy.dialects.postgresql import VARCHAR
import app
from app.api import api_bp as bp, api_web_bp
from app.scodoc.sco_utils import json_error
from app.api import tools
from app.decorators import scodoc, permission_required
from app.models import (
@ -31,6 +32,8 @@ from app.scodoc import sco_bulletins
from app.scodoc import sco_groups
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error, suppress_accents
# Un exemple:
# @bp.route("/api_function/<int:arg>")
@ -164,12 +167,39 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
)
if not None in allowed_depts:
# restreint aux départements autorisés:
etuds = etuds.join(Departement).filter(
query = query.join(Departement).filter(
or_(Departement.acronym == acronym for acronym in allowed_depts)
)
return [etud.to_dict_api() for etud in query]
@bp.route("/etudiants/name/<string:start>")
@api_web_bp.route("/etudiants/name/<string:start>")
@scodoc
@permission_required(Permission.ScoView)
@as_json
def etudiants_by_name(start: str = "", min_len=3, limit=32):
"""Liste des étudiants dont le nom débute par start.
Si start fait moins de min_len=3 caractères, liste vide.
La casse et les accents sont ignorés.
"""
if len(start) < min_len:
return []
start = suppress_accents(start).lower()
query = Identite.query.filter(
func.lower(func.unaccent(Identite.nom, type_=VARCHAR)).ilike(start + "%")
)
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
if not None in allowed_depts:
# restreint aux départements autorisés:
query = query.join(Departement).filter(
or_(Departement.acronym == acronym for acronym in allowed_depts)
)
etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
# Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))]
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
@bp.route("/etudiant/nip/<string:nip>/formsemestres")
@bp.route("/etudiant/ine/<string:ine>/formsemestres")

View File

@ -8,7 +8,7 @@
ScoDoc 9 API : accès aux évaluations
"""
from flask import g
from flask import g, request
from flask_json import as_json
from flask_login import login_required
@ -17,7 +17,7 @@ import app
from app.api import api_bp as bp, api_web_bp
from app.decorators import scodoc, permission_required
from app.models import Evaluation, ModuleImpl, FormSemestre
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_evaluation_db, sco_saisie_notes
from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu
@ -28,7 +28,7 @@ import app.scodoc.sco_utils as scu
@scodoc
@permission_required(Permission.ScoView)
@as_json
def the_eval(evaluation_id: int):
def evaluation(evaluation_id: int):
"""Description d'une évaluation.
{
@ -93,24 +93,22 @@ def evaluations(moduleimpl_id: int):
@as_json
def evaluation_notes(evaluation_id: int):
"""
Retourne la liste des notes à partir de l'id d'une évaluation donnée
Retourne la liste des notes de l'évaluation
evaluation_id : l'id d'une évaluation
evaluation_id : l'id de l'évaluation
Exemple de résultat :
{
"1": {
"id": 1,
"etudid": 10,
"11": {
"etudid": 11,
"evaluation_id": 1,
"value": 15.0,
"comment": "",
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
"uid": 2
},
"2": {
"id": 2,
"etudid": 1,
"12": {
"etudid": 12,
"evaluation_id": 1,
"value": 12.0,
"comment": "",
@ -128,8 +126,8 @@ def evaluation_notes(evaluation_id: int):
.filter_by(dept_id=g.scodoc_dept_id)
)
the_eval = query.first_or_404()
dept = the_eval.moduleimpl.formsemestre.departement
evaluation = query.first_or_404()
dept = evaluation.moduleimpl.formsemestre.departement
app.set_sco_dept(dept.acronym)
notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
@ -137,7 +135,49 @@ def evaluation_notes(evaluation_id: int):
# "ABS", "EXC", etc mais laisse les notes sur le barème de l'éval.
note = notes[etudid]
note["value"] = scu.fmt_note(note["value"], keep_numeric=True)
note["note_max"] = the_eval.note_max
note["note_max"] = evaluation.note_max
del note["id"]
return notes
# in JS, keys must be string, not integers
return {str(etudid): note for etudid, note in notes.items()}
@bp.route("/evaluation/<int:evaluation_id>/notes/set", methods=["POST"])
@api_web_bp.route("/evaluation/<int:evaluation_id>/notes/set", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoEnsView)
@as_json
def evaluation_set_notes(evaluation_id: int):
"""Écriture de notes dans une évaluation.
The request content type should be "application/json",
and contains:
{
'notes' : [ [etudid, value], ... ],
'comment' : optional string
}
Result:
- nb_changed: nombre de notes changées
- nb_suppress: nombre de notes effacées
- etudids_with_decision: liste des etudiants dont la note a changé
alors qu'ils ont une décision de jury enregistrée.
"""
query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept:
query = (
query.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
)
evaluation = query.first_or_404()
dept = evaluation.moduleimpl.formsemestre.departement
app.set_sco_dept(dept.acronym)
data = request.get_json(force=True) # may raise 400 Bad Request
notes = data.get("notes")
if notes is None:
return scu.json_error(404, "no notes")
if not isinstance(notes, list):
return scu.json_error(404, "invalid notes argument (must be a list)")
return sco_saisie_notes.save_notes(
evaluation, notes, comment=data.get("comment", "")
)

View File

@ -18,7 +18,7 @@ import pandas as pd
from flask import g
from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.codes_cursus import UE_SPORT, UE_STANDARD
from app.scodoc.codes_cursus import CursusDUT, CursusDUTMono
from app.scodoc.sco_utils import ModuleType
@ -740,6 +740,7 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif):
name = "bonus_iut1grenoble_2017"
displayed_name = "IUT de Grenoble 1"
# C'est un bonus "multiplicatif": on l'exprime en additif,
# sur chaque moyenne d'UE m_0
# Augmenter de 5% correspond à multiplier par a=1.05
@ -782,6 +783,7 @@ class BonusIUTRennes1(BonusSportAdditif):
seuil_moy_gen = 10.0
proportion_point = 1 / 20.0
classic_use_bonus_ues = False
# S'applique aussi en classic, sur la moy. gen.
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
@ -1336,6 +1338,7 @@ class BonusStNazaire(BonusSport):
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
amplitude = 0.01 / 4 # 4pt => 1%
factor_max = 0.1 # 10% max
# Modifié 2022-11-29: calculer chaque bonus
# (de 1 à 3 modules) séparément.
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
@ -1533,6 +1536,63 @@ class BonusIUTV(BonusSportAdditif):
# c'est le bonus par défaut: aucune méthode à surcharger
# Finalement inutile: un bonus direct est mieux adapté à leurs besoins.
# # class BonusMastersUSPNIG(BonusSportAdditif):
# """Calcul bonus modules optionnels (sport, culture), règle Masters de l'Institut Galilée (USPN)
# Les étudiants peuvent suivre des enseignements optionnels
# de l'USPN (sports, musique, deuxième langue, culture, etc) dans une
# UE libre. Les points au-dessus de 10 sur 20 obtenus dans cette UE
# libre sont ajoutés au total des points obtenus pour les UE obligatoires
# du semestre concerné.
# """
# name = "bonus_masters__uspn_ig"
# displayed_name = "Masters de l'Institut Galilée (USPN)"
# proportion_point = 1.0
# seuil_moy_gen = 10.0
# def __init__(
# self,
# formsemestre: "FormSemestre",
# sem_modimpl_moys: np.array,
# ues: list,
# modimpl_inscr_df: pd.DataFrame,
# modimpl_coefs: np.array,
# etud_moy_gen,
# etud_moy_ue,
# ):
# # Pour ce bonus, il nous faut la somme des coefs des modules non bonus
# # du formsemestre (et non auxquels les étudiants sont inscrits !)
# self.sum_coefs = sum(
# [
# m.module.coefficient
# for m in formsemestre.modimpls_sorted
# if (m.module.module_type == ModuleType.STANDARD)
# and (m.module.ue.type == UE_STANDARD)
# ]
# )
# super().__init__(
# formsemestre,
# sem_modimpl_moys,
# ues,
# modimpl_inscr_df,
# modimpl_coefs,
# etud_moy_gen,
# etud_moy_ue,
# )
# # Bonus sur la moyenne générale seulement
# # On a dans bonus_moy_arr le bonus additif classique
# # Sa valeur sera appliquée comme moy_gen += bonus_moy_gen
# # or ici on veut
# # moy_gen = (somme des notes + bonus_moy_arr) / somme des coefs
# # moy_gen += bonus_moy_arr / somme des coefs
# self.bonus_moy_gen = (
# None if self.bonus_moy_gen is None else self.bonus_moy_gen / self.sum_coefs
# )
def get_bonus_class_dict(start=BonusSport, d=None):
"""Dictionnaire des classes de bonus
(liste les sous-classes de BonusSport ayant un nom)

View File

@ -288,7 +288,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
if ref_comp is None:
return set()
if parcour_id is None:
ues_ids = {ue.id for ue in self.ues}
ues_ids = {ue.id for ue in self.ues if ue.type != UE_SPORT}
else:
parcour: ApcParcours = ApcParcours.query.get(parcour_id)
annee = (self.formsemestre.semestre_id + 1) // 2
@ -306,12 +306,12 @@ class ResultatsSemestreBUT(NotesTableCompat):
return ues_ids
def etud_has_decision(self, etudid):
def etud_has_decision(self, etudid) -> bool:
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
prend aussi en compte les autorisations de passage.
Sous-classée en BUT pour les RCUEs et années.
"""
return (
return bool(
super().etud_has_decision(etudid)
or ApcValidationAnnee.query.filter_by(
formsemestre_id=self.formsemestre.id, etudid=etudid

View File

@ -283,12 +283,12 @@ class NotesTableCompat(ResultatsSemestre):
]
return etudids
def etud_has_decision(self, etudid):
def etud_has_decision(self, etudid) -> bool:
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
prend aussi en compte les autorisations de passage.
Sous-classée en BUT pour les RCUEs et années.
"""
return (
return bool(
self.get_etud_decisions_ue(etudid)
or self.get_etud_decision_sem(etudid)
or ScolarAutorisationInscription.query.filter_by(

View File

@ -36,6 +36,7 @@ from sqlalchemy import text
from wtforms import (
BooleanField,
DateField,
DecimalField,
FieldList,
FormField,
HiddenField,
@ -122,13 +123,13 @@ class EntrepriseCreationForm(FlaskForm):
origine = _build_string_field("Origine du correspondant", required=False)
notes = _build_string_field("Notes sur le correspondant", required=False)
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def validate(self):
def validate(self, extra_validators=None):
validate = True
if not FlaskForm.validate(self):
validate = False
if not super().validate(extra_validators):
return False
if EntreprisePreferences.get_check_siret() and self.siret.data != "":
siret_data = self.siret.data.strip().replace(" ", "")
@ -248,13 +249,13 @@ class SiteCreationForm(FlaskForm):
codepostal = _build_string_field("Code postal (*)")
ville = _build_string_field("Ville (*)")
pays = _build_string_field("Pays", required=False)
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def validate(self):
def validate(self, extra_validators=None):
validate = True
if not FlaskForm.validate(self):
validate = False
if not super().validate(extra_validators):
return False
site = EntrepriseSite.query.filter_by(
entreprise_id=self.hidden_entreprise_id.data, nom=self.nom.data
@ -278,10 +279,10 @@ class SiteModificationForm(FlaskForm):
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def validate(self):
def validate(self, extra_validators=None):
validate = True
if not FlaskForm.validate(self):
validate = False
if not super().validate(extra_validators):
return False
site = EntrepriseSite.query.filter(
EntrepriseSite.entreprise_id == self.hidden_entreprise_id.data,
@ -326,7 +327,7 @@ class OffreCreationForm(FlaskForm):
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"),
],
)
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def __init__(self, *args, **kwargs):
@ -344,10 +345,10 @@ class OffreCreationForm(FlaskForm):
(dept.id, dept.acronym) for dept in Departement.query.all()
]
def validate(self):
def validate(self, extra_validators=None):
validate = True
if not FlaskForm.validate(self):
validate = False
if not super().validate(extra_validators):
return False
if len(self.depts.data) < 1:
self.depts.errors.append("Choisir au moins un département")
@ -392,10 +393,10 @@ class OffreModificationForm(FlaskForm):
(dept.id, dept.acronym) for dept in Departement.query.all()
]
def validate(self):
def validate(self, extra_validators=None):
validate = True
if not FlaskForm.validate(self):
validate = False
if not super().validate(extra_validators):
return False
if len(self.depts.data) < 1:
self.depts.errors.append("Choisir au moins un département")
@ -442,10 +443,10 @@ class CorrespondantCreationForm(FlaskForm):
"Notes", required=False, render_kw={"class": "form-control"}
)
def validate(self):
def validate(self, extra_validators=None):
validate = True
if not FlaskForm.validate(self):
validate = False
if not super().validate(extra_validators):
return False
if not self.telephone.data and not self.mail.data:
msg = "Saisir un moyen de contact (mail ou téléphone)"
@ -458,13 +459,13 @@ class CorrespondantCreationForm(FlaskForm):
class CorrespondantsCreationForm(FlaskForm):
hidden_site_id = HiddenField()
correspondants = FieldList(FormField(CorrespondantCreationForm), min_entries=1)
submit = SubmitField("Envoyer")
submit = SubmitField("Enregistrer")
cancel = SubmitField("Annuler")
def validate(self):
def validate(self, extra_validators=None):
validate = True
if not FlaskForm.validate(self):
validate = False
if not super().validate(extra_validators):
return False
correspondant_list = []
for entry in self.correspondants.entries:
@ -531,10 +532,10 @@ class CorrespondantModificationForm(FlaskForm):
.all()
]
def validate(self):
def validate(self, extra_validators=None):
validate = True
if not FlaskForm.validate(self):
validate = False
if not super().validate(extra_validators):
return False
correspondant = EntrepriseCorrespondant.query.filter(
EntrepriseCorrespondant.id != self.hidden_correspondant_id.data,
@ -566,7 +567,7 @@ class ContactCreationForm(FlaskForm):
render_kw={"placeholder": "Tapez le nom de l'utilisateur"},
)
notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)])
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def validate_utilisateur(self, utilisateur):
@ -613,8 +614,9 @@ class ContactModificationForm(FlaskForm):
class StageApprentissageCreationForm(FlaskForm):
etudiant = _build_string_field(
"Étudiant (*)",
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
render_kw={"placeholder": "Tapez le nom de l'étudiant", "autocomplete": "off"},
)
etudid = HiddenField()
type_offre = SelectField(
"Type de l'offre (*)",
choices=[("Stage"), ("Alternance")],
@ -627,12 +629,12 @@ class StageApprentissageCreationForm(FlaskForm):
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
)
notes = TextAreaField("Notes")
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def validate(self):
def validate(self, extra_validators=None):
validate = True
if not FlaskForm.validate(self):
if not super().validate(extra_validators):
validate = False
if (
@ -646,64 +648,27 @@ class StageApprentissageCreationForm(FlaskForm):
return validate
def validate_etudiant(self, etudiant):
etudiant_data = etudiant.data.upper().strip()
stm = text(
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
)
etudiant = (
Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
)
def validate_etudid(self, field):
"L'etudid doit avoit été placé par le JS"
etudid = int(field.data) if field.data else None
etudiant = Identite.query.get(etudid) if etudid is not None else None
if etudiant is None:
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
raise ValidationError("Étudiant introuvable (sélectionnez dans la liste)")
class StageApprentissageModificationForm(FlaskForm):
etudiant = _build_string_field(
"Étudiant (*)",
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
)
type_offre = SelectField(
"Type de l'offre (*)",
choices=[("Stage"), ("Alternance")],
validators=[DataRequired(message=CHAMP_REQUIS)],
)
date_debut = DateField(
"Date début (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
)
date_fin = DateField(
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
)
notes = TextAreaField("Notes")
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
class FrenchFloatField(StringField):
"A field allowing to enter . or ,"
def validate(self):
validate = True
if not FlaskForm.validate(self):
validate = False
if (
self.date_debut.data
and self.date_fin.data
and self.date_debut.data > self.date_fin.data
):
self.date_debut.errors.append("Les dates sont incompatibles")
self.date_fin.errors.append("Les dates sont incompatibles")
validate = False
return validate
def validate_etudiant(self, etudiant):
etudiant_data = etudiant.data.upper().strip()
stm = text(
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
)
etudiant = (
Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
)
if etudiant is None:
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
def process_formdata(self, valuelist):
"catch incoming data"
if not valuelist:
return
try:
value = valuelist[0].replace(",", ".")
self.data = float(value)
except ValueError as exc:
self.data = None
raise ValueError(self.gettext("Not a valid decimal value.")) from exc
class TaxeApprentissageForm(FlaskForm):
@ -720,25 +685,26 @@ class TaxeApprentissageForm(FlaskForm):
],
default=int(datetime.now().strftime("%Y")),
)
montant = IntegerField(
montant = FrenchFloatField(
"Montant (*)",
validators=[
DataRequired(message=CHAMP_REQUIS),
NumberRange(
min=1,
message="Le montant doit être supérieur à 0",
),
# NumberRange(
# min=0.1,
# max=1e8,
# message="Le montant doit être supérieur à 0",
# ),
],
default=1,
)
notes = TextAreaField("Notes")
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def validate(self):
def validate(self, extra_validators=None):
validate = True
if not FlaskForm.validate(self):
validate = False
if not super().validate(extra_validators):
return False
taxe = EntrepriseTaxeApprentissage.query.filter_by(
entreprise_id=self.hidden_entreprise_id.data, annee=self.annee.data
@ -788,12 +754,12 @@ class EnvoiOffreForm(FlaskForm):
submit = SubmitField("Envoyer")
cancel = SubmitField("Annuler")
def validate(self):
def validate(self, extra_validators=None):
validate = True
list_select = True
if not FlaskForm.validate(self):
validate = False
if not super().validate(extra_validators):
return False
for entry in self.responsables.entries:
if entry.data:

View File

@ -164,7 +164,10 @@ class EntrepriseStageApprentissage(db.Model):
entreprise_id = db.Column(
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
)
etudid = db.Column(db.Integer)
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id", ondelete="CASCADE"),
)
type_offre = db.Column(db.Text)
date_debut = db.Column(db.Date)
date_fin = db.Column(db.Date)
@ -180,7 +183,7 @@ class EntrepriseTaxeApprentissage(db.Model):
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
)
annee = db.Column(db.Integer)
montant = db.Column(db.Integer)
montant = db.Column(db.Float)
notes = db.Column(db.Text)

View File

@ -28,7 +28,6 @@ from app.entreprises.forms import (
ContactCreationForm,
ContactModificationForm,
StageApprentissageCreationForm,
StageApprentissageModificationForm,
EnvoiOffreForm,
AjoutFichierForm,
TaxeApprentissageForm,
@ -239,7 +238,7 @@ def delete_validation_entreprise(entreprise_id):
text=f"Non validation de la fiche entreprise ({entreprise.nom})",
)
db.session.add(log)
flash("L'entreprise a été supprimé de la liste des entreprise à valider.")
flash("L'entreprise a été supprimée de la liste des entreprises à valider.")
return redirect(url_for("entreprises.validation"))
return render_template(
"entreprises/form_confirmation.j2",
@ -770,7 +769,7 @@ def delete_taxe_apprentissage(entreprise_id, taxe_id):
)
db.session.add(log)
db.session.commit()
flash("La taxe d'apprentissage a été supprimé de la liste.")
flash("La taxe d'apprentissage a été supprimée de la liste.")
return redirect(
url_for("entreprises.fiche_entreprise", entreprise_id=taxe.entreprise_id)
)
@ -966,7 +965,7 @@ def delete_offre(entreprise_id, offre_id):
)
db.session.add(log)
db.session.commit()
flash("L'offre a été supprimé de la fiche entreprise.")
flash("L'offre a été supprimée de la fiche entreprise.")
return redirect(
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
)
@ -1473,7 +1472,8 @@ def delete_contact(entreprise_id, contact_id):
@permission_required(Permission.RelationsEntreprisesChange)
def add_stage_apprentissage(entreprise_id):
"""
Permet d'ajouter un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
Permet d'ajouter un étudiant ayant réalisé un stage ou alternance
sur la fiche de l'entreprise
"""
entreprise = Entreprise.query.filter_by(
id=entreprise_id, visible=True
@ -1484,15 +1484,8 @@ def add_stage_apprentissage(entreprise_id):
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
)
if form.validate_on_submit():
etudiant_nomcomplet = form.etudiant.data.upper().strip()
stm = text(
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
)
etudiant = (
Identite.query.from_statement(stm)
.params(nom_prenom=etudiant_nomcomplet)
.first()
)
etudid = form.etudid.data
etudiant = Identite.query.get_or_404(etudid)
formation = etudiant.inscription_courante_date(
form.date_debut.data, form.date_fin.data
)
@ -1538,7 +1531,7 @@ def add_stage_apprentissage(entreprise_id):
@permission_required(Permission.RelationsEntreprisesChange)
def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
"""
Permet de modifier un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
Permet de modifier un étudiant ayant réalisé un stage ou alternance sur la fiche de l'entreprise
"""
stage_apprentissage = EntrepriseStageApprentissage.query.filter_by(
id=stage_apprentissage_id, entreprise_id=entreprise_id
@ -1548,21 +1541,14 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
etudiant = Identite.query.filter_by(id=stage_apprentissage.etudid).first_or_404(
description=f"etudiant {stage_apprentissage.etudid} inconnue"
)
form = StageApprentissageModificationForm()
form = StageApprentissageCreationForm()
if request.method == "POST" and form.cancel.data:
return redirect(
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
)
if form.validate_on_submit():
etudiant_nomcomplet = form.etudiant.data.upper().strip()
stm = text(
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
)
etudiant = (
Identite.query.from_statement(stm)
.params(nom_prenom=etudiant_nomcomplet)
.first()
)
etudid = form.etudid.data
etudiant = Identite.query.get_or_404(etudid)
formation = etudiant.inscription_courante_date(
form.date_debut.data, form.date_fin.data
)
@ -1577,6 +1563,7 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
formation.formsemestre.formsemestre_id if formation else None,
)
stage_apprentissage.notes = form.notes.data.strip()
db.session.add(stage_apprentissage)
log = EntrepriseHistorique(
authenticated_user=current_user.user_name,
entreprise_id=stage_apprentissage.entreprise_id,
@ -1593,7 +1580,9 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
)
)
elif request.method == "GET":
form.etudiant.data = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}"
form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} {
sco_etud.format_prenom(etudiant.prenom)}"""
form.etudid.data = etudiant.id
form.type_offre.data = stage_apprentissage.type_offre
form.date_debut.data = stage_apprentissage.date_debut
form.date_fin.data = stage_apprentissage.date_fin

View File

@ -43,8 +43,8 @@ class Identite(db.Model):
"optionnel (si present, affiché à la place du nom)"
civilite = db.Column(db.String(1), nullable=False)
# données d'état-civil. Si présent remplace les données d'usage dans les documents officiels (bulletins, PV)
# cf nomprenom_etat_civil()
# données d'état-civil. Si présent remplace les données d'usage dans les documents
# officiels (bulletins, PV): voir nomprenom_etat_civil()
civilite_etat_civil = db.Column(db.String(1), nullable=False, server_default="X")
prenom_etat_civil = db.Column(db.Text(), nullable=False, server_default="")
@ -220,7 +220,7 @@ class Identite(db.Model):
}
args_dict = {}
for key, value in args.items():
if hasattr(cls, key):
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
# compat scodoc7 (mauvaise idée de l'époque)
if key in fs_empty_stored_as_nulls and value == "":
value = None

View File

@ -145,6 +145,18 @@ class Evaluation(db.Model):
db.session.add(copy)
return copy
def is_matin(self) -> bool:
"Evaluation ayant lieu le matin (faux si pas de date)"
heure_debut_dt = self.heure_debut or datetime.time(8, 00)
# 8:00 au cas ou pas d'heure (note externe?)
return bool(self.jour) and heure_debut_dt < datetime.time(12, 00)
def is_apresmidi(self) -> bool:
"Evaluation ayant lieu l'après midi (faux si pas de date)"
heure_debut_dt = self.heure_debut or datetime.time(8, 00)
# 8:00 au cas ou pas d'heure (note externe?)
return bool(self.jour) and heure_debut_dt >= datetime.time(12, 00)
def set_default_poids(self) -> bool:
"""Initialize les poids bvers les UE à leurs valeurs par défaut
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.

View File

@ -782,6 +782,8 @@ class FormSemestre(db.Model):
Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS
et leur nom est le code du parcours (eg "Cyber").
"""
if self.formation.referentiel_competence_id is None:
return # safety net
partition = Partition.query.filter_by(
formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS
).first()
@ -805,7 +807,10 @@ class FormSemestre(db.Model):
query = (
ApcParcours.query.filter_by(code=group.group_name)
.join(ApcReferentielCompetences)
.filter_by(dept_id=g.scodoc_dept_id)
.filter_by(
dept_id=g.scodoc_dept_id,
id=self.formation.referentiel_competence_id,
)
)
if query.count() != 1:
log(

View File

@ -55,7 +55,7 @@ class Module(db.Model):
secondary=parcours_modules,
lazy="subquery",
backref=db.backref("modules", lazy=True),
order_by="ApcParcours.numero",
order_by="ApcParcours.numero, ApcParcours.code",
)
app_critiques = db.relationship(

View File

@ -56,7 +56,7 @@ class NotesNotes(db.Model):
"pour debug"
from app.models.evaluations import Evaluation
return f"""<{self.__class__.__name__} {self.id} v={self.value} {self.date.isoformat()
return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} v={self.value} {self.date.isoformat()
} {Evaluation.query.get(self.evaluation_id) if self.evaluation_id else "X" }>"""

View File

@ -58,7 +58,10 @@ class UniteEns(db.Model):
# Une UE appartient soit à tous les parcours (tronc commun), soit à un sous-ensemble
parcours = db.relationship(
ApcParcours, secondary="ue_parcours", backref=db.backref("ues", lazy=True)
ApcParcours,
secondary="ue_parcours",
backref=db.backref("ues", lazy=True),
order_by="ApcParcours.numero, ApcParcours.code",
)
# relations

View File

@ -70,7 +70,7 @@ from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import Departement, FormSemestre
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ScoPermissionDenied
from app.scodoc.sco_exceptions import ScoException, ScoPermissionDenied
from app.scodoc import html_sco_header
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_groups
@ -125,6 +125,12 @@ class BaseArchiver(object):
if not os.path.isdir(obj_dir):
log(f"creating directory {obj_dir}")
os.mkdir(obj_dir)
except FileExistsError as exc:
raise ScoException(
f"""BaseArchiver error: obj_dir={obj_dir} exists={
os.path.exists(obj_dir)
} isdir={os.path.isdir(obj_dir)}"""
) from exc
finally:
scu.GSL.release()
return obj_dir

View File

@ -62,7 +62,9 @@ def format_etud_ident(etud):
else:
etud["prenom_etat_civil"] = ""
etud["civilite_str"] = format_civilite(etud["civilite"])
etud["civilite_etat_civil_str"] = format_civilite(etud["civilite_etat_civil"])
etud["civilite_etat_civil_str"] = format_civilite(
etud.get("civilite_etat_civil", "X")
)
# Nom à afficher:
if etud["nom_usuel"]:
etud["nom_disp"] = etud["nom_usuel"]
@ -145,7 +147,7 @@ def format_civilite(civilite):
def format_etat_civil(etud: dict):
if etud["prenom_etat_civil"]:
civ = {"M": "M.", "F": "Mme", "X": ""}[etud["civilite_etat_civil"]]
civ = {"M": "M.", "F": "Mme", "X": ""}[etud.get("civilite_etat_civil", "X")]
return f'{civ} {etud["prenom_etat_civil"]} {etud["nom"]}'
else:
return etud["nomprenom"]

View File

@ -252,12 +252,11 @@ def do_evaluation_delete(evaluation_id):
def do_evaluation_get_all_notes(
evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
):
"""Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
"""Toutes les notes pour une évaluation: { etudid : { 'value' : value, 'date' : date ... }}
Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
"""
do_cache = (
filter_suppressed and table == "notes_notes" and (by_uid is None)
) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
# pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
do_cache = filter_suppressed and table == "notes_notes" and (by_uid is None)
if do_cache:
r = sco_cache.EvaluationCache.get(evaluation_id)
if r is not None:

View File

@ -433,7 +433,7 @@ def excel_simple_table(
return ws.generate()
def excel_feuille_saisie(e, titreannee, description, lines):
def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, lines):
"""Genere feuille excel pour saisie des notes.
E: evaluation (dict)
lines: liste de tuples
@ -512,18 +512,20 @@ def excel_feuille_saisie(e, titreannee, description, lines):
# description evaluation
ws.append_single_cell_row(scu.unescape_html(description), style_titres)
ws.append_single_cell_row(
"Evaluation du %s (coef. %g)" % (e["jour"], e["coefficient"]), style
"Evaluation du %s (coef. %g)"
% (evaluation.jour or "sans date", evaluation.coefficient or 0.0),
style,
)
# ligne blanche
ws.append_blank_row()
# code et titres colonnes
ws.append_row(
[
ws.make_cell("!%s" % e["evaluation_id"], style_ro),
ws.make_cell("!%s" % evaluation.id, style_ro),
ws.make_cell("Nom", style_titres),
ws.make_cell("Prénom", style_titres),
ws.make_cell("Groupe", style_titres),
ws.make_cell("Note sur %g" % e["note_max"], style_titres),
ws.make_cell("Note sur %g" % (evaluation.note_max or 0.0), style_titres),
ws.make_cell("Remarque", style_titres),
]
)

View File

@ -1333,11 +1333,18 @@ Ceci n'est possible que si :
cancelbutton="Annuler",
)
if tf[0] == 0:
if formsemestre_has_decisions_or_compensations(formsemestre):
has_decisions, message = formsemestre_has_decisions_or_compensations(
formsemestre
)
if has_decisions:
H.append(
"""<p><b>Ce semestre ne peut pas être supprimé !
(il y a des décisions de jury ou des compensations par d'autres semestres)</b>
</p>"""
f"""<p><b>Ce semestre ne peut pas être supprimé !</b></p>
<p>il y a des décisions de jury ou des compensations par d'autres semestres:
</p>
<ul>
<li>{message}</li>
</ul>
"""
)
else:
H.append(tf[1])
@ -1372,32 +1379,46 @@ def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
return flask.redirect(scu.ScoURL())
def formsemestre_has_decisions_or_compensations(formsemestre: FormSemestre):
def formsemestre_has_decisions_or_compensations(
formsemestre: FormSemestre,
) -> tuple[bool, str]:
"""True if decision de jury (sem. UE, RCUE, année) émanant de ce semestre
ou compensation de ce semestre par d'autres semestres
ou autorisations de passage.
"""
# Validations de semestre ou d'UEs
if ScolarFormSemestreValidation.query.filter_by(
nb_validations = ScolarFormSemestreValidation.query.filter_by(
formsemestre_id=formsemestre.id
).count():
return True
if ScolarFormSemestreValidation.query.filter_by(
).count()
if nb_validations:
return True, f"{nb_validations} validations de semestre ou d'UE"
nb_validations = ScolarFormSemestreValidation.query.filter_by(
compense_formsemestre_id=formsemestre.id
).count():
return True
).count()
if nb_validations:
return True, f"{nb_validations} compensations utilisées dans d'autres semestres"
# Autorisations d'inscription:
if ScolarAutorisationInscription.query.filter_by(
nb_validations = ScolarAutorisationInscription.query.filter_by(
origin_formsemestre_id=formsemestre.id
).count():
return True
).count()
if nb_validations:
return (
True,
f"{nb_validations} autorisations d'inscriptions émanant de ce semestre",
)
# Validations d'années BUT
if ApcValidationAnnee.query.filter_by(formsemestre_id=formsemestre.id).count():
return True
nb_validations = ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id
).count()
if nb_validations:
return True, f"{nb_validations} validations d'année BUT utilisant ce semestre"
# Validations de RCUEs
if ApcValidationRCUE.query.filter_by(formsemestre_id=formsemestre.id).count():
return True
return False
nb_validations = ApcValidationRCUE.query.filter_by(
formsemestre_id=formsemestre.id
).count()
if nb_validations:
return True, f"{nb_validations} validations de RCUE utilisant ce semestre"
return False, ""
def do_formsemestre_delete(formsemestre_id):

View File

@ -175,9 +175,7 @@ def do_formsemestre_demission(
)
db.session.add(event)
db.session.commit()
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > démission ou défaillance
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
if etat_new == scu.DEMISSION:
flash("Démission enregistrée")
elif etat_new == scu.DEF:
@ -210,7 +208,7 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
if nt.etud_has_decision(etudid):
raise ScoValueError(
"""désinscription impossible: l'étudiant {etud.nomprenom} a
f"""désinscription impossible: l'étudiant {etud.nomprenom} a
une décision de jury (la supprimer avant si nécessaire)"""
)

View File

@ -600,6 +600,7 @@ def formsemestre_description_table(
formsemestre: FormSemestre = FormSemestre.query.filter_by(
id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404()
is_apc = formsemestre.formation.is_apc()
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
parcours = codes_cursus.get_cursus_from_code(formsemestre.formation.type_parcours)
@ -613,7 +614,7 @@ def formsemestre_description_table(
else:
ues = formsemestre.get_ues()
columns_ids += [f"ue_{ue.id}" for ue in ues]
if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
if sco_preferences.get_preference("bul_show_ects", formsemestre_id) and not is_apc:
columns_ids += ["ects"]
columns_ids += ["Inscrits", "Responsable", "Enseignants"]
if with_evals:
@ -640,6 +641,7 @@ def formsemestre_description_table(
sum_coef = 0
sum_ects = 0
last_ue_id = None
formsemestre_parcours_ids = {p.id for p in formsemestre.parcours}
for modimpl in formsemestre.modimpls_sorted:
# Ligne UE avec ECTS:
ue = modimpl.module.ue
@ -666,7 +668,7 @@ def formsemestre_description_table(
ue_info[
f"_{k}_td_attrs"
] = f'style="background-color: {ue.color} !important;"'
if not formsemestre.formation.is_apc():
if not is_apc:
# n'affiche la ligne UE qu'en formation classique
# car l'UE de rattachement n'a pas d'intérêt en BUT
rows.append(ue_info)
@ -707,8 +709,17 @@ def formsemestre_description_table(
for ue in ues:
row[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or ""
if with_parcours:
# Intersection des parcours du module avec ceux du formsemestre
row["parcours"] = ", ".join(
sorted([pa.code for pa in modimpl.module.parcours])
[
pa.code
for pa in (
modimpl.module.parcours
if modimpl.module.parcours
else modimpl.formsemestre.parcours
)
if pa.id in formsemestre_parcours_ids
]
)
rows.append(row)

View File

@ -28,7 +28,7 @@
"""Opérations d'inscriptions aux modules (interface pour gérer options ou parcours)
"""
import collections
from operator import itemgetter
from operator import attrgetter
import flask
from flask import url_for, g, request
@ -553,8 +553,11 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
>{etud.nomprenom}</a></td>"""
)
# Parcours:
group = partition_parcours.get_etud_group(etud.id)
parcours_name = group.group_name if group else ""
if partition_parcours:
group = partition_parcours.get_etud_group(etud.id)
parcours_name = group.group_name if group else ""
else:
parcours_name = ""
H.append(f"""<td class="parcours">{parcours_name}</td>""")
# UEs:
for ue in ues:
@ -668,7 +671,7 @@ def descr_inscrs_module(moduleimpl_id, set_all, partitions):
gr.append((partition["partition_name"], grp))
#
d = []
for (partition_name, grp) in gr:
for partition_name, grp in gr:
if grp:
d.append("groupes de %s: %s" % (partition_name, ", ".join(grp)))
r = []
@ -680,25 +683,25 @@ def descr_inscrs_module(moduleimpl_id, set_all, partitions):
return False, len(ins), " et ".join(r)
def _fmt_etud_set(ins, max_list_size=7):
def _fmt_etud_set(etudids, max_list_size=7) -> str:
# max_list_size est le nombre max de noms d'etudiants listés
# au delà, on indique juste le nombre, sans les noms.
if len(ins) > max_list_size:
return "%d étudiants" % len(ins)
if len(etudids) > max_list_size:
return f"{len(etudids)} étudiants"
etuds = []
for etudid in ins:
etuds.append(sco_etud.get_etud_info(etudid=etudid, filled=True)[0])
etuds.sort(key=itemgetter("nom"))
for etudid in etudids:
etud = Identite.query.get(etudid)
if etud:
etuds.append(etud)
return ", ".join(
[
'<a class="discretelink" href="%s">%s</a>'
% (
f"""<a class="discretelink" href="{
url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
),
etud["nomprenom"],
)
for etud in etuds
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id
)
}">{etud.nomprenom}</a>"""
for etud in sorted(etuds, key=attrgetter("sort_key"))
]
)

View File

@ -337,17 +337,18 @@ def ficheEtud(etudid=None):
if not sco_permissions_check.can_suppress_annotation(a["id"]):
a["dellink"] = ""
else:
a[
"dellink"
] = '<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>' % (
etudid,
a["id"],
scu.icontag(
"delete_img",
border="0",
alt="suppress",
title="Supprimer cette annotation",
),
a["dellink"] = (
'<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>'
% (
etudid,
a["id"],
scu.icontag(
"delete_img",
border="0",
alt="suppress",
title="Supprimer cette annotation",
),
)
)
author = sco_users.user_info(a["author"])
alist.append(
@ -446,7 +447,7 @@ def ficheEtud(etudid=None):
info[
"inscriptions_mkup"
] = f"""<div class="ficheinscriptions" id="ficheinscriptions">
<div class="fichetitre">Parcours</div>{info["liste_inscriptions"]}
<div class="fichetitre">Cursus</div>{info["liste_inscriptions"]}
{info["link_bul_pdf"]} {info["link_inscrire_ailleurs"]}
</div>"""

View File

@ -489,6 +489,7 @@ def _normalize_apo_fields(infolist):
recode les champs: paiementinscription (-> booleen), datefinalisationinscription (date)
ajoute le champs 'paiementinscription_str' : 'ok', 'Non' ou '?'
ajoute les champs 'etape' (= None) et 'prenom' ('') s'ils ne sont pas présents.
ajoute le champ 'civilite_etat_civil' (='X'), et 'prenom_etat_civil' (='') si non présent.
"""
for infos in infolist:
if "paiementinscription" in infos:
@ -520,6 +521,15 @@ def _normalize_apo_fields(infolist):
if "prenom" not in infos:
infos["prenom"] = ""
if "civilite_etat_civil" not in infos:
infos["civilite_etat_civil"] = "X"
if "civilite_etat_civil" not in infos:
infos["civilite_etat_civil"] = "X"
if "prenom_etat_civil" not in infos:
infos["prenom_etat_civil"] = ""
return infolist

View File

@ -36,41 +36,49 @@ import flask
from flask import g, url_for, request
from flask_login import current_user
from app import log
from app.auth.models import User
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import Evaluation, FormSemestre
from app.models import ModuleImpl, ScolarNews
from app.models import (
Evaluation,
FormSemestre,
Module,
ModuleImpl,
NotesNotes,
ScolarNews,
)
from app.models.etudiants import Identite
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
from app import log
from app.scodoc.sco_exceptions import (
AccessDenied,
InvalidNoteValue,
NoteProcessError,
ScoGenError,
ScoBugCatcher,
ScoException,
ScoInvalidParamError,
ScoValueError,
)
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
from app.scodoc import html_sco_header, sco_users
from app.scodoc import htmlutils
from app.scodoc import sco_abs
from app.scodoc import sco_cache
from app.scodoc import sco_edit_module
from app.scodoc import sco_evaluations
from app.scodoc import sco_etud
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_evaluations
from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check
from app.scodoc import sco_undo_notes
from app.scodoc import sco_etud
import app.scodoc.notesdb as ndb
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import json_error
from app.scodoc.sco_utils import ModuleType
def convert_note_from_string(
@ -128,29 +136,30 @@ def _displayNote(val):
return val
def _check_notes(notes: list[(int, float)], evaluation: dict, mod: dict):
def _check_notes(notes: list[(int, float)], evaluation: Evaluation):
# XXX typehint : float or str
"""notes is a list of tuples (etudid, value)
mod is the module (used to ckeck type, for malus)
returns list of valid notes (etudid, float value)
and 4 lists of etudid: invalids, withoutnotes, absents, tosuppress, existingjury
and 4 lists of etudid: etudids_invalids, etudids_without_notes, etudids_absents, etudid_to_suppress
"""
note_max = evaluation["note_max"]
if mod["module_type"] in (
note_max = evaluation.note_max or 0.0
module: Module = evaluation.moduleimpl.module
if module.module_type in (
scu.ModuleType.STANDARD,
scu.ModuleType.RESSOURCE,
scu.ModuleType.SAE,
):
note_min = scu.NOTES_MIN
elif mod["module_type"] == ModuleType.MALUS:
elif module.module_type == ModuleType.MALUS:
note_min = -20.0
else:
raise ValueError("Invalid module type") # bug
L = [] # liste (etudid, note) des notes ok (ou absent)
invalids = [] # etudid avec notes invalides
withoutnotes = [] # etudid sans notes (champs vides)
absents = [] # etudid absents
tosuppress = [] # etudids avec ancienne note à supprimer
valid_notes = [] # liste (etudid, note) des notes ok (ou absent)
etudids_invalids = [] # etudid avec notes invalides
etudids_without_notes = [] # etudid sans notes (champs vides)
etudids_absents = [] # etudid absents
etudid_to_suppress = [] # etudids avec ancienne note à supprimer
for etudid, note in notes:
note = str(note).strip().upper()
@ -166,31 +175,34 @@ def _check_notes(notes: list[(int, float)], evaluation: dict, mod: dict):
note_max,
note_min=note_min,
etudid=etudid,
absents=absents,
tosuppress=tosuppress,
invalids=invalids,
absents=etudids_absents,
tosuppress=etudid_to_suppress,
invalids=etudids_invalids,
)
if not invalid:
L.append((etudid, value))
valid_notes.append((etudid, value))
else:
withoutnotes.append(etudid)
return L, invalids, withoutnotes, absents, tosuppress
etudids_without_notes.append(etudid)
return (
valid_notes,
etudids_invalids,
etudids_without_notes,
etudids_absents,
etudid_to_suppress,
)
def do_evaluation_upload_xls():
"""
Soumission d'un fichier XLS (evaluation_id, notefile)
"""
authuser = current_user
vals = scu.get_request_args()
evaluation_id = int(vals["evaluation_id"])
comment = vals["comment"]
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
# Check access
# (admin, respformation, and responsable_id)
if not sco_permissions_check.can_edit_notes(authuser, E["moduleimpl_id"]):
raise AccessDenied("Modification des notes impossible pour %s" % authuser)
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
# Check access (admin, respformation, and responsable_id)
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
#
diag, lines = sco_excel.excel_file_to_list(vals["notefile"])
try:
@ -239,14 +251,16 @@ def do_evaluation_upload_xls():
if etudid:
notes.append((etudid, val))
ni += 1
except:
except Exception as exc:
diag.append(
f"""Erreur: Ligne invalide ! (erreur ligne {ni})<br>{lines[ni]}"""
)
raise InvalidNoteValue()
raise InvalidNoteValue() from exc
# -- check values
L, invalids, withoutnotes, absents, _ = _check_notes(notes, E, M["module"])
if len(invalids):
valid_notes, invalids, withoutnotes, absents, _ = _check_notes(
notes, evaluation
)
if invalids:
diag.append(
f"Erreur: la feuille contient {len(invalids)} notes invalides</p>"
)
@ -258,37 +272,33 @@ def do_evaluation_upload_xls():
diag.append("Notes invalides pour: " + ", ".join(etudsnames))
raise InvalidNoteValue()
else:
nb_changed, nb_suppress, existing_decisions = notes_add(
authuser, evaluation_id, L, comment
etudids_changed, nb_suppress, etudids_with_decisions = notes_add(
current_user, evaluation_id, valid_notes, comment
)
# news
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[
0
]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = url_for(
module: Module = evaluation.moduleimpl.module
status_url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=mod["moduleimpl_id"],
moduleimpl_id=evaluation.moduleimpl_id,
_external=True,
)
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=M["moduleimpl_id"],
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"],
obj=evaluation.moduleimpl_id,
text=f"""Chargement notes dans <a href="{status_url}">{
module.titre or module.code}</a>""",
url=status_url,
max_frequency=30 * 60, # 30 minutes
)
msg = (
"<p>%d notes changées (%d sans notes, %d absents, %d note supprimées)</p>"
% (nb_changed, len(withoutnotes), len(absents), nb_suppress)
)
if existing_decisions:
msg += """<p class="warning">Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !</p>"""
# msg += '<p>' + str(notes) # debug
msg = f"""<p>{len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes, {
len(absents)} absents, {nb_suppress} note supprimées)
</p>"""
if etudids_with_decisions:
msg += """<p class="warning">Important: il y avait déjà des décisions de jury
enregistrées, qui sont peut-être à revoir suite à cette modification !</p>
"""
return 1, msg
except InvalidNoteValue:
@ -310,14 +320,12 @@ def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) -
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl.id):
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
# Convert and check value
L, invalids, _, _, _ = _check_notes(
[(etud.id, value)], evaluation.to_dict(), evaluation.moduleimpl.module.to_dict()
)
L, invalids, _, _, _ = _check_notes([(etud.id, value)], evaluation)
if len(invalids) == 0:
nb_changed, _, _ = notes_add(
etudids_changed, _, _ = notes_add(
current_user, evaluation.id, L, "Initialisation notes"
)
if nb_changed == 1:
if len(etudids_changed) == 1:
return True
return False # error
@ -352,9 +360,7 @@ def do_evaluation_set_missing(
if etudid not in notes_db: # pas de note
notes.append((etudid, value))
# Convert and check values
L, invalids, _, _, _ = _check_notes(
notes, evaluation.to_dict(), modimpl.module.to_dict()
)
valid_notes, invalids, _, _, _ = _check_notes(notes, evaluation)
dest_url = url_for(
"notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id
)
@ -372,13 +378,13 @@ def do_evaluation_set_missing(
"""
# Confirm action
if not dialog_confirmed:
plural = len(L) > 1
plural = len(valid_notes) > 1
return scu.confirm_dialog(
f"""<h2>Mettre toutes les notes manquantes de l'évaluation
à la valeur {value} ?</h2>
<p>Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC)
n'a été rentrée seront affectés.</p>
<p><b>{len(L)} étudiant{"s" if plural else ""} concerné{"s" if plural else ""}
<p><b>{len(valid_notes)} étudiant{"s" if plural else ""} concerné{"s" if plural else ""}
par ce changement de note.</b>
</p>
""",
@ -392,7 +398,7 @@ def do_evaluation_set_missing(
)
# ok
comment = "Initialisation notes manquantes"
nb_changed, _, _ = notes_add(current_user, evaluation_id, L, comment)
etudids_changed, _, _ = notes_add(current_user, evaluation_id, valid_notes, comment)
# news
url = url_for(
"notes.moduleimpl_status",
@ -408,7 +414,7 @@ def do_evaluation_set_missing(
)
return f"""
{ html_sco_header.sco_header() }
<h2>{nb_changed} notes changées</h2>
<h2>{len(etudids_changed)} notes changées</h2>
<ul>
<li><a class="stdlink" href="{dest_url}">
Revenir au formulaire de saisie des notes</a>
@ -454,7 +460,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
)
if not dialog_confirmed:
nb_changed, nb_suppress, existing_decisions = notes_add(
etudids_changed, nb_suppress, existing_decisions = notes_add(
current_user, evaluation_id, notes, do_it=False, check_inscription=False
)
msg = f"""<p>Confirmer la suppression des {nb_suppress} notes ?
@ -475,14 +481,14 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
)
# modif
nb_changed, nb_suppress, existing_decisions = notes_add(
etudids_changed, nb_suppress, existing_decisions = notes_add(
current_user,
evaluation_id,
notes,
comment="effacer tout",
check_inscription=False,
)
assert nb_changed == nb_suppress
assert len(etudids_changed) == nb_suppress
H = [f"""<p>{nb_suppress} notes supprimées</p>"""]
if existing_decisions:
H.append(
@ -516,7 +522,7 @@ def notes_add(
comment=None,
do_it=True,
check_inscription=True,
) -> tuple:
) -> tuple[list[int], int, list[int]]:
"""
Insert or update notes
notes is a list of tuples (etudid,value)
@ -524,12 +530,12 @@ def notes_add(
WOULD be changed or suppressed.
Nota:
- si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log)
Return tuple (nb_changed, nb_suppress, existing_decisions)
Return: tuple (etudids_changed, nb_suppress, etudids_with_decision)
"""
now = psycopg2.Timestamp(
*time.localtime()[:6]
) # datetime.datetime.now().isoformat()
# Verifie inscription et valeur note
now = psycopg2.Timestamp(*time.localtime()[:6])
# Vérifie inscription et valeur note
inscrits = {
x[0]
for x in sco_groups.do_evaluation_listeetuds_groups(
@ -548,13 +554,13 @@ def notes_add(
# Met a jour la base
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
nb_changed = 0
etudids_changed = []
nb_suppress = 0
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
existing_decisions = (
[]
) # etudids pour lesquels il y a une decision de jury et que la note change
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# etudids pour lesquels il y a une decision de jury et que la note change:
etudids_with_decision = []
try:
for etudid, value in notes:
changed = False
@ -562,7 +568,7 @@ def notes_add(
# nouvelle note
if value != scu.NOTES_SUPPRESS:
if do_it:
aa = {
args = {
"etudid": etudid,
"evaluation_id": evaluation_id,
"value": value,
@ -570,13 +576,20 @@ def notes_add(
"uid": user.id,
"date": now,
}
ndb.quote_dict(aa)
ndb.quote_dict(args)
# Note: le conflit ci-dessous peut arriver si un autre thread
# a modifié la base après qu'on ait lu notes_db
cursor.execute(
"""INSERT INTO notes_notes
(etudid, evaluation_id, value, comment, date, uid)
VALUES (%(etudid)s,%(evaluation_id)s,%(value)s,%(comment)s,%(date)s,%(uid)s)
VALUES
(%(etudid)s,%(evaluation_id)s,%(value)s,
%(comment)s,%(date)s,%(uid)s)
ON CONFLICT ON CONSTRAINT notes_notes_etudid_evaluation_id_key
DO UPDATE SET etudid=%(etudid)s, evaluation_id=%(evaluation_id)s,
value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s
""",
aa,
args,
)
changed = True
else:
@ -584,7 +597,7 @@ def notes_add(
oldval = notes_db[etudid]["value"]
if type(value) != type(oldval):
changed = True
elif type(value) == float and (
elif isinstance(value, float) and (
abs(value - oldval) > scu.NOTES_PRECISION
):
changed = True
@ -603,7 +616,7 @@ def notes_add(
""",
{"etudid": etudid, "evaluation_id": evaluation_id},
)
aa = {
args = {
"etudid": etudid,
"evaluation_id": evaluation_id,
"value": value,
@ -611,7 +624,7 @@ def notes_add(
"comment": comment,
"uid": user.id,
}
ndb.quote_dict(aa)
ndb.quote_dict(args)
if value != scu.NOTES_SUPPRESS:
if do_it:
cursor.execute(
@ -620,52 +633,49 @@ def notes_add(
WHERE etudid = %(etudid)s
and evaluation_id = %(evaluation_id)s
""",
aa,
args,
)
else: # suppression ancienne note
if do_it:
log(
"notes_add, suppress, evaluation_id=%s, etudid=%s, oldval=%s"
% (evaluation_id, etudid, oldval)
f"""notes_add, suppress, evaluation_id={evaluation_id}, etudid={
etudid}, oldval={oldval}"""
)
cursor.execute(
"""DELETE FROM notes_notes
WHERE etudid = %(etudid)s
AND evaluation_id = %(evaluation_id)s
""",
aa,
args,
)
# garde trace de la suppression dans l'historique:
aa["value"] = scu.NOTES_SUPPRESS
args["value"] = scu.NOTES_SUPPRESS
cursor.execute(
"""INSERT INTO notes_notes_log (etudid,evaluation_id,value,comment,date,uid)
VALUES (%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s)
"""INSERT INTO notes_notes_log
(etudid,evaluation_id,value,comment,date,uid)
VALUES
(%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s)
""",
aa,
args,
)
nb_suppress += 1
if changed:
nb_changed += 1
if has_existing_decision(M, E, etudid):
existing_decisions.append(etudid)
etudids_changed.append(etudid)
if res.etud_has_decision(etudid):
etudids_with_decision.append(etudid)
except Exception as exc:
log("*** exception in notes_add")
if do_it:
cnx.rollback() # abort
# inval cache
sco_cache.invalidate_formsemestre(
formsemestre_id=M["formsemestre_id"]
) # > modif notes (exception)
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
sco_cache.EvaluationCache.delete(evaluation_id)
raise # XXX
raise ScoGenError("Erreur enregistrement note: merci de ré-essayer") from exc
raise ScoException from exc
if do_it:
cnx.commit()
sco_cache.invalidate_formsemestre(
formsemestre_id=M["formsemestre_id"]
) # > modif notes
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
sco_cache.EvaluationCache.delete(evaluation_id)
return nb_changed, nb_suppress, existing_decisions
return etudids_changed, nb_suppress, etudids_with_decision
def saisie_notes_tableur(evaluation_id, group_ids=()):
@ -868,44 +878,39 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
def feuille_saisie_notes(evaluation_id, group_ids=[]):
"""Document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals:
evaluation: Evaluation = Evaluation.query.get(evaluation_id)
if not evaluation:
raise ScoValueError("invalid evaluation_id")
eval_dict = evals[0]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=eval_dict["moduleimpl_id"])[0]
formsemestre_id = M["formsemestre_id"]
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
mod_responsable = sco_users.user_info(M["responsable_id"])
if eval_dict["jour"]:
indication_date = ndb.DateDMYtoISO(eval_dict["jour"])
modimpl = evaluation.moduleimpl
formsemestre = modimpl.formsemestre
mod_responsable = sco_users.user_info(modimpl.responsable_id)
if evaluation.jour:
indication_date = evaluation.jour.isoformat()
else:
indication_date = scu.sanitize_filename(eval_dict["description"])[:12]
eval_name = "%s-%s" % (Mod["code"], indication_date)
indication_date = scu.sanitize_filename(evaluation.description or "")[:12]
eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
if eval_dict["description"]:
evaltitre = "%s du %s" % (eval_dict["description"], eval_dict["jour"])
else:
evaltitre = "évaluation du %s" % eval_dict["jour"]
description = "%s en %s (%s) resp. %s" % (
evaltitre,
Mod["abbrev"] or "",
Mod["code"] or "",
mod_responsable["prenomnom"],
date_str = (
f"""du {evaluation.jour.strftime("%d/%m/%Y")}"""
if evaluation.jour
else "(sans date)"
)
eval_titre = f"""{evaluation.description if evaluation.description else "évaluation"} {date_str}"""
description = f"""{eval_titre} en {evaluation.moduleimpl.module.abbrev or ""} ({
evaluation.moduleimpl.module.code
}) resp. {mod_responsable["prenomnom"]}"""
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids=group_ids,
formsemestre_id=formsemestre_id,
formsemestre_id=formsemestre.id,
select_all_when_unspecified=True,
etat=None,
)
groups = sco_groups.listgroups(groups_infos.group_ids)
gr_title_filename = sco_groups.listgroups_filename(groups)
# gr_title = sco_groups.listgroups_abbrev(groups)
if None in [g["group_name"] for g in groups]: # tous les etudiants
getallstudents = True
# gr_title = "tous"
gr_title_filename = "tous"
else:
getallstudents = False
@ -917,17 +922,17 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
]
# une liste de liste de chaines: lignes de la feuille de calcul
L = []
rows = []
etuds = _get_sorted_etuds(eval_dict, etudids, formsemestre_id)
etuds = _get_sorted_etuds(evaluation, etudids, formsemestre.id)
for e in etuds:
etudid = e["etudid"]
groups = sco_groups.get_etud_groups(etudid, formsemestre_id)
groups = sco_groups.get_etud_groups(etudid, formsemestre.id)
grc = sco_groups.listgroups_abbrev(groups)
L.append(
rows.append(
[
"%s" % etudid,
str(etudid),
e["nom"].upper(),
e["prenom"].lower().capitalize(),
e["inscr"]["etat"],
@ -937,31 +942,11 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
]
)
filename = "notes_%s_%s" % (eval_name, gr_title_filename)
filename = f"notes_{eval_name}_{gr_title_filename}"
xls = sco_excel.excel_feuille_saisie(
eval_dict, sem["titreannee"], description, lines=L
evaluation, formsemestre.titre_annee(), description, lines=rows
)
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
# return sco_excel.send_excel_file(xls, filename)
def has_existing_decision(M, E, etudid):
"""Verifie s'il y a une validation pour cet etudiant dans ce semestre ou UE
Si oui, return True
"""
formsemestre_id = M["formsemestre_id"]
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if nt.get_etud_decision_sem(etudid):
return True
dec_ues = nt.get_etud_decisions_ue(etudid)
if dec_ues:
mod = sco_edit_module.module_list({"module_id": M["module_id"]})[0]
ue_id = mod["ue_id"]
if ue_id in dec_ues:
return True # decision pour l'UE a laquelle appartient cette evaluation
return False # pas de decision de jury affectee par cette note
# -----------------------------
@ -973,20 +958,18 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
if not isinstance(evaluation_id, int):
raise ScoInvalidParamError()
group_ids = [int(group_id) for group_id in (group_ids or [])]
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals:
evaluation: Evaluation = Evaluation.query.get(evaluation_id)
if evaluation is None:
raise ScoValueError("évaluation inexistante")
E = evals[0]
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
formsemestre_id = M["formsemestre_id"]
modimpl = evaluation.moduleimpl
moduleimpl_status_url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=E["moduleimpl_id"],
moduleimpl_id=evaluation.moduleimpl_id,
)
# Check access
# (admin, respformation, and responsable_id)
if not sco_permissions_check.can_edit_notes(current_user, E["moduleimpl_id"]):
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
return f"""
{html_sco_header.sco_header()}
<h2>Modification des notes impossible pour {current_user.user_name}</h2>
@ -1001,16 +984,16 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
# Informations sur les groupes à afficher:
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids=group_ids,
formsemestre_id=formsemestre_id,
formsemestre_id=modimpl.formsemestre_id,
select_all_when_unspecified=True,
etat=None,
)
if E["description"]:
page_title = 'Saisie "%s"' % E["description"]
else:
page_title = "Saisie des notes"
page_title = (
f'Saisie "{evaluation.description}"'
if evaluation.description
else "Saisie des notes"
)
# HTML page:
H = [
html_sco_header.sco_header(
@ -1036,19 +1019,19 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
"id": "menu_saisie_tableur",
"endpoint": "notes.saisie_notes_tableur",
"args": {
"evaluation_id": E["evaluation_id"],
"evaluation_id": evaluation.id,
"group_ids": groups_infos.group_ids,
},
},
{
"title": "Voir toutes les notes du module",
"endpoint": "notes.evaluation_listenotes",
"args": {"moduleimpl_id": E["moduleimpl_id"]},
"args": {"moduleimpl_id": evaluation.moduleimpl_id},
},
{
"title": "Effacer toutes les notes de cette évaluation",
"endpoint": "notes.evaluation_suppress_alln",
"args": {"evaluation_id": E["evaluation_id"]},
"args": {"evaluation_id": evaluation.id},
},
],
alone=True,
@ -1077,7 +1060,9 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
)
# Le formulaire de saisie des notes:
form = _form_saisie_notes(E, M, groups_infos, destination=moduleimpl_status_url)
form = _form_saisie_notes(
evaluation, modimpl, groups_infos, destination=moduleimpl_status_url
)
if form is None:
return flask.redirect(moduleimpl_status_url)
H.append(form)
@ -1101,10 +1086,9 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
return "\n".join(H)
def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
eval_dict["evaluation_id"]
) # Notes existantes
def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: int):
# Notes existantes
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
cnx = ndb.GetDBConnexion()
etuds = []
for etudid in etudids:
@ -1123,17 +1107,17 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id)
# Information sur absence (tenant compte de la demi-journée)
jour_iso = ndb.DateDMYtoISO(eval_dict["jour"])
jour_iso = evaluation.jour.isoformat() if evaluation.jour else ""
warn_abs_lst = []
if eval_dict["matin"]:
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=1)
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=1)
if evaluation.is_matin():
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=True)
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=True)
if nbabs:
if nbabsjust:
warn_abs_lst.append("absent justifié le matin !")
else:
warn_abs_lst.append("absent le matin !")
if eval_dict["apresmidi"]:
if evaluation.is_apresmidi():
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0)
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0)
if nbabs:
@ -1169,35 +1153,38 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
return etuds
def _form_saisie_notes(E, M, groups_infos, destination=""):
def _form_saisie_notes(
evaluation: Evaluation, modimpl: ModuleImpl, groups_infos, destination=""
):
"""Formulaire HTML saisie des notes dans l'évaluation E du moduleimpl M
pour les groupes indiqués.
On charge tous les étudiants, ne seront montrés que ceux
des groupes sélectionnés grace a un filtre en javascript.
"""
evaluation_id = E["evaluation_id"]
formsemestre_id = M["formsemestre_id"]
formsemestre_id = modimpl.formsemestre_id
formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
etudids = [
x[0]
for x in sco_groups.do_evaluation_listeetuds_groups(
evaluation_id, getallstudents=True, include_demdef=True
evaluation.id, getallstudents=True, include_demdef=True
)
]
if not etudids:
return '<div class="ue_warning"><span>Aucun étudiant sélectionné !</span></div>'
# Decisions de jury existantes ?
decisions_jury = {etudid: has_existing_decision(M, E, etudid) for etudid in etudids}
# Nb de decisions de jury (pour les inscrits à l'évaluation):
# Décisions de jury existantes ?
decisions_jury = {etudid: res.etud_has_decision(etudid) for etudid in etudids}
# Nb de décisions de jury (pour les inscrits à l'évaluation):
nb_decisions = sum(decisions_jury.values())
etuds = _get_sorted_etuds(E, etudids, formsemestre_id)
etuds = _get_sorted_etuds(evaluation, etudids, formsemestre_id)
# Build form:
descr = [
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
("evaluation_id", {"default": evaluation.id, "input_type": "hidden"}),
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
(
"group_ids",
@ -1207,7 +1194,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
("comment", {"size": 44, "title": "Commentaire", "return_focus_next": True}),
("changed", {"default": "0", "input_type": "hidden"}), # changed in JS
]
if M["module"]["module_type"] in (
if modimpl.module.module_type in (
ModuleType.STANDARD,
ModuleType.RESSOURCE,
ModuleType.SAE,
@ -1220,11 +1207,11 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
"title": "Notes ",
"cssclass": "formnote_bareme",
"readonly": True,
"default": "&nbsp;/ %g" % E["note_max"],
"default": "&nbsp;/ %g" % evaluation.note_max,
},
)
)
elif M["module"]["module_type"] == ModuleType.MALUS:
elif modimpl.module.module_type == ModuleType.MALUS:
descr.append(
(
"s3",
@ -1238,7 +1225,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
)
)
else:
raise ValueError("invalid module type (%s)" % M["module"]["module_type"]) # bug
raise ValueError(f"invalid module type ({modimpl.module.module_type})") # bug
initvalues = {}
for e in etuds:
@ -1248,7 +1235,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
if disabled:
classdem = " etud_dem"
etud_classes.append("etud_dem")
disabled_attr = 'disabled="%d"' % disabled
disabled_attr = f'disabled="{disabled}"'
else:
classdem = ""
disabled_attr = ""
@ -1265,18 +1252,17 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
)
# Historique des saisies de notes:
if not disabled:
explanation = (
'<span id="hist_%s">' % etudid
+ get_note_history_menu(evaluation_id, etudid)
+ "</span>"
)
else:
explanation = ""
explanation = (
""
if disabled
else f"""<span id="hist_{etudid}">{
get_note_history_menu(evaluation.id, etudid)
}</span>"""
)
explanation = e["absinfo"] + explanation
# Lien modif decision de jury:
explanation += '<span id="jurylink_%s" class="jurylink"></span>' % etudid
explanation += f'<span id="jurylink_{etudid}" class="jurylink"></span>'
# Valeur actuelle du champ:
initvalues["note_" + str(etudid)] = e["val"]
@ -1330,7 +1316,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
H.append(tf.getform()) # check and init
H.append(
f"""<a href="{url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
moduleimpl_id=M["moduleimpl_id"])
moduleimpl_id=modimpl.id)
}" class="btn btn-primary">Terminer</a>
"""
)
@ -1345,7 +1331,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
Mettre les notes manquantes à
<input type="text" size="5" name="value"/>
<input type="submit" value="OK"/>
<input type="hidden" name="evaluation_id" value="{evaluation_id}"/>
<input type="hidden" name="evaluation_id" value="{evaluation.id}"/>
<input class="group_ids_str" type="hidden" name="group_ids_str" value="{
",".join([str(x) for x in groups_infos.group_ids])
}"/>
@ -1362,50 +1348,56 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
return None
def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
"""Enregistre une note (ajax)"""
authuser = current_user
log(
"save_note: evaluation_id=%s etudid=%s uid=%s value=%s"
% (evaluation_id, etudid, authuser, value)
)
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
Mod["url"] = url_for(
def save_notes(
evaluation: Evaluation, notes: list[tuple[(int, float)]], comment: str = ""
) -> dict:
"""Enregistre une liste de notes.
Vérifie que les étudiants sont bien inscrits à ce module, et que l'on a le droit.
Result: dict avec
"""
log(f"save_note: evaluation_id={evaluation.id} uid={current_user} notes={notes}")
status_url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=M["moduleimpl_id"],
moduleimpl_id=evaluation.moduleimpl_id,
_external=True,
)
result = {"nbchanged": 0} # JSON
# Check access: admin, respformation, or responsable_id
if not sco_permissions_check.can_edit_notes(authuser, E["moduleimpl_id"]):
result["status"] = "unauthorized"
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
return json_error(403, "modification notes non autorisee pour cet utilisateur")
#
valid_notes, _, _, _, _ = _check_notes(notes, evaluation)
if valid_notes:
etudids_changed, _, etudids_with_decision = notes_add(
current_user, evaluation.id, valid_notes, comment=comment, do_it=True
)
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=evaluation.moduleimpl_id,
text=f"""Chargement notes dans <a href="{status_url}">{
evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code}</a>""",
url=status_url,
max_frequency=30 * 60, # 30 minutes
)
result = {
"etudids_with_decision": etudids_with_decision,
"etudids_changed": etudids_changed,
"history_menu": {
etudid: get_note_history_menu(evaluation.id, etudid)
for etudid in etudids_changed
},
}
else:
L, _, _, _, _ = _check_notes([(etudid, value)], E, Mod)
if L:
nbchanged, _, existing_decisions = notes_add(
authuser, evaluation_id, L, comment=comment, do_it=True
)
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=M["moduleimpl_id"],
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod,
url=Mod["url"],
max_frequency=30 * 60, # 30 minutes
)
result["nbchanged"] = nbchanged
result["existing_decisions"] = existing_decisions
if nbchanged > 0:
result["history_menu"] = get_note_history_menu(evaluation_id, etudid)
else:
result["history_menu"] = "" # no update needed
result["status"] = "ok"
return scu.sendJSON(result)
result = {
"etudids_changed": [],
"etudids_with_decision": [],
"history_menu": [],
}
return result
def get_note_history_menu(evaluation_id, etudid):
def get_note_history_menu(evaluation_id: int, etudid: int) -> str:
"""Menu HTML historique de la note"""
history = sco_undo_notes.get_note_history(evaluation_id, etudid)
if not history:

View File

@ -354,6 +354,10 @@ body.editionActivated .filtres .nonEditable .move {
display: initial;
}
.groupe:has(.etudiants:empty) {
display: none;
}
/* .filtres .unselect {
background: rgba(0, 153, 204, 0.5) !important;
} */

View File

@ -3173,6 +3173,19 @@ li.tf-msg {
/* EMO_WARNING, "&#9888;&#65039;" */
}
p.error {
font-weight: bold;
color: red;
}
p.error::before {
content: "\2049 \fe0f";
margin-right: 8px;
}
mark {
padding-right: 0px;
}
.infop {
font-weight: normal;

View File

@ -0,0 +1,65 @@
// Mécanisme d'auto-complétion (choix) d'un étudiant
// Il faut un champs #etudiant (text input) et à coté un champ hidden etudid qui sera rempli.
// utilise autoComplete.js, source https://tarekraafat.github.io/autoComplete.js
// EV 2023-06-01
function etud_autocomplete_config(with_dept = false) {
return {
selector: "#etudiant",
placeHolder: "Nom...",
threshold: 3,
data: {
src: async (query) => {
try {
// Fetch Data from external Source
const source = await fetch(`/ScoDoc/api/etudiants/name/${query}`);
// Data should be an array of `Objects` or `Strings`
const data = await source.json();
return data;
} catch (error) {
return error;
}
},
// Data source 'Object' key to be searched
keys: ["nom"]
},
events: {
input: {
selection: (event) => {
const prenom = sco_capitalize(event.detail.selection.value.prenom);
const selection = with_dept ? `${event.detail.selection.value.nom} ${prenom} (${event.detail.selection.value.dept_acronym})` : `${event.detail.selection.value.nom} ${prenom}`;
// store etudid
const etudidField = document.getElementById('etudid');
etudidField.value = event.detail.selection.value.id;
autoCompleteJS.input.value = selection;
}
}
},
resultsList: {
element: (list, data) => {
if (!data.results.length) {
// Create "No Results" message element
const message = document.createElement("div");
// Add class to the created element
message.setAttribute("class", "no_result");
// Add message text content
message.innerHTML = `<span>Pas de résultat pour "${data.query}"</span>`;
// Append message element to the results list
list.prepend(message);
// Efface l'etudid
const etudidField = document.getElementById('etudid');
etudidField.value = "";
}
},
noResults: true,
},
resultItem: {
highlight: true,
element: (item, data) => {
const prenom = sco_capitalize(data.value.prenom);
item.innerHTML += with_dept ? ` ${prenom} (${data.value.dept_acronym})` : ` ${prenom}`;
},
},
}
}

View File

@ -1,132 +1,142 @@
// Formulaire saisie des notes
$().ready(function () {
$("#formnotes .note").bind("blur", valid_note);
$("#formnotes .note").bind("blur", valid_note);
$("#formnotes input").bind("paste", paste_text);
$(".btn_masquer_DEM").bind("click", masquer_DEM);
$("#formnotes input").bind("paste", paste_text);
$(".btn_masquer_DEM").bind("click", masquer_DEM);
});
function is_valid_note(v) {
if (!v)
return true;
if (!v) return true;
var note_min = parseFloat($("#eval_note_min").text());
var note_max = parseFloat($("#eval_note_max").text());
var note_min = parseFloat($("#eval_note_min").text());
var note_max = parseFloat($("#eval_note_max").text());
if (!v.match("^-?[0-9]*.?[0-9]*$")) {
return (v == "ABS") || (v == "EXC") || (v == "SUPR") || (v == "ATT") || (v == "DEM");
} else {
var x = parseFloat(v);
return (x >= note_min) && (x <= note_max);
}
if (!v.match("^-?[0-9]*.?[0-9]*$")) {
return v == "ABS" || v == "EXC" || v == "SUPR" || v == "ATT" || v == "DEM";
} else {
var x = parseFloat(v);
return x >= note_min && x <= note_max;
}
}
function valid_note(e) {
var v = this.value.trim().toUpperCase().replace(",", ".");
if (is_valid_note(v)) {
if (v && (v != $(this).attr('data-last-saved-value'))) {
this.className = "note_valid_new";
var etudid = $(this).attr('data-etudid');
save_note(this, v, etudid);
}
} else {
/* Saisie invalide */
this.className = "note_invalid";
sco_message("valeur invalide ou hors barème");
var v = this.value.trim().toUpperCase().replace(",", ".");
if (is_valid_note(v)) {
if (v && v != $(this).attr("data-last-saved-value")) {
this.className = "note_valid_new";
const etudid = parseInt($(this).attr("data-etudid"));
save_note(this, v, etudid);
}
} else {
/* Saisie invalide */
this.className = "note_invalid";
sco_message("valeur invalide ou hors barème");
}
}
function save_note(elem, v, etudid) {
var evaluation_id = $("#formnotes_evaluation_id").attr("value");
var formsemestre_id = $("#formnotes_formsemestre_id").attr("value");
$('#sco_msg').html("en cours...").show();
$.post(SCO_URL + '/Notes/save_note',
{
'etudid': etudid,
'evaluation_id': evaluation_id,
'value': v,
'comment': document.getElementById('formnotes_comment').value
async function save_note(elem, v, etudid) {
let evaluation_id = $("#formnotes_evaluation_id").attr("value");
let formsemestre_id = $("#formnotes_formsemestre_id").attr("value");
$("#sco_msg").html("en cours...").show();
try {
const response = await fetch(
SCO_URL + "/../api/evaluation/" + evaluation_id + "/notes/set",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
function (result) {
if (result['nbchanged'] > 0) {
sco_message("enregistré");
elem.className = "note_saved";
// il y avait une decision de jury ?
if (result.existing_decisions[0] == etudid) {
if (v != $(elem).attr('data-orig-value')) {
$("#jurylink_" + etudid).html('<a href="formsemestre_validation_etud_form?formsemestre_id=' + formsemestre_id + '&etudid=' + etudid + '">mettre à jour décision de jury</a>');
} else {
$("#jurylink_" + etudid).html('');
}
}
// mise a jour menu historique
if (result['history_menu']) {
$("#hist_" + etudid).html(result['history_menu']);
}
$(elem).attr('data-last-saved-value', v);
} else {
$('#sco_msg').html("").show();
sco_message("valeur non enregistrée");
}
}
body: JSON.stringify({
notes: [[etudid, v]],
comment: document.getElementById("formnotes_comment").value,
}),
}
);
if (!response.ok) {
sco_message("Erreur: valeur non enregistrée");
} else {
const data = await response.json();
$("#sco_msg").hide();
if (data.etudids_changed.length > 0) {
sco_message("enregistré");
elem.className = "note_saved";
// Il y avait une decision de jury ?
if (data.etudids_with_decision.includes(etudid)) {
if (v != $(elem).attr("data-orig-value")) {
$("#jurylink_" + etudid).html(
'<a href="formsemestre_validation_etud_form?formsemestre_id=' +
formsemestre_id +
"&etudid=" +
etudid +
'">mettre à jour décision de jury</a>'
);
} else {
$("#jurylink_" + etudid).html("");
}
}
// Mise à jour menu historique
if (data.history_menu[etudid]) {
$("#hist_" + etudid).html(data.history_menu[etudid]);
}
$(elem).attr("data-last-saved-value", v);
}
}
} catch (error) {
console.error("Fetch error:", error);
sco_message("Erreur réseau: valeur non enregistrée");
}
}
function change_history(e) {
var opt = e.selectedOptions[0];
var val = $(opt).attr("data-note");
var etudid = $(e).attr('data-etudid');
// le input associé a ce menu:
var input_elem = e.parentElement.parentElement.parentElement.childNodes[0];
input_elem.value = val;
save_note(input_elem, val, etudid);
let opt = e.selectedOptions[0];
let val = $(opt).attr("data-note");
const etudid = parseInt($(e).attr("data-etudid"));
// le input associé a ce menu:
let input_elem = e.parentElement.parentElement.parentElement.childNodes[0];
input_elem.value = val;
save_note(input_elem, val, etudid);
}
// Contribution S.L.: copier/coller des notes
function paste_text(e) {
var event = e.originalEvent;
event.stopPropagation();
event.preventDefault();
var clipb = e.originalEvent.clipboardData;
var data = clipb.getData('Text');
var list = data.split(/\r\n|\r|\n|\t| /g);
var currentInput = event.currentTarget;
var masquerDEM = document.querySelector("body").classList.contains("masquer_DEM");
var event = e.originalEvent;
event.stopPropagation();
event.preventDefault();
var clipb = e.originalEvent.clipboardData;
var data = clipb.getData("Text");
var list = data.split(/\r\n|\r|\n|\t| /g);
var currentInput = event.currentTarget;
var masquerDEM = document
.querySelector("body")
.classList.contains("masquer_DEM");
for (var i = 0; i < list.length; i++) {
currentInput.value = list[i];
var evt = document.createEvent("HTMLEvents");
evt.initEvent("blur", false, true);
currentInput.dispatchEvent(evt);
var sibbling = currentInput.parentElement.parentElement.nextElementSibling;
while (
sibbling &&
(
sibbling.style.display == "none" ||
(
masquerDEM && sibbling.classList.contains("etud_dem")
)
)
) {
sibbling = sibbling.nextElementSibling;
}
if (sibbling) {
currentInput = sibbling.querySelector("input");
if (!currentInput) {
return;
}
} else {
return;
}
for (var i = 0; i < list.length; i++) {
currentInput.value = list[i];
var evt = document.createEvent("HTMLEvents");
evt.initEvent("blur", false, true);
currentInput.dispatchEvent(evt);
var sibbling = currentInput.parentElement.parentElement.nextElementSibling;
while (
sibbling &&
(sibbling.style.display == "none" ||
(masquerDEM && sibbling.classList.contains("etud_dem")))
) {
sibbling = sibbling.nextElementSibling;
}
if (sibbling) {
currentInput = sibbling.querySelector("input");
if (!currentInput) {
return;
}
} else {
return;
}
}
}
function masquer_DEM() {
document.querySelector("body").classList.toggle("masquer_DEM");
document.querySelector("body").classList.toggle("masquer_DEM");
}

View File

@ -67,6 +67,10 @@ $(function () {
}
});
function sco_capitalize(string) {
return string[0].toUpperCase() + string.slice(1).toLowerCase();
}
// Affiche un message transitoire (duration milliseconds, 0 means infinity)
function sco_message(msg, className = "message_custom", duration = 0) {
var div = document.createElement("div");

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,92 @@
.autoComplete_wrapper {
display: inline-block;
position: relative;
}
.autoComplete_wrapper > input {
width: 370px;
height: 40px;
padding-left: 20px;
font-size: 1rem;
color: rgba(123, 123, 123, 1);
border-radius: 8px;
border: 0;
outline: none;
background-color: #f1f3f4;
}
.autoComplete_wrapper > input::placeholder {
color: rgba(123, 123, 123, 0.5);
transition: all 0.3s ease;
}
.autoComplete_wrapper > ul {
position: absolute;
max-height: 226px;
overflow-y: scroll;
top: 100%;
left: 0;
right: 0;
padding: 0;
margin: 0.5rem 0 0 0;
border-radius: 0.6rem;
background-color: #fff;
box-shadow: 0 3px 6px rgba(149, 157, 165, 0.15);
border: 1px solid rgba(33, 33, 33, 0.07);
z-index: 1000;
outline: none;
}
.autoComplete_wrapper > ul[hidden],
.autoComplete_wrapper > ul:empty {
display: block;
opacity: 0;
transform: scale(0);
}
.autoComplete_wrapper > ul > li {
margin: 0.3rem;
padding: 0.3rem 0.5rem;
list-style: none;
text-align: left;
font-size: 1rem;
color: #212121;
transition: all 0.1s ease-in-out;
border-radius: 0.35rem;
background-color: rgba(255, 255, 255, 1);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.2s ease;
}
.autoComplete_wrapper > ul > li::selection {
color: rgba(#ffffff, 0);
background-color: rgba(#ffffff, 0);
}
.autoComplete_wrapper > ul > li:hover {
cursor: pointer;
background-color: rgba(123, 123, 123, 0.1);
}
.autoComplete_wrapper > ul > li mark {
background-color: transparent;
color: rgba(255, 122, 122, 1);
font-weight: bold;
}
.autoComplete_wrapper > ul > li mark::selection {
color: rgba(#ffffff, 0);
background-color: rgba(#ffffff, 0);
}
.autoComplete_wrapper > ul > li[aria-selected="true"] {
background-color: rgba(123, 123, 123, 0.1);
}
@media only screen and (max-width: 600px) {
.autoComplete_wrapper > input {
width: 18rem;
}
}

View File

@ -0,0 +1,82 @@
.autoComplete_wrapper {
display: inline-block;
position: relative;
}
.autoComplete_wrapper > input {
width: 370px;
height: 40px;
padding-left: 10px;
font-size: 1rem;
color: rgb(116, 116, 116);
border-radius: 4px;
border: 1px solid rgba(33, 33, 33, 0.2);
outline: none;
}
.autoComplete_wrapper > input::placeholder {
color: rgba(123, 123, 123, 0.5);
transition: all 0.3s ease;
}
.autoComplete_wrapper > ul {
position: absolute;
max-height: 226px;
overflow-y: scroll;
top: 100%;
left: 0;
right: 0;
padding: 0;
margin: 0.5rem 0 0 0;
border-radius: 4px;
background-color: #fff;
border: 1px solid rgba(33, 33, 33, 0.1);
z-index: 1000;
outline: none;
}
.autoComplete_wrapper > ul > li {
padding: 10px 20px;
list-style: none;
text-align: left;
font-size: 16px;
color: #212121;
transition: all 0.1s ease-in-out;
border-radius: 3px;
background-color: rgba(255, 255, 255, 1);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.2s ease;
}
.autoComplete_wrapper > ul > li::selection {
color: rgba(#ffffff, 0);
background-color: rgba(#ffffff, 0);
}
.autoComplete_wrapper > ul > li:hover {
cursor: pointer;
background-color: rgba(123, 123, 123, 0.1);
}
.autoComplete_wrapper > ul > li mark {
background-color: transparent;
color: rgba(255, 122, 122, 1);
font-weight: bold;
}
.autoComplete_wrapper > ul > li mark::selection {
color: rgba(#ffffff, 0);
background-color: rgba(#ffffff, 0);
}
.autoComplete_wrapper > ul > li[aria-selected="true"] {
background-color: rgba(123, 123, 123, 0.1);
}
@media only screen and (max-width: 600px) {
.autoComplete_wrapper > input {
width: 18rem;
}
}

View File

@ -0,0 +1,128 @@
.autoComplete_wrapper {
display: inline-block;
position: relative;
}
.autoComplete_wrapper > input {
height: 3rem;
width: 370px;
margin: 0;
padding: 0 2rem 0 3.2rem;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
font-size: 1rem;
text-overflow: ellipsis;
color: rgba(255, 122, 122, 0.3);
outline: none;
border-radius: 10rem;
border: 0.05rem solid rgba(255, 122, 122, 0.5);
background-image: url(./images/search.svg);
background-size: 1.4rem;
background-position: left 1.05rem top 0.8rem;
background-repeat: no-repeat;
background-origin: border-box;
background-color: #fff;
transition: all 0.4s ease;
-webkit-transition: all -webkit-transform 0.4s ease;
}
.autoComplete_wrapper > input::placeholder {
color: rgba(255, 122, 122, 0.5);
transition: all 0.3s ease;
-webkit-transition: all -webkit-transform 0.3s ease;
}
.autoComplete_wrapper > input:hover::placeholder {
color: rgba(255, 122, 122, 0.6);
transition: all 0.3s ease;
-webkit-transition: all -webkit-transform 0.3s ease;
}
.autoComplete_wrapper > input:focus::placeholder {
padding: 0.1rem 0.6rem;
font-size: 0.95rem;
color: rgba(255, 122, 122, 0.4);
}
.autoComplete_wrapper > input:focus::selection {
background-color: rgba(255, 122, 122, 0.15);
}
.autoComplete_wrapper > input::selection {
background-color: rgba(255, 122, 122, 0.15);
}
.autoComplete_wrapper > input:hover {
color: rgba(255, 122, 122, 0.8);
transition: all 0.3s ease;
-webkit-transition: all -webkit-transform 0.3s ease;
}
.autoComplete_wrapper > input:focus {
color: rgba(255, 122, 122, 1);
border: 0.06rem solid rgba(255, 122, 122, 0.8);
}
.autoComplete_wrapper > ul {
position: absolute;
max-height: 226px;
overflow-y: scroll;
box-sizing: border-box;
left: 0;
right: 0;
margin: 0.5rem 0 0 0;
padding: 0;
z-index: 1;
list-style: none;
border-radius: 0.6rem;
background-color: #fff;
border: 1px solid rgba(33, 33, 33, 0.07);
box-shadow: 0 3px 6px rgba(149, 157, 165, 0.15);
outline: none;
transition: opacity 0.15s ease-in-out;
-moz-transition: opacity 0.15s ease-in-out;
-webkit-transition: opacity 0.15s ease-in-out;
}
.autoComplete_wrapper > ul[hidden],
.autoComplete_wrapper > ul:empty {
display: block;
opacity: 0;
transform: scale(0);
}
.autoComplete_wrapper > ul > li {
margin: 0.3rem;
padding: 0.3rem 0.5rem;
text-align: left;
font-size: 1rem;
color: #212121;
border-radius: 0.35rem;
background-color: rgba(255, 255, 255, 1);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.2s ease;
}
.autoComplete_wrapper > ul > li mark {
background-color: transparent;
color: rgba(255, 122, 122, 1);
font-weight: bold;
}
.autoComplete_wrapper > ul > li:hover {
cursor: pointer;
background-color: rgba(255, 122, 122, 0.15);
}
.autoComplete_wrapper > ul > li[aria-selected="true"] {
background-color: rgba(255, 122, 122, 0.15);
}
@media only screen and (max-width: 600px) {
.autoComplete_wrapper > input {
width: 18rem;
}
}

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" focusable="false" x="0px" y="0px" width="30" height="30" viewBox="0 0 171 171" style=" fill:#000000;">
<g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal">
<path d="M0,171.99609v-171.99609h171.99609v171.99609z" fill="none"></path>
<g fill="#ff7a7a">
<path d="M74.1,17.1c-31.41272,0 -57,25.58728 -57,57c0,31.41272 25.58728,57 57,57c13.6601,0 26.20509,-4.85078 36.03692,-12.90293l34.03301,34.03301c1.42965,1.48907 3.55262,2.08891 5.55014,1.56818c1.99752,-0.52073 3.55746,-2.08067 4.07819,-4.07819c0.52073,-1.99752 -0.0791,-4.12049 -1.56818,-5.55014l-34.03301,-34.03301c8.05215,-9.83182 12.90293,-22.37682 12.90293,-36.03692c0,-31.41272 -25.58728,-57 -57,-57zM74.1,28.5c25.2517,0 45.6,20.3483 45.6,45.6c0,25.2517 -20.3483,45.6 -45.6,45.6c-25.2517,0 -45.6,-20.3483 -45.6,-45.6c0,-25.2517 20.3483,-45.6 45.6,-45.6z"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -479,17 +479,18 @@ class TableRecap(tb.Table):
for row in self.rows:
etud = row.etud
admission = etud.admission.first()
first = True
for cid, title in fields.items():
cell = row.add_cell(
cid,
title,
getattr(admission, cid) or "",
"admission",
)
if first:
cell.classes.append("admission_first")
first = False
if admission:
first = True
for cid, title in fields.items():
cell = row.add_cell(
cid,
title,
getattr(admission, cid) or "",
"admission",
)
if first:
cell.classes.append("admission_first")
first = False
def add_cursus(self):
"""Ajoute colonne avec code cursus, eg 'S1 S2 S1'

View File

@ -85,7 +85,15 @@
{{ super() }}
{{ moment.include_moment() }}
{{ moment.lang(g.locale) }}
<script src="{{scu.STATIC_DIR}}/libjs/menu.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/bubble.js"></script>
<script src="{{scu.STATIC_DIR}}/jQuery/jquery.js"></script>
<script src="{{scu.STATIC_DIR}}/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/jquery.field.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script>
<script>
var SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)[:-11] }}";
</script>
{% endblock %}

View File

@ -4,33 +4,27 @@
{% block styles %}
{{super()}}
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/css/autosuggest_inquisitor.css" />
<script src="/ScoDoc/static/libjs/AutoSuggest.js"></script>
<link rel="stylesheet" href="/ScoDoc/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.02.css">
{% endblock %}
{% block app_content %}
<h1>{{ title }}</h1>
<br>
<div class="row">
<div class="col-md-4">
<div class="col-md-3">
<p>
(*) champs requis
</p>
{{ wtf.quick_form(form, novalidate=True) }}
</div>
</div>
{% endblock %}
{% block scripts %}
{{super()}}
<script src="/ScoDoc/static/libjs/autoComplete.js-10.2.7/dist/autoComplete.min.js"></script>
<script src="/ScoDoc/static/js/etud_autocomplete.js"></script>
<script>
window.onload = function (e) {
var etudiants_options = {
script: "/ScoDoc/entreprises/etudiants?",
varname: "term",
json: true,
noresults: "Valeur invalide !",
minchars: 2,
timeout: 60000
};
var as_etudiants = new bsn.AutoSuggest('etudiant', etudiants_options);
}
const autoCompleteJS = new autoComplete(etud_autocomplete_config(with_dept = true));
</script>
{% endblock %}
{% endblock %}

View File

@ -5,7 +5,7 @@
<h2>Erreur !</h2>
{{ exc }}
{{ exc | safe }}
<p>
{% if g.scodoc_dept %}

View File

@ -496,7 +496,7 @@ sco_publish(
@permission_required(Permission.ScoView)
@scodoc7func
def formation_table_recap(formation_id, format="html"):
return sco_formation_recap.formation_table_recap(formation_id, format="html")
return sco_formation_recap.formation_table_recap(formation_id, format=format)
sco_publish(
@ -1874,12 +1874,6 @@ sco_publish(
Permission.ScoEnsView,
)
sco_publish("/saisie_notes", sco_saisie_notes.saisie_notes, Permission.ScoEnsView)
sco_publish(
"/save_note",
sco_saisie_notes.save_note,
Permission.ScoEnsView,
methods=["GET", "POST"],
)
sco_publish(
"/do_evaluation_set_missing",
sco_saisie_notes.do_evaluation_set_missing,

View File

@ -1306,6 +1306,8 @@ def _do_cancel_dem_or_def(
db.session.delete(event)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
flash(f"{operation_name} annulée.")
return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
@ -1755,9 +1757,7 @@ def _etudident_create_or_edit_form(edit):
# Inval semesters with this student:
to_inval = [s["formsemestre_id"] for s in etud["sems"]]
for formsemestre_id in to_inval:
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > etudident_create_or_edit
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
#
return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
@ -1833,7 +1833,7 @@ def etudident_delete(etudid, dialog_confirmed=False):
# Inval semestres où il était inscrit:
to_inval = [s["formsemestre_id"] for s in etud["sems"]]
for formsemestre_id in to_inval:
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id) # >
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
flash("Étudiant supprimé !")
return flask.redirect(scu.ScoURL())

View File

@ -0,0 +1,77 @@
"""Extension unaccent
Revision ID: d84bc592584e
Revises: b8df1b913c79
Create Date: 2023-06-01 13:46:52.927951
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker # added by ev
# revision identifiers, used by Alembic.
revision = "d84bc592584e"
down_revision = "b8df1b913c79"
branch_labels = None
depends_on = None
Session = sessionmaker()
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
bind = op.get_bind()
session = Session(bind=bind)
# Ajout extension pour recherches sans accents:
session.execute(sa.text("""CREATE EXTENSION IF NOT EXISTS "unaccent";"""))
# Clé étrangère sur identite
session.execute(
sa.text(
"""UPDATE are_stages_apprentissages
SET etudid = NULL
WHERE are_stages_apprentissages.etudid NOT IN (
SELECT id
FROM Identite
);
"""
)
)
with op.batch_alter_table("are_stages_apprentissages", schema=None) as batch_op:
batch_op.create_foreign_key(
"are_stages_apprentissages_etudid_fkey",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
# Les montants de taxe en float:
with op.batch_alter_table("are_taxe_apprentissage", schema=None) as batch_op:
batch_op.alter_column(
"montant",
existing_type=sa.INTEGER(),
type_=sa.Float(),
existing_nullable=True,
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("are_taxe_apprentissage", schema=None) as batch_op:
batch_op.alter_column(
"montant",
existing_type=sa.Float(),
type_=sa.INTEGER(),
existing_nullable=True,
)
with op.batch_alter_table("are_stages_apprentissages", schema=None) as batch_op:
batch_op.drop_constraint(
"are_stages_apprentissages_etudid_fkey", type_="foreignkey"
)
# ### end Alembic commands ###

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.4.77"
SCOVERSION = "9.4.82"
SCONAME = "ScoDoc"

View File

@ -224,6 +224,32 @@ def test_etudiants(api_headers):
assert r.status_code == 404
def test_etudiants_by_name(api_headers):
"""
Route: /etudiants/name/<string:start>
"""
r = requests.get(
API_URL + "/etudiants/name/A",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 200
etuds = r.json()
assert etuds == []
#
r = requests.get(
API_URL + "/etudiants/name/REG",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 200
etuds = r.json()
assert len(etuds) == 1
assert etuds[0]["nom"] == "RÉGNIER"
def test_etudiant_formsemestres(api_headers):
"""
Route: /etudiant/etudid/<etudid:int>/formsemestres

View File

@ -24,7 +24,7 @@ from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers
from tests.api.tools_test_api import (
verify_fields,
EVALUATIONS_FIELDS,
EVALUATION_FIELDS,
NOTES_FIELDS,
)
@ -35,7 +35,7 @@ def test_evaluations(api_headers):
Route :
- /moduleimpl/<int:moduleimpl_id>/evaluations
"""
moduleimpl_id = 1
moduleimpl_id = 20
r = requests.get(
f"{API_URL}/moduleimpl/{moduleimpl_id}/evaluations",
headers=api_headers,
@ -44,6 +44,7 @@ def test_evaluations(api_headers):
)
assert r.status_code == 200
list_eval = r.json()
assert list_eval
assert isinstance(list_eval, list)
for eval in list_eval:
assert verify_fields(eval, EVALUATIONS_FIELDS) is True
@ -63,16 +64,14 @@ def test_evaluations(api_headers):
assert eval["moduleimpl_id"] == moduleimpl_id
def test_evaluation_notes(
api_headers,
): # XXX TODO changer la boucle pour parcourir le dict sans les indices
def test_evaluation_notes(api_headers):
"""
Test 'evaluation_notes'
Route :
- /evaluation/<int:evaluation_id>/notes
"""
eval_id = 1
eval_id = 20
r = requests.get(
f"{API_URL}/evaluation/{eval_id}/notes",
headers=api_headers,
@ -81,14 +80,15 @@ def test_evaluation_notes(
)
assert r.status_code == 200
eval_notes = r.json()
for i in range(1, len(eval_notes)):
assert verify_fields(eval_notes[f"{i}"], EVALUATION_FIELDS)
assert isinstance(eval_notes[f"{i}"]["id"], int)
assert isinstance(eval_notes[f"{i}"]["etudid"], int)
assert isinstance(eval_notes[f"{i}"]["evaluation_id"], int)
assert isinstance(eval_notes[f"{i}"]["value"], float)
assert isinstance(eval_notes[f"{i}"]["comment"], str)
assert isinstance(eval_notes[f"{i}"]["date"], str)
assert isinstance(eval_notes[f"{i}"]["uid"], int)
assert eval_notes
for etudid, note in eval_notes.items():
assert int(etudid) == note["etudid"]
assert verify_fields(note, NOTES_FIELDS)
assert isinstance(note["etudid"], int)
assert isinstance(note["evaluation_id"], int)
assert isinstance(note["value"], float)
assert isinstance(note["comment"], str)
assert isinstance(note["date"], str)
assert isinstance(note["uid"], int)
assert eval_id == eval_notes[f"{i}"]["evaluation_id"]
assert eval_id == note["evaluation_id"]

View File

@ -58,6 +58,7 @@ def test_permissions(api_headers):
"nip": 1,
"partition_id": 1,
"role_name": "Ens",
"start": "abc",
"uid": 1,
"version": "long",
}

View File

@ -568,8 +568,7 @@ EVALUATIONS_FIELDS = {
"visi_bulletin",
}
EVALUATION_FIELDS = {
"id",
NOTES_FIELDS = {
"etudid",
"evaluation_id",
"value",

View File

@ -0,0 +1,554 @@
<?xml version="1.0" encoding="utf-8"?>
<formation acronyme="BUT INFO" titre_officiel="Bachelor Universitaire de Technologie" commentaire="Exemple incomplet. À partir du S4, seuls les parcours A et B ont des UEs." type_parcours="700" referentiel_competence_id="1" titre="BUT INFORMATIQUE" version="1" formation_code="FCOD49" code_specialite="" refcomp_version_orebut="2021-12-11 00:00:00" refcomp_specialite="INFO" refcomp_type_titre="B.U.T.">
<ue numero="0" code_apogee="V1INFU11" titre="Compétence 1 : Réaliser un développement d'application" coefficient="0.0" semestre_idx="1" coef_rcue="1.0" type="0" color="#b80004" ue_code="UCOD62" ects="5.0" is_external="0" acronyme="UE11" apc_niveau_libelle="Développer des applications informatiques simples " apc_niveau_annee="BUT1" apc_niveau_ordre="1" reference="1">
<matiere titre="Portfolio" numero="0">
<module titre="Initiation au développement" abbrev="Initiation au dev." code="R1.01-A" heures_cours="0.0" heures_td="24.0" heures_tp="30.0" coefficient="66.0" ects="" semestre_id="1" numero="10" code_apogee="VINFR101" module_type="2">
<coefficients ue_reference="2" coef="12.0" />
<coefficients ue_reference="1" coef="21.0" />
</module>
<module titre="Implémentation d'un besoin client" abbrev="Implémentation" code="S1.01" heures_cours="0.0" heures_td="2.0" heures_tp="2.0" coefficient="40.0" ects="" semestre_id="1" numero="10" code_apogee="VINFS101" module_type="3">
<coefficients ue_reference="1" coef="40.0" />
</module>
<module titre="Portfolio" abbrev="" code="P1" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="1" numero="700" code_apogee="" module_type="3">
<coefficients ue_reference="3" coef="1.0" />
<coefficients ue_reference="1" coef="1.0" />
<coefficients ue_reference="6" coef="1.0" />
<coefficients ue_reference="5" coef="1.0" />
<coefficients ue_reference="2" coef="1.0" />
<coefficients ue_reference="4" coef="1.0" />
</module>
</matiere>
</ue>
<ue numero="1" code_apogee="V1INFU12" titre="Compétence 2 : Optimiser des applications informatiques" coefficient="0.0" semestre_idx="1" coef_rcue="1.0" type="0" color="#f97b3d" ue_code="UCOD61" ects="5.0" is_external="0" acronyme="UE12" apc_niveau_libelle="Appréhender et construire des algorithmes " apc_niveau_annee="BUT1" apc_niveau_ordre="1" reference="2">
<matiere titre="Ressource" numero="2">
<module titre="Initiation au développement" abbrev="Initiation au dev." code="R1.01-B" heures_cours="0.0" heures_td="16.0" heures_tp="24.0" coefficient="66.0" ects="" semestre_id="1" numero="20" code_apogee="VINFR101" module_type="2">
<coefficients ue_reference="2" coef="12.0" />
<coefficients ue_reference="1" coef="21.0" />
</module>
<module titre="Comparaison d'approches algorithmiques" abbrev="Comparaison d'algo." code="S1.02" heures_cours="0.0" heures_td="2.0" heures_tp="2.0" coefficient="40.0" ects="" semestre_id="1" numero="20" code_apogee="VINFS102" module_type="3">
<coefficients ue_reference="2" coef="40.0" />
</module>
<module titre="Développement d'interfaces web" abbrev="Dev. interfaces web" code="R1.02" heures_cours="0.0" heures_td="5.0" heures_tp="14.0" coefficient="35.0" ects="" semestre_id="1" numero="30" code_apogee="VINFR102" module_type="2">
<coefficients ue_reference="1" coef="12.0" />
<coefficients ue_reference="5" coef="18.0" />
<coefficients ue_reference="6" coef="5.0" />
</module>
<module titre="Mathématiques discrètes" abbrev="Maths discrètes" code="R1.06" heures_cours="0.0" heures_td="30.0" heures_tp="10.0" coefficient="33.0" ects="" semestre_id="1" numero="70" code_apogee="VINFR106" module_type="2">
<coefficients ue_reference="2" coef="15.0" />
<coefficients ue_reference="4" coef="18.0" />
</module>
<module titre="Outils mathématiques fondamentaux" abbrev="Outils fondamentaux" code="R1.07" heures_cours="0.0" heures_td="14.0" heures_tp="10.0" coefficient="15.0" ects="" semestre_id="1" numero="80" code_apogee="VINFR107" module_type="2">
<coefficients ue_reference="2" coef="15.0" />
</module>
<module titre="Gestion de projet &amp; des organisations" abbrev="Gestion proj. orga." code="R1.08" heures_cours="0.0" heures_td="21.0" heures_tp="10.0" coefficient="38.0" ects="" semestre_id="1" numero="90" code_apogee="VINFR108" module_type="2">
<coefficients ue_reference="5" coef="27.0" />
<coefficients ue_reference="6" coef="11.0" />
</module>
</matiere>
</ue>
<ue numero="2" code_apogee="V1INFU13" titre="Compétence 3 : Administrer des systèmes informatiques communicants" coefficient="0.0" semestre_idx="1" coef_rcue="1.0" type="0" color="#feb40b" ue_code="UCOD50" ects="5.0" is_external="0" acronyme="UE13" apc_niveau_libelle="Installer et configurer un poste de travail " apc_niveau_annee="BUT1" apc_niveau_ordre="1" reference="3">
<matiere titre="SAE" numero="1">
<module titre="Installation d'un poste pour le développement" abbrev="Installation poste" code="S1.03" heures_cours="0.0" heures_td="5.0" heures_tp="4.0" coefficient="40.0" ects="" semestre_id="1" numero="30" code_apogee="VINFS103" module_type="3">
<coefficients ue_reference="3" coef="40.0" />
</module>
<module titre="Introduction à l'architecture des ordinateurs" abbrev="Intro. archi." code="R1.03" heures_cours="0.0" heures_td="12.0" heures_tp="8.0" coefficient="24.0" ects="" semestre_id="1" numero="40" code_apogee="VINFR103" module_type="2">
<coefficients ue_reference="3" coef="21.0" />
<coefficients ue_reference="2" coef="6.0" />
</module>
<module titre="Introduction aux systèmes dexploitation et à leur fonctionnement" abbrev="Intro. systèmes" code="R1.04" heures_cours="0.0" heures_td="5.0" heures_tp="20.0" coefficient="24.0" ects="" semestre_id="1" numero="50" code_apogee="VINFR104" module_type="2">
<coefficients ue_reference="3" coef="21.0" />
</module>
</matiere>
</ue>
<ue numero="3" code_apogee="V1INFU14" titre="Compétence 4 : Gérer des données de l'information" coefficient="0.0" semestre_idx="1" coef_rcue="1.0" type="0" color="#80cb3f" ue_code="UCOD10" ects="5.0" is_external="0" acronyme="UE14" apc_niveau_libelle="Concevoir et mettre en place une base de données à partir dun cahier des charges client " apc_niveau_annee="BUT1" apc_niveau_ordre="1" reference="4">
<matiere titre="Ressources" numero="0">
<module titre="Création d'une base de données" abbrev="Création BD" code="S1.04" heures_cours="0.0" heures_td="4.0" heures_tp="2.0" coefficient="40.0" ects="" semestre_id="1" numero="40" code_apogee="VINFS104" module_type="3">
<coefficients ue_reference="4" coef="40.0" />
</module>
<module titre="Introduction aux bases de données et SQL" abbrev="Introduction BD" code="R1.05" heures_cours="12.0" heures_td="9.0" heures_tp="24.0" coefficient="36.0" ects="" semestre_id="1" numero="60" code_apogee="VINFR105" module_type="2">
<coefficients ue_reference="4" coef="36.0" />
</module>
</matiere>
</ue>
<ue numero="4" code_apogee="V1INFU15" titre="Compétence 5 : Conduire un projet" coefficient="0.0" semestre_idx="1" coef_rcue="1.0" type="0" color="#05162e" ue_code="UCOD15" ects="5.0" is_external="0" acronyme="UE15" apc_niveau_libelle="Identifier les besoins métiers des clients et des utilisateurs " apc_niveau_annee="BUT1" apc_niveau_ordre="1" reference="5">
<matiere titre="Compétence 5 : Conduire un projet" numero="1">
<module titre="Recueil de besoins" abbrev="Recueil de besoins" code="S1.05" heures_cours="0.0" heures_td="4.0" heures_tp="2.0" coefficient="40.0" ects="" semestre_id="1" numero="50" code_apogee="VINFS105" module_type="3">
<coefficients ue_reference="5" coef="40.0" />
</module>
<module titre="Économie durable et numérique" abbrev="Économie" code="R1.09" heures_cours="0.0" heures_td="18.0" heures_tp="5.0" coefficient="17.0" ects="" semestre_id="1" numero="100" code_apogee="VINFR109" module_type="2">
<coefficients ue_reference="4" coef="6.0" />
<coefficients ue_reference="6" coef="11.0" />
</module>
</matiere>
</ue>
<ue numero="5" code_apogee="V1INFU16" titre="Compétence 6 : Travailler dans une équipe informatique" coefficient="0.0" semestre_idx="1" coef_rcue="1.0" type="0" color="#548687" ue_code="UCOD16" ects="5.0" is_external="0" acronyme="UE16" apc_niveau_libelle="Identifier ses aptitudes pour travailler dans une équipe " apc_niveau_annee="BUT1" apc_niveau_ordre="1" reference="6">
<matiere titre="Compétence 6 : Travailler dans une équipe informatique" numero="1">
<module titre="Découverte de l'environnement économique et écologique" abbrev="Environnement éco." code="S1.06" heures_cours="0.0" heures_td="0.0" heures_tp="6.0" coefficient="40.0" ects="" semestre_id="1" numero="60" code_apogee="VINFS106" module_type="3">
<coefficients ue_reference="6" coef="40.0" />
</module>
<module titre="Anglais technique" abbrev="Anglais technique" code="R1.10" heures_cours="0.0" heures_td="15.0" heures_tp="9.0" coefficient="29.0" ects="" semestre_id="1" numero="110" code_apogee="VINFR110" module_type="2">
<coefficients ue_reference="3" coef="12.0" />
<coefficients ue_reference="1" coef="6.0" />
<coefficients ue_reference="6" coef="11.0" />
</module>
<module titre="Bases de la communication" abbrev="Bases de la comm" code="R1.11" heures_cours="0.0" heures_td="15.0" heures_tp="9.0" coefficient="32.0" ects="" semestre_id="1" numero="120" code_apogee="VINFR111" module_type="2">
<coefficients ue_reference="3" coef="6.0" />
<coefficients ue_reference="5" coef="15.0" />
<coefficients ue_reference="6" coef="11.0" />
</module>
<module titre="Projet professionnel et personnel" abbrev="PPP" code="R1.12" heures_cours="0.0" heures_td="8.0" heures_tp="2.0" coefficient="11.0" ects="" semestre_id="1" numero="130" code_apogee="VINFR112" module_type="2">
<coefficients ue_reference="6" coef="11.0" />
</module>
</matiere>
</ue>
<ue numero="6" code_apogee="" titre="Sport/Culture" coefficient="0.0" semestre_idx="1" coef_rcue="1.0" type="1" color="#ff4500" ue_code="X7.2" ects="0.0" is_external="0" acronyme="UE17" reference="7">
<matiere titre="Sport/Culture" numero="1">
<module titre="Sport / Culture" abbrev="Sport" code="X1.01" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="1" numero="520" code_apogee="" module_type="0" />
</matiere>
</ue>
<ue numero="7" code_apogee="V1INFU21" titre="Compétence 1: Réaliser un développement d'application" coefficient="0.0" semestre_idx="2" coef_rcue="1.0" type="0" color="#b80004" ue_code="UCOD17" ects="5.0" is_external="0" acronyme="UE21" apc_niveau_libelle="Développer des applications informatiques simples " apc_niveau_annee="BUT1" apc_niveau_ordre="1" reference="8">
<matiere titre="Compétence 1: Réaliser un développement d'application" numero="1">
<module titre="Développement orienté objets" abbrev="Développement orienté objets" code="R2.01" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="140" code_apogee="VINFR201" module_type="2">
<coefficients ue_reference="8" coef="21.0" />
<coefficients ue_reference="9" coef="15.0" />
</module>
<module titre="Développement d'applications avec IHM" abbrev="Développement d'applications avec IHM" code="R2.02" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="150" code_apogee="VINFR202" module_type="2">
<coefficients ue_reference="8" coef="21.0" />
<coefficients ue_reference="12" coef="3.0" />
<coefficients ue_reference="13" coef="4.0" />
</module>
<module titre="Qualité de développement" abbrev="Qualité de développement" code="R2.03" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="160" code_apogee="VINFR203" module_type="2">
<coefficients ue_reference="8" coef="12.0" />
<coefficients ue_reference="12" coef="6.0" />
</module>
<module titre="Développement d'une application" abbrev="Développement d'une application" code="S2.01" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="280" code_apogee="VINFS201" module_type="3">
<coefficients ue_reference="8" coef="40.0" />
</module>
</matiere>
</ue>
<ue numero="8" code_apogee="V1INFU22" titre="Compétence 2 : Optimiser des applications informatiques" coefficient="0.0" semestre_idx="2" coef_rcue="1.0" type="0" color="#f97b3d" ue_code="UCOD18" ects="5.0" is_external="0" acronyme="UE22" apc_niveau_libelle="Appréhender et construire des algorithmes " apc_niveau_annee="BUT1" apc_niveau_ordre="1" reference="9">
<matiere titre="Compétence 2 : Optimiser des applications informatiques" numero="1">
<module titre="Graphes" abbrev="Graphes" code="R2.07" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="210" code_apogee="VINFR207" module_type="2">
<coefficients ue_reference="9" coef="21.0" />
<coefficients ue_reference="12" coef="6.0" />
</module>
<module titre="Méthodes numériques" abbrev="Méthodes numériques" code="R2.09" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="230" code_apogee="VINFR209" module_type="2">
<coefficients ue_reference="9" coef="12.0" />
</module>
<module titre="Exploration algorithmique d'un problème" abbrev="Exploration algorithmique d'un problème" code="S2.02" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="290" code_apogee="VINFS202" module_type="3">
<coefficients ue_reference="9" coef="40.0" />
</module>
</matiere>
</ue>
<ue numero="9" code_apogee="V1INFU23" titre="Compétence 3 : Administrer des systèmes informatiques communicants" coefficient="0.0" semestre_idx="2" coef_rcue="1.0" type="0" color="#feb40b" ue_code="UCOD19" ects="5.0" is_external="0" acronyme="UE23" apc_niveau_libelle="Installer et configurer un poste de travail " apc_niveau_annee="BUT1" apc_niveau_ordre="1" reference="10">
<matiere titre="Compétence 3 : Administrer des systèmes informatiques communicants" numero="1">
<module titre="Communication et fonctionnement bas niveau" abbrev="Communication et fonctionnement bas niveau" code="R2.04-A" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="170" code_apogee="VINFR204" module_type="2">
<coefficients ue_reference="9" coef="6.0" />
<coefficients ue_reference="10" coef="18.0" />
</module>
<module titre="Communication et fonctionnement bas niveau" abbrev="Communication et fonctionnement bas niveau" code="R2.04-B" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="180" code_apogee="VINFR204" module_type="2">
<coefficients ue_reference="9" coef="6.0" />
<coefficients ue_reference="10" coef="18.0" />
</module>
<module titre="Introduction aux services réseaux" abbrev="Introduction aux services réseaux" code="R2.05" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="190" code_apogee="VINFR205" module_type="2">
<coefficients ue_reference="10" coef="15.0" />
</module>
<module titre="Installation de services réseau" abbrev="Installation de services réseau" code="S2.03" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="300" code_apogee="VINFS203" module_type="3">
<coefficients ue_reference="10" coef="40.0" />
</module>
</matiere>
</ue>
<ue numero="10" code_apogee="V1INFU24" titre="Compétence 4 : Gérer des données de l'information" coefficient="0.0" semestre_idx="2" coef_rcue="1.0" type="0" color="#80cb3f" ue_code="UCOD20" ects="5.0" is_external="0" acronyme="UE24" apc_niveau_libelle="Concevoir et mettre en place une base de données à partir dun cahier des charges client " apc_niveau_annee="BUT1" apc_niveau_ordre="1" reference="11">
<matiere titre="Compétence 4 : Gérer des données de l'information" numero="1">
<module titre="Exploitation d'une base de données" abbrev="Exploitation d'une base de données" code="R2.06" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="200" code_apogee="VINFR206" module_type="2">
<coefficients ue_reference="11" coef="30.0" />
</module>
<module titre="Outils numériques pour les statistiques descriptives" abbrev="Outils numériques pour les statistiques descriptives" code="R2.08" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="220" code_apogee="VINFR208" module_type="2">
<coefficients ue_reference="11" coef="12.0" />
</module>
<module titre="Exploitation d'une base de données" abbrev="Exploitation d'une base de données" code="S2.04" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="310" code_apogee="VINFS204" module_type="3">
<coefficients ue_reference="11" coef="40.0" />
</module>
</matiere>
</ue>
<ue numero="11" code_apogee="V1INFU25" titre="Compétence 5 : Conduire un projet" coefficient="0.0" semestre_idx="2" coef_rcue="1.0" type="0" color="#05162e" ue_code="UCOD21" ects="5.0" is_external="0" acronyme="UE25" apc_niveau_libelle="Identifier les besoins métiers des clients et des utilisateurs " apc_niveau_annee="BUT1" apc_niveau_ordre="1" reference="12">
<matiere titre="Compétence 5 : Conduire un projet" numero="1">
<module titre="Gestion de projet &amp; des organisations" abbrev="Gestion de projet &amp; des organisations" code="R2.10" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="240" code_apogee="VINFR210" module_type="2">
<coefficients ue_reference="11" coef="12.0" />
<coefficients ue_reference="12" coef="30.0" />
</module>
<module titre="Anglais d'entreprise" abbrev="Anglais d'entreprise" code="R2.12" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="260" code_apogee="VINFR212" module_type="2">
<coefficients ue_reference="10" coef="6.0" />
<coefficients ue_reference="11" coef="6.0" />
<coefficients ue_reference="12" coef="6.0" />
<coefficients ue_reference="13" coef="17.0" />
</module>
<module titre="Gestion d'un projet" abbrev="Gestion d'un projet" code="S2.05" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="320" code_apogee="VINFS205" module_type="3">
<coefficients ue_reference="12" coef="40.0" />
</module>
</matiere>
</ue>
<ue numero="12" code_apogee="V1INFU26" titre="Compétence 6 : Travailler dans une équipe informatique" coefficient="0.0" semestre_idx="2" coef_rcue="1.0" type="0" color="#548687" ue_code="UCOD22" ects="5.0" is_external="0" acronyme="UE26" apc_niveau_libelle="Identifier ses aptitudes pour travailler dans une équipe " apc_niveau_annee="BUT1" apc_niveau_ordre="1" reference="13">
<matiere titre="Compétence 6 : Travailler dans une équipe informatique" numero="1">
<module titre="Droit des contrats et du numérique" abbrev="Droit des contrats et du numérique" code="R2.11" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="250" code_apogee="VINFR211" module_type="2">
<coefficients ue_reference="13" coef="17.0" />
</module>
<module titre="Communication avec le milieu professionnel" abbrev="Communication avec le milieu professionnel" code="R2.13" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="270" code_apogee="VINFR213" module_type="2">
<coefficients ue_reference="8" coef="6.0" />
<coefficients ue_reference="10" coef="3.0" />
<coefficients ue_reference="12" coef="9.0" />
<coefficients ue_reference="13" coef="11.0" />
</module>
<module titre="Organisation d'un travail d'équipe" abbrev="Organisation d'un travail d'équipe" code="S2.06" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="330" code_apogee="VINFS206" module_type="3">
<coefficients ue_reference="13" coef="40.0" />
</module>
<module titre="Portfolio" abbrev="Portfolio" code="P2" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="340" code_apogee="VINFPOR2" module_type="3">
<coefficients ue_reference="8" coef="1.0" />
<coefficients ue_reference="9" coef="1.0" />
<coefficients ue_reference="10" coef="1.0" />
<coefficients ue_reference="11" coef="1.0" />
<coefficients ue_reference="12" coef="1.0" />
<coefficients ue_reference="13" coef="1.0" />
</module>
<module titre="Projet professionnel et personnel : métiers de l'informatique" abbrev="PPP" code="R2.14" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="2" numero="350" code_apogee="VINFR214" module_type="2">
<coefficients ue_reference="13" coef="11.0" />
</module>
</matiere>
</ue>
<ue numero="13" code_apogee="" titre="Compétence 1 : Réaliser un développement d'application" coefficient="0.0" semestre_idx="3" coef_rcue="1.0" type="0" color="#b80004" ue_code="UCOD71" ects="5.0" is_external="0" acronyme="UE31" apc_niveau_libelle="Partir des exigences et aller jusquà une application complète " apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="14">
<matiere titre="Compétence 1 : Réaliser un développement d'application" numero="1" />
</ue>
<ue numero="14" code_apogee="" titre="Compétence 2 : Optimiser des applications informatiques" coefficient="0.0" semestre_idx="3" coef_rcue="1.0" type="0" color="#f97b3d" ue_code="UCOD79" ects="5.0" is_external="0" acronyme="UE32" apc_niveau_libelle="Sélectionner les algorithmes adéquats pour répondre à un problème donné " apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="15">
<matiere titre="Compétence 2 : Optimiser des applications informatiques" numero="1" />
</ue>
<ue numero="15" code_apogee="" titre="Compétence 3 : Administrer des systèmes informatiques communicants" coefficient="0.0" semestre_idx="3" coef_rcue="1.0" type="0" color="#feb40b" ue_code="UCOD102" ects="5.0" is_external="0" acronyme="UE33" apc_niveau_libelle="Déployer des services dans une architecture réseau" apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="16">
<matiere titre="Compétence 3 : Administrer des systèmes informatiques communicants" numero="1" />
</ue>
<ue numero="16" code_apogee="" titre="Compétence 4 : Gérer des données de l'information" coefficient="0.0" semestre_idx="3" coef_rcue="1.0" type="0" color="#80cb3f" ue_code="UCOD103" ects="5.0" is_external="0" acronyme="UE34" apc_niveau_libelle="Optimiser une base de données, interagir avec une application et mettre en œuvre la sécurité " apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="17">
<matiere titre="Compétence 4 : Gérer des données de l'information" numero="1" />
</ue>
<ue numero="17" code_apogee="" titre="Compétence 5 : Conduire un projet" coefficient="0.0" semestre_idx="3" coef_rcue="1.0" type="0" color="#05162e" ue_code="UCOD104" ects="5.0" is_external="0" acronyme="UE35" apc_niveau_libelle="Appliquer une démarche de suivi de projet en fonction des besoins métiers des clients et des utilisateurs " apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="18">
<matiere titre="Compétence 5 : Conduire un projet" numero="1" />
</ue>
<ue numero="18" code_apogee="" titre="Compétence 6 : Travailler dans une équipe informatique" coefficient="0.0" semestre_idx="3" coef_rcue="1.0" type="0" color="#548687" ue_code="UCOD105" ects="5.0" is_external="0" acronyme="UE36" apc_niveau_libelle="Situer son rôle et ses missions au sein dune équipe informatique " apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="19">
<matiere titre="Compétence 6 : Travailler dans une équipe informatique" numero="1">
<module titre="Développement web" abbrev="Développement web" code="R3.01" heures_cours="0.0" heures_td="8.0" heures_tp="25.0" coefficient="0.0" ects="" semestre_id="3" numero="360" code_apogee="VINFR301" module_type="2">
<coefficients ue_reference="14" coef="15.0" />
<coefficients ue_reference="15" coef="5.0" />
<coefficients ue_reference="16" coef="5.0" />
<coefficients ue_reference="17" coef="10.0" />
</module>
<module titre="Développement efficace" abbrev="Développement efficace" code="R3.02" heures_cours="0.0" heures_td="8.0" heures_tp="8.0" coefficient="0.0" ects="" semestre_id="3" numero="370" code_apogee="VINFR302" module_type="2">
<coefficients ue_reference="14" coef="10.0" />
<coefficients ue_reference="15" coef="13.0" />
</module>
<module titre="Analyse" abbrev="Analyse" code="R3.03" heures_cours="0.0" heures_td="8.0" heures_tp="8.0" coefficient="0.0" ects="" semestre_id="3" numero="380" code_apogee="VINFR303" module_type="2">
<coefficients ue_reference="14" coef="12.0" />
<coefficients ue_reference="15" coef="5.0" />
<coefficients ue_reference="18" coef="10.0" />
</module>
<module titre="Qualité de développement" abbrev="Qualité de développement" code="R3.04" heures_cours="0.0" heures_td="16.0" heures_tp="24.0" coefficient="0.0" ects="" semestre_id="3" numero="390" code_apogee="VINFR304" module_type="2">
<coefficients ue_reference="14" coef="15.0" />
<coefficients ue_reference="18" coef="8.0" />
<coefficients ue_reference="19" coef="5.0" />
</module>
<module titre="Programmation système" abbrev="Programmation système" code="R3.05" heures_cours="0.0" heures_td="12.0" heures_tp="12.0" coefficient="0.0" ects="" semestre_id="3" numero="400" code_apogee="VINFR305" module_type="2">
<coefficients ue_reference="16" coef="22.0" />
</module>
<module titre="Architecture des réseaux" abbrev="Architecture des réseaux" code="R3.06" heures_cours="0.0" heures_td="8.0" heures_tp="8.0" coefficient="0.0" ects="" semestre_id="3" numero="410" code_apogee="VINFR306" module_type="2">
<coefficients ue_reference="15" coef="5.0" />
<coefficients ue_reference="16" coef="18.0" />
</module>
<module titre="SQL dans un langage de programmation" abbrev="SQL dans un langage de programmation" code="R3.07" heures_cours="0.0" heures_td="12.0" heures_tp="16.0" coefficient="0.0" ects="" semestre_id="3" numero="420" code_apogee="VINFR307" module_type="2">
<coefficients ue_reference="17" coef="25.0" />
</module>
<module titre="Probabilités" abbrev="Probabilités" code="R3.08" heures_cours="0.0" heures_td="22.0" heures_tp="7.0" coefficient="0.0" ects="" semestre_id="3" numero="430" code_apogee="VINFR308" module_type="2">
<coefficients ue_reference="15" coef="17.0" />
<coefficients ue_reference="17" coef="5.0" />
</module>
<module titre="Cryptographie et sécurité" abbrev="Cryptographie et sécurité" code="R3.09" heures_cours="0.0" heures_td="8.0" heures_tp="12.0" coefficient="0.0" ects="" semestre_id="3" numero="440" code_apogee="VINFR309" module_type="2">
<coefficients ue_reference="15" coef="10.0" />
<coefficients ue_reference="16" coef="10.0" />
<coefficients ue_reference="17" coef="5.0" />
</module>
<module titre="Management des systèmes d'information" abbrev="Management des systèmes d'information" code="R3.10" heures_cours="0.0" heures_td="24.0" heures_tp="8.0" coefficient="0.0" ects="" semestre_id="3" numero="450" code_apogee="VINFR310" module_type="2">
<coefficients ue_reference="17" coef="10.0" />
<coefficients ue_reference="18" coef="18.0" />
<coefficients ue_reference="19" coef="16.0" />
</module>
<module titre="Droit des contrats et du numérique" abbrev="Droit des contrats et du numérique" code="R3.11" heures_cours="0.0" heures_td="28.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="3" numero="460" code_apogee="VINFR311" module_type="2">
<coefficients ue_reference="14" coef="8.0" />
<coefficients ue_reference="17" coef="5.0" />
<coefficients ue_reference="18" coef="10.0" />
</module>
<module titre="Anglais professionnel" abbrev="Anglais professionnel" code="R3.12" heures_cours="0.0" heures_td="16.0" heures_tp="8.0" coefficient="0.0" ects="" semestre_id="3" numero="470" code_apogee="VINFR312" module_type="2">
<coefficients ue_reference="15" coef="5.0" />
<coefficients ue_reference="16" coef="5.0" />
<coefficients ue_reference="18" coef="7.0" />
<coefficients ue_reference="19" coef="8.0" />
</module>
<module titre="Communication professionnelle" abbrev="Communication professionnelle" code="R3.13" heures_cours="0.0" heures_td="16.0" heures_tp="8.0" coefficient="0.0" ects="" semestre_id="3" numero="480" code_apogee="VINFR313" module_type="2">
<coefficients ue_reference="18" coef="7.0" />
<coefficients ue_reference="19" coef="16.0" />
</module>
<module titre="Développement d'une application" abbrev="Développement d'une application" code="S3.01" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="3" numero="490" code_apogee="VINFS301" module_type="3">
<coefficients ue_reference="14" coef="40.0" />
<coefficients ue_reference="15" coef="40.0" />
<coefficients ue_reference="16" coef="40.0" />
<coefficients ue_reference="17" coef="40.0" />
<coefficients ue_reference="18" coef="40.0" />
<coefficients ue_reference="19" coef="40.0" />
</module>
<module titre="Portfolio" abbrev="Portfolio" code="P3" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="3" numero="500" code_apogee="" module_type="3">
<coefficients ue_reference="14" coef="1.0" />
<coefficients ue_reference="19" coef="1.0" />
<coefficients ue_reference="15" coef="1.0" />
<coefficients ue_reference="16" coef="1.0" />
<coefficients ue_reference="17" coef="1.0" />
<coefficients ue_reference="18" coef="1.0" />
</module>
<module titre="Projet personnel et professionnel" abbrev="Projet personnel et professionnel" code="R3.14" heures_cours="0.0" heures_td="8.0" heures_tp="5.0" coefficient="0.0" ects="" semestre_id="3" numero="510" code_apogee="VINFR314" module_type="2">
<coefficients ue_reference="19" coef="15.0" />
</module>
</matiere>
</ue>
<ue numero="19" code_apogee="" titre="Sport / Culture" coefficient="0.0" semestre_idx="3" coef_rcue="1.0" type="1" color="#444054" ue_code="X7.1" ects="0.0" is_external="0" acronyme="UE37" reference="20">
<matiere titre="UE7" numero="1">
<module titre="Sport / Culture" abbrev="Sport" code="X3.01" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="3" numero="680" code_apogee="" module_type="0" />
</matiere>
</ue>
<ue numero="20" code_apogee="" titre="Réaliser un développement d'application" coefficient="0.0" semestre_idx="4" coef_rcue="1.0" type="0" color="#b80004" ue_code="UCOD170" ects="5.0" is_external="0" acronyme="UE41-A" apc_niveau_libelle="Partir des exigences et aller jusquà une application complète " apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="21">
<parcours code="A" numero="0" libelle="A : Réalisation dapplications : conception, développement, validation" />
<matiere titre="UE41 Compétence 1 : Réaliser un développement d'application" numero="1">
<module titre="Développement d'une application complexe" abbrev="Développement d'une application complexe" code="S4.A.01" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="0" code_apogee="" module_type="3">
<coefficients ue_reference="22" coef="15.0" />
<coefficients ue_reference="23" coef="15.0" />
<coefficients ue_reference="24" coef="15.0" />
<coefficients ue_reference="25" coef="15.0" />
<coefficients ue_reference="26" coef="15.0" />
<coefficients ue_reference="27" coef="15.0" />
<coefficients ue_reference="21" coef="15.0" />
</module>
<module titre="Stage" abbrev="Stage" code="S4.St" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="2" code_apogee="" module_type="3">
<coefficients ue_reference="22" coef="40.0" />
<coefficients ue_reference="23" coef="40.0" />
<coefficients ue_reference="24" coef="40.0" />
<coefficients ue_reference="25" coef="40.0" />
<coefficients ue_reference="26" coef="40.0" />
<coefficients ue_reference="27" coef="40.0" />
<coefficients ue_reference="21" coef="40.0" />
</module>
<module titre="Portfolio" abbrev="Portfolio" code="P4-A" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="4" code_apogee="" module_type="3">
<coefficients ue_reference="23" coef="5.0" />
<coefficients ue_reference="24" coef="5.0" />
<coefficients ue_reference="25" coef="5.0" />
<coefficients ue_reference="26" coef="5.0" />
<coefficients ue_reference="27" coef="5.0" />
<coefficients ue_reference="21" coef="5.0" />
<parcours code="A" numero="0" libelle="A : Réalisation dapplications : conception, développement, validation" />
</module>
<module titre="Architecture logicielle" abbrev="Architecture logicielle" code="R4.01" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="530" code_apogee="" module_type="2">
<coefficients ue_reference="22" coef="23.0" />
<coefficients ue_reference="24" coef="12.0" />
<coefficients ue_reference="27" coef="4.0" />
<coefficients ue_reference="21" coef="14.0" />
</module>
<module titre="Projet Personnel et Professionnel" abbrev="Projet Personnel et Professionnel" code="R4.07" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="590" code_apogee="" module_type="2">
<coefficients ue_reference="27" coef="10.0" />
</module>
<module titre="Complétement web" abbrev="Complétement web" code="R4.A.10" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="620" code_apogee="" module_type="2">
<coefficients ue_reference="22" coef="8.0" />
<coefficients ue_reference="23" coef="4.0" />
<coefficients ue_reference="25" coef="8.0" />
<coefficients ue_reference="26" coef="4.0" />
<coefficients ue_reference="21" coef="10.0" />
</module>
<module titre="Développement pour applications mobiles" abbrev="Développement pour applications mobiles" code="R4.A.11" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="630" code_apogee="" module_type="2">
<coefficients ue_reference="22" coef="8.0" />
<coefficients ue_reference="23" coef="4.0" />
<coefficients ue_reference="25" coef="8.0" />
<coefficients ue_reference="26" coef="4.0" />
<coefficients ue_reference="21" coef="10.0" />
</module>
</matiere>
</ue>
<ue numero="21" code_apogee="" titre="Réaliser un développement d'application" coefficient="0.0" semestre_idx="4" coef_rcue="1.0" type="0" color="#b80004" ue_code="UCOD171" ects="5.0" is_external="0" acronyme="UE41-B" apc_niveau_libelle="Partir des exigences et aller jusquà une application complète " apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="22">
<parcours code="B" numero="0" libelle="B : Déploiement dapplications communicantes et sécurisées" />
<matiere titre="UE41 Compétence 1 : Réaliser un développement d'application" numero="1">
<module titre="Développement d'une application complexe" abbrev="Développement d'une application complexe" code="S4.A.01" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="1" code_apogee="" module_type="3">
<coefficients ue_reference="22" coef="15.0" />
<coefficients ue_reference="23" coef="15.0" />
<coefficients ue_reference="24" coef="15.0" />
<coefficients ue_reference="25" coef="15.0" />
<coefficients ue_reference="26" coef="15.0" />
<coefficients ue_reference="27" coef="15.0" />
<coefficients ue_reference="21" coef="15.0" />
</module>
<module titre="Stage" abbrev="Stage" code="S4.St" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="3" code_apogee="" module_type="3">
<coefficients ue_reference="22" coef="40.0" />
<coefficients ue_reference="23" coef="40.0" />
<coefficients ue_reference="24" coef="40.0" />
<coefficients ue_reference="25" coef="40.0" />
<coefficients ue_reference="26" coef="40.0" />
<coefficients ue_reference="27" coef="40.0" />
<coefficients ue_reference="21" coef="40.0" />
</module>
<module titre="Portfolio" abbrev="Portfolio" code="P4-B" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="5" code_apogee="" module_type="3">
<coefficients ue_reference="22" coef="5.0" />
<coefficients ue_reference="23" coef="5.0" />
<coefficients ue_reference="24" coef="5.0" />
<coefficients ue_reference="25" coef="5.0" />
<coefficients ue_reference="26" coef="5.0" />
<coefficients ue_reference="27" coef="5.0" />
<parcours code="B" numero="0" libelle="B : Déploiement dapplications communicantes et sécurisées" />
</module>
<module titre="Architecture logicielle" abbrev="Architecture logicielle" code="R4.01" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="530" code_apogee="" module_type="2">
<coefficients ue_reference="22" coef="27.0" />
<coefficients ue_reference="24" coef="1.0" />
<coefficients ue_reference="27" coef="4.0" />
<coefficients ue_reference="21" coef="27.0" />
</module>
<module titre="Projet Personnel et Professionnel" abbrev="Projet Personnel et Professionnel" code="R4.07" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="590" code_apogee="" module_type="2">
<coefficients ue_reference="27" coef="10.0" />
</module>
<module titre="Complétement web" abbrev="Complétement web" code="R4.A.10" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="620" code_apogee="" module_type="2">
<coefficients ue_reference="22" coef="8.0" />
<coefficients ue_reference="23" coef="4.0" />
<coefficients ue_reference="25" coef="8.0" />
<coefficients ue_reference="26" coef="4.0" />
<coefficients ue_reference="21" coef="10.0" />
</module>
<module titre="Développement pour applications mobiles" abbrev="Développement pour applications mobiles" code="R4.A.11" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="630" code_apogee="" module_type="2">
<coefficients ue_reference="22" coef="8.0" />
<coefficients ue_reference="23" coef="4.0" />
<coefficients ue_reference="25" coef="8.0" />
<coefficients ue_reference="26" coef="4.0" />
<coefficients ue_reference="21" coef="11.0" />
</module>
</matiere>
</ue>
<ue numero="22" code_apogee="" titre="Optimiser des applications informatiques" coefficient="0.0" semestre_idx="4" coef_rcue="1.0" type="0" color="#f97b3d" ue_code="UCOD172" ects="5.0" is_external="0" acronyme="UE42" apc_niveau_libelle="Sélectionner les algorithmes adéquats pour répondre à un problème donné " apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="23">
<matiere titre="UE42 Compétence 2 : Optimiser des applications informatiques" numero="1">
<module titre="Méthodes d'optimisation" abbrev="Méthodes d'optimisation" code="R4.04" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="560" code_apogee="" module_type="2">
<coefficients ue_reference="23" coef="12.0" />
</module>
<module titre="Automates et langages" abbrev="Automates et langages" code="R4.A.12" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="640" code_apogee="" module_type="2">
<coefficients ue_reference="23" coef="12.0" />
</module>
</matiere>
</ue>
<ue numero="23" code_apogee="" titre="Administrer des systèmes informatiques communicants" coefficient="0.0" semestre_idx="4" coef_rcue="1.0" type="0" color="#feb40b" ue_code="UCOD173" ects="5.0" is_external="0" acronyme="UE43" apc_niveau_libelle="Déployer des services dans une architecture réseau" apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="24">
<matiere titre="UE43 Compétence 3 : Administrer des systèmes informatiques communicants" numero="1">
<module titre="Virtualisation" abbrev="Virtualisation" code="R4.A.08" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="600" code_apogee="" module_type="2">
<coefficients ue_reference="24" coef="28.0" />
</module>
</matiere>
</ue>
<ue numero="24" code_apogee="" titre="Gérer des données de l'information" coefficient="0.0" semestre_idx="4" coef_rcue="1.0" type="0" color="#80cb3f" ue_code="UCOD174" ects="5.0" is_external="0" acronyme="UE44" apc_niveau_libelle="Optimiser une base de données, interagir avec une application et mettre en œuvre la sécurité " apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="25">
<matiere titre="UE44 Compétence 4 : Gérer des données de l'information" numero="1">
<module titre="Qualité &amp; Non-relationnel" abbrev="Qualité &amp; Non-relationnel" code="R4.03" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="550" code_apogee="" module_type="2">
<coefficients ue_reference="25" coef="18.0" />
</module>
</matiere>
</ue>
<ue numero="25" code_apogee="" titre="Conduire un projet" coefficient="0.0" semestre_idx="4" coef_rcue="1.0" type="0" color="#05162e" ue_code="UCOD175" ects="5.0" is_external="0" acronyme="UE45" apc_niveau_libelle="Appliquer une démarche de suivi de projet en fonction des besoins métiers des clients et des utilisateurs " apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="26">
<matiere titre="UE45 Compétence 5 : Conduire un projet" numero="1">
<module titre="Qualité de développement" abbrev="Qualité de développement" code="R4.02" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="540" code_apogee="" module_type="2">
<coefficients ue_reference="22" coef="8.0" />
<coefficients ue_reference="26" coef="10.0" />
<coefficients ue_reference="21" coef="8.0" />
</module>
<module titre="Management avancé des systèmes d'information" abbrev="Management avancé des systèmes d'information" code="R4.A.09" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="610" code_apogee="" module_type="2">
<coefficients ue_reference="23" coef="4.0" />
<coefficients ue_reference="26" coef="22.0" />
</module>
</matiere>
</ue>
<ue numero="26" code_apogee="" titre="Travailler dans une équipe informatique" coefficient="0.0" semestre_idx="4" coef_rcue="1.0" type="0" color="#0c0c0c" ue_code="UCOD176" ects="5.0" is_external="0" acronyme="UE46" apc_niveau_libelle="Situer son rôle et ses missions au sein dune équipe informatique " apc_niveau_annee="BUT2" apc_niveau_ordre="2" reference="27">
<parcours code="C" numero="0" libelle="C : Administration, gestion et exploitation des données" />
<parcours code="B" numero="0" libelle="B : Déploiement dapplications communicantes et sécurisées" ects="4.0" />
<parcours code="D" numero="0" libelle="D : Intégration dapplications et management du système dinformation" ects="6.0" />
<parcours code="A" numero="0" libelle="A : Réalisation dapplications : conception, développement, validation" ects="3.0" />
<matiere titre="UE46 Compétence 6 : Travailler dans une équipe informatique" numero="1">
<module titre="Anglais" abbrev="Anglais" code="R4.05" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="570" code_apogee="" module_type="2">
<coefficients ue_reference="23" coef="4.0" />
<coefficients ue_reference="27" coef="13.0" />
</module>
<module titre="Communication interne" abbrev="Communication interne" code="R4.06" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="580" code_apogee="" module_type="2">
<coefficients ue_reference="25" coef="6.0" />
<coefficients ue_reference="27" coef="13.0" />
</module>
</matiere>
</ue>
<ue numero="27" code_apogee="" titre="Sport, culture, engagement" coefficient="0.0" semestre_idx="4" coef_rcue="1.0" type="1" color="#444054" ue_code="OPT4" ects="1.0" is_external="0" acronyme="Sport, culture, engagement" reference="28">
<matiere titre="Sport, culture, engagement" numero="1">
<module titre="Bonus" abbrev="Bonus" code="Bonus" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="4" numero="690" code_apogee="" module_type="0" />
</matiere>
</ue>
<ue numero="28" code_apogee="" titre="Réaliser S5" coefficient="0.0" semestre_idx="5" coef_rcue="1.0" type="0" color="#b80004" ue_code="UCOD177" ects="5.0" is_external="0" acronyme="UE51" apc_niveau_libelle="Adapter des applications sur un ensemble de supports (embarqué, web, mobile, IoT…) " apc_niveau_annee="BUT3" apc_niveau_ordre="3" reference="29">
<matiere titre="Réaliser S5" numero="1">
<module titre="Portfolio" abbrev="" code="P5-A" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="5" numero="710" code_apogee="" module_type="3">
<parcours code="A" numero="0" libelle="A : Réalisation dapplications : conception, développement, validation" />
</module>
<module titre="Portfolio" abbrev="" code="P5-B" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="5" numero="720" code_apogee="" module_type="3">
<parcours code="B" numero="0" libelle="B : Déploiement dapplications communicantes et sécurisées" />
</module>
</matiere>
</ue>
<ue numero="29" code_apogee="" titre="Optimiser" coefficient="0.0" semestre_idx="5" coef_rcue="1.0" type="0" color="#f97b3d" ue_code="UCOD178" ects="5.0" is_external="0" acronyme="UE52" apc_niveau_libelle="Analyser et optimiser des applications" apc_niveau_annee="BUT3" apc_niveau_ordre="3" reference="30">
<parcours code="A" numero="0" libelle="A : Réalisation dapplications : conception, développement, validation" />
<matiere titre="Optimiser S5" numero="1" />
</ue>
<ue numero="31" code_apogee="" titre="Administrer" coefficient="0.0" semestre_idx="5" coef_rcue="1.0" type="0" color="#dfb03f" ue_code="UCOD180" ects="5.0" is_external="0" acronyme="UE53" apc_niveau_libelle="Faire évoluer et maintenir un système informatique communicant en conditions opérationnelles " apc_niveau_annee="BUT3" apc_niveau_ordre="3" reference="31">
<parcours code="B" numero="0" libelle="B : Déploiement dapplications communicantes et sécurisées" />
<matiere titre="Administrer S5" numero="1" />
</ue>
<ue numero="32" code_apogee="" titre="Gérer" coefficient="0.0" semestre_idx="5" coef_rcue="1.0" type="0" color="#8abd53" ue_code="UCOD181" ects="5.0" is_external="0" acronyme="UE54" apc_niveau_libelle="Administrer une base de données, concevoir et réaliser des systèmes dinformations décisionnels " apc_niveau_annee="BUT3" apc_niveau_ordre="3" reference="32">
<parcours code="C" numero="0" libelle="C : Administration, gestion et exploitation des données" />
<matiere titre="Gérer S5 - TC" numero="1" />
</ue>
<ue numero="34" code_apogee="" titre="Conduire" coefficient="0.0" semestre_idx="5" coef_rcue="1.0" type="0" color="#0c1548" ue_code="UCOD185" ects="5.0" is_external="0" acronyme="UE55" apc_niveau_libelle="Participer à la conception et à la mise en œuvre dun projet système dinformation " apc_niveau_annee="BUT3" apc_niveau_ordre="3" reference="33">
<parcours code="D" numero="0" libelle="D : Intégration dapplications et management du système dinformation" ects="6.0" />
<parcours code="C" numero="0" libelle="C : Administration, gestion et exploitation des données" ects="6.666" />
<matiere titre="Conduire S5 - TC" numero="1" />
</ue>
<ue numero="35" code_apogee="" titre="Collaborer S5" coefficient="0.0" semestre_idx="5" coef_rcue="1.0" type="0" color="#0c0c0c" ue_code="UCOD186" ects="5.0" is_external="0" acronyme="UE56" apc_niveau_libelle="Manager une équipe informatique" apc_niveau_annee="BUT3" apc_niveau_ordre="3" reference="34">
<matiere titre="Collaborer S5" numero="1" />
</ue>
<ue numero="36" code_apogee="" titre="Réaliser" coefficient="0.0" semestre_idx="6" coef_rcue="1.0" type="0" color="#b80004" ue_code="UCOD600" ects="5.0" is_external="0" acronyme="UE61" apc_niveau_libelle="Adapter des applications sur un ensemble de supports (embarqué, web, mobile, IoT…) " apc_niveau_annee="BUT3" apc_niveau_ordre="3" reference="35">
<matiere titre="Réaliser S6" numero="1">
<module titre="Portfolio" abbrev="" code="P6-A" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="6" numero="730" code_apogee="" module_type="3">
<coefficients ue_reference="35" coef="1.0" />
<coefficients ue_reference="36" coef="1.0" />
<coefficients ue_reference="40" coef="1.0" />
<parcours code="A" numero="0" libelle="A : Réalisation dapplications : conception, développement, validation" />
</module>
<module titre="Portfolio" abbrev="" code="P6-B" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="0.0" ects="" semestre_id="6" numero="740" code_apogee="" module_type="3">
<coefficients ue_reference="35" coef="1.0" />
<coefficients ue_reference="37" coef="1.0" />
<coefficients ue_reference="40" coef="1.0" />
<parcours code="B" numero="0" libelle="B : Déploiement dapplications communicantes et sécurisées" />
</module>
</matiere>
</ue>
<ue numero="37" code_apogee="" titre="Optimiser" coefficient="0.0" semestre_idx="6" coef_rcue="1.0" type="0" color="#f97b3d" ue_code="UCOD601" ects="5.0" is_external="0" acronyme="UE62" apc_niveau_libelle="Analyser et optimiser des applications" apc_niveau_annee="BUT3" apc_niveau_ordre="3" reference="36">
<parcours code="A" numero="0" libelle="A : Réalisation dapplications : conception, développement, validation" />
<matiere titre="Optimiser S6" numero="1" />
</ue>
<ue numero="39" code_apogee="" titre="Administrer" coefficient="0.0" semestre_idx="6" coef_rcue="1.0" type="0" color="#dfb03f" ue_code="UCOD603" ects="5.0" is_external="0" acronyme="UE63" apc_niveau_libelle="Faire évoluer et maintenir un système informatique communicant en conditions opérationnelles " apc_niveau_annee="BUT3" apc_niveau_ordre="3" reference="37">
<parcours code="B" numero="0" libelle="B : Déploiement dapplications communicantes et sécurisées" />
<matiere titre="Administrer S6" numero="1" />
</ue>
<ue numero="40" code_apogee="" titre="Gérer" coefficient="0.0" semestre_idx="6" coef_rcue="1.0" type="0" color="#8abd53" ue_code="UCOD604" ects="5.0" is_external="0" acronyme="UE64" apc_niveau_libelle="Administrer une base de données, concevoir et réaliser des systèmes dinformations décisionnels " apc_niveau_annee="BUT3" apc_niveau_ordre="3" reference="38">
<parcours code="C" numero="0" libelle="C : Administration, gestion et exploitation des données" />
<matiere titre="Gérer S6 - TC" numero="1" />
</ue>
<ue numero="41" code_apogee="" titre="Conduire" coefficient="0.0" semestre_idx="6" coef_rcue="1.0" type="0" color="#0c1548" ue_code="UCOD605" ects="5.0" is_external="0" acronyme="UE65" apc_niveau_libelle="Participer à la conception et à la mise en œuvre dun projet système dinformation " apc_niveau_annee="BUT3" apc_niveau_ordre="3" reference="39">
<parcours code="C" numero="0" libelle="C : Administration, gestion et exploitation des données" />
<parcours code="D" numero="0" libelle="D : Intégration dapplications et management du système dinformation" />
<matiere titre="Gérer Semestre 5 - C" numero="1" />
</ue>
<ue numero="43" code_apogee="" titre="Collaborer" coefficient="0.0" semestre_idx="6" coef_rcue="1.0" type="0" color="#0c0c0c" ue_code="UCOD607" ects="5.0" is_external="0" acronyme="UE66" apc_niveau_libelle="Manager une équipe informatique" apc_niveau_annee="BUT3" apc_niveau_ordre="3" reference="40">
<matiere titre="Collaborer S6" numero="1" />
</ue>
</formation>

View File

@ -103,14 +103,14 @@ def run_sco_basic(verbose=False) -> FormSemestre:
# --- Saisie toutes les notes de l'évaluation
for idx, etud in enumerate(etuds):
nb_changed, nb_suppress, existing_decisions = G.create_note(
etudids_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e["id"],
etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)],
)
assert not existing_decisions
assert nb_suppress == 0
assert nb_changed == 1
assert len(etudids_changed) == 1
# --- Vérifie que les notes sont prises en compte:
b = sco_bulletins.formsemestre_bulletinetud_dict(formsemestre_id, etud["etudid"])
@ -136,7 +136,7 @@ def run_sco_basic(verbose=False) -> FormSemestre:
)
# Saisie les notes des 5 premiers étudiants:
for idx, etud in enumerate(etuds[:5]):
nb_changed, nb_suppress, existing_decisions = G.create_note(
etudids_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e2["id"],
etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)],
@ -158,7 +158,7 @@ def run_sco_basic(verbose=False) -> FormSemestre:
# Saisie des notes qui manquent:
for idx, etud in enumerate(etuds[5:]):
nb_changed, nb_suppress, existing_decisions = G.create_note(
etudids_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e2["id"],
etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)],

View File

@ -239,7 +239,7 @@ def create_evaluations(formsemestre: FormSemestre):
"jour": datetime.date(2022, 3, 1) + datetime.timedelta(days=modimpl.id),
"heure_debut": "8h00",
"heure_fin": "9h00",
"description": None,
"description": f"Evaluation-{modimpl.module.code}",
"note_max": 20,
"coefficient": 1.0,
"visibulletin": True,