From 30eb738c25033c2aa48b95735acc13dd45993678 Mon Sep 17 00:00:00 2001 From: Arthur ZHU Date: Thu, 23 Dec 2021 19:28:25 +0100 Subject: [PATCH 01/27] applications relations entreprises --- app/__init__.py | 4 + app/entreprises/__init__.py | 25 ++ app/entreprises/forms.py | 120 ++++++ app/entreprises/models.py | 52 +++ app/entreprises/routes.py | 374 ++++++++++++++++++ app/models/__init__.py | 6 - app/models/departements.py | 2 +- app/models/entreprises.py | 69 ---- app/models/etudiants.py | 8 + app/models/formsemestre.py | 6 + app/templates/entreprises/_contact.html | 13 + app/templates/entreprises/_offre.html | 15 + .../entreprises/delete_confirmation.html | 14 + app/templates/entreprises/entreprises.html | 65 +++ .../entreprises/fiche_entreprise.html | 70 ++++ app/templates/entreprises/form.html | 71 ++++ ...a_creation_tables_relations_entreprises.py | 127 ++++++ 17 files changed, 965 insertions(+), 76 deletions(-) create mode 100644 app/entreprises/__init__.py create mode 100644 app/entreprises/forms.py create mode 100644 app/entreprises/models.py create mode 100644 app/entreprises/routes.py delete mode 100644 app/models/entreprises.py create mode 100644 app/templates/entreprises/_contact.html create mode 100644 app/templates/entreprises/_offre.html create mode 100644 app/templates/entreprises/delete_confirmation.html create mode 100644 app/templates/entreprises/entreprises.html create mode 100644 app/templates/entreprises/fiche_entreprise.html create mode 100644 app/templates/entreprises/form.html create mode 100644 migrations/versions/f62d3a0bde1a_creation_tables_relations_entreprises.py diff --git a/app/__init__.py b/app/__init__.py index 0943a91f6..0ea6bb52c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -199,6 +199,10 @@ def create_app(config_class=DevConfig): app.register_blueprint(auth_bp, url_prefix="/auth") + from app.entreprises import bp as entreprises_bp + + app.register_blueprint(entreprises_bp, url_prefix="/ScoDoc/entreprises") + from app.views import scodoc_bp from app.views import scolar_bp from app.views import notes_bp diff --git a/app/entreprises/__init__.py b/app/entreprises/__init__.py new file mode 100644 index 000000000..d9358508e --- /dev/null +++ b/app/entreprises/__init__.py @@ -0,0 +1,25 @@ +"""entreprises.__init__ +""" + +from flask import Blueprint +from app.scodoc import sco_etud +from app.auth.models import User + +bp = Blueprint("entreprises", __name__) + +LOGS_LEN = 10 + +@bp.app_template_filter() +def format_prenom(s): + return sco_etud.format_prenom(s) + +@bp.app_template_filter() +def format_nom(s): + return sco_etud.format_nom(s) + +@bp.app_template_filter() +def get_nomcomplet(s): + user = User.query.filter_by(user_name=s).first() + return user.get_nomcomplet(); + +from app.entreprises import routes \ No newline at end of file diff --git a/app/entreprises/forms.py b/app/entreprises/forms.py new file mode 100644 index 000000000..f7fd62816 --- /dev/null +++ b/app/entreprises/forms.py @@ -0,0 +1,120 @@ +from flask import flash +from markupsafe import Markup +from flask.app import Flask +import requests, re +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, TextAreaField, SelectField, FileField +from wtforms.fields.html5 import EmailField, DateField +from wtforms.validators import ValidationError, DataRequired, Email +from app.entreprises.models import Entreprise +from app.models import Identite +from app.auth.models import User +from app.scodoc import sco_etud +from sqlalchemy import text + +DATA_REQUIRED_ERROR_MESSAGE = "Ce champ est requis" + +class EntrepriseCreationForm(FlaskForm): + siret = StringField("SIRET", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)], render_kw={"placeholder": "Numéro composé de 14 chiffres"}) + nom_entreprise = StringField("Nom de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + adresse = StringField("Adresse de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + codepostal = StringField("Code postal de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + ville = StringField("Ville de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + pays = StringField("Pays de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + + nom_contact = StringField("Nom du contact", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + prenom_contact = StringField("Prénom du contact", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + telephone = StringField("Téléphone du contact", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + mail = EmailField("Mail du contact", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE), Email(message="Adresse e-mail invalide")]) + submit = SubmitField("Envoyer") + + def validate_siret(self, siret): + siret = siret.data.strip() + if re.match("^\d{14}$", siret) == None: + raise ValidationError("Format incorrect") + req = requests.get(f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret}") + if req.status_code != 200: + raise ValidationError("SIRET inexistant") + entreprise = Entreprise.query.filter_by(siret=siret).first() + if entreprise is not None: + lien = f"ici" + raise ValidationError(Markup(f"Entreprise déjà présent, lien vers la fiche : {lien}")) + +class EntrepriseModificationForm(FlaskForm): + siret = StringField("SIRET", validators=[], render_kw={"disabled":""}) + nom = StringField("Nom de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + adresse = StringField("Adresse", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + codepostal = StringField("Code postal", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + ville = StringField("Ville", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + pays = StringField("Pays", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + submit = SubmitField("Modifier") + +class OffreCreationForm(FlaskForm): + intitule = StringField("Intitulé", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + description = TextAreaField("Description", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + type_offre = SelectField("Type de l'offre", choices=[('Stage'), ('Alternance')], validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + missions = TextAreaField("Missions", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + duree = StringField("Durée", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + ficher = FileField("Fichier", validators=[]) + submit = SubmitField("Envoyer") + +class OffreModificationForm(FlaskForm): + intitule = StringField("Intitulé", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + description = TextAreaField("Description", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + type_offre = SelectField("Type de l'offre", choices=[('Stage'), ('Alternance')], validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + missions = TextAreaField("Missions", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + duree = StringField("Durée", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + submit = SubmitField("Modifier") + +class ContactCreationForm(FlaskForm): + nom = StringField("Nom", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + prenom = StringField("Prénom", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + telephone = StringField("Téléphone", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + mail = EmailField("Mail", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE), Email(message="Adresse e-mail invalide")]) + submit = SubmitField("Envoyer") + +class ContactModificationForm(FlaskForm): + nom = StringField("Nom", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + prenom = StringField("Prénom", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + telephone = StringField("Téléphone", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + mail = EmailField("Mail", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE), Email(message="Adresse e-mail invalide")]) + submit = SubmitField("Modifier") + +class HistoriqueCreationForm(FlaskForm): + etudiant = StringField("Étudiant", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)], render_kw={"placeholder": "Tapez le nom de l'étudiant puis selectionnez"}) + type_offre = SelectField("Type de l'offre", choices=[('Stage'), ('Alternance')], validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + date_debut = DateField("Date début", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + date_fin = DateField("Date fin", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + submit = SubmitField("Envoyer") + + def validate(self): + rv = FlaskForm.validate(self) + if not rv: + return False + + if 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") + return False + return True + + 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 = StringField("Responsable de formation", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + submit = SubmitField("Envoyer") + + 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)") + +class SuppressionConfirmationForm(FlaskForm): + submit = SubmitField("Supprimer") \ No newline at end of file diff --git a/app/entreprises/models.py b/app/entreprises/models.py new file mode 100644 index 000000000..a079cacf8 --- /dev/null +++ b/app/entreprises/models.py @@ -0,0 +1,52 @@ +from app import db + +class Entreprise(db.Model): + __tablename__ = "entreprises" + id = db.Column(db.Integer, primary_key=True) + siret = db.Column(db.Text) + nom = db.Column(db.Text) + adresse = db.Column(db.Text) + codepostal = db.Column(db.Text) + ville = db.Column(db.Text) + pays = db.Column(db.Text) + contacts = db.relationship('EntrepriseContact', backref='entreprise', lazy='dynamic') + offres = db.relationship('EntrepriseOffre', backref='entreprise', lazy='dynamic') + +class EntrepriseContact(db.Model): + __tablename__ = "entreprise_contact" + id = db.Column(db.Integer, primary_key=True) + entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id")) + nom = db.Column(db.Text) + prenom = db.Column(db.Text) + telephone = db.Column(db.Text) + mail = db.Column(db.Text) + +class EntrepriseOffre(db.Model): + __tablename__ = "entreprise_offre" + id = db.Column(db.Integer, primary_key=True) + entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id")) + date_ajout = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + intitule = db.Column(db.Text) + description = db.Column(db.Text) + type_offre = db.Column(db.Text) + missions = db.Column(db.Text) + duree = db.Column(db.Text) + +class EntrepriseLog(db.Model): + __tablename__ = "entreprise_log" + id = db.Column(db.Integer, primary_key=True) + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + authenticated_user = db.Column(db.Text) + object = db.Column(db.Integer) + text = db.Column(db.Text) + +class EntrepriseHistory(db.Model): + __tablename__ = "entreprise_history" + id = db.Column(db.Integer, primary_key=True) + entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id")) + etudid = db.Column(db.Integer) + type_offre = db.Column(db.Text) + date_debut = db.Column(db.Date) + date_fin = db.Column(db.Date) + formation_text = db.Column(db.Text) + formation_scodoc = db.Column(db.Integer) diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py new file mode 100644 index 000000000..b74390873 --- /dev/null +++ b/app/entreprises/routes.py @@ -0,0 +1,374 @@ +from flask import render_template, redirect, url_for, request, flash +from flask.json import jsonify +from flask_login import current_user +from app.decorators import permission_required +from app.entreprises import LOGS_LEN +from app.scodoc.sco_permissions import Permission +from app.entreprises.forms import ( + EntrepriseCreationForm, + EntrepriseModificationForm, + SuppressionConfirmationForm, + OffreCreationForm, + OffreModificationForm, + ContactCreationForm, + ContactModificationForm, + HistoriqueCreationForm, + EnvoiOffreForm +) +from app.entreprises import bp +from app.entreprises.models import ( + Entreprise, + EntrepriseOffre, + EntrepriseContact, + EntrepriseLog, + EntrepriseHistory +) +from app.models import ( + Identite +) +from app.auth.models import User +from app.scodoc.sco_find_etud import search_etud_by_name +from app import db +from app.scodoc import sco_etud +from sqlalchemy import text + +@bp.route("/", methods=["GET", "POST"]) +def index(): + entreprises = Entreprise.query.all() + logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all() + return render_template("entreprises/entreprises.html", title=("Entreprises"), entreprises=entreprises, logs=logs) + +@bp.route("/add_entreprise", methods=["GET", "POST"]) +def add_entreprise(): + form = EntrepriseCreationForm() + if form.validate_on_submit(): + entreprise = Entreprise( + nom=form.nom_entreprise.data.strip(), + siret=form.siret.data.strip(), + adresse=form.adresse.data.strip(), + codepostal=form.codepostal.data.strip(), + ville=form.ville.data.strip(), + pays=form.pays.data.strip() + ) + db.session.add(entreprise) + db.session.commit() + db.session.refresh(entreprise) + contact = EntrepriseContact( + entreprise_id=entreprise.id, + nom=form.nom_contact.data.strip(), + prenom=form.prenom_contact.data.strip(), + telephone=form.telephone.data.strip(), + mail=form.mail.data.strip() + ) + db.session.add(contact) + nom_entreprise = f"{entreprise.nom}" + log = EntrepriseLog( + authenticated_user = current_user.user_name, + text = f"{nom_entreprise} - Création de la fiche entreprise ({entreprise.nom}) avec un contact", + ) + db.session.add(log) + db.session.commit() + flash("L'entreprise a été ajouté à la liste.") + return redirect(url_for("entreprises.index")) + return render_template("entreprises/form.html", title=("Ajout entreprise + contact"), form=form) + +@bp.route("/edit_entreprise/", methods=["GET", "POST"]) +def edit_entreprise(id): + entreprise = Entreprise.query.filter_by(id=id).first_or_404() + form = EntrepriseModificationForm() + if form.validate_on_submit(): + nom_entreprise = f"{form.nom.data.strip()}" + if entreprise.nom != form.nom.data.strip(): + log = EntrepriseLog( + authenticated_user = current_user.user_name, + object = entreprise.id, + text = f"{nom_entreprise} - Modification du nom (ancien nom : {entreprise.nom})", + ) + entreprise.nom = form.nom.data.strip() + db.session.add(log) + if entreprise.adresse != form.adresse.data.strip(): + log = EntrepriseLog( + authenticated_user = current_user.user_name, + object = entreprise.id, + text = f"{nom_entreprise} - Modification de l'adresse (ancienne adresse : {entreprise.adresse})", + ) + entreprise.adresse = form.adresse.data.strip() + db.session.add(log) + if entreprise.codepostal != form.codepostal.data.strip(): + log = EntrepriseLog( + authenticated_user = current_user.user_name, + object = entreprise.id, + text = f"{nom_entreprise} - Modification du code postal (ancien code postal : {entreprise.codepostal})", + ) + entreprise.codepostal = form.codepostal.data.strip() + db.session.add(log) + if entreprise.ville != form.ville.data.strip(): + log = EntrepriseLog( + authenticated_user = current_user.user_name, + object = entreprise.id, + text = f"{nom_entreprise} - Modification de la ville (ancienne ville : {entreprise.ville})", + ) + entreprise.ville = form.ville.data.strip() + db.session.add(log) + if entreprise.pays != form.pays.data.strip(): + log = EntrepriseLog( + authenticated_user = current_user.user_name, + object = entreprise.id, + text = f"{nom_entreprise} - Modification du pays (ancien pays : {entreprise.pays})", + ) + entreprise.pays = form.pays.data.strip() + db.session.add(log) + db.session.commit() + flash("L'entreprise a été modifié.") + return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) + elif request.method == 'GET': + form.siret.data = entreprise.siret + form.nom.data = entreprise.nom + form.adresse.data = entreprise.adresse + form.codepostal.data = entreprise.codepostal + form.ville.data = entreprise.ville + form.pays.data = entreprise.pays + return render_template("entreprises/form.html", title=("Modification entreprise"), form=form) + +@bp.route("/delete_entreprise/", methods=["GET", "POST"]) +def delete_entreprise(id): + entreprise = Entreprise.query.filter_by(id=id).first_or_404() + form = SuppressionConfirmationForm() + if form.validate_on_submit(): + db.session.delete(entreprise) + log = EntrepriseLog( + authenticated_user = current_user.user_name, + object = entreprise.id, + text = f"Suppression de la fiche entreprise ({entreprise.nom})", + ) + db.session.add(log) + db.session.commit() + flash("L'entreprise a été supprimé de la liste.") + return redirect(url_for("entreprises.index")) + return render_template("entreprises/delete_confirmation.html", title=("Supression entreprise"), form=form) + +@bp.route("/fiche_entreprise/", methods=["GET", "POST"]) +def fiche_entreprise(id): + entreprise = Entreprise.query.filter_by(id=id).first_or_404() + offres = entreprise.offres + contacts = entreprise.contacts + logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).filter_by(object=id).limit(LOGS_LEN).all() + historique = db.session.query(EntrepriseHistory, Identite).order_by(EntrepriseHistory.date_debut.desc()).\ + filter_by(entreprise_id=id).\ + join(Identite, Identite.id == EntrepriseHistory.etudid).all() + return render_template("entreprises/fiche_entreprise.html", title=("Fiche entreprise"), entreprise=entreprise, contacts=contacts, offres=offres, logs=logs, historique=historique) + +@bp.route("/add_offre/", methods=["GET", "POST"]) +def add_offre(id): + entreprise = Entreprise.query.filter_by(id=id).first_or_404() + form = OffreCreationForm() + if form.validate_on_submit(): + offre = EntrepriseOffre( + entreprise_id=entreprise.id, + intitule=form.intitule.data.strip(), + description=form.description.data.strip(), + type_offre=form.type_offre.data.strip(), + missions=form.missions.data.strip(), + duree=form.duree.data.strip() + ) + log = EntrepriseLog( + authenticated_user = current_user.user_name, + object = entreprise.id, + text = "Création d'une offre", + ) + db.session.add(log) + db.session.add(offre) + db.session.commit() + flash("L'offre a été ajouté à la fiche entreprise.") + return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) + return render_template("entreprises/form.html", title=("Ajout offre"), form=form) + +@bp.route("/edit_offre/", methods=["GET", "POST"]) +def edit_offre(id): + offre = EntrepriseOffre.query.filter_by(id=id).first_or_404() + form = OffreModificationForm() + if form.validate_on_submit(): + offre.intitule = form.intitule.data.strip() + offre.description = form.description.data.strip() + offre.type_offre = form.type_offre.data.strip() + offre.missions = form.missions.data.strip() + offre.duree = form.duree.data.strip() + log = EntrepriseLog( + authenticated_user = current_user.user_name, + object = offre.entreprise_id, + text = "Modification d'une offre", + ) + db.session.add(log) + db.session.commit() + flash("L'offre a été modifié.") + return redirect(url_for("entreprises.fiche_entreprise", id=offre.entreprise.id)) + elif request.method == 'GET': + form.intitule.data = offre.intitule + form.description.data = offre.description + form.type_offre.data = offre.type_offre + form.missions.data = offre.missions + form.duree.data = offre.duree + return render_template("entreprises/form.html", title=("Modification offre"), form=form) + +@bp.route("/delete_offre/", methods=["GET", "POST"]) +def delete_offre(id): + offre = EntrepriseOffre.query.filter_by(id=id).first_or_404() + entreprise_id = offre.entreprise.id + form = SuppressionConfirmationForm() + if form.validate_on_submit(): + db.session.delete(offre) + log = EntrepriseLog( + authenticated_user = current_user.user_name, + object = offre.entreprise_id, + text = "Suppression d'une offre", + ) + db.session.add(log) + db.session.commit() + flash("L'offre a été supprimé de la fiche entreprise.") + return redirect(url_for("entreprises.fiche_entreprise", id=entreprise_id)) + return render_template("entreprises/delete_confirmation.html", title=("Supression offre"), form=form) + +@bp.route("/add_contact/", methods=["GET", "POST"]) +def add_contact(id): + entreprise = Entreprise.query.filter_by(id=id).first_or_404() + form = ContactCreationForm() + if form.validate_on_submit(): + contact = EntrepriseContact( + entreprise_id=entreprise.id, + nom=form.nom.data.strip(), + prenom=form.prenom.data.strip(), + telephone=form.telephone.data.strip(), + mail=form.mail.data.strip() + ) + log = EntrepriseLog( + authenticated_user = current_user.user_name, + object = entreprise.id, + text = "Création d'un contact", + ) + db.session.add(log) + 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 render_template("entreprises/form.html", title=("Ajout contact"), form=form) + +@bp.route("/edit_contact/", methods=["GET", "POST"]) +def edit_contact(id): + contact = EntrepriseContact.query.filter_by(id=id).first_or_404() + 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() + log = EntrepriseLog( + authenticated_user = current_user.user_name, + object = contact.entreprise_id, + text = "Modification d'un contact", + ) + db.session.add(log) + db.session.commit() + flash("Le contact a été modifié.") + return redirect(url_for("entreprises.fiche_entreprise", id=contact.entreprise.id)) + elif request.method == 'GET': + form.nom.data = contact.nom + form.prenom.data = contact.prenom + form.telephone.data = contact.telephone + form.mail.data = contact.mail + return render_template("entreprises/form.html", title=("Modification contact"), form=form) + +@bp.route("/delete_contact/", methods=["GET", "POST"]) +def delete_contact(id): + contact = EntrepriseContact.query.filter_by(id=id).first_or_404() + entreprise_id = contact.entreprise.id + 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=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=entreprise_id)) + return render_template("entreprises/delete_confirmation.html", title=("Supression contact"), form=form) + +@bp.route("/add_historique/", methods=["GET", "POST"]) +def add_historique(id): + entreprise = Entreprise.query.filter_by(id=id).first_or_404() + form = HistoriqueCreationForm() + 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) + historique = EntrepriseHistory( + entreprise_id = entreprise.id, + etudid = etudiant.id, + type_offre = form.type_offre.data.strip(), + date_debut = form.date_debut.data, + date_fin = form.date_fin.data, + formation_text = formation.formsemestre.titre + if formation else None, + formation_scodoc = formation.formsemestre.formsemestre_id + if formation else None + ) + db.session.add(historique) + 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/form.html", title=("Ajout historique"), form=form) + +@bp.route("/etudiants") +def json_etudiants(): + term = request.args.get('term').strip() + etudiants = Identite.query.filter(Identite.nom.ilike(f"%{term}%")).all() + list = [] + content = {} + for etudiant in etudiants: + value = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}" + if etudiant.inscription_courante() is not None: + content = { + "id": f"{etudiant.id}", + "value": value, + "info": f"{etudiant.inscription_courante().formsemestre.titre}" + } + else: + content = { + "id": f"{etudiant.id}", + "value": value + } + list.append(content) + content = {} + return jsonify(results=list) + +@bp.route("/envoyer_offre/", methods=["GET", "POST"]) +def envoyer_offre(id): + form = EnvoiOffreForm() + if form.validate_on_submit(): + print("tmp") # faire l'envoie du mail + return render_template("entreprises/form.html", title=("Envoyer une offre"), form=form) + +@bp.route("/responsables") +def json_responsables(): + term = request.args.get('term').strip() + responsables = User.query.filter(User.nom.ilike(f"%{term}%"), User.nom.is_not(None), User.prenom.is_not(None)).all() + list = [] + content = {} + for responsable in responsables: + value = f"{responsable.get_nomplogin()}" + content = { + "id": f"{responsable.id}", + "value": value, + "info": "" + } + list.append(content) + content = {} + return jsonify(results=list) \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py index 0fee7bc48..642e31873 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -14,12 +14,6 @@ from app.models.raw_sql_init import create_database_functions from app.models.absences import Absence, AbsenceNotification, BilletAbsence from app.models.departements import Departement - -from app.models.entreprises import ( - Entreprise, - EntrepriseCorrespondant, - EntrepriseContact, -) from app.models.etudiants import ( Identite, Adresse, diff --git a/app/models/departements.py b/app/models/departements.py index 0734e35b0..7ed2f4b56 100644 --- a/app/models/departements.py +++ b/app/models/departements.py @@ -19,7 +19,7 @@ class Departement(db.Model): db.Boolean(), nullable=False, default=True, server_default="true" ) # sur page d'accueil - entreprises = db.relationship("Entreprise", lazy="dynamic", backref="departement") + # entreprises = db.relationship("Entreprise", lazy="dynamic", backref="departement") etudiants = db.relationship("Identite", lazy="dynamic", backref="departement") formations = db.relationship("Formation", lazy="dynamic", backref="departement") formsemestres = db.relationship( diff --git a/app/models/entreprises.py b/app/models/entreprises.py deleted file mode 100644 index bdb5672a8..000000000 --- a/app/models/entreprises.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: UTF-8 -* - -"""Gestion des absences -""" - -from app import db -from app.models import APO_CODE_STR_LEN -from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN - - -class Entreprise(db.Model): - """une entreprise""" - - __tablename__ = "entreprises" - id = db.Column(db.Integer, primary_key=True) - entreprise_id = db.synonym("id") - dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) - nom = db.Column(db.Text) - adresse = db.Column(db.Text) - ville = db.Column(db.Text) - codepostal = db.Column(db.Text) - pays = db.Column(db.Text) - contact_origine = db.Column(db.Text) - secteur = db.Column(db.Text) - note = db.Column(db.Text) - privee = db.Column(db.Text) - localisation = db.Column(db.Text) - # -1 inconnue, 0, 25, 50, 75, 100: - qualite_relation = db.Column(db.Integer) - plus10salaries = db.Column(db.Boolean()) - date_creation = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) - - -class EntrepriseCorrespondant(db.Model): - """Personne contact en entreprise""" - - __tablename__ = "entreprise_correspondant" - id = db.Column(db.Integer, primary_key=True) - entreprise_corresp_id = db.synonym("id") - entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id")) - nom = db.Column(db.Text) - prenom = db.Column(db.Text) - civilite = db.Column(db.Text) - fonction = db.Column(db.Text) - phone1 = db.Column(db.Text) - phone2 = db.Column(db.Text) - mobile = db.Column(db.Text) - mail1 = db.Column(db.Text) - mail2 = db.Column(db.Text) - fax = db.Column(db.Text) - note = db.Column(db.Text) - - -class EntrepriseContact(db.Model): - """Evènement (contact) avec une entreprise""" - - __tablename__ = "entreprise_contact" - id = db.Column(db.Integer, primary_key=True) - entreprise_contact_id = db.synonym("id") - date = db.Column(db.DateTime(timezone=True)) - type_contact = db.Column(db.Text) - entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id")) - entreprise_corresp_id = db.Column( - db.Integer, db.ForeignKey("entreprise_correspondant.id") - ) - etudid = db.Column(db.Integer) # sans contrainte pour garder logs après suppression - description = db.Column(db.Text) - enseignant = db.Column(db.Text) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 0ae36bd28..0f81ad6bf 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -107,6 +107,14 @@ class Identite(db.Model): ] return r[0] if r else None + def inscription_courante_date(self, date_debut, date_fin): + r = [ + ins + for ins in self.formsemestre_inscriptions + if ins.formsemestre.est_courant_date(date_debut, date_fin) + ] + return r[0] if r else None + def etat_inscription(self, formsemestre_id): """etat de l'inscription de cet étudiant au semestre: False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 450ea2ccf..dc13c1cc8 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -146,6 +146,12 @@ class FormSemestre(db.Model): today = datetime.date.today() return (self.date_debut <= today) and (today <= self.date_fin) + def est_courant_date(self, date_debut, date_fin) -> bool: + """Vrai si la date actuelle (now) est dans le semestre + (les dates de début et fin sont incluses) + """ + return (self.date_debut <= date_debut) and (date_fin <= self.date_fin) + def est_decale(self): """Vrai si semestre "décalé" c'est à dire semestres impairs commençant entre janvier et juin diff --git a/app/templates/entreprises/_contact.html b/app/templates/entreprises/_contact.html new file mode 100644 index 000000000..eff3a8cb2 --- /dev/null +++ b/app/templates/entreprises/_contact.html @@ -0,0 +1,13 @@ +
+

+ Nom : {{ contact.nom }}
+ Prénom : {{ contact.prenom }}
+ Téléphone : {{ contact.telephone }}
+ Mail : {{ contact.mail }}
+

+ + +
\ No newline at end of file diff --git a/app/templates/entreprises/_offre.html b/app/templates/entreprises/_offre.html new file mode 100644 index 000000000..e64f3c16c --- /dev/null +++ b/app/templates/entreprises/_offre.html @@ -0,0 +1,15 @@ +
+

+ Intitulé : {{ offre.intitule }}
+ Description : {{ offre.description }}
+ Type de l'offre : {{ offre.type_offre }}
+ Missions : {{ offre.missions }}
+ Durée : {{ offre.duree }}
+

+ + +
\ No newline at end of file diff --git a/app/templates/entreprises/delete_confirmation.html b/app/templates/entreprises/delete_confirmation.html new file mode 100644 index 000000000..06b75b6ad --- /dev/null +++ b/app/templates/entreprises/delete_confirmation.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +

{{ title }}

+
+
Cliquez sur le bouton supprimer pour confirmer votre supression
+
+
+
+ {{ wtf.quick_form(form) }} +
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/entreprises.html b/app/templates/entreprises/entreprises.html new file mode 100644 index 000000000..4ca2860ee --- /dev/null +++ b/app/templates/entreprises/entreprises.html @@ -0,0 +1,65 @@ +{% extends 'base.html' %} + +{% block app_content %} + + {% if logs %} +
+

Dernières opérations

+
    + {% for log in logs %} +
  • {{ log.date.strftime('%d %b %Hh%M') }}{{ log.text|safe }} par {{ log.authenticated_user|get_nomcomplet }}
  • + {% endfor %} +
+
+ {% endif %} +
+

Liste des entreprises

+ {% if entreprises %} +
+ + + + + + + + + + + {% for entreprise in entreprises %} + + + + + + + + + + {% endfor %} +
SIRETNomAdresseCode postalVillePaysAction
{{ entreprise.siret }}{{ entreprise.nom }}{{ entreprise.adresse }}{{ entreprise.codepostal }}{{ entreprise.ville }}{{ entreprise.pays }} + +
+
+ {% else %} +
Aucune entreprise présent dans la base
+
+
+ {% endif %} + Ajouter une entreprise +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/fiche_entreprise.html b/app/templates/entreprises/fiche_entreprise.html new file mode 100644 index 000000000..5667d1328 --- /dev/null +++ b/app/templates/entreprises/fiche_entreprise.html @@ -0,0 +1,70 @@ +{% extends 'base.html' %} + +{% block app_content %} + {% if logs %} +
+

Dernières opérations sur cette fiche

+
    + {% for log in logs %} +
  • + {{ log.date.strftime('%d %b %Hh%M') }} + {{ log.text|safe }} par {{ log.authenticated_user|get_nomcomplet }} +
  • + {% endfor %} +
+
+ {% endif %} + {% if historique %} +
+

Historique

+
    + {% for data in historique %} +
  • + {{ data[0].date_debut.strftime('%d/%m/%Y') }} - {{ data[0].date_fin.strftime('%d/%m/%Y') }} + {{ data[0].type_offre }} réalisé par {{ data[1].nom|format_nom }} {{ data[1].prenom|format_prenom }} +
  • + {% endfor %} +
+
+ {% endif %} +
+

Fiche entreprise - {{ entreprise.nom }} ({{ entreprise.siret }})

+ +
+

+ SIRET : {{ entreprise.siret }}
+ Nom : {{ entreprise.nom }}
+ Adresse : {{ entreprise.adresse }}
+ Code postal : {{ entreprise.codepostal }}
+ Ville : {{ entreprise.ville }}
+ Pays : {{ entreprise.pays }} +

+
+ + {% if contacts %} +
+ {% for contact in contacts %} + Contact {{loop.index}} + {% include 'entreprises/_contact.html' %} + {% endfor %} +
+ {% endif %} + + {% if offres %} +
+ {% for offre in offres %} + Offre {{loop.index}} (ajouté le {{offre.date_ajout.strftime('%d/%m/%Y') }}) + {% include 'entreprises/_offre.html' %} + {% endfor %} +
+ {% endif %} + + +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/form.html b/app/templates/entreprises/form.html new file mode 100644 index 000000000..2efa5aedc --- /dev/null +++ b/app/templates/entreprises/form.html @@ -0,0 +1,71 @@ +{% extends 'base.html' %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block styles %} +{{super()}} + + +{% endblock %} + +{% block app_content %} +

{{ title }}

+
+
+
+ {{ wtf.quick_form(form, novalidate=True) }} +
+
+ +{% endblock %} \ No newline at end of file diff --git a/migrations/versions/f62d3a0bde1a_creation_tables_relations_entreprises.py b/migrations/versions/f62d3a0bde1a_creation_tables_relations_entreprises.py new file mode 100644 index 000000000..ae414038b --- /dev/null +++ b/migrations/versions/f62d3a0bde1a_creation_tables_relations_entreprises.py @@ -0,0 +1,127 @@ +"""creation tables relations entreprises + +Revision ID: f62d3a0bde1a +Revises: 39818df276aa +Create Date: 2021-12-10 11:25:04.135491 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'f62d3a0bde1a' +down_revision = '91be8a06d423' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('entreprise_log', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('authenticated_user', sa.Text(), nullable=True), + sa.Column('object', sa.Integer(), nullable=True), + sa.Column('text', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('entreprise_history', + 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.ForeignKeyConstraint(['entreprise_id'], ['entreprises.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('entreprise_offre', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('entreprise_id', sa.Integer(), nullable=True), + sa.Column('date_ajout', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('intitule', sa.Text(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('type_offre', sa.Text(), nullable=True), + sa.Column('missions', sa.Text(), nullable=True), + sa.Column('duree', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['entreprise_id'], ['entreprises.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + op.drop_constraint('entreprise_contact_entreprise_corresp_id_fkey', 'entreprise_contact', type_='foreignkey') + op.drop_table('entreprise_correspondant') + op.add_column('entreprise_contact', sa.Column('nom', sa.Text(), nullable=True)) + op.add_column('entreprise_contact', sa.Column('prenom', sa.Text(), nullable=True)) + op.add_column('entreprise_contact', sa.Column('telephone', sa.Text(), nullable=True)) + op.add_column('entreprise_contact', sa.Column('mail', sa.Text(), nullable=True)) + op.drop_column('entreprise_contact', 'date') + op.drop_column('entreprise_contact', 'description') + op.drop_column('entreprise_contact', 'type_contact') + op.drop_column('entreprise_contact', 'enseignant') + op.drop_column('entreprise_contact', 'entreprise_corresp_id') + op.drop_column('entreprise_contact', 'etudid') + + op.add_column('entreprises', sa.Column('siret', sa.Text(), nullable=True)) + op.drop_index('ix_entreprises_dept_id', table_name='entreprises') + op.drop_constraint('entreprises_dept_id_fkey', 'entreprises', type_='foreignkey') + op.drop_column('entreprises', 'localisation') + op.drop_column('entreprises', 'contact_origine') + op.drop_column('entreprises', 'qualite_relation') + op.drop_column('entreprises', 'secteur') + op.drop_column('entreprises', 'plus10salaries') + op.drop_column('entreprises', 'dept_id') + op.drop_column('entreprises', 'privee') + op.drop_column('entreprises', 'date_creation') + op.drop_column('entreprises', 'note') + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('entreprises', sa.Column('note', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('entreprises', sa.Column('date_creation', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True)) + op.add_column('entreprises', sa.Column('privee', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('entreprises', sa.Column('dept_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('entreprises', sa.Column('plus10salaries', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.add_column('entreprises', sa.Column('secteur', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('entreprises', sa.Column('qualite_relation', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('entreprises', sa.Column('contact_origine', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('entreprises', sa.Column('localisation', sa.TEXT(), autoincrement=False, nullable=True)) + op.create_foreign_key('entreprises_dept_id_fkey', 'entreprises', 'departement', ['dept_id'], ['id']) + op.create_index('ix_entreprises_dept_id', 'entreprises', ['dept_id'], unique=False) + op.drop_column('entreprises', 'siret') + op.add_column('entreprise_contact', sa.Column('etudid', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('entreprise_contact', sa.Column('entreprise_corresp_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('entreprise_contact', sa.Column('enseignant', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('entreprise_contact', sa.Column('type_contact', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('entreprise_contact', sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('entreprise_contact', sa.Column('date', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True)) + op.create_table('entreprise_correspondant', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('entreprise_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('nom', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('prenom', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('civilite', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('fonction', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('phone1', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('phone2', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('mobile', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('mail1', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('mail2', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('fax', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('note', sa.TEXT(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['entreprise_id'], ['entreprises.id'], name='entreprise_correspondant_entreprise_id_fkey'), + sa.PrimaryKeyConstraint('id', name='entreprise_correspondant_pkey') + ) + op.create_foreign_key('entreprise_contact_entreprise_corresp_id_fkey', 'entreprise_contact', 'entreprise_correspondant', ['entreprise_corresp_id'], ['id']) + op.drop_column('entreprise_contact', 'mail') + op.drop_column('entreprise_contact', 'telephone') + op.drop_column('entreprise_contact', 'prenom') + op.drop_column('entreprise_contact', 'nom') + + op.drop_table('entreprise_offre') + op.drop_table('entreprise_history') + op.drop_table('entreprise_log') + # ### end Alembic commands ### \ No newline at end of file From 93d026d2ac527b8b54b0bd80a9d6a98ffcacd848 Mon Sep 17 00:00:00 2001 From: Arthur ZHU Date: Thu, 23 Dec 2021 23:19:31 +0100 Subject: [PATCH 02/27] cascade sur les offres et les contacts --- app/entreprises/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/entreprises/models.py b/app/entreprises/models.py index a079cacf8..a04d115bc 100644 --- a/app/entreprises/models.py +++ b/app/entreprises/models.py @@ -9,8 +9,8 @@ class Entreprise(db.Model): codepostal = db.Column(db.Text) ville = db.Column(db.Text) pays = db.Column(db.Text) - contacts = db.relationship('EntrepriseContact', backref='entreprise', lazy='dynamic') - offres = db.relationship('EntrepriseOffre', backref='entreprise', lazy='dynamic') + contacts = db.relationship('EntrepriseContact', backref='entreprise', lazy='dynamic', cascade="all, delete-orphan") + offres = db.relationship('EntrepriseOffre', backref='entreprise', lazy='dynamic', cascade="all, delete-orphan") class EntrepriseContact(db.Model): __tablename__ = "entreprise_contact" From d740244d253713d2df3ab79ff9bac60651a18e4d Mon Sep 17 00:00:00 2001 From: Arthur ZHU Date: Thu, 23 Dec 2021 23:21:47 +0100 Subject: [PATCH 03/27] ajout page contacts --- app/entreprises/routes.py | 45 +++++++++++-------- app/templates/entreprises/contacts.html | 43 ++++++++++++++++++ app/templates/entreprises/entreprises.html | 11 +---- .../entreprises/fiche_entreprise.html | 4 +- app/templates/entreprises/form.html | 32 +++++++------ 5 files changed, 92 insertions(+), 43 deletions(-) create mode 100644 app/templates/entreprises/contacts.html diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index b74390873..628a52335 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -32,12 +32,30 @@ from app import db from app.scodoc import sco_etud from sqlalchemy import text -@bp.route("/", methods=["GET", "POST"]) +@bp.route("/", methods=["GET"]) def index(): entreprises = Entreprise.query.all() logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all() return render_template("entreprises/entreprises.html", title=("Entreprises"), entreprises=entreprises, logs=logs) +@bp.route("/contacts", methods=["GET"]) +def contacts(): + contacts = db.session.query(EntrepriseContact, Entreprise).\ + join(Entreprise, EntrepriseContact.entreprise_id == Entreprise.id).all() + logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all() + return render_template("entreprises/contacts.html", title=("Contacts"), contacts=contacts, logs=logs) + +@bp.route("/fiche_entreprise/", methods=["GET"]) +def fiche_entreprise(id): + entreprise = Entreprise.query.filter_by(id=id).first_or_404() + offres = entreprise.offres + contacts = entreprise.contacts + logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).filter_by(object=id).limit(LOGS_LEN).all() + historique = db.session.query(EntrepriseHistory, Identite).order_by(EntrepriseHistory.date_debut.desc()).\ + filter_by(entreprise_id=id).\ + join(Identite, Identite.id == EntrepriseHistory.etudid).all() + return render_template("entreprises/fiche_entreprise.html", title=("Fiche entreprise"), entreprise=entreprise, contacts=contacts, offres=offres, logs=logs, historique=historique) + @bp.route("/add_entreprise", methods=["GET", "POST"]) def add_entreprise(): form = EntrepriseCreationForm() @@ -147,17 +165,6 @@ def delete_entreprise(id): return redirect(url_for("entreprises.index")) return render_template("entreprises/delete_confirmation.html", title=("Supression entreprise"), form=form) -@bp.route("/fiche_entreprise/", methods=["GET", "POST"]) -def fiche_entreprise(id): - entreprise = Entreprise.query.filter_by(id=id).first_or_404() - offres = entreprise.offres - contacts = entreprise.contacts - logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).filter_by(object=id).limit(LOGS_LEN).all() - historique = db.session.query(EntrepriseHistory, Identite).order_by(EntrepriseHistory.date_debut.desc()).\ - filter_by(entreprise_id=id).\ - join(Identite, Identite.id == EntrepriseHistory.etudid).all() - return render_template("entreprises/fiche_entreprise.html", title=("Fiche entreprise"), entreprise=entreprise, contacts=contacts, offres=offres, logs=logs, historique=historique) - @bp.route("/add_offre/", methods=["GET", "POST"]) def add_offre(id): entreprise = Entreprise.query.filter_by(id=id).first_or_404() @@ -326,6 +333,13 @@ def add_historique(id): return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) return render_template("entreprises/form.html", title=("Ajout historique"), form=form) +@bp.route("/envoyer_offre/", methods=["GET", "POST"]) +def envoyer_offre(id): + form = EnvoiOffreForm() + if form.validate_on_submit(): + print("tmp") # faire l'envoie du mail + return render_template("entreprises/form.html", title=("Envoyer une offre"), form=form) + @bp.route("/etudiants") def json_etudiants(): term = request.args.get('term').strip() @@ -348,13 +362,6 @@ def json_etudiants(): list.append(content) content = {} return jsonify(results=list) - -@bp.route("/envoyer_offre/", methods=["GET", "POST"]) -def envoyer_offre(id): - form = EnvoiOffreForm() - if form.validate_on_submit(): - print("tmp") # faire l'envoie du mail - return render_template("entreprises/form.html", title=("Envoyer une offre"), form=form) @bp.route("/responsables") def json_responsables(): diff --git a/app/templates/entreprises/contacts.html b/app/templates/entreprises/contacts.html new file mode 100644 index 000000000..7f1433334 --- /dev/null +++ b/app/templates/entreprises/contacts.html @@ -0,0 +1,43 @@ +{% extends 'base.html' %} + +{% block app_content %} + {% if logs %} +
+

Dernières opérations

+
    + {% for log in logs %} +
  • {{ log.date.strftime('%d %b %Hh%M') }}{{ log.text|safe }} par {{ log.authenticated_user|get_nomcomplet }}
  • + {% endfor %} +
+
+ {% endif %} +
+

Liste des contacts

+ {% if contacts %} +
+ + + + + + + + + {% for contact in contacts %} + + + + + + + + {% endfor %} +
NomPrenomTelephoneMailEntreprise
{{ contact[0].nom }}{{ contact[0].prenom }}{{ contact[0].telephone }}{{ contact[0].mail }}{{ contact[1].nom }}
+
+ {% else %} +
Aucun contact présent dans la base
+
+
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/entreprises.html b/app/templates/entreprises/entreprises.html index 4ca2860ee..a33dd7e77 100644 --- a/app/templates/entreprises/entreprises.html +++ b/app/templates/entreprises/entreprises.html @@ -1,13 +1,6 @@ {% extends 'base.html' %} {% block app_content %} - {% if logs %}

Dernières opérations

@@ -22,7 +15,7 @@

Liste des entreprises

{% if entreprises %}
- +
@@ -35,7 +28,7 @@ {% for entreprise in entreprises %} - + diff --git a/app/templates/entreprises/fiche_entreprise.html b/app/templates/entreprises/fiche_entreprise.html index 5667d1328..b84980366 100644 --- a/app/templates/entreprises/fiche_entreprise.html +++ b/app/templates/entreprises/fiche_entreprise.html @@ -21,7 +21,9 @@ {% for data in historique %}
  • {{ data[0].date_debut.strftime('%d/%m/%Y') }} - {{ data[0].date_fin.strftime('%d/%m/%Y') }} - {{ data[0].type_offre }} réalisé par {{ data[1].nom|format_nom }} {{ data[1].prenom|format_prenom }} + + {{ data[0].type_offre }} réalisé par {{ data[1].nom|format_nom }} {{ data[1].prenom|format_prenom }} en {{ data[0].formation_text }} +
  • {% endfor %} diff --git a/app/templates/entreprises/form.html b/app/templates/entreprises/form.html index 2efa5aedc..25cb579dd 100644 --- a/app/templates/entreprises/form.html +++ b/app/templates/entreprises/form.html @@ -47,25 +47,29 @@ if(response.ok) return response.json() else { - document.getElementById("nom_entreprise").value = '' - document.getElementById("adresse").value = '' - document.getElementById("codepostal").value = '' - document.getElementById("ville").value = '' - document.getElementById("pays").value = '' - throw new Error("404") + emptyForm() } }) - .then(function(response) { - data = JSON.parse(JSON.stringify(response)) - document.getElementById("nom_entreprise").value = data.etablissement.l1_normalisee - document.getElementById("adresse").value = data.etablissement.l4_normalisee - document.getElementById("codepostal").value = data.etablissement.code_postal - document.getElementById("ville").value = data.etablissement.libelle_commune - document.getElementById("pays").value = 'France' - }) + .then(response => fillForm(response)) .catch(err => err) } } + + function fillForm(response) { + document.getElementById("nom_entreprise").value = response.etablissement.l1_normalisee + document.getElementById("adresse").value = response.etablissement.l4_normalisee + document.getElementById("codepostal").value = response.etablissement.code_postal + document.getElementById("ville").value = response.etablissement.libelle_commune + document.getElementById("pays").value = 'FRANCE' + } + + function emptyForm() { + document.getElementById("nom_entreprise").value = '' + document.getElementById("adresse").value = '' + document.getElementById("codepostal").value = '' + document.getElementById("ville").value = '' + document.getElementById("pays").value = '' + } } {% endblock %} \ No newline at end of file From dce7dc42cbb0ec81f5b78d7de7a5282b334c0b26 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 24 Dec 2021 00:08:25 +0100 Subject: [PATCH 04/27] =?UTF-8?q?Pr=C3=A9pare=20modernisation=20de=20NT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but.py | 182 ++---------------- app/but/bulletin_but_xml_compat.py | 10 - app/comp/res_sem.py | 216 ++++++++++++++++++++++ app/models/formsemestre.py | 6 +- app/pe/pe_tagtable.py | 12 +- app/scodoc/notes_table.py | 61 ++---- app/scodoc/sco_abs_views.py | 4 +- app/scodoc/sco_apogee_csv.py | 4 +- app/scodoc/sco_bulletins.py | 6 +- app/scodoc/sco_bulletins_json.py | 4 +- app/scodoc/sco_bulletins_xml.py | 4 +- app/scodoc/sco_cache.py | 10 +- app/scodoc/sco_formsemestre_validation.py | 16 +- app/scodoc/sco_moduleimpl_inscriptions.py | 6 +- app/scodoc/sco_parcours_dut.py | 20 +- app/scodoc/sco_poursuite_dut.py | 2 +- app/scodoc/sco_preferences.py | 28 +++ app/scodoc/sco_prepajury.py | 12 +- app/scodoc/sco_pvjury.py | 2 +- app/scodoc/sco_recapcomplet.py | 6 +- app/views/absences.py | 26 +-- app/views/notes.py | 2 +- 22 files changed, 346 insertions(+), 293 deletions(-) create mode 100644 app/comp/res_sem.py diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 4ff2849f8..65756767f 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -4,7 +4,6 @@ # See LICENSE ############################################################################## -from collections import defaultdict import datetime from flask import url_for, g import numpy as np @@ -15,62 +14,24 @@ from app import db from app.comp import moy_ue, moy_sem, inscr_mod from app.models import ModuleImpl from app.scodoc import sco_utils as scu -from app.scodoc.sco_cache import ResultatsSemestreBUTCache from app.scodoc import sco_bulletins_json from app.scodoc import sco_preferences from app.scodoc.sco_utils import jsnan, fmt_note +from app.comp.res_sem import ResultatsSemestre, NotesTableCompat -class ResultatsSemestreBUT: - """Structure légère pour stocker les résultats du semestre et - générer les bulletins. - __init__ : charge depuis le cache ou calcule - """ +class ResultatsSemestreBUT(NotesTableCompat): + """Résultats BUT: organisation des calculs""" - _cached_attrs = ( - "sem_cube", - "modimpl_inscr_df", - "modimpl_coefs_df", - "etud_moy_ue", - "modimpls_evals_poids", - "modimpls_evals_notes", - "etud_moy_gen", - "etud_moy_gen_ranks", - "modimpls_evaluations_complete", - ) + _cached_attrs = NotesTableCompat._cached_attrs + () def __init__(self, formsemestre): - self.formsemestre = formsemestre - self.ues = formsemestre.query_ues().all() - self.modimpls = formsemestre.modimpls.all() - self.etuds = self.formsemestre.get_inscrits(include_dem=False) - self.etud_index = {e.id: idx for idx, e in enumerate(self.etuds)} - self.saes = [ - m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE - ] - self.ressources = [ - m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE - ] + super().__init__(formsemestre) + if not self.load_cached(): self.compute() self.store() - def load_cached(self) -> bool: - "Load cached dataframes, returns False si pas en cache" - data = ResultatsSemestreBUTCache.get(self.formsemestre.id) - if not data: - return False - for attr in self._cached_attrs: - setattr(self, attr, data[attr]) - return True - - def store(self): - "Cache our dataframes" - ResultatsSemestreBUTCache.set( - self.formsemestre.id, - {attr: getattr(self, attr) for attr in self._cached_attrs}, - ) - def compute(self): "Charge les notes et inscriptions et calcule toutes les moyennes" ( @@ -100,6 +61,13 @@ class ResultatsSemestreBUT: ) self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen) + +class BulletinBUT(ResultatsSemestreBUT): + """Génération du bulletin BUT. + Cette classe génère des dictionnaires avec toutes les informations + du bulletin, qui sont immédiatement traduisibles en JSON. + """ + def etud_ue_mod_results(self, etud, ue, modimpls) -> dict: "dict synthèse résultats dans l'UE pour les modules indiqués" d = {} @@ -233,7 +201,7 @@ class ResultatsSemestreBUT: }, "formsemestre_id": formsemestre.id, "etat_inscription": etat_inscription, - "options": bulletin_option_affichage(formsemestre), + "options": sco_preferences.bulletin_option_affichage(formsemestre.id), } semestre_infos = { "etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo], @@ -298,125 +266,3 @@ class ResultatsSemestreBUT: ) return d - - -def bulletin_option_affichage(formsemestre): - "dict avec les options d'affichages (préférences) pour ce semestre" - prefs = sco_preferences.SemPreferences(formsemestre.id) - fields = ( - "bul_show_abs", - "bul_show_abs_modules", - "bul_show_ects", - "bul_show_codemodules", - "bul_show_matieres", - "bul_show_rangs", - "bul_show_ue_rangs", - "bul_show_mod_rangs", - "bul_show_moypromo", - "bul_show_minmax", - "bul_show_minmax_mod", - "bul_show_minmax_eval", - "bul_show_coef", - "bul_show_ue_cap_details", - "bul_show_ue_cap_current", - "bul_show_temporary", - "bul_temporary_txt", - "bul_show_uevalid", - "bul_show_date_inscr", - ) - # on enlève le "bul_" de la clé: - return {field[4:]: prefs[field] for field in fields} - - -# Pour raccorder le code des anciens bulletins qui attendent une NoteTable -class APCNotesTableCompat: - """Implementation partielle de NotesTable pour les formations APC - Accès aux notes et rangs. - """ - - def __init__(self, formsemestre): - self.results = ResultatsSemestreBUT(formsemestre) - nb_etuds = len(self.results.etuds) - self.rangs = self.results.etud_moy_gen_ranks - self.moy_min = self.results.etud_moy_gen.min() - self.moy_max = self.results.etud_moy_gen.max() - self.moy_moy = self.results.etud_moy_gen.mean() - self.bonus = defaultdict(lambda: 0.0) # XXX - self.ue_rangs = { - u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.results.ues - } - self.mod_rangs = { - m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.results.modimpls - } - - def get_ues(self): - ues = [] - for ue in self.results.ues: - d = ue.to_dict() - d.update( - { - "max": self.results.etud_moy_ue[ue.id].max(), - "min": self.results.etud_moy_ue[ue.id].min(), - "moy": self.results.etud_moy_ue[ue.id].mean(), - "nb_moy": len(self.results.etud_moy_ue), - } - ) - ues.append(d) - return ues - - def get_modimpls(self): - return [m.to_dict() for m in self.results.modimpls] - - def get_etud_moy_gen(self, etudid): - return self.results.etud_moy_gen[etudid] - - def get_moduleimpls_attente(self): - return [] # XXX TODO - - def get_etud_rang(self, etudid): - return self.rangs[etudid] - - def get_etud_rang_group(self, etudid, group_id): - return (None, 0) # XXX unimplemented TODO - - def get_etud_ue_status(self, etudid, ue_id): - return { - "cur_moy_ue": self.results.etud_moy_ue[ue_id][etudid], - "is_capitalized": False, # XXX TODO - } - - def get_etud_mod_moy(self, moduleimpl_id, etudid): - mod_idx = self.results.modimpl_coefs_df.columns.get_loc(moduleimpl_id) - etud_idx = self.results.etud_index[etudid] - # moyenne sur les UE: - self.results.sem_cube[etud_idx, mod_idx].mean() - - def get_mod_stats(self, moduleimpl_id): - return { - "moy": "-", - "max": "-", - "min": "-", - "nb_notes": "-", - "nb_missing": "-", - "nb_valid_evals": "-", - } - - def get_evals_in_mod(self, moduleimpl_id): - mi = ModuleImpl.query.get(moduleimpl_id) - evals_results = [] - for e in mi.evaluations: - d = e.to_dict() - d["heure_debut"] = e.heure_debut # datetime.time - d["heure_fin"] = e.heure_fin - d["jour"] = e.jour # datetime - d["notes"] = { - etud.id: { - "etudid": etud.id, - "value": self.results.modimpls_evals_notes[e.moduleimpl_id][e.id][ - etud.id - ], - } - for etud in self.results.etuds - } - evals_results.append(d) - return evals_results diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py index 9e234dc0b..6e2f14dbf 100644 --- a/app/but/bulletin_but_xml_compat.py +++ b/app/but/bulletin_but_xml_compat.py @@ -314,13 +314,3 @@ def bulletin_but_xml_compat( return None else: return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING) - - -""" -formsemestre_id=718 -etudid=12496 -from app.but.bulletin_but import * -mapp.set_sco_dept("RT") -sem = FormSemestre.query.get(formsemestre_id) -r = ResultatsSemestreBUT(sem) -""" diff --git a/app/comp/res_sem.py b/app/comp/res_sem.py new file mode 100644 index 000000000..01a2e8720 --- /dev/null +++ b/app/comp/res_sem.py @@ -0,0 +1,216 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +from collections import defaultdict +from functools import cached_property +import numpy as np +import pandas as pd +from app.scodoc import sco_utils as scu +from app.scodoc.sco_cache import ResultatsSemestreCache +from app.scodoc.sco_codes_parcours import UE_SPORT + +# Il faut bien distinguer +# - ce qui est caché de façon persistente (via redis): +# ce sont les attributs listés dans `_cached_attrs` +# le stockage et l'invalidation sont gérés dans sco_cache.py +# +# - les valeurs cachées durant le temps d'une requête +# (durée de vie de l'instance de ResultatsSemestre) +# qui sont notamment les attributs décorés par `@cached_property`` +# +class ResultatsSemestre: + _cached_attrs = ( + "sem_cube", + "modimpl_inscr_df", + "modimpl_coefs_df", + "etud_moy_ue", + "modimpls_evals_poids", + "modimpls_evals_notes", + "etud_moy_gen", + "etud_moy_gen_ranks", + "modimpls_evaluations_complete", + ) + + def __init__(self, formsemestre): + self.formsemestre = formsemestre + # TODO + + def load_cached(self) -> bool: + "Load cached dataframes, returns False si pas en cache" + data = ResultatsSemestreCache.get(self.formsemestre.id) + if not data: + return False + for attr in self._cached_attrs: + setattr(self, attr, data[attr]) + return True + + def store(self): + "Cache our data" + "Cache our dataframes" + ResultatsSemestreCache.set( + self.formsemestre.id, + {attr: getattr(self, attr) for attr in self._cached_attrs}, + ) + + def compute(self): + "Charge les notes et inscriptions et calcule toutes les moyennes" + # voir ce qui est chargé / calculé ici et dans les sous-classes + TODO + + @cached_property + def etuds(self): + "Liste des inscrits au semestre, sans les démissionnaires" + # nb: si les liste des inscrits change, ResultatsSemestre devient invalide + return self.formsemestre.get_inscrits(include_dem=False) + + @cached_property + def etud_index(self): + "dict { etudid : indice dans les inscrits }" + return {e.id: idx for idx, e in enumerate(self.etuds)} + + @cached_property + def ues(self): + "Liste des UE du semestre" + return self.formsemestre.query_ues().all() + + @cached_property + def modimpls(self): + "Liste des modimpls du semestre (triée par numéro de module)" + modimpls = self.formsemestre.modimpls.all() + modimpls.sort(key=lambda m: m.module.numero) + return modimpls + + @cached_property + def ressources(self): + "Liste des ressources du semestre, triées par numéro de module" + return [ + m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE + ] + + @cached_property + def saes(self): + "Liste des SAÉs du semestre, triées par numéro de module" + return [m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE] + + +class StatsMoyenne: + """Une moyenne d'un ensemble étudiants sur quelque chose + (moyenne générale d'un semestre, d'un module, d'un groupe...) + et les statistiques associées: min, max, moy, effectif + """ + + def __init__(self, vals): + """Calcul les statistiques. + Les valeurs NAN ou non numériques sont toujours enlevées. + """ + self.moy = np.nanmean(vals) + self.min = np.nanmin(vals) + self.max = np.nanmax(vals) + self.size = len(vals) + self.nb_vals = self.size - np.count_nonzero(np.isnan(vals)) + + def to_dict(self): + return { + "min": self.min, + "max": self.max, + "moy": self.moy, + "size": self.size, + "nb_vals": self.nb_vals, + } + + +# Pour raccorder le code des anciens codes qui attendent une NoteTable +class NotesTableCompat(ResultatsSemestre): + """Implementation partielle de NotesTable WIP TODO + Accès aux notes et rangs. + """ + + _cached_attrs = ResultatsSemestre._cached_attrs + () + + def __init__(self, formsemestre): + super().__init__(formsemestre) + nb_etuds = len(self.etuds) + self.bonus = defaultdict(lambda: 0.0) # XXX TODO + self.ue_rangs = {u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.ues} + self.mod_rangs = { + m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.modimpls + } + + @cached_property + def stats_moy_gen(self): + """Stats (moy/min/max) sur la moyenne générale""" + return StatsMoyenne(self.etud_moy_gen) + + def get_ues_stat_dict(self, filter_sport=False): # was get_ues() + """Liste des UEs, ordonnée par numero. + Si filter_sport, retire les UE de type SPORT. + Résultat: liste de dicts { champs UE U stats moyenne UE } + """ + ues = [] + for ue in self.ues: + if filter_sport and ue.type == UE_SPORT: + continue + d = ue.to_dict() + d.update(StatsMoyenne(self.etud_moy_ue[ue.id]).to_dict()) + ues.append(d) + return ues + + def get_modimpls(self): + return [m.to_dict() for m in self.results.modimpls] + + def get_etud_moy_gen(self, etudid): + return self.results.etud_moy_gen[etudid] + + def get_moduleimpls_attente(self): + return [] # XXX TODO + + def get_etud_rang(self, etudid): + return self.etud_moy_gen_ranks[etudid] + + def get_etud_rang_group(self, etudid, group_id): + return (None, 0) # XXX unimplemented TODO + + def get_etud_ue_status(self, etudid, ue_id): + return { + "cur_moy_ue": self.results.etud_moy_ue[ue_id][etudid], + "is_capitalized": False, # XXX TODO + } + + def get_etud_mod_moy(self, moduleimpl_id, etudid): + mod_idx = self.results.modimpl_coefs_df.columns.get_loc(moduleimpl_id) + etud_idx = self.results.etud_index[etudid] + # moyenne sur les UE: + self.results.sem_cube[etud_idx, mod_idx].mean() + + def get_mod_stats(self, moduleimpl_id): + return { + "moy": "-", + "max": "-", + "min": "-", + "nb_notes": "-", + "nb_missing": "-", + "nb_valid_evals": "-", + } + + def get_evals_in_mod(self, moduleimpl_id): + mi = ModuleImpl.query.get(moduleimpl_id) + evals_results = [] + for e in mi.evaluations: + d = e.to_dict() + d["heure_debut"] = e.heure_debut # datetime.time + d["heure_fin"] = e.heure_fin + d["jour"] = e.jour # datetime + d["notes"] = { + etud.id: { + "etudid": etud.id, + "value": self.results.modimpls_evals_notes[e.moduleimpl_id][e.id][ + etud.id + ], + } + for etud in self.results.etuds + } + evals_results.append(d) + return evals_results diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 450ea2ccf..98edbba1e 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -84,7 +84,11 @@ class FormSemestre(db.Model): etapes = db.relationship( "FormSemestreEtape", cascade="all,delete", backref="formsemestre" ) - modimpls = db.relationship("ModuleImpl", backref="formsemestre", lazy="dynamic") + modimpls = db.relationship( + "ModuleImpl", + backref="formsemestre", + lazy="dynamic", + ) etuds = db.relationship( "Identite", secondary="notes_formsemestre_inscription", diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tagtable.py index e32a11735..caba16362 100644 --- a/app/pe/pe_tagtable.py +++ b/app/pe/pe_tagtable.py @@ -68,7 +68,7 @@ class TableTag(object): self.taglist = [] self.resultats = {} - self.rangs = {} + self.etud_moy_gen_ranks = {} self.statistiques = {} # ***************************************************************************************************************** @@ -117,15 +117,15 @@ class TableTag(object): # ----------------------------------------------------------------------------------------------------------- def get_moy_from_stats(self, tag): - """ Renvoie la moyenne des notes calculées pour d'un tag donné""" + """Renvoie la moyenne des notes calculées pour d'un tag donné""" return self.statistiques[tag][0] if tag in self.statistiques else None def get_min_from_stats(self, tag): - """ Renvoie la plus basse des notes calculées pour d'un tag donné""" + """Renvoie la plus basse des notes calculées pour d'un tag donné""" return self.statistiques[tag][1] if tag in self.statistiques else None def get_max_from_stats(self, tag): - """ Renvoie la plus haute des notes calculées pour d'un tag donné""" + """Renvoie la plus haute des notes calculées pour d'un tag donné""" return self.statistiques[tag][2] if tag in self.statistiques else None # ----------------------------------------------------------------------------------------------------------- @@ -236,7 +236,7 @@ class TableTag(object): return moystr def str_res_d_un_etudiant(self, etudid, delim=";"): - """Renvoie sur une ligne les résultats d'un étudiant à tous les tags (par ordre alphabétique). """ + """Renvoie sur une ligne les résultats d'un étudiant à tous les tags (par ordre alphabétique).""" return delim.join( [self.str_resTag_d_un_etudiant(tag, etudid) for tag in self.get_all_tags()] ) @@ -256,7 +256,7 @@ class TableTag(object): # ----------------------------------------------------------------------- def str_tagtable(self, delim=";", decimal_sep=","): - """Renvoie une chaine de caractère listant toutes les moyennes, les rangs des étudiants pour tous les tags. """ + """Renvoie une chaine de caractère listant toutes les moyennes, les rangs des étudiants pour tous les tags.""" entete = ["etudid", "nom", "prenom"] for tag in self.get_all_tags(): entete += [titre + "_" + tag for titre in ["note", "rang", "nb_inscrit"]] diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index 052b387e1..69c37694f 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -25,7 +25,9 @@ # ############################################################################## -"""Calculs sur les notes et cache des resultats +"""Calculs sur les notes et cache des résultats + + Ancien code ScoDoc 7 en cours de rénovation """ from operator import itemgetter @@ -102,7 +104,7 @@ def comp_ranks(T): def get_sem_ues_modimpls(formsemestre_id, modimpls=None): """Get liste des UE du semestre (à partir des moduleimpls) - (utilisé quand on ne peut pas construire nt et faire nt.get_ues()) + (utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict()) """ if modimpls is None: modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) @@ -316,7 +318,7 @@ class NotesTable: self.moy_min = self.moy_max = "NA" # calcul rangs (/ moyenne generale) - self.rangs = comp_ranks(T) + self.etud_moy_gen_ranks = comp_ranks(T) self.rangs_groupes = ( {} @@ -417,43 +419,14 @@ class NotesTable: else: return ' (%s) ' % etat - def get_ues(self, filter_sport=False, filter_non_inscrit=False, etudid=None): - """liste des ue, ordonnée par numero. - Si filter_non_inscrit, retire les UE dans lesquelles l'etudiant n'est - inscrit à aucun module. + def get_ues_stat_dict(self, filter_sport=False): # was get_ues() + """Liste des UEs, ordonnée par numero. Si filter_sport, retire les UE de type SPORT """ - if not filter_sport and not filter_non_inscrit: + if not filter_sport: return self._ues - - if filter_sport: - ues_src = [ue for ue in self._ues if ue["type"] != UE_SPORT] else: - ues_src = self._ues - if not filter_non_inscrit: - return ues_src - ues = [] - for ue in ues_src: - if self.get_etud_ue_status(etudid, ue["ue_id"])["is_capitalized"]: - # garde toujours les UE capitalisees - has_note = True - else: - has_note = False - # verifie que l'etud. est inscrit a au moins un module de l'UE - # (en fait verifie qu'il a une note) - modimpls = self.get_modimpls(ue["ue_id"]) - - for modi in modimpls: - moy = self.get_etud_mod_moy(modi["moduleimpl_id"], etudid) - try: - float(moy) - has_note = True - break - except: - pass - if has_note: - ues.append(ue) - return ues + return [ue for ue in self._ues if ue["type"] != UE_SPORT] def get_modimpls(self, ue_id=None): "liste des modules pour une UE (ou toutes si ue_id==None), triés par matières." @@ -522,7 +495,7 @@ class NotesTable: Les moyennes d'UE ne tiennent pas compte des capitalisations. """ - ues = self.get_ues() + ues = self.get_ues_stat_dict() sum_moy = 0 # la somme des moyennes générales valides nb_moy = 0 # le nombre de moyennes générales valides for ue in ues: @@ -561,9 +534,9 @@ class NotesTable: i = 0 for ue in ues: i += 1 - ue["nb_moy"] = len(ue["_notes"]) - if ue["nb_moy"] > 0: - ue["moy"] = sum(ue["_notes"]) / ue["nb_moy"] + ue["nb_vals"] = len(ue["_notes"]) + if ue["nb_vals"] > 0: + ue["moy"] = sum(ue["_notes"]) / ue["nb_vals"] ue["max"] = max(ue["_notes"]) ue["min"] = min(ue["_notes"]) else: @@ -767,7 +740,7 @@ class NotesTable: sem_ects_pot_fond = 0.0 sem_ects_pot_pro = 0.0 - for ue in self.get_ues(): + for ue in self.get_ues_stat_dict(): # - On calcule la moyenne d'UE courante: if not block_computation: mu = self.comp_etud_moy_ue(etudid, ue_id=ue["ue_id"], cnx=cnx) @@ -981,7 +954,7 @@ class NotesTable: return self.T def get_etud_rang(self, etudid) -> str: - return self.rangs.get(etudid, "999") + return self.etud_moy_gen_ranks.get(etudid, "999") def get_etud_rang_group(self, etudid, group_id): """Returns rank of etud in this group and number of etuds in group. @@ -1347,7 +1320,7 @@ class NotesTable: # Rappel des épisodes précédents: T est une liste de liste # Colonnes: 0 moy_gen, moy_ue1, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid - ues = self.get_ues() # incluant le(s) UE de sport + ues = self.get_ues_stat_dict() # incluant le(s) UE de sport for t in self.T: etudid = t[-1] if etudid in results.etud_moy_gen: # evite les démissionnaires @@ -1358,4 +1331,4 @@ class NotesTable: # re-trie selon la nouvelle moyenne générale: self.T.sort(key=self._row_key) # Remplace aussi le rang: - self.rangs = results.etud_moy_gen_ranks + self.etud_moy_gen_ranks = results.etud_moy_gen_ranks diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py index 4e5aaa469..c719df312 100644 --- a/app/scodoc/sco_abs_views.py +++ b/app/scodoc/sco_abs_views.py @@ -118,7 +118,7 @@ def doSignaleAbsence( mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] formsemestre_id = mod["formsemestre_id"] nt = sco_cache.NotesTableCache.get(formsemestre_id) - ues = nt.get_ues(etudid=etudid) + ues = nt.get_ues_stat_dict() for ue in ues: modimpls = nt.get_modimpls(ue_id=ue["ue_id"]) for modimpl in modimpls: @@ -175,7 +175,7 @@ def SignaleAbsenceEtud(): # etudid implied "abs_require_module", formsemestre_id ) nt = sco_cache.NotesTableCache.get(formsemestre_id) - ues = nt.get_ues(etudid=etudid) + ues = nt.get_ues_stat_dict() if require_module: menu_module = """ +{% endblock %} + +{% block app_content %} +

    {{ title }}

    +
    +
    +
    + {{ wtf.quick_form(form, novalidate=True) }} +
    +
    + +{% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/ajout_historique.html b/app/templates/entreprises/ajout_historique.html new file mode 100644 index 000000000..f4da2661b --- /dev/null +++ b/app/templates/entreprises/ajout_historique.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block styles %} +{{super()}} + + +{% endblock %} + +{% block app_content %} +

    {{ title }}

    +
    +
    +
    + {{ wtf.quick_form(form, novalidate=True) }} +
    +
    + +{% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/envoi_offre_form.html b/app/templates/entreprises/envoi_offre_form.html new file mode 100644 index 000000000..c9db2c6c1 --- /dev/null +++ b/app/templates/entreprises/envoi_offre_form.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block styles %} +{{super()}} + + +{% endblock %} + +{% block app_content %} +

    {{ title }}

    +
    +
    +
    + {{ wtf.quick_form(form, novalidate=True) }} +
    +
    + +{% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/form.html b/app/templates/entreprises/form.html index 25cb579dd..26add23e1 100644 --- a/app/templates/entreprises/form.html +++ b/app/templates/entreprises/form.html @@ -15,61 +15,4 @@ {{ wtf.quick_form(form, novalidate=True) }} - {% endblock %} \ No newline at end of file From 9477106e9a4547c2bd5a332028d8deb4bcd678be Mon Sep 17 00:00:00 2001 From: Arthur ZHU Date: Fri, 24 Dec 2021 12:12:41 +0100 Subject: [PATCH 09/27] =?UTF-8?q?empeche=20les=20doublons=20sur=20contact?= =?UTF-8?q?=20dans=20une=20m=C3=AAme=20entreprise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entreprises/forms.py | 23 +++++++++++++++++-- app/entreprises/routes.py | 2 +- .../entreprises/ajout_entreprise.html | 3 +++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/entreprises/forms.py b/app/entreprises/forms.py index f7fd62816..d2f3762b6 100644 --- a/app/entreprises/forms.py +++ b/app/entreprises/forms.py @@ -3,10 +3,10 @@ from markupsafe import Markup from flask.app import Flask import requests, re from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, TextAreaField, SelectField, FileField +from wtforms import StringField, SubmitField, TextAreaField, SelectField, FileField, HiddenField from wtforms.fields.html5 import EmailField, DateField from wtforms.validators import ValidationError, DataRequired, Email -from app.entreprises.models import Entreprise +from app.entreprises.models import Entreprise, EntrepriseContact from app.models import Identite from app.auth.models import User from app.scodoc import sco_etud @@ -71,8 +71,27 @@ class ContactCreationForm(FlaskForm): prenom = StringField("Prénom", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) telephone = StringField("Téléphone", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) mail = EmailField("Mail", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE), Email(message="Adresse e-mail invalide")]) + hidden_entreprise_id = HiddenField() submit = SubmitField("Envoyer") + def validate(self): + rv = FlaskForm.validate(self) + if not rv: + return 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("") + return False + + return True + class ContactModificationForm(FlaskForm): nom = StringField("Nom", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) prenom = StringField("Prénom", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index 4808cbebe..1791199e5 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -238,7 +238,7 @@ def delete_offre(id): @bp.route("/add_contact/", methods=["GET", "POST"]) def add_contact(id): entreprise = Entreprise.query.filter_by(id=id).first_or_404() - form = ContactCreationForm() + form = ContactCreationForm(hidden_entreprise_id=entreprise.id) if form.validate_on_submit(): contact = EntrepriseContact( entreprise_id=entreprise.id, diff --git a/app/templates/entreprises/ajout_entreprise.html b/app/templates/entreprises/ajout_entreprise.html index 76a9619e4..6b0ba311e 100644 --- a/app/templates/entreprises/ajout_entreprise.html +++ b/app/templates/entreprises/ajout_entreprise.html @@ -12,6 +12,9 @@
    +

    + Les champs s'autocomplète selon le SIRET +

    {{ wtf.quick_form(form, novalidate=True) }}
    From 178016b682c3767754454f0f44f02b4b78cb6e57 Mon Sep 17 00:00:00 2001 From: Arthur ZHU Date: Fri, 24 Dec 2021 16:07:36 +0100 Subject: [PATCH 10/27] =?UTF-8?q?ajout=20page=20offres,=20insertion=20en?= =?UTF-8?q?=20base=20des=20diff=C3=A9rents=20objets,=20enrichissement=20de?= =?UTF-8?q?s=20champs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entreprises/forms.py | 4 +++- app/entreprises/routes.py | 32 +++++++++++++++++++++---- app/templates/entreprises/contacts.html | 1 - app/templates/entreprises/offres.html | 28 ++++++++++++++++++++++ 4 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 app/templates/entreprises/offres.html diff --git a/app/entreprises/forms.py b/app/entreprises/forms.py index d2f3762b6..502db347d 100644 --- a/app/entreprises/forms.py +++ b/app/entreprises/forms.py @@ -67,11 +67,13 @@ class OffreModificationForm(FlaskForm): submit = SubmitField("Modifier") class ContactCreationForm(FlaskForm): + hidden_entreprise_id = HiddenField() nom = StringField("Nom", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) prenom = StringField("Prénom", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) telephone = StringField("Téléphone", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) mail = EmailField("Mail", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE), Email(message="Adresse e-mail invalide")]) - hidden_entreprise_id = HiddenField() + poste = StringField("Poste", validators=[]) + service = StringField("Service", validators=[]) submit = SubmitField("Envoyer") def validate(self): diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index 1791199e5..7570c806d 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -21,7 +21,8 @@ from app.entreprises.models import ( EntrepriseOffre, EntrepriseContact, EntrepriseLog, - EntrepriseEtudiant + EntrepriseEtudiant, + EntrepriseEnvoiOffre ) from app.models import ( Identite @@ -52,10 +53,16 @@ def fiche_entreprise(id): contacts = entreprise.contacts 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_by(entreprise_id=id).\ + filter(EntrepriseEtudiant.entreprise_id == id).\ join(Identite, Identite.id == EntrepriseEtudiant.etudid).all() return render_template("entreprises/fiche_entreprise.html", title=("Fiche entreprise"), entreprise=entreprise, contacts=contacts, offres=offres, logs=logs, historique=historique) +@bp.route("/offres", methods=["GET"]) +def offres(): + offres_recus = db.session.query(EntrepriseEnvoiOffre, EntrepriseOffre).filter(EntrepriseEnvoiOffre.user_id == current_user.id).\ + join(EntrepriseOffre, EntrepriseOffre.id == EntrepriseEnvoiOffre.offre_id).all() + return render_template("entreprises/offres.html", title=("Offres"), offres_recus=offres_recus) + @bp.route("/add_entreprise", methods=["GET", "POST"]) def add_entreprise(): form = EntrepriseCreationForm() @@ -245,7 +252,9 @@ def add_contact(id): nom=form.nom.data.strip(), prenom=form.prenom.data.strip(), telephone=form.telephone.data.strip(), - mail=form.mail.data.strip() + mail=form.mail.data.strip(), + poste=form.poste.data.strip(), + service=form.service.data.strip() ) log = EntrepriseLog( authenticated_user = current_user.user_name, @@ -268,6 +277,8 @@ def edit_contact(id): 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, @@ -282,6 +293,8 @@ def edit_contact(id): 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 return render_template("entreprises/form.html", title=("Modification contact"), form=form) @bp.route("/delete_contact/", methods=["GET", "POST"]) @@ -335,9 +348,20 @@ def add_historique(id): @bp.route("/envoyer_offre/", methods=["GET", "POST"]) def envoyer_offre(id): + offre = EntrepriseOffre.query.filter_by(id=id).first_or_404() form = EnvoiOffreForm() if form.validate_on_submit(): - print("tmp") # faire l'envoie du mail + responsable_data = form.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() + envoi_offre = EntrepriseEnvoiOffre( + user_id = responsable.id, + offre_id = offre.id + ) + db.session.add(envoi_offre) + db.session.commit() + flash(f"L'offre a été envoyé à {responsable.get_nomplogin()}.") + return redirect(url_for("entreprises.fiche_entreprise", id=offre.entreprise_id)) return render_template("entreprises/envoi_offre_form.html", title=("Envoyer une offre"), form=form) @bp.route("/etudiants") diff --git a/app/templates/entreprises/contacts.html b/app/templates/entreprises/contacts.html index 7f1433334..fdf2a747b 100644 --- a/app/templates/entreprises/contacts.html +++ b/app/templates/entreprises/contacts.html @@ -36,7 +36,6 @@
    {% else %}
    Aucun contact présent dans la base
    -
    {% endif %} diff --git a/app/templates/entreprises/offres.html b/app/templates/entreprises/offres.html new file mode 100644 index 000000000..717364b9b --- /dev/null +++ b/app/templates/entreprises/offres.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block app_content %} +
    +

    {{ title }}

    + {% if offres_recus %} +
    +
    + {% for offre in offres_recus %} +
    +

    + Date envoi : {{ offre[0].date_envoi.strftime('%d/%m/%Y %H:%M') }}
    + Intitulé : {{ offre[1].intitule }}
    + Description : {{ offre[1].description }}
    + Type de l'offre : {{ offre[1].type_offre }}
    + Missions : {{ offre[1].missions }}
    + Durée : {{ offre[1].duree }}
    +

    +
    + {% endfor %} +
    +
    + {% else %} +
    Aucune offre reçue
    +
    + {% endif %} +
    +{% endblock %} \ No newline at end of file From 2a6d63cf03db2002d849e1527607f4940feffe99 Mon Sep 17 00:00:00 2001 From: Arthur ZHU Date: Fri, 24 Dec 2021 18:10:19 +0100 Subject: [PATCH 11/27] export liste entreprises --- app/entreprises/models.py | 9 +++++++ app/entreprises/routes.py | 28 ++++++++++++++++++---- app/templates/entreprises/entreprises.html | 7 +++++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/app/entreprises/models.py b/app/entreprises/models.py index ec963be20..d88a88127 100644 --- a/app/entreprises/models.py +++ b/app/entreprises/models.py @@ -12,6 +12,15 @@ class Entreprise(db.Model): contacts = db.relationship('EntrepriseContact', backref='entreprise', lazy='dynamic', cascade="all, delete-orphan") offres = db.relationship('EntrepriseOffre', backref='entreprise', lazy='dynamic', cascade="all, delete-orphan") + def to_dict(self): + return { + "siret": self.siret, + "nom": self.nom, + "adresse": self.adresse, + "codepostal": self.codepostal, + "pays": self.pays + } + class EntrepriseContact(db.Model): __tablename__ = "entreprise_contact" id = db.Column(db.Integer, primary_key=True) diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index 7570c806d..118e7e032 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -1,9 +1,10 @@ -from flask import render_template, redirect, url_for, request, flash +from flask import render_template, redirect, url_for, request, flash, send_file from flask.json import jsonify from flask_login import current_user from app.decorators import permission_required -from app.entreprises import LOGS_LEN + from app.scodoc.sco_permissions import Permission +from app.entreprises import LOGS_LEN from app.entreprises.forms import ( EntrepriseCreationForm, EntrepriseModificationForm, @@ -30,7 +31,8 @@ from app.models import ( from app.auth.models import User from app.scodoc.sco_find_etud import search_etud_by_name from app import db -from app.scodoc import sco_etud +from app.scodoc import sco_etud, sco_excel +import app.scodoc.sco_utils as scu from sqlalchemy import text @bp.route("/", methods=["GET"]) @@ -402,4 +404,22 @@ def json_responsables(): } list.append(content) content = {} - return jsonify(results=list) \ No newline at end of file + return jsonify(results=list) + +@bp.route("/export_entreprises") +def export_entreprises(): + entreprises = Entreprise.query.all() + keys=[ + "siret", + "nom", + "adresse", + "ville", + "codepostal", + "pays" + ] + titles = keys[:] + L = [[entreprise.to_dict().get(k, "") for k in keys] for entreprise in entreprises] + title = "entreprises" + 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) \ No newline at end of file diff --git a/app/templates/entreprises/entreprises.html b/app/templates/entreprises/entreprises.html index a33dd7e77..62d1fc11a 100644 --- a/app/templates/entreprises/entreprises.html +++ b/app/templates/entreprises/entreprises.html @@ -53,6 +53,11 @@
    {% endif %} - Ajouter une entreprise +
    + Ajouter une entreprise + {% if entreprises %} + Exporter la liste des entreprises + {% endif %} +
    {% endblock %} \ No newline at end of file From 0fe5cdb4099b41585273fbf4c3e77d7c4bf30eef Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 26 Dec 2021 19:15:47 +0100 Subject: [PATCH 12/27] WIP: refactoring calculs --- app/but/bulletin_but.py | 54 ++--- app/but/bulletin_but_xml_compat.py | 10 +- app/comp/aux.py | 37 +++ app/comp/df_cache.py | 2 +- app/comp/moy_mod.py | 374 ++++++++++++++++------------- app/comp/moy_ue.py | 28 +-- app/comp/res_sem.py | 77 +++--- app/models/etudiants.py | 81 ++++++- app/models/moduleimpls.py | 4 +- app/scodoc/notes_table.py | 10 +- app/scodoc/sco_abs_views.py | 42 ++-- app/scodoc/sco_apogee_csv.py | 4 +- app/scodoc/sco_bulletins.py | 4 +- app/scodoc/sco_bulletins_json.py | 2 +- app/scodoc/sco_bulletins_xml.py | 2 +- app/scodoc/sco_etud.py | 37 +-- app/scodoc/sco_liste_notes.py | 2 +- app/scodoc/sco_page_etud.py | 3 +- app/scodoc/sco_poursuite_dut.py | 2 +- app/scodoc/sco_recapcomplet.py | 6 +- app/scodoc/sco_tag_module.py | 2 +- app/templates/sidebar.html | 92 ++++--- app/views/absences.py | 4 +- app/views/scolar.py | 5 +- pylintrc | 2 + sco_version.py | 2 +- tests/unit/test_but_modules.py | 135 +++++------ tests/unit/test_but_ues.py | 4 +- 28 files changed, 580 insertions(+), 447 deletions(-) create mode 100644 app/comp/aux.py diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 65756767f..658f8f634 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -4,19 +4,17 @@ # See LICENSE ############################################################################## +"""Génération bulletin BUT +""" + import datetime from flask import url_for, g -import numpy as np -import pandas as pd - -from app import db from app.comp import moy_ue, moy_sem, inscr_mod -from app.models import ModuleImpl from app.scodoc import sco_utils as scu from app.scodoc import sco_bulletins_json from app.scodoc import sco_preferences -from app.scodoc.sco_utils import jsnan, fmt_note +from app.scodoc.sco_utils import fmt_note from app.comp.res_sem import ResultatsSemestre, NotesTableCompat @@ -37,9 +35,7 @@ class ResultatsSemestreBUT(NotesTableCompat): ( self.sem_cube, self.modimpls_evals_poids, - self.modimpls_evals_notes, - modimpls_evaluations, - self.modimpls_evaluations_complete, + self.modimpls_results, ) = moy_ue.notes_sem_load_cube(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs( @@ -74,16 +70,16 @@ class BulletinBUT(ResultatsSemestreBUT): etud_idx = self.etud_index[etud.id] ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id) etud_moy_module = self.sem_cube[etud_idx] # module x UE - for mi in modimpls: - coef = self.modimpl_coefs_df[mi.id][ue.id] + for modimpl in modimpls: + coef = self.modimpl_coefs_df[modimpl.id][ue.id] if coef > 0: - d[mi.module.code] = { - "id": mi.id, + d[modimpl.module.code] = { + "id": modimpl.id, "coef": coef, "moyenne": fmt_note( - etud_moy_module[self.modimpl_coefs_df.columns.get_loc(mi.id)][ - ue_idx - ] + etud_moy_module[ + self.modimpl_coefs_df.columns.get_loc(modimpl.id) + ][ue_idx] ), } return d @@ -117,7 +113,7 @@ class BulletinBUT(ResultatsSemestreBUT): avec évaluations de chacun.""" d = {} # etud_idx = self.etud_index[etud.id] - for mi in modimpls: + for modimpl in modimpls: # mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id) # # moyennes indicatives (moyennes de moyennes d'UE) # try: @@ -131,14 +127,15 @@ class BulletinBUT(ResultatsSemestreBUT): # moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx]) # except RuntimeWarning: # all nans in np.nanmean # pass - d[mi.module.code] = { - "id": mi.id, - "titre": mi.module.titre, - "code_apogee": mi.module.code_apogee, + modimpl_results = self.modimpls_results[modimpl.id] + d[modimpl.module.code] = { + "id": modimpl.id, + "titre": modimpl.module.titre, + "code_apogee": modimpl.module.code_apogee, "url": url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, - moduleimpl_id=mi.id, + moduleimpl_id=modimpl.id, ), "moyenne": { # # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan) @@ -149,16 +146,17 @@ class BulletinBUT(ResultatsSemestreBUT): }, "evaluations": [ self.etud_eval_results(etud, e) - for eidx, e in enumerate(mi.evaluations) + for e in modimpl.evaluations if e.visibulletin - and self.modimpls_evaluations_complete[mi.id][eidx] + and modimpl_results.evaluations_etat[e.id].is_complete ], } return d def etud_eval_results(self, etud, e) -> dict: "dict resultats d'un étudiant à une évaluation" - eval_notes = self.modimpls_evals_notes[e.moduleimpl_id][e.id] # pd.Series + # eval_notes est une pd.Series avec toutes les notes des étudiants inscrits + eval_notes = self.modimpls_results[e.moduleimpl_id].evals_notes[e.id] notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna() d = { "id": e.id, @@ -170,7 +168,7 @@ class BulletinBUT(ResultatsSemestreBUT): "poids": {p.ue.acronyme: p.poids for p in e.ue_poids}, "note": { "value": fmt_note( - self.modimpls_evals_notes[e.moduleimpl_id][e.id][etud.id], + eval_notes[etud.id], note_max=e.note_max, ), "min": fmt_note(notes_ok.min()), @@ -212,8 +210,8 @@ class BulletinBUT(ResultatsSemestreBUT): "numero": formsemestre.semestre_id, "groupes": [], # XXX TODO "absences": { # XXX TODO - "injustifie": 1, - "total": 33, + "injustifie": -1, + "total": -1, }, } semestre_infos.update( diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py index 6e2f14dbf..478141cbb 100644 --- a/app/but/bulletin_but_xml_compat.py +++ b/app/but/bulletin_but_xml_compat.py @@ -108,8 +108,8 @@ def bulletin_but_xml_compat( code_ine=etud.code_ine or "", nom=scu.quote_xml_attr(etud.nom), prenom=scu.quote_xml_attr(etud.prenom), - civilite=scu.quote_xml_attr(etud.civilite_str()), - sexe=scu.quote_xml_attr(etud.civilite_str()), # compat + civilite=scu.quote_xml_attr(etud.civilite_str), + sexe=scu.quote_xml_attr(etud.civilite_str), # compat photo_url=scu.quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)), email=scu.quote_xml_attr(etud.get_first_email() or ""), emailperso=scu.quote_xml_attr(etud.get_first_email("emailperso") or ""), @@ -216,9 +216,9 @@ def bulletin_but_xml_compat( Element( "note", value=scu.fmt_note( - results.modimpls_evals_notes[e.moduleimpl_id][ - e.id - ][etud.id] + results.modimpls_results[ + e.moduleimpl_id + ].evals_notes[e.id][etud.id] ), ) ) diff --git a/app/comp/aux.py b/app/comp/aux.py new file mode 100644 index 000000000..46285a4b1 --- /dev/null +++ b/app/comp/aux.py @@ -0,0 +1,37 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +import numpy as np + +"""Quelques classes auxiliaires pour les calculs des notes +""" + + +class StatsMoyenne: + """Une moyenne d'un ensemble étudiants sur quelque chose + (moyenne générale d'un semestre, d'un module, d'un groupe...) + et les statistiques associées: min, max, moy, effectif + """ + + def __init__(self, vals): + """Calcul les statistiques. + Les valeurs NAN ou non numériques sont toujours enlevées. + """ + self.moy = np.nanmean(vals) + self.min = np.nanmin(vals) + self.max = np.nanmax(vals) + self.size = len(vals) + self.nb_vals = self.size - np.count_nonzero(np.isnan(vals)) + + def to_dict(self): + "Tous les attributs dans un dict" + return { + "min": self.min, + "max": self.max, + "moy": self.moy, + "size": self.size, + "nb_vals": self.nb_vals, + } diff --git a/app/comp/df_cache.py b/app/comp/df_cache.py index dec325e2a..67b59242f 100644 --- a/app/comp/df_cache.py +++ b/app/comp/df_cache.py @@ -43,7 +43,7 @@ class ModuleCoefsCache(sco_cache.ScoDocCache): class EvaluationsPoidsCache(sco_cache.ScoDocCache): """Cache for poids evals Clé: moduleimpl_id - Valeur: DataFrame (df_load_evaluations_poids) + Valeur: DataFrame (load_evaluations_poids) """ prefix = "EPC" diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 40616cbab..dc912c286 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -31,17 +31,217 @@ Rappel: pour éviter les confusions, on appelera *poids* les coefficients d'une évaluation dans un module, et *coefficients* ceux utilisés pour le calcul de la moyenne générale d'une UE. """ +from dataclasses import dataclass import numpy as np import pandas as pd -from pandas.core.frame import DataFrame from app import db -from app import models from app.models import ModuleImpl, Evaluation, EvaluationUEPoids from app.scodoc import sco_utils as scu -def df_load_evaluations_poids( +@dataclass +class EvaluationEtat: + """Classe pour stocker quelques infos sur les résultats d'une évaluation""" + + evaluation_id: int + nb_attente: int + is_complete: bool + + +class ModuleImplResultsAPC: + """Les notes des étudiants d'un moduleimpl. + Les poids des évals sont à part car on a a besoin sans les notes pour les tableaux + de bord. + Les attributs sont tous des objets simples cachables dans Redis; + les caches sont gérés par ResultatsSemestre. + """ + + def __init__(self, moduleimpl: ModuleImpl): + self.moduleimpl_id = moduleimpl.id + self.module_id = moduleimpl.module.id + self.etudids = None + "liste des étudiants inscrits au SEMESTRE" + self.nb_inscrits_module = None + "nombre d'inscrits (non DEM) au module" + self.evaluations_completes = [] + "séquence de booléens, indiquant les évals à prendre en compte." + self.evaluations_etat = {} + "{ evaluation_id: EvaluationEtat }" + # + self.evals_notes = None + """DataFrame, colonnes: EVALS, Lignes: etudid + valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE, + NOTES_ABSENCE. + Les NaN désignent les notes manquantes (non saisies). + """ + self.etuds_moy_module = None + """DataFrame, colonnes UE, lignes etud + = la note de l'étudiant dans chaque UE pour ce module. + ou NaN si les évaluations (dans lesquelles l'étudiant a des notes) + ne donnent pas de coef vers cette UE. + """ + self.load_notes() + + def load_notes(self): # ré-écriture de df_load_modimpl_notes + """Charge toutes les notes de toutes les évaluations du module. + Dataframe evals_notes + colonnes: le nom de la colonne est l'evaluation_id (int) + index (lignes): etudid (int) + + L'ensemble des étudiants est celui des inscrits au SEMESTRE. + + Les notes sont "brutes" (séries de floats) et peuvent prendre les valeurs: + note : float (valeur enregistrée brute, NON normalisée sur 20) + pas de note: NaN (rien en bd, ou étudiant non inscrit au module) + absent: NOTES_ABSENCE (NULL en bd) + excusé: NOTES_NEUTRALISE (voir sco_utils) + attente: NOTES_ATTENTE + + Évaluation "complete" (prise en compte dans les calculs) si: + - soit tous les étudiants inscrits au module ont des notes + - soit elle a été déclarée "à prise ne compte immédiate" (publish_incomplete) + + Évaluation "attente" (prise en compte dans les calculs, mais il y + manque des notes) ssi il y a des étudiants inscrits au semestre et au module + qui ont des notes ATT. + """ + moduleimpl = ModuleImpl.query.get(self.moduleimpl_id) + self.etudids = self._etudids() + + # --- Calcul nombre d'inscrits pour déterminer les évaluations "completes": + # on prend les inscrits au module ET au semestre (donc sans démissionnaires) + inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection( + self.etudids + ) + self.nb_inscrits_module = len(inscrits_module) + + # dataFrame vide, index = tous les inscrits au SEMESTRE + evals_notes = pd.DataFrame(index=self.etudids, dtype=float) + self.evaluations_completes = [] + for evaluation in moduleimpl.evaluations: + eval_df = self._load_evaluation_notes(evaluation) + # is_complete ssi tous les inscrits (non dem) au semestre ont une note + # ou évaluaton déclarée "à prise en compte immédiate" + is_complete = ( + len(set(eval_df.index).intersection(self.etudids)) + == self.nb_inscrits_module + ) or evaluation.publish_incomplete # immédiate + self.evaluations_completes.append(is_complete) + + # NULL en base => ABS (= -999) + eval_df.fillna(scu.NOTES_ABSENCE, inplace=True) + # Ce merge ne garde que les étudiants inscrits au module + # et met à NULL les notes non présentes + # (notes non saisies ou etuds non inscrits au module): + evals_notes = evals_notes.merge( + eval_df, how="left", left_index=True, right_index=True + ) + # Notes en attente: (on prend dans evals_notes pour ne pas avoir les dem.) + nb_att = sum(evals_notes[str(evaluation.id)] == scu.NOTES_ATTENTE) + self.evaluations_etat[evaluation.id] = EvaluationEtat( + evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete + ) + + # Force columns names to integers (evaluation ids) + evals_notes.columns = pd.Int64Index( + [int(x) for x in evals_notes.columns], dtype="int" + ) + self.evals_notes = evals_notes + + def _load_evaluation_notes(self, evaluation: Evaluation) -> pd.DataFrame: + """Charge les notes de l'évaluation + Resultat: dataframe, index: etudid ayant une note, valeur: note brute. + """ + eval_df = pd.read_sql_query( + """SELECT n.etudid, n.value AS "%(evaluation_id)s" + FROM notes_notes n, notes_moduleimpl_inscription i + WHERE evaluation_id=%(evaluation_id)s + AND n.etudid = i.etudid + AND i.moduleimpl_id = %(moduleimpl_id)s + """, + db.engine, + params={ + "evaluation_id": evaluation.id, + "moduleimpl_id": evaluation.moduleimpl.id, + }, + index_col="etudid", + ) + eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)]) + return eval_df + + def _etudids(self): + """L'index du dataframe est la liste des étudiants inscrits au semestre, + sans les démissionnaires. + """ + return [ + e.etudid + for e in ModuleImpl.query.get(self.moduleimpl_id).formsemestre.get_inscrits( + include_dem=False + ) + ] + + def compute_module_moy( + self, + evals_poids_df: pd.DataFrame, + ) -> pd.DataFrame: + """Calcule les moyennes des étudiants dans ce module + + Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs + + Résultat: DataFrame, colonnes UE, lignes etud + = la note de l'étudiant dans chaque UE pour ce module. + ou NaN si les évaluations (dans lesquelles l'étudiant a des notes) + ne donnent pas de coef vers cette UE. + """ + moduleimpl = ModuleImpl.query.get(self.moduleimpl_id) + nb_etuds, nb_evals = self.evals_notes.shape + nb_ues = evals_poids_df.shape[1] + assert evals_poids_df.shape[0] == nb_evals # compat notes/poids + if nb_etuds == 0: + return pd.DataFrame(index=[], columns=evals_poids_df.columns) + # Coefficients des évaluations, met à zéro ceux des évals incomplètes: + evals_coefs = ( + np.array( + [e.coefficient for e in moduleimpl.evaluations], + dtype=float, + ) + * self.evaluations_completes + ).reshape(-1, 1) + evals_poids = evals_poids_df.values * evals_coefs + # -> evals_poids shape : (nb_evals, nb_ues) + assert evals_poids.shape == (nb_evals, nb_ues) + # Remplace les notes ATT, EXC, ABS, NaN par zéro et mets les notes sur 20: + evals_notes = np.where( + self.evals_notes.values > scu.NOTES_ABSENCE, self.evals_notes.values, 0.0 + ) / [e.note_max / 20.0 for e in moduleimpl.evaluations] + # Les poids des évals pour les étudiant: là où il a des notes non neutralisées + # (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui) + # Note: les NaN sont remplacés par des 0 dans evals_notes + # et dans dans evals_poids_etuds + # (rappel: la comparaison est toujours false face à un NaN) + # shape: (nb_etuds, nb_evals, nb_ues) + poids_stacked = np.stack([evals_poids] * nb_etuds) + evals_poids_etuds = np.where( + np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE, + poids_stacked, + 0, + ) + # Calcule la moyenne pondérée sur les notes disponibles: + evals_notes_stacked = np.stack([evals_notes] * nb_ues, axis=2) + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + etuds_moy_module = np.sum( + evals_poids_etuds * evals_notes_stacked, axis=1 + ) / np.sum(evals_poids_etuds, axis=1) + self.etuds_moy_module = pd.DataFrame( + etuds_moy_module, + index=self.evals_notes.index, + columns=evals_poids_df.columns, + ) + return self.etuds_moy_module + + +def load_evaluations_poids( moduleimpl_id: int, default_poids=1.0 ) -> tuple[pd.DataFrame, list]: """Charge poids des évaluations d'un module et retourne un dataframe @@ -55,23 +255,25 @@ def df_load_evaluations_poids( ues = modimpl.formsemestre.query_ues(with_sport=False).all() ue_ids = [ue.id for ue in ues] evaluation_ids = [evaluation.id for evaluation in evaluations] - df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float) - for eval_poids in EvaluationUEPoids.query.join( + evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float) + for ue_poids in EvaluationUEPoids.query.join( EvaluationUEPoids.evaluation ).filter_by(moduleimpl_id=moduleimpl_id): - df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids + evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids if default_poids is not None: - df.fillna(value=default_poids, inplace=True) - return df, ues + evals_poids.fillna(value=default_poids, inplace=True) + return evals_poids, ues -def check_moduleimpl_conformity( +def moduleimpl_is_conforme( moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame ) -> bool: """Vérifie que les évaluations de ce moduleimpl sont bien conformes au PN. Un module est dit *conforme* si et seulement si la somme des poids de ses évaluations vers une UE de coefficient non nul est non nulle. + + Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs """ nb_evals, nb_ues = evals_poids.shape if nb_evals == 0: @@ -79,160 +281,10 @@ def check_moduleimpl_conformity( if nb_ues == 0: return False # situation absurde (pas d'UE) if len(modules_coefficients) != nb_ues: - raise ValueError("check_moduleimpl_conformity: nb ue incoherent") + raise ValueError("moduleimpl_is_conforme: nb ue incoherent") module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0 check = all( - (modules_coefficients[moduleimpl.module.id].to_numpy() != 0) + (modules_coefficients[moduleimpl.module_id].to_numpy() != 0) == module_evals_poids ) return check - - -def df_load_modimpl_notes(moduleimpl_id: int) -> tuple: - """Construit un dataframe avec toutes les notes de toutes les évaluations du module. - colonnes: le nom de la colonne est l'evaluation_id (int) - index (lignes): etudid (int) - - Résultat: (evals_notes, liste de évaluations du moduleimpl, - liste de booleens indiquant si l'évaluation est "complete") - - L'ensemble des étudiants est celui des inscrits au SEMESTRE. - - Les notes renvoyées sont "brutes" (séries de floats) et peuvent prendre les valeurs: - note : float (valeur enregistrée brute, non normalisée sur 20) - pas de note: NaN (rien en bd, ou étudiant non inscrit au module) - absent: NOTES_ABSENCE (NULL en bd) - excusé: NOTES_NEUTRALISE (voir sco_utils) - attente: NOTES_ATTENTE - - L'évaluation "complete" (prise en compte dans les calculs) si: - - soit tous les étudiants inscrits au module ont des notes - - soit elle a été déclarée "à prise ne compte immédiate" (publish_incomplete) - - N'utilise pas de cache ScoDoc. - """ - # L'index du dataframe est la liste des étudiants inscrits au semestre, - # sans les démissionnaires - etudids = [ - e.etudid - for e in ModuleImpl.query.get(moduleimpl_id).formsemestre.get_inscrits( - include_dem=False - ) - ] - evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all() - # --- Calcul nombre d'inscrits pour détermnier si évaluation "complete": - if evaluations: - # on prend les inscrits au module ET au semestre (donc sans démissionnaires) - inscrits_module = { - ins.etud.id for ins in evaluations[0].moduleimpl.inscriptions - }.intersection(etudids) - nb_inscrits_module = len(inscrits_module) - else: - nb_inscrits_module = 0 - # empty df with all students: - evals_notes = pd.DataFrame(index=etudids, dtype=float) - evaluations_completes = [] - for evaluation in evaluations: - eval_df = pd.read_sql_query( - """SELECT n.etudid, n.value AS "%(evaluation_id)s" - FROM notes_notes n, notes_moduleimpl_inscription i - WHERE evaluation_id=%(evaluation_id)s - AND n.etudid = i.etudid - AND i.moduleimpl_id = %(moduleimpl_id)s - ORDER BY n.etudid - """, - db.engine, - params={ - "evaluation_id": evaluation.id, - "moduleimpl_id": evaluation.moduleimpl.id, - }, - index_col="etudid", - ) - eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)]) - # is_complete ssi tous les inscrits (non dem) au semestre ont une note - is_complete = ( - len(set(eval_df.index).intersection(etudids)) == nb_inscrits_module - ) or evaluation.publish_incomplete - evaluations_completes.append(is_complete) - # NULL en base => ABS (= -999) - eval_df.fillna(scu.NOTES_ABSENCE, inplace=True) - # Ce merge met à NULL les élements non présents - # (notes non saisies ou etuds non inscrits au module): - evals_notes = evals_notes.merge( - eval_df, how="left", left_index=True, right_index=True - ) - # Force columns names to integers (evaluation ids) - evals_notes.columns = pd.Int64Index( - [int(x) for x in evals_notes.columns], dtype="int64" - ) - return evals_notes, evaluations, evaluations_completes - - -def compute_module_moy( - evals_notes_df: pd.DataFrame, - evals_poids_df: pd.DataFrame, - evaluations: list, - evaluations_completes: list, -) -> pd.DataFrame: - """Calcule les moyennes des étudiants dans ce module - - - evals_notes : DataFrame, colonnes: EVALS, Lignes: etudid - valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE, - NOTES_ABSENCE. - Les NaN désignent les notes manquantes (non saisies). - - - evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs - - - evaluations: séquence d'évaluations (utilisées pour le coef et - le barème) - - - evaluations_completes: séquence de booléens indiquant les - évals à prendre en compte. - - Résultat: DataFrame, colonnes UE, lignes etud - = la note de l'étudiant dans chaque UE pour ce module. - ou NaN si les évaluations (dans lesquelles l'étudiant à des notes) - ne donnent pas de coef vers cette UE. - """ - nb_etuds, nb_evals = evals_notes_df.shape - nb_ues = evals_poids_df.shape[1] - assert evals_poids_df.shape[0] == nb_evals # compat notes/poids - if nb_etuds == 0: - return pd.DataFrame(index=[], columns=evals_poids_df.columns) - # Coefficients des évaluations, met à zéro ceux des évals incomplètes: - evals_coefs = ( - np.array( - [e.coefficient for e in evaluations], - dtype=float, - ) - * evaluations_completes - ).reshape(-1, 1) - evals_poids = evals_poids_df.values * evals_coefs - # -> evals_poids shape : (nb_evals, nb_ues) - assert evals_poids.shape == (nb_evals, nb_ues) - # Remplace les notes ATT, EXC, ABS, NaN par zéro et mets les notes sur 20: - evals_notes = np.where( - evals_notes_df.values > scu.NOTES_ABSENCE, evals_notes_df.values, 0.0 - ) / [e.note_max / 20.0 for e in evaluations] - # Les poids des évals pour les étudiant: là où il a des notes non neutralisées - # (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui) - # Note: les NaN sont remplacés par des 0 dans evals_notes - # et dans dans evals_poids_etuds - # (rappel: la comparaison est toujours false face à un NaN) - # shape: (nb_etuds, nb_evals, nb_ues) - poids_stacked = np.stack([evals_poids] * nb_etuds) - evals_poids_etuds = np.where( - np.stack([evals_notes_df.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE, - poids_stacked, - 0, - ) - # Calcule la moyenne pondérée sur les notes disponibles: - evals_notes_stacked = np.stack([evals_notes] * nb_ues, axis=2) - with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) - etuds_moy_module = np.sum( - evals_poids_etuds * evals_notes_stacked, axis=1 - ) / np.sum(evals_poids_etuds, axis=1) - etuds_moy_module_df = pd.DataFrame( - etuds_moy_module, index=evals_notes_df.index, columns=evals_poids_df.columns - ) - return etuds_moy_module_df diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 74994f26d..0c640fe93 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -34,7 +34,6 @@ from app import db from app import models from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef from app.comp import moy_mod -from app.models.formsemestre import FormSemestre from app.scodoc import sco_codes_parcours @@ -134,34 +133,21 @@ def notes_sem_load_cube(formsemestre): Resultat: sem_cube : ndarray (etuds x modimpls x UEs) modimpls_evals_poids dict { modimpl.id : evals_poids } - modimpls_evals_notes dict { modimpl.id : evals_notes } - modimpls_evaluations dict { modimpl.id : liste des évaluations } - modimpls_evaluations_complete: {modimpl_id : liste de booleens (complete/non)} + modimpls_results dict { modimpl.id : ModuleImplResultsAPC } """ + modimpls_results = {} modimpls_evals_poids = {} - modimpls_evals_notes = {} - modimpls_evaluations = {} - modimpls_evaluations_complete = {} modimpls_notes = [] for modimpl in formsemestre.modimpls: - evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes( - modimpl.id - ) - evals_poids, ues = moy_mod.df_load_evaluations_poids(modimpl.id) - etuds_moy_module = moy_mod.compute_module_moy( - evals_notes, evals_poids, evaluations, evaluations_completes - ) - modimpls_evals_poids[modimpl.id] = evals_poids - modimpls_evals_notes[modimpl.id] = evals_notes - modimpls_evaluations[modimpl.id] = evaluations - modimpls_evaluations_complete[modimpl.id] = evaluations_completes + mod_results = moy_mod.ModuleImplResultsAPC(modimpl) + evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id) + etuds_moy_module = mod_results.compute_module_moy(evals_poids) + modimpls_results[modimpl.id] = mod_results modimpls_notes.append(etuds_moy_module) return ( notes_sem_assemble_cube(modimpls_notes), modimpls_evals_poids, - modimpls_evals_notes, - modimpls_evaluations, - modimpls_evaluations_complete, + modimpls_results, ) diff --git a/app/comp/res_sem.py b/app/comp/res_sem.py index 01a2e8720..c0b78b539 100644 --- a/app/comp/res_sem.py +++ b/app/comp/res_sem.py @@ -8,6 +8,8 @@ from collections import defaultdict from functools import cached_property import numpy as np import pandas as pd +from app.comp.aux import StatsMoyenne +from app.models import ModuleImpl from app.scodoc import sco_utils as scu from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_codes_parcours import UE_SPORT @@ -28,14 +30,20 @@ class ResultatsSemestre: "modimpl_coefs_df", "etud_moy_ue", "modimpls_evals_poids", - "modimpls_evals_notes", + "modimpls_results", "etud_moy_gen", "etud_moy_gen_ranks", - "modimpls_evaluations_complete", ) def __init__(self, formsemestre): self.formsemestre = formsemestre + # BUT ou standard ? (apc == "approche par compétences") + self.is_apc = formsemestre.formation.is_apc() + # Attributs "virtuels", définis pas les sous-classes + # ResultatsSemestreBUT ou ResultatsSemestreStd + self.etud_moy_ue = {} + self.etud_moy_gen = {} + self.etud_moy_gen_ranks = {} # TODO def load_cached(self) -> bool: @@ -49,7 +57,6 @@ class ResultatsSemestre: def store(self): "Cache our data" - "Cache our dataframes" ResultatsSemestreCache.set( self.formsemestre.id, {attr: getattr(self, attr) for attr in self._cached_attrs}, @@ -58,7 +65,7 @@ class ResultatsSemestre: def compute(self): "Charge les notes et inscriptions et calcule toutes les moyennes" # voir ce qui est chargé / calculé ici et dans les sous-classes - TODO + raise NotImplementedError() @cached_property def etuds(self): @@ -78,9 +85,22 @@ class ResultatsSemestre: @cached_property def modimpls(self): - "Liste des modimpls du semestre (triée par numéro de module)" + """Liste des modimpls du semestre + - triée par numéro de module en APC + - triée par numéros d'UE/matières/modules pour les formations standard. + """ modimpls = self.formsemestre.modimpls.all() - modimpls.sort(key=lambda m: m.module.numero) + if self.is_apc: + modimpls.sort(key=lambda m: (m.module.numero, m.module.code)) + else: + modimpls.sort( + key=lambda m: ( + m.module.ue.numero, + m.module.matiere.numero, + m.module.numero, + m.module.code, + ) + ) return modimpls @cached_property @@ -96,32 +116,6 @@ class ResultatsSemestre: return [m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE] -class StatsMoyenne: - """Une moyenne d'un ensemble étudiants sur quelque chose - (moyenne générale d'un semestre, d'un module, d'un groupe...) - et les statistiques associées: min, max, moy, effectif - """ - - def __init__(self, vals): - """Calcul les statistiques. - Les valeurs NAN ou non numériques sont toujours enlevées. - """ - self.moy = np.nanmean(vals) - self.min = np.nanmin(vals) - self.max = np.nanmax(vals) - self.size = len(vals) - self.nb_vals = self.size - np.count_nonzero(np.isnan(vals)) - - def to_dict(self): - return { - "min": self.min, - "max": self.max, - "moy": self.moy, - "size": self.size, - "nb_vals": self.nb_vals, - } - - # Pour raccorder le code des anciens codes qui attendent une NoteTable class NotesTableCompat(ResultatsSemestre): """Implementation partielle de NotesTable WIP TODO @@ -158,11 +152,22 @@ class NotesTableCompat(ResultatsSemestre): ues.append(d) return ues - def get_modimpls(self): - return [m.to_dict() for m in self.results.modimpls] + def get_modimpls_dict(self, ue_id=None): + """Liste des modules pour une UE (ou toutes si ue_id==None), + triés par numéros (selon le type de formation) + """ + if ue_id is None: + return [m.to_dict() for m in self.modimpls] + else: + return [m.to_dict() for m in self.modimpls if m.module.ue.id == ue_id] - def get_etud_moy_gen(self, etudid): - return self.results.etud_moy_gen[etudid] + def get_etud_moy_gen(self, etudid): # -> float | str + """Moyenne générale de cet etudiant dans ce semestre. + Prend en compte les UE capitalisées. (TODO) + Si apc, moyenne indicative. + Si pas de notes: 'NA' + """ + return self.etud_moy_gen[etudid] def get_moduleimpls_attente(self): return [] # XXX TODO diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 0ae36bd28..220bf28be 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -4,7 +4,9 @@ et données rattachées (adresses, annotations, ...) """ -from flask import g, url_for +from functools import cached_property +from flask import abort, url_for +from flask import g, request from app import db from app import models @@ -53,14 +55,24 @@ class Identite(db.Model): def __repr__(self): return f"" + @classmethod + def from_request(cls, etudid=None, code_nip=None): + """Etudiant à partir de l'etudid ou du code_nip, soit + passés en argument soit retrouvés directement dans la requête web. + Erreur 404 si inexistant. + """ + args = make_etud_args(etudid=etudid, code_nip=code_nip) + return Identite.query.filter_by(**args).first_or_404() + + @property def civilite_str(self): """returns 'M.' ou 'Mme' ou '' (pour le genre neutre, personnes ne souhaitant pas d'affichage). """ return {"M": "M.", "F": "Mme", "X": ""}[self.civilite] - def nom_disp(self): - "nom à afficher" + def nom_disp(self) -> str: + "Nom à afficher" if self.nom_usuel: return ( (self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel @@ -68,8 +80,33 @@ class Identite(db.Model): else: return self.nom + @cached_property + def nomprenom(self, reverse=False) -> str: + """Civilité/nom/prenom pour affichages: "M. Pierre Dupont" + Si reverse, "Dupont Pierre", sans civilité. + """ + nom = self.nom_usuel or self.nom + prenom = self.prenom_str + if reverse: + fields = (nom, prenom) + else: + fields = (self.civilite_str, prenom, nom) + return " ".join([x for x in fields if x]) + + @property + def prenom_str(self): + """Prénom à afficher. Par exemple: "Jean-Christophe" """ + if not self.prenom: + return "" + frags = self.prenom.split() + r = [] + for frag in frags: + fields = frag.split("-") + r.append("-".join([x.lower().capitalize() for x in fields])) + return " ".join(r) + def get_first_email(self, field="email") -> str: - "le mail associé à la première adrese de l'étudiant, ou None" + "Le mail associé à la première adrese de l'étudiant, ou None" return self.adresses[0].email or None if self.adresses.count() > 0 else None def to_dict_bul(self, include_urls=True): @@ -120,6 +157,42 @@ class Identite(db.Model): return False +def make_etud_args( + etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True +) -> dict: + """forme args dict pour requete recherche etudiant + On peut specifier etudid + ou bien (si use_request) cherche dans la requete http: etudid, code_nip, code_ine + (dans cet ordre). + + Résultat: dict avec soit "etudid", soit "code_nip", soit "code_ine" + """ + args = None + if etudid: + args = {"etudid": etudid} + elif code_nip: + args = {"code_nip": code_nip} + elif use_request: # use form from current request (Flask global) + if request.method == "POST": + vals = request.form + elif request.method == "GET": + vals = request.args + else: + vals = {} + if "etudid" in vals: + args = {"etudid": int(vals["etudid"])} + elif "code_nip" in vals: + args = {"code_nip": str(vals["code_nip"])} + elif "code_ine" in vals: + args = {"code_ine": str(vals["code_ine"])} + if not args: + if abort_404: + abort(404, "pas d'étudiant sélectionné") + elif raise_exc: + raise ValueError("make_etud_args: pas d'étudiant sélectionné !") + return args + + class Adresse(db.Model): """Adresse d'un étudiant (le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule) diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 172fe0767..fe48555ef 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -50,7 +50,7 @@ class ModuleImpl(db.Model): if evaluations_poids is None: from app.comp import moy_mod - evaluations_poids, _ = moy_mod.df_load_evaluations_poids(self.id) + evaluations_poids, _ = moy_mod.load_evaluations_poids(self.id) df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids) return evaluations_poids @@ -69,7 +69,7 @@ class ModuleImpl(db.Model): return True from app.comp import moy_mod - return moy_mod.check_moduleimpl_conformity( + return moy_mod.moduleimpl_is_conforme( self, self.get_evaluations_poids(), self.module.formation.get_module_coefs(self.module.semestre_id), diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index 69c37694f..abbfec96b 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -296,7 +296,7 @@ class NotesTable: for ue in self._ues: is_cap[ue["ue_id"]] = ue_status[ue["ue_id"]]["is_capitalized"] - for modimpl in self.get_modimpls(): + for modimpl in self.get_modimpls_dict(): val = self.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid) if is_cap[modimpl["module"]["ue_id"]]: t.append("-c-") @@ -428,8 +428,8 @@ class NotesTable: else: return [ue for ue in self._ues if ue["type"] != UE_SPORT] - def get_modimpls(self, ue_id=None): - "liste des modules pour une UE (ou toutes si ue_id==None), triés par matières." + def get_modimpls_dict(self, ue_id=None): + "Liste des modules pour une UE (ou toutes si ue_id==None), triés par matières." if ue_id is None: r = self._modimpls else: @@ -564,7 +564,7 @@ class NotesTable: Si non inscrit, moy == 'NI' et sum_coefs==0 """ assert ue_id - modimpls = self.get_modimpls(ue_id) + modimpls = self.get_modimpls_dict(ue_id) nb_notes = 0 # dans cette UE sum_notes = 0.0 sum_coefs = 0.0 @@ -921,7 +921,7 @@ class NotesTable: return infos - def get_etud_moy_gen(self, etudid): + def get_etud_moy_gen(self, etudid): # -> float | str """Moyenne generale de cet etudiant dans ce semestre. Prend en compte les UE capitalisées. Si pas de notes: 'NA' diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py index c719df312..27de71d7d 100644 --- a/app/scodoc/sco_abs_views.py +++ b/app/scodoc/sco_abs_views.py @@ -32,6 +32,8 @@ import datetime from flask import url_for, g, request, abort +from app import log +from app.models import Identite import app.scodoc.sco_utils as scu from app.scodoc import notesdb as ndb from app.scodoc.scolog import logdb @@ -46,7 +48,6 @@ from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl from app.scodoc import sco_photos from app.scodoc import sco_preferences -from app import log from app.scodoc.sco_exceptions import ScoValueError @@ -71,8 +72,8 @@ def doSignaleAbsence( etudid: etudiant concerné. Si non spécifié, cherche dans les paramètres de la requête courante. """ - etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] - etudid = etud["etudid"] + etud = Identite.from_request(etudid) + if not moduleimpl_id: moduleimpl_id = None description_abs = description @@ -82,7 +83,7 @@ def doSignaleAbsence( for jour in dates: if demijournee == 2: sco_abs.add_absence( - etudid, + etud.id, jour, False, estjust, @@ -90,7 +91,7 @@ def doSignaleAbsence( moduleimpl_id, ) sco_abs.add_absence( - etudid, + etud.id, jour, True, estjust, @@ -100,7 +101,7 @@ def doSignaleAbsence( nbadded += 2 else: sco_abs.add_absence( - etudid, + etud.id, jour, demijournee, estjust, @@ -113,27 +114,27 @@ def doSignaleAbsence( J = "" else: J = "NON " - M = "" + indication_module = "" if moduleimpl_id and moduleimpl_id != "NULL": mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] formsemestre_id = mod["formsemestre_id"] nt = sco_cache.NotesTableCache.get(formsemestre_id) ues = nt.get_ues_stat_dict() for ue in ues: - modimpls = nt.get_modimpls(ue_id=ue["ue_id"]) + modimpls = nt.get_modimpls_dict(ue_id=ue["ue_id"]) for modimpl in modimpls: if modimpl["moduleimpl_id"] == moduleimpl_id: - M = "dans le module %s" % modimpl["module"]["code"] + indication_module = "dans le module %s" % modimpl["module"]["code"] H = [ html_sco_header.sco_header( - page_title="Signalement d'une absence pour %(nomprenom)s" % etud, + page_title=f"Signalement d'une absence pour {etud.nomprenom}", ), """

    Signalement d'absences

    """, ] if dates: H.append( """

    Ajout de %d absences %sjustifiées du %s au %s %s

    """ - % (nbadded, J, datedebut, datefin, M) + % (nbadded, J, datedebut, datefin, indication_module) ) else: H.append( @@ -142,11 +143,18 @@ def doSignaleAbsence( ) H.append( - """ -
    """ - % etud + f""" +
    + """ ) H.append(sco_find_etud.form_search_etud()) H.append(html_sco_header.sco_footer()) @@ -200,7 +208,7 @@ def SignaleAbsenceEtud(): # etudid implied menu_module += """""" for ue in ues: - modimpls = nt.get_modimpls(ue_id=ue["ue_id"]) + modimpls = nt.get_modimpls_dict(ue_id=ue["ue_id"]) for modimpl in modimpls: menu_module += ( """\n""" diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 94c2f86e0..74a9823dd 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -456,7 +456,7 @@ class ApoEtud(dict): return VOID_APO_RES # Elements Modules - modimpls = nt.get_modimpls() + modimpls = nt.get_modimpls_dict() module_code_found = False for modimpl in modimpls: if code in modimpl["module"]["code_apogee"].split(","): @@ -978,7 +978,7 @@ class ApoData(object): s.add(code) continue # associé à un module: - modimpls = nt.get_modimpls() + modimpls = nt.get_modimpls_dict() for modimpl in modimpls: if code in modimpl["module"]["code_apogee"].split(","): s.add(code) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 7990ead8d..0a3fd9598 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -219,7 +219,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): # --- Notes ues = nt.get_ues_stat_dict() - modimpls = nt.get_modimpls() + modimpls = nt.get_modimpls_dict() moy_gen = nt.get_etud_moy_gen(etudid) I["nb_inscrits"] = len(nt.etud_moy_gen_ranks) I["moy_gen"] = scu.fmt_note(moy_gen) @@ -352,7 +352,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): etudid, formsemestre_id, ue_status["capitalized_ue_id"], - nt_cap.get_modimpls(), + nt_cap.get_modimpls_dict(), nt_cap, version, ) diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 6900a450c..5300d6a26 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -154,7 +154,7 @@ def formsemestre_bulletinetud_published_dict( partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid) ues = nt.get_ues_stat_dict() - modimpls = nt.get_modimpls() + modimpls = nt.get_modimpls_dict() nbetuds = len(nt.etud_moy_gen_ranks) mg = scu.fmt_note(nt.get_etud_moy_gen(etudid)) if ( diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index a0d14a136..4e3f0fcee 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -152,7 +152,7 @@ def make_xml_formsemestre_bulletinetud( nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes ues = nt.get_ues_stat_dict() - modimpls = nt.get_modimpls() + modimpls = nt.get_modimpls_dict() nbetuds = len(nt.etud_moy_gen_ranks) mg = scu.fmt_note(nt.get_etud_moy_gen(etudid)) if ( diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index 7dc30c829..8771eced7 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -38,7 +38,7 @@ from flask_mail import Message from app import email from app import log - +from app.models.etudiants import make_etud_args import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc.sco_exceptions import ScoGenError, ScoValueError @@ -87,6 +87,8 @@ def force_uppercase(s): def format_nomprenom(etud, reverse=False): """Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont" Si reverse, "Dupont Pierre", sans civilité. + + DEPRECATED: utiliser Identite.nomprenom """ nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"] prenom = format_prenom(etud["prenom"]) @@ -99,7 +101,9 @@ def format_nomprenom(etud, reverse=False): def format_prenom(s): - "Formatte prenom etudiant pour affichage" + """Formatte prenom etudiant pour affichage + DEPRECATED: utiliser Identite.prenom_str + """ if not s: return "" frags = s.split() @@ -590,35 +594,6 @@ etudident_edit = _etudidentEditor.edit etudident_create = _etudidentEditor.create -def make_etud_args(etudid=None, code_nip=None, use_request=True, raise_exc=True): - """forme args dict pour requete recherche etudiant - On peut specifier etudid - ou bien (si use_request) cherche dans la requete http: etudid, code_nip, code_ine - (dans cet ordre). - """ - args = None - if etudid: - args = {"etudid": etudid} - elif code_nip: - args = {"code_nip": code_nip} - elif use_request: # use form from current request (Flask global) - if request.method == "POST": - vals = request.form - elif request.method == "GET": - vals = request.args - else: - vals = {} - if "etudid" in vals: - args = {"etudid": int(vals["etudid"])} - elif "code_nip" in vals: - args = {"code_nip": str(vals["code_nip"])} - elif "code_ine" in vals: - args = {"code_ine": str(vals["code_ine"])} - if not args and raise_exc: - raise ValueError("getEtudInfo: no parameter !") - return args - - def log_unknown_etud(): """Log request: cas ou getEtudInfo n'a pas ramene de resultat""" etud_args = make_etud_args(raise_exc=False) diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index 6da2e3f8c..3ac60a223 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -240,7 +240,7 @@ def _make_table_notes( if is_apc: modimpl = ModuleImpl.query.get(moduleimpl_id) is_conforme = modimpl.check_apc_conformity() - evals_poids, ues = moy_mod.df_load_evaluations_poids(moduleimpl_id) + evals_poids, ues = moy_mod.load_evaluations_poids(moduleimpl_id) if not ues: is_apc = False else: diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 573d79455..a16fab1e3 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -36,6 +36,7 @@ from flask_login import current_user import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log +from app.models.etudiants import make_etud_args from app.scodoc import html_sco_header from app.scodoc import htmlutils from app.scodoc import sco_archives_etud @@ -156,7 +157,7 @@ def ficheEtud(etudid=None): # la sidebar est differente s'il y a ou pas un etudid # voir html_sidebar.sidebar() g.etudid = etudid - args = sco_etud.make_etud_args(etudid=etudid) + args = make_etud_args(etudid=etudid) etuds = sco_etud.etudident_list(cnx, args) if not etuds: log("ficheEtud: etudid=%s request.args=%s" % (etudid, request.args)) diff --git a/app/scodoc/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py index 94772dfe9..8a36f685e 100644 --- a/app/scodoc/sco_poursuite_dut.py +++ b/app/scodoc/sco_poursuite_dut.py @@ -75,7 +75,7 @@ def etud_get_poursuite_info(sem, etud): ] # Moyennes et rang des modules - modimpls = nt.get_modimpls() # recupération des modules + modimpls = nt.get_modimpls_dict() # recupération des modules modules = [] rangs = [] for ue in ues: # on parcourt chaque UE diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index ddcdc9548..cfd3634ec 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -302,10 +302,8 @@ def make_formsemestre_recapcomplet( sem = sco_formsemestre.do_formsemestre_list( args={"formsemestre_id": formsemestre_id} )[0] - nt = sco_cache.NotesTableCache.get( - formsemestre_id - ) # > get_modimpls, get_ues_stat_dict, get_table_moyennes_triees, get_etud_decision_sem, get_etud_etat, get_etud_rang, get_nom_short, get_mod_stats, nt.moy_moy, get_etud_decision_sem, - modimpls = nt.get_modimpls() + nt = sco_cache.NotesTableCache.get(formsemestre_id) + modimpls = nt.get_modimpls_dict() ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport # if formsemestre.formation.is_apc(): diff --git a/app/scodoc/sco_tag_module.py b/app/scodoc/sco_tag_module.py index 50ec8f01c..88040957b 100644 --- a/app/scodoc/sco_tag_module.py +++ b/app/scodoc/sco_tag_module.py @@ -270,7 +270,7 @@ def get_etud_tagged_modules(etudid, tagname): R = [] for sem in etud["sems"]: nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"]) - modimpls = nt.get_modimpls() + modimpls = nt.get_modimpls_dict() for modimpl in modimpls: tags = module_tag_list(module_id=modimpl["module_id"]) if tagname in tags: diff --git a/app/templates/sidebar.html b/app/templates/sidebar.html index ba55e6886..3334dbb6a 100644 --- a/app/templates/sidebar.html +++ b/app/templates/sidebar.html @@ -3,96 +3,92 @@ \ No newline at end of file diff --git a/app/views/absences.py b/app/views/absences.py index edb2204cb..37753d671 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -431,7 +431,7 @@ def SignaleAbsenceGrHebdo( modimpls_list = [] ues = nt.get_ues_stat_dict() for ue in ues: - modimpls_list += nt.get_modimpls(ue_id=ue["ue_id"]) + modimpls_list += nt.get_modimpls_dict(ue_id=ue["ue_id"]) menu_module = "" for modimpl in modimpls_list: @@ -599,7 +599,7 @@ def SignaleAbsenceGrSemestre( modimpls_list = [] ues = nt.get_ues_stat_dict() for ue in ues: - modimpls_list += nt.get_modimpls(ue_id=ue["ue_id"]) + modimpls_list += nt.get_modimpls_dict(ue_id=ue["ue_id"]) menu_module = "" for modimpl in modimpls_list: diff --git a/app/views/scolar.py b/app/views/scolar.py index b83dbcace..f79c5798d 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -41,6 +41,7 @@ from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed from wtforms import SubmitField +from app import log from app.decorators import ( scodoc, scodoc7func, @@ -50,12 +51,12 @@ from app.decorators import ( login_required, ) from app.models.etudiants import Identite +from app.models.etudiants import make_etud_args from app.views import scolar_bp as bp import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb -from app import log from app.scodoc.scolog import logdb from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ( @@ -455,7 +456,7 @@ def etud_info(etudid=None, format="xml"): if not format in ("xml", "json"): raise ScoValueError("format demandé non supporté par cette fonction.") t0 = time.time() - args = sco_etud.make_etud_args(etudid=etudid) + args = make_etud_args(etudid=etudid) cnx = ndb.GetDBConnexion() etuds = sco_etud.etudident_list(cnx, args) if not etuds: diff --git a/pylintrc b/pylintrc index 057a85cd0..21454d07e 100644 --- a/pylintrc +++ b/pylintrc @@ -19,3 +19,5 @@ ignored-classes=Permission, # and thus existing member attributes cannot be deduced by static analysis). It # supports qualified module names, as well as Unix pattern matching. ignored-modules=entreprises + +good-names=d,e,f,i,j,k,t,u,v,x,y,z,H,F,ue \ No newline at end of file diff --git a/sco_version.py b/sco_version.py index bc906576b..84e9b0f57 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.16" +SCOVERSION = "9.2.0a" SCONAME = "ScoDoc" diff --git a/tests/unit/test_but_modules.py b/tests/unit/test_but_modules.py index a73c98bbc..07705c211 100644 --- a/tests/unit/test_but_modules.py +++ b/tests/unit/test_but_modules.py @@ -4,6 +4,8 @@ et calcul moyennes modules """ import numpy as np import pandas as pd +from app.models.modules import Module +from app.models.moduleimpls import ModuleImpl from tests.unit import setup from app import db @@ -135,70 +137,72 @@ def test_module_conformity(test_client): ) assert isinstance(modules_coefficients, pd.DataFrame) assert modules_coefficients.shape == (nb_ues, nb_mods) - evals_poids, ues = moy_mod.df_load_evaluations_poids(evaluation.moduleimpl_id) + evals_poids, ues = moy_mod.load_evaluations_poids(evaluation.moduleimpl_id) assert isinstance(evals_poids, pd.DataFrame) assert len(ues) == nb_ues assert all(evals_poids.dtypes == np.float64) assert evals_poids.shape == (nb_evals, nb_ues) - assert not moy_mod.check_moduleimpl_conformity( + assert not moy_mod.moduleimpl_is_conforme( evaluation.moduleimpl, evals_poids, modules_coefficients ) -def test_module_moy_elem(test_client): - """Vérification calcul moyenne d'un module - (notes entrées dans un DataFrame sans passer par ScoDoc) - """ - # Création de deux évaluations: - e1 = Evaluation(note_max=20.0, coefficient=1.0) - e2 = Evaluation(note_max=20.0, coefficient=1.0) - db.session.add(e1) - db.session.add(e2) - db.session.commit() - # Repris du notebook CalculNotesBUT.ipynb - data = [ # Les notes de chaque étudiant dans les 2 evals: - { - e1.id: 11.0, - e2.id: 16.0, - }, - { - e1.id: None, # une absence - e2.id: 17.0, - }, - { - e1.id: 13.0, - e2.id: NOTES_NEUTRALISE, # une abs EXC - }, - { - e1.id: 14.0, - e2.id: 19.0, - }, - { - e1.id: NOTES_ATTENTE, # une ATT (traitée comme EXC) - e2.id: None, # et une ABS - }, - ] - evals_notes_df = pd.DataFrame( - data, index=["etud1", "etud2", "etud3", "etud4", "etud5"] - ) - # Poids des évaluations (1 ligne / évaluation) - data = [ - {"UE1": 1, "UE2": 0, "UE3": 0}, - {"UE1": 2, "UE2": 5, "UE3": 0}, - ] - evals_poids_df = pd.DataFrame(data, index=[e1.id, e2.id], dtype=float) - evaluations = [e1, e2] - etuds_moy_module_df = moy_mod.compute_module_moy( - evals_notes_df.fillna(0.0), evals_poids_df, evaluations, [True, True] - ) - NAN = 666.0 # pour pouvoir comparer NaN et NaN (car NaN != NaN) - r = etuds_moy_module_df.fillna(NAN) - assert tuple(r.loc["etud1"]) == (14 + 1 / 3, 16.0, NAN) - assert tuple(r.loc["etud2"]) == (11 + 1 / 3, 17.0, NAN) - assert tuple(r.loc["etud3"]) == (13, NAN, NAN) - assert tuple(r.loc["etud4"]) == (17 + 1 / 3, 19, NAN) - assert tuple(r.loc["etud5"]) == (0.0, 0.0, NAN) - # note: les notes UE3 sont toutes NAN car les poids vers l'UE3 sont nuls +# En ScoDoc 9.2 test ne peut plus exister car compute_module_moy +# est maintenant incorporé dans la classe ModuleImplResultsAPC +# def test_module_moy_elem(test_client): +# """Vérification calcul moyenne d'un module +# (notes entrées dans un DataFrame sans passer par ScoDoc) +# """ +# # Création de deux évaluations: +# e1 = Evaluation(note_max=20.0, coefficient=1.0) +# e2 = Evaluation(note_max=20.0, coefficient=1.0) +# db.session.add(e1) +# db.session.add(e2) +# db.session.flush() +# # Repris du notebook CalculNotesBUT.ipynb +# data = [ # Les notes de chaque étudiant dans les 2 evals: +# { +# e1.id: 11.0, +# e2.id: 16.0, +# }, +# { +# e1.id: None, # une absence +# e2.id: 17.0, +# }, +# { +# e1.id: 13.0, +# e2.id: NOTES_NEUTRALISE, # une abs EXC +# }, +# { +# e1.id: 14.0, +# e2.id: 19.0, +# }, +# { +# e1.id: NOTES_ATTENTE, # une ATT (traitée comme EXC) +# e2.id: None, # et une ABS +# }, +# ] +# evals_notes_df = pd.DataFrame( +# data, index=["etud1", "etud2", "etud3", "etud4", "etud5"] +# ) +# # Poids des évaluations (1 ligne / évaluation) +# data = [ +# {"UE1": 1, "UE2": 0, "UE3": 0}, +# {"UE1": 2, "UE2": 5, "UE3": 0}, +# ] +# evals_poids_df = pd.DataFrame(data, index=[e1.id, e2.id], dtype=float) +# evaluations = [e1, e2] +# etuds_moy_module_df = moy_mod.compute_module_moy( +# evals_notes_df.fillna(0.0), evals_poids_df, evaluations, [True, True] +# ) +# NAN = 666.0 # pour pouvoir comparer NaN et NaN (car NaN != NaN) +# r = etuds_moy_module_df.fillna(NAN) +# assert tuple(r.loc["etud1"]) == (14 + 1 / 3, 16.0, NAN) +# assert tuple(r.loc["etud2"]) == (11 + 1 / 3, 17.0, NAN) +# assert tuple(r.loc["etud3"]) == (13, NAN, NAN) +# assert tuple(r.loc["etud4"]) == (17 + 1 / 3, 19, NAN) +# assert tuple(r.loc["etud5"]) == (0.0, 0.0, NAN) +# # note: les notes UE3 sont toutes NAN car les poids vers l'UE3 sont nuls def test_module_moy(test_client): @@ -237,7 +241,7 @@ def test_module_moy(test_client): nb_evals = models.Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).count() assert nb_evals == 2 nb_ues = 3 - + modimpl = ModuleImpl.query.get(moduleimpl_id) # --- Change les notes et recalcule les moyennes du module # (rappel: on a deux évaluations: evaluation1, evaluation2, et un seul étudiant) def change_notes(n1, n2): @@ -245,17 +249,14 @@ def test_module_moy(test_client): _ = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, n1)]) _ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)]) # Calcul de la moyenne du module - evals_poids, ues = moy_mod.df_load_evaluations_poids(moduleimpl_id) + evals_poids, ues = moy_mod.load_evaluations_poids(moduleimpl_id) assert evals_poids.shape == (nb_evals, nb_ues) - evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes( - moduleimpl_id - ) - assert evals_notes[evaluations[0].id].dtype == np.float64 - assert evaluation1.id == evaluations[0].id - assert evaluation2.id == evaluations[1].id - etuds_moy_module = moy_mod.compute_module_moy( - evals_notes, evals_poids, evaluations, evaluations_completes - ) + + mod_results = moy_mod.ModuleImplResultsAPC(modimpl) + evals_notes = mod_results.evals_notes + assert evals_notes[evaluation1.id].dtype == np.float64 + + etuds_moy_module = mod_results.compute_module_moy(evals_poids) return etuds_moy_module # --- Notes ordinaires: diff --git a/tests/unit/test_but_ues.py b/tests/unit/test_but_ues.py index d27463f40..947e2f11c 100644 --- a/tests/unit/test_but_ues.py +++ b/tests/unit/test_but_ues.py @@ -69,7 +69,7 @@ def test_ue_moy(test_client): _ = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, n1)]) _ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)]) # Recalcul des moyennes - sem_cube, _, _, _, _ = moy_ue.notes_sem_load_cube(formsemestre) + sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre) etuds = formsemestre.etuds.all() etud_moy_ue = moy_ue.compute_ue_moys( sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df @@ -112,7 +112,7 @@ def test_ue_moy(test_client): exception_raised = True assert exception_raised # Recalcule les notes: - sem_cube, _, _, _, _ = moy_ue.notes_sem_load_cube(formsemestre) + sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre) etuds = formsemestre.etuds.all() etud_moy_ue = moy_ue.compute_ue_moys( sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df From 5684f57ca5c0388ed4d7b580a460d66448a5af18 Mon Sep 17 00:00:00 2001 From: Arthur ZHU Date: Mon, 27 Dec 2021 11:48:58 +0100 Subject: [PATCH 13/27] export liste contacts --- app/entreprises/forms.py | 29 +++++---- app/entreprises/models.py | 17 ++++- app/entreprises/routes.py | 64 +++++++++++++------ app/templates/entreprises/contacts.html | 6 +- app/templates/entreprises/entreprises.html | 1 - ..._creation_table_relations_entreprrises.py} | 4 +- 6 files changed, 83 insertions(+), 38 deletions(-) rename migrations/versions/{f3b62d64efa3_creation_tables_relations_entreprises.py => 2dfafee725ae_creation_table_relations_entreprrises.py} (99%) diff --git a/app/entreprises/forms.py b/app/entreprises/forms.py index 502db347d..1965c9cba 100644 --- a/app/entreprises/forms.py +++ b/app/entreprises/forms.py @@ -1,8 +1,7 @@ from flask import flash -from markupsafe import Markup -from flask.app import Flask -import requests, re from flask_wtf import FlaskForm +from markupsafe import Markup +import requests, re from wtforms import StringField, SubmitField, TextAreaField, SelectField, FileField, HiddenField from wtforms.fields.html5 import EmailField, DateField from wtforms.validators import ValidationError, DataRequired, Email @@ -20,13 +19,15 @@ class EntrepriseCreationForm(FlaskForm): adresse = StringField("Adresse de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) codepostal = StringField("Code postal de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) ville = StringField("Ville de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) - pays = StringField("Pays de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) + pays = StringField("Pays de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)], render_kw={"style": "margin-bottom: 50px;"}) nom_contact = StringField("Nom du contact", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) prenom_contact = StringField("Prénom du contact", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) telephone = StringField("Téléphone du contact", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) mail = EmailField("Mail du contact", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE), Email(message="Adresse e-mail invalide")]) - submit = SubmitField("Envoyer") + poste = StringField("Poste du contact", validators=[]) + service = StringField("Service du contact", validators=[]) + submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"}) def validate_siret(self, siret): siret = siret.data.strip() @@ -47,7 +48,7 @@ class EntrepriseModificationForm(FlaskForm): codepostal = StringField("Code postal", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) ville = StringField("Ville", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) pays = StringField("Pays", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) - submit = SubmitField("Modifier") + submit = SubmitField("Modifier", render_kw={"style": "margin-bottom: 10px;"}) class OffreCreationForm(FlaskForm): intitule = StringField("Intitulé", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) @@ -56,7 +57,7 @@ class OffreCreationForm(FlaskForm): missions = TextAreaField("Missions", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) duree = StringField("Durée", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) ficher = FileField("Fichier", validators=[]) - submit = SubmitField("Envoyer") + submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"}) class OffreModificationForm(FlaskForm): intitule = StringField("Intitulé", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) @@ -64,7 +65,7 @@ class OffreModificationForm(FlaskForm): type_offre = SelectField("Type de l'offre", choices=[('Stage'), ('Alternance')], validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) missions = TextAreaField("Missions", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) duree = StringField("Durée", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) - submit = SubmitField("Modifier") + submit = SubmitField("Modifier", render_kw={"style": "margin-bottom: 10px;"}) class ContactCreationForm(FlaskForm): hidden_entreprise_id = HiddenField() @@ -74,7 +75,7 @@ class ContactCreationForm(FlaskForm): mail = EmailField("Mail", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE), Email(message="Adresse e-mail invalide")]) poste = StringField("Poste", validators=[]) service = StringField("Service", validators=[]) - submit = SubmitField("Envoyer") + submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"}) def validate(self): rv = FlaskForm.validate(self) @@ -99,14 +100,16 @@ class ContactModificationForm(FlaskForm): prenom = StringField("Prénom", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) telephone = StringField("Téléphone", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) mail = EmailField("Mail", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE), Email(message="Adresse e-mail invalide")]) - submit = SubmitField("Modifier") + poste = StringField("Poste", validators=[]) + service = StringField("Service", validators=[]) + submit = SubmitField("Modifier", render_kw={"style": "margin-bottom: 10px;"}) class HistoriqueCreationForm(FlaskForm): etudiant = StringField("Étudiant", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)], render_kw={"placeholder": "Tapez le nom de l'étudiant puis selectionnez"}) type_offre = SelectField("Type de l'offre", choices=[('Stage'), ('Alternance')], validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) date_debut = DateField("Date début", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) date_fin = DateField("Date fin", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) - submit = SubmitField("Envoyer") + submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"}) def validate(self): rv = FlaskForm.validate(self) @@ -128,7 +131,7 @@ class HistoriqueCreationForm(FlaskForm): class EnvoiOffreForm(FlaskForm): responsable = StringField("Responsable de formation", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) - submit = SubmitField("Envoyer") + submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"}) def validate_responsable(self, responsable): responsable_data = responsable.data.upper().strip() @@ -138,4 +141,4 @@ class EnvoiOffreForm(FlaskForm): raise ValidationError("Champ incorrect (selectionnez dans la liste)") class SuppressionConfirmationForm(FlaskForm): - submit = SubmitField("Supprimer") \ No newline at end of file + submit = SubmitField("Supprimer", render_kw={"style": "margin-bottom: 10px;"}) \ No newline at end of file diff --git a/app/entreprises/models.py b/app/entreprises/models.py index d88a88127..f31c11af8 100644 --- a/app/entreprises/models.py +++ b/app/entreprises/models.py @@ -12,30 +12,41 @@ class Entreprise(db.Model): contacts = db.relationship('EntrepriseContact', backref='entreprise', lazy='dynamic', cascade="all, delete-orphan") offres = db.relationship('EntrepriseOffre', backref='entreprise', lazy='dynamic', cascade="all, delete-orphan") - def to_dict(self): + def to_dict_export(self): return { "siret": self.siret, "nom": self.nom, "adresse": self.adresse, "codepostal": self.codepostal, + "ville": self.ville, "pays": self.pays } class EntrepriseContact(db.Model): __tablename__ = "entreprise_contact" id = db.Column(db.Integer, primary_key=True) - entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id")) + entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id", ondelete="cascade")) nom = db.Column(db.Text) prenom = db.Column(db.Text) telephone = db.Column(db.Text) mail = db.Column(db.Text) poste = db.Column(db.Text) service = db.Column(db.Text) + + def to_dict_export(self): + return { + "nom": self.nom, + "prenom": self.prenom, + "telephone": self.telephone, + "mail": self.mail, + "poste": self.poste, + "service": self.service + } class EntrepriseOffre(db.Model): __tablename__ = "entreprise_offre" id = db.Column(db.Integer, primary_key=True) - entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id")) + entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id", ondelete="cascade")) date_ajout = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) intitule = db.Column(db.Text) description = db.Column(db.Text) diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index 118e7e032..b85c9938f 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -1,9 +1,9 @@ -from flask import render_template, redirect, url_for, request, flash, send_file +from flask import render_template, redirect, url_for, request, flash, send_file, abort from flask.json import jsonify from flask_login import current_user + from app.decorators import permission_required -from app.scodoc.sco_permissions import Permission from app.entreprises import LOGS_LEN from app.entreprises.forms import ( EntrepriseCreationForm, @@ -30,9 +30,11 @@ from app.models import ( ) from app.auth.models import User from app.scodoc.sco_find_etud import search_etud_by_name -from app import db +from app.scodoc.sco_permissions import Permission from app.scodoc import sco_etud, sco_excel import app.scodoc.sco_utils as scu + +from app import db from sqlalchemy import text @bp.route("/", methods=["GET"]) @@ -85,7 +87,9 @@ def add_entreprise(): nom=form.nom_contact.data.strip(), prenom=form.prenom_contact.data.strip(), telephone=form.telephone.data.strip(), - mail=form.mail.data.strip() + mail=form.mail.data.strip(), + poste=form.poste.data.strip(), + service=form.service.data.strip() ) db.session.add(contact) nom_entreprise = f"{entreprise.nom}" @@ -409,17 +413,41 @@ def json_responsables(): @bp.route("/export_entreprises") def export_entreprises(): entreprises = Entreprise.query.all() - keys=[ - "siret", - "nom", - "adresse", - "ville", - "codepostal", - "pays" - ] - titles = keys[:] - L = [[entreprise.to_dict().get(k, "") for k in keys] for entreprise in entreprises] - title = "entreprises" - 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) \ No newline at end of file + if entreprises: + keys=[ + "siret", + "nom", + "adresse", + "ville", + "codepostal", + "pays" + ] + titles = keys[:] + L = [[entreprise.to_dict_export().get(k, "") for k in keys] for entreprise in entreprises] + title = "entreprises" + 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) + +@bp.route("/export_contacts") +def export_contacts(): + contacts = EntrepriseContact.query.all() + if contacts: + keys=[ + "nom", + "prenom", + "telephone", + "mail", + "poste", + "service" + ] + titles = keys[:] + L = [[contact.to_dict_export().get(k, "") for k in keys] for contact in contacts] + title = "contacts" + 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) \ No newline at end of file diff --git a/app/templates/entreprises/contacts.html b/app/templates/entreprises/contacts.html index fdf2a747b..87112710f 100644 --- a/app/templates/entreprises/contacts.html +++ b/app/templates/entreprises/contacts.html @@ -33,10 +33,14 @@
    {% endfor %}
    SIRET Nom
    {{ entreprise.siret }}{{ entreprise.nom }}{{ entreprise.nom }} {{ entreprise.adresse }} {{ entreprise.codepostal }} {{ entreprise.ville }}
    -
    {% else %}
    Aucun contact présent dans la base
    {% endif %} +
    + {% if contacts %} + Exporter la liste des contacts + {% endif %} +
    {% endblock %} \ No newline at end of file diff --git a/app/templates/entreprises/entreprises.html b/app/templates/entreprises/entreprises.html index 62d1fc11a..a52f75805 100644 --- a/app/templates/entreprises/entreprises.html +++ b/app/templates/entreprises/entreprises.html @@ -47,7 +47,6 @@ {% endfor %} -
    {% else %}
    Aucune entreprise présent dans la base

    diff --git a/migrations/versions/f3b62d64efa3_creation_tables_relations_entreprises.py b/migrations/versions/2dfafee725ae_creation_table_relations_entreprrises.py similarity index 99% rename from migrations/versions/f3b62d64efa3_creation_tables_relations_entreprises.py rename to migrations/versions/2dfafee725ae_creation_table_relations_entreprrises.py index 7d629e280..126147c27 100644 --- a/migrations/versions/f3b62d64efa3_creation_tables_relations_entreprises.py +++ b/migrations/versions/2dfafee725ae_creation_table_relations_entreprrises.py @@ -36,7 +36,7 @@ def upgrade(): 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.ForeignKeyConstraint(['entreprise_id'], ['entreprises.id'], ), + sa.ForeignKeyConstraint(['entreprise_id'], ['entreprises.id'], ondelete='cascade'), sa.PrimaryKeyConstraint('id') ) @@ -49,7 +49,7 @@ def upgrade(): sa.Column('type_offre', sa.Text(), nullable=True), sa.Column('missions', sa.Text(), nullable=True), sa.Column('duree', sa.Text(), nullable=True), - sa.ForeignKeyConstraint(['entreprise_id'], ['entreprises.id'], ), + sa.ForeignKeyConstraint(['entreprise_id'], ['entreprises.id'], ondelete='cascade'), sa.PrimaryKeyConstraint('id') ) From a7ef5d81c528f21cb05b13ac3a4e5761b9393672 Mon Sep 17 00:00:00 2001 From: Arthur ZHU Date: Mon, 27 Dec 2021 19:00:38 +0100 Subject: [PATCH 14/27] =?UTF-8?q?attacher=20un=20fichier=20(pdf,=20docx)?= =?UTF-8?q?=20=C3=A0=20une=20offre=20(stock=C3=A9s=20sur=20disque)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entreprises/forms.py | 7 +-- app/entreprises/models.py | 1 + app/entreprises/routes.py | 45 ++++++++++++++----- app/templates/entreprises/_offre.html | 3 ++ app/templates/entreprises/entreprises.html | 2 +- ...e_creation_table_relations_entreprrises.py | 1 + 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/app/entreprises/forms.py b/app/entreprises/forms.py index 1965c9cba..1ce49cc02 100644 --- a/app/entreprises/forms.py +++ b/app/entreprises/forms.py @@ -2,8 +2,9 @@ from flask import flash from flask_wtf import FlaskForm from markupsafe import Markup import requests, re -from wtforms import StringField, SubmitField, TextAreaField, SelectField, FileField, HiddenField +from wtforms import StringField, SubmitField, TextAreaField, SelectField, HiddenField from wtforms.fields.html5 import EmailField, DateField +from flask_wtf.file import FileField, FileAllowed from wtforms.validators import ValidationError, DataRequired, Email from app.entreprises.models import Entreprise, EntrepriseContact from app.models import Identite @@ -14,7 +15,7 @@ from sqlalchemy import text DATA_REQUIRED_ERROR_MESSAGE = "Ce champ est requis" class EntrepriseCreationForm(FlaskForm): - siret = StringField("SIRET", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)], render_kw={"placeholder": "Numéro composé de 14 chiffres"}) + siret = StringField("SIRET", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)], render_kw={"placeholder": "Numéro composé de 14 chiffres", "maxlength": "14"}) nom_entreprise = StringField("Nom de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) adresse = StringField("Adresse de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) codepostal = StringField("Code postal de l'entreprise", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) @@ -56,7 +57,7 @@ class OffreCreationForm(FlaskForm): type_offre = SelectField("Type de l'offre", choices=[('Stage'), ('Alternance')], validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) missions = TextAreaField("Missions", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) duree = StringField("Durée", validators=[DataRequired(message=DATA_REQUIRED_ERROR_MESSAGE)]) - ficher = FileField("Fichier", validators=[]) + fichier = FileField("Fichier", validators=[FileAllowed(['pdf', 'docx'], 'Fichier .pdf ou .docx uniquement')]) submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"}) class OffreModificationForm(FlaskForm): diff --git a/app/entreprises/models.py b/app/entreprises/models.py index f31c11af8..3e2211e22 100644 --- a/app/entreprises/models.py +++ b/app/entreprises/models.py @@ -53,6 +53,7 @@ class EntrepriseOffre(db.Model): type_offre = db.Column(db.Text) missions = db.Column(db.Text) duree = db.Column(db.Text) + filename = db.Column(db.Text) class EntrepriseLog(db.Model): __tablename__ = "entreprise_log" diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index b85c9938f..db11ff33f 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -1,3 +1,6 @@ +import os +from config import Config + from flask import render_template, redirect, url_for, request, flash, send_file, abort from flask.json import jsonify from flask_login import current_user @@ -36,6 +39,7 @@ import app.scodoc.sco_utils as scu from app import db from sqlalchemy import text +from werkzeug.utils import secure_filename, send_from_directory @bp.route("/", methods=["GET"]) def index(): @@ -50,7 +54,7 @@ def contacts(): logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all() return render_template("entreprises/contacts.html", title=("Contacts"), contacts=contacts, logs=logs) -@bp.route("/fiche_entreprise/", methods=["GET"]) +@bp.route("/fiche_entreprise/", methods=["GET"]) def fiche_entreprise(id): entreprise = Entreprise.query.filter_by(id=id).first_or_404() offres = entreprise.offres @@ -103,7 +107,7 @@ def add_entreprise(): return redirect(url_for("entreprises.index")) return render_template("entreprises/ajout_entreprise.html", title=("Ajout entreprise + contact"), form=form) -@bp.route("/edit_entreprise/", methods=["GET", "POST"]) +@bp.route("/edit_entreprise/", methods=["GET", "POST"]) def edit_entreprise(id): entreprise = Entreprise.query.filter_by(id=id).first_or_404() form = EntrepriseModificationForm() @@ -161,7 +165,7 @@ def edit_entreprise(id): form.pays.data = entreprise.pays return render_template("entreprises/form.html", title=("Modification entreprise"), form=form) -@bp.route("/delete_entreprise/", methods=["GET", "POST"]) +@bp.route("/delete_entreprise/", methods=["GET", "POST"]) def delete_entreprise(id): entreprise = Entreprise.query.filter_by(id=id).first_or_404() form = SuppressionConfirmationForm() @@ -178,7 +182,7 @@ def delete_entreprise(id): return redirect(url_for("entreprises.index")) return render_template("entreprises/delete_confirmation.html", title=("Supression entreprise"), form=form) -@bp.route("/add_offre/", methods=["GET", "POST"]) +@bp.route("/add_offre/", methods=["GET", "POST"]) def add_offre(id): entreprise = Entreprise.query.filter_by(id=id).first_or_404() form = OffreCreationForm() @@ -191,19 +195,29 @@ def add_offre(id): missions=form.missions.data.strip(), duree=form.duree.data.strip() ) + db.session.add(offre) + db.session.commit() + if form.fichier.data is not None: + db.session.refresh(offre) + path = os.path.join(Config.SCODOC_VAR_DIR, "entreprises", f"{entreprise.id}", f"{offre.id}") + os.makedirs(path) + file = form.fichier.data + filename = secure_filename(file.filename) + file.save(os.path.join(path, filename)) + offre.offre_filename = f"{filename}" + db.session.commit() log = EntrepriseLog( authenticated_user = current_user.user_name, object = entreprise.id, text = "Création d'une offre", ) db.session.add(log) - db.session.add(offre) db.session.commit() flash("L'offre a été ajouté à la fiche entreprise.") return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) return render_template("entreprises/form.html", title=("Ajout offre"), form=form) -@bp.route("/edit_offre/", methods=["GET", "POST"]) +@bp.route("/edit_offre/", methods=["GET", "POST"]) def edit_offre(id): offre = EntrepriseOffre.query.filter_by(id=id).first_or_404() form = OffreModificationForm() @@ -230,7 +244,7 @@ def edit_offre(id): form.duree.data = offre.duree return render_template("entreprises/form.html", title=("Modification offre"), form=form) -@bp.route("/delete_offre/", methods=["GET", "POST"]) +@bp.route("/delete_offre/", methods=["GET", "POST"]) def delete_offre(id): offre = EntrepriseOffre.query.filter_by(id=id).first_or_404() entreprise_id = offre.entreprise.id @@ -248,7 +262,7 @@ def delete_offre(id): return redirect(url_for("entreprises.fiche_entreprise", id=entreprise_id)) return render_template("entreprises/delete_confirmation.html", title=("Supression offre"), form=form) -@bp.route("/add_contact/", methods=["GET", "POST"]) +@bp.route("/add_contact/", methods=["GET", "POST"]) def add_contact(id): entreprise = Entreprise.query.filter_by(id=id).first_or_404() form = ContactCreationForm(hidden_entreprise_id=entreprise.id) @@ -274,7 +288,7 @@ def add_contact(id): return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) return render_template("entreprises/form.html", title=("Ajout contact"), form=form) -@bp.route("/edit_contact/", methods=["GET", "POST"]) +@bp.route("/edit_contact/", methods=["GET", "POST"]) def edit_contact(id): contact = EntrepriseContact.query.filter_by(id=id).first_or_404() form = ContactModificationForm() @@ -303,7 +317,7 @@ def edit_contact(id): form.service.data = contact.service return render_template("entreprises/form.html", title=("Modification contact"), form=form) -@bp.route("/delete_contact/", methods=["GET", "POST"]) +@bp.route("/delete_contact/", methods=["GET", "POST"]) def delete_contact(id): contact = EntrepriseContact.query.filter_by(id=id).first_or_404() entreprise_id = contact.entreprise.id @@ -326,7 +340,7 @@ def delete_contact(id): return redirect(url_for("entreprises.fiche_entreprise", id=entreprise_id)) return render_template("entreprises/delete_confirmation.html", title=("Supression contact"), form=form) -@bp.route("/add_historique/", methods=["GET", "POST"]) +@bp.route("/add_historique/", methods=["GET", "POST"]) def add_historique(id): entreprise = Entreprise.query.filter_by(id=id).first_or_404() form = HistoriqueCreationForm() @@ -352,7 +366,7 @@ def add_historique(id): return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id)) return render_template("entreprises/ajout_historique.html", title=("Ajout historique"), form=form) -@bp.route("/envoyer_offre/", methods=["GET", "POST"]) +@bp.route("/envoyer_offre/", methods=["GET", "POST"]) def envoyer_offre(id): offre = EntrepriseOffre.query.filter_by(id=id).first_or_404() form = EnvoiOffreForm() @@ -449,5 +463,12 @@ def export_contacts(): 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) + +@bp.route("/download_offre///") +def download_offre(entreprise_id, offre_id, filename): + if os.path.isfile(os.path.join(Config.SCODOC_VAR_DIR, "entreprises", f"{entreprise_id}", f"{offre_id}", f"{filename}")): + return send_file(os.path.join(Config.SCODOC_VAR_DIR, "entreprises", f"{entreprise_id}", f"{offre_id}", f"{filename}"), as_attachment=True) else: abort(404) \ No newline at end of file diff --git a/app/templates/entreprises/_offre.html b/app/templates/entreprises/_offre.html index e64f3c16c..581ad7afd 100644 --- a/app/templates/entreprises/_offre.html +++ b/app/templates/entreprises/_offre.html @@ -5,6 +5,9 @@ Type de l'offre : {{ offre.type_offre }}
    Missions : {{ offre.missions }}
    Durée : {{ offre.duree }}
    + {% if offre.offre_filename %} + Téléchargez le fichier de l'offre + {% endif %}

    diff --git a/app/templates/entreprises/entreprises.html b/app/templates/entreprises/entreprises.html index a52f75805..85854efb3 100644 --- a/app/templates/entreprises/entreprises.html +++ b/app/templates/entreprises/entreprises.html @@ -38,7 +38,7 @@ Action - diff --git a/app/templates/scolar/affect_groups.html b/app/templates/scolar/affect_groups.html index 56393575d..2dc3a1f60 100644 --- a/app/templates/scolar/affect_groups.html +++ b/app/templates/scolar/affect_groups.html @@ -1,4 +1,4 @@ - +{# -*- mode: jinja-html -*- #} {{ sco_header|safe }}

    Affectation aux groupes de {{ partition["partition_name"] }}

    diff --git a/app/templates/scolar/photos_import_files.html b/app/templates/scolar/photos_import_files.html index f4bae574a..460d7d7e4 100644 --- a/app/templates/scolar/photos_import_files.html +++ b/app/templates/scolar/photos_import_files.html @@ -1,3 +1,4 @@ +{# -*- mode: jinja-html -*- #} {% extends 'base.html' %} {% block app_content %} diff --git a/app/templates/scolar/photos_import_files.txt b/app/templates/scolar/photos_import_files.txt index d9aab53ee..cb6777b5c 100755 --- a/app/templates/scolar/photos_import_files.txt +++ b/app/templates/scolar/photos_import_files.txt @@ -1,4 +1,4 @@ - +{# -*- mode: jinja-raw -*- #} Importation des photo effectuée {% if ignored_zipfiles %} diff --git a/app/templates/sidebar.html b/app/templates/sidebar.html index 3334dbb6a..eb5df4afa 100644 --- a/app/templates/sidebar.html +++ b/app/templates/sidebar.html @@ -1,13 +1,14 @@ {# Barre marge gauche ScoDoc #} +{# -*- mode: jinja-html -*- #}