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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,14 +12,17 @@ from app.decorators import permission_required
from app.entreprises import LOGS_LEN
from app.entreprises.forms import (
CorrespondantsCreationForm,
EntrepriseCreationForm,
EntrepriseModificationForm,
SuppressionConfirmationForm,
OffreCreationForm,
OffreModificationForm,
CorrespondantModificationForm,
ContactCreationForm,
ContactModificationForm,
HistoriqueCreationForm,
StageApprentissageCreationForm,
StageApprentissageModificationForm,
EnvoiOffreForm,
AjoutFichierForm,
ValidationConfirmationForm,
@ -30,9 +33,10 @@ from app.entreprises import bp
from app.entreprises.models import (
Entreprise,
EntrepriseOffre,
EntrepriseContact,
EntrepriseCorrespondant,
EntrepriseLog,
EntrepriseEtudiant,
EntrepriseContact,
EntrepriseStageApprentissage,
EntrepriseEnvoiOffre,
EntrepriseOffreDepartement,
EntreprisePreferences,
@ -96,22 +100,22 @@ def validation():
)
@bp.route("/contacts", methods=["GET"])
@bp.route("/correspondants", methods=["GET"])
@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 = (
db.session.query(EntrepriseContact, Entreprise)
.join(Entreprise, EntrepriseContact.entreprise_id == Entreprise.id)
correspondants = (
db.session.query(EntrepriseCorrespondant, Entreprise)
.join(Entreprise, EntrepriseCorrespondant.entreprise_id == Entreprise.id)
.filter_by(visible=True)
)
logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all()
return render_template(
"entreprises/contacts.html",
title="Contacts",
contacts=contacts,
"entreprises/correspondants.html",
title="Correspondants",
correspondants=correspondants,
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
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.
"""
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)
if offre_with_files is not None:
offres_with_files.append(offre_with_files)
contacts = entreprise.contacts[:]
correspondants = entreprise.correspondants[:]
logs = (
EntrepriseLog.query.order_by(EntrepriseLog.date.desc())
.filter_by(object=id)
.limit(LOGS_LEN)
.all()
)
historique = (
db.session.query(EntrepriseEtudiant, Identite)
.order_by(EntrepriseEtudiant.date_debut.desc())
.filter(EntrepriseEtudiant.entreprise_id == id)
.join(Identite, Identite.id == EntrepriseEtudiant.etudid)
stages_apprentissages = (
db.session.query(EntrepriseStageApprentissage, Identite)
.order_by(EntrepriseStageApprentissage.date_debut.desc())
.filter(EntrepriseStageApprentissage.entreprise_id == id)
.join(Identite, Identite.id == EntrepriseStageApprentissage.etudid)
.all()
)
return render_template(
"entreprises/fiche_entreprise.html",
title="Fiche entreprise",
entreprise=entreprise,
contacts=contacts,
correspondants=correspondants,
offres=offres_with_files,
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)
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(
description=f"fiche entreprise (validation) {id} inconnue"
)
contacts = entreprise.contacts
correspondants = entreprise.correspondants
return render_template(
"entreprises/fiche_entreprise_validation.html",
title="Validation fiche entreprise",
entreprise=entreprise,
contacts=contacts,
correspondants=correspondants,
)
@ -221,6 +225,9 @@ def offres_recues():
)
offres_recues_with_files = []
for offre in offres_recues:
correspondant = EntrepriseCorrespondant.query.filter_by(
id=offre[1].correspondant_id
).first()
files = []
path = os.path.join(
Config.SCODOC_VAR_DIR,
@ -235,7 +242,7 @@ def offres_recues():
for file in glob.glob(f"{dir}/*"):
file = [os.path.basename(dir), os.path.basename(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(
"entreprises/offres_recues.html",
title="Offres reçues",
@ -287,23 +294,24 @@ def add_entreprise():
)
db.session.add(entreprise)
db.session.commit()
if form.nom_correspondant.data.strip():
db.session.refresh(entreprise)
contact = EntrepriseContact(
correspondant = EntrepriseCorrespondant(
entreprise_id=entreprise.id,
nom=form.nom_contact.data.strip(),
prenom=form.prenom_contact.data.strip(),
nom=form.nom_correspondant.data.strip(),
prenom=form.prenom_correspondant.data.strip(),
telephone=form.telephone.data.strip(),
mail=form.mail.data.strip(),
poste=form.poste.data.strip(),
service=form.service.data.strip(),
)
db.session.add(contact)
db.session.add(correspondant)
if current_user.has_permission(Permission.RelationsEntreprisesValidate, None):
entreprise.visible = True
nom_entreprise = f"<a href=/ScoDoc/entreprises/fiche_entreprise/{entreprise.id}>{entreprise.nom}</a>"
log = EntrepriseLog(
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.commit()
@ -314,18 +322,18 @@ def add_entreprise():
db.session.commit()
if EntreprisePreferences.get_email_notifications():
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.")
return redirect(url_for("entreprises.index"))
return render_template(
"entreprises/ajout_entreprise.html",
title="Ajout entreprise avec contact",
title="Ajout entreprise avec correspondant",
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)
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)
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)
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>"
log = EntrepriseLog(
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.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)
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)
def add_offre(id):
"""
@ -491,7 +504,7 @@ def add_offre(id):
entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404(
description=f"entreprise {id} inconnue"
)
form = OffreCreationForm()
form = OffreCreationForm(hidden_entreprise_id=id)
if form.validate_on_submit():
offre = EntrepriseOffre(
entreprise_id=entreprise.id,
@ -501,6 +514,7 @@ def add_offre(id):
missions=form.missions.data.strip(),
duree=form.duree.data.strip(),
expiration_date=form.expiration_date.data,
correspondant_id=form.correspondant.data,
)
db.session.add(offre)
db.session.commit()
@ -511,6 +525,19 @@ def add_offre(id):
dept_id=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(
authenticated_user=current_user.user_name,
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)
def edit_offre(id):
"""
@ -537,7 +564,9 @@ def edit_offre(id):
description=f"offre {id} inconnue"
)
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]
if form.validate_on_submit():
offre.intitule = form.intitule.data.strip()
@ -546,6 +575,7 @@ def edit_offre(id):
offre.missions = form.missions.data.strip()
offre.duree = form.duree.data.strip()
offre.expiration_date = form.expiration_date.data
offre.correspondant_id = form.correspondant.data
if offre_depts_list != form.depts.data:
for dept in form.depts.data:
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)
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)
def delete_offre_recue(id):
"""
@ -635,7 +665,7 @@ def delete_offre_recue(id):
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)
def expired(id):
"""
@ -653,36 +683,153 @@ def expired(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)
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(
description=f"entreprise {id} inconnue"
)
form = ContactCreationForm(hidden_entreprise_id=entreprise.id)
form = CorrespondantsCreationForm(hidden_entreprise_id=entreprise.id)
if form.validate_on_submit():
contact = EntrepriseContact(
for correspondant_entry in form.correspondants.entries:
correspondant = EntrepriseCorrespondant(
entreprise_id=entreprise.id,
nom=form.nom.data.strip(),
prenom=form.prenom.data.strip(),
telephone=form.telephone.data.strip(),
mail=form.mail.data.strip(),
poste=form.poste.data.strip(),
service=form.service.data.strip(),
nom=correspondant_entry.nom.data.strip(),
prenom=correspondant_entry.prenom.data.strip(),
telephone=correspondant_entry.telephone.data.strip(),
mail=correspondant_entry.mail.data.strip(),
poste=correspondant_entry.poste.data.strip(),
service=correspondant_entry.service.data.strip(),
)
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=entreprise.id,
text="Création d'un contact",
text="Création d'un correspondant",
)
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.commit()
flash("Le contact a été ajouté à la fiche entreprise.")
return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id))
return redirect(url_for("entreprises.contacts", id=entreprise.id))
return render_template(
"entreprises/form.html",
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)
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(
description=f"contact {id} inconnu"
)
form = ContactModificationForm(
hidden_entreprise_id=contact.entreprise_id,
hidden_contact_id=contact.id,
)
form = ContactModificationForm()
if form.validate_on_submit():
contact.nom = form.nom.data.strip()
contact.prenom = form.prenom.data.strip()
contact.telephone = form.telephone.data.strip()
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",
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"
)
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()
flash("Le contact a été modifié.")
return redirect(
url_for("entreprises.fiche_entreprise", id=contact.entreprise.id)
)
return redirect(url_for("entreprises.contacts", id=contact.entreprise))
elif request.method == "GET":
form.nom.data = contact.nom
form.prenom.data = contact.prenom
form.telephone.data = contact.telephone
form.mail.data = contact.mail
form.poste.data = contact.poste
form.service.data = contact.service
utilisateur = User.query.filter_by(id=contact.user).first()
form.date.data = contact.date.strftime("%Y-%m-%dT%H:%M")
form.utilisateur.data = (
f"{utilisateur.nom} {utilisateur.prenom} ({utilisateur.user_name})"
)
form.notes.data = contact.notes
return render_template(
"entreprises/form.html",
title="Modification contact",
@ -735,57 +876,31 @@ def edit_contact(id):
)
@bp.route("/delete_contact/<int:id>", methods=["GET", "POST"])
@permission_required(Permission.RelationsEntreprisesChange)
def delete_contact(id):
@bp.route("/fiche_entreprise/<int:id>/contacts")
@permission_required(Permission.RelationsEntreprisesView)
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(
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)
)
contacts = EntrepriseContact.query.filter_by(entreprise=id).all()
return render_template(
"entreprises/delete_confirmation.html",
title="Supression contact",
form=form,
"entreprises/contacts.html",
title="Liste des contacts",
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)
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
"""
entreprise = Entreprise.query.filter_by(id=id, visible=True).first_or_404(
description=f"entreprise {id} inconnue"
)
form = HistoriqueCreationForm()
form = StageApprentissageCreationForm()
if form.validate_on_submit():
etudiant_nomcomplet = form.etudiant.data.upper().strip()
stm = text(
@ -799,7 +914,7 @@ def add_historique(id):
formation = etudiant.inscription_courante_date(
form.date_debut.data, form.date_fin.data
)
historique = EntrepriseEtudiant(
stage_apprentissage = EntrepriseStageApprentissage(
entreprise_id=entreprise.id,
etudid=etudiant.id,
type_offre=form.type_offre.data.strip(),
@ -809,19 +924,105 @@ def add_historique(id):
formation_scodoc=formation.formsemestre.formsemestre_id
if formation
else None,
notes=form.notes.data.strip(),
)
db.session.add(historique)
db.session.add(stage_apprentissage)
db.session.commit()
flash("L'étudiant a été ajouté sur la fiche entreprise.")
return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id))
return render_template(
"entreprises/ajout_historique.html",
title="Ajout historique",
"entreprises/ajout_stage_apprentissage.html",
title="Ajout stage / apprentissage",
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)
def envoyer_offre(id):
"""
@ -832,7 +1033,9 @@ def envoyer_offre(id):
)
form = EnvoiOffreForm()
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(
"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
return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
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)
def get_import_entreprises_file_sample():
"""
@ -978,20 +1182,19 @@ def import_entreprises():
ligne += 1
if (
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(
siret=entreprise_data[0],
nom=entreprise_data[1],
adresse=entreprise_data[2],
ville=entreprise_data[3],
codepostal=entreprise_data[4],
pays=entreprise_data[5],
siret=entreprise_data[0].replace(" ", ""),
nom=entreprise_data[1].strip(),
adresse=entreprise_data[2].strip(),
ville=entreprise_data[3].strip(),
codepostal=entreprise_data[4].strip(),
pays=entreprise_data[5].strip(),
visible=True,
)
entreprises_import.append(entreprise)
else:
flash(f"Erreur lors de l'importation (ligne {ligne})")
return render_template(
@ -1026,19 +1229,19 @@ def import_entreprises():
)
@bp.route("/export_contacts")
@bp.route("/export_correspondants")
@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 = (
db.session.query(EntrepriseContact)
.join(Entreprise, EntrepriseContact.entreprise_id == Entreprise.id)
correspondants = (
db.session.query(EntrepriseCorrespondant)
.join(Entreprise, EntrepriseCorrespondant.entreprise_id == Entreprise.id)
.filter_by(visible=True)
.all()
)
if contacts:
if correspondants:
keys = [
"nom",
"prenom",
@ -1049,20 +1252,24 @@ def export_contacts():
"entreprise_siret",
]
titles = keys[:]
L = [[contact.to_dict().get(k, "") for k in keys] for contact in contacts]
title = "Contacts"
L = [
[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)
filename = title
return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
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)
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 = [
"nom",
@ -1074,17 +1281,17 @@ def get_import_contacts_file_sample():
"entreprise_siret",
]
titles = keys[:]
title = "ImportContacts"
xlsx = sco_excel.excel_simple_table(titles=titles, sheet_name="Contacts")
title = "ImportCorrespondants"
xlsx = sco_excel.excel_simple_table(titles=titles, sheet_name="Correspondants")
filename = title
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)
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()
if form.validate_on_submit():
@ -1095,8 +1302,8 @@ def import_contacts():
file.save(file_path)
data = sco_excel.excel_file_to_list(file_path)
os.remove(file_path)
contacts_import = []
contact_list = []
correspondants_import = []
correspondant_list = []
ligne = 0
titles = [
"nom",
@ -1110,57 +1317,72 @@ def import_contacts():
if data[1][0] != titles:
flash("Veuillez utilisez la feuille excel à remplir")
return render_template(
"entreprises/import_contacts.html",
title="Importation contacts",
"entreprises/import_correspondants.html",
title="Importation correspondants",
form=form,
)
for contact_data in data[1][1:]:
for correspondant_data in data[1][1:]:
ligne += 1
if (
are.verif_contact_data(contact_data)
and (contact_data[0], contact_data[1], contact_data[6])
not in contact_list
):
contact_list.append((contact_data[0], contact_data[1], contact_data[6]))
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],
are.verif_correspondant_data(correspondant_data)
and (
correspondant_data[0].strip(),
correspondant_data[1].strip(),
correspondant_data[6].strip(),
)
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:
flash(f"Erreur lors de l'importation (ligne {ligne})")
return render_template(
"entreprises/import_contacts.html",
title="Importation contacts",
"entreprises/import_correspondants.html",
title="Importation correspondants",
form=form,
)
if len(contacts_import) > 0:
for contact in contacts_import:
db.session.add(contact)
if len(correspondants_import) > 0:
for correspondant in correspondants_import:
db.session.add(correspondant)
log = EntrepriseLog(
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.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(
"entreprises/import_contacts.html",
title="Importation Contacts",
"entreprises/import_correspondants.html",
title="Importation correspondants",
form=form,
contacts_import=contacts_import,
correspondants_import=correspondants_import,
)
else:
flash('Feuille "Contacts" vide')
flash('Feuille "Correspondants" vide')
return render_template(
"entreprises/import_contacts.html",
title="Importation contacts",
"entreprises/import_correspondants.html",
title="Importation correspondants",
form=form,
)
@ -1198,7 +1420,7 @@ def get_offre_file(entreprise_id, offre_id, filedir, filename):
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)
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)
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.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_utils as scu
from app.scodoc.sco_config_actions import (
LogoDelete,
LogoUpdate,
LogoInsert,
)
from app.scodoc.sco_config_actions import LogoInsert
from app.scodoc.sco_logos import find_logo
@ -120,6 +117,8 @@ def logo_name_validator(message=None):
class AddLogoForm(FlaskForm):
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
from app.scodoc.sco_config_actions import LogoInsert
dept_key = HiddenField()
name = StringField(
label="Nom",
@ -151,7 +150,7 @@ class AddLogoForm(FlaskForm):
dept_id = dept_key_to_id(self.dept_key.data)
if dept_id == GLOBAL:
dept_id = None
if find_logo(logoname=name.data, dept_id=dept_id) is not None:
if find_logo(logoname=name.data, dept_id=dept_id, strict=True) is not None:
raise validators.ValidationError("Un logo de même nom existe déjà")
def select_action(self):
@ -160,6 +159,14 @@ class AddLogoForm(FlaskForm):
return LogoInsert.build_action(self.data)
return None
def opened(self):
if self.do_insert.data:
if self.name.errors:
return "open"
if self.upload.errors:
return "open"
return ""
class LogoForm(FlaskForm):
"""Embed both presentation of a logo (cf. template file configuration.html)
@ -176,7 +183,18 @@ class LogoForm(FlaskForm):
)
],
)
do_delete = SubmitField("Supprimer l'image")
do_delete = SubmitField("Supprimer")
do_rename = SubmitField("Renommer")
new_name = StringField(
label="Nom",
validators=[
logo_name_validator("Nom de logo invalide (alphanumérique, _)"),
validators.Length(
max=20, message="Un nom ne doit pas dépasser 20 caractères"
),
validators.DataRequired("Nom de logo requis (alphanumériques ou '-')"),
],
)
def __init__(self, *args, **kwargs):
kwargs["meta"] = {"csrf": False}
@ -205,12 +223,25 @@ class LogoForm(FlaskForm):
self.titre = "Logo pied de page"
def select_action(self):
from app.scodoc.sco_config_actions import LogoRename
from app.scodoc.sco_config_actions import LogoUpdate
from app.scodoc.sco_config_actions import LogoDelete
if self.do_delete.data and self.can_delete:
return LogoDelete.build_action(self.data)
if self.upload.data and self.validate():
return LogoUpdate.build_action(self.data)
if self.do_rename.data and self.validate():
return LogoRename.build_action(self.data)
return None
def opened(self):
if self.upload.data and self.upload.errors:
return "open"
if self.new_name.data and self.new_name.errors:
return "open"
return ""
class DeptForm(FlaskForm):
dept_key = HiddenField()
@ -244,6 +275,23 @@ class DeptForm(FlaskForm):
return self
return self.index.get(logoname, None)
def opened(self):
if self.add_logo.opened():
return "open"
for logo_form in self.logos:
if logo_form.opened():
return "open"
return ""
def count(self):
compte = len(self.logos.entries)
if compte == 0:
return "vide"
elif compte == 1:
return "1 élément"
else:
return f"{compte} éléments"
def _make_dept_id_name():
"""Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Ajout entreprise avec contact</h1>
<h1>Ajout entreprise</h1>
<br>
<div class="row">
<div class="col-md-4">
@ -16,12 +16,18 @@
</div>
<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);
function autocomplete() {
var input = document.getElementById("siret").value;
if(input.length == 14) {
var input = document.getElementById("siret").value.replaceAll(" ", "")
if(input.length >= 14) {
fetch("https://entreprise.data.gouv.fr/api/sirene/v1/siret/" + input)
.then(response => {
if(response.ok)
@ -48,13 +54,5 @@
document.getElementById("codepostal").value = ''
document.getElementById("ville").value = ''
}
}
{# ajout margin-bottom sur le champ pays #}
var champ_pays = document.getElementById("pays")
if (champ_pays !== null) {
var closest_form_group = champ_pays.closest(".form-group")
closest_form_group.style.marginBottom = "50px"
}
</script>
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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