Merge branch 'dev93' of https://scodoc.org/git/ScoDoc/ScoDoc into new_api

This commit is contained in:
leonard_montalbano 2022-04-25 15:20:38 +02:00
commit b383c378f6
65 changed files with 2704 additions and 838 deletions

View File

@ -1072,6 +1072,29 @@ class BonusTours(BonusDirect):
)
class BonusIUTvannes(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT Vannes
<p><b>Ne concerne actuellement que les DUT et LP</b></p>
<p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'U.B.S. (sports, musique, deuxième langue, culture, etc) non
rattachés à une unité d'enseignement.
</p><p>
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés.
</p><p>
3% de ces points cumulés s'ajoutent à la moyenne générale du semestre
déjà obtenue par l'étudiant.
</p>
"""
name = "bonus_iutvannes"
displayed_name = "IUT de Vannes"
seuil_moy_gen = 10.0
proportion_point = 0.03 # 3%
classic_use_bonus_ues = False # seulement sur moy gen.
class BonusVilleAvray(BonusSport):
"""Bonus modules optionnels (sport, culture), règle IUT Ville d'Avray.

View File

@ -34,7 +34,7 @@ def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
)
df = df.merge(ins_df, how="left", left_index=True, right_index=True)
# Force columns names to integers (moduleimpl ids)
df.columns = pd.Int64Index([int(x) for x in df.columns], dtype="int")
df.columns = pd.Index([int(x) for x in df.columns], dtype=int)
# les colonnes de df sont en float (Nan) quand il n'y a
# aucun inscrit au module.
df.fillna(0, inplace=True) # les non-inscrits

View File

@ -169,9 +169,7 @@ class ModuleImplResults:
self.en_attente = True
# Force columns names to integers (evaluation ids)
evals_notes.columns = pd.Int64Index(
[int(x) for x in evals_notes.columns], dtype="int"
)
evals_notes.columns = pd.Index([int(x) for x in evals_notes.columns], dtype=int)
self.evals_notes = evals_notes
def _load_evaluation_notes(self, evaluation: Evaluation) -> pd.DataFrame:

View File

@ -100,8 +100,9 @@ def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
if (notes is None) or (len(notes) == 0):
return (pd.Series([], dtype=object), pd.Series([], dtype=int))
notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant
rangs_str = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne
rangs_int = pd.Series(index=notes.index, dtype=int) # le rang numérique pour tris
rangs_str = pd.Series("", index=notes.index, dtype=str) # le rang est une chaîne
# le rang numérique pour tris:
rangs_int = pd.Series(0, index=notes.index, dtype=int)
N = len(notes)
nb_ex = 0 # nb d'ex-aequo consécutifs en cours
notes_i = notes.iat
@ -128,4 +129,5 @@ def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
rangs_int[etudid] = i + 1
srang = "%d" % (i + 1)
rangs_str[etudid] = srang
assert rangs_int.dtype == int
return rangs_str, rangs_int

View File

@ -271,7 +271,7 @@ def compute_ue_moys_apc(
)
# Annule les coefs des modules NaN
modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 0.0, modimpl_coefs_etuds)
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
#
# Version vectorisée
@ -356,7 +356,7 @@ def compute_ue_moys_classic(
modimpl_coefs_etuds_no_nan = np.where(
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
)
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
# --------------------- Calcul des moyennes d'UE
ue_modules = np.array(
@ -367,7 +367,7 @@ def compute_ue_moys_classic(
)
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions:
coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
if coefs.dtype == np.object: # arrive sur des tableaux vides
if coefs.dtype == object: # arrive sur des tableaux vides
coefs = coefs.astype(np.float)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_ue = (
@ -462,7 +462,7 @@ def compute_mat_moys_classic(
modimpl_coefs_etuds_no_nan = np.where(
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
)
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(

View File

@ -34,7 +34,7 @@ from flask_login import current_user
from app.entreprises.models import (
Entreprise,
EntrepriseContact,
EntrepriseCorrespondant,
EntrepriseOffre,
EntrepriseOffreDepartement,
EntreprisePreferences,
@ -85,6 +85,9 @@ def get_offre_files_and_depts(offre: EntrepriseOffre, depts: list):
Retourne l'offre, les fichiers attachés a l'offre et les département liés
"""
offre_depts = EntrepriseOffreDepartement.query.filter_by(offre_id=offre.id).all()
correspondant = EntrepriseCorrespondant.query.filter_by(
id=offre.correspondant_id
).first()
if not offre_depts or check_offre_depts(depts, offre_depts):
files = []
path = os.path.join(
@ -100,13 +103,11 @@ def get_offre_files_and_depts(offre: EntrepriseOffre, depts: list):
for _file in glob.glob(f"{dir}/*"):
file = [os.path.basename(dir), os.path.basename(_file)]
files.append(file)
return [offre, files, offre_depts]
return [offre, files, offre_depts, correspondant]
return None
def send_email_notifications_entreprise(
subject, entreprise: Entreprise, contact: EntrepriseContact
):
def send_email_notifications_entreprise(subject, entreprise: Entreprise):
txt = [
"Une entreprise est en attente de validation",
"Entreprise:",
@ -116,14 +117,6 @@ def send_email_notifications_entreprise(
f"\tcode postal: {entreprise.codepostal}",
f"\tville: {entreprise.ville}",
f"\tpays: {entreprise.pays}",
"",
"Contact:",
f"nom: {contact.nom}",
f"prenom: {contact.prenom}",
f"telephone: {contact.telephone}",
f"mail: {contact.mail}",
f"poste: {contact.poste}",
f"service: {contact.service}",
]
txt = "\n".join(txt)
email.send_email(
@ -135,34 +128,42 @@ def send_email_notifications_entreprise(
return txt
def verif_contact_data(contact_data):
def verif_correspondant_data(correspondant_data):
"""
Verifie les données d'une ligne Excel (contact)
contact_data[0]: nom
contact_data[1]: prenom
contact_data[2]: telephone
contact_data[3]: mail
contact_data[4]: poste
contact_data[5]: service
contact_data[6]: entreprise_id
Verifie les données d'une ligne Excel (correspondant)
correspondant_data[0]: nom
correspondant_data[1]: prenom
correspondant_data[2]: telephone
correspondant_data[3]: mail
correspondant_data[4]: poste
correspondant_data[5]: service
correspondant_data[6]: entreprise_id
"""
# champs obligatoires
if contact_data[0] == "" or contact_data[1] == "" or contact_data[6] == "":
if (
correspondant_data[0].strip() == ""
or correspondant_data[1].strip() == ""
or correspondant_data[6].strip() == ""
):
return False
# entreprise_id existant
entreprise = Entreprise.query.filter_by(siret=contact_data[6]).first()
entreprise = Entreprise.query.filter_by(siret=correspondant_data[6].strip()).first()
if entreprise is None:
return False
# contact possède le meme nom et prénom dans la meme entreprise
contact = EntrepriseContact.query.filter_by(
nom=contact_data[0], prenom=contact_data[1], entreprise_id=entreprise.id
# correspondant possède le meme nom et prénom dans la meme entreprise
correspondant = EntrepriseCorrespondant.query.filter_by(
nom=correspondant_data[0].strip(),
prenom=correspondant_data[1].strip(),
entreprise_id=entreprise.id,
).first()
if contact is not None:
if correspondant is not None:
return False
if contact_data[2] == "" and contact_data[3] == "": # 1 moyen de contact
if (
correspondant_data[2].strip() == "" and correspondant_data[3].strip() == ""
): # 1 moyen de contact
return False
return True
@ -174,23 +175,23 @@ def verif_entreprise_data(entreprise_data):
"""
if EntreprisePreferences.get_check_siret():
for data in entreprise_data: # champs obligatoires
if data == "":
if data.strip() == "":
return False
else:
for data in entreprise_data[1:]: # champs obligatoires
if data == "":
if data.strip() == "":
return False
if EntreprisePreferences.get_check_siret():
siret = entreprise_data[0].strip() # vérification sur le siret
siret = entreprise_data[0].replace(" ", "") # vérification sur le siret
if re.match("^\d{14}$", siret) is None:
return False
try:
req = requests.get(
f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret}"
)
if req.status_code != 200:
return False
except requests.ConnectionError:
print("no internet")
if req.status_code != 200:
return False
entreprise = Entreprise.query.filter_by(siret=siret).first()
if entreprise is not None:

View File

@ -40,11 +40,17 @@ from wtforms import (
SelectMultipleField,
DateField,
BooleanField,
FieldList,
FormField,
)
from wtforms.validators import ValidationError, DataRequired, Email, Optional
from wtforms.widgets import ListWidget, CheckboxInput
from app.entreprises.models import Entreprise, EntrepriseContact, EntreprisePreferences
from app.entreprises.models import (
Entreprise,
EntrepriseCorrespondant,
EntreprisePreferences,
)
from app.models import Identite, Departement
from app.auth.models import User
@ -66,7 +72,7 @@ def _build_string_field(label, required=True, render_kw=None):
class EntrepriseCreationForm(FlaskForm):
siret = _build_string_field(
"SIRET (*)",
render_kw={"placeholder": "Numéro composé de 14 chiffres", "maxlength": "14"},
render_kw={"placeholder": "Numéro composé de 14 chiffres"},
)
nom_entreprise = _build_string_field("Nom de l'entreprise (*)")
adresse = _build_string_field("Adresse de l'entreprise (*)")
@ -74,15 +80,18 @@ class EntrepriseCreationForm(FlaskForm):
ville = _build_string_field("Ville de l'entreprise (*)")
pays = _build_string_field("Pays de l'entreprise", required=False)
nom_contact = _build_string_field("Nom du contact (*)")
prenom_contact = _build_string_field("Prénom du contact (*)")
telephone = _build_string_field("Téléphone du contact (*)", required=False)
nom_correspondant = _build_string_field("Nom du correspondant", required=False)
prenom_correspondant = _build_string_field(
"Prénom du correspondant", required=False
)
telephone = _build_string_field("Téléphone du correspondant", required=False)
mail = StringField(
"Mail du contact (*)",
"Mail du correspondant",
validators=[Optional(), Email(message="Adresse e-mail invalide")],
)
poste = _build_string_field("Poste du contact", required=False)
service = _build_string_field("Service du contact", required=False)
poste = _build_string_field("Poste du correspondant", required=False)
service = _build_string_field("Service du correspondant", required=False)
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
def validate(self):
@ -90,29 +99,46 @@ class EntrepriseCreationForm(FlaskForm):
if not FlaskForm.validate(self):
validate = False
if not self.telephone.data and not self.mail.data:
self.telephone.errors.append(
"Saisir un moyen de contact (mail ou téléphone)"
)
self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)")
validate = False
if (
self.nom_correspondant.data.strip()
or self.prenom_correspondant.data.strip()
or self.telephone.data.strip()
or self.mail.data.strip()
or self.poste.data.strip()
or self.service.data.strip()
):
if not self.nom_correspondant.data.strip():
self.nom_correspondant.errors.append("Ce champ est requis")
validate = False
if not self.prenom_correspondant.data.strip():
self.prenom_correspondant.errors.append("Ce champ est requis")
validate = False
if not self.telephone.data.strip() and not self.mail.data.strip():
self.telephone.errors.append(
"Saisir un moyen de contact (mail ou téléphone)"
)
self.mail.errors.append(
"Saisir un moyen de contact (mail ou téléphone)"
)
validate = False
return validate
def validate_siret(self, siret):
if EntreprisePreferences.get_check_siret():
siret = siret.data.strip()
if re.match("^\d{14}$", siret) is None:
siret_data = siret.data.replace(" ", "")
self.siret.data = siret_data
if re.match("^\d{14}$", siret_data) is None:
raise ValidationError("Format incorrect")
try:
req = requests.get(
f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret}"
f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret_data}"
)
if req.status_code != 200:
raise ValidationError("SIRET inexistant")
except requests.ConnectionError:
print("no internet")
if req.status_code != 200:
raise ValidationError("SIRET inexistant")
entreprise = Entreprise.query.filter_by(siret=siret).first()
raise ValidationError("Impossible de vérifier l'existance du SIRET")
entreprise = Entreprise.query.filter_by(siret=siret_data).first()
if entreprise is not None:
lien = f'<a href="/ScoDoc/entreprises/fiche_entreprise/{entreprise.id}">ici</a>'
raise ValidationError(
@ -144,6 +170,7 @@ class MultiCheckboxField(SelectMultipleField):
class OffreCreationForm(FlaskForm):
hidden_entreprise_id = HiddenField()
intitule = _build_string_field("Intitulé (*)")
description = TextAreaField(
"Description (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
@ -159,17 +186,44 @@ class OffreCreationForm(FlaskForm):
duree = _build_string_field("Durée (*)")
depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int)
expiration_date = DateField("Date expiration", validators=[Optional()])
correspondant = SelectField("Correspondant à contacté", validators=[Optional()])
fichier = FileField(
"Fichier (*)",
validators=[
Optional(),
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"),
],
)
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.correspondant.choices = [
(correspondant.id, f"{correspondant.nom} {correspondant.prenom}")
for correspondant in EntrepriseCorrespondant.query.filter_by(
entreprise_id=self.hidden_entreprise_id.data
)
]
self.depts.choices = [
(dept.id, dept.acronym) for dept in Departement.query.all()
]
def validate(self):
validate = True
if not FlaskForm.validate(self):
validate = False
if len(self.depts.data) < 1:
self.depts.errors.append("Choisir au moins un département")
validate = False
return validate
class OffreModificationForm(FlaskForm):
hidden_entreprise_id = HiddenField()
intitule = _build_string_field("Intitulé (*)")
description = TextAreaField(
"Description (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
@ -185,27 +239,79 @@ class OffreModificationForm(FlaskForm):
duree = _build_string_field("Durée (*)")
depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int)
expiration_date = DateField("Date expiration", validators=[Optional()])
correspondant = SelectField("Correspondant à contacté", validators=[Optional()])
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.correspondant.choices = [
(correspondant.id, f"{correspondant.nom} {correspondant.prenom}")
for correspondant in EntrepriseCorrespondant.query.filter_by(
entreprise_id=self.hidden_entreprise_id.data
)
]
self.depts.choices = [
(dept.id, dept.acronym) for dept in Departement.query.all()
]
def validate(self):
validate = True
if not FlaskForm.validate(self):
validate = False
class ContactCreationForm(FlaskForm):
hidden_entreprise_id = HiddenField()
nom = _build_string_field("Nom (*)")
prenom = _build_string_field("Prénom (*)")
telephone = _build_string_field("Téléphone (*)", required=False)
if len(self.depts.data) < 1:
self.depts.errors.append("Choisir au moins un département")
validate = False
return validate
class CorrespondantCreationForm(FlaskForm):
nom = _build_string_field("Nom (*)", render_kw={"class": "form-control"})
prenom = _build_string_field("Prénom (*)", render_kw={"class": "form-control"})
telephone = _build_string_field(
"Téléphone (*)", required=False, render_kw={"class": "form-control"}
)
mail = StringField(
"Mail (*)",
validators=[Optional(), Email(message="Adresse e-mail invalide")],
render_kw={"class": "form-control"},
)
poste = _build_string_field("Poste", required=False)
service = _build_string_field("Service", required=False)
poste = _build_string_field(
"Poste", required=False, render_kw={"class": "form-control"}
)
service = _build_string_field(
"Service", required=False, render_kw={"class": "form-control"}
)
# depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int)
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# self.depts.choices = [
# (dept.id, dept.acronym) for dept in Departement.query.all()
# ]
def validate(self):
validate = True
if not FlaskForm.validate(self):
validate = False
if not self.telephone.data and not self.mail.data:
self.telephone.errors.append(
"Saisir un moyen de contact (mail ou téléphone)"
)
self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)")
validate = False
return validate
class CorrespondantsCreationForm(FlaskForm):
hidden_entreprise_id = HiddenField()
correspondants = FieldList(FormField(CorrespondantCreationForm), min_entries=1)
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
def validate(self):
@ -213,28 +319,37 @@ class ContactCreationForm(FlaskForm):
if not FlaskForm.validate(self):
validate = False
contact = EntrepriseContact.query.filter_by(
entreprise_id=self.hidden_entreprise_id.data,
nom=self.nom.data,
prenom=self.prenom.data,
).first()
if contact is not None:
self.nom.errors.append("Ce contact existe déjà (même nom et prénom)")
self.prenom.errors.append("")
validate = False
if not self.telephone.data and not self.mail.data:
self.telephone.errors.append(
"Saisir un moyen de contact (mail ou téléphone)"
)
self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)")
validate = False
correspondant_list = []
for entry in self.correspondants.entries:
if entry.nom.data.strip() and entry.prenom.data.strip():
if (
entry.nom.data.strip(),
entry.prenom.data.strip(),
) in correspondant_list:
entry.nom.errors.append(
"Vous avez saisi 2 fois le même nom et prenom"
)
entry.prenom.errors.append("")
validate = False
correspondant_list.append(
(entry.nom.data.strip(), entry.prenom.data.strip())
)
correspondant = EntrepriseCorrespondant.query.filter_by(
entreprise_id=self.hidden_entreprise_id.data,
nom=entry.nom.data,
prenom=entry.prenom.data,
).first()
if correspondant is not None:
entry.nom.errors.append(
"Ce correspondant existe déjà (même nom et prénom)"
)
entry.prenom.errors.append("")
validate = False
return validate
class ContactModificationForm(FlaskForm):
hidden_contact_id = HiddenField()
class CorrespondantModificationForm(FlaskForm):
hidden_correspondant_id = HiddenField()
hidden_entreprise_id = HiddenField()
nom = _build_string_field("Nom (*)")
prenom = _build_string_field("Prénom (*)")
@ -245,21 +360,29 @@ class ContactModificationForm(FlaskForm):
)
poste = _build_string_field("Poste", required=False)
service = _build_string_field("Service", required=False)
# depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int)
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# self.depts.choices = [
# (dept.id, dept.acronym) for dept in Departement.query.all()
# ]
def validate(self):
validate = True
if not FlaskForm.validate(self):
validate = False
contact = EntrepriseContact.query.filter(
EntrepriseContact.id != self.hidden_contact_id.data,
EntrepriseContact.entreprise_id == self.hidden_entreprise_id.data,
EntrepriseContact.nom == self.nom.data,
EntrepriseContact.prenom == self.prenom.data,
correspondant = EntrepriseCorrespondant.query.filter(
EntrepriseCorrespondant.id != self.hidden_correspondant_id.data,
EntrepriseCorrespondant.entreprise_id == self.hidden_entreprise_id.data,
EntrepriseCorrespondant.nom == self.nom.data,
EntrepriseCorrespondant.prenom == self.prenom.data,
).first()
if contact is not None:
self.nom.errors.append("Ce contact existe déjà (même nom et prénom)")
if correspondant is not None:
self.nom.errors.append("Ce correspondant existe déjà (même nom et prénom)")
self.prenom.errors.append("")
validate = False
@ -273,7 +396,59 @@ class ContactModificationForm(FlaskForm):
return validate
class HistoriqueCreationForm(FlaskForm):
class ContactCreationForm(FlaskForm):
date = _build_string_field(
"Date (*)",
render_kw={"type": "datetime-local"},
)
utilisateur = _build_string_field(
"Utilisateur (*)",
render_kw={"placeholder": "Tapez le nom de l'utilisateur"},
)
notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)])
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
def validate_utilisateur(self, utilisateur):
utilisateur_data = self.utilisateur.data.upper().strip()
stm = text(
"SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:utilisateur_data"
)
utilisateur = (
User.query.from_statement(stm)
.params(utilisateur_data=utilisateur_data)
.first()
)
if utilisateur is None:
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
class ContactModificationForm(FlaskForm):
date = _build_string_field(
"Date (*)",
render_kw={"type": "datetime-local"},
)
utilisateur = _build_string_field(
"Utilisateur (*)",
render_kw={"placeholder": "Tapez le nom de l'utilisateur"},
)
notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)])
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
def validate_utilisateur(self, utilisateur):
utilisateur_data = self.utilisateur.data.upper().strip()
stm = text(
"SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:utilisateur_data"
)
utilisateur = (
User.query.from_statement(stm)
.params(utilisateur_data=utilisateur_data)
.first()
)
if utilisateur is None:
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
class StageApprentissageCreationForm(FlaskForm):
etudiant = _build_string_field(
"Étudiant (*)",
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
@ -289,6 +464,7 @@ class HistoriqueCreationForm(FlaskForm):
date_fin = DateField(
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
)
notes = TextAreaField("Notes")
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
def validate(self):
@ -319,25 +495,87 @@ class HistoriqueCreationForm(FlaskForm):
raise ValidationError("Champ incorrect (selectionnez 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)
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)")
class EnvoiOffreForm(FlaskForm):
responsable = _build_string_field(
"Responsable de formation (*)",
render_kw={"placeholder": "Tapez le nom du responsable de formation"},
responsables = FieldList(
_build_string_field(
"Responsable (*)",
render_kw={
"placeholder": "Tapez le nom du responsable de formation",
"class": "form-control",
},
),
min_entries=1,
)
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
def validate_responsable(self, responsable):
responsable_data = responsable.data.upper().strip()
stm = text(
"SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:responsable_data"
)
responsable = (
User.query.from_statement(stm)
.params(responsable_data=responsable_data)
.first()
)
if responsable is None:
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
def validate(self):
validate = True
if not FlaskForm.validate(self):
validate = False
for entry in self.responsables.entries:
if entry.data:
responsable_data = entry.data.upper().strip()
stm = text(
"SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:responsable_data"
)
responsable = (
User.query.from_statement(stm)
.params(responsable_data=responsable_data)
.first()
)
if responsable is None:
entry.errors.append("Champ incorrect (selectionnez dans la liste)")
validate = False
return validate
class AjoutFichierForm(FlaskForm):

View File

@ -11,8 +11,8 @@ class Entreprise(db.Model):
ville = db.Column(db.Text)
pays = db.Column(db.Text, default="FRANCE")
visible = db.Column(db.Boolean, default=False)
contacts = db.relationship(
"EntrepriseContact",
correspondants = db.relationship(
"EntrepriseCorrespondant",
backref="entreprise",
lazy="dynamic",
cascade="all, delete-orphan",
@ -35,12 +35,22 @@ class Entreprise(db.Model):
}
class EntrepriseContact(db.Model):
__tablename__ = "are_contacts"
# class EntrepriseSite(db.Model):
# __tablename__ = "are_sites"
# id = db.Column(db.Integer, primary_key=True)
# entreprise_id = db.Column(
# db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
# )
# nom = db.Column(db.Text)
class EntrepriseCorrespondant(db.Model):
__tablename__ = "are_correspondants"
id = db.Column(db.Integer, primary_key=True)
entreprise_id = db.Column(
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
)
# site_id = db.Column(db.Integer, db.ForeignKey("are_sites.id", ondelete="cascade"))
nom = db.Column(db.Text)
prenom = db.Column(db.Text)
telephone = db.Column(db.Text)
@ -61,6 +71,17 @@ class EntrepriseContact(db.Model):
}
class EntrepriseContact(db.Model):
__tablename__ = "are_contacts"
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.DateTime(timezone=True))
user = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="cascade"))
entreprise = db.Column(
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
)
notes = db.Column(db.Text)
class EntrepriseOffre(db.Model):
__tablename__ = "are_offres"
id = db.Column(db.Integer, primary_key=True)
@ -75,6 +96,9 @@ class EntrepriseOffre(db.Model):
duree = db.Column(db.Text)
expiration_date = db.Column(db.Date)
expired = db.Column(db.Boolean, default=False)
correspondant_id = db.Column(
db.Integer, db.ForeignKey("are_correspondants.id", ondelete="cascade")
)
def to_dict(self):
return {
@ -95,8 +119,8 @@ class EntrepriseLog(db.Model):
text = db.Column(db.Text)
class EntrepriseEtudiant(db.Model):
__tablename__ = "are_etudiants"
class EntrepriseStageApprentissage(db.Model):
__tablename__ = "are_stages_apprentissages"
id = db.Column(db.Integer, primary_key=True)
entreprise_id = db.Column(
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
@ -107,6 +131,7 @@ class EntrepriseEtudiant(db.Model):
date_fin = db.Column(db.Date)
formation_text = db.Column(db.Text)
formation_scodoc = db.Column(db.Integer)
notes = db.Column(db.Text)
class EntrepriseEnvoiOffre(db.Model):
@ -136,6 +161,15 @@ class EntrepriseOffreDepartement(db.Model):
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id", ondelete="cascade"))
# class EntrepriseCorrespondantDepartement(db.Model):
# __tablename__ = "are_correspondant_departement"
# id = db.Column(db.Integer, primary_key=True)
# correspondant_id = db.Column(
# db.Integer, db.ForeignKey("are_correspondants.id", ondelete="cascade")
# )
# dept_id = db.Column(db.Integer, db.ForeignKey("departement.id", ondelete="cascade"))
class EntreprisePreferences(db.Model):
__tablename__ = "are_preferences"
id = db.Column(db.Integer, primary_key=True)

File diff suppressed because it is too large Load Diff

View File

@ -41,11 +41,8 @@ from wtforms.fields.simple import StringField, HiddenField
from app.models import Departement
from app.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_utils as scu
from app.scodoc.sco_config_actions import (
LogoDelete,
LogoUpdate,
LogoInsert,
)
from app.scodoc.sco_config_actions import LogoInsert
from app.scodoc.sco_logos import find_logo
@ -120,6 +117,8 @@ def logo_name_validator(message=None):
class AddLogoForm(FlaskForm):
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
from app.scodoc.sco_config_actions import LogoInsert
dept_key = HiddenField()
name = StringField(
label="Nom",
@ -151,7 +150,7 @@ class AddLogoForm(FlaskForm):
dept_id = dept_key_to_id(self.dept_key.data)
if dept_id == GLOBAL:
dept_id = None
if find_logo(logoname=name.data, dept_id=dept_id) is not None:
if find_logo(logoname=name.data, dept_id=dept_id, strict=True) is not None:
raise validators.ValidationError("Un logo de même nom existe déjà")
def select_action(self):
@ -160,6 +159,14 @@ class AddLogoForm(FlaskForm):
return LogoInsert.build_action(self.data)
return None
def opened(self):
if self.do_insert.data:
if self.name.errors:
return "open"
if self.upload.errors:
return "open"
return ""
class LogoForm(FlaskForm):
"""Embed both presentation of a logo (cf. template file configuration.html)
@ -176,7 +183,18 @@ class LogoForm(FlaskForm):
)
],
)
do_delete = SubmitField("Supprimer l'image")
do_delete = SubmitField("Supprimer")
do_rename = SubmitField("Renommer")
new_name = StringField(
label="Nom",
validators=[
logo_name_validator("Nom de logo invalide (alphanumérique, _)"),
validators.Length(
max=20, message="Un nom ne doit pas dépasser 20 caractères"
),
validators.DataRequired("Nom de logo requis (alphanumériques ou '-')"),
],
)
def __init__(self, *args, **kwargs):
kwargs["meta"] = {"csrf": False}
@ -205,12 +223,25 @@ class LogoForm(FlaskForm):
self.titre = "Logo pied de page"
def select_action(self):
from app.scodoc.sco_config_actions import LogoRename
from app.scodoc.sco_config_actions import LogoUpdate
from app.scodoc.sco_config_actions import LogoDelete
if self.do_delete.data and self.can_delete:
return LogoDelete.build_action(self.data)
if self.upload.data and self.validate():
return LogoUpdate.build_action(self.data)
if self.do_rename.data and self.validate():
return LogoRename.build_action(self.data)
return None
def opened(self):
if self.upload.data and self.upload.errors:
return "open"
if self.new_name.data and self.new_name.errors:
return "open"
return ""
class DeptForm(FlaskForm):
dept_key = HiddenField()
@ -244,6 +275,23 @@ class DeptForm(FlaskForm):
return self
return self.index.get(logoname, None)
def opened(self):
if self.add_logo.opened():
return "open"
for logo_form in self.logos:
if logo_form.opened():
return "open"
return ""
def count(self):
compte = len(self.logos.entries)
if compte == 0:
return "vide"
elif compte == 1:
return "1 élément"
else:
return f"{compte} éléments"
def _make_dept_id_name():
"""Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)

View File

@ -302,22 +302,46 @@ class Identite(db.Model):
else:
date_ins = events[0].event_date
situation += date_ins.strftime(" le %d/%m/%Y")
elif inscr.etat == scu.DEF:
situation = f"défaillant en {inscr.formsemestre.titre_mois()}"
event = (
models.ScolarEvent.query.filter_by(
etudid=self.id,
formsemestre_id=inscr.formsemestre.id,
event_type="DEFAILLANCE",
)
.order_by(models.ScolarEvent.event_date)
.first()
)
if not event:
log(
f"*** situation inconsistante pour {self} (def mais pas d'event)"
)
situation += "???" # ???
else:
date_def = event.event_date
situation += date_def.strftime(" le %d/%m/%Y")
else:
situation = f"démission de {inscr.formsemestre.titre_mois()}"
# Cherche la date de demission dans scolar_events:
events = models.ScolarEvent.query.filter_by(
etudid=self.id,
formsemestre_id=inscr.formsemestre.id,
event_type="DEMISSION",
).all()
if not events:
event = (
models.ScolarEvent.query.filter_by(
etudid=self.id,
formsemestre_id=inscr.formsemestre.id,
event_type="DEMISSION",
)
.order_by(models.ScolarEvent.event_date)
.first()
)
if not event:
log(
f"*** situation inconsistante pour {self} (demission mais pas d'event)"
)
date_dem = "???" # ???
situation += "???" # ???
else:
date_dem = events[0].event_date
situation += date_dem.strftime(" le %d/%m/%Y")
date_dem = event.event_date
situation += date_dem.strftime(" le %d/%m/%Y")
else:
situation = "non inscrit" + self.e

View File

@ -36,18 +36,21 @@ class Scolog(db.Model):
class ScolarNews(db.Model):
"""Nouvelles pour page d'accueil"""
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
NEWS_FORM = "FORM" # modification formation (object=formation_id)
NEWS_SEM = "SEM" # creation semestre (object=None)
NEWS_ABS = "ABS" # saisie absence
NEWS_APO = "APO" # changements de codes APO
NEWS_FORM = "FORM" # modification formation (object=formation_id)
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
NEWS_MISC = "MISC" # unused
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
NEWS_SEM = "SEM" # creation semestre (object=None)
NEWS_MAP = {
NEWS_INSCR: "inscription d'étudiants",
NEWS_NOTE: "saisie note",
NEWS_ABS: "saisie absence",
NEWS_APO: "modif. code Apogée",
NEWS_FORM: "modification formation",
NEWS_SEM: "création semestre",
NEWS_INSCR: "inscription d'étudiants",
NEWS_MISC: "opération", # unused
NEWS_NOTE: "saisie note",
NEWS_SEM: "création semestre",
}
NEWS_TYPES = list(NEWS_MAP.keys())

View File

@ -146,7 +146,8 @@ class Formation(db.Model):
db.session.add(ue)
db.session.commit()
app.clear_scodoc_cache()
if change:
app.clear_scodoc_cache()
class Matiere(db.Model):

View File

@ -286,7 +286,7 @@ class FormSemestre(db.Model):
"""
if not self.etapes:
return ""
return ", ".join([str(x.etape_apo) for x in self.etapes])
return ", ".join(sorted([str(x.etape_apo) for x in self.etapes]))
def responsables_str(self, abbrev_prenom=True) -> str:
"""chaîne "J. Dupond, X. Martin"
@ -375,7 +375,7 @@ class FormSemestre(db.Model):
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"
def sem_modalite(self) -> str:
"""Le semestre et la modialité, ex "S2 FI" ou "S3 APP" """
"""Le semestre et la modalité, ex "S2 FI" ou "S3 APP" """
if self.semestre_id > 0:
descr_sem = f"S{self.semestre_id}"
else:
@ -433,7 +433,7 @@ notes_formsemestre_responsables = db.Table(
class FormSemestreEtape(db.Model):
"""Étape Apogée associées au semestre"""
"""Étape Apogée associée au semestre"""
__tablename__ = "notes_formsemestre_etapes"
id = db.Column(db.Integer, primary_key=True)

View File

@ -486,7 +486,10 @@ class JuryPE(object):
sesdates = [
pe_tagtable.conversionDate_StrToDate(sem["date_fin"]) for sem in sessems
] # association 1 date -> 1 semestrePE pour les semestres de l'étudiant
lastdate = max(sesdates) # date de fin de l'inscription la plus récente
if sesdates:
lastdate = max(sesdates) # date de fin de l'inscription la plus récente
else:
return False
# if PETable.AFFICHAGE_DEBUG_PE == True : pe_tools.pe_print(" derniere inscription = ", lastDateSem)
@ -585,7 +588,7 @@ class JuryPE(object):
for (i, fid) in enumerate(lesFids):
if pe_tools.PE_DEBUG:
pe_tools.pe_print(
u"%d) Semestre taggué %s (avec classement dans groupe)"
"%d) Semestre taggué %s (avec classement dans groupe)"
% (i + 1, fid)
)
self.add_semtags_in_jury(fid)
@ -620,7 +623,7 @@ class JuryPE(object):
nbinscrit = self.semTagDict[fid].get_nbinscrits()
if pe_tools.PE_DEBUG:
pe_tools.pe_print(
u" - %d étudiants classés " % (nbinscrit)
" - %d étudiants classés " % (nbinscrit)
+ ": "
+ ",".join(
[etudid for etudid in self.semTagDict[fid].get_etudids()]
@ -628,12 +631,12 @@ class JuryPE(object):
)
if lesEtudidsManquants:
pe_tools.pe_print(
u" - dont %d étudiants manquants ajoutés aux données du jury"
" - dont %d étudiants manquants ajoutés aux données du jury"
% (len(lesEtudidsManquants))
+ ": "
+ ", ".join(lesEtudidsManquants)
)
pe_tools.pe_print(u" - Export csv")
pe_tools.pe_print(" - Export csv")
filename = self.NOM_EXPORT_ZIP + self.semTagDict[fid].nom + ".csv"
self.zipfile.writestr(filename, self.semTagDict[fid].str_tagtable())
@ -742,7 +745,7 @@ class JuryPE(object):
for fid in fids_finaux:
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1:
pe_tools.pe_print(u" - semestre final %s" % (fid))
pe_tools.pe_print(" - semestre final %s" % (fid))
settag = pe_settag.SetTag(
nom, parcours=parcours
) # Le set tag fusionnant les données
@ -762,7 +765,7 @@ class JuryPE(object):
for ffid in settag.get_Fids_in_settag():
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1:
pe_tools.pe_print(
u" -> ajout du semestre tagué %s" % (ffid)
" -> ajout du semestre tagué %s" % (ffid)
)
self.add_semtags_in_jury(ffid)
settag.set_SemTagDict(
@ -791,7 +794,7 @@ class JuryPE(object):
if nbreEtudInscrits > 0:
if pe_tools.PE_DEBUG:
pe_tools.pe_print(
u"%d) %s avec interclassement sur la promo" % (i + 1, nom)
"%d) %s avec interclassement sur la promo" % (i + 1, nom)
)
if nom in ["S1", "S2", "S3", "S4"]:
settag.set_SetTagDict(self.semTagDict)
@ -802,7 +805,7 @@ class JuryPE(object):
else:
if pe_tools.PE_DEBUG:
pe_tools.pe_print(
u"%d) Pas d'interclassement %s sur la promo faute de notes"
"%d) Pas d'interclassement %s sur la promo faute de notes"
% (i + 1, nom)
)
@ -1152,11 +1155,14 @@ class JuryPE(object):
return sesSems
# **********************************************
def calcul_anneePromoDUT_d_un_etudiant(self, etudid):
def calcul_anneePromoDUT_d_un_etudiant(self, etudid) -> int:
"""Calcule et renvoie la date de diplome prévue pour un étudiant fourni avec son etudid
en fonction de sesSemestres de scolarisation"""
sesSemestres = self.get_semestresDUT_d_un_etudiant(etudid)
return max([get_annee_diplome_semestre(sem) for sem in sesSemestres])
en fonction de ses semestres de scolarisation"""
semestres = self.get_semestresDUT_d_un_etudiant(etudid)
if semestres:
return max([get_annee_diplome_semestre(sem) for sem in semestres])
else:
return None
# *********************************************
# Fonctions d'affichage pour debug
@ -1184,18 +1190,21 @@ class JuryPE(object):
chaine += "\n"
return chaine
def get_date_entree_etudiant(self, etudid):
"""Renvoie la date d'entree d'un étudiant"""
return str(
min([int(sem["annee_debut"]) for sem in self.ETUDINFO_DICT[etudid]["sems"]])
)
def get_date_entree_etudiant(self, etudid) -> str:
"""Renvoie la date d'entree d'un étudiant: "1996" """
annees_debut = [
int(sem["annee_debut"]) for sem in self.ETUDINFO_DICT[etudid]["sems"]
]
if annees_debut:
return str(min(annees_debut))
return ""
# ----------------------------------------------------------------------------------------
# Fonctions
# ----------------------------------------------------------------------------------------
def get_annee_diplome_semestre(sem):
def get_annee_diplome_semestre(sem) -> int:
"""Pour un semestre donne, décrit par le biais du dictionnaire sem usuel :
sem = {'formestre_id': ..., 'semestre_id': ..., 'annee_debut': ...},
à condition qu'il soit un semestre de formation DUT,

View File

@ -121,6 +121,7 @@ class GenTable(object):
html_with_td_classes=False, # put class=column_id in each <td>
html_before_table="", # html snippet to put before the <table> in the page
html_empty_element="", # replace table when empty
html_table_attrs="", # for html
base_url=None,
origin=None, # string added to excel and xml versions
filename="table", # filename, without extension
@ -146,6 +147,7 @@ class GenTable(object):
self.html_header = html_header
self.html_before_table = html_before_table
self.html_empty_element = html_empty_element
self.html_table_attrs = html_table_attrs
self.page_title = page_title
self.pdf_link = pdf_link
self.xls_link = xls_link
@ -383,12 +385,16 @@ class GenTable(object):
colspan_count = colspan
else:
colspan_txt = ""
attrs = row.get("_%s_td_attrs" % cid, "")
order = row.get(f"_{cid}_order")
if order:
attrs += f' data-order="{order}"'
r.append(
"<%s%s %s%s%s>%s</%s>"
% (
elem,
std,
row.get("_%s_td_attrs" % cid, ""),
attrs,
klass,
colspan_txt,
content,
@ -413,8 +419,7 @@ class GenTable(object):
cls = ' class="%s"' % " ".join(tablclasses)
else:
cls = ""
H = [self.html_before_table, "<table%s%s>" % (hid, cls)]
H = [self.html_before_table, f"<table{hid}{cls} {self.html_table_attrs}>"]
line_num = 0
# thead

View File

@ -57,7 +57,6 @@ def sidebar_common():
<a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br/>
"""
]
if current_user.has_permission(
Permission.ScoUsersAdmin
) or current_user.has_permission(Permission.ScoUsersView):

View File

@ -272,9 +272,15 @@ def _build_etud_res(e, apo_data):
r = {}
for elt_code in apo_data.apo_elts:
elt = apo_data.apo_elts[elt_code]
col_ids_type = [
(ec["apoL_a01_code"], ec["Type R\xc3\xa9s."]) for ec in elt.cols
] # les colonnes de cet élément
try:
# les colonnes de cet élément
col_ids_type = [
(ec["apoL_a01_code"], ec["Type R\xc3\xa9s."]) for ec in elt.cols
]
except KeyError as exc:
raise ScoValueError(
"Erreur: un élément sans 'Type R\xc3\xa9s.'. Vérifiez l'encodage de vos fichiers."
) from exc
r[elt_code] = {}
for (col_id, type_res) in col_ids_type:
r[elt_code][type_res] = e.cols[col_id]

View File

@ -396,7 +396,7 @@ class ApoEtud(dict):
# Element etape (annuel ou non):
if sco_formsemestre.sem_has_etape(sem, code) or (
code in sem["elt_annee_apo"].split(",")
code in {x.strip() for x in sem["elt_annee_apo"].split(",")}
):
export_res_etape = self.export_res_etape
if (not export_res_etape) and cur_sem:
@ -412,7 +412,7 @@ class ApoEtud(dict):
return VOID_APO_RES
# Element semestre:
if code in sem["elt_sem_apo"].split(","):
if code in {x.strip() for x in sem["elt_sem_apo"].split(",")}:
if self.export_res_sem:
return self.comp_elt_semestre(nt, decision, etudid)
else:
@ -421,7 +421,9 @@ class ApoEtud(dict):
# Elements UE
decisions_ue = nt.get_etud_decision_ues(etudid)
for ue in nt.get_ues_stat_dict():
if ue["code_apogee"] and code in ue["code_apogee"].split(","):
if ue["code_apogee"] and code in {
x.strip() for x in ue["code_apogee"].split(",")
}:
if self.export_res_ues:
if decisions_ue and ue["ue_id"] in decisions_ue:
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
@ -442,9 +444,10 @@ class ApoEtud(dict):
modimpls = nt.get_modimpls_dict()
module_code_found = False
for modimpl in modimpls:
if modimpl["module"]["code_apogee"] and code in modimpl["module"][
"code_apogee"
].split(","):
module = modimpl["module"]
if module["code_apogee"] and code in {
x.strip() for x in module["code_apogee"].split(",")
}:
n = nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
if n != "NI" and self.export_res_modules:
return dict(N=self.fmt_note(n), B=20, J="", R="")
@ -949,8 +952,9 @@ class ApoData(object):
return maq_elems, sem_elems
def get_codes_by_sem(self):
"""Pour chaque semestre associé, donne l'ensemble des codes Apogée qui s'y trouvent
(dans le semestre, les UE et les modules)
"""Pour chaque semestre associé, donne l'ensemble des codes de cette maquette Apogée
qui s'y trouvent (dans le semestre, les UE ou les modules).
Return: { formsemestre_id : { 'code1', 'code2', ... }}
"""
codes_by_sem = {}
for sem in self.sems_etape:
@ -961,8 +965,8 @@ class ApoData(object):
# associé à l'étape, l'année ou les semestre:
if (
sco_formsemestre.sem_has_etape(sem, code)
or (code in sem["elt_sem_apo"].split(","))
or (code in sem["elt_annee_apo"].split(","))
or (code in {x.strip() for x in sem["elt_sem_apo"].split(",")})
or (code in {x.strip() for x in sem["elt_annee_apo"].split(",")})
):
s.add(code)
continue
@ -970,17 +974,20 @@ class ApoData(object):
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
for ue in nt.get_ues_stat_dict():
if ue["code_apogee"] and code in ue["code_apogee"].split(","):
s.add(code)
continue
if ue["code_apogee"]:
codes = {x.strip() for x in ue["code_apogee"].split(",")}
if code in codes:
s.add(code)
continue
# associé à un module:
modimpls = nt.get_modimpls_dict()
for modimpl in modimpls:
if modimpl["module"]["code_apogee"] and code in modimpl["module"][
"code_apogee"
].split(","):
s.add(code)
continue
module = modimpl["module"]
if module["code_apogee"]:
codes = {x.strip() for x in module["code_apogee"].split(",")}
if code in codes:
s.add(code)
continue
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
return codes_by_sem

View File

@ -28,11 +28,10 @@
"""
"""
from app.models import ScoDocSiteConfig
from app.scodoc.sco_logos import write_logo, find_logo, delete_logo
import app
from flask import current_app
from app.scodoc.sco_logos import find_logo
class Action:
"""Base class for all classes describing an action from from config form."""
@ -42,9 +41,9 @@ class Action:
self.parameters = parameters
@staticmethod
def build_action(parameters, stream=None):
def build_action(parameters):
"""Check (from parameters) if some action has to be done and
then return list of action (or else return empty list)."""
then return list of action (or else return None)."""
raise NotImplementedError
def display(self):
@ -59,6 +58,45 @@ class Action:
GLOBAL = "_"
class LogoRename(Action):
"""Action: rename a logo
dept_id: dept_id or '-'
logo_id: logo_id (old name)
new_name: new_name
"""
def __init__(self, parameters):
super().__init__(
f"Renommage du logo {parameters['logo_id']} en {parameters['new_name']}",
parameters,
)
@staticmethod
def build_action(parameters):
dept_id = parameters["dept_key"]
if dept_id == GLOBAL:
dept_id = None
parameters["dept_id"] = dept_id
if parameters["new_name"]:
logo = find_logo(
logoname=parameters["new_name"],
dept_id=parameters["dept_key"],
strict=True,
)
if logo is None:
return LogoRename(parameters)
def execute(self):
from app.scodoc.sco_logos import rename_logo
current_app.logger.info(self.message)
rename_logo(
old_name=self.parameters["logo_id"],
new_name=self.parameters["new_name"],
dept_id=self.parameters["dept_id"],
)
class LogoUpdate(Action):
"""Action: change a logo
dept_id: dept_id or '_',
@ -83,6 +121,8 @@ class LogoUpdate(Action):
return None
def execute(self):
from app.scodoc.sco_logos import write_logo
current_app.logger.info(self.message)
write_logo(
stream=self.parameters["upload"],
@ -113,6 +153,8 @@ class LogoDelete(Action):
return None
def execute(self):
from app.scodoc.sco_logos import delete_logo
current_app.logger.info(self.message)
delete_logo(name=self.parameters["logo_id"], dept_id=self.parameters["dept_id"])
@ -136,13 +178,15 @@ class LogoInsert(Action):
parameters["dept_id"] = None
if parameters["upload"] and parameters["name"]:
logo = find_logo(
logoname=parameters["name"], dept_id=parameters["dept_key"]
logoname=parameters["name"], dept_id=parameters["dept_key"], strict=True
)
if logo is None:
return LogoInsert(parameters)
return None
def execute(self):
from app.scodoc.sco_logos import write_logo
dept_id = self.parameters["dept_key"]
if dept_id == GLOBAL:
dept_id = None

View File

@ -29,6 +29,7 @@
"""
from flask import g, request
from flask import url_for
from flask_login import current_user
import app
@ -79,7 +80,7 @@ def index_html(showcodes=0, showsemtable=0):
sco_formsemestre.sem_set_responsable_name(sem)
if showcodes:
sem["tmpcode"] = "<td><tt>%s</tt></td>" % sem["formsemestre_id"]
sem["tmpcode"] = f"<td><tt>{sem['formsemestre_id']}</tt></td>"
else:
sem["tmpcode"] = ""
# Nombre d'inscrits:
@ -121,26 +122,27 @@ def index_html(showcodes=0, showsemtable=0):
if showsemtable:
H.append(
"""<hr/>
<h2>Semestres de %s</h2>
f"""<hr>
<h2>Semestres de {sco_preferences.get_preference("DeptName")}</h2>
"""
% sco_preferences.get_preference("DeptName")
)
H.append(_sem_table_gt(sems, showcodes=showcodes).html())
H.append("</table>")
if not showsemtable:
H.append(
'<hr/><p><a href="%s?showsemtable=1">Voir tous les semestres</a></p>'
% request.base_url
f"""<hr>
<p><a class="stdlink" href="{url_for('scolar.index_html', scodoc_dept=g.scodoc_dept, showsemtable=1)
}">Voir tous les semestres ({len(othersems)} verrouillés)</a>
</p>"""
)
H.append(
"""<p><form action="%s/view_formsemestre_by_etape">
Chercher étape courante: <input name="etape_apo" type="text" size="8" spellcheck="false"></input>
</form
</p>
"""
% scu.NotesURL()
f"""<p>
<form action="{url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)}">
Chercher étape courante:
<input name="etape_apo" type="text" size="8" spellcheck="false"></input>
</form>
</p>"""
)
#
if current_user.has_permission(Permission.ScoEtudInscrit):
@ -148,23 +150,26 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8" spellchec
"""<hr>
<h3>Gestion des étudiants</h3>
<ul>
<li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a></li>
<li><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a> (ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans
le tableau de bord semestre si vous souhaitez inscrire les
étudiants importés à un semestre)</li>
<li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a>
</li>
<li><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a>
(ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans
le tableau de bord semestre si vous souhaitez inscrire les
étudiants importés à un semestre)
</li>
</ul>
"""
)
#
if current_user.has_permission(Permission.ScoEditApo):
H.append(
"""<hr>
f"""<hr>
<h3>Exports Apogée</h3>
<ul>
<li><a class="stdlink" href="%s/semset_page">Années scolaires / exports Apogée</a></li>
<li><a class="stdlink" href="{url_for('notes.semset_page', scodoc_dept=g.scodoc_dept)
}">Années scolaires / exports Apogée</a></li>
</ul>
"""
% scu.NotesURL()
)
#
H.append(
@ -176,7 +181,13 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8" spellchec
"""
)
#
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()
return (
html_sco_header.sco_header(
page_title=f"ScoDoc {g.scodoc_dept}", javascripts=["js/scolar_index.js"]
)
+ "\n".join(H)
+ html_sco_header.sco_footer()
)
def _sem_table(sems):
@ -213,7 +224,9 @@ def _sem_table(sems):
def _sem_table_gt(sems, showcodes=False):
"""Nouvelle version de la table des semestres"""
"""Nouvelle version de la table des semestres
Utilise une datatables.
"""
_style_sems(sems)
columns_ids = (
"lockimg",
@ -224,10 +237,15 @@ def _sem_table_gt(sems, showcodes=False):
"titre_resp",
"nb_inscrits",
"etapes_apo_str",
"elt_annee_apo",
"elt_sem_apo",
)
if showcodes:
columns_ids = ("formsemestre_id",) + columns_ids
html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
if current_user.has_permission(Permission.ScoEditApo):
html_class += " apo_editable"
tab = GenTable(
titles={
"formsemestre_id": "id",
@ -236,14 +254,23 @@ def _sem_table_gt(sems, showcodes=False):
"mois_debut": "Début",
"dash_mois_fin": "Année",
"titre_resp": "Semestre",
"nb_inscrits": "N", # groupicon,
"nb_inscrits": "N",
"etapes_apo_str": "Étape Apo.",
"elt_annee_apo": "Elt. année Apo.",
"elt_sem_apo": "Elt. sem. Apo.",
},
columns_ids=columns_ids,
rows=sems,
html_class="table_leftalign semlist",
table_id="semlist",
html_class_ignore_default=True,
html_class=html_class,
html_sortable=True,
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
# caption='Maquettes enregistrées',
html_table_attrs=f"""
data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
""",
html_with_td_classes=True,
preferences=sco_preferences.SemPreferences(),
)
@ -276,6 +303,16 @@ def _style_sems(sems):
sem["semestre_id_n"] = ""
else:
sem["semestre_id_n"] = sem["semestre_id"]
# pour édition codes Apogée:
sem[
"_etapes_apo_str_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
sem[
"_elt_annee_apo_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_annee_apo']}" """
sem[
"_elt_sem_apo_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
def delete_dept(dept_id: int):

View File

@ -43,10 +43,8 @@ import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import (
ScoGenError,
ScoValueError,
ScoLockedFormError,
ScoNonEmptyFormationObject,
@ -61,7 +59,6 @@ from app.scodoc import sco_edit_module
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module
_ueEditor = ndb.EditableTable(
@ -1355,93 +1352,6 @@ def ue_is_locked(ue_id):
return len(r) > 0
# ---- Table recap formation
def formation_table_recap(formation_id, format="html"):
"""Table recapitulant formation."""
from app.scodoc import sco_formations
F = sco_formations.formation_list(args={"formation_id": formation_id})
if not F:
raise ScoValueError("invalid formation_id")
F = F[0]
T = []
ues = ue_list(args={"formation_id": formation_id})
for ue in ues:
Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
for Mat in Matlist:
Modlist = sco_edit_module.module_list(
args={"matiere_id": Mat["matiere_id"]}
)
for Mod in Modlist:
Mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls(
Mod["module_id"]
)
#
T.append(
{
"UE_acro": ue["acronyme"],
"Mat_tit": Mat["titre"],
"Mod_tit": Mod["abbrev"] or Mod["titre"],
"Mod_code": Mod["code"],
"Mod_coef": Mod["coefficient"],
"Mod_sem": Mod["semestre_id"],
"nb_moduleimpls": Mod["nb_moduleimpls"],
"heures_cours": Mod["heures_cours"],
"heures_td": Mod["heures_td"],
"heures_tp": Mod["heures_tp"],
"ects": Mod["ects"],
}
)
columns_ids = [
"UE_acro",
"Mat_tit",
"Mod_tit",
"Mod_code",
"Mod_coef",
"Mod_sem",
"nb_moduleimpls",
"heures_cours",
"heures_td",
"heures_tp",
"ects",
]
titles = {
"UE_acro": "UE",
"Mat_tit": "Matière",
"Mod_tit": "Module",
"Mod_code": "Code",
"Mod_coef": "Coef.",
"Mod_sem": "Sem.",
"nb_moduleimpls": "Nb utilisé",
"heures_cours": "Cours (h)",
"heures_td": "TD (h)",
"heures_tp": "TP (h)",
"ects": "ECTS",
}
title = (
"""Formation %(titre)s (%(acronyme)s) [version %(version)s] code %(formation_code)s"""
% F
)
tab = GenTable(
columns_ids=columns_ids,
rows=T,
titles=titles,
origin="Généré par %s le " % scu.sco_version.SCONAME
+ scu.timedate_human_repr()
+ "",
caption=title,
html_caption=title,
html_class="table_leftalign",
base_url="%s?formation_id=%s" % (request.base_url, formation_id),
page_title=title,
html_title="<h2>" + title + "</h2>",
pdf_title=title,
preferences=sco_preferences.SemPreferences(),
)
return tab.make_page(format=format)
def ue_list_semestre_ids(ue: dict):
"""Liste triée des numeros de semestres des modules dans cette UE
Il est recommandable que tous les modules d'une UE aient le même indice de semestre.

View File

@ -32,11 +32,8 @@
Voir sco_apogee_csv.py pour la structure du fichier Apogée.
Stockage: utilise sco_archive.py
=> /opt/scodoc/var/scodoc/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR.csv
pour une maquette de l'année scolaire 2016, semestre 1, etape V3ASR
ou bien (à partir de ScoDoc 1678) :
/opt/scodoc/var/scodoc/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR!111.csv
exemple:
/opt/scodoc-data/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR!111.csv
pour une maquette de l'étape V3ASR version VDI 111.
La version VDI sera ignorée sauf si elle est indiquée dans l'étape du semestre.

View File

@ -34,8 +34,6 @@ from zipfile import ZipFile
import flask
from flask import url_for, g, send_file, request
# from werkzeug.utils import send_file
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc import html_sco_header

View File

@ -0,0 +1,193 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Table recap formation (avec champs éditables)
"""
import io
from zipfile import ZipFile, BadZipfile
from flask import Response
from flask import send_file, url_for
from flask import g, request
from flask_login import current_user
from app.models import Formation, FormSemestre, UniteEns, Module
from app.models.formations import Matiere
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_preferences
import app.scodoc.sco_utils as scu
# ---- Table recap formation
def formation_table_recap(formation_id, format="html") -> Response:
"""Table recapitulant formation."""
T = []
formation = Formation.query.get_or_404(formation_id)
ues = formation.ues.order_by(UniteEns.semestre_idx, UniteEns.numero)
can_edit = current_user.has_permission(Permission.ScoChangeFormation)
li = 0
for ue in ues:
# L'UE
T.append(
{
"sem": f"S{ue.semestre_idx}" if ue.semestre_idx is not None else "-",
"_sem_order": f"{li:04d}",
"code": ue.acronyme,
"titre": ue.titre or "",
"_titre_target": url_for(
"notes.ue_edit",
scodoc_dept=g.scodoc_dept,
ue_id=ue.id,
)
if can_edit
else None,
"apo": ue.code_apogee or "",
"_apo_td_attrs": f""" data-oid="{ue.id}" data-value="{ue.code_apogee or ''}" """,
"coef": ue.coefficient or "",
"ects": ue.ects,
"_css_row_class": "ue",
}
)
li += 1
matieres = ue.matieres.order_by(Matiere.numero)
for mat in matieres:
modules = mat.modules.order_by(Module.numero)
for mod in modules:
nb_moduleimpls = mod.modimpls.count()
# le module (ou ressource ou sae)
T.append(
{
"sem": f"S{mod.semestre_id}"
if mod.semestre_id is not None
else "-",
"_sem_order": f"{li:04d}",
"code": mod.code,
"titre": mod.abbrev or mod.titre,
"_titre_target": url_for(
"notes.module_edit",
scodoc_dept=g.scodoc_dept,
module_id=mod.id,
)
if can_edit
else None,
"apo": mod.code_apogee,
"_apo_td_attrs": f""" data-oid="{mod.id}" data-value="{mod.code_apogee or ''}" """,
"coef": mod.coefficient,
"nb_moduleimpls": nb_moduleimpls,
"heures_cours": mod.heures_cours,
"heures_td": mod.heures_td,
"heures_tp": mod.heures_tp,
"_css_row_class": f"mod {mod.type_abbrv()}",
}
)
columns_ids = [
"sem",
"code",
"apo",
# "mat", inutile d'afficher la matière
"titre",
"coef",
"ects",
"nb_moduleimpls",
"heures_cours",
"heures_td",
"heures_tp",
]
titles = {
"ue": "UE",
"mat": "Matière",
"titre": "Titre",
"code": "Code",
"apo": "Apo",
"coef": "Coef.",
"sem": "Sem.",
"nb_moduleimpls": "Nb utilisé",
"heures_cours": "Cours (h)",
"heures_td": "TD (h)",
"heures_tp": "TP (h)",
"ects": "ECTS",
}
title = f"""Formation {formation.titre} ({formation.acronyme})
[version {formation.version}] code {formation.formation_code}"""
html_class = "stripe cell-border compact hover order-column formation_table_recap"
if current_user.has_permission(Permission.ScoEditApo):
html_class += " apo_editable"
tab = GenTable(
columns_ids=columns_ids,
rows=T,
titles=titles,
origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}",
caption=title,
html_caption=title,
html_class=html_class,
html_class_ignore_default=True,
html_table_attrs=f"""
data-apo_ue_save_url="{url_for('notes.ue_set_apo', scodoc_dept=g.scodoc_dept)}"
data-apo_mod_save_url="{url_for('notes.module_set_apo', scodoc_dept=g.scodoc_dept)}"
""",
html_with_td_classes=True,
base_url=f"{request.base_url}?formation_id={formation_id}",
page_title=title,
html_title=f"<h2>{title}</h2>",
pdf_title=title,
preferences=sco_preferences.SemPreferences(),
table_id="formation_table_recap",
)
return tab.make_page(format=format, javascripts=["js/formation_recap.js"])
def export_recap_formations_annee_scolaire(annee_scolaire):
"""Exporte un zip des recap (excel) des formatons de tous les semestres
de l'année scolaire indiquée.
"""
annee_scolaire = int(annee_scolaire)
data = io.BytesIO()
zip_file = ZipFile(data, "w")
formsemestres = FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id).filter(
FormSemestre.date_debut >= scu.date_debut_anne_scolaire(annee_scolaire),
FormSemestre.date_debut <= scu.date_fin_anne_scolaire(annee_scolaire),
)
formation_ids = {formsemestre.formation.id for formsemestre in formsemestres}
for formation_id in formation_ids:
formation = Formation.query.get(formation_id)
xls = formation_table_recap(formation_id, format="xlsx").data
filename = (
scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX
)
zip_file.writestr(filename, xls)
zip_file.close()
data.seek(0)
return send_file(
data,
mimetype="application/zip",
download_name=f"formations-{g.scodoc_dept}-{annee_scolaire}-{annee_scolaire+1}.zip",
as_attachment=True,
)

View File

@ -95,9 +95,12 @@ _formsemestreEditor = ndb.EditableTable(
def get_formsemestre(formsemestre_id, raise_soft_exc=False):
"list ONE formsemestre"
if formsemestre_id is None:
raise ValueError(f"get_formsemestre: id manquant")
if formsemestre_id in g.stored_get_formsemestre:
return g.stored_get_formsemestre[formsemestre_id]
if not isinstance(formsemestre_id, int):
log(f"get_formsemestre: invalid id '{formsemestre_id}'")
raise ScoInvalidIdType("formsemestre_id must be an integer !")
sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
if not sems:
@ -141,7 +144,6 @@ def _formsemestre_enrich(sem):
"""Ajoute champs souvent utiles: titre + annee et dateord (pour tris)"""
# imports ici pour eviter refs circulaires
from app.scodoc import sco_formsemestre_edit
from app.scodoc import sco_etud
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
@ -350,6 +352,7 @@ def read_formsemestre_etapes(formsemestre_id): # OBSOLETE
"""SELECT etape_apo
FROM notes_formsemestre_etapes
WHERE formsemestre_id = %(formsemestre_id)s
ORDER BY etape_apo
""",
{"formsemestre_id": formsemestre_id},
)

View File

@ -28,6 +28,7 @@
"""Tableau de bord semestre
"""
import datetime
from flask import current_app
from flask import g
from flask import request
@ -760,8 +761,7 @@ def _make_listes_sem(sem, with_absences=True):
)
formsemestre_id = sem["formsemestre_id"]
# calcule dates 1er jour semaine pour absences
weekday = datetime.datetime.today().weekday()
try:
if with_absences:
first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday()
@ -780,8 +780,8 @@ def _make_listes_sem(sem, with_absences=True):
<select name="datedebut" class="noprint">
"""
date = first_monday
for jour in sco_abs.day_names():
form_abs_tmpl += f'<option value="{date}">{jour}s</option>'
for idx, jour in enumerate(sco_abs.day_names()):
form_abs_tmpl += f"""<option value="{date}" {'selected' if idx == weekday else ''}>{jour}s</option>"""
date = date.next_day()
form_abs_tmpl += f"""
</select>
@ -966,6 +966,7 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
def formsemestre_status(formsemestre_id=None):
"""Tableau de bord semestre HTML"""
# porté du DTML
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
@ -987,7 +988,9 @@ def formsemestre_status(formsemestre_id=None):
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
H = [
html_sco_header.sco_header(page_title="Semestre %s" % sem["titreannee"]),
html_sco_header.sco_header(
page_title=f"{formsemestre.sem_modalite()} {formsemestre.titre_annee()}"
),
'<div class="formsemestre_status">',
formsemestre_status_head(
formsemestre_id=formsemestre_id, page_title="Tableau de bord"

View File

@ -89,6 +89,11 @@ def write_logo(stream, name, dept_id=None):
Logo(logoname=name, dept_id=dept_id).create(stream)
def rename_logo(old_name, new_name, dept_id):
logo = find_logo(old_name, dept_id, True)
logo.rename(new_name)
def list_logos():
"""Crée l'inventaire de tous les logos existants.
L'inventaire se présente comme un dictionnaire de dictionnaire de Logo:
@ -285,6 +290,20 @@ class Logo:
dt = path.stat().st_mtime
return path.stat().st_mtime
def rename(self, new_name):
"""Change le nom (pas le département)
Les éléments non utiles ne sont pas recalculés (car rechargés lors des accès ultérieurs)
"""
old_path = Path(self.filepath)
self.logoname = secure_filename(new_name)
if not self.logoname:
self.logoname = "*** *** nom de logo invalide *** à changer ! *** ***"
else:
new_path = os.path.sep.join(
[self.dirpath, self.prefix + self.logoname + "." + self.suffix]
)
old_path.rename(new_path)
def guess_image_type(stream) -> str:
"guess image type from header in stream"

View File

@ -37,12 +37,20 @@ import xml.dom.minidom
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc import sco_cache
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
SCO_CACHE_ETAPE_FILENAME = os.path.join(scu.SCO_TMP_DIR, "last_etapes.xml")
class ApoInscritsEtapeCache(sco_cache.ScoDocCache):
"""Cache liste des inscrits à une étape Apogée"""
timeout = 10 * 60 # 10 minutes
prefix = "APOINSCRETAP"
def has_portal():
"True if we are connected to a portal"
return get_portal_url()
@ -139,14 +147,20 @@ get_maquette_url = _PI.get_maquette_url
get_portal_api_version = _PI.get_portal_api_version
def get_inscrits_etape(code_etape, anneeapogee=None, ntrials=2):
def get_inscrits_etape(code_etape, anneeapogee=None, ntrials=4, use_cache=True):
"""Liste des inscrits à une étape Apogée
Result = list of dicts
ntrials: try several time the same request, useful for some bad web services
use_cache: use (redis) cache
"""
log("get_inscrits_etape: code=%s anneeapogee=%s" % (code_etape, anneeapogee))
if anneeapogee is None:
anneeapogee = str(time.localtime()[0])
if use_cache:
obj = ApoInscritsEtapeCache.get((code_etape, anneeapogee))
if obj:
log("get_inscrits_etape: using cached data")
return obj
etud_url = get_etud_url()
api_ver = get_portal_api_version()
@ -189,6 +203,8 @@ def get_inscrits_etape(code_etape, anneeapogee=None, ntrials=2):
return False # ??? pas d'annee d'inscription dans la réponse
etuds = [e for e in etuds if check_inscription(e)]
if use_cache and etuds:
ApoInscritsEtapeCache.set((code_etape, anneeapogee), etuds)
return etuds

View File

@ -103,7 +103,7 @@ def formsemestre_recapcomplet(
return data
H = [
html_sco_header.sco_header(
page_title="Récapitulatif",
page_title=f"{formsemestre.sem_modalite()}: moyennes",
no_side_bar=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/table_recap.js"],

View File

@ -704,6 +704,7 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
typ=ScolarNews.NEWS_INSCR,
text="Import Apogée de %d étudiants en " % len(created_etudids),
obj=sem["formsemestre_id"],
max_frequency=10 * 60, # 10'
)

View File

@ -728,15 +728,13 @@ def sendResult(
def send_file(data, filename="", suffix="", mime=None, attached=None):
"""Build Flask Response for file download of given type
By default (attached is None), json and xml are inlined and otrher types are attached.
By default (attached is None), json and xml are inlined and other types are attached.
"""
if attached is None:
if mime == XML_MIMETYPE or mime == JSON_MIMETYPE:
attached = False
else:
attached = True
# if attached and not filename:
# raise ValueError("send_file: missing attachement filename")
if filename:
if suffix:
filename += suffix
@ -755,7 +753,7 @@ def send_docx(document, filename):
buffer.seek(0)
return flask.send_file(
buffer,
attachment_filename=sanitize_filename(filename),
download_name=sanitize_filename(filename),
mimetype=DOCX_MIMETYPE,
)
@ -873,6 +871,20 @@ def annee_scolaire_debut(year, month):
return int(year) - 1
def date_debut_anne_scolaire(annee_scolaire: int) -> datetime:
"""La date de début de l'année scolaire
= 1er aout
"""
return datetime.datetime(year=annee_scolaire, month=8, day=1)
def date_fin_anne_scolaire(annee_scolaire: int) -> datetime:
"""La date de fin de l'année scolaire
= 31 juillet de l'année suivante
"""
return datetime.datetime(year=annee_scolaire + 1, month=7, day=31)
def sem_decale_str(sem):
"""'D' si semestre decalé, ou ''"""
# considère "décalé" les semestre impairs commençant entre janvier et juin

View File

@ -15,6 +15,10 @@
}
.form-error {
color: #a94442;
}
.nav-entreprise>ul>li>a:hover {
color: red;
}
@ -50,23 +54,23 @@
margin-bottom: -5px;
}
.entreprise, .contact, .offre {
.entreprise, .correspondant, .offre {
border: solid 2px;
border-radius: 10px;
padding: 10px;
margin-bottom: 10px;
}
.contacts-et-offres {
.correspondants-et-offres {
display: flex;
justify-content: space-between;
}
.contacts-et-offres > div {
.correspondants-et-offres > div {
flex: 1 0 0;
}
.contacts-et-offres > div:nth-child(2) {
.correspondants-et-offres > div:nth-child(2) {
margin-left: 20px;
}

View File

@ -427,8 +427,8 @@ table.semlist tr td {
border: none;
}
table.semlist tr a.stdlink,
table.semlist tr a.stdlink:visited {
table.semlist tbody tr a.stdlink,
table.semlist tbody tr a.stdlink:visited {
color: navy;
text-decoration: none;
}
@ -442,32 +442,86 @@ table.semlist tr td.semestre_id {
text-align: right;
}
table.semlist tr td.modalite {
table.semlist tbody tr td.modalite {
text-align: left;
padding-right: 1em;
}
div#gtrcontent table.semlist tr.css_S-1 {
/***************************/
/* Statut des cellules */
/***************************/
.sco_selected {
outline: 1px solid #c09;
}
.sco_modifying {
outline: 2px dashed #c09;
background-color: white !important;
}
.sco_wait {
outline: 2px solid #c90;
}
.sco_good {
outline: 2px solid #9c0;
}
.sco_modified {
font-weight: bold;
color: indigo
}
/***************************/
/* Message */
/***************************/
.message {
position: fixed;
bottom: 100%;
left: 50%;
z-index: 10;
padding: 20px;
border-radius: 0 0 10px 10px;
background: #ec7068;
background: #90c;
color: #FFF;
font-size: 24px;
animation: message 3s;
transform: translate(-50%, 0);
}
@keyframes message {
20% {
transform: translate(-50%, 100%)
}
80% {
transform: translate(-50%, 100%)
}
}
div#gtrcontent table.semlist tbody tr.css_S-1 td {
background-color: rgb(251, 250, 216);
}
div#gtrcontent table.semlist tr.css_S1 {
div#gtrcontent table.semlist tbody tr.css_S1 td {
background-color: rgb(92%, 95%, 94%);
}
div#gtrcontent table.semlist tr.css_S2 {
div#gtrcontent table.semlist tbody tr.css_S2 td {
background-color: rgb(214, 223, 236);
}
div#gtrcontent table.semlist tr.css_S3 {
div#gtrcontent table.semlist tbody tr.css_S3 td {
background-color: rgb(167, 216, 201);
}
div#gtrcontent table.semlist tr.css_S4 {
div#gtrcontent table.semlist tbody tr.css_S4 td {
background-color: rgb(131, 225, 140);
}
div#gtrcontent table.semlist tr.css_MEXT {
div#gtrcontent table.semlist tbody tr.css_MEXT td {
color: #0b6e08;
}
@ -1001,6 +1055,14 @@ span.wtf-field ul.errors li {
display: list-item !important;
}
.configuration_logo entete_dept {
display: inline-block;
}
.configuration_logo .effectifs {
float: right;
}
.configuration_logo h1 {
display: inline-block;
}
@ -3909,4 +3971,18 @@ table.evaluations_recap td.nb_abs,
table.evaluations_recap td.nb_att,
table.evaluations_recap td.nb_exc {
text-align: center;
}
/* ------------- Tableau récap formation ------------ */
table.formation_table_recap tr.ue td {
font-weight: bold;
}
table.formation_table_recap td.coef,
table.formation_table_recap td.ects,
table.formation_table_recap td.nb_moduleimpls,
table.formation_table_recap td.heures_cours,
table.formation_table_recap td.heures_td,
table.formation_table_recap td.heures_tp {
text-align: right;
}

View File

@ -52,6 +52,9 @@ div.title_STANDARD, .champs_STANDARD {
div.title_MALUS {
background-color: #ff4700;
}
.sums {
background: #ddd;
}
/***************************/
/* Statut des cellules */
/***************************/

View File

@ -0,0 +1,28 @@
/* Page accueil département */
var apo_ue_editor = null;
var apo_mod_editor = null;
$(document).ready(function () {
var table_options = {
"paging": false,
"searching": false,
"info": false,
/* "autoWidth" : false, */
"fixedHeader": {
"header": true,
"footer": true
},
"orderCellsTop": true, // cellules ligne 1 pour tri
"aaSorting": [], // Prevent initial sorting
};
$('table#formation_table_recap').DataTable(table_options);
let table_editable = document.querySelector("table#formation_table_recap.apo_editable");
if (table_editable) {
let apo_ue_save_url = document.querySelector("table#formation_table_recap.apo_editable").dataset.apo_ue_save_url;
apo_ue_editor = new ScoFieldEditor("table#formation_table_recap tr.ue td.apo", apo_ue_save_url, false);
let apo_mod_save_url = document.querySelector("table#formation_table_recap.apo_editable").dataset.apo_mod_save_url;
apo_mod_editor = new ScoFieldEditor("table#formation_table_recap tr.mod td.apo", apo_mod_save_url, false);
}
});

View File

@ -133,3 +133,134 @@ function readOnlyTags(nodes) {
node.after('<span class="ro_tags"><span class="ro_tag">' + tags.join('</span><span class="ro_tag">') + '</span></span>');
}
}
/* Editeur pour champs
* Usage: créer un élément avec data-oid (object id)
* La méthode d'URL save sera appelée en POST avec deux arguments: oid et value,
* value contenant la valeur du champs.
* Inspiré par les codes et conseils de Seb. L.
*/
class ScoFieldEditor {
constructor(selector, save_url, read_only) {
this.save_url = save_url;
this.read_only = read_only;
this.selector = selector;
this.installListeners();
}
// Enregistre l'élément obj
save(obj) {
var value = obj.innerText.trim();
if (value.length == 0) {
value = "";
}
if (value == obj.dataset.value) {
return true; // Aucune modification, pas d'enregistrement mais on continue normalement
}
obj.classList.add("sco_wait");
// DEBUG
// console.log(`
// data : ${value},
// id: ${obj.dataset.oid}
// `);
$.post(this.save_url,
{
oid: obj.dataset.oid,
value: value,
},
function (result) {
obj.classList.remove("sco_wait");
obj.classList.add("sco_modified");
}
);
return true;
}
/*****************************/
/* Gestion des évènements */
/*****************************/
installListeners() {
if (this.read_only) {
return;
}
document.body.addEventListener("keydown", this.key);
let editor = this;
this.handleSelectCell = (event) => { editor.selectCell(event) };
this.handleModifCell = (event) => { editor.modifCell(event) };
this.handleBlur = (event) => { editor.blurCell(event) };
this.handleKeyCell = (event) => { editor.keyCell(event) };
document.querySelectorAll(this.selector).forEach(cellule => {
cellule.addEventListener("click", this.handleSelectCell);
cellule.addEventListener("dblclick", this.handleModifCell);
cellule.addEventListener("blur", this.handleBlur);
});
}
/*********************************/
/* Interaction avec les cellules */
/*********************************/
blurCell(event) {
let currentModif = document.querySelector(".sco_modifying");
if (currentModif) {
if (!this.save(currentModif)) {
return;
}
}
}
selectCell(event) {
let obj = event.currentTarget;
if (obj) {
if (obj.classList.contains("sco_modifying")) {
return; // Cellule en cours de modification, ne pas sélectionner.
}
let currentModif = document.querySelector(".sco_modifying");
if (currentModif) {
if (!this.save(currentModif)) {
return;
}
}
this.unselectCell();
obj.classList.add("sco_selected");
}
}
unselectCell() {
document.querySelectorAll(".sco_selected, .sco_modifying").forEach(cellule => {
cellule.classList.remove("sco_selected", "sco_modifying");
cellule.removeAttribute("contentEditable");
cellule.removeEventListener("keydown", this.handleKeyCell);
});
}
modifCell(event) {
let obj = event.currentTarget;
if (obj) {
obj.classList.add("sco_modifying");
obj.contentEditable = true;
obj.addEventListener("keydown", this.handleKeyCell);
obj.focus();
}
}
key(event) {
switch (event.key) {
case "Enter":
this.modifCell(document.querySelector(".sco_selected"));
event.preventDefault();
break;
}
}
keyCell(event) {
let obj = event.currentTarget;
if (obj) {
if (event.key == "Enter") {
event.preventDefault();
event.stopPropagation();
if (!this.save(obj)) {
return
}
obj.classList.remove("sco_modifying");
// ArrowMove(0, 1);
// modifCell(document.querySelector(".sco_selected"));
this.unselectCell();
}
}
}
}

View File

@ -0,0 +1,33 @@
/* Page accueil département */
var apo_editor = null;
var elt_annee_apo_editor = null;
var elt_sem_apo_editor = null;
$(document).ready(function () {
var table_options = {
"paging": false,
"searching": false,
"info": false,
/* "autoWidth" : false, */
"fixedHeader": {
"header": true,
"footer": true
},
"orderCellsTop": true, // cellules ligne 1 pour tri
"aaSorting": [], // Prevent initial sorting
};
$('table.semlist').DataTable(table_options);
let table_editable = document.querySelector("table#semlist.apo_editable");
if (table_editable) {
let save_url = document.querySelector("table#semlist.apo_editable").dataset.apo_save_url;
apo_editor = new ScoFieldEditor(".etapes_apo_str", save_url, false);
save_url = document.querySelector("table#semlist.apo_editable").dataset.elt_annee_apo_save_url;
elt_annee_apo_editor = new ScoFieldEditor(".elt_annee_apo", save_url, false);
save_url = document.querySelector("table#semlist.apo_editable").dataset.elt_sem_apo_save_url;
elt_sem_apo_editor = new ScoFieldEditor(".elt_sem_apo", save_url, false);
}
});

View File

@ -4,8 +4,13 @@
/*****************************/
/* Mise en place des données */
/*****************************/
let lastX;
let lastY;
function build_table(data) {
let output = "";
let sumsUE = {};
let sumsRessources = {};
data.forEach((cellule) => {
output += `
@ -29,11 +34,61 @@ function build_table(data) {
">
${cellule.data}
</div>`;
if (cellule.editable) {
sumsRessources[cellule.y] = (sumsRessources[cellule.y] ?? 0) + (parseFloat(cellule.data) || 0);
sumsUE[cellule.x] = (sumsUE[cellule.x] ?? 0) + (parseFloat(cellule.data) || 0);
}
})
output += showSums(sumsRessources, sumsUE);
document.querySelector(".tableau").innerHTML = output;
installListeners();
}
function showSums(sumsRessources, sumsUE) {
lastX = Object.keys(sumsUE).length + 2;
lastY = Object.keys(sumsRessources).length + 2;
let output = "";
Object.entries(sumsUE).forEach(([num, value]) => {
output += `
<div
class="sums"
data-editable="false"
data-x="${num}"
data-y="${lastY}"
style="
--x:${num};
--y:${lastY};
--nbX:1;
--nbY:1;
">
${value}
</div>`;
})
Object.entries(sumsRessources).forEach(([num, value]) => {
output += `
<div
class="sums"
data-editable="false"
data-x="${lastX}"
data-y="${num}"
style="
--x:${lastX};
--y:${num};
--nbX:1;
--nbY:1;
">
${value}
</div>`;
})
return output;
}
/*****************************/
/* Gestion des évènements */
/*****************************/
@ -54,6 +109,7 @@ function installListeners() {
}
}
});
cellule.addEventListener("input", processSums);
});
}
@ -120,11 +176,28 @@ function keyCell(event) {
return
}
this.classList.remove("modifying");
let selected = document.querySelector(".selected");
ArrowMove(0, 1);
modifCell(document.querySelector(".selected"));
if (selected != document.querySelector(".selected")) {
modifCell(document.querySelector(".selected"));
}
}
}
function processSums() {
let sum = 0;
document.querySelectorAll(`[data-editable="true"][data-x="${this.dataset.x}"]`).forEach(e => {
sum += parseFloat(e.innerText) || 0;
})
document.querySelector(`.sums[data-x="${this.dataset.x}"][data-y="${lastY}"]`).innerText = sum;
sum = 0;
document.querySelectorAll(`[data-editable="true"][data-y="${this.dataset.y}"]`).forEach(e => {
sum += parseFloat(e.innerText) || 0;
})
document.querySelector(`.sums[data-x="${lastX}"][data-y="${this.dataset.y}"]`).innerText = sum;
}
/******************************/
/* Affichage d'un message */
/******************************/

View File

@ -31,6 +31,18 @@
display: none;
}`;
releve.shadowRoot.appendChild(style);
})
.catch(error => {
let div = document.createElement("div");
div.innerText = "Une erreur s'est produite lors du transfère des données.";
div.style.fontSize = "24px";
div.style.color = "#d93030";
let releve = document.querySelector("releve-but");
releve.after(div);
releve.remove();
throw 'Fin du script - données invalides';
});
document.querySelector("html").style.scrollBehavior = "smooth";
</script>

View File

@ -20,7 +20,7 @@
{% endmacro %}
{% macro render_add_logo(add_logo_form) %}
<details>
<details {{ add_logo_form.opened() }}>
<summary>
<h3>Ajouter un logo</h3>
</summary>
@ -33,7 +33,7 @@
{% endmacro %}
{% macro render_logo(dept_form, logo_form) %}
<details>
<details {{ logo_form.opened() }}>
{{ logo_form.hidden_tag() }}
<summary>
{% if logo_form.titre %}
@ -65,6 +65,11 @@
<span class="wtf-field">{{ render_field(logo_form.upload, False, onchange="submit_form()") }}</span>
</div>
{% if logo_form.can_delete %}
<div class="action_label">Renommer</div>
<div class="action_button">
{{ render_field(logo_form.new_name, False) }}
{{ render_field(logo_form.do_rename, False, onSubmit="submit_form()") }}
</div>
<div class="action_label">Supprimer l'image</div>
<div class="action_button">
{{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }}
@ -97,20 +102,24 @@
<div class="configuration_logo">
<h1>Bibliothèque de logos</h1>
{% for dept_entry in form.depts.entries %}
<details>
{% set dept_form = dept_entry.form %}
{{ dept_entry.form.hidden_tag() }}
<details {{ dept_form.opened() }}>
<summary>
{% if dept_entry.form.is_local() %}
<h2>Département {{ dept_form.dept_name.data }}</h2>
<div class="sco_help">Les paramètres donnés sont spécifiques à ce département.<br />
Les logos du département se substituent aux logos de même nom définis globalement:</div>
{% else %}
<h2>Logos généraux</h2>
<div class="sco_help">Les images de cette section sont utilisé pour tous les départements,
mais peuvent être redéfinies localement au niveau de chaque département
(il suffit de définir un logo local de même nom)</div>
{% endif %}
<span class="entete_dept">
{% if dept_entry.form.is_local() %}
<h2>Département {{ dept_form.dept_name.data }}</h2>
<h3 class="effectifs">{{ dept_form.count() }}</h3>
<div class="sco_help">Les paramètres donnés sont spécifiques à ce département.<br />
Les logos du département se substituent aux logos de même nom définis globalement:</div>
{% else %}
<h2>Logos généraux</h2>
<h3 class="effectifs">{{ dept_form.count() }}</h3>
<div class="sco_help">Les images de cette section sont utilisé pour tous les départements,
mais peuvent être redéfinies localement au niveau de chaque département
(il suffit de définir un logo local de même nom)</div>
{% endif %}
</span>
</summary>
<div>
{{ render_logos(dept_form) }}

View File

@ -1,26 +0,0 @@
{# -*- mode: jinja-html -*- #}
<div class="contact">
<div>
Nom : {{ contact.nom }}<br>
Prénom : {{ contact.prenom }}<br>
{% if contact.telephone %}
Téléphone : {{ contact.telephone }}<br>
{% endif %}
{% if contact.mail %}
Mail : {{ contact.mail }}<br>
{% endif %}
{% if contact.poste %}
Poste : {{ contact.poste }}<br>
{% endif %}
{% if contact.service %}
Service : {{ contact.service }}<br>
{% endif %}
</div>
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<div class="parent-btn">
<a class="btn btn-primary" href="{{ url_for('entreprises.edit_contact', id=contact.id) }}">Modifier contact</a>
<a class="btn btn-danger" href="{{ url_for('entreprises.delete_contact', id=contact.id) }}">Supprimer contact</a>
</div>
{% endif %}
</div>

View File

@ -0,0 +1,26 @@
{# -*- mode: jinja-html -*- #}
<div class="correspondant">
<div>
Nom : {{ correspondant.nom }}<br>
Prénom : {{ correspondant.prenom }}<br>
{% if correspondant.telephone %}
Téléphone : {{ correspondant.telephone }}<br>
{% endif %}
{% if correspondant.mail %}
Mail : {{ correspondant.mail }}<br>
{% endif %}
{% if correspondant.poste %}
Poste : {{ correspondant.poste }}<br>
{% endif %}
{% if correspondant.service %}
Service : {{ correspondant.service }}<br>
{% endif %}
</div>
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<div class="parent-btn">
<a class="btn btn-primary" href="{{ url_for('entreprises.edit_correspondant', id=correspondant.id) }}">Modifier correspondant</a>
<a class="btn btn-danger" href="{{ url_for('entreprises.delete_correspondant', id=correspondant.id) }}">Supprimer correspondant</a>
</div>
{% endif %}
</div>

View File

@ -1,6 +1,7 @@
{# -*- mode: jinja-html -*- #}
<div class="offre">
<div>
Ajouté le {{ offre[0].date_ajout.strftime('%d/%m/%y') }} à {{ offre[0].date_ajout.strftime('%Hh%M') }}<br>
Intitulé : {{ offre[0].intitule }}<br>
Description : {{ offre[0].description }}<br>
Type de l'offre : {{ offre[0].type_offre }}<br>
@ -9,6 +10,16 @@
{% if offre[2] %}
Département(s) : {% for offre_dept in offre[2] %} <div class="offre-depts">{{ offre_dept.dept_id|get_dept_acronym }}</div> {% endfor %}<br>
{% endif %}
{% if offre[0].correspondant_id %}
Contacté {{ offre[3].nom }} {{ offre[3].prenom }}
{% if offre[3].mail and offre[3].telephone %}
({{ offre[3].mail }} - {{ offre[3].telephone }})<br>
{% else %}
({{ offre[3].mail }}{{offre[3].telephone}})<br>
{% endif %}
{% endif %}
{% for fichier in offre[1] %}
<a href="{{ url_for('entreprises.get_offre_file', entreprise_id=entreprise.id, offre_id=offre[0].id, filedir=fichier[0], filename=fichier[1] )}}">{{ fichier[1] }}</a>
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
@ -16,6 +27,7 @@
{% endif %}
<br>
{% endfor %}
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<a href="{{ url_for('entreprises.add_offre_file', offre_id=offre[0].id) }}">Ajoutez un fichier</a>
{% endif %}
@ -26,9 +38,11 @@
<a class="btn btn-primary" href="{{ url_for('entreprises.edit_offre', id=offre[0].id) }}">Modifier l'offre</a>
<a class="btn btn-danger" href="{{ url_for('entreprises.delete_offre', id=offre[0].id) }}">Supprimer l'offre</a>
{% endif %}
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesSend, None) %}
<a class="btn btn-primary" href="{{ url_for('entreprises.envoyer_offre', id=offre[0].id) }}">Envoyer l'offre</a>
{% endif %}
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
{% if not offre[0].expired %}
<a class="btn btn-danger" href="{{ url_for('entreprises.expired', id=offre[0].id) }}">Rendre expirée</a>

View File

@ -0,0 +1,56 @@
{# -*- mode: jinja-html -*- #}
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block styles %}
{{super()}}
{% endblock %}
{% block app_content %}
<h1>{{ title }}</h1>
<br>
<div class="row">
<div class="col-md-4">
<p>
(*) champs requis
</p>
<form method="POST" action="" novalidate>
{{ form.hidden_tag() }}
{% for subfield in form.correspondants %}
{% for subsubfield in subfield %}
{% if subsubfield.errors %}
{% for error in subsubfield.errors %}
<p class="help-block form-error">{{ error }}</p>
{% endfor %}
{% endif %}
{% endfor %}
{% endfor %}
{{ form.correspondants }}
<div style="margin-bottom: 10px;">
<button class="btn btn-default" id="add-correspondant-field">Ajouter un correspondant</button>
<input class="btn btn-default" type="submit" value="Envoyer">
</div>
</form>
</div>
</div>
<script>
window.onload = function(e) {
let addCorrespondantFieldBtn = document.getElementById('add-correspondant-field');
addCorrespondantFieldBtn.addEventListener('click', function(e){
e.preventDefault();
let allCorrepondantsFieldWrapper = document.getElementById('correspondants');
let allCorrepondantsField = allCorrepondantsFieldWrapper.getElementsByTagName('input');
let correspondantInputIds = []
let csrf_token = document.getElementById('csrf_token').value;
for(let i = 0; i < allCorrepondantsField.length; i++) {
correspondantInputIds.push(parseInt(allCorrepondantsField[i].name.split('-')[1]));
}
let newFieldName = `correspondants-${Math.max(...correspondantInputIds) + 1}`;
allCorrepondantsFieldWrapper.insertAdjacentHTML('beforeend',`
<li><label for="${newFieldName}">Correspondants-${Math.max(...correspondantInputIds) + 1}</label> <table id="${newFieldName}"><tr><th><label for="${newFieldName}-nom">Nom (*)</label></th><td><input class="form-control" id="${newFieldName}-nom" name="${newFieldName}-nom" required type="text" value=""></td></tr><tr><th><label for="${newFieldName}-prenom">Prénom (*)</label></th><td><input class="form-control" id="${newFieldName}-prenom" name="${newFieldName}-prenom" required type="text" value=""></td></tr><tr><th><label for="${newFieldName}-telephone">Téléphone (*)</label></th><td><input class="form-control" id="${newFieldName}-telephone" name="${newFieldName}-telephone" type="text" value=""></td></tr><tr><th><label for="${newFieldName}-mail">Mail (*)</label></th><td><input class="form-control" id="${newFieldName}-mail" name="${newFieldName}-mail" type="text" value=""></td></tr><tr><th><label for="${newFieldName}-poste">Poste</label></th><td><input class="form-control" id="${newFieldName}-poste" name="${newFieldName}-poste" type="text" value=""></td></tr><tr><th><label for="${newFieldName}-service">Service</label></th><td><input class="form-control" id="${newFieldName}-service" name="${newFieldName}-service" type="text" value=""></td></tr></table><input id="${newFieldName}-csrf_token" name="${newFieldName}-csrf_token" type="hidden" value=${csrf_token}></li>
`);
});
}
</script>
{% endblock %}

View File

@ -3,7 +3,7 @@
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Ajout entreprise avec contact</h1>
<h1>Ajout entreprise</h1>
<br>
<div class="row">
<div class="col-md-4">
@ -16,45 +16,43 @@
</div>
<script>
window.onload = function(e){
document.getElementById("siret").addEventListener("keyup", autocomplete);
function autocomplete() {
var input = document.getElementById("siret").value;
if(input.length == 14) {
fetch("https://entreprise.data.gouv.fr/api/sirene/v1/siret/" + input)
.then(response => {
if(response.ok)
return response.json()
else {
emptyForm()
}
})
.then(response => fillForm(response))
.catch(err => err)
}
}
function fillForm(response) {
document.getElementById("nom_entreprise").value = response.etablissement.l1_normalisee
document.getElementById("adresse").value = response.etablissement.l4_normalisee
document.getElementById("codepostal").value = response.etablissement.code_postal
document.getElementById("ville").value = response.etablissement.libelle_commune
}
function emptyForm() {
document.getElementById("nom_entreprise").value = ''
document.getElementById("adresse").value = ''
document.getElementById("codepostal").value = ''
document.getElementById("ville").value = ''
}
}
{# ajout margin-bottom sur le champ pays #}
var champ_pays = document.getElementById("pays")
if (champ_pays !== null) {
var closest_form_group = champ_pays.closest(".form-group")
closest_form_group.style.marginBottom = "50px"
}
document.getElementById("siret").addEventListener("keyup", autocomplete);
function autocomplete() {
var input = document.getElementById("siret").value.replaceAll(" ", "")
if(input.length >= 14) {
fetch("https://entreprise.data.gouv.fr/api/sirene/v1/siret/" + input)
.then(response => {
if(response.ok)
return response.json()
else {
emptyForm()
}
})
.then(response => fillForm(response))
.catch(err => err)
}
}
function fillForm(response) {
document.getElementById("nom_entreprise").value = response.etablissement.l1_normalisee
document.getElementById("adresse").value = response.etablissement.l4_normalisee
document.getElementById("codepostal").value = response.etablissement.code_postal
document.getElementById("ville").value = response.etablissement.libelle_commune
}
function emptyForm() {
document.getElementById("nom_entreprise").value = ''
document.getElementById("adresse").value = ''
document.getElementById("codepostal").value = ''
document.getElementById("ville").value = ''
}
</script>
{% endblock %}

View File

@ -9,7 +9,7 @@
{% endblock %}
{% block app_content %}
<h1>Ajout historique</h1>
<h1>{{ title }}</h1>
<br>
<div class="row">
<div class="col-md-4">

View File

@ -9,64 +9,51 @@
{% endblock %}
{% block app_content %}
{% include 'entreprises/nav.html' %}
{% if logs %}
<div class="container">
<h3>Dernières opérations <a href="{{ url_for('entreprises.logs') }}">Voir tout</a></h3>
<ul>
{% for log in logs %}
<li><span style="margin-right: 10px;">{{ log.date.strftime('%d %b %Hh%M') }}</span><span>{{ log.text|safe }} par {{ log.authenticated_user|get_nomcomplet_by_username }}</span></li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="container boutons">
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesExport, None) %}
<a class="btn btn-default" href="{{ url_for('entreprises.import_contacts') }}">Importer des contacts</a>
{% endif %}
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesExport, None) and contacts %}
<a class="btn btn-default" href="{{ url_for('entreprises.export_contacts') }}">Exporter la liste des contacts</a>
{% endif %}
</div>
<div class="container" style="margin-bottom: 10px;">
<div class="container" style="margin-bottom: 10px;">
<h1>Liste des contacts</h1>
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<a class="btn btn-primary" style="margin-bottom:10px;" href="{{ url_for('entreprises.add_contact', id=entreprise_id) }}">Ajouter contact</a>
{% endif %}
<table id="table-contacts">
<thead>
<tr>
<td data-priority="1">Nom</td>
<td data-priority="3">Prenom</td>
<td data-priority="4">Telephone</td>
<td data-priority="5">Mail</td>
<td data-priority="6">Poste</td>
<td data-priority="7">Service</td>
<td data-priority="2">Entreprise</td>
<td data-priority="">Date</td>
<td data-priority="">Utilisateur</td>
<td data-priority="">Notes</td>
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<td data-priority="">Action</td>
{% endif %}
</tr>
</thead>
<tbody>
{% for contact in contacts %}
{% for contact in contacts %}
<tr>
<td>{{ contact[0].nom }}</td>
<td>{{ contact[0].prenom }}</td>
<td>{{ contact[0].telephone }}</td>
<td>{{ contact[0].mail }}</td>
<td>{{ contact[0].poste}}</td>
<td>{{ contact[0].service}}</td>
<td><a href="{{ url_for('entreprises.fiche_entreprise', id=contact[1].id) }}">{{ contact[1].nom }}</a></td>
<td>{{ contact.date.strftime('%d/%m/%Y %Hh%M') }}</td>
<td>{{ contact.user|get_nomcomplet_by_id }}</td>
<td>{{ contact.notes }}</td>
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<td>
<div class="btn-group">
<a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#">Action
<span class="caret"></span>
</a>
<ul class="dropdown-menu pull-left">
<li><a href="{{ url_for('entreprises.edit_contact', id=contact.id) }}">Modifier</a></li>
</ul>
</div>
</td>
{% endif %}
</tr>
{% endfor %}
{% endfor %}
</tbody>
<tfoot>
<tr>
<td>Nom</td>
<td>Prenom</td>
<td>Telephone</td>
<td>Mail</td>
<td>Poste</td>
<td>Service</td>
<td>Entreprise</td>
<td>Date</td>
<td>Utilisateur</td>
<td>Notes</td>
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<td>Action</td>
{% endif %}
</tr>
</tfoot>
</table>

View File

@ -0,0 +1,102 @@
{# -*- mode: jinja-html -*- #}
{% extends 'base.html' %}
{% block styles %}
{{super()}}
<script src="/ScoDoc/static/jQuery/jquery-1.12.4.min.js"></script>
<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css">
<script type="text/javascript" charset="utf8" src="/ScoDoc/static/DataTables/datatables.min.js"></script>
{% endblock %}
{% block app_content %}
{% include 'entreprises/nav.html' %}
{% if logs %}
<div class="container">
<h3>Dernières opérations <a href="{{ url_for('entreprises.logs') }}">Voir tout</a></h3>
<ul>
{% for log in logs %}
<li><span style="margin-right: 10px;">{{ log.date.strftime('%d %b %Hh%M') }}</span><span>{{ log.text|safe }} par {{ log.authenticated_user|get_nomcomplet_by_username }}</span></li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="container boutons">
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesExport, None) %}
<a class="btn btn-default" href="{{ url_for('entreprises.import_correspondants') }}">Importer des correspondants</a>
{% endif %}
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesExport, None) and correspondants %}
<a class="btn btn-default" href="{{ url_for('entreprises.export_correspondants') }}">Exporter la liste des correspondants</a>
{% endif %}
</div>
<div class="container" style="margin-bottom: 10px;">
<h1>Liste des correspondants</h1>
<table id="table-correspondants">
<thead>
<tr>
<td data-priority="1">Nom</td>
<td data-priority="3">Prenom</td>
<td data-priority="4">Téléphone</td>
<td data-priority="5">Mail</td>
<td data-priority="6">Poste</td>
<td data-priority="7">Service</td>
<td data-priority="2">Entreprise</td>
</tr>
</thead>
<tbody>
{% for correspondant in correspondants %}
<tr>
<td>{{ correspondant[0].nom }}</td>
<td>{{ correspondant[0].prenom }}</td>
<td>{{ correspondant[0].telephone }}</td>
<td>{{ correspondant[0].mail }}</td>
<td>{{ correspondant[0].poste}}</td>
<td>{{ correspondant[0].service}}</td>
<td><a href="{{ url_for('entreprises.fiche_entreprise', id=correspondant[1].id) }}">{{ correspondant[1].nom }}</a></td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td>Nom</td>
<td>Prenom</td>
<td>Téléphone</td>
<td>Mail</td>
<td>Poste</td>
<td>Service</td>
<td>Entreprise</td>
</tr>
</tfoot>
</table>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
let table = new DataTable('#table-correspondants',
{
"autoWidth": false,
"responsive": {
"details": true
},
"pageLength": 10,
"language": {
"emptyTable": "Aucune donnée disponible dans le tableau",
"info": "Affichage de _START_ à _END_ sur _TOTAL_ entrées",
"infoEmpty": "Affichage de 0 à 0 sur 0 entrées",
"infoFiltered": "(filtrées depuis un total de _MAX_ entrées)",
"lengthMenu": "Afficher _MENU_ entrées",
"loadingRecords": "Chargement...",
"processing": "Traitement...",
"search": "Rechercher:",
"zeroRecords": "Aucune entrée correspondante trouvée",
"paginate": {
"next": "Suivante",
"previous": "Précédente"
}
}
});
});
</script>
{% endblock %}

View File

@ -16,7 +16,22 @@
<p>
(*) champs requis
</p>
{{ wtf.quick_form(form, novalidate=True) }}
<form method="POST" action="" novalidate>
{{ form.hidden_tag() }}
{{ form.responsables.label }}<br>
{% for subfield in form.responsables %}
{% if subfield.errors %}
{% for error in subfield.errors %}
<p class="help-block form-error">{{ error }}</p>
{% endfor %}
{% endif %}
{% endfor %}
{{ form.responsables }}
<div style="margin-bottom: 10px;">
<button class="btn btn-default" id="add-responsable-field">Ajouter un responsable</button>
<input class="btn btn-default" type="submit" value="Envoyer">
</div>
</form>
</div>
</div>
@ -30,7 +45,28 @@
minchars: 2,
timeout: 60000
};
var as_responsables = new bsn.AutoSuggest('responsable', responsables_options);
let allResponsablesFieldWrapper = document.getElementById('responsables');
let allResponsablesField = allResponsablesFieldWrapper.getElementsByTagName('input');
for(let i = 0; i < allResponsablesField.length; i++) {
new bsn.AutoSuggest(allResponsablesField[i].id, responsables_options);
}
let addResponsableFieldBtn = document.getElementById('add-responsable-field');
addResponsableFieldBtn.addEventListener('click', function(e){
e.preventDefault();
let allResponsablesFieldWrapper = document.getElementById('responsables');
let allResponsablesField = allResponsablesFieldWrapper.getElementsByTagName('input');
let responsableInputIds = []
for(let i = 0; i < allResponsablesField.length; i++) {
responsableInputIds.push(parseInt(allResponsablesField[i].name.split('-')[1]));
}
let newFieldName = `responsables-${Math.max(...responsableInputIds) + 1}`;
allResponsablesFieldWrapper.insertAdjacentHTML('beforeend',`
<li><label for="${newFieldName}">Responsable (*)</label> <input class="form-control" id="${newFieldName}" name="${newFieldName}" type="text" value="" placeholder="Tapez le nom du responsable de formation"></li>
`);
var as_r = new bsn.AutoSuggest(newFieldName, responsables_options);
});
}
</script>
{% endblock %}

View File

@ -1,6 +1,13 @@
{# -*- mode: jinja-html -*- #}
{% extends 'base.html' %}
{% block styles %}
{{super()}}
<script src="/ScoDoc/static/jQuery/jquery-1.12.4.min.js"></script>
<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css">
<script type="text/javascript" charset="utf8" src="/ScoDoc/static/DataTables/datatables.min.js"></script>
{% endblock %}
{% block app_content %}
{% if logs %}
<div class="container">
@ -15,24 +22,6 @@
</ul>
</div>
{% endif %}
{% if historique %}
<div class="container">
<h3>Historique</h3>
<ul>
{% for data in historique %}
<li>
<span style="margin-right: 10px;">{{ data[0].date_debut.strftime('%d/%m/%Y') }} - {{
data[0].date_fin.strftime('%d/%m/%Y') }}</span>
<span style="margin-right: 10px;">
{{ data[0].type_offre }} réalisé par {{ data[1].nom|format_nom }} {{ data[1].prenom|format_prenom }}
{% if data[0].formation_text %} en {{ data[0].formation_text }}{% endif %}
</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="container fiche-entreprise">
<h2>Fiche entreprise - {{ entreprise.nom }} ({{ entreprise.siret }})</h2>
@ -53,20 +42,19 @@
<a class="btn btn-primary" href="{{ url_for('entreprises.edit_entreprise', id=entreprise.id) }}">Modifier</a>
<a class="btn btn-danger" href="{{ url_for('entreprises.delete_entreprise', id=entreprise.id) }}">Supprimer</a>
<a class="btn btn-primary" href="{{ url_for('entreprises.add_offre', id=entreprise.id) }}">Ajouter offre</a>
<a class="btn btn-primary" href="{{ url_for('entreprises.add_contact', id=entreprise.id) }}">Ajouter contact</a>
<a class="btn btn-primary" href="{{ url_for('entreprises.add_historique', id=entreprise.id) }}">Ajouter
historique</a>
<a class="btn btn-primary" href="{{ url_for('entreprises.add_correspondant', id=entreprise.id) }}">Ajouter correspondant</a>
{% endif %}
<a class="btn btn-primary" href="{{ url_for('entreprises.contacts', id=entreprise.id) }}">Liste contacts</a>
<a class="btn btn-primary" href="{{ url_for('entreprises.offres_expirees', id=entreprise.id) }}">Voir les offres expirées</a>
<div>
<div class="contacts-et-offres">
{% if contacts %}
<div class="correspondants-et-offres">
{% if correspondants %}
<div>
<h3>Contacts</h3>
{% for contact in contacts %}
{% include 'entreprises/_contact.html' %}
<h3>Correspondants</h3>
{% for correspondant in correspondants %}
{% include 'entreprises/_correspondant.html' %}
{% endfor %}
</div>
{% endif %}
@ -81,4 +69,95 @@
{% endif %}
</div>
</div>
<div style="margin-bottom: 10px;">
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<a class="btn btn-primary" href="{{ url_for('entreprises.add_stage_apprentissage', id=entreprise.id) }}">Ajouter stage ou apprentissage</a>
{% endif %}
<h3>Liste des stages et apprentissages réalisés au sein de l'entreprise</h3>
<table id="table-stages-apprentissages">
<thead>
<tr>
<td data-priority="">Date début</td>
<td data-priority="">Date fin</td>
<td data-priority="">Durée</td>
<td data-priority="">Type</td>
<td data-priority="">Étudiant</td>
<td data-priority="">Formation</td>
<td data-priority="">Notes</td>
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<td data-priority="3">Action</td>
{% endif %}
</tr>
</thead>
<tbody>
{% for data in stages_apprentissages %}
<tr>
<td>{{ data[0].date_debut.strftime('%d/%m/%Y') }}</td>
<td>{{ data[0].date_fin.strftime('%d/%m/%Y') }}</td>
<td>{{ (data[0].date_fin-data[0].date_debut).days//7 }} semaines</td>
<td>{{ data[0].type_offre }}</td>
<td>{{ data[1].nom|format_nom }} {{ data[1].prenom|format_prenom }}</td>
<td>{% if data[0].formation_text %}{{ data[0].formation_text }}{% endif %}</td>
<td>{{ data[0].notes }}</td>
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<td>
<div class="btn-group">
<a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#">Action
<span class="caret"></span>
</a>
<ul class="dropdown-menu pull-left">
<li><a href="{{ url_for('entreprises.edit_stage_apprentissage', id=data[0].id) }}">Modifier</a></li>
<li><a href="{{ url_for('entreprises.delete_stage_apprentissage', id=data[0].id) }}" style="color:red">Supprimer</a></li>
</ul>
</div>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td>Date début</td>
<td>Date fin</td>
<td>Durée</td>
<td>Type</td>
<td>Étudiant</td>
<td>Formation</td>
<td>Notes</td>
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<td>Action</td>
{% endif %}
</tr>
</tfoot>
</table>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
let table = new DataTable('#table-stages-apprentissages',
{
"autoWidth": false,
"responsive": {
"details": true
},
"pageLength": 10,
"language": {
"emptyTable": "Aucune donnée disponible dans le tableau",
"info": "Affichage de _START_ à _END_ sur _TOTAL_ entrées",
"infoEmpty": "Affichage de 0 à 0 sur 0 entrées",
"infoFiltered": "(filtrées depuis un total de _MAX_ entrées)",
"lengthMenu": "Afficher _MENU_ entrées",
"loadingRecords": "Chargement...",
"processing": "Traitement...",
"search": "Rechercher:",
"zeroRecords": "Aucune entrée correspondante trouvée",
"paginate": {
"next": "Suivante",
"previous": "Précédente"
}
}
});
});
</script>
{% endblock %}

View File

@ -16,25 +16,25 @@
</div>
</div>
{% if contacts %}
{% if correspondants %}
<div>
{% for contact in contacts %}
{% for correspondant in correspondants %}
<div>
<h3>Contact</h3>
<div class="contact">
Nom : {{ contact.nom }}<br>
Prénom : {{ contact.prenom }}<br>
{% if contact.telephone %}
Téléphone : {{ contact.telephone }}<br>
<h3>Correspondant</h3>
<div class="correspondant">
Nom : {{ correspondant.nom }}<br>
Prénom : {{ correspondant.prenom }}<br>
{% if correspondant.telephone %}
Téléphone : {{ correspondant.telephone }}<br>
{% endif %}
{% if contact.mail %}
Mail : {{ contact.mail }}<br>
{% if correspondant.mail %}
Mail : {{ correspondant.mail }}<br>
{% endif %}
{% if contact.poste %}
Poste : {{ contact.poste }}<br>
{% if correspondant.poste %}
Poste : {{ correspondant.poste }}<br>
{% endif %}
{% if contact.service %}
Service : {{ contact.service }}<br>
{% if correspondant.service %}
Service : {{ correspondant.service }}<br>
{% endif %}
</div>
</div>

View File

@ -4,6 +4,8 @@
{% block styles %}
{{super()}}
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/css/autosuggest_inquisitor.css" />
<script src="/ScoDoc/static/libjs/AutoSuggest.js"></script>
{% endblock %}
{% block app_content %}
@ -25,5 +27,36 @@
var closest_form_control = champ_depts.closest(".form-control")
closest_form_control.classList.remove("form-control")
}
if(document.getElementById("expiration_date") !== null && document.getElementById("expiration_date").value === "")
expiration()
if(document.getElementById("type_offre") !== null)
document.getElementById("type_offre").addEventListener("change", expiration);
function expiration() {
var date = new Date()
var expiration = document.getElementById("expiration_date")
var type_offre = document.getElementById("type_offre").value
if (type_offre === "Alternance") {
expiration.value = `${date.getFullYear() + 1}-01-01`
} else {
if(date.getMonth() + 1 < 8)
expiration.value = `${date.getFullYear()}-08-01`
else
expiration.value = `${date.getFullYear() + 1}-08-01`
}
}
var responsables_options = {
script: "/ScoDoc/entreprises/responsables?",
varname: "term",
json: true,
noresults: "Valeur invalide !",
minchars: 2,
timeout: 60000
};
var as_utilisateurs = new bsn.AutoSuggest('utilisateur', responsables_options);
</script>
{% endblock %}

View File

@ -1,62 +0,0 @@
{# -*- mode: jinja-html -*- #}
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block styles %}
{{super()}}
{% endblock %}
{% block app_content %}
<h1>Importation contacts</h1>
<br>
<div>
<a href="{{ url_for('entreprises.get_import_contacts_file_sample') }}">Obtenir la feuille excel à remplir</a>
</div>
<br>
<div class="row">
<div class="col-md-4">
<p>
(*) champs requis
</p>
{{ wtf.quick_form(form, novalidate=True) }}
</div>
</div>
{% if not contacts_import %}
<table class="table">
<thead><tr><td><b>Attribut</b></td><td><b>Type</b></td><td><b>Description</b></td></tr></thead>
<tr><td>nom</td><td>text</td><td>nom du contact</td></tr>
<tr><td>prenom</td><td>text</td><td>prenom du contact</td></tr>
<tr><td>telephone</td><td>text</td><td>telephone du contact</td></tr>
<tr><td>mail</td><td>text</td><td>mail du contact</td></tr>
<tr><td>poste</td><td>text</td><td>poste du contact</td></tr>
<tr><td>service</td><td>text</td><td>service dans lequel travaille le contact</td></tr>
<tr><td>entreprise_siret</td><td>integer</td><td>SIRET de l'entreprise</td></tr>
</table>
{% endif %}
{% if contacts_import %}
<br><div>Importation de {{ contacts_import|length }} contact(s)</div>
{% for contact in contacts_import %}
<div class="contact">
<div>
Nom : {{ contact.nom }}<br>
Prénom : {{ contact.prenom }}<br>
{% if contact.telephone %}
Téléphone : {{ contact.telephone }}<br>
{% endif %}
{% if contact.mail %}
Mail : {{ contact.mail }}<br>
{% endif %}
{% if contact.poste %}
Poste : {{ contact.poste }}<br>
{% endif %}
{% if contact.service %}
Service : {{ contact.service }}<br>
{% endif %}
<a href="{{ url_for('entreprises.fiche_entreprise', id=contact.entreprise_id )}}">lien vers l'entreprise</a>
</div>
</div>
{% endfor %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,62 @@
{# -*- mode: jinja-html -*- #}
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block styles %}
{{super()}}
{% endblock %}
{% block app_content %}
<h1>Importation correspondants</h1>
<br>
<div>
<a href="{{ url_for('entreprises.get_import_correspondants_file_sample') }}">Obtenir la feuille excel à remplir</a>
</div>
<br>
<div class="row">
<div class="col-md-4">
<p>
(*) champs requis
</p>
{{ wtf.quick_form(form, novalidate=True) }}
</div>
</div>
{% if not correspondants_import %}
<table class="table">
<thead><tr><td><b>Attribut</b></td><td><b>Type</b></td><td><b>Description</b></td></tr></thead>
<tr><td>nom</td><td>text</td><td>nom du correspondant</td></tr>
<tr><td>prenom</td><td>text</td><td>prenom du correspondant</td></tr>
<tr><td>telephone</td><td>text</td><td>telephone du correspondant</td></tr>
<tr><td>mail</td><td>text</td><td>mail du correspondant</td></tr>
<tr><td>poste</td><td>text</td><td>poste du correspondant</td></tr>
<tr><td>service</td><td>text</td><td>service dans lequel travaille le correspondant</td></tr>
<tr><td>entreprise_siret</td><td>integer</td><td>SIRET de l'entreprise</td></tr>
</table>
{% endif %}
{% if correspondants_import %}
<br><div>Importation de {{ correspondants_import|length }} correspondant(s)</div>
{% for correspondant in correspondants_import %}
<div class="correspondant">
<div>
Nom : {{ correspondant.nom }}<br>
Prénom : {{ correspondant.prenom }}<br>
{% if correspondant.telephone %}
Téléphone : {{ correspondant.telephone }}<br>
{% endif %}
{% if correspondant.mail %}
Mail : {{ correspondant.mail }}<br>
{% endif %}
{% if correspondant.poste %}
Poste : {{ correspondant.poste }}<br>
{% endif %}
{% if correspondant.service %}
Service : {{ correspondant.service }}<br>
{% endif %}
<a href="{{ url_for('entreprises.fiche_entreprise', id=correspondant.entreprise_id )}}">lien vers l'entreprise</a>
</div>
</div>
{% endfor %}
{% endif %}
{% endblock %}

View File

@ -2,7 +2,7 @@
<nav class="nav-entreprise">
<ul>
<li><a href="{{ url_for('entreprises.index') }}">Entreprises</a></li>
<li><a href="{{ url_for('entreprises.contacts') }}">Contacts</a></li>
<li><a href="{{ url_for('entreprises.correspondants') }}">Correspondants</a></li>
<li><a href="{{ url_for('entreprises.offres_recues') }}">Offres reçues</a></li>
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesValidate, None) %}
<li><a href="{{ url_for('entreprises.validation') }}">Entreprises à valider</a></li>

View File

@ -10,12 +10,22 @@
{% for offre in offres_recues %}
<div class="offre offre-recue">
<div>
Envoyé le {{ offre[0].date_envoi.strftime('%d %B %Y à %H:%M') }} par {{ offre[0].sender_id|get_nomcomplet_by_id }}<br>
Envoyé le {{ offre[0].date_envoi.strftime('%d/%m/%Y') }} à {{ offre[0].date_envoi.strftime('%Hh%M') }} par {{ offre[0].sender_id|get_nomcomplet_by_id }}<br>
Intitulé : {{ offre[1].intitule }}<br>
Description : {{ offre[1].description }}<br>
Type de l'offre : {{ offre[1].type_offre }}<br>
Missions : {{ offre[1].missions }}<br>
Durée : {{ offre[1].duree }}<br>
{% if offre[1].correspondant_id %}
Contacté {{ offre[3].nom }} {{ offre[3].prenom }}
{% if offre[3].mail and offre[3].telephone %}
({{ offre[3].mail }} - {{ offre[3].telephone }})<br>
{% else %}
({{ offre[3].mail }}{{offre[3].telephone}})<br>
{% endif %}
{% endif %}
<a href="{{ url_for('entreprises.fiche_entreprise', id=offre[1].entreprise_id) }}">lien vers l'entreprise</a><br>
{% for fichier in offre[2] %}

View File

@ -1,6 +1,5 @@
{# -*- mode: jinja-html -*- #}
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}

View File

@ -45,11 +45,13 @@ from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models.formsemestre import FormSemestre
from app.models.formsemestre import FormSemestreUEComputationExpr
from app.models.modules import Module
from app.models.ues import UniteEns
from app import api
from app import db
from app import models
from app.models import ScolarNews
from app.auth.models import User
from app.but import bulletin_but
from app.decorators import (
@ -86,7 +88,6 @@ from app.scodoc import sco_archives
from app.scodoc import sco_bulletins
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_cache
from app.scodoc import sco_compute_moy
from app.scodoc import sco_cost_formation
from app.scodoc import sco_debouche
from app.scodoc import sco_edit_apc
@ -103,6 +104,7 @@ from app.scodoc import sco_evaluation_edit
from app.scodoc import sco_evaluation_recap
from app.scodoc import sco_export_results
from app.scodoc import sco_formations
from app.scodoc import sco_formation_recap
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_custommenu
from app.scodoc import sco_formsemestre_edit
@ -288,10 +290,9 @@ def formsemestre_bulletinetud(
code_ine=None,
):
format = format or "html"
if not formsemestre_id:
flask.abort(404, "argument manquant: formsemestre_id")
if not isinstance(formsemestre_id, int):
raise ScoInvalidIdType("formsemestre_id must be an integer !")
raise ValueError("formsemestre_id must be an integer !")
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if etudid:
etud = models.Identite.query.get_or_404(etudid)
@ -472,6 +473,7 @@ sco_publish(
"/edit_ue_set_code_apogee",
sco_edit_ue.edit_ue_set_code_apogee,
Permission.ScoChangeFormation,
methods=["POST"],
)
sco_publish(
"/formsemestre_edit_uecoefs",
@ -479,8 +481,20 @@ sco_publish(
Permission.ScoView,
methods=["GET", "POST"],
)
@bp.route("/formation_table_recap")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formation_table_recap(formation_id, format="html"):
return sco_formation_recap.formation_table_recap(formation_id, format="html")
sco_publish(
"/formation_table_recap", sco_edit_ue.formation_table_recap, Permission.ScoView
"/export_recap_formations_annee_scolaire",
sco_formation_recap.export_recap_formations_annee_scolaire,
Permission.ScoView,
)
sco_publish(
"/formation_add_malus_modules",
@ -571,6 +585,20 @@ def index_html():
</li>
<li><a class="stdlink" href="formation_import_xml_form">Importer une formation (xml)</a>
</li>
<li><a class="stdlink" href="{
url_for("notes.export_recap_formations_annee_scolaire",
scodoc_dept=g.scodoc_dept, annee_scolaire=scu.AnneeScolaire()-1)
}">exporter les formations de l'année scolaire
{scu.AnneeScolaire()-1} - {scu.AnneeScolaire()}
</a>
</li>
<li><a class="stdlink" href="{
url_for("notes.export_recap_formations_annee_scolaire",
scodoc_dept=g.scodoc_dept, annee_scolaire=scu.AnneeScolaire())
}">exporter les formations de l'année scolaire
{scu.AnneeScolaire()} - {scu.AnneeScolaire()+1}
</a>
</li>
</ul>
<h3>Référentiels de compétences</h3>
<ul>
@ -2410,6 +2438,125 @@ sco_publish(
Permission.ScoEditApo,
)
@bp.route("/formsemestre_set_apo_etapes", methods=["POST"])
@scodoc
@permission_required(Permission.ScoEditApo)
def formsemestre_set_apo_etapes():
"""Change les codes étapes du semestre indiqué.
Args: oid=formsemestre_id, value=chaine "V1RT, V1RT2", codes séparés par des virgules
"""
formsemestre_id = int(request.form.get("oid"))
etapes_apo_str = request.form.get("value")
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
current_etapes = {e.etape_apo for e in formsemestre.etapes}
new_etapes = {s.strip() for s in etapes_apo_str.split(",")}
if new_etapes != current_etapes:
formsemestre.etapes = []
for etape_apo in new_etapes:
etape = models.FormSemestreEtape(
formsemestre_id=formsemestre_id, etape_apo=etape_apo
)
formsemestre.etapes.append(etape)
db.session.add(formsemestre)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_APO,
text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})",
max_frequency=10 * 60,
)
return ("", 204)
@bp.route("/formsemestre_set_elt_annee_apo", methods=["POST"])
@scodoc
@permission_required(Permission.ScoEditApo)
def formsemestre_set_elt_annee_apo():
"""Change les codes étapes du semestre indiqué.
Args: oid=formsemestre_id, value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
"""
oid = int(request.form.get("oid"))
value = (request.form.get("value") or "").strip()
formsemestre: FormSemestre = FormSemestre.query.get_or_404(oid)
if value != formsemestre.elt_annee_apo:
formsemestre.elt_annee_apo = value
db.session.add(formsemestre)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_APO,
text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})",
max_frequency=10 * 60,
)
return ("", 204)
@bp.route("/formsemestre_set_elt_sem_apo", methods=["POST"])
@scodoc
@permission_required(Permission.ScoEditApo)
def formsemestre_set_elt_sem_apo():
"""Change les codes étapes du semestre indiqué.
Args: oid=formsemestre_id, value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
"""
oid = int(request.form.get("oid"))
value = (request.form.get("value") or "").strip()
formsemestre: FormSemestre = FormSemestre.query.get_or_404(oid)
if value != formsemestre.elt_sem_apo:
formsemestre.elt_sem_apo = value
db.session.add(formsemestre)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_APO,
text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})",
max_frequency=10 * 60,
)
return ("", 204)
@bp.route("/ue_set_apo", methods=["POST"])
@scodoc
@permission_required(Permission.ScoEditApo)
def ue_set_apo():
"""Change le code APO de l'UE
Args: oid=ue_id, value=chaine "VRTU12" (1 seul code / UE)
"""
ue_id = int(request.form.get("oid"))
code_apo = (request.form.get("value") or "").strip()
ue = UniteEns.query.get_or_404(ue_id)
if code_apo != ue.code_apogee:
ue.code_apogee = code_apo
db.session.add(ue)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
text=f"Modification code Apogée d'UE dans la formation {ue.formation.titre} ({ue.formation.acronyme})",
max_frequency=10 * 60,
)
return ("", 204)
@bp.route("/module_set_apo", methods=["POST"])
@scodoc
@permission_required(Permission.ScoEditApo)
def module_set_apo():
"""Change le code APO du module
Args: oid=ue_id, value=chaine "VRTU12" (1 seul code / UE)
"""
oid = int(request.form.get("oid"))
code_apo = (request.form.get("value") or "").strip()
mod = Module.query.get_or_404(oid)
if code_apo != mod.code_apogee:
mod.code_apogee = code_apo
db.session.add(mod)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
text=f"Modification code Apogée d'UE dans la formation {mod.formation.titre} ({mod.formation.acronyme})",
max_frequency=10 * 60,
)
return ("", 204)
# sco_semset
sco_publish("/semset_page", sco_semset.semset_page, Permission.ScoEditApo)
sco_publish(

View File

@ -325,7 +325,7 @@ def showEtudLog(etudid, format="html"):
# ---------- PAGE ACCUEIL (listes) --------------
@bp.route("/")
@bp.route("/", alias=True)
@bp.route("/index_html")
@scodoc
@permission_required(Permission.ScoView)

View File

@ -0,0 +1,158 @@
"""tables module gestions relations entreprises suite
Revision ID: e97b2a10f86c
Revises: af05f03b81be
Create Date: 2022-04-19 17:39:08.197835
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "e97b2a10f86c"
down_revision = "af05f03b81be"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"are_correspondants",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("entreprise_id", sa.Integer(), nullable=True),
sa.Column("nom", sa.Text(), nullable=True),
sa.Column("prenom", sa.Text(), nullable=True),
sa.Column("telephone", sa.Text(), nullable=True),
sa.Column("mail", sa.Text(), nullable=True),
sa.Column("poste", sa.Text(), nullable=True),
sa.Column("service", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(
["entreprise_id"], ["are_entreprises.id"], ondelete="cascade"
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"are_stages_apprentissages",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("entreprise_id", sa.Integer(), nullable=True),
sa.Column("etudid", sa.Integer(), nullable=True),
sa.Column("type_offre", sa.Text(), nullable=True),
sa.Column("date_debut", sa.Date(), nullable=True),
sa.Column("date_fin", sa.Date(), nullable=True),
sa.Column("formation_text", sa.Text(), nullable=True),
sa.Column("formation_scodoc", sa.Integer(), nullable=True),
sa.Column("notes", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(
["entreprise_id"], ["are_entreprises.id"], ondelete="cascade"
),
sa.PrimaryKeyConstraint("id"),
)
op.drop_table("are_etudiants")
op.add_column(
"are_contacts", sa.Column("date", sa.DateTime(timezone=True), nullable=True)
)
op.add_column("are_contacts", sa.Column("user", sa.Integer(), nullable=True))
op.add_column("are_contacts", sa.Column("entreprise", sa.Integer(), nullable=True))
op.add_column("are_contacts", sa.Column("notes", sa.Text(), nullable=True))
op.drop_constraint(
"are_contacts_entreprise_id_fkey", "are_contacts", type_="foreignkey"
)
op.create_foreign_key(
None,
"are_contacts",
"are_entreprises",
["entreprise"],
["id"],
ondelete="cascade",
)
op.create_foreign_key(
None, "are_contacts", "user", ["user"], ["id"], ondelete="cascade"
)
op.drop_column("are_contacts", "nom")
op.drop_column("are_contacts", "telephone")
op.drop_column("are_contacts", "service")
op.drop_column("are_contacts", "entreprise_id")
op.drop_column("are_contacts", "mail")
op.drop_column("are_contacts", "poste")
op.drop_column("are_contacts", "prenom")
op.add_column(
"are_offres", sa.Column("correspondant_id", sa.Integer(), nullable=True)
)
op.create_foreign_key(
None,
"are_offres",
"are_correspondants",
["correspondant_id"],
["id"],
ondelete="cascade",
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "are_offres", type_="foreignkey")
op.drop_column("are_offres", "correspondant_id")
op.add_column(
"are_contacts",
sa.Column("prenom", sa.TEXT(), autoincrement=False, nullable=True),
)
op.add_column(
"are_contacts",
sa.Column("poste", sa.TEXT(), autoincrement=False, nullable=True),
)
op.add_column(
"are_contacts", sa.Column("mail", sa.TEXT(), autoincrement=False, nullable=True)
)
op.add_column(
"are_contacts",
sa.Column("entreprise_id", sa.INTEGER(), autoincrement=False, nullable=True),
)
op.add_column(
"are_contacts",
sa.Column("service", sa.TEXT(), autoincrement=False, nullable=True),
)
op.add_column(
"are_contacts",
sa.Column("telephone", sa.TEXT(), autoincrement=False, nullable=True),
)
op.add_column(
"are_contacts", sa.Column("nom", sa.TEXT(), autoincrement=False, nullable=True)
)
op.drop_constraint(None, "are_contacts", type_="foreignkey")
op.drop_constraint(None, "are_contacts", type_="foreignkey")
op.create_foreign_key(
"are_contacts_entreprise_id_fkey",
"are_contacts",
"are_entreprises",
["entreprise_id"],
["id"],
ondelete="CASCADE",
)
op.drop_column("are_contacts", "notes")
op.drop_column("are_contacts", "entreprise")
op.drop_column("are_contacts", "user")
op.drop_column("are_contacts", "date")
op.create_table(
"are_etudiants",
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column("entreprise_id", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("etudid", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("type_offre", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("date_debut", sa.DATE(), autoincrement=False, nullable=True),
sa.Column("date_fin", sa.DATE(), autoincrement=False, nullable=True),
sa.Column("formation_text", sa.TEXT(), autoincrement=False, nullable=True),
sa.Column("formation_scodoc", sa.INTEGER(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(
["entreprise_id"],
["are_entreprises.id"],
name="are_etudiants_entreprise_id_fkey",
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name="are_etudiants_pkey"),
)
op.drop_table("are_stages_apprentissages")
op.drop_table("are_correspondants")
# ### end Alembic commands ###

View File

@ -1,74 +1,87 @@
alembic==1.7.5
alembic==1.7.7
astroid==2.11.2
async-timeout==4.0.2
attrs==21.4.0
Babel==2.9.1
black==22.3.0
blinker==1.4
certifi==2021.10.8
cffi==1.15.0
chardet==4.0.0
charset-normalizer==2.0.9
click==8.0.3
charset-normalizer==2.0.12
click==8.1.2
cracklib==2.9.3
cryptography==36.0.1
cryptography==36.0.2
Deprecated==1.2.13
dnspython==2.1.0
dill==0.3.4
dominate==2.6.0
email-validator==1.1.3
et-xmlfile==1.1.0
Flask==2.0.2
Flask==2.1.1
Flask-Babel==2.0.0
Flask-Bootstrap==3.3.7.1
Flask-Caching==1.10.1
Flask-HTTPAuth==4.5.0
Flask-Login==0.5.0
Flask-Login==0.6.0
Flask-Mail==0.9.1
Flask-Migrate==3.1.0
Flask-Moment==1.0.2
Flask-SQLAlchemy==2.5.1
Flask-WTF==1.0.0
Flask-WTF==1.0.1
greenlet==1.1.2
gunicorn==20.1.0
icalendar==4.0.9
idna==3.3
importlib-metadata==4.11.3
iniconfig==1.1.1
isort==5.10.1
itsdangerous==2.0.1
Jinja2==3.0.3
itsdangerous==2.1.2
Jinja2==3.1.1
lazy-object-proxy==1.7.1
lxml==4.8.0
Mako==1.1.6
MarkupSafe==2.0.1
mccabe==0.6.1
numpy==1.22.0
Mako==1.2.0
MarkupSafe==2.1.1
mccabe==0.7.0
mypy==0.942
mypy-extensions==0.4.3
numpy==1.22.3
openpyxl==3.0.9
packaging==21.3
pandas==1.3.5
Pillow==8.4.0
pandas==1.4.2
pathspec==0.9.0
Pillow==9.1.0
pkg_resources==0.0.0
platformdirs==2.5.1
pluggy==1.0.0
psycopg2==2.9.3
py==1.11.0
pycparser==2.21
pydot==1.4.2
PyJWT==2.3.0
pyOpenSSL==21.0.0
pyparsing==3.0.6
pytest==6.2.5
pylint==2.13.5
pylint-flask==0.6
pylint-flask-sqlalchemy==0.2.0
pylint-plugin-utils==0.7
pyOpenSSL==22.0.0
pyparsing==3.0.8
pytest==7.1.1
python-dateutil==2.8.2
python-docx==0.8.11
python-dotenv==0.19.2
python-dotenv==0.20.0
python-editor==1.0.4
pytz==2021.3
redis==4.1.0
reportlab==3.6.5
requests==2.26.0
pytz==2022.1
redis==4.2.2
reportlab==3.6.9
requests==2.27.1
rq==1.10.1
six==1.16.0
SQLAlchemy==1.4.29
SQLAlchemy==1.4.35
toml==0.10.2
tornado==6.1
typing-extensions==4.0.1
urllib3==1.26.7
typing_extensions==4.1.1
urllib3==1.26.9
visitor==0.1.3
Werkzeug==2.0.2
wrapt==1.13.3
Werkzeug==2.1.1
wrapt==1.14.0
WTForms==3.0.1
zipp==3.8.0

View File

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

View File

@ -204,7 +204,7 @@ class ScoFake(object):
abbrev=None,
ects=None,
code_apogee=None,
module_type=None,
module_type=scu.ModuleType.STANDARD,
) -> int:
oid = sco_edit_module.do_module_create(locals())
oids = sco_edit_module.module_list(args={"module_id": oid})

View File

@ -2,20 +2,15 @@
Test calcul moyennes UE
"""
import numpy as np
from numpy.lib.nanfunctions import _nanquantile_1d
import pandas as pd
from tests.unit import setup
from tests.unit import sco_fake_gen
from app import db
from app import models
from app.comp import moy_mod
from app.comp import moy_ue
from app.comp import inscr_mod
from app.models import FormSemestre, Evaluation, ModuleImplInscription
from app.models.etudiants import Identite
from app.scodoc import sco_codes_parcours, sco_saisie_notes
from app.scodoc.sco_utils import NOTES_ATTENTE, NOTES_NEUTRALISE
from app.scodoc import sco_saisie_notes
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import NOTES_NEUTRALISE
from app.scodoc import sco_exceptions
@ -69,9 +64,20 @@ def test_ue_moy(test_client):
_ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)])
# Recalcul des moyennes
sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre)
# Masque de tous les modules _sauf_ les bonus (sport)
modimpl_mask = [
modimpl.module.ue.type != UE_SPORT
for modimpl in formsemestre.modimpls_sorted
]
etuds = formsemestre.etuds.all()
etud_moy_ue = moy_ue.compute_ue_moys_apc(
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df
sem_cube,
etuds,
modimpls,
ues,
modimpl_inscr_df,
modimpl_coefs_df,
modimpl_mask,
)
return etud_moy_ue
@ -113,8 +119,11 @@ def test_ue_moy(test_client):
# Recalcule les notes:
sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre)
etuds = formsemestre.etuds.all()
modimpl_mask = [
modimpl.module.ue.type != UE_SPORT for modimpl in formsemestre.modimpls_sorted
]
etud_moy_ue = moy_ue.compute_ue_moys_apc(
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df, modimpl_mask
)
assert etud_moy_ue[ue1.id][etudid] == n1
assert etud_moy_ue[ue2.id][etudid] == n1