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): class BonusVilleAvray(BonusSport):
"""Bonus modules optionnels (sport, culture), règle IUT Ville d'Avray. """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) df = df.merge(ins_df, how="left", left_index=True, right_index=True)
# Force columns names to integers (moduleimpl ids) # 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 # les colonnes de df sont en float (Nan) quand il n'y a
# aucun inscrit au module. # aucun inscrit au module.
df.fillna(0, inplace=True) # les non-inscrits df.fillna(0, inplace=True) # les non-inscrits

View File

@ -169,9 +169,7 @@ class ModuleImplResults:
self.en_attente = True self.en_attente = True
# Force columns names to integers (evaluation ids) # Force columns names to integers (evaluation ids)
evals_notes.columns = pd.Int64Index( evals_notes.columns = pd.Index([int(x) for x in evals_notes.columns], dtype=int)
[int(x) for x in evals_notes.columns], dtype="int"
)
self.evals_notes = evals_notes self.evals_notes = evals_notes
def _load_evaluation_notes(self, evaluation: Evaluation) -> pd.DataFrame: 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): if (notes is None) or (len(notes) == 0):
return (pd.Series([], dtype=object), pd.Series([], dtype=int)) return (pd.Series([], dtype=object), pd.Series([], dtype=int))
notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant 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_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 # le rang numérique pour tris:
rangs_int = pd.Series(0, index=notes.index, dtype=int)
N = len(notes) N = len(notes)
nb_ex = 0 # nb d'ex-aequo consécutifs en cours nb_ex = 0 # nb d'ex-aequo consécutifs en cours
notes_i = notes.iat notes_i = notes.iat
@ -128,4 +129,5 @@ def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
rangs_int[etudid] = i + 1 rangs_int[etudid] = i + 1
srang = "%d" % (i + 1) srang = "%d" % (i + 1)
rangs_str[etudid] = srang rangs_str[etudid] = srang
assert rangs_int.dtype == int
return rangs_str, rangs_int return rangs_str, rangs_int

View File

@ -271,7 +271,7 @@ def compute_ue_moys_apc(
) )
# Annule les coefs des modules NaN # Annule les coefs des modules NaN
modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 0.0, modimpl_coefs_etuds) 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) modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
# #
# Version vectorisée # Version vectorisée
@ -356,7 +356,7 @@ def compute_ue_moys_classic(
modimpl_coefs_etuds_no_nan = np.where( modimpl_coefs_etuds_no_nan = np.where(
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds 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) modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
# --------------------- Calcul des moyennes d'UE # --------------------- Calcul des moyennes d'UE
ue_modules = np.array( 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: # 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) 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) coefs = coefs.astype(np.float)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_ue = ( etud_moy_ue = (
@ -462,7 +462,7 @@ def compute_mat_moys_classic(
modimpl_coefs_etuds_no_nan = np.where( modimpl_coefs_etuds_no_nan = np.where(
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds 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) 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( 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 ( from app.entreprises.models import (
Entreprise, Entreprise,
EntrepriseContact, EntrepriseCorrespondant,
EntrepriseOffre, EntrepriseOffre,
EntrepriseOffreDepartement, EntrepriseOffreDepartement,
EntreprisePreferences, 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 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() 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): if not offre_depts or check_offre_depts(depts, offre_depts):
files = [] files = []
path = os.path.join( 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}/*"): for _file in glob.glob(f"{dir}/*"):
file = [os.path.basename(dir), os.path.basename(_file)] file = [os.path.basename(dir), os.path.basename(_file)]
files.append(file) files.append(file)
return [offre, files, offre_depts] return [offre, files, offre_depts, correspondant]
return None return None
def send_email_notifications_entreprise( def send_email_notifications_entreprise(subject, entreprise: Entreprise):
subject, entreprise: Entreprise, contact: EntrepriseContact
):
txt = [ txt = [
"Une entreprise est en attente de validation", "Une entreprise est en attente de validation",
"Entreprise:", "Entreprise:",
@ -116,14 +117,6 @@ def send_email_notifications_entreprise(
f"\tcode postal: {entreprise.codepostal}", f"\tcode postal: {entreprise.codepostal}",
f"\tville: {entreprise.ville}", f"\tville: {entreprise.ville}",
f"\tpays: {entreprise.pays}", 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) txt = "\n".join(txt)
email.send_email( email.send_email(
@ -135,34 +128,42 @@ def send_email_notifications_entreprise(
return txt return txt
def verif_contact_data(contact_data): def verif_correspondant_data(correspondant_data):
""" """
Verifie les données d'une ligne Excel (contact) Verifie les données d'une ligne Excel (correspondant)
contact_data[0]: nom correspondant_data[0]: nom
contact_data[1]: prenom correspondant_data[1]: prenom
contact_data[2]: telephone correspondant_data[2]: telephone
contact_data[3]: mail correspondant_data[3]: mail
contact_data[4]: poste correspondant_data[4]: poste
contact_data[5]: service correspondant_data[5]: service
contact_data[6]: entreprise_id correspondant_data[6]: entreprise_id
""" """
# champs obligatoires # 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 return False
# entreprise_id existant # 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: if entreprise is None:
return False return False
# contact possède le meme nom et prénom dans la meme entreprise # correspondant possède le meme nom et prénom dans la meme entreprise
contact = EntrepriseContact.query.filter_by( correspondant = EntrepriseCorrespondant.query.filter_by(
nom=contact_data[0], prenom=contact_data[1], entreprise_id=entreprise.id nom=correspondant_data[0].strip(),
prenom=correspondant_data[1].strip(),
entreprise_id=entreprise.id,
).first() ).first()
if contact is not None: if correspondant is not None:
return False 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 False
return True return True
@ -174,24 +175,24 @@ def verif_entreprise_data(entreprise_data):
""" """
if EntreprisePreferences.get_check_siret(): if EntreprisePreferences.get_check_siret():
for data in entreprise_data: # champs obligatoires for data in entreprise_data: # champs obligatoires
if data == "": if data.strip() == "":
return False return False
else: else:
for data in entreprise_data[1:]: # champs obligatoires for data in entreprise_data[1:]: # champs obligatoires
if data == "": if data.strip() == "":
return False return False
if EntreprisePreferences.get_check_siret(): 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: if re.match("^\d{14}$", siret) is None:
return False return False
try: try:
req = requests.get( 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}"
) )
except requests.ConnectionError:
print("no internet")
if req.status_code != 200: if req.status_code != 200:
return False return False
except requests.ConnectionError:
return False
entreprise = Entreprise.query.filter_by(siret=siret).first() entreprise = Entreprise.query.filter_by(siret=siret).first()
if entreprise is not None: if entreprise is not None:
return False return False

View File

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

View File

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

View File

@ -12,14 +12,17 @@ from app.decorators import permission_required
from app.entreprises import LOGS_LEN from app.entreprises import LOGS_LEN
from app.entreprises.forms import ( from app.entreprises.forms import (
CorrespondantsCreationForm,
EntrepriseCreationForm, EntrepriseCreationForm,
EntrepriseModificationForm, EntrepriseModificationForm,
SuppressionConfirmationForm, SuppressionConfirmationForm,
OffreCreationForm, OffreCreationForm,
OffreModificationForm, OffreModificationForm,
CorrespondantModificationForm,
ContactCreationForm, ContactCreationForm,
ContactModificationForm, ContactModificationForm,
HistoriqueCreationForm, StageApprentissageCreationForm,
StageApprentissageModificationForm,
EnvoiOffreForm, EnvoiOffreForm,
AjoutFichierForm, AjoutFichierForm,
ValidationConfirmationForm, ValidationConfirmationForm,
@ -30,9 +33,10 @@ from app.entreprises import bp
from app.entreprises.models import ( from app.entreprises.models import (
Entreprise, Entreprise,
EntrepriseOffre, EntrepriseOffre,
EntrepriseContact, EntrepriseCorrespondant,
EntrepriseLog, EntrepriseLog,
EntrepriseEtudiant, EntrepriseContact,
EntrepriseStageApprentissage,
EntrepriseEnvoiOffre, EntrepriseEnvoiOffre,
EntrepriseOffreDepartement, EntrepriseOffreDepartement,
EntreprisePreferences, EntreprisePreferences,
@ -96,22 +100,22 @@ def validation():
) )
@bp.route("/contacts", methods=["GET"]) @bp.route("/correspondants", methods=["GET"])
@permission_required(Permission.RelationsEntreprisesView) @permission_required(Permission.RelationsEntreprisesView)
def contacts(): def correspondants():
""" """
Permet d'afficher une page avec la liste des contacts des entreprises visibles et une liste des dernières opérations Permet d'afficher une page avec la liste des correspondants des entreprises visibles et une liste des dernières opérations
""" """
contacts = ( correspondants = (
db.session.query(EntrepriseContact, Entreprise) db.session.query(EntrepriseCorrespondant, Entreprise)
.join(Entreprise, EntrepriseContact.entreprise_id == Entreprise.id) .join(Entreprise, EntrepriseCorrespondant.entreprise_id == Entreprise.id)
.filter_by(visible=True) .filter_by(visible=True)
) )
logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all() logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all()
return render_template( return render_template(
"entreprises/contacts.html", "entreprises/correspondants.html",
title="Contacts", title="Correspondants",
contacts=contacts, correspondants=correspondants,
logs=logs, logs=logs,
) )
@ -122,7 +126,7 @@ def fiche_entreprise(id):
""" """
Permet d'afficher la fiche entreprise d'une entreprise avec une liste des dernières opérations et Permet d'afficher la fiche entreprise d'une entreprise avec une liste des dernières opérations et
l'historique des étudiants ayant réaliser un stage ou une alternance dans cette entreprise. l'historique des étudiants ayant réaliser un stage ou une alternance dans cette entreprise.
La fiche entreprise comporte les informations de l'entreprise, les contacts de l'entreprise et La fiche entreprise comporte les informations de l'entreprise, les correspondants de l'entreprise et
les offres de l'entreprise. les offres de l'entreprise.
""" """
entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404(
@ -141,32 +145,32 @@ def fiche_entreprise(id):
offre_with_files = are.get_offre_files_and_depts(offre, depts) offre_with_files = are.get_offre_files_and_depts(offre, depts)
if offre_with_files is not None: if offre_with_files is not None:
offres_with_files.append(offre_with_files) offres_with_files.append(offre_with_files)
contacts = entreprise.contacts[:] correspondants = entreprise.correspondants[:]
logs = ( logs = (
EntrepriseLog.query.order_by(EntrepriseLog.date.desc()) EntrepriseLog.query.order_by(EntrepriseLog.date.desc())
.filter_by(object=id) .filter_by(object=id)
.limit(LOGS_LEN) .limit(LOGS_LEN)
.all() .all()
) )
historique = ( stages_apprentissages = (
db.session.query(EntrepriseEtudiant, Identite) db.session.query(EntrepriseStageApprentissage, Identite)
.order_by(EntrepriseEtudiant.date_debut.desc()) .order_by(EntrepriseStageApprentissage.date_debut.desc())
.filter(EntrepriseEtudiant.entreprise_id == id) .filter(EntrepriseStageApprentissage.entreprise_id == id)
.join(Identite, Identite.id == EntrepriseEtudiant.etudid) .join(Identite, Identite.id == EntrepriseStageApprentissage.etudid)
.all() .all()
) )
return render_template( return render_template(
"entreprises/fiche_entreprise.html", "entreprises/fiche_entreprise.html",
title="Fiche entreprise", title="Fiche entreprise",
entreprise=entreprise, entreprise=entreprise,
contacts=contacts, correspondants=correspondants,
offres=offres_with_files, offres=offres_with_files,
logs=logs, logs=logs,
historique=historique, stages_apprentissages=stages_apprentissages,
) )
@bp.route("/logs/<int:id>", methods=["GET"]) @bp.route("/fiche_entreprise/<int:id>/logs", methods=["GET"])
@permission_required(Permission.RelationsEntreprisesView) @permission_required(Permission.RelationsEntreprisesView)
def logs_entreprise(id): def logs_entreprise(id):
""" """
@ -198,12 +202,12 @@ def fiche_entreprise_validation(id):
entreprise = Entreprise.query.filter_by(id=id, visible=False).first_or_404( entreprise = Entreprise.query.filter_by(id=id, visible=False).first_or_404(
description=f"fiche entreprise (validation) {id} inconnue" description=f"fiche entreprise (validation) {id} inconnue"
) )
contacts = entreprise.contacts correspondants = entreprise.correspondants
return render_template( return render_template(
"entreprises/fiche_entreprise_validation.html", "entreprises/fiche_entreprise_validation.html",
title="Validation fiche entreprise", title="Validation fiche entreprise",
entreprise=entreprise, entreprise=entreprise,
contacts=contacts, correspondants=correspondants,
) )
@ -221,6 +225,9 @@ def offres_recues():
) )
offres_recues_with_files = [] offres_recues_with_files = []
for offre in offres_recues: for offre in offres_recues:
correspondant = EntrepriseCorrespondant.query.filter_by(
id=offre[1].correspondant_id
).first()
files = [] files = []
path = os.path.join( path = os.path.join(
Config.SCODOC_VAR_DIR, Config.SCODOC_VAR_DIR,
@ -235,7 +242,7 @@ def offres_recues():
for file in glob.glob(f"{dir}/*"): for file in glob.glob(f"{dir}/*"):
file = [os.path.basename(dir), os.path.basename(file)] file = [os.path.basename(dir), os.path.basename(file)]
files.append(file) files.append(file)
offres_recues_with_files.append([offre[0], offre[1], files]) offres_recues_with_files.append([offre[0], offre[1], files, correspondant])
return render_template( return render_template(
"entreprises/offres_recues.html", "entreprises/offres_recues.html",
title="Offres reçues", title="Offres reçues",
@ -287,23 +294,24 @@ def add_entreprise():
) )
db.session.add(entreprise) db.session.add(entreprise)
db.session.commit() db.session.commit()
if form.nom_correspondant.data.strip():
db.session.refresh(entreprise) db.session.refresh(entreprise)
contact = EntrepriseContact( correspondant = EntrepriseCorrespondant(
entreprise_id=entreprise.id, entreprise_id=entreprise.id,
nom=form.nom_contact.data.strip(), nom=form.nom_correspondant.data.strip(),
prenom=form.prenom_contact.data.strip(), prenom=form.prenom_correspondant.data.strip(),
telephone=form.telephone.data.strip(), telephone=form.telephone.data.strip(),
mail=form.mail.data.strip(), mail=form.mail.data.strip(),
poste=form.poste.data.strip(), poste=form.poste.data.strip(),
service=form.service.data.strip(), service=form.service.data.strip(),
) )
db.session.add(contact) db.session.add(correspondant)
if current_user.has_permission(Permission.RelationsEntreprisesValidate, None): if current_user.has_permission(Permission.RelationsEntreprisesValidate, None):
entreprise.visible = True entreprise.visible = True
nom_entreprise = f"<a href=/ScoDoc/entreprises/fiche_entreprise/{entreprise.id}>{entreprise.nom}</a>" nom_entreprise = f"<a href=/ScoDoc/entreprises/fiche_entreprise/{entreprise.id}>{entreprise.nom}</a>"
log = EntrepriseLog( log = EntrepriseLog(
authenticated_user=current_user.user_name, authenticated_user=current_user.user_name,
text=f"{nom_entreprise} - Création de la fiche entreprise ({entreprise.nom}) avec un contact", text=f"{nom_entreprise} - Création de la fiche entreprise ({entreprise.nom})",
) )
db.session.add(log) db.session.add(log)
db.session.commit() db.session.commit()
@ -314,18 +322,18 @@ def add_entreprise():
db.session.commit() db.session.commit()
if EntreprisePreferences.get_email_notifications(): if EntreprisePreferences.get_email_notifications():
are.send_email_notifications_entreprise( are.send_email_notifications_entreprise(
"entreprise en attente de validation", entreprise, contact "entreprise en attente de validation", entreprise
) )
flash("L'entreprise a été ajouté à la liste pour la validation.") flash("L'entreprise a été ajouté à la liste pour la validation.")
return redirect(url_for("entreprises.index")) return redirect(url_for("entreprises.index"))
return render_template( return render_template(
"entreprises/ajout_entreprise.html", "entreprises/ajout_entreprise.html",
title="Ajout entreprise avec contact", title="Ajout entreprise avec correspondant",
form=form, form=form,
) )
@bp.route("/edit_entreprise/<int:id>", methods=["GET", "POST"]) @bp.route("/fiche_entreprise/edit_entreprise/<int:id>", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def edit_entreprise(id): def edit_entreprise(id):
""" """
@ -396,7 +404,7 @@ def edit_entreprise(id):
) )
@bp.route("/delete_entreprise/<int:id>", methods=["GET", "POST"]) @bp.route("/fiche_entreprise/delete_entreprise/<int:id>", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def delete_entreprise(id): def delete_entreprise(id):
""" """
@ -432,7 +440,9 @@ def delete_entreprise(id):
) )
@bp.route("/validate_entreprise/<int:id>", methods=["GET", "POST"]) @bp.route(
"/fiche_entreprise_validation/<int:id>/validate_entreprise", methods=["GET", "POST"]
)
@permission_required(Permission.RelationsEntreprisesValidate) @permission_required(Permission.RelationsEntreprisesValidate)
def validate_entreprise(id): def validate_entreprise(id):
""" """
@ -447,7 +457,7 @@ def validate_entreprise(id):
nom_entreprise = f"<a href=/ScoDoc/entreprises/fiche_entreprise/{entreprise.id}>{entreprise.nom}</a>" nom_entreprise = f"<a href=/ScoDoc/entreprises/fiche_entreprise/{entreprise.id}>{entreprise.nom}</a>"
log = EntrepriseLog( log = EntrepriseLog(
authenticated_user=current_user.user_name, authenticated_user=current_user.user_name,
text=f"{nom_entreprise} - Validation de la fiche entreprise ({entreprise.nom}) avec un contact", text=f"{nom_entreprise} - Validation de la fiche entreprise ({entreprise.nom}) avec un correspondant",
) )
db.session.add(log) db.session.add(log)
db.session.commit() db.session.commit()
@ -460,7 +470,10 @@ def validate_entreprise(id):
) )
@bp.route("/delete_validation_entreprise/<int:id>", methods=["GET", "POST"]) @bp.route(
"/fiche_entreprise_validation/<int:id>/delete_validation_entreprise",
methods=["GET", "POST"],
)
@permission_required(Permission.RelationsEntreprisesValidate) @permission_required(Permission.RelationsEntreprisesValidate)
def delete_validation_entreprise(id): def delete_validation_entreprise(id):
""" """
@ -482,7 +495,7 @@ def delete_validation_entreprise(id):
) )
@bp.route("/add_offre/<int:id>", methods=["GET", "POST"]) @bp.route("/fiche_entreprise/<int:id>/add_offre", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def add_offre(id): def add_offre(id):
""" """
@ -491,7 +504,7 @@ def add_offre(id):
entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404(
description=f"entreprise {id} inconnue" description=f"entreprise {id} inconnue"
) )
form = OffreCreationForm() form = OffreCreationForm(hidden_entreprise_id=id)
if form.validate_on_submit(): if form.validate_on_submit():
offre = EntrepriseOffre( offre = EntrepriseOffre(
entreprise_id=entreprise.id, entreprise_id=entreprise.id,
@ -501,6 +514,7 @@ def add_offre(id):
missions=form.missions.data.strip(), missions=form.missions.data.strip(),
duree=form.duree.data.strip(), duree=form.duree.data.strip(),
expiration_date=form.expiration_date.data, expiration_date=form.expiration_date.data,
correspondant_id=form.correspondant.data,
) )
db.session.add(offre) db.session.add(offre)
db.session.commit() db.session.commit()
@ -511,6 +525,19 @@ def add_offre(id):
dept_id=dept, dept_id=dept,
) )
db.session.add(offre_dept) db.session.add(offre_dept)
if form.fichier.data:
date = f"{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}"
path = os.path.join(
Config.SCODOC_VAR_DIR,
"entreprises",
f"{offre.entreprise_id}",
f"{offre.id}",
f"{date}",
)
os.makedirs(path)
file = form.fichier.data
filename = secure_filename(file.filename)
file.save(os.path.join(path, filename))
log = EntrepriseLog( log = EntrepriseLog(
authenticated_user=current_user.user_name, authenticated_user=current_user.user_name,
object=entreprise.id, object=entreprise.id,
@ -527,7 +554,7 @@ def add_offre(id):
) )
@bp.route("/edit_offre/<int:id>", methods=["GET", "POST"]) @bp.route("/fiche_entreprise/edit_offre/<int:id>", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def edit_offre(id): def edit_offre(id):
""" """
@ -537,7 +564,9 @@ def edit_offre(id):
description=f"offre {id} inconnue" description=f"offre {id} inconnue"
) )
offre_depts = EntrepriseOffreDepartement.query.filter_by(offre_id=offre.id).all() offre_depts = EntrepriseOffreDepartement.query.filter_by(offre_id=offre.id).all()
form = OffreModificationForm() form = OffreModificationForm(
hidden_entreprise_id=offre.entreprise_id, correspondant=offre.correspondant_id
)
offre_depts_list = [(offre_dept.dept_id) for offre_dept in offre_depts] offre_depts_list = [(offre_dept.dept_id) for offre_dept in offre_depts]
if form.validate_on_submit(): if form.validate_on_submit():
offre.intitule = form.intitule.data.strip() offre.intitule = form.intitule.data.strip()
@ -546,6 +575,7 @@ def edit_offre(id):
offre.missions = form.missions.data.strip() offre.missions = form.missions.data.strip()
offre.duree = form.duree.data.strip() offre.duree = form.duree.data.strip()
offre.expiration_date = form.expiration_date.data offre.expiration_date = form.expiration_date.data
offre.correspondant_id = form.correspondant.data
if offre_depts_list != form.depts.data: if offre_depts_list != form.depts.data:
for dept in form.depts.data: for dept in form.depts.data:
if dept not in offre_depts_list: if dept not in offre_depts_list:
@ -584,7 +614,7 @@ def edit_offre(id):
) )
@bp.route("/delete_offre/<int:id>", methods=["GET", "POST"]) @bp.route("/fiche_entreprise/delete_offre/<int:id>", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def delete_offre(id): def delete_offre(id):
""" """
@ -621,7 +651,7 @@ def delete_offre(id):
) )
@bp.route("/delete_offre_recue/<int:id>", methods=["GET", "POST"]) @bp.route("/offres_recues/delete_offre_recue/<int:id>", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesView) @permission_required(Permission.RelationsEntreprisesView)
def delete_offre_recue(id): def delete_offre_recue(id):
""" """
@ -635,7 +665,7 @@ def delete_offre_recue(id):
return redirect(url_for("entreprises.offres_recues")) return redirect(url_for("entreprises.offres_recues"))
@bp.route("/expired/<int:id>", methods=["GET", "POST"]) @bp.route("/fiche_entreprise/expired/<int:id>", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def expired(id): def expired(id):
""" """
@ -653,36 +683,153 @@ def expired(id):
return redirect(url_for("entreprises.fiche_entreprise", id=offre.entreprise_id)) return redirect(url_for("entreprises.fiche_entreprise", id=offre.entreprise_id))
@bp.route("/add_contact/<int:id>", methods=["GET", "POST"]) @bp.route("/fiche_entreprise/<int:id>/add_correspondant", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def add_contact(id): def add_correspondant(id):
""" """
Permet d'ajouter un contact a une entreprise Permet d'ajouter un correspondant a une entreprise
""" """
entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404(
description=f"entreprise {id} inconnue" description=f"entreprise {id} inconnue"
) )
form = ContactCreationForm(hidden_entreprise_id=entreprise.id) form = CorrespondantsCreationForm(hidden_entreprise_id=entreprise.id)
if form.validate_on_submit(): if form.validate_on_submit():
contact = EntrepriseContact( for correspondant_entry in form.correspondants.entries:
correspondant = EntrepriseCorrespondant(
entreprise_id=entreprise.id, entreprise_id=entreprise.id,
nom=form.nom.data.strip(), nom=correspondant_entry.nom.data.strip(),
prenom=form.prenom.data.strip(), prenom=correspondant_entry.prenom.data.strip(),
telephone=form.telephone.data.strip(), telephone=correspondant_entry.telephone.data.strip(),
mail=form.mail.data.strip(), mail=correspondant_entry.mail.data.strip(),
poste=form.poste.data.strip(), poste=correspondant_entry.poste.data.strip(),
service=form.service.data.strip(), service=correspondant_entry.service.data.strip(),
) )
log = EntrepriseLog( log = EntrepriseLog(
authenticated_user=current_user.user_name, authenticated_user=current_user.user_name,
object=entreprise.id, object=entreprise.id,
text="Création d'un contact", text="Création d'un correspondant",
) )
db.session.add(log) db.session.add(log)
db.session.add(correspondant)
db.session.commit()
flash("Le correspondant a été ajouté à la fiche entreprise.")
return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id))
return render_template(
"entreprises/ajout_correspondants.html",
title="Ajout correspondant",
form=form,
)
@bp.route("/fiche_entreprise/edit_correspondant/<int:id>", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange)
def edit_correspondant(id):
"""
Permet de modifier un correspondant
"""
correspondant = EntrepriseCorrespondant.query.filter_by(id=id).first_or_404(
description=f"correspondant {id} inconnu"
)
form = CorrespondantModificationForm(
hidden_entreprise_id=correspondant.entreprise_id,
hidden_correspondant_id=correspondant.id,
)
if form.validate_on_submit():
correspondant.nom = form.nom.data.strip()
correspondant.prenom = form.prenom.data.strip()
correspondant.telephone = form.telephone.data.strip()
correspondant.mail = form.mail.data.strip()
correspondant.poste = form.poste.data.strip()
correspondant.service = form.service.data.strip()
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=correspondant.entreprise_id,
text="Modification d'un correspondant",
)
db.session.add(log)
db.session.commit()
flash("Le correspondant a été modifié.")
return redirect(
url_for("entreprises.fiche_entreprise", id=correspondant.entreprise.id)
)
elif request.method == "GET":
form.nom.data = correspondant.nom
form.prenom.data = correspondant.prenom
form.telephone.data = correspondant.telephone
form.mail.data = correspondant.mail
form.poste.data = correspondant.poste
form.service.data = correspondant.service
return render_template(
"entreprises/form.html",
title="Modification correspondant",
form=form,
)
@bp.route("/fiche_entreprise/delete_correspondant/<int:id>", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange)
def delete_correspondant(id):
"""
Permet de supprimer un correspondant
"""
correspondant = EntrepriseCorrespondant.query.filter_by(id=id).first_or_404(
description=f"correspondant {id} inconnu"
)
form = SuppressionConfirmationForm()
if form.validate_on_submit():
db.session.delete(correspondant)
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=correspondant.entreprise_id,
text="Suppression d'un correspondant",
)
db.session.add(log)
db.session.commit()
flash("Le correspondant a été supprimé de la fiche entreprise.")
return redirect(
url_for("entreprises.fiche_entreprise", id=correspondant.entreprise_id)
)
return render_template(
"entreprises/delete_confirmation.html",
title="Supression correspondant",
form=form,
)
@bp.route("/fiche_entreprise/<int:id>/add_contact", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange)
def add_contact(id):
"""
Permet d'ajouter un contact avec une entreprise
"""
entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404(
description=f"entreprise {id} inconnue"
)
form = ContactCreationForm(
date=f"{datetime.now().strftime('%Y-%m-%dT%H:%M')}",
utilisateur=f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
if current_user.nom and current_user.prenom
else "",
)
if form.validate_on_submit():
utilisateur_data = form.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()
)
contact = EntrepriseContact(
date=form.date.data,
user=utilisateur.id,
entreprise=entreprise.id,
notes=form.notes.data.strip(),
)
db.session.add(contact) db.session.add(contact)
db.session.commit() db.session.commit()
flash("Le contact a été ajouté à la fiche entreprise.") return redirect(url_for("entreprises.contacts", id=entreprise.id))
return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id))
return render_template( return render_template(
"entreprises/form.html", "entreprises/form.html",
title="Ajout contact", title="Ajout contact",
@ -690,44 +837,38 @@ def add_contact(id):
) )
@bp.route("/edit_contact/<int:id>", methods=["GET", "POST"]) @bp.route("/fiche_entreprise/edit_contact/<int:id>", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def edit_contact(id): def edit_contact(id):
""" """
Permet de modifier un contact Permet d'editer un contact avec une entreprise
""" """
contact = EntrepriseContact.query.filter_by(id=id).first_or_404( contact = EntrepriseContact.query.filter_by(id=id).first_or_404(
description=f"contact {id} inconnu" description=f"contact {id} inconnu"
) )
form = ContactModificationForm( form = ContactModificationForm()
hidden_entreprise_id=contact.entreprise_id,
hidden_contact_id=contact.id,
)
if form.validate_on_submit(): if form.validate_on_submit():
contact.nom = form.nom.data.strip() utilisateur_data = form.utilisateur.data.upper().strip()
contact.prenom = form.prenom.data.strip() stm = text(
contact.telephone = form.telephone.data.strip() "SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:utilisateur_data"
contact.mail = form.mail.data.strip()
contact.poste = form.poste.data.strip()
contact.service = form.service.data.strip()
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=contact.entreprise_id,
text="Modification d'un contact",
) )
db.session.add(log) utilisateur = (
User.query.from_statement(stm)
.params(utilisateur_data=utilisateur_data)
.first()
)
contact.date = form.date.data
contact.user = utilisateur.id
contact.notes = form.notes.data
db.session.commit() db.session.commit()
flash("Le contact a été modifié.") return redirect(url_for("entreprises.contacts", id=contact.entreprise))
return redirect(
url_for("entreprises.fiche_entreprise", id=contact.entreprise.id)
)
elif request.method == "GET": elif request.method == "GET":
form.nom.data = contact.nom utilisateur = User.query.filter_by(id=contact.user).first()
form.prenom.data = contact.prenom form.date.data = contact.date.strftime("%Y-%m-%dT%H:%M")
form.telephone.data = contact.telephone form.utilisateur.data = (
form.mail.data = contact.mail f"{utilisateur.nom} {utilisateur.prenom} ({utilisateur.user_name})"
form.poste.data = contact.poste )
form.service.data = contact.service form.notes.data = contact.notes
return render_template( return render_template(
"entreprises/form.html", "entreprises/form.html",
title="Modification contact", title="Modification contact",
@ -735,57 +876,31 @@ def edit_contact(id):
) )
@bp.route("/delete_contact/<int:id>", methods=["GET", "POST"]) @bp.route("/fiche_entreprise/<int:id>/contacts")
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesView)
def delete_contact(id): def contacts(id):
""" """
Permet de supprimer un contact Permet d'afficher une page avec la liste des contacts d'une entreprise
""" """
contact = EntrepriseContact.query.filter_by(id=id).first_or_404( contacts = EntrepriseContact.query.filter_by(entreprise=id).all()
description=f"contact {id} inconnu"
)
form = SuppressionConfirmationForm()
if form.validate_on_submit():
contact_count = EntrepriseContact.query.filter_by(
entreprise_id=contact.entreprise_id
).count()
if contact_count == 1:
flash(
"Le contact n'a pas été supprimé de la fiche entreprise. (1 contact minimum)"
)
return redirect(
url_for("entreprises.fiche_entreprise", id=contact.entreprise_id)
)
else:
db.session.delete(contact)
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=contact.entreprise_id,
text="Suppression d'un contact",
)
db.session.add(log)
db.session.commit()
flash("Le contact a été supprimé de la fiche entreprise.")
return redirect(
url_for("entreprises.fiche_entreprise", id=contact.entreprise_id)
)
return render_template( return render_template(
"entreprises/delete_confirmation.html", "entreprises/contacts.html",
title="Supression contact", title="Liste des contacts",
form=form, contacts=contacts,
entreprise_id=id,
) )
@bp.route("/add_historique/<int:id>", methods=["GET", "POST"]) @bp.route("/fiche_entreprise/<int:id>/add_stage_apprentissage", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def add_historique(id): def add_stage_apprentissage(id):
""" """
Permet d'ajouter un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise Permet d'ajouter un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
""" """
entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404( entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404(
description=f"entreprise {id} inconnue" description=f"entreprise {id} inconnue"
) )
form = HistoriqueCreationForm() form = StageApprentissageCreationForm()
if form.validate_on_submit(): if form.validate_on_submit():
etudiant_nomcomplet = form.etudiant.data.upper().strip() etudiant_nomcomplet = form.etudiant.data.upper().strip()
stm = text( stm = text(
@ -799,7 +914,7 @@ def add_historique(id):
formation = etudiant.inscription_courante_date( formation = etudiant.inscription_courante_date(
form.date_debut.data, form.date_fin.data form.date_debut.data, form.date_fin.data
) )
historique = EntrepriseEtudiant( stage_apprentissage = EntrepriseStageApprentissage(
entreprise_id=entreprise.id, entreprise_id=entreprise.id,
etudid=etudiant.id, etudid=etudiant.id,
type_offre=form.type_offre.data.strip(), type_offre=form.type_offre.data.strip(),
@ -809,19 +924,105 @@ def add_historique(id):
formation_scodoc=formation.formsemestre.formsemestre_id formation_scodoc=formation.formsemestre.formsemestre_id
if formation if formation
else None, else None,
notes=form.notes.data.strip(),
) )
db.session.add(historique) db.session.add(stage_apprentissage)
db.session.commit() db.session.commit()
flash("L'étudiant a été ajouté sur la fiche entreprise.") flash("L'étudiant a été ajouté sur la fiche entreprise.")
return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id))
return render_template( return render_template(
"entreprises/ajout_historique.html", "entreprises/ajout_stage_apprentissage.html",
title="Ajout historique", title="Ajout stage / apprentissage",
form=form, form=form,
) )
@bp.route("/envoyer_offre/<int:id>", methods=["GET", "POST"]) @bp.route(
"/fiche_entreprise/edit_stage_apprentissage/<int:id>", methods=["GET", "POST"]
)
@permission_required(Permission.RelationsEntreprisesChange)
def edit_stage_apprentissage(id):
"""
Permet de modifier un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
"""
stage_apprentissage = EntrepriseStageApprentissage.query.filter_by(
id=id
).first_or_404(description=f"stage_apprentissage {id} inconnue")
etudiant = Identite.query.filter_by(id=stage_apprentissage.etudid).first_or_404(
description=f"etudiant {id} inconnue"
)
form = StageApprentissageModificationForm()
if form.validate_on_submit():
etudiant_nomcomplet = form.etudiant.data.upper().strip()
stm = text(
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
)
etudiant = (
Identite.query.from_statement(stm)
.params(nom_prenom=etudiant_nomcomplet)
.first()
)
formation = etudiant.inscription_courante_date(
form.date_debut.data, form.date_fin.data
)
stage_apprentissage.etudid = etudiant.id
stage_apprentissage.type_offre = form.type_offre.data.strip()
stage_apprentissage.date_debut = form.date_debut.data
stage_apprentissage.date_fin = form.date_fin.data
stage_apprentissage.formation_text = (
formation.formsemestre.titre if formation else None,
)
stage_apprentissage.formation_scodoc = (
formation.formsemestre.formsemestre_id if formation else None,
)
stage_apprentissage.notes = form.notes.data.strip()
db.session.commit()
return redirect(
url_for(
"entreprises.fiche_entreprise", id=stage_apprentissage.entreprise_id
)
)
elif request.method == "GET":
form.etudiant.data = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}"
form.type_offre.data = stage_apprentissage.type_offre
form.date_debut.data = stage_apprentissage.date_debut
form.date_fin.data = stage_apprentissage.date_fin
form.notes.data = stage_apprentissage.notes
return render_template(
"entreprises/ajout_stage_apprentissage.html",
title="Modification stage / apprentissage",
form=form,
)
@bp.route(
"/fiche_entreprise/delete_stage_apprentissage/<int:id>", methods=["GET", "POST"]
)
@permission_required(Permission.RelationsEntreprisesChange)
def delete_stage_apprentissage(id):
"""
Permet de supprimer un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
"""
stage_apprentissage = EntrepriseStageApprentissage.query.filter_by(
id=id
).first_or_404(description=f"stage_apprentissage {id} inconnu")
form = SuppressionConfirmationForm()
if form.validate_on_submit():
db.session.delete(stage_apprentissage)
db.session.commit()
return redirect(
url_for(
"entreprises.fiche_entreprise", id=stage_apprentissage.entreprise_id
)
)
return render_template(
"entreprises/delete_confirmation.html",
title="Supression stage/apprentissage",
form=form,
)
@bp.route("/fiche_entreprise/envoyer_offre/<int:id>", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesSend) @permission_required(Permission.RelationsEntreprisesSend)
def envoyer_offre(id): def envoyer_offre(id):
""" """
@ -832,7 +1033,9 @@ def envoyer_offre(id):
) )
form = EnvoiOffreForm() form = EnvoiOffreForm()
if form.validate_on_submit(): if form.validate_on_submit():
responsable_data = form.responsable.data.upper().strip() for responsable in form.responsables.entries:
if responsable.data.strip():
responsable_data = responsable.data.upper().strip()
stm = text( stm = text(
"SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:responsable_data" "SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:responsable_data"
) )
@ -924,10 +1127,11 @@ def export_entreprises():
filename = title filename = title
return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE) return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
else: else:
abort(404) flash("Aucune entreprise dans la base.")
return redirect(url_for("entreprises.index"))
@bp.route("/get_import_entreprises_file_sample") @bp.route("/import_entreprises/get_import_entreprises_file_sample")
@permission_required(Permission.RelationsEntreprisesExport) @permission_required(Permission.RelationsEntreprisesExport)
def get_import_entreprises_file_sample(): def get_import_entreprises_file_sample():
""" """
@ -978,20 +1182,19 @@ def import_entreprises():
ligne += 1 ligne += 1
if ( if (
are.verif_entreprise_data(entreprise_data) are.verif_entreprise_data(entreprise_data)
and entreprise_data[0] not in siret_list and entreprise_data[0].replace(" ", "") not in siret_list
): ):
siret_list.append(entreprise_data[0]) siret_list.append(entreprise_data[0].replace(" ", ""))
entreprise = Entreprise( entreprise = Entreprise(
siret=entreprise_data[0], siret=entreprise_data[0].replace(" ", ""),
nom=entreprise_data[1], nom=entreprise_data[1].strip(),
adresse=entreprise_data[2], adresse=entreprise_data[2].strip(),
ville=entreprise_data[3], ville=entreprise_data[3].strip(),
codepostal=entreprise_data[4], codepostal=entreprise_data[4].strip(),
pays=entreprise_data[5], pays=entreprise_data[5].strip(),
visible=True, visible=True,
) )
entreprises_import.append(entreprise) entreprises_import.append(entreprise)
else: else:
flash(f"Erreur lors de l'importation (ligne {ligne})") flash(f"Erreur lors de l'importation (ligne {ligne})")
return render_template( return render_template(
@ -1026,19 +1229,19 @@ def import_entreprises():
) )
@bp.route("/export_contacts") @bp.route("/export_correspondants")
@permission_required(Permission.RelationsEntreprisesExport) @permission_required(Permission.RelationsEntreprisesExport)
def export_contacts(): def export_correspondants():
""" """
Permet d'exporter la liste des contacts sous format excel (.xlsx) Permet d'exporter la liste des correspondants sous format excel (.xlsx)
""" """
contacts = ( correspondants = (
db.session.query(EntrepriseContact) db.session.query(EntrepriseCorrespondant)
.join(Entreprise, EntrepriseContact.entreprise_id == Entreprise.id) .join(Entreprise, EntrepriseCorrespondant.entreprise_id == Entreprise.id)
.filter_by(visible=True) .filter_by(visible=True)
.all() .all()
) )
if contacts: if correspondants:
keys = [ keys = [
"nom", "nom",
"prenom", "prenom",
@ -1049,20 +1252,24 @@ def export_contacts():
"entreprise_siret", "entreprise_siret",
] ]
titles = keys[:] titles = keys[:]
L = [[contact.to_dict().get(k, "") for k in keys] for contact in contacts] L = [
title = "Contacts" [correspondant.to_dict().get(k, "") for k in keys]
for correspondant in correspondants
]
title = "Correspondants"
xlsx = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title) xlsx = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title)
filename = title filename = title
return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE) return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
else: else:
abort(404) flash("Aucun correspondant dans la base.")
return redirect(url_for("entreprises.correspondants"))
@bp.route("/get_import_contacts_file_sample") @bp.route("/import_correspondants/get_import_correspondants_file_sample")
@permission_required(Permission.RelationsEntreprisesExport) @permission_required(Permission.RelationsEntreprisesExport)
def get_import_contacts_file_sample(): def get_import_correspondants_file_sample():
""" """
Permet de récupérer un fichier exemple vide pour pouvoir importer des contacts Permet de récupérer un fichier exemple vide pour pouvoir importer des correspondants
""" """
keys = [ keys = [
"nom", "nom",
@ -1074,17 +1281,17 @@ def get_import_contacts_file_sample():
"entreprise_siret", "entreprise_siret",
] ]
titles = keys[:] titles = keys[:]
title = "ImportContacts" title = "ImportCorrespondants"
xlsx = sco_excel.excel_simple_table(titles=titles, sheet_name="Contacts") xlsx = sco_excel.excel_simple_table(titles=titles, sheet_name="Correspondants")
filename = title filename = title
return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE) return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
@bp.route("/import_contacts", methods=["GET", "POST"]) @bp.route("/import_correspondants", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesExport) @permission_required(Permission.RelationsEntreprisesExport)
def import_contacts(): def import_correspondants():
""" """
Permet d'importer des contacts a l'aide d'un fichier excel (.xlsx) Permet d'importer des correspondants a l'aide d'un fichier excel (.xlsx)
""" """
form = ImportForm() form = ImportForm()
if form.validate_on_submit(): if form.validate_on_submit():
@ -1095,8 +1302,8 @@ def import_contacts():
file.save(file_path) file.save(file_path)
data = sco_excel.excel_file_to_list(file_path) data = sco_excel.excel_file_to_list(file_path)
os.remove(file_path) os.remove(file_path)
contacts_import = [] correspondants_import = []
contact_list = [] correspondant_list = []
ligne = 0 ligne = 0
titles = [ titles = [
"nom", "nom",
@ -1110,57 +1317,72 @@ def import_contacts():
if data[1][0] != titles: if data[1][0] != titles:
flash("Veuillez utilisez la feuille excel à remplir") flash("Veuillez utilisez la feuille excel à remplir")
return render_template( return render_template(
"entreprises/import_contacts.html", "entreprises/import_correspondants.html",
title="Importation contacts", title="Importation correspondants",
form=form, form=form,
) )
for contact_data in data[1][1:]: for correspondant_data in data[1][1:]:
ligne += 1 ligne += 1
if ( if (
are.verif_contact_data(contact_data) are.verif_correspondant_data(correspondant_data)
and (contact_data[0], contact_data[1], contact_data[6]) and (
not in contact_list correspondant_data[0].strip(),
): correspondant_data[1].strip(),
contact_list.append((contact_data[0], contact_data[1], contact_data[6])) correspondant_data[6].strip(),
contact = EntrepriseContact(
nom=contact_data[0],
prenom=contact_data[1],
telephone=contact_data[2],
mail=contact_data[3],
poste=contact_data[4],
service=contact_data[5],
entreprise_id=contact_data[6],
) )
contacts_import.append(contact) not in correspondant_list
):
correspondant_list.append(
(
correspondant_data[0].strip(),
correspondant_data[1].strip(),
correspondant_data[6].strip(),
)
)
entreprise = Entreprise.query.filter_by(
siret=correspondant_data[6].strip()
).first()
correspondant = EntrepriseCorrespondant(
nom=correspondant_data[0].strip(),
prenom=correspondant_data[1].strip(),
telephone=correspondant_data[2].strip(),
mail=correspondant_data[3].strip(),
poste=correspondant_data[4].strip(),
service=correspondant_data[5].strip(),
entreprise_id=entreprise.id,
)
correspondants_import.append(correspondant)
else: else:
flash(f"Erreur lors de l'importation (ligne {ligne})") flash(f"Erreur lors de l'importation (ligne {ligne})")
return render_template( return render_template(
"entreprises/import_contacts.html", "entreprises/import_correspondants.html",
title="Importation contacts", title="Importation correspondants",
form=form, form=form,
) )
if len(contacts_import) > 0: if len(correspondants_import) > 0:
for contact in contacts_import: for correspondant in correspondants_import:
db.session.add(contact) db.session.add(correspondant)
log = EntrepriseLog( log = EntrepriseLog(
authenticated_user=current_user.user_name, authenticated_user=current_user.user_name,
text=f"Importation de {len(contacts_import)} contact(s)", text=f"Importation de {len(correspondants_import)} correspondant(s)",
) )
db.session.add(log) db.session.add(log)
db.session.commit() db.session.commit()
flash(f"Importation réussie de {len(contacts_import)} contact(s)") flash(
f"Importation réussie de {len(correspondants_import)} correspondant(s)"
)
return render_template( return render_template(
"entreprises/import_contacts.html", "entreprises/import_correspondants.html",
title="Importation Contacts", title="Importation correspondants",
form=form, form=form,
contacts_import=contacts_import, correspondants_import=correspondants_import,
) )
else: else:
flash('Feuille "Contacts" vide') flash('Feuille "Correspondants" vide')
return render_template( return render_template(
"entreprises/import_contacts.html", "entreprises/import_correspondants.html",
title="Importation contacts", title="Importation correspondants",
form=form, form=form,
) )
@ -1198,7 +1420,7 @@ def get_offre_file(entreprise_id, offre_id, filedir, filename):
abort(404, description=f"fichier {filename} inconnu") abort(404, description=f"fichier {filename} inconnu")
@bp.route("/add_offre_file/<int:offre_id>", methods=["GET", "POST"]) @bp.route("/fiche_entreprise/add_offre_file/<int:offre_id>", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def add_offre_file(offre_id): def add_offre_file(offre_id):
""" """
@ -1230,7 +1452,10 @@ def add_offre_file(offre_id):
) )
@bp.route("/delete_offre_file/<int:offre_id>/<string:filedir>", methods=["GET", "POST"]) @bp.route(
"/fiche_entreprise/delete_offre_file/<int:offre_id>/<string:filedir>",
methods=["GET", "POST"],
)
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def delete_offre_file(offre_id, filedir): def delete_offre_file(offre_id, filedir):
""" """

View File

@ -41,11 +41,8 @@ from wtforms.fields.simple import StringField, HiddenField
from app.models import Departement from app.models import Departement
from app.scodoc import sco_logos, html_sco_header from app.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_config_actions import (
LogoDelete, from app.scodoc.sco_config_actions import LogoInsert
LogoUpdate,
LogoInsert,
)
from app.scodoc.sco_logos import find_logo from app.scodoc.sco_logos import find_logo
@ -120,6 +117,8 @@ def logo_name_validator(message=None):
class AddLogoForm(FlaskForm): class AddLogoForm(FlaskForm):
"""Formulaire permettant l'ajout d'un logo (dans un département)""" """Formulaire permettant l'ajout d'un logo (dans un département)"""
from app.scodoc.sco_config_actions import LogoInsert
dept_key = HiddenField() dept_key = HiddenField()
name = StringField( name = StringField(
label="Nom", label="Nom",
@ -151,7 +150,7 @@ class AddLogoForm(FlaskForm):
dept_id = dept_key_to_id(self.dept_key.data) dept_id = dept_key_to_id(self.dept_key.data)
if dept_id == GLOBAL: if dept_id == GLOBAL:
dept_id = None 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à") raise validators.ValidationError("Un logo de même nom existe déjà")
def select_action(self): def select_action(self):
@ -160,6 +159,14 @@ class AddLogoForm(FlaskForm):
return LogoInsert.build_action(self.data) return LogoInsert.build_action(self.data)
return None 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): class LogoForm(FlaskForm):
"""Embed both presentation of a logo (cf. template file configuration.html) """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): def __init__(self, *args, **kwargs):
kwargs["meta"] = {"csrf": False} kwargs["meta"] = {"csrf": False}
@ -205,12 +223,25 @@ class LogoForm(FlaskForm):
self.titre = "Logo pied de page" self.titre = "Logo pied de page"
def select_action(self): 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: if self.do_delete.data and self.can_delete:
return LogoDelete.build_action(self.data) return LogoDelete.build_action(self.data)
if self.upload.data and self.validate(): if self.upload.data and self.validate():
return LogoUpdate.build_action(self.data) return LogoUpdate.build_action(self.data)
if self.do_rename.data and self.validate():
return LogoRename.build_action(self.data)
return None 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): class DeptForm(FlaskForm):
dept_key = HiddenField() dept_key = HiddenField()
@ -244,6 +275,23 @@ class DeptForm(FlaskForm):
return self return self
return self.index.get(logoname, None) 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(): 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) """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,21 +302,45 @@ class Identite(db.Model):
else: else:
date_ins = events[0].event_date date_ins = events[0].event_date
situation += date_ins.strftime(" le %d/%m/%Y") 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: else:
situation = f"démission de {inscr.formsemestre.titre_mois()}" situation = f"démission de {inscr.formsemestre.titre_mois()}"
# Cherche la date de demission dans scolar_events: # Cherche la date de demission dans scolar_events:
events = models.ScolarEvent.query.filter_by( event = (
models.ScolarEvent.query.filter_by(
etudid=self.id, etudid=self.id,
formsemestre_id=inscr.formsemestre.id, formsemestre_id=inscr.formsemestre.id,
event_type="DEMISSION", event_type="DEMISSION",
).all() )
if not events: .order_by(models.ScolarEvent.event_date)
.first()
)
if not event:
log( log(
f"*** situation inconsistante pour {self} (demission mais pas d'event)" f"*** situation inconsistante pour {self} (demission mais pas d'event)"
) )
date_dem = "???" # ??? situation += "???" # ???
else: else:
date_dem = events[0].event_date date_dem = event.event_date
situation += date_dem.strftime(" le %d/%m/%Y") situation += date_dem.strftime(" le %d/%m/%Y")
else: else:
situation = "non inscrit" + self.e situation = "non inscrit" + self.e

View File

@ -36,18 +36,21 @@ class Scolog(db.Model):
class ScolarNews(db.Model): class ScolarNews(db.Model):
"""Nouvelles pour page d'accueil""" """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_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_MISC = "MISC" # unused
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
NEWS_SEM = "SEM" # creation semestre (object=None)
NEWS_MAP = { NEWS_MAP = {
NEWS_INSCR: "inscription d'étudiants", NEWS_ABS: "saisie absence",
NEWS_NOTE: "saisie note", NEWS_APO: "modif. code Apogée",
NEWS_FORM: "modification formation", NEWS_FORM: "modification formation",
NEWS_SEM: "création semestre", NEWS_INSCR: "inscription d'étudiants",
NEWS_MISC: "opération", # unused NEWS_MISC: "opération", # unused
NEWS_NOTE: "saisie note",
NEWS_SEM: "création semestre",
} }
NEWS_TYPES = list(NEWS_MAP.keys()) NEWS_TYPES = list(NEWS_MAP.keys())

View File

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

View File

@ -286,7 +286,7 @@ class FormSemestre(db.Model):
""" """
if not self.etapes: if not self.etapes:
return "" 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: def responsables_str(self, abbrev_prenom=True) -> str:
"""chaîne "J. Dupond, X. Martin" """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}" return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"
def sem_modalite(self) -> str: 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: if self.semestre_id > 0:
descr_sem = f"S{self.semestre_id}" descr_sem = f"S{self.semestre_id}"
else: else:
@ -433,7 +433,7 @@ notes_formsemestre_responsables = db.Table(
class FormSemestreEtape(db.Model): class FormSemestreEtape(db.Model):
"""Étape Apogée associées au semestre""" """Étape Apogée associée au semestre"""
__tablename__ = "notes_formsemestre_etapes" __tablename__ = "notes_formsemestre_etapes"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)

View File

@ -486,7 +486,10 @@ class JuryPE(object):
sesdates = [ sesdates = [
pe_tagtable.conversionDate_StrToDate(sem["date_fin"]) for sem in sessems pe_tagtable.conversionDate_StrToDate(sem["date_fin"]) for sem in sessems
] # association 1 date -> 1 semestrePE pour les semestres de l'étudiant ] # association 1 date -> 1 semestrePE pour les semestres de l'étudiant
if sesdates:
lastdate = max(sesdates) # date de fin de l'inscription la plus récente 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) # 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): for (i, fid) in enumerate(lesFids):
if pe_tools.PE_DEBUG: if pe_tools.PE_DEBUG:
pe_tools.pe_print( pe_tools.pe_print(
u"%d) Semestre taggué %s (avec classement dans groupe)" "%d) Semestre taggué %s (avec classement dans groupe)"
% (i + 1, fid) % (i + 1, fid)
) )
self.add_semtags_in_jury(fid) self.add_semtags_in_jury(fid)
@ -620,7 +623,7 @@ class JuryPE(object):
nbinscrit = self.semTagDict[fid].get_nbinscrits() nbinscrit = self.semTagDict[fid].get_nbinscrits()
if pe_tools.PE_DEBUG: if pe_tools.PE_DEBUG:
pe_tools.pe_print( pe_tools.pe_print(
u" - %d étudiants classés " % (nbinscrit) " - %d étudiants classés " % (nbinscrit)
+ ": " + ": "
+ ",".join( + ",".join(
[etudid for etudid in self.semTagDict[fid].get_etudids()] [etudid for etudid in self.semTagDict[fid].get_etudids()]
@ -628,12 +631,12 @@ class JuryPE(object):
) )
if lesEtudidsManquants: if lesEtudidsManquants:
pe_tools.pe_print( 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)) % (len(lesEtudidsManquants))
+ ": " + ": "
+ ", ".join(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" filename = self.NOM_EXPORT_ZIP + self.semTagDict[fid].nom + ".csv"
self.zipfile.writestr(filename, self.semTagDict[fid].str_tagtable()) self.zipfile.writestr(filename, self.semTagDict[fid].str_tagtable())
@ -742,7 +745,7 @@ class JuryPE(object):
for fid in fids_finaux: for fid in fids_finaux:
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1: 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( settag = pe_settag.SetTag(
nom, parcours=parcours nom, parcours=parcours
) # Le set tag fusionnant les données ) # Le set tag fusionnant les données
@ -762,7 +765,7 @@ class JuryPE(object):
for ffid in settag.get_Fids_in_settag(): for ffid in settag.get_Fids_in_settag():
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1: if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1:
pe_tools.pe_print( pe_tools.pe_print(
u" -> ajout du semestre tagué %s" % (ffid) " -> ajout du semestre tagué %s" % (ffid)
) )
self.add_semtags_in_jury(ffid) self.add_semtags_in_jury(ffid)
settag.set_SemTagDict( settag.set_SemTagDict(
@ -791,7 +794,7 @@ class JuryPE(object):
if nbreEtudInscrits > 0: if nbreEtudInscrits > 0:
if pe_tools.PE_DEBUG: if pe_tools.PE_DEBUG:
pe_tools.pe_print( 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"]: if nom in ["S1", "S2", "S3", "S4"]:
settag.set_SetTagDict(self.semTagDict) settag.set_SetTagDict(self.semTagDict)
@ -802,7 +805,7 @@ class JuryPE(object):
else: else:
if pe_tools.PE_DEBUG: if pe_tools.PE_DEBUG:
pe_tools.pe_print( 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) % (i + 1, nom)
) )
@ -1152,11 +1155,14 @@ class JuryPE(object):
return sesSems 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 """Calcule et renvoie la date de diplome prévue pour un étudiant fourni avec son etudid
en fonction de sesSemestres de scolarisation""" en fonction de ses semestres de scolarisation"""
sesSemestres = self.get_semestresDUT_d_un_etudiant(etudid) semestres = self.get_semestresDUT_d_un_etudiant(etudid)
return max([get_annee_diplome_semestre(sem) for sem in sesSemestres]) if semestres:
return max([get_annee_diplome_semestre(sem) for sem in semestres])
else:
return None
# ********************************************* # *********************************************
# Fonctions d'affichage pour debug # Fonctions d'affichage pour debug
@ -1184,18 +1190,21 @@ class JuryPE(object):
chaine += "\n" chaine += "\n"
return chaine return chaine
def get_date_entree_etudiant(self, etudid): def get_date_entree_etudiant(self, etudid) -> str:
"""Renvoie la date d'entree d'un étudiant""" """Renvoie la date d'entree d'un étudiant: "1996" """
return str( annees_debut = [
min([int(sem["annee_debut"]) for sem in self.ETUDINFO_DICT[etudid]["sems"]]) int(sem["annee_debut"]) for sem in self.ETUDINFO_DICT[etudid]["sems"]
) ]
if annees_debut:
return str(min(annees_debut))
return ""
# ---------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------
# Fonctions # 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 : """Pour un semestre donne, décrit par le biais du dictionnaire sem usuel :
sem = {'formestre_id': ..., 'semestre_id': ..., 'annee_debut': ...}, sem = {'formestre_id': ..., 'semestre_id': ..., 'annee_debut': ...},
à condition qu'il soit un semestre de formation DUT, à 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_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_before_table="", # html snippet to put before the <table> in the page
html_empty_element="", # replace table when empty html_empty_element="", # replace table when empty
html_table_attrs="", # for html
base_url=None, base_url=None,
origin=None, # string added to excel and xml versions origin=None, # string added to excel and xml versions
filename="table", # filename, without extension filename="table", # filename, without extension
@ -146,6 +147,7 @@ class GenTable(object):
self.html_header = html_header self.html_header = html_header
self.html_before_table = html_before_table self.html_before_table = html_before_table
self.html_empty_element = html_empty_element self.html_empty_element = html_empty_element
self.html_table_attrs = html_table_attrs
self.page_title = page_title self.page_title = page_title
self.pdf_link = pdf_link self.pdf_link = pdf_link
self.xls_link = xls_link self.xls_link = xls_link
@ -383,12 +385,16 @@ class GenTable(object):
colspan_count = colspan colspan_count = colspan
else: else:
colspan_txt = "" colspan_txt = ""
attrs = row.get("_%s_td_attrs" % cid, "")
order = row.get(f"_{cid}_order")
if order:
attrs += f' data-order="{order}"'
r.append( r.append(
"<%s%s %s%s%s>%s</%s>" "<%s%s %s%s%s>%s</%s>"
% ( % (
elem, elem,
std, std,
row.get("_%s_td_attrs" % cid, ""), attrs,
klass, klass,
colspan_txt, colspan_txt,
content, content,
@ -413,8 +419,7 @@ class GenTable(object):
cls = ' class="%s"' % " ".join(tablclasses) cls = ' class="%s"' % " ".join(tablclasses)
else: else:
cls = "" cls = ""
H = [self.html_before_table, f"<table{hid}{cls} {self.html_table_attrs}>"]
H = [self.html_before_table, "<table%s%s>" % (hid, cls)]
line_num = 0 line_num = 0
# thead # thead

View File

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

View File

@ -272,9 +272,15 @@ def _build_etud_res(e, apo_data):
r = {} r = {}
for elt_code in apo_data.apo_elts: for elt_code in apo_data.apo_elts:
elt = apo_data.apo_elts[elt_code] elt = apo_data.apo_elts[elt_code]
try:
# les colonnes de cet élément
col_ids_type = [ col_ids_type = [
(ec["apoL_a01_code"], ec["Type R\xc3\xa9s."]) for ec in elt.cols (ec["apoL_a01_code"], ec["Type R\xc3\xa9s."]) for ec in elt.cols
] # les colonnes de cet élément ]
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] = {} r[elt_code] = {}
for (col_id, type_res) in col_ids_type: for (col_id, type_res) in col_ids_type:
r[elt_code][type_res] = e.cols[col_id] r[elt_code][type_res] = e.cols[col_id]

View File

@ -396,7 +396,7 @@ class ApoEtud(dict):
# Element etape (annuel ou non): # Element etape (annuel ou non):
if sco_formsemestre.sem_has_etape(sem, code) or ( 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 export_res_etape = self.export_res_etape
if (not export_res_etape) and cur_sem: if (not export_res_etape) and cur_sem:
@ -412,7 +412,7 @@ class ApoEtud(dict):
return VOID_APO_RES return VOID_APO_RES
# Element semestre: # 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: if self.export_res_sem:
return self.comp_elt_semestre(nt, decision, etudid) return self.comp_elt_semestre(nt, decision, etudid)
else: else:
@ -421,7 +421,9 @@ class ApoEtud(dict):
# Elements UE # Elements UE
decisions_ue = nt.get_etud_decision_ues(etudid) decisions_ue = nt.get_etud_decision_ues(etudid)
for ue in nt.get_ues_stat_dict(): 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 self.export_res_ues:
if decisions_ue and ue["ue_id"] in decisions_ue: if decisions_ue and ue["ue_id"] in decisions_ue:
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
@ -442,9 +444,10 @@ class ApoEtud(dict):
modimpls = nt.get_modimpls_dict() modimpls = nt.get_modimpls_dict()
module_code_found = False module_code_found = False
for modimpl in modimpls: for modimpl in modimpls:
if modimpl["module"]["code_apogee"] and code in modimpl["module"][ module = modimpl["module"]
"code_apogee" if module["code_apogee"] and code in {
].split(","): x.strip() for x in module["code_apogee"].split(",")
}:
n = nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid) n = nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
if n != "NI" and self.export_res_modules: if n != "NI" and self.export_res_modules:
return dict(N=self.fmt_note(n), B=20, J="", R="") return dict(N=self.fmt_note(n), B=20, J="", R="")
@ -949,8 +952,9 @@ class ApoData(object):
return maq_elems, sem_elems return maq_elems, sem_elems
def get_codes_by_sem(self): def get_codes_by_sem(self):
"""Pour chaque semestre associé, donne l'ensemble des codes Apogée qui s'y trouvent """Pour chaque semestre associé, donne l'ensemble des codes de cette maquette Apogée
(dans le semestre, les UE et les modules) qui s'y trouvent (dans le semestre, les UE ou les modules).
Return: { formsemestre_id : { 'code1', 'code2', ... }}
""" """
codes_by_sem = {} codes_by_sem = {}
for sem in self.sems_etape: for sem in self.sems_etape:
@ -961,8 +965,8 @@ class ApoData(object):
# associé à l'étape, l'année ou les semestre: # associé à l'étape, l'année ou les semestre:
if ( if (
sco_formsemestre.sem_has_etape(sem, code) sco_formsemestre.sem_has_etape(sem, code)
or (code in sem["elt_sem_apo"].split(",")) or (code in {x.strip() for x in sem["elt_sem_apo"].split(",")})
or (code in sem["elt_annee_apo"].split(",")) or (code in {x.strip() for x in sem["elt_annee_apo"].split(",")})
): ):
s.add(code) s.add(code)
continue continue
@ -970,15 +974,18 @@ class ApoData(object):
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
for ue in nt.get_ues_stat_dict(): for ue in nt.get_ues_stat_dict():
if ue["code_apogee"] and code in ue["code_apogee"].split(","): if ue["code_apogee"]:
codes = {x.strip() for x in ue["code_apogee"].split(",")}
if code in codes:
s.add(code) s.add(code)
continue continue
# associé à un module: # associé à un module:
modimpls = nt.get_modimpls_dict() modimpls = nt.get_modimpls_dict()
for modimpl in modimpls: for modimpl in modimpls:
if modimpl["module"]["code_apogee"] and code in modimpl["module"][ module = modimpl["module"]
"code_apogee" if module["code_apogee"]:
].split(","): codes = {x.strip() for x in module["code_apogee"].split(",")}
if code in codes:
s.add(code) s.add(code)
continue continue
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem)) # log('codes_by_sem=%s' % pprint.pformat(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 flask import current_app
from app.scodoc.sco_logos import find_logo
class Action: class Action:
"""Base class for all classes describing an action from from config form.""" """Base class for all classes describing an action from from config form."""
@ -42,9 +41,9 @@ class Action:
self.parameters = parameters self.parameters = parameters
@staticmethod @staticmethod
def build_action(parameters, stream=None): def build_action(parameters):
"""Check (from parameters) if some action has to be done and """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 raise NotImplementedError
def display(self): def display(self):
@ -59,6 +58,45 @@ class Action:
GLOBAL = "_" 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): class LogoUpdate(Action):
"""Action: change a logo """Action: change a logo
dept_id: dept_id or '_', dept_id: dept_id or '_',
@ -83,6 +121,8 @@ class LogoUpdate(Action):
return None return None
def execute(self): def execute(self):
from app.scodoc.sco_logos import write_logo
current_app.logger.info(self.message) current_app.logger.info(self.message)
write_logo( write_logo(
stream=self.parameters["upload"], stream=self.parameters["upload"],
@ -113,6 +153,8 @@ class LogoDelete(Action):
return None return None
def execute(self): def execute(self):
from app.scodoc.sco_logos import delete_logo
current_app.logger.info(self.message) current_app.logger.info(self.message)
delete_logo(name=self.parameters["logo_id"], dept_id=self.parameters["dept_id"]) delete_logo(name=self.parameters["logo_id"], dept_id=self.parameters["dept_id"])
@ -136,13 +178,15 @@ class LogoInsert(Action):
parameters["dept_id"] = None parameters["dept_id"] = None
if parameters["upload"] and parameters["name"]: if parameters["upload"] and parameters["name"]:
logo = find_logo( 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: if logo is None:
return LogoInsert(parameters) return LogoInsert(parameters)
return None return None
def execute(self): def execute(self):
from app.scodoc.sco_logos import write_logo
dept_id = self.parameters["dept_key"] dept_id = self.parameters["dept_key"]
if dept_id == GLOBAL: if dept_id == GLOBAL:
dept_id = None dept_id = None

View File

@ -29,6 +29,7 @@
""" """
from flask import g, request from flask import g, request
from flask import url_for
from flask_login import current_user from flask_login import current_user
import app import app
@ -79,7 +80,7 @@ def index_html(showcodes=0, showsemtable=0):
sco_formsemestre.sem_set_responsable_name(sem) sco_formsemestre.sem_set_responsable_name(sem)
if showcodes: if showcodes:
sem["tmpcode"] = "<td><tt>%s</tt></td>" % sem["formsemestre_id"] sem["tmpcode"] = f"<td><tt>{sem['formsemestre_id']}</tt></td>"
else: else:
sem["tmpcode"] = "" sem["tmpcode"] = ""
# Nombre d'inscrits: # Nombre d'inscrits:
@ -121,26 +122,27 @@ def index_html(showcodes=0, showsemtable=0):
if showsemtable: if showsemtable:
H.append( H.append(
"""<hr/> f"""<hr>
<h2>Semestres de %s</h2> <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(_sem_table_gt(sems, showcodes=showcodes).html())
H.append("</table>") H.append("</table>")
if not showsemtable: if not showsemtable:
H.append( H.append(
'<hr/><p><a href="%s?showsemtable=1">Voir tous les semestres</a></p>' f"""<hr>
% request.base_url <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( H.append(
"""<p><form action="%s/view_formsemestre_by_etape"> f"""<p>
Chercher étape courante: <input name="etape_apo" type="text" size="8" spellcheck="false"></input> <form action="{url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)}">
</form Chercher étape courante:
</p> <input name="etape_apo" type="text" size="8" spellcheck="false"></input>
""" </form>
% scu.NotesURL() </p>"""
) )
# #
if current_user.has_permission(Permission.ScoEtudInscrit): if current_user.has_permission(Permission.ScoEtudInscrit):
@ -148,23 +150,26 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8" spellchec
"""<hr> """<hr>
<h3>Gestion des étudiants</h3> <h3>Gestion des étudiants</h3>
<ul> <ul>
<li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a></li> <li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a>
<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 </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 le tableau de bord semestre si vous souhaitez inscrire les
étudiants importés à un semestre)</li> étudiants importés à un semestre)
</li>
</ul> </ul>
""" """
) )
# #
if current_user.has_permission(Permission.ScoEditApo): if current_user.has_permission(Permission.ScoEditApo):
H.append( H.append(
"""<hr> f"""<hr>
<h3>Exports Apogée</h3> <h3>Exports Apogée</h3>
<ul> <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> </ul>
""" """
% scu.NotesURL()
) )
# #
H.append( 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): def _sem_table(sems):
@ -213,7 +224,9 @@ def _sem_table(sems):
def _sem_table_gt(sems, showcodes=False): 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) _style_sems(sems)
columns_ids = ( columns_ids = (
"lockimg", "lockimg",
@ -224,10 +237,15 @@ def _sem_table_gt(sems, showcodes=False):
"titre_resp", "titre_resp",
"nb_inscrits", "nb_inscrits",
"etapes_apo_str", "etapes_apo_str",
"elt_annee_apo",
"elt_sem_apo",
) )
if showcodes: if showcodes:
columns_ids = ("formsemestre_id",) + columns_ids 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( tab = GenTable(
titles={ titles={
"formsemestre_id": "id", "formsemestre_id": "id",
@ -236,14 +254,23 @@ def _sem_table_gt(sems, showcodes=False):
"mois_debut": "Début", "mois_debut": "Début",
"dash_mois_fin": "Année", "dash_mois_fin": "Année",
"titre_resp": "Semestre", "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, columns_ids=columns_ids,
rows=sems, rows=sems,
html_class="table_leftalign semlist", table_id="semlist",
html_class_ignore_default=True,
html_class=html_class,
html_sortable=True, html_sortable=True,
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id), html_table_attrs=f"""
# caption='Maquettes enregistrées', 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(), preferences=sco_preferences.SemPreferences(),
) )
@ -276,6 +303,16 @@ def _style_sems(sems):
sem["semestre_id_n"] = "" sem["semestre_id_n"] = ""
else: else:
sem["semestre_id_n"] = sem["semestre_id"] 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): 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 import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import (
ScoGenError,
ScoValueError, ScoValueError,
ScoLockedFormError, ScoLockedFormError,
ScoNonEmptyFormationObject, ScoNonEmptyFormationObject,
@ -61,7 +59,6 @@ from app.scodoc import sco_edit_module
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
_ueEditor = ndb.EditableTable( _ueEditor = ndb.EditableTable(
@ -1355,93 +1352,6 @@ def ue_is_locked(ue_id):
return len(r) > 0 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): def ue_list_semestre_ids(ue: dict):
"""Liste triée des numeros de semestres des modules dans cette UE """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. 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. Voir sco_apogee_csv.py pour la structure du fichier Apogée.
Stockage: utilise sco_archive.py Stockage: utilise sco_archive.py
=> /opt/scodoc/var/scodoc/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR.csv exemple:
pour une maquette de l'année scolaire 2016, semestre 1, etape V3ASR /opt/scodoc-data/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR!111.csv
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
pour une maquette de l'étape V3ASR version VDI 111. 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. 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 import flask
from flask import url_for, g, send_file, request from flask import url_for, g, send_file, request
# from werkzeug.utils import send_file
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc import html_sco_header 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): def get_formsemestre(formsemestre_id, raise_soft_exc=False):
"list ONE formsemestre" "list ONE formsemestre"
if formsemestre_id is None:
raise ValueError(f"get_formsemestre: id manquant")
if formsemestre_id in g.stored_get_formsemestre: if formsemestre_id in g.stored_get_formsemestre:
return g.stored_get_formsemestre[formsemestre_id] return g.stored_get_formsemestre[formsemestre_id]
if not isinstance(formsemestre_id, int): if not isinstance(formsemestre_id, int):
log(f"get_formsemestre: invalid id '{formsemestre_id}'")
raise ScoInvalidIdType("formsemestre_id must be an integer !") raise ScoInvalidIdType("formsemestre_id must be an integer !")
sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id}) sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
if not sems: if not sems:
@ -141,7 +144,6 @@ def _formsemestre_enrich(sem):
"""Ajoute champs souvent utiles: titre + annee et dateord (pour tris)""" """Ajoute champs souvent utiles: titre + annee et dateord (pour tris)"""
# imports ici pour eviter refs circulaires # imports ici pour eviter refs circulaires
from app.scodoc import sco_formsemestre_edit 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] F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) 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 """SELECT etape_apo
FROM notes_formsemestre_etapes FROM notes_formsemestre_etapes
WHERE formsemestre_id = %(formsemestre_id)s WHERE formsemestre_id = %(formsemestre_id)s
ORDER BY etape_apo
""", """,
{"formsemestre_id": formsemestre_id}, {"formsemestre_id": formsemestre_id},
) )

View File

@ -28,6 +28,7 @@
"""Tableau de bord semestre """Tableau de bord semestre
""" """
import datetime
from flask import current_app from flask import current_app
from flask import g from flask import g
from flask import request from flask import request
@ -760,8 +761,7 @@ def _make_listes_sem(sem, with_absences=True):
) )
formsemestre_id = sem["formsemestre_id"] formsemestre_id = sem["formsemestre_id"]
weekday = datetime.datetime.today().weekday()
# calcule dates 1er jour semaine pour absences
try: try:
if with_absences: if with_absences:
first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday() 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"> <select name="datedebut" class="noprint">
""" """
date = first_monday date = first_monday
for jour in sco_abs.day_names(): for idx, jour in enumerate(sco_abs.day_names()):
form_abs_tmpl += f'<option value="{date}">{jour}s</option>' form_abs_tmpl += f"""<option value="{date}" {'selected' if idx == weekday else ''}>{jour}s</option>"""
date = date.next_day() date = date.next_day()
form_abs_tmpl += f""" form_abs_tmpl += f"""
</select> </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): def formsemestre_status(formsemestre_id=None):
"""Tableau de bord semestre HTML""" """Tableau de bord semestre HTML"""
# porté du DTML # porté du DTML
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True) sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
modimpls = sco_moduleimpl.moduleimpl_withmodule_list( modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id 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) use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
H = [ 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">', '<div class="formsemestre_status">',
formsemestre_status_head( formsemestre_status_head(
formsemestre_id=formsemestre_id, page_title="Tableau de bord" 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) 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(): def list_logos():
"""Crée l'inventaire de tous les logos existants. """Crée l'inventaire de tous les logos existants.
L'inventaire se présente comme un dictionnaire de dictionnaire de Logo: L'inventaire se présente comme un dictionnaire de dictionnaire de Logo:
@ -285,6 +290,20 @@ class Logo:
dt = path.stat().st_mtime dt = path.stat().st_mtime
return 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: def guess_image_type(stream) -> str:
"guess image type from header in stream" "guess image type from header in stream"

View File

@ -37,12 +37,20 @@ import xml.dom.minidom
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc import sco_cache
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
SCO_CACHE_ETAPE_FILENAME = os.path.join(scu.SCO_TMP_DIR, "last_etapes.xml") 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(): def has_portal():
"True if we are connected to a portal" "True if we are connected to a portal"
return get_portal_url() return get_portal_url()
@ -139,14 +147,20 @@ get_maquette_url = _PI.get_maquette_url
get_portal_api_version = _PI.get_portal_api_version 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 """Liste des inscrits à une étape Apogée
Result = list of dicts Result = list of dicts
ntrials: try several time the same request, useful for some bad web services 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)) log("get_inscrits_etape: code=%s anneeapogee=%s" % (code_etape, anneeapogee))
if anneeapogee is None: if anneeapogee is None:
anneeapogee = str(time.localtime()[0]) 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() etud_url = get_etud_url()
api_ver = get_portal_api_version() 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 return False # ??? pas d'annee d'inscription dans la réponse
etuds = [e for e in etuds if check_inscription(e)] etuds = [e for e in etuds if check_inscription(e)]
if use_cache and etuds:
ApoInscritsEtapeCache.set((code_etape, anneeapogee), etuds)
return etuds return etuds

View File

@ -103,7 +103,7 @@ def formsemestre_recapcomplet(
return data return data
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Récapitulatif", page_title=f"{formsemestre.sem_modalite()}: moyennes",
no_side_bar=True, no_side_bar=True,
init_qtip=True, init_qtip=True,
javascripts=["js/etud_info.js", "js/table_recap.js"], 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, typ=ScolarNews.NEWS_INSCR,
text="Import Apogée de %d étudiants en " % len(created_etudids), text="Import Apogée de %d étudiants en " % len(created_etudids),
obj=sem["formsemestre_id"], 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): def send_file(data, filename="", suffix="", mime=None, attached=None):
"""Build Flask Response for file download of given type """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 attached is None:
if mime == XML_MIMETYPE or mime == JSON_MIMETYPE: if mime == XML_MIMETYPE or mime == JSON_MIMETYPE:
attached = False attached = False
else: else:
attached = True attached = True
# if attached and not filename:
# raise ValueError("send_file: missing attachement filename")
if filename: if filename:
if suffix: if suffix:
filename += suffix filename += suffix
@ -755,7 +753,7 @@ def send_docx(document, filename):
buffer.seek(0) buffer.seek(0)
return flask.send_file( return flask.send_file(
buffer, buffer,
attachment_filename=sanitize_filename(filename), download_name=sanitize_filename(filename),
mimetype=DOCX_MIMETYPE, mimetype=DOCX_MIMETYPE,
) )
@ -873,6 +871,20 @@ def annee_scolaire_debut(year, month):
return int(year) - 1 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): def sem_decale_str(sem):
"""'D' si semestre decalé, ou ''""" """'D' si semestre decalé, ou ''"""
# considère "décalé" les semestre impairs commençant entre janvier et juin # 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 { .nav-entreprise>ul>li>a:hover {
color: red; color: red;
} }
@ -50,23 +54,23 @@
margin-bottom: -5px; margin-bottom: -5px;
} }
.entreprise, .contact, .offre { .entreprise, .correspondant, .offre {
border: solid 2px; border: solid 2px;
border-radius: 10px; border-radius: 10px;
padding: 10px; padding: 10px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.contacts-et-offres { .correspondants-et-offres {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.contacts-et-offres > div { .correspondants-et-offres > div {
flex: 1 0 0; flex: 1 0 0;
} }
.contacts-et-offres > div:nth-child(2) { .correspondants-et-offres > div:nth-child(2) {
margin-left: 20px; margin-left: 20px;
} }

View File

@ -427,8 +427,8 @@ table.semlist tr td {
border: none; border: none;
} }
table.semlist tr a.stdlink, table.semlist tbody tr a.stdlink,
table.semlist tr a.stdlink:visited { table.semlist tbody tr a.stdlink:visited {
color: navy; color: navy;
text-decoration: none; text-decoration: none;
} }
@ -442,32 +442,86 @@ table.semlist tr td.semestre_id {
text-align: right; text-align: right;
} }
table.semlist tr td.modalite { table.semlist tbody tr td.modalite {
text-align: left; text-align: left;
padding-right: 1em; 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); 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%); 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); 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); 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); background-color: rgb(131, 225, 140);
} }
div#gtrcontent table.semlist tr.css_MEXT { div#gtrcontent table.semlist tbody tr.css_MEXT td {
color: #0b6e08; color: #0b6e08;
} }
@ -1001,6 +1055,14 @@ span.wtf-field ul.errors li {
display: list-item !important; display: list-item !important;
} }
.configuration_logo entete_dept {
display: inline-block;
}
.configuration_logo .effectifs {
float: right;
}
.configuration_logo h1 { .configuration_logo h1 {
display: inline-block; display: inline-block;
} }
@ -3910,3 +3972,17 @@ table.evaluations_recap td.nb_att,
table.evaluations_recap td.nb_exc { table.evaluations_recap td.nb_exc {
text-align: center; 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 { div.title_MALUS {
background-color: #ff4700; background-color: #ff4700;
} }
.sums {
background: #ddd;
}
/***************************/ /***************************/
/* Statut des cellules */ /* 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>'); 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 */ /* Mise en place des données */
/*****************************/ /*****************************/
let lastX;
let lastY;
function build_table(data) { function build_table(data) {
let output = ""; let output = "";
let sumsUE = {};
let sumsRessources = {};
data.forEach((cellule) => { data.forEach((cellule) => {
output += ` output += `
@ -29,11 +34,61 @@ function build_table(data) {
"> ">
${cellule.data} ${cellule.data}
</div>`; </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; document.querySelector(".tableau").innerHTML = output;
installListeners(); 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 */ /* Gestion des évènements */
/*****************************/ /*****************************/
@ -54,6 +109,7 @@ function installListeners() {
} }
} }
}); });
cellule.addEventListener("input", processSums);
}); });
} }
@ -120,9 +176,26 @@ function keyCell(event) {
return return
} }
this.classList.remove("modifying"); this.classList.remove("modifying");
let selected = document.querySelector(".selected");
ArrowMove(0, 1); ArrowMove(0, 1);
if (selected != document.querySelector(".selected")) {
modifCell(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;
} }
/******************************/ /******************************/

View File

@ -31,6 +31,18 @@
display: none; display: none;
}`; }`;
releve.shadowRoot.appendChild(style); 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"; document.querySelector("html").style.scrollBehavior = "smooth";
</script> </script>

View File

@ -20,7 +20,7 @@
{% endmacro %} {% endmacro %}
{% macro render_add_logo(add_logo_form) %} {% macro render_add_logo(add_logo_form) %}
<details> <details {{ add_logo_form.opened() }}>
<summary> <summary>
<h3>Ajouter un logo</h3> <h3>Ajouter un logo</h3>
</summary> </summary>
@ -33,7 +33,7 @@
{% endmacro %} {% endmacro %}
{% macro render_logo(dept_form, logo_form) %} {% macro render_logo(dept_form, logo_form) %}
<details> <details {{ logo_form.opened() }}>
{{ logo_form.hidden_tag() }} {{ logo_form.hidden_tag() }}
<summary> <summary>
{% if logo_form.titre %} {% if logo_form.titre %}
@ -65,6 +65,11 @@
<span class="wtf-field">{{ render_field(logo_form.upload, False, onchange="submit_form()") }}</span> <span class="wtf-field">{{ render_field(logo_form.upload, False, onchange="submit_form()") }}</span>
</div> </div>
{% if logo_form.can_delete %} {% 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_label">Supprimer l'image</div>
<div class="action_button"> <div class="action_button">
{{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }} {{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }}
@ -97,20 +102,24 @@
<div class="configuration_logo"> <div class="configuration_logo">
<h1>Bibliothèque de logos</h1> <h1>Bibliothèque de logos</h1>
{% for dept_entry in form.depts.entries %} {% for dept_entry in form.depts.entries %}
<details>
{% set dept_form = dept_entry.form %} {% set dept_form = dept_entry.form %}
{{ dept_entry.form.hidden_tag() }} {{ dept_entry.form.hidden_tag() }}
<details {{ dept_form.opened() }}>
<summary> <summary>
<span class="entete_dept">
{% if dept_entry.form.is_local() %} {% if dept_entry.form.is_local() %}
<h2>Département {{ dept_form.dept_name.data }}</h2> <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 /> <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> Les logos du département se substituent aux logos de même nom définis globalement:</div>
{% else %} {% else %}
<h2>Logos généraux</h2> <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, <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 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> (il suffit de définir un logo local de même nom)</div>
{% endif %} {% endif %}
</span>
</summary> </summary>
<div> <div>
{{ render_logos(dept_form) }} {{ 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 -*- #} {# -*- mode: jinja-html -*- #}
<div class="offre"> <div class="offre">
<div> <div>
Ajouté le {{ offre[0].date_ajout.strftime('%d/%m/%y') }} à {{ offre[0].date_ajout.strftime('%Hh%M') }}<br>
Intitulé : {{ offre[0].intitule }}<br> Intitulé : {{ offre[0].intitule }}<br>
Description : {{ offre[0].description }}<br> Description : {{ offre[0].description }}<br>
Type de l'offre : {{ offre[0].type_offre }}<br> Type de l'offre : {{ offre[0].type_offre }}<br>
@ -9,6 +10,16 @@
{% if offre[2] %} {% 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> Département(s) : {% for offre_dept in offre[2] %} <div class="offre-depts">{{ offre_dept.dept_id|get_dept_acronym }}</div> {% endfor %}<br>
{% endif %} {% 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] %} {% 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> <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) %} {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
@ -16,6 +27,7 @@
{% endif %} {% endif %}
<br> <br>
{% endfor %} {% endfor %}
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} {% 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> <a href="{{ url_for('entreprises.add_offre_file', offre_id=offre[0].id) }}">Ajoutez un fichier</a>
{% endif %} {% 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-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> <a class="btn btn-danger" href="{{ url_for('entreprises.delete_offre', id=offre[0].id) }}">Supprimer l'offre</a>
{% endif %} {% endif %}
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesSend, None) %} {% 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> <a class="btn btn-primary" href="{{ url_for('entreprises.envoyer_offre', id=offre[0].id) }}">Envoyer l'offre</a>
{% endif %} {% endif %}
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %} {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
{% if not offre[0].expired %} {% if not offre[0].expired %}
<a class="btn btn-danger" href="{{ url_for('entreprises.expired', id=offre[0].id) }}">Rendre expirée</a> <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 %} {% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %} {% block app_content %}
<h1>Ajout entreprise avec contact</h1> <h1>Ajout entreprise</h1>
<br> <br>
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
@ -16,12 +16,18 @@
</div> </div>
<script> <script>
window.onload = function(e){ {# 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); document.getElementById("siret").addEventListener("keyup", autocomplete);
function autocomplete() { function autocomplete() {
var input = document.getElementById("siret").value; var input = document.getElementById("siret").value.replaceAll(" ", "")
if(input.length == 14) { if(input.length >= 14) {
fetch("https://entreprise.data.gouv.fr/api/sirene/v1/siret/" + input) fetch("https://entreprise.data.gouv.fr/api/sirene/v1/siret/" + input)
.then(response => { .then(response => {
if(response.ok) if(response.ok)
@ -48,13 +54,5 @@
document.getElementById("codepostal").value = '' document.getElementById("codepostal").value = ''
document.getElementById("ville").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"
}
</script> </script>
{% endblock %} {% endblock %}

View File

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

View File

@ -9,64 +9,51 @@
{% endblock %} {% endblock %}
{% block app_content %} {% block app_content %}
{% include 'entreprises/nav.html' %} <div class="container" style="margin-bottom: 10px;">
{% 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;">
<h1>Liste des contacts</h1> <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"> <table id="table-contacts">
<thead> <thead>
<tr> <tr>
<td data-priority="1">Nom</td> <td data-priority="">Date</td>
<td data-priority="3">Prenom</td> <td data-priority="">Utilisateur</td>
<td data-priority="4">Telephone</td> <td data-priority="">Notes</td>
<td data-priority="5">Mail</td> {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<td data-priority="6">Poste</td> <td data-priority="">Action</td>
<td data-priority="7">Service</td> {% endif %}
<td data-priority="2">Entreprise</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for contact in contacts %} {% for contact in contacts %}
<tr> <tr>
<td>{{ contact[0].nom }}</td> <td>{{ contact.date.strftime('%d/%m/%Y %Hh%M') }}</td>
<td>{{ contact[0].prenom }}</td> <td>{{ contact.user|get_nomcomplet_by_id }}</td>
<td>{{ contact[0].telephone }}</td> <td>{{ contact.notes }}</td>
<td>{{ contact[0].mail }}</td> {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<td>{{ contact[0].poste}}</td> <td>
<td>{{ contact[0].service}}</td> <div class="btn-group">
<td><a href="{{ url_for('entreprises.fiche_entreprise', id=contact[1].id) }}">{{ contact[1].nom }}</a></td> <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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<td>Nom</td> <td>Date</td>
<td>Prenom</td> <td>Utilisateur</td>
<td>Telephone</td> <td>Notes</td>
<td>Mail</td> {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesChange, None) %}
<td>Poste</td> <td>Action</td>
<td>Service</td> {% endif %}
<td>Entreprise</td>
</tr> </tr>
</tfoot> </tfoot>
</table> </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> <p>
(*) champs requis (*) champs requis
</p> </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>
</div> </div>
@ -30,7 +45,28 @@
minchars: 2, minchars: 2,
timeout: 60000 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> </script>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,13 @@
{# -*- mode: jinja-html -*- #} {# -*- mode: jinja-html -*- #}
{% extends 'base.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 %} {% block app_content %}
{% if logs %} {% if logs %}
<div class="container"> <div class="container">
@ -16,24 +23,6 @@
</div> </div>
{% endif %} {% 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"> <div class="container fiche-entreprise">
<h2>Fiche entreprise - {{ entreprise.nom }} ({{ entreprise.siret }})</h2> <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-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-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_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_correspondant', id=entreprise.id) }}">Ajouter correspondant</a>
<a class="btn btn-primary" href="{{ url_for('entreprises.add_historique', id=entreprise.id) }}">Ajouter
historique</a>
{% endif %} {% 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> <a class="btn btn-primary" href="{{ url_for('entreprises.offres_expirees', id=entreprise.id) }}">Voir les offres expirées</a>
<div> <div>
<div class="contacts-et-offres"> <div class="correspondants-et-offres">
{% if contacts %} {% if correspondants %}
<div> <div>
<h3>Contacts</h3> <h3>Correspondants</h3>
{% for contact in contacts %} {% for correspondant in correspondants %}
{% include 'entreprises/_contact.html' %} {% include 'entreprises/_correspondant.html' %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
@ -81,4 +69,95 @@
{% endif %} {% endif %}
</div> </div>
</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 %} {% endblock %}

View File

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

View File

@ -4,6 +4,8 @@
{% block styles %} {% block styles %}
{{super()}} {{super()}}
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/css/autosuggest_inquisitor.css" />
<script src="/ScoDoc/static/libjs/AutoSuggest.js"></script>
{% endblock %} {% endblock %}
{% block app_content %} {% block app_content %}
@ -25,5 +27,36 @@
var closest_form_control = champ_depts.closest(".form-control") var closest_form_control = champ_depts.closest(".form-control")
closest_form_control.classList.remove("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> </script>
{% endblock %} {% 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"> <nav class="nav-entreprise">
<ul> <ul>
<li><a href="{{ url_for('entreprises.index') }}">Entreprises</a></li> <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> <li><a href="{{ url_for('entreprises.offres_recues') }}">Offres reçues</a></li>
{% if current_user.has_permission(current_user.Permission.RelationsEntreprisesValidate, None) %} {% if current_user.has_permission(current_user.Permission.RelationsEntreprisesValidate, None) %}
<li><a href="{{ url_for('entreprises.validation') }}">Entreprises à valider</a></li> <li><a href="{{ url_for('entreprises.validation') }}">Entreprises à valider</a></li>

View File

@ -10,12 +10,22 @@
{% for offre in offres_recues %} {% for offre in offres_recues %}
<div class="offre offre-recue"> <div class="offre offre-recue">
<div> <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> Intitulé : {{ offre[1].intitule }}<br>
Description : {{ offre[1].description }}<br> Description : {{ offre[1].description }}<br>
Type de l'offre : {{ offre[1].type_offre }}<br> Type de l'offre : {{ offre[1].type_offre }}<br>
Missions : {{ offre[1].missions }}<br> Missions : {{ offre[1].missions }}<br>
Durée : {{ offre[1].duree }}<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> <a href="{{ url_for('entreprises.fiche_entreprise', id=offre[1].entreprise_id) }}">lien vers l'entreprise</a><br>
{% for fichier in offre[2] %} {% for fichier in offre[2] %}

View File

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

View File

@ -45,11 +45,13 @@ from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.models.formsemestre import FormSemestreUEComputationExpr from app.models.formsemestre import FormSemestreUEComputationExpr
from app.models.modules import Module
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app import api from app import api
from app import db from app import db
from app import models from app import models
from app.models import ScolarNews
from app.auth.models import User from app.auth.models import User
from app.but import bulletin_but from app.but import bulletin_but
from app.decorators import ( 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
from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_cache 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_cost_formation
from app.scodoc import sco_debouche from app.scodoc import sco_debouche
from app.scodoc import sco_edit_apc 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_evaluation_recap
from app.scodoc import sco_export_results from app.scodoc import sco_export_results
from app.scodoc import sco_formations 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
from app.scodoc import sco_formsemestre_custommenu from app.scodoc import sco_formsemestre_custommenu
from app.scodoc import sco_formsemestre_edit from app.scodoc import sco_formsemestre_edit
@ -288,10 +290,9 @@ def formsemestre_bulletinetud(
code_ine=None, code_ine=None,
): ):
format = format or "html" format = format or "html"
if not formsemestre_id:
flask.abort(404, "argument manquant: formsemestre_id")
if not isinstance(formsemestre_id, int): 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) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if etudid: if etudid:
etud = models.Identite.query.get_or_404(etudid) etud = models.Identite.query.get_or_404(etudid)
@ -472,6 +473,7 @@ sco_publish(
"/edit_ue_set_code_apogee", "/edit_ue_set_code_apogee",
sco_edit_ue.edit_ue_set_code_apogee, sco_edit_ue.edit_ue_set_code_apogee,
Permission.ScoChangeFormation, Permission.ScoChangeFormation,
methods=["POST"],
) )
sco_publish( sco_publish(
"/formsemestre_edit_uecoefs", "/formsemestre_edit_uecoefs",
@ -479,8 +481,20 @@ sco_publish(
Permission.ScoView, Permission.ScoView,
methods=["GET", "POST"], 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( 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( sco_publish(
"/formation_add_malus_modules", "/formation_add_malus_modules",
@ -571,6 +585,20 @@ def index_html():
</li> </li>
<li><a class="stdlink" href="formation_import_xml_form">Importer une formation (xml)</a> <li><a class="stdlink" href="formation_import_xml_form">Importer une formation (xml)</a>
</li> </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> </ul>
<h3>Référentiels de compétences</h3> <h3>Référentiels de compétences</h3>
<ul> <ul>
@ -2410,6 +2438,125 @@ sco_publish(
Permission.ScoEditApo, 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_semset
sco_publish("/semset_page", sco_semset.semset_page, Permission.ScoEditApo) sco_publish("/semset_page", sco_semset.semset_page, Permission.ScoEditApo)
sco_publish( sco_publish(

View File

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

View File

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

View File

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

View File

@ -2,20 +2,15 @@
Test calcul moyennes UE Test calcul moyennes UE
""" """
import numpy as np import numpy as np
from numpy.lib.nanfunctions import _nanquantile_1d
import pandas as pd
from tests.unit import setup from tests.unit import setup
from tests.unit import sco_fake_gen
from app import db from app import db
from app import models
from app.comp import moy_mod
from app.comp import moy_ue from app.comp import moy_ue
from app.comp import inscr_mod from app.comp import inscr_mod
from app.models import FormSemestre, Evaluation, ModuleImplInscription from app.models import FormSemestre, Evaluation, ModuleImplInscription
from app.models.etudiants import Identite from app.scodoc import sco_saisie_notes
from app.scodoc import sco_codes_parcours, sco_saisie_notes from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import NOTES_ATTENTE, NOTES_NEUTRALISE from app.scodoc.sco_utils import NOTES_NEUTRALISE
from app.scodoc import sco_exceptions 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)]) _ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)])
# Recalcul des moyennes # Recalcul des moyennes
sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre) 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() etuds = formsemestre.etuds.all()
etud_moy_ue = moy_ue.compute_ue_moys_apc( 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 return etud_moy_ue
@ -113,8 +119,11 @@ def test_ue_moy(test_client):
# Recalcule les notes: # Recalcule les notes:
sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre) sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre)
etuds = formsemestre.etuds.all() 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( 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[ue1.id][etudid] == n1
assert etud_moy_ue[ue2.id][etudid] == n1 assert etud_moy_ue[ue2.id][etudid] == n1