diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index bc6dca94..da8cce5d 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -1072,6 +1072,29 @@ class BonusTours(BonusDirect): ) +class BonusIUTvannes(BonusSportAdditif): + """Calcul bonus modules optionels (sport, culture), règle IUT Vannes + +
Ne concerne actuellement que les DUT et LP
+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. +
+ Les points au-dessus de 10 sur 20 obtenus dans chacune des matières + optionnelles sont cumulés. +
+ 3% de ces points cumulés s'ajoutent à la moyenne générale du semestre + déjà obtenue par l'étudiant. +
+ """ + + 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. diff --git a/app/comp/inscr_mod.py b/app/comp/inscr_mod.py index 56567f80..a76f2430 100644 --- a/app/comp/inscr_mod.py +++ b/app/comp/inscr_mod.py @@ -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 diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 13829a39..f5efbeb2 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -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: diff --git a/app/comp/moy_sem.py b/app/comp/moy_sem.py index 61b5fd15..6fa3cbac 100644 --- a/app/comp/moy_sem.py +++ b/app/comp/moy_sem.py @@ -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 diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 6d80f0b7..2d337a3d 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -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( diff --git a/app/entreprises/app_relations_entreprises.py b/app/entreprises/app_relations_entreprises.py index 68ff9986..e3c0d1da 100644 --- a/app/entreprises/app_relations_entreprises.py +++ b/app/entreprises/app_relations_entreprises.py @@ -34,7 +34,7 @@ from flask_login import current_user from app.entreprises.models import ( Entreprise, - EntrepriseContact, + EntrepriseCorrespondant, EntrepriseOffre, EntrepriseOffreDepartement, EntreprisePreferences, @@ -85,6 +85,9 @@ def get_offre_files_and_depts(offre: EntrepriseOffre, depts: list): Retourne l'offre, les fichiers attachés a l'offre et les département liés """ offre_depts = EntrepriseOffreDepartement.query.filter_by(offre_id=offre.id).all() + correspondant = EntrepriseCorrespondant.query.filter_by( + id=offre.correspondant_id + ).first() if not offre_depts or check_offre_depts(depts, offre_depts): files = [] path = os.path.join( @@ -100,13 +103,11 @@ def get_offre_files_and_depts(offre: EntrepriseOffre, depts: list): for _file in glob.glob(f"{dir}/*"): file = [os.path.basename(dir), os.path.basename(_file)] files.append(file) - return [offre, files, offre_depts] + return [offre, files, offre_depts, correspondant] return None -def send_email_notifications_entreprise( - subject, entreprise: Entreprise, contact: EntrepriseContact -): +def send_email_notifications_entreprise(subject, entreprise: Entreprise): txt = [ "Une entreprise est en attente de validation", "Entreprise:", @@ -116,14 +117,6 @@ def send_email_notifications_entreprise( f"\tcode postal: {entreprise.codepostal}", f"\tville: {entreprise.ville}", f"\tpays: {entreprise.pays}", - "", - "Contact:", - f"nom: {contact.nom}", - f"prenom: {contact.prenom}", - f"telephone: {contact.telephone}", - f"mail: {contact.mail}", - f"poste: {contact.poste}", - f"service: {contact.service}", ] txt = "\n".join(txt) email.send_email( @@ -135,34 +128,42 @@ def send_email_notifications_entreprise( return txt -def verif_contact_data(contact_data): +def verif_correspondant_data(correspondant_data): """ - Verifie les données d'une ligne Excel (contact) - contact_data[0]: nom - contact_data[1]: prenom - contact_data[2]: telephone - contact_data[3]: mail - contact_data[4]: poste - contact_data[5]: service - contact_data[6]: entreprise_id + Verifie les données d'une ligne Excel (correspondant) + correspondant_data[0]: nom + correspondant_data[1]: prenom + correspondant_data[2]: telephone + correspondant_data[3]: mail + correspondant_data[4]: poste + correspondant_data[5]: service + correspondant_data[6]: entreprise_id """ # champs obligatoires - if contact_data[0] == "" or contact_data[1] == "" or contact_data[6] == "": + if ( + correspondant_data[0].strip() == "" + or correspondant_data[1].strip() == "" + or correspondant_data[6].strip() == "" + ): return False # entreprise_id existant - entreprise = Entreprise.query.filter_by(siret=contact_data[6]).first() + entreprise = Entreprise.query.filter_by(siret=correspondant_data[6].strip()).first() if entreprise is None: return False - # contact possède le meme nom et prénom dans la meme entreprise - contact = EntrepriseContact.query.filter_by( - nom=contact_data[0], prenom=contact_data[1], entreprise_id=entreprise.id + # correspondant possède le meme nom et prénom dans la meme entreprise + correspondant = EntrepriseCorrespondant.query.filter_by( + nom=correspondant_data[0].strip(), + prenom=correspondant_data[1].strip(), + entreprise_id=entreprise.id, ).first() - if contact is not None: + if correspondant is not None: return False - if contact_data[2] == "" and contact_data[3] == "": # 1 moyen de contact + if ( + correspondant_data[2].strip() == "" and correspondant_data[3].strip() == "" + ): # 1 moyen de contact return False return True @@ -174,23 +175,23 @@ def verif_entreprise_data(entreprise_data): """ if EntreprisePreferences.get_check_siret(): for data in entreprise_data: # champs obligatoires - if data == "": + if data.strip() == "": return False else: for data in entreprise_data[1:]: # champs obligatoires - if data == "": + if data.strip() == "": return False if EntreprisePreferences.get_check_siret(): - siret = entreprise_data[0].strip() # vérification sur le siret + siret = entreprise_data[0].replace(" ", "") # vérification sur le siret if re.match("^\d{14}$", siret) is None: return False try: req = requests.get( f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret}" ) + if req.status_code != 200: + return False except requests.ConnectionError: - print("no internet") - if req.status_code != 200: return False entreprise = Entreprise.query.filter_by(siret=siret).first() if entreprise is not None: diff --git a/app/entreprises/forms.py b/app/entreprises/forms.py index 0540c080..654b2618 100644 --- a/app/entreprises/forms.py +++ b/app/entreprises/forms.py @@ -40,11 +40,17 @@ from wtforms import ( SelectMultipleField, DateField, BooleanField, + FieldList, + FormField, ) from wtforms.validators import ValidationError, DataRequired, Email, Optional from wtforms.widgets import ListWidget, CheckboxInput -from app.entreprises.models import Entreprise, EntrepriseContact, EntreprisePreferences +from app.entreprises.models import ( + Entreprise, + EntrepriseCorrespondant, + EntreprisePreferences, +) from app.models import Identite, Departement from app.auth.models import User @@ -66,7 +72,7 @@ def _build_string_field(label, required=True, render_kw=None): class EntrepriseCreationForm(FlaskForm): siret = _build_string_field( "SIRET (*)", - render_kw={"placeholder": "Numéro composé de 14 chiffres", "maxlength": "14"}, + render_kw={"placeholder": "Numéro composé de 14 chiffres"}, ) nom_entreprise = _build_string_field("Nom de l'entreprise (*)") adresse = _build_string_field("Adresse de l'entreprise (*)") @@ -74,15 +80,18 @@ class EntrepriseCreationForm(FlaskForm): ville = _build_string_field("Ville de l'entreprise (*)") pays = _build_string_field("Pays de l'entreprise", required=False) - nom_contact = _build_string_field("Nom du contact (*)") - prenom_contact = _build_string_field("Prénom du contact (*)") - telephone = _build_string_field("Téléphone du contact (*)", required=False) + nom_correspondant = _build_string_field("Nom du correspondant", required=False) + prenom_correspondant = _build_string_field( + "Prénom du correspondant", required=False + ) + telephone = _build_string_field("Téléphone du correspondant", required=False) mail = StringField( - "Mail du contact (*)", + "Mail du correspondant", validators=[Optional(), Email(message="Adresse e-mail invalide")], ) - poste = _build_string_field("Poste du contact", required=False) - service = _build_string_field("Service du contact", required=False) + poste = _build_string_field("Poste du correspondant", required=False) + service = _build_string_field("Service du correspondant", required=False) + submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) def validate(self): @@ -90,29 +99,46 @@ class EntrepriseCreationForm(FlaskForm): if not FlaskForm.validate(self): validate = False - if not self.telephone.data and not self.mail.data: - self.telephone.errors.append( - "Saisir un moyen de contact (mail ou téléphone)" - ) - self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)") - validate = False + if ( + self.nom_correspondant.data.strip() + or self.prenom_correspondant.data.strip() + or self.telephone.data.strip() + or self.mail.data.strip() + or self.poste.data.strip() + or self.service.data.strip() + ): + if not self.nom_correspondant.data.strip(): + self.nom_correspondant.errors.append("Ce champ est requis") + validate = False + if not self.prenom_correspondant.data.strip(): + self.prenom_correspondant.errors.append("Ce champ est requis") + validate = False + if not self.telephone.data.strip() and not self.mail.data.strip(): + self.telephone.errors.append( + "Saisir un moyen de contact (mail ou téléphone)" + ) + self.mail.errors.append( + "Saisir un moyen de contact (mail ou téléphone)" + ) + validate = False return validate def validate_siret(self, siret): if EntreprisePreferences.get_check_siret(): - siret = siret.data.strip() - if re.match("^\d{14}$", siret) is None: + siret_data = siret.data.replace(" ", "") + self.siret.data = siret_data + if re.match("^\d{14}$", siret_data) is None: raise ValidationError("Format incorrect") try: req = requests.get( - f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret}" + f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret_data}" ) + if req.status_code != 200: + raise ValidationError("SIRET inexistant") except requests.ConnectionError: - print("no internet") - if req.status_code != 200: - raise ValidationError("SIRET inexistant") - entreprise = Entreprise.query.filter_by(siret=siret).first() + raise ValidationError("Impossible de vérifier l'existance du SIRET") + entreprise = Entreprise.query.filter_by(siret=siret_data).first() if entreprise is not None: lien = f'ici' raise ValidationError( @@ -144,6 +170,7 @@ class MultiCheckboxField(SelectMultipleField): class OffreCreationForm(FlaskForm): + hidden_entreprise_id = HiddenField() intitule = _build_string_field("Intitulé (*)") description = TextAreaField( "Description (*)", validators=[DataRequired(message=CHAMP_REQUIS)] @@ -159,17 +186,44 @@ class OffreCreationForm(FlaskForm): duree = _build_string_field("Durée (*)") depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int) expiration_date = DateField("Date expiration", validators=[Optional()]) + correspondant = SelectField("Correspondant à contacté", validators=[Optional()]) + fichier = FileField( + "Fichier (*)", + validators=[ + Optional(), + FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"), + ], + ) submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.correspondant.choices = [ + (correspondant.id, f"{correspondant.nom} {correspondant.prenom}") + for correspondant in EntrepriseCorrespondant.query.filter_by( + entreprise_id=self.hidden_entreprise_id.data + ) + ] + self.depts.choices = [ (dept.id, dept.acronym) for dept in Departement.query.all() ] + def validate(self): + validate = True + if not FlaskForm.validate(self): + validate = False + + if len(self.depts.data) < 1: + self.depts.errors.append("Choisir au moins un département") + validate = False + + return validate + class OffreModificationForm(FlaskForm): + hidden_entreprise_id = HiddenField() intitule = _build_string_field("Intitulé (*)") description = TextAreaField( "Description (*)", validators=[DataRequired(message=CHAMP_REQUIS)] @@ -185,27 +239,79 @@ class OffreModificationForm(FlaskForm): duree = _build_string_field("Durée (*)") depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int) expiration_date = DateField("Date expiration", validators=[Optional()]) + correspondant = SelectField("Correspondant à contacté", validators=[Optional()]) submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.correspondant.choices = [ + (correspondant.id, f"{correspondant.nom} {correspondant.prenom}") + for correspondant in EntrepriseCorrespondant.query.filter_by( + entreprise_id=self.hidden_entreprise_id.data + ) + ] + self.depts.choices = [ (dept.id, dept.acronym) for dept in Departement.query.all() ] + def validate(self): + validate = True + if not FlaskForm.validate(self): + validate = False -class ContactCreationForm(FlaskForm): - hidden_entreprise_id = HiddenField() - nom = _build_string_field("Nom (*)") - prenom = _build_string_field("Prénom (*)") - telephone = _build_string_field("Téléphone (*)", required=False) + if len(self.depts.data) < 1: + self.depts.errors.append("Choisir au moins un département") + validate = False + + return validate + + +class CorrespondantCreationForm(FlaskForm): + nom = _build_string_field("Nom (*)", render_kw={"class": "form-control"}) + prenom = _build_string_field("Prénom (*)", render_kw={"class": "form-control"}) + telephone = _build_string_field( + "Téléphone (*)", required=False, render_kw={"class": "form-control"} + ) mail = StringField( "Mail (*)", validators=[Optional(), Email(message="Adresse e-mail invalide")], + render_kw={"class": "form-control"}, ) - poste = _build_string_field("Poste", required=False) - service = _build_string_field("Service", required=False) + poste = _build_string_field( + "Poste", required=False, render_kw={"class": "form-control"} + ) + service = _build_string_field( + "Service", required=False, render_kw={"class": "form-control"} + ) + # depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int) + + # def __init__(self, *args, **kwargs): + # super().__init__(*args, **kwargs) + + # self.depts.choices = [ + # (dept.id, dept.acronym) for dept in Departement.query.all() + # ] + + def validate(self): + validate = True + if not FlaskForm.validate(self): + validate = False + + if not self.telephone.data and not self.mail.data: + self.telephone.errors.append( + "Saisir un moyen de contact (mail ou téléphone)" + ) + self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)") + validate = False + + return validate + + +class CorrespondantsCreationForm(FlaskForm): + hidden_entreprise_id = HiddenField() + correspondants = FieldList(FormField(CorrespondantCreationForm), min_entries=1) submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) def validate(self): @@ -213,28 +319,37 @@ class ContactCreationForm(FlaskForm): if not FlaskForm.validate(self): validate = False - contact = EntrepriseContact.query.filter_by( - entreprise_id=self.hidden_entreprise_id.data, - nom=self.nom.data, - prenom=self.prenom.data, - ).first() - if contact is not None: - self.nom.errors.append("Ce contact existe déjà (même nom et prénom)") - self.prenom.errors.append("") - validate = False - - if not self.telephone.data and not self.mail.data: - self.telephone.errors.append( - "Saisir un moyen de contact (mail ou téléphone)" - ) - self.mail.errors.append("Saisir un moyen de contact (mail ou téléphone)") - validate = False - + correspondant_list = [] + for entry in self.correspondants.entries: + if entry.nom.data.strip() and entry.prenom.data.strip(): + if ( + entry.nom.data.strip(), + entry.prenom.data.strip(), + ) in correspondant_list: + entry.nom.errors.append( + "Vous avez saisi 2 fois le même nom et prenom" + ) + entry.prenom.errors.append("") + validate = False + correspondant_list.append( + (entry.nom.data.strip(), entry.prenom.data.strip()) + ) + correspondant = EntrepriseCorrespondant.query.filter_by( + entreprise_id=self.hidden_entreprise_id.data, + nom=entry.nom.data, + prenom=entry.prenom.data, + ).first() + if correspondant is not None: + entry.nom.errors.append( + "Ce correspondant existe déjà (même nom et prénom)" + ) + entry.prenom.errors.append("") + validate = False return validate -class ContactModificationForm(FlaskForm): - hidden_contact_id = HiddenField() +class CorrespondantModificationForm(FlaskForm): + hidden_correspondant_id = HiddenField() hidden_entreprise_id = HiddenField() nom = _build_string_field("Nom (*)") prenom = _build_string_field("Prénom (*)") @@ -245,21 +360,29 @@ class ContactModificationForm(FlaskForm): ) poste = _build_string_field("Poste", required=False) service = _build_string_field("Service", required=False) + # depts = MultiCheckboxField("Départements", validators=[Optional()], coerce=int) submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) + # def __init__(self, *args, **kwargs): + # super().__init__(*args, **kwargs) + + # self.depts.choices = [ + # (dept.id, dept.acronym) for dept in Departement.query.all() + # ] + def validate(self): validate = True if not FlaskForm.validate(self): validate = False - contact = EntrepriseContact.query.filter( - EntrepriseContact.id != self.hidden_contact_id.data, - EntrepriseContact.entreprise_id == self.hidden_entreprise_id.data, - EntrepriseContact.nom == self.nom.data, - EntrepriseContact.prenom == self.prenom.data, + correspondant = EntrepriseCorrespondant.query.filter( + EntrepriseCorrespondant.id != self.hidden_correspondant_id.data, + EntrepriseCorrespondant.entreprise_id == self.hidden_entreprise_id.data, + EntrepriseCorrespondant.nom == self.nom.data, + EntrepriseCorrespondant.prenom == self.prenom.data, ).first() - if contact is not None: - self.nom.errors.append("Ce contact existe déjà (même nom et prénom)") + if correspondant is not None: + self.nom.errors.append("Ce correspondant existe déjà (même nom et prénom)") self.prenom.errors.append("") validate = False @@ -273,7 +396,59 @@ class ContactModificationForm(FlaskForm): return validate -class HistoriqueCreationForm(FlaskForm): +class ContactCreationForm(FlaskForm): + date = _build_string_field( + "Date (*)", + render_kw={"type": "datetime-local"}, + ) + utilisateur = _build_string_field( + "Utilisateur (*)", + render_kw={"placeholder": "Tapez le nom de l'utilisateur"}, + ) + notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)]) + submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) + + def validate_utilisateur(self, utilisateur): + utilisateur_data = self.utilisateur.data.upper().strip() + stm = text( + "SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:utilisateur_data" + ) + utilisateur = ( + User.query.from_statement(stm) + .params(utilisateur_data=utilisateur_data) + .first() + ) + if utilisateur is None: + raise ValidationError("Champ incorrect (selectionnez dans la liste)") + + +class ContactModificationForm(FlaskForm): + date = _build_string_field( + "Date (*)", + render_kw={"type": "datetime-local"}, + ) + utilisateur = _build_string_field( + "Utilisateur (*)", + render_kw={"placeholder": "Tapez le nom de l'utilisateur"}, + ) + notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)]) + submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) + + def validate_utilisateur(self, utilisateur): + utilisateur_data = self.utilisateur.data.upper().strip() + stm = text( + "SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:utilisateur_data" + ) + utilisateur = ( + User.query.from_statement(stm) + .params(utilisateur_data=utilisateur_data) + .first() + ) + if utilisateur is None: + raise ValidationError("Champ incorrect (selectionnez dans la liste)") + + +class StageApprentissageCreationForm(FlaskForm): etudiant = _build_string_field( "Étudiant (*)", render_kw={"placeholder": "Tapez le nom de l'étudiant"}, @@ -289,6 +464,7 @@ class HistoriqueCreationForm(FlaskForm): date_fin = DateField( "Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)] ) + notes = TextAreaField("Notes") submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) def validate(self): @@ -319,25 +495,87 @@ class HistoriqueCreationForm(FlaskForm): raise ValidationError("Champ incorrect (selectionnez dans la liste)") +class StageApprentissageModificationForm(FlaskForm): + etudiant = _build_string_field( + "Étudiant (*)", + render_kw={"placeholder": "Tapez le nom de l'étudiant"}, + ) + type_offre = SelectField( + "Type de l'offre (*)", + choices=[("Stage"), ("Alternance")], + validators=[DataRequired(message=CHAMP_REQUIS)], + ) + date_debut = DateField( + "Date début (*)", validators=[DataRequired(message=CHAMP_REQUIS)] + ) + date_fin = DateField( + "Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)] + ) + notes = TextAreaField("Notes") + submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE) + + def validate(self): + validate = True + if not FlaskForm.validate(self): + validate = False + + if ( + self.date_debut.data + and self.date_fin.data + and self.date_debut.data > self.date_fin.data + ): + self.date_debut.errors.append("Les dates sont incompatibles") + self.date_fin.errors.append("Les dates sont incompatibles") + validate = False + + return validate + + def validate_etudiant(self, etudiant): + etudiant_data = etudiant.data.upper().strip() + stm = text( + "SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom" + ) + etudiant = ( + Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first() + ) + if etudiant is None: + raise ValidationError("Champ incorrect (selectionnez dans la liste)") + + class EnvoiOffreForm(FlaskForm): - responsable = _build_string_field( - "Responsable de formation (*)", - render_kw={"placeholder": "Tapez le nom du responsable de formation"}, + responsables = FieldList( + _build_string_field( + "Responsable (*)", + render_kw={ + "placeholder": "Tapez le nom du responsable de formation", + "class": "form-control", + }, + ), + min_entries=1, ) submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) - def validate_responsable(self, responsable): - responsable_data = responsable.data.upper().strip() - stm = text( - "SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:responsable_data" - ) - responsable = ( - User.query.from_statement(stm) - .params(responsable_data=responsable_data) - .first() - ) - if responsable is None: - raise ValidationError("Champ incorrect (selectionnez dans la liste)") + def validate(self): + validate = True + if not FlaskForm.validate(self): + validate = False + + for entry in self.responsables.entries: + if entry.data: + responsable_data = entry.data.upper().strip() + stm = text( + "SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:responsable_data" + ) + responsable = ( + User.query.from_statement(stm) + .params(responsable_data=responsable_data) + .first() + ) + if responsable is None: + entry.errors.append("Champ incorrect (selectionnez dans la liste)") + validate = False + + return validate class AjoutFichierForm(FlaskForm): diff --git a/app/entreprises/models.py b/app/entreprises/models.py index dd0b4ba4..90fcf335 100644 --- a/app/entreprises/models.py +++ b/app/entreprises/models.py @@ -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) diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index d1633e71..7fe2e727 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -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/%s | " % sem["formsemestre_id"] + sem["tmpcode"] = f"{sem['formsemestre_id']} | " else: sem["tmpcode"] = "" # Nombre d'inscrits: @@ -121,26 +122,27 @@ def index_html(showcodes=0, showsemtable=0): if showsemtable: H.append( - """
Voir tous les semestres ({len(othersems)} verrouillés) +
""" ) H.append( - """ - """ - % scu.NotesURL() + f"""+
+ """ ) # if current_user.has_permission(Permission.ScoEtudInscrit): @@ -148,23 +150,26 @@ Chercher étape courante: