1
0
forked from ScoDoc/ScoDoc

Merge branch 'refactor_nt' of https://scodoc.org/git/viennet/ScoDoc into orebut

This commit is contained in:
Emmanuel Viennet 2022-01-05 16:53:47 +01:00
commit 96130f1a75
32 changed files with 652 additions and 184 deletions

View File

@ -190,6 +190,7 @@ def create_app(config_class=DevConfig):
app.register_error_handler(ScoGenError, handle_sco_value_error) app.register_error_handler(ScoGenError, handle_sco_value_error)
app.register_error_handler(ScoValueError, handle_sco_value_error) app.register_error_handler(ScoValueError, handle_sco_value_error)
app.register_error_handler(AccessDenied, handle_access_denied) app.register_error_handler(AccessDenied, handle_access_denied)
app.register_error_handler(500, internal_server_error) app.register_error_handler(500, internal_server_error)
app.register_error_handler(503, postgresql_server_error) app.register_error_handler(503, postgresql_server_error)

View File

@ -11,7 +11,7 @@ from time import time
from typing import Optional from typing import Optional
import cracklib # pylint: disable=import-error import cracklib # pylint: disable=import-error
from flask import current_app, url_for, g from flask import current_app, g
from flask_login import UserMixin, AnonymousUserMixin from flask_login import UserMixin, AnonymousUserMixin
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
@ -136,6 +136,7 @@ class User(UserMixin, db.Model):
return check_password_hash(self.password_hash, password) return check_password_hash(self.password_hash, password)
def get_reset_password_token(self, expires_in=600): def get_reset_password_token(self, expires_in=600):
"Un token pour réinitialiser son mot de passe"
return jwt.encode( return jwt.encode(
{"reset_password": self.id, "exp": time() + expires_in}, {"reset_password": self.id, "exp": time() + expires_in},
current_app.config["SECRET_KEY"], current_app.config["SECRET_KEY"],
@ -144,15 +145,17 @@ class User(UserMixin, db.Model):
@staticmethod @staticmethod
def verify_reset_password_token(token): def verify_reset_password_token(token):
"Vérification du token de reéinitialisation du mot de passe"
try: try:
id = jwt.decode( user_id = jwt.decode(
token, current_app.config["SECRET_KEY"], algorithms=["HS256"] token, current_app.config["SECRET_KEY"], algorithms=["HS256"]
)["reset_password"] )["reset_password"]
except: except:
return return
return User.query.get(id) return User.query.get(user_id)
def to_dict(self, include_email=True): def to_dict(self, include_email=True):
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
data = { data = {
"date_expiration": self.date_expiration.isoformat() + "Z" "date_expiration": self.date_expiration.isoformat() + "Z"
if self.date_expiration if self.date_expiration
@ -472,5 +475,5 @@ def get_super_admin():
@login.user_loader @login.user_loader
def load_user(id): def load_user(uid):
return User.query.get(int(id)) return User.query.get(int(uid))

View File

@ -72,7 +72,7 @@ def bulletin_but_xml_compat(
etud = Identite.query.get_or_404(etudid) etud = Identite.query.get_or_404(etudid)
results = bulletin_but.ResultatsSemestreBUT(sem) results = bulletin_but.ResultatsSemestreBUT(sem)
nb_inscrits = len(results.etuds) nb_inscrits = len(results.etuds)
if sem.bul_hide_xml or force_publishing: if (not sem.bul_hide_xml) or force_publishing:
published = "1" published = "1"
else: else:
published = "0" published = "0"

View File

@ -121,6 +121,8 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
(DataFrames rendus par compute_module_moy, (etud x UE)) (DataFrames rendus par compute_module_moy, (etud x UE))
Resultat: ndarray (etud x module x UE) Resultat: ndarray (etud x module x UE)
""" """
if len(modimpls_notes) == 0:
return np.zeros((0, 0, 0), dtype=float)
modimpls_notes_arr = [df.values for df in modimpls_notes] modimpls_notes_arr = [df.values for df in modimpls_notes]
modimpls_notes = np.stack(modimpls_notes_arr) modimpls_notes = np.stack(modimpls_notes_arr)
# passe de (mod x etud x ue) à (etud x mod x UE) # passe de (mod x etud x ue) à (etud x mod x UE)

View File

@ -230,7 +230,7 @@ class NotesTableCompat(ResultatsSemestre):
def get_etud_ue_status(self, etudid: int, ue_id: int): def get_etud_ue_status(self, etudid: int, ue_id: int):
return { return {
"cur_moy_ue": self.results.etud_moy_ue[ue_id][etudid], "cur_moy_ue": self.etud_moy_ue[ue_id][etudid],
"is_capitalized": False, # XXX TODO "is_capitalized": False, # XXX TODO
} }
@ -242,9 +242,9 @@ class NotesTableCompat(ResultatsSemestre):
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]: def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
"liste des évaluations valides dans un module" "liste des évaluations valides dans un module"
mi = ModuleImpl.query.get(moduleimpl_id) modimpl = ModuleImpl.query.get(moduleimpl_id)
evals_results = [] evals_results = []
for e in mi.evaluations: for e in modimpl.evaluations:
d = e.to_dict() d = e.to_dict()
d["heure_debut"] = e.heure_debut # datetime.time d["heure_debut"] = e.heure_debut # datetime.time
d["heure_fin"] = e.heure_fin d["heure_fin"] = e.heure_fin

View File

@ -57,6 +57,23 @@ class EntrepriseContact(db.Model):
"service": self.service, "service": self.service,
} }
def to_dict_export(self):
entreprise = Entreprise.query.get(self.entreprise_id)
return {
"nom": self.nom,
"prenom": self.prenom,
"telephone": self.telephone,
"mail": self.mail,
"poste": self.poste,
"service": self.service,
"siret": entreprise.siret,
"nom_entreprise": entreprise.nom,
"adresse_entreprise": entreprise.adresse,
"codepostal": entreprise.codepostal,
"ville": entreprise.ville,
"pays": entreprise.pays,
}
class EntrepriseOffre(db.Model): class EntrepriseOffre(db.Model):
__tablename__ = "entreprise_offre" __tablename__ = "entreprise_offre"
@ -71,6 +88,15 @@ class EntrepriseOffre(db.Model):
missions = db.Column(db.Text) missions = db.Column(db.Text)
duree = db.Column(db.Text) duree = db.Column(db.Text)
def to_dict(self):
return {
"intitule": self.intitule,
"description": self.description,
"type_offre": self.type_offre,
"missions": self.missions,
"duree": self.duree,
}
class EntrepriseLog(db.Model): class EntrepriseLog(db.Model):
__tablename__ = "entreprise_log" __tablename__ = "entreprise_log"
@ -100,3 +126,12 @@ class EntrepriseEnvoiOffre(db.Model):
receiver_id = db.Column(db.Integer, db.ForeignKey("user.id")) receiver_id = db.Column(db.Integer, db.ForeignKey("user.id"))
offre_id = db.Column(db.Integer, db.ForeignKey("entreprise_offre.id")) offre_id = db.Column(db.Integer, db.ForeignKey("entreprise_offre.id"))
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
class EntrepriseEnvoiOffreEtudiant(db.Model):
__tablename__ = "entreprise_envoi_offre_etudiant"
id = db.Column(db.Integer, primary_key=True)
sender_id = db.Column(db.Integer, db.ForeignKey("user.id"))
receiver_id = db.Column(db.Integer, db.ForeignKey("identite.id"))
offre_id = db.Column(db.Integer, db.ForeignKey("entreprise_offre.id"))
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())

View File

@ -1,6 +1,6 @@
import os import os
from config import Config from config import Config
from datetime import datetime from datetime import datetime, timedelta
import glob import glob
import shutil import shutil
@ -45,6 +45,18 @@ from werkzeug.utils import secure_filename
@bp.route("/", methods=["GET"]) @bp.route("/", methods=["GET"])
def index(): def index():
"""
Permet d'afficher une page avec la liste des entreprises et une liste des dernières opérations
Retourne: template de la page (entreprises.html)
Arguments du template:
title:
titre de la page
entreprises:
liste des entreprises
logs:
liste des logs
"""
entreprises = Entreprise.query.all() entreprises = Entreprise.query.all()
logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all() logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all()
return render_template( return render_template(
@ -57,6 +69,18 @@ def index():
@bp.route("/contacts", methods=["GET"]) @bp.route("/contacts", methods=["GET"])
def contacts(): def contacts():
"""
Permet d'afficher une page la liste des contacts et une liste des dernières opérations
Retourne: template de la page (contacts.html)
Arguments du template:
title:
titre de la page
contacts:
liste des contacts
logs:
liste des logs
"""
contacts = ( contacts = (
db.session.query(EntrepriseContact, Entreprise) db.session.query(EntrepriseContact, Entreprise)
.join(Entreprise, EntrepriseContact.entreprise_id == Entreprise.id) .join(Entreprise, EntrepriseContact.entreprise_id == Entreprise.id)
@ -70,10 +94,39 @@ def contacts():
@bp.route("/fiche_entreprise/<int:id>", methods=["GET"]) @bp.route("/fiche_entreprise/<int:id>", methods=["GET"])
def fiche_entreprise(id): def fiche_entreprise(id):
"""
Permet d'afficher la fiche entreprise d'une entreprise avec une liste des dernières opérations et
l'historique des étudiants ayant réaliser un stage ou une alternance dans cette entreprise.
La fiche entreprise comporte les informations de l'entreprise, les contacts de l'entreprise et
les offres de l'entreprise.
Arguments:
id:
l'id de l'entreprise
Retourne: template de la page (fiche_entreprise.html)
Arguments du template:
title:
titre de la page
entreprise:
un objet entreprise
contacts:
liste des contacts de l'entreprise
offres:
liste des offres de l'entreprise avec leurs fichiers
logs:
liste des logs
historique:
liste des étudiants ayant réaliser un stage ou une alternance dans l'entreprise
"""
entreprise = Entreprise.query.filter_by(id=id).first_or_404() entreprise = Entreprise.query.filter_by(id=id).first_or_404()
offres = entreprise.offres offres = entreprise.offres
offres_with_files = [] offres_with_files = []
for offre in offres: for offre in offres:
if datetime.now() - offre.date_ajout.replace(tzinfo=None) >= timedelta(
days=90
): # pour une date d'expiration ?
break
files = [] files = []
path = os.path.join( path = os.path.join(
Config.SCODOC_VAR_DIR, Config.SCODOC_VAR_DIR,
@ -116,19 +169,32 @@ def fiche_entreprise(id):
@bp.route("/offres", methods=["GET"]) @bp.route("/offres", methods=["GET"])
def offres(): def offres():
offres_recus = ( """
Permet d'afficher la page où l'on recoit les offres
Retourne: template de la page (offres.html)
Arguments du template:
title:
titre de la page
offres_recus:
liste des offres reçues
"""
offres_recues = (
db.session.query(EntrepriseEnvoiOffre, EntrepriseOffre) db.session.query(EntrepriseEnvoiOffre, EntrepriseOffre)
.filter(EntrepriseEnvoiOffre.receiver_id == current_user.id) .filter(EntrepriseEnvoiOffre.receiver_id == current_user.id)
.join(EntrepriseOffre, EntrepriseOffre.id == EntrepriseEnvoiOffre.offre_id) .join(EntrepriseOffre, EntrepriseOffre.id == EntrepriseEnvoiOffre.offre_id)
.all() .all()
) )
return render_template( return render_template(
"entreprises/offres.html", title=("Offres"), offres_recus=offres_recus "entreprises/offres.html", title=("Offres"), offres_recues=offres_recues
) )
@bp.route("/add_entreprise", methods=["GET", "POST"]) @bp.route("/add_entreprise", methods=["GET", "POST"])
def add_entreprise(): def add_entreprise():
"""
Permet d'ajouter une entreprise dans la base avec un formulaire
"""
form = EntrepriseCreationForm() form = EntrepriseCreationForm()
if form.validate_on_submit(): if form.validate_on_submit():
entreprise = Entreprise( entreprise = Entreprise(
@ -170,6 +236,13 @@ def add_entreprise():
@bp.route("/edit_entreprise/<int:id>", methods=["GET", "POST"]) @bp.route("/edit_entreprise/<int:id>", methods=["GET", "POST"])
def edit_entreprise(id): def edit_entreprise(id):
"""
Permet de modifier une entreprise de la base avec un formulaire
Arguments:
id:
l'id de l'entreprise
"""
entreprise = Entreprise.query.filter_by(id=id).first_or_404() entreprise = Entreprise.query.filter_by(id=id).first_or_404()
form = EntrepriseModificationForm() form = EntrepriseModificationForm()
if form.validate_on_submit(): if form.validate_on_submit():
@ -231,6 +304,13 @@ def edit_entreprise(id):
@bp.route("/delete_entreprise/<int:id>", methods=["GET", "POST"]) @bp.route("/delete_entreprise/<int:id>", methods=["GET", "POST"])
def delete_entreprise(id): def delete_entreprise(id):
"""
Permet de supprimer une entreprise de la base avec un formulaire de confirmation
Arguments:
id:
l'id de l'entreprise
"""
entreprise = Entreprise.query.filter_by(id=id).first_or_404() entreprise = Entreprise.query.filter_by(id=id).first_or_404()
form = SuppressionConfirmationForm() form = SuppressionConfirmationForm()
if form.validate_on_submit(): if form.validate_on_submit():
@ -253,6 +333,13 @@ def delete_entreprise(id):
@bp.route("/add_offre/<int:id>", methods=["GET", "POST"]) @bp.route("/add_offre/<int:id>", methods=["GET", "POST"])
def add_offre(id): def add_offre(id):
"""
Permet d'ajouter une offre a une entreprise
Arguments:
id:
l'id de l'entreprise
"""
entreprise = Entreprise.query.filter_by(id=id).first_or_404() entreprise = Entreprise.query.filter_by(id=id).first_or_404()
form = OffreCreationForm() form = OffreCreationForm()
if form.validate_on_submit(): if form.validate_on_submit():
@ -279,6 +366,13 @@ def add_offre(id):
@bp.route("/edit_offre/<int:id>", methods=["GET", "POST"]) @bp.route("/edit_offre/<int:id>", methods=["GET", "POST"])
def edit_offre(id): def edit_offre(id):
"""
Permet de modifier une offre
Arguments:
id:
l'id de l'offre
"""
offre = EntrepriseOffre.query.filter_by(id=id).first_or_404() offre = EntrepriseOffre.query.filter_by(id=id).first_or_404()
form = OffreModificationForm() form = OffreModificationForm()
if form.validate_on_submit(): if form.validate_on_submit():
@ -309,6 +403,13 @@ def edit_offre(id):
@bp.route("/delete_offre/<int:id>", methods=["GET", "POST"]) @bp.route("/delete_offre/<int:id>", methods=["GET", "POST"])
def delete_offre(id): def delete_offre(id):
"""
Permet de supprimer une offre
Arguments:
id:
l'id de l'offre
"""
offre = EntrepriseOffre.query.filter_by(id=id).first_or_404() offre = EntrepriseOffre.query.filter_by(id=id).first_or_404()
entreprise_id = offre.entreprise.id entreprise_id = offre.entreprise.id
form = SuppressionConfirmationForm() form = SuppressionConfirmationForm()
@ -330,6 +431,13 @@ def delete_offre(id):
@bp.route("/add_contact/<int:id>", methods=["GET", "POST"]) @bp.route("/add_contact/<int:id>", methods=["GET", "POST"])
def add_contact(id): def add_contact(id):
"""
Permet d'ajouter un contact a une entreprise
Arguments:
id:
l'id de l'entreprise
"""
entreprise = Entreprise.query.filter_by(id=id).first_or_404() entreprise = Entreprise.query.filter_by(id=id).first_or_404()
form = ContactCreationForm(hidden_entreprise_id=entreprise.id) form = ContactCreationForm(hidden_entreprise_id=entreprise.id)
if form.validate_on_submit(): if form.validate_on_submit():
@ -357,6 +465,13 @@ def add_contact(id):
@bp.route("/edit_contact/<int:id>", methods=["GET", "POST"]) @bp.route("/edit_contact/<int:id>", methods=["GET", "POST"])
def edit_contact(id): def edit_contact(id):
"""
Permet de modifier un contact
Arguments:
id:
l'id du contact
"""
contact = EntrepriseContact.query.filter_by(id=id).first_or_404() contact = EntrepriseContact.query.filter_by(id=id).first_or_404()
form = ContactModificationForm() form = ContactModificationForm()
if form.validate_on_submit(): if form.validate_on_submit():
@ -391,6 +506,13 @@ def edit_contact(id):
@bp.route("/delete_contact/<int:id>", methods=["GET", "POST"]) @bp.route("/delete_contact/<int:id>", methods=["GET", "POST"])
def delete_contact(id): def delete_contact(id):
"""
Permet de supprimer un contact
Arguments:
id:
l'id du contact
"""
contact = EntrepriseContact.query.filter_by(id=id).first_or_404() contact = EntrepriseContact.query.filter_by(id=id).first_or_404()
entreprise_id = contact.entreprise.id entreprise_id = contact.entreprise.id
form = SuppressionConfirmationForm() form = SuppressionConfirmationForm()
@ -421,6 +543,13 @@ def delete_contact(id):
@bp.route("/add_historique/<int:id>", methods=["GET", "POST"]) @bp.route("/add_historique/<int:id>", methods=["GET", "POST"])
def add_historique(id): def add_historique(id):
"""
Permet d'ajouter un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
Arguments:
id:
l'id de l'entreprise
"""
entreprise = Entreprise.query.filter_by(id=id).first_or_404() entreprise = Entreprise.query.filter_by(id=id).first_or_404()
form = HistoriqueCreationForm() form = HistoriqueCreationForm()
if form.validate_on_submit(): if form.validate_on_submit():
@ -458,6 +587,13 @@ def add_historique(id):
@bp.route("/envoyer_offre/<int:id>", methods=["GET", "POST"]) @bp.route("/envoyer_offre/<int:id>", methods=["GET", "POST"])
def envoyer_offre(id): def envoyer_offre(id):
"""
Permet d'envoyer une offre à un utilisateur
Arguments:
id:
l'id de l'offre
"""
offre = EntrepriseOffre.query.filter_by(id=id).first_or_404() offre = EntrepriseOffre.query.filter_by(id=id).first_or_404()
form = EnvoiOffreForm() form = EnvoiOffreForm()
if form.validate_on_submit(): if form.validate_on_submit():
@ -484,6 +620,18 @@ def envoyer_offre(id):
@bp.route("/etudiants") @bp.route("/etudiants")
def json_etudiants(): def json_etudiants():
"""
Permet de récuperer un JSON avec tous les étudiants
Arguments:
term:
le terme utilisé pour le filtre de l'autosuggest
Retourne:
le JSON de tous les étudiants (nom, prenom, formation actuelle?) correspondant au terme
"""
if request.args.get("term") == None:
abort(400)
term = request.args.get("term").strip() term = request.args.get("term").strip()
etudiants = Identite.query.filter(Identite.nom.ilike(f"%{term}%")).all() etudiants = Identite.query.filter(Identite.nom.ilike(f"%{term}%")).all()
list = [] list = []
@ -505,6 +653,18 @@ def json_etudiants():
@bp.route("/responsables") @bp.route("/responsables")
def json_responsables(): def json_responsables():
"""
Permet de récuperer un JSON avec tous les étudiants
Arguments:
term:
le terme utilisé pour le filtre de l'autosuggest
Retourne:
le JSON de tous les utilisateurs (nom, prenom, login) correspondant au terme
"""
if request.args.get("term") == None:
abort(400)
term = request.args.get("term").strip() term = request.args.get("term").strip()
responsables = User.query.filter( responsables = User.query.filter(
User.nom.ilike(f"%{term}%"), User.nom.is_not(None), User.prenom.is_not(None) User.nom.ilike(f"%{term}%"), User.nom.is_not(None), User.prenom.is_not(None)
@ -521,6 +681,9 @@ def json_responsables():
@bp.route("/export_entreprises") @bp.route("/export_entreprises")
def export_entreprises(): def export_entreprises():
"""
Permet d'exporter la liste des entreprises sous format excel (.xlsx)
"""
entreprises = Entreprise.query.all() entreprises = Entreprise.query.all()
if entreprises: if entreprises:
keys = ["siret", "nom", "adresse", "ville", "codepostal", "pays"] keys = ["siret", "nom", "adresse", "ville", "codepostal", "pays"]
@ -539,6 +702,9 @@ def export_entreprises():
@bp.route("/export_contacts") @bp.route("/export_contacts")
def export_contacts(): def export_contacts():
"""
Permet d'exporter la liste des contacts sous format excel (.xlsx)
"""
contacts = EntrepriseContact.query.all() contacts = EntrepriseContact.query.all()
if contacts: if contacts:
keys = ["nom", "prenom", "telephone", "mail", "poste", "service"] keys = ["nom", "prenom", "telephone", "mail", "poste", "service"]
@ -552,10 +718,56 @@ def export_contacts():
abort(404) abort(404)
@bp.route("/export_contacts_bis")
def export_contacts_bis():
"""
Permet d'exporter la liste des contacts avec leur entreprise sous format excel (.xlsx)
"""
contacts = EntrepriseContact.query.all()
if contacts:
keys = [
"nom",
"prenom",
"telephone",
"mail",
"poste",
"service",
"nom_entreprise",
"siret",
"adresse_entreprise",
"ville",
"codepostal",
"pays",
]
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)
@bp.route( @bp.route(
"/get_offre_file/<int:entreprise_id>/<int:offre_id>/<string:filedir>/<string:filename>" "/get_offre_file/<int:entreprise_id>/<int:offre_id>/<string:filedir>/<string:filename>"
) )
def get_offre_file(entreprise_id, offre_id, filedir, filename): def get_offre_file(entreprise_id, offre_id, filedir, filename):
"""
Permet de télécharger un fichier d'une offre
Arguments:
entreprise_id:
l'id de l'entreprise
offre_id:
l'id de l'offre
filedir:
le répertoire du fichier
filename:
le nom du fichier
"""
if os.path.isfile( if os.path.isfile(
os.path.join( os.path.join(
Config.SCODOC_VAR_DIR, Config.SCODOC_VAR_DIR,
@ -583,6 +795,13 @@ def get_offre_file(entreprise_id, offre_id, filedir, filename):
@bp.route("/add_offre_file/<int:offre_id>", methods=["GET", "POST"]) @bp.route("/add_offre_file/<int:offre_id>", methods=["GET", "POST"])
def add_offre_file(offre_id): def add_offre_file(offre_id):
"""
Permet d'ajouter un fichier à une offre
Arguments:
offre_id:
l'id de l'offre
"""
offre = EntrepriseOffre.query.filter_by(id=offre_id).first_or_404() offre = EntrepriseOffre.query.filter_by(id=offre_id).first_or_404()
form = AjoutFichierForm() form = AjoutFichierForm()
if form.validate_on_submit(): if form.validate_on_submit():
@ -607,6 +826,15 @@ def add_offre_file(offre_id):
@bp.route("/delete_offre_file/<int:offre_id>/<string:filedir>", methods=["GET", "POST"]) @bp.route("/delete_offre_file/<int:offre_id>/<string:filedir>", methods=["GET", "POST"])
def delete_offre_file(offre_id, filedir): def delete_offre_file(offre_id, filedir):
"""
Permet de supprimer un fichier d'une offre
Arguments:
offre_id:
l'id de l'offre
filedir:
le répertoire du fichier
"""
offre = EntrepriseOffre.query.filter_by(id=offre_id).first_or_404() offre = EntrepriseOffre.query.filter_by(id=offre_id).first_or_404()
form = SuppressionConfirmationForm() form = SuppressionConfirmationForm()
if form.validate_on_submit(): if form.validate_on_submit():

View File

@ -245,7 +245,7 @@ class DeptForm(FlaskForm):
def _make_dept_id_name(): def _make_dept_id_name():
"""Cette section assute que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ) """Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)
et détermine l'ordre d'affichage des DeptForm (GLOBAL d'abord, puis par ordre alpha de nom de département) et détermine l'ordre d'affichage des DeptForm (GLOBAL d'abord, puis par ordre alpha de nom de département)
-> [ (None, None), (dept_id, dept_name)... ]""" -> [ (None, None), (dept_id, dept_name)... ]"""
depts = [(None, GLOBAL)] depts = [(None, GLOBAL)]

View File

@ -31,16 +31,10 @@ Formulaires création département
from flask import flash, url_for, redirect, render_template from flask import flash, url_for, redirect, render_template
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import SelectField, SubmitField, FormField, validators, FieldList from wtforms import SubmitField, validators
from wtforms.fields.simple import StringField, HiddenField from wtforms.fields.simple import StringField, BooleanField
from app import AccessDenied
from app.models import Departement
from app.models import ScoPreference
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.scodoc import sco_utils as scu
from flask_login import current_user
class CreateDeptForm(FlaskForm): class CreateDeptForm(FlaskForm):
@ -60,5 +54,10 @@ class CreateDeptForm(FlaskForm):
validators.DataRequired("acronyme du département requis"), validators.DataRequired("acronyme du département requis"),
], ],
) )
# description = StringField(label="Description")
visible = BooleanField(
"Visible sur page d'accueil",
default=True,
)
submit = SubmitField("Valider") submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -12,8 +12,10 @@ class Departement(db.Model):
"""Un département ScoDoc""" """Un département ScoDoc"""
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
acronym = db.Column(db.String(SHORT_STR_LEN), nullable=False, index=True) acronym = db.Column(
description = db.Column(db.Text()) db.String(SHORT_STR_LEN), nullable=False, index=True
) # ne change jamais, voir la pref. DeptName
description = db.Column(db.Text()) # pas utilisé par ScoDoc : voir DeptFullName
date_creation = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) date_creation = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
visible = db.Column( visible = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true" db.Boolean(), nullable=False, default=True, server_default="true"
@ -49,11 +51,11 @@ class Departement(db.Model):
return dept return dept
def create_dept(acronym: str) -> Departement: def create_dept(acronym: str, visible=True) -> Departement:
"Create new departement" "Create new departement"
from app.models import ScoPreference from app.models import ScoPreference
departement = Departement(acronym=acronym) departement = Departement(acronym=acronym, visible=visible)
p1 = ScoPreference(name="DeptName", value=acronym, departement=departement) p1 = ScoPreference(name="DeptName", value=acronym, departement=departement)
db.session.add(p1) db.session.add(p1)
db.session.add(departement) db.session.add(departement)

View File

@ -49,9 +49,7 @@ class Module(db.Model):
super(Module, self).__init__(**kwargs) super(Module, self).__init__(**kwargs)
def __repr__(self): def __repr__(self):
return ( return f"<Module{ModuleType(self.module_type or ModuleType.STANDARD).name} id={self.id} code={self.code}>"
f"<Module{ModuleType(self.module_type).name} id={self.id} code={self.code}>"
)
def to_dict(self): def to_dict(self):
e = dict(self.__dict__) e = dict(self.__dict__)

View File

@ -78,7 +78,8 @@ def html_edit_formation_apc(
alt="supprimer", alt="supprimer",
), ),
"delete_disabled": scu.icontag( "delete_disabled": scu.icontag(
"delete_small_dis_img", title="Suppression impossible (module utilisé)" "delete_small_dis_img",
title="Suppression impossible (utilisé dans des semestres)",
), ),
} }

View File

@ -30,13 +30,18 @@
""" """
import flask import flask
from flask import g, url_for, request from flask import g, url_for, request
from app.models.formations import Matiere
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.models import Formation from app.models import Formation
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError from app.scodoc.sco_exceptions import (
ScoValueError,
ScoLockedFormError,
ScoNonEmptyFormationObject,
)
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
_matiereEditor = ndb.EditableTable( _matiereEditor = ndb.EditableTable(
@ -156,6 +161,16 @@ associé.
return flask.redirect(dest_url) return flask.redirect(dest_url)
def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]:
"True si la matiere n'est pas utilisée dans des formsemestre"
locked = matiere_is_locked(matiere.id)
if locked:
return False
if any(m.modimpls.all() for m in matiere.modules):
return False
return True
def do_matiere_delete(oid): def do_matiere_delete(oid):
"delete matiere and attached modules" "delete matiere and attached modules"
from app.scodoc import sco_formations from app.scodoc import sco_formations
@ -165,17 +180,16 @@ def do_matiere_delete(oid):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# check # check
mat = matiere_list({"matiere_id": oid})[0] matiere = Matiere.query.get_or_404(oid)
mat = matiere_list({"matiere_id": oid})[0] # compat sco7
ue = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0] ue = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]
locked = matiere_is_locked(mat["matiere_id"]) if not can_delete_matiere(matiere):
if locked: # il y a au moins un modimpl dans un module de cette matière
log("do_matiere_delete: mat=%s" % mat) raise ScoNonEmptyFormationObject("Matière", matiere.titre)
log("do_matiere_delete: ue=%s" % ue)
log("do_matiere_delete: locked sems: %s" % locked) log("do_matiere_delete: matiere_id=%s" % matiere.id)
raise ScoLockedFormError()
log("do_matiere_delete: matiere_id=%s" % oid)
# delete all modules in this matiere # delete all modules in this matiere
mods = sco_edit_module.module_list({"matiere_id": oid}) mods = sco_edit_module.module_list({"matiere_id": matiere.id})
for mod in mods: for mod in mods:
sco_edit_module.do_module_delete(mod["module_id"]) sco_edit_module.do_module_delete(mod["module_id"])
_matiereEditor.delete(cnx, oid) _matiereEditor.delete(cnx, oid)
@ -194,11 +208,25 @@ def matiere_delete(matiere_id=None):
"""Delete matière""" """Delete matière"""
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
M = matiere_list(args={"matiere_id": matiere_id})[0] matiere = Matiere.query.get_or_404(matiere_id)
UE = sco_edit_ue.ue_list(args={"ue_id": M["ue_id"]})[0] if not can_delete_matiere(matiere):
# il y a au moins un modimpl dans un module de cette matière
raise ScoNonEmptyFormationObject(
"Matière",
matiere.titre,
dest_url=url_for(
"notes.ue_table",
formation_id=matiere.ue.formation_id,
semestre_idx=matiere.ue.semestre_idx,
scodoc_dept=g.scodoc_dept,
),
)
mat = matiere_list(args={"matiere_id": matiere_id})[0]
UE = sco_edit_ue.ue_list(args={"ue_id": mat["ue_id"]})[0]
H = [ H = [
html_sco_header.sco_header(page_title="Suppression d'une matière"), html_sco_header.sco_header(page_title="Suppression d'une matière"),
"<h2>Suppression de la matière %(titre)s" % M, "<h2>Suppression de la matière %(titre)s" % mat,
" dans l'UE (%(acronyme)s))</h2>" % UE, " dans l'UE (%(acronyme)s))</h2>" % UE,
] ]
dest_url = url_for( dest_url = url_for(
@ -210,7 +238,7 @@ def matiere_delete(matiere_id=None):
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),
(("matiere_id", {"input_type": "hidden"}),), (("matiere_id", {"input_type": "hidden"}),),
initvalues=M, initvalues=mat,
submitlabel="Confirmer la suppression", submitlabel="Confirmer la suppression",
cancelbutton="Annuler", cancelbutton="Annuler",
) )

View File

@ -43,7 +43,12 @@ from app import models
from app.models import Formation from app.models import Formation
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError, ScoGenError from app.scodoc.sco_exceptions import (
ScoValueError,
ScoLockedFormError,
ScoGenError,
ScoNonEmptyFormationObject,
)
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
@ -330,20 +335,37 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
) )
def can_delete_module(module):
"True si le module n'est pas utilisée dans des formsemestre"
return len(module.modimpls.all()) == 0
def do_module_delete(oid): def do_module_delete(oid):
"delete module" "delete module"
from app.scodoc import sco_formations from app.scodoc import sco_formations
mod = module_list({"module_id": oid})[0] module = Module.query.get_or_404(oid)
if module_is_locked(mod["module_id"]): mod = module_list({"module_id": oid})[0] # sco7
if module_is_locked(module.id):
raise ScoLockedFormError() raise ScoLockedFormError()
if not can_delete_module(module):
raise ScoNonEmptyFormationObject(
"Module",
msg=module.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=module.formation_id,
semestre_idx=module.ue.semestre_idx,
),
)
# S'il y a des moduleimpls, on ne peut pas detruire le module ! # S'il y a des moduleimpls, on ne peut pas detruire le module !
mods = sco_moduleimpl.moduleimpl_list(module_id=oid) mods = sco_moduleimpl.moduleimpl_list(module_id=oid)
if mods: if mods:
err_page = f"""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3> err_page = f"""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>
<p class="help">Il faut d'abord supprimer le semestre. Mais il est peut être préférable de <p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce module). Mais il est peut être préférable de
laisser ce programme intact et d'en créer une nouvelle version pour la modifier. laisser ce programme intact et d'en créer une nouvelle version pour la modifier sans affecter les semestres déjà en place.
</p> </p>
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=mod["formation_id"])}">reprendre</a> formation_id=mod["formation_id"])}">reprendre</a>
@ -365,12 +387,21 @@ def do_module_delete(oid):
def module_delete(module_id=None): def module_delete(module_id=None):
"""Delete a module""" """Delete a module"""
if not module_id: module = Module.query.get_or_404(module_id)
raise ScoValueError("invalid module !") mod = module_list(args={"module_id": module_id})[0] # sco7
modules = module_list(args={"module_id": module_id})
if not modules: if not can_delete_module(module):
raise ScoValueError("Module inexistant !") raise ScoNonEmptyFormationObject(
mod = modules[0] "Module",
msg=module.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=module.formation_id,
semestre_idx=module.ue.semestre_idx,
),
)
H = [ H = [
html_sco_header.sco_header(page_title="Suppression d'un module"), html_sco_header.sco_header(page_title="Suppression d'un module"),
"""<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % mod, """<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % mod,

View File

@ -42,7 +42,12 @@ from app import log
from app.scodoc.TrivialFormulator import TrivialFormulator, TF from app.scodoc.TrivialFormulator import TrivialFormulator, TF
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError from app.scodoc.sco_exceptions import (
ScoGenError,
ScoValueError,
ScoLockedFormError,
ScoNonEmptyFormationObject,
)
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cache from app.scodoc import sco_cache
@ -130,64 +135,83 @@ def do_ue_create(args):
return ue_id return ue_id
def can_delete_ue(ue: UniteEns) -> bool:
"""True si l'UE n'est pas utilisée dans des formsemestre
et n'a pas de module rattachés
"""
# "pas un seul module de cette UE n'a de modimpl...""
return (not len(ue.modules.all())) and not any(m.modimpls.all() for m in ue.modules)
def do_ue_delete(ue_id, delete_validations=False, force=False): def do_ue_delete(ue_id, delete_validations=False, force=False):
"delete UE and attached matieres (but not modules)" "delete UE and attached matieres (but not modules)"
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_parcours_dut from app.scodoc import sco_parcours_dut
ue = UniteEns.query.get_or_404(ue_id)
if not can_delete_ue(ue):
raise ScoNonEmptyFormationObject(
"UE",
msg=ue.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
),
)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue_id, delete_validations)) log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue.id, delete_validations))
# check # check
ue = ue_list({"ue_id": ue_id}) # if ue_is_locked(ue.id):
if not ue: # raise ScoLockedFormError()
raise ScoValueError("UE inexistante !")
ue = ue[0]
if ue_is_locked(ue["ue_id"]):
raise ScoLockedFormError()
# Il y a-t-il des etudiants ayant validé cette UE ? # Il y a-t-il des etudiants ayant validé cette UE ?
# si oui, propose de supprimer les validations # si oui, propose de supprimer les validations
validations = sco_parcours_dut.scolar_formsemestre_validation_list( validations = sco_parcours_dut.scolar_formsemestre_validation_list(
cnx, args={"ue_id": ue_id} cnx, args={"ue_id": ue.id}
) )
if validations and not delete_validations and not force: if validations and not delete_validations and not force:
return scu.confirm_dialog( return scu.confirm_dialog(
"<p>%d étudiants ont validé l'UE %s (%s)</p><p>Si vous supprimez cette UE, ces validations vont être supprimées !</p>" "<p>%d étudiants ont validé l'UE %s (%s)</p><p>Si vous supprimez cette UE, ces validations vont être supprimées !</p>"
% (len(validations), ue["acronyme"], ue["titre"]), % (len(validations), ue.acronyme, ue.titre),
dest_url="", dest_url="",
target_variable="delete_validations", target_variable="delete_validations",
cancel_url=url_for( cancel_url=url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]), formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
), ),
parameters={"ue_id": ue_id, "dialog_confirmed": 1}, parameters={"ue_id": ue.id, "dialog_confirmed": 1},
) )
if delete_validations: if delete_validations:
log("deleting all validations of UE %s" % ue_id) log("deleting all validations of UE %s" % ue.id)
ndb.SimpleQuery( ndb.SimpleQuery(
"DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s",
{"ue_id": ue_id}, {"ue_id": ue.id},
) )
# delete all matiere in this UE # delete all matiere in this UE
mats = sco_edit_matiere.matiere_list({"ue_id": ue_id}) mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
for mat in mats: for mat in mats:
sco_edit_matiere.do_matiere_delete(mat["matiere_id"]) sco_edit_matiere.do_matiere_delete(mat["matiere_id"])
# delete uecoef and events # delete uecoef and events
ndb.SimpleQuery( ndb.SimpleQuery(
"DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s", "DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s",
{"ue_id": ue_id}, {"ue_id": ue.id},
) )
ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue_id}) ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue.id})
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
_ueEditor.delete(cnx, ue_id) _ueEditor.delete(cnx, ue.id)
# > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement utilisé: acceptable de tout invalider ?): # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement
# utilisé: acceptable de tout invalider):
sco_cache.invalidate_formsemestre() sco_cache.invalidate_formsemestre()
# news # news
F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0] F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0]
sco_news.add( sco_news.add(
typ=sco_news.NEWS_FORM, typ=sco_news.NEWS_FORM,
object=ue["formation_id"], object=ue.formation_id,
text="Modification de la formation %(acronyme)s" % F, text="Modification de la formation %(acronyme)s" % F,
max_frequency=3, max_frequency=3,
) )
@ -197,11 +221,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
url_for( url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=ue["formation_id"], formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
) )
) )
else: return None
return None
def ue_create(formation_id=None): def ue_create(formation_id=None):
@ -211,8 +235,6 @@ def ue_create(formation_id=None):
def ue_edit(ue_id=None, create=False, formation_id=None): def ue_edit(ue_id=None, create=False, formation_id=None):
"""Modification ou création d'une UE""" """Modification ou création d'une UE"""
from app.scodoc import sco_formations
create = int(create) create = int(create)
if not create: if not create:
U = ue_list(args={"ue_id": ue_id}) U = ue_list(args={"ue_id": ue_id})
@ -444,24 +466,38 @@ def next_ue_numero(formation_id, semestre_id=None):
def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False): def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
"""Delete an UE""" """Delete an UE"""
ues = ue_list(args={"ue_id": ue_id}) ue = UniteEns.query.get_or_404(ue_id)
if not ues: if ue.modules.all():
raise ScoValueError("UE inexistante !") raise ScoValueError(
ue = ues[0] f"""Suppression de l'UE {ue.titre} impossible car
des modules (ou SAÉ ou ressources) lui sont rattachés."""
if not dialog_confirmed: )
return scu.confirm_dialog( if not can_delete_ue(ue):
"<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue, raise ScoNonEmptyFormationObject(
dest_url="", "UE",
parameters={"ue_id": ue_id}, msg=ue.titre,
cancel_url=url_for( dest_url=url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]), formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
), ),
) )
return do_ue_delete(ue_id, delete_validations=delete_validations) if not dialog_confirmed:
return scu.confirm_dialog(
f"<h2>Suppression de l'UE {ue.titre} ({ue.acronyme})</h2>",
dest_url="",
parameters={"ue_id": ue.id},
cancel_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
),
)
return do_ue_delete(ue.id, delete_validations=delete_validations)
def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
@ -1010,12 +1046,14 @@ def _ue_table_modules(
H.append(arrow_none) H.append(arrow_none)
im += 1 im += 1
if mod["nb_moduleimpls"] == 0 and editable: if mod["nb_moduleimpls"] == 0 and editable:
H.append( icon = delete_icon
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (mod["module_id"], delete_icon)
)
else: else:
H.append(delete_disabled_icon) icon = delete_disabled_icon
H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (mod["module_id"], icon)
)
H.append("</span>") H.append("</span>")
mod_editable = ( mod_editable = (

View File

@ -52,7 +52,7 @@ class InvalidNoteValue(ScoException):
# Exception qui stoque dest_url, utilisee dans Zope standard_error_message # Exception qui stoque dest_url, utilisee dans Zope standard_error_message
class ScoValueError(ScoException): class ScoValueError(ScoException):
def __init__(self, msg, dest_url=None): def __init__(self, msg, dest_url=None):
ScoException.__init__(self, msg) super().__init__(msg)
self.dest_url = dest_url self.dest_url = dest_url
@ -72,20 +72,35 @@ class ScoConfigurationError(ScoValueError):
pass pass
class ScoLockedFormError(ScoException): class ScoLockedFormError(ScoValueError):
def __init__(self, msg=""): "Modification d'une formation verrouillée"
def __init__(self, msg="", dest_url=None):
msg = ( msg = (
"Cette formation est verrouillée (car il y a un semestre verrouillé qui s'y réfère). " "Cette formation est verrouillée (car il y a un semestre verrouillé qui s'y réfère). "
+ str(msg) + str(msg)
) )
ScoException.__init__(self, msg) super().__init__(msg=msg, dest_url=dest_url)
class ScoNonEmptyFormationObject(ScoValueError):
"""On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent"""
def __init__(self, type_objet="objet'", msg="", dest_url=None):
msg = f"""<h3>{type_objet} "{msg}" utilisé dans des semestres: suppression impossible.</h3>
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce {type_objet}).
Mais il est peut-être préférable de laisser ce programme intact et d'en créer une
nouvelle version pour la modifier sans affecter les semestres déjà en place.
</p>
"""
super().__init__(msg=msg, dest_url=dest_url)
class ScoGenError(ScoException): class ScoGenError(ScoException):
"exception avec affichage d'une page explicative ad-hoc" "exception avec affichage d'une page explicative ad-hoc"
def __init__(self, msg=""): def __init__(self, msg=""):
ScoException.__init__(self, msg) super().__init__(msg)
class AccessDenied(ScoGenError): class AccessDenied(ScoGenError):
@ -101,7 +116,7 @@ class APIInvalidParams(Exception):
status_code = 400 status_code = 400
def __init__(self, message, status_code=None, payload=None): def __init__(self, message, status_code=None, payload=None):
Exception.__init__(self) super().__init__()
self.message = message self.message = message
if status_code is not None: if status_code is not None:
self.status_code = status_code self.status_code = status_code

View File

@ -198,10 +198,13 @@ def do_formsemestre_createwithmodules(edit=False):
NB_SEM = parcours.NB_SEM NB_SEM = parcours.NB_SEM
else: else:
NB_SEM = 10 # fallback, max 10 semestres NB_SEM = 10 # fallback, max 10 semestres
semestre_id_list = [-1] + list(range(1, NB_SEM + 1)) if NB_SEM == 1:
semestre_id_list = [-1]
else:
semestre_id_list = [-1] + list(range(1, NB_SEM + 1))
semestre_id_labels = [] semestre_id_labels = []
for sid in semestre_id_list: for sid in semestre_id_list:
if sid == "-1": if sid == -1:
semestre_id_labels.append("pas de semestres") semestre_id_labels.append("pas de semestres")
else: else:
semestre_id_labels.append(f"S{sid}") semestre_id_labels.append(f"S{sid}")
@ -329,6 +332,8 @@ def do_formsemestre_createwithmodules(edit=False):
"labels": modalites_titles, "labels": modalites_titles,
}, },
), ),
]
modform.append(
( (
"semestre_id", "semestre_id",
{ {
@ -338,7 +343,7 @@ def do_formsemestre_createwithmodules(edit=False):
"labels": semestre_id_labels, "labels": semestre_id_labels,
}, },
), ),
] )
etapes = sco_portal_apogee.get_etapes_apogee_dept() etapes = sco_portal_apogee.get_etapes_apogee_dept()
# Propose les etapes renvoyées par le portail # Propose les etapes renvoyées par le portail
# et ajoute les étapes du semestre qui ne sont pas dans la liste (soit la liste a changé, soit l'étape a été ajoutée manuellement) # et ajoute les étapes du semestre qui ne sont pas dans la liste (soit la liste a changé, soit l'étape a été ajoutée manuellement)

View File

@ -49,16 +49,12 @@ from app.scodoc import sco_etud
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_parcours_dut from app.scodoc import sco_parcours_dut
from app.scodoc import sco_pdf
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
import sco_version import sco_version
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app import log from app import log
from app.scodoc.sco_codes_parcours import code_semestre_validant from app.scodoc.sco_codes_parcours import code_semestre_validant
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_pdf import SU
MAX_ETUD_IN_DESCR = 20 MAX_ETUD_IN_DESCR = 20
@ -121,9 +117,9 @@ def _categories_and_results(etuds, category, result):
categories[etud[category]] = True categories[etud[category]] = True
results[etud[result]] = True results[etud[result]] = True
categories = list(categories.keys()) categories = list(categories.keys())
categories.sort() categories.sort(key=scu.heterogeneous_sorting_key)
results = list(results.keys()) results = list(results.keys())
results.sort() results.sort(key=scu.heterogeneous_sorting_key)
return categories, results return categories, results
@ -166,7 +162,7 @@ def _results_by_category(
l["sumpercent"] = "%2.1f%%" % ((100.0 * l["sum"]) / tot) l["sumpercent"] = "%2.1f%%" % ((100.0 * l["sum"]) / tot)
# #
codes = list(results.keys()) codes = list(results.keys())
codes.sort() codes.sort(key=scu.heterogeneous_sorting_key)
bottom_titles = [] bottom_titles = []
if C: # ligne du bas avec totaux: if C: # ligne du bas avec totaux:
@ -314,7 +310,7 @@ def formsemestre_report_counts(
"type_admission", "type_admission",
"boursier_prec", "boursier_prec",
] ]
keys.sort() keys.sort(key=scu.heterogeneous_sorting_key)
F = [ F = [
"""<form name="f" method="get" action="%s"><p> """<form name="f" method="get" action="%s"><p>
Colonnes: <select name="result" onchange="document.f.submit()">""" Colonnes: <select name="result" onchange="document.f.submit()">"""
@ -497,7 +493,7 @@ def table_suivi_cohorte(
P.append(p) P.append(p)
# 4-- regroupe par indice de semestre S_i # 4-- regroupe par indice de semestre S_i
indices_sems = list(set([s["semestre_id"] for s in sems])) indices_sems = list({s["semestre_id"] for s in sems})
indices_sems.sort() indices_sems.sort()
for p in P: for p in P:
p.nb_etuds = 0 # nombre total d'etudiants dans la periode p.nb_etuds = 0 # nombre total d'etudiants dans la periode
@ -788,9 +784,9 @@ def _gen_form_selectetuds(
): ):
"""HTML form pour choix criteres selection etudiants""" """HTML form pour choix criteres selection etudiants"""
bacs = list(bacs) bacs = list(bacs)
bacs.sort() bacs.sort(key=scu.heterogeneous_sorting_key)
bacspecialites = list(bacspecialites) bacspecialites = list(bacspecialites)
bacspecialites.sort() bacspecialites.sort(key=scu.heterogeneous_sorting_key)
# on peut avoir un mix de chaines vides et d'entiers: # on peut avoir un mix de chaines vides et d'entiers:
annee_bacs = [int(x) if x else 0 for x in annee_bacs] annee_bacs = [int(x) if x else 0 for x in annee_bacs]
annee_bacs.sort() annee_bacs.sort()

View File

@ -93,6 +93,7 @@ MODULE_TYPE_NAMES = {
ModuleType.MALUS: "Malus", ModuleType.MALUS: "Malus",
ModuleType.RESSOURCE: "Ressource", ModuleType.RESSOURCE: "Ressource",
ModuleType.SAE: "SAÉ", ModuleType.SAE: "SAÉ",
None: "Module",
} }
MALUS_MAX = 20.0 MALUS_MAX = 20.0
@ -897,6 +898,11 @@ def sort_dates(L, reverse=False):
raise raise
def heterogeneous_sorting_key(x):
"key to sort non homogeneous sequences"
return (float(x), "") if isinstance(x, (bool, float, int)) else (-1e34, str(x))
def query_portal(req, msg="Portail Apogee", timeout=3): def query_portal(req, msg="Portail Apogee", timeout=3):
"""Retreives external data using HTTP request """Retreives external data using HTTP request
(used to connect to Apogee portal, or ScoDoc server) (used to connect to Apogee portal, or ScoDoc server)

View File

@ -5,6 +5,12 @@
Prénom : {{ contact.prenom }}<br> Prénom : {{ contact.prenom }}<br>
Téléphone : {{ contact.telephone }}<br> Téléphone : {{ contact.telephone }}<br>
Mail : {{ contact.mail }}<br> Mail : {{ contact.mail }}<br>
{% if contact.poste %}
Poste : {{ contact.poste }}<br>
{% endif %}
{% if contact.service %}
Service : {{ contact.service }}<br>
{% endif %}
</p> </p>
<div style="margin-bottom: 10px;"> <div style="margin-bottom: 10px;">

View File

@ -22,6 +22,8 @@
<th>Prenom</th> <th>Prenom</th>
<th>Telephone</th> <th>Telephone</th>
<th>Mail</th> <th>Mail</th>
<th>Poste</th>
<th>Service</th>
<th>Entreprise</th> <th>Entreprise</th>
</tr> </tr>
{% for contact in contacts %} {% for contact in contacts %}
@ -30,6 +32,8 @@
<th>{{ contact[0].prenom }}</th> <th>{{ contact[0].prenom }}</th>
<th>{{ contact[0].telephone }}</th> <th>{{ contact[0].telephone }}</th>
<th>{{ contact[0].mail }}</th> <th>{{ contact[0].mail }}</th>
<th>{{ contact[0].poste}}</th>
<th>{{ contact[0].service}}</th>
<th><a href="{{ url_for('entreprises.fiche_entreprise', id=contact[1].id) }}">{{ contact[1].nom }}</a></th> <th><a href="{{ url_for('entreprises.fiche_entreprise', id=contact[1].id) }}">{{ contact[1].nom }}</a></th>
</tr> </tr>
{% endfor %} {% endfor %}
@ -41,6 +45,7 @@
<div> <div>
{% if contacts %} {% if contacts %}
<a class="btn btn-default" href="{{ url_for('entreprises.export_contacts') }}">Exporter la liste des contacts</a> <a class="btn btn-default" href="{{ url_for('entreprises.export_contacts') }}">Exporter la liste des contacts</a>
<a class="btn btn-default" href="{{ url_for('entreprises.export_contacts_bis') }}">Exporter la liste des contacts avec leur entreprise</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -24,8 +24,8 @@
<span style="margin-right: 10px;">{{ data[0].date_debut.strftime('%d/%m/%Y') }} - {{ <span style="margin-right: 10px;">{{ data[0].date_debut.strftime('%d/%m/%Y') }} - {{
data[0].date_fin.strftime('%d/%m/%Y') }}</span> data[0].date_fin.strftime('%d/%m/%Y') }}</span>
<span style="margin-right: 10px;"> <span style="margin-right: 10px;">
{{ data[0].type_offre }} réalisé par {{ data[1].nom|format_nom }} {{ data[1].prenom|format_prenom }} en {{ data[0].type_offre }} réalisé par {{ data[1].nom|format_nom }} {{ data[1].prenom|format_prenom }}
{{ data[0].formation_text }} {% if data[0].formation_text %} en {{ data[0].formation_text }}{% endif %}
</span> </span>
</li> </li>
{% endfor %} {% endfor %}

View File

@ -24,13 +24,10 @@
{{icons.arrow_none|safe}} {{icons.arrow_none|safe}}
{% endif %} {% endif %}
</span> </span>
{% if editable and not ue.modules.count() %}
<a class="smallbutton" href="{{ url_for('notes.ue_delete', <a class="smallbutton" href="{{ url_for('notes.ue_delete',
scodoc_dept=g.scodoc_dept, ue_id=ue.id) scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}}">{{icons.delete|safe}}</a> }}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else %}{{icons.delete_disabled|safe}}{% endif %}</a>
{% else %}
{{icons.delete_disabled|safe}}
{% endif %}
<b>{{ue.acronyme}}</b> <a class="discretelink" href="{{ <b>{{ue.acronyme}}</b> <a class="discretelink" href="{{
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}" url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}"

View File

@ -8,10 +8,9 @@
{{ exc | safe }} {{ exc | safe }}
<p class="footer"> <p>
{% if g.scodoc_dept %} {% if g.scodoc_dept %}
<a href="{{ exc.dest_url or url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}">retour page d'accueil <a href="{{ exc.dest_url or url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}">continuer</a>
departement {{ g.scodoc_dept }}</a>
{% else %} {% else %}
<a href="{{ exc.dest_url or url_for('scodoc.index') }}">retour page d'accueil</a> <a href="{{ exc.dest_url or url_for('scodoc.index') }}">retour page d'accueil</a>
{% endif %} {% endif %}

View File

@ -13,11 +13,23 @@
<ul class="main"> <ul class="main">
{% for dept in depts %} {% for dept in depts %}
{% if dept.visible or current_user.is_administrator() %}
<li> <li>
<a class="stdlink {{'link_accessible' if current_user.has_permission(Permission.ScoView, dept=dept.acronym) else 'link_unauthorized'}}" <a class="stdlink {{'link_accessible' if current_user.has_permission(Permission.ScoView, dept=dept.acronym) else 'link_unauthorized'}}"
href="{{url_for('scolar.index_html', scodoc_dept=dept.acronym)}}">Département href="{{url_for('scolar.index_html', scodoc_dept=dept.acronym)}}">Département
{{dept.preferences.filter_by(name="DeptName").first().value}}</a> {{dept.preferences.filter_by(name="DeptName").first().value}}
{{ dept.preferences.filter_by( name="DeptFullName" ).first().value or "" }}
</a>
{% if current_user.is_administrator() %}
<span class="dept_visibility">
{% if dept.visible %}visible{% else %}caché aux utilisateurs{% endif %}
<a href="{{ url_for('scodoc.toggle_dept_vis', dept_id=dept.id) }}">
{% if dept.visible %}cacher{% else %}rendre visible{% endif %}
</a>
</span>
{% endif %}
</li> </li>
{% endif %}
{% else %} {% else %}
<li> <li>
<b>Aucun département défini !</b> <b>Aucun département défini !</b>

View File

@ -53,6 +53,7 @@ from wtforms.fields.simple import BooleanField, StringField, TextAreaField, Hidd
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
import app import app
from app import db
from app.forms.main import config_forms from app.forms.main import config_forms
from app.forms.main.create_dept import CreateDeptForm from app.forms.main.create_dept import CreateDeptForm
from app.models import Departement, Identite from app.models import Departement, Identite
@ -82,9 +83,7 @@ from PIL import Image as PILImage
@bp.route("/ScoDoc/index") @bp.route("/ScoDoc/index")
def index(): def index():
"Page d'accueil: liste des départements" "Page d'accueil: liste des départements"
depts = ( depts = Departement.query.filter_by().order_by(Departement.acronym).all()
Departement.query.filter_by(visible=True).order_by(Departement.acronym).all()
)
return render_template( return render_template(
"scodoc.html", "scodoc.html",
title=sco_version.SCONAME, title=sco_version.SCONAME,
@ -108,7 +107,11 @@ def create_dept():
if request.method == "POST" and form.cancel.data: # cancel button if request.method == "POST" and form.cancel.data: # cancel button
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
if form.validate_on_submit(): if form.validate_on_submit():
departements.create_dept(form.acronym.data) departements.create_dept(
form.acronym.data,
visible=form.visible.data,
# description=form.description.data,
)
flash(f"Département {form.acronym.data} créé.") flash(f"Département {form.acronym.data} créé.")
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
return render_template( return render_template(
@ -118,6 +121,17 @@ def create_dept():
) )
@bp.route("/ScoDoc/toggle_dept_vis/<dept_id>", methods=["GET", "POST"])
@admin_required
def toggle_dept_vis(dept_id):
"""Cache ou rend visible un dept"""
dept = Departement.query.get_or_404(dept_id)
dept.visible = not dept.visible
db.session.add(dept)
db.session.commit()
return redirect(url_for("scodoc.index"))
@bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"]) @bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"])
@login_required @login_required
def table_etud_in_accessible_depts(): def table_etud_in_accessible_depts():

View File

@ -152,7 +152,6 @@ def user_info(user_name, format="json"):
def create_user_form(user_name=None, edit=0, all_roles=1): def create_user_form(user_name=None, edit=0, all_roles=1):
"form. création ou edition utilisateur" "form. création ou edition utilisateur"
auth_dept = current_user.dept auth_dept = current_user.dept
auth_username = current_user.user_name
from_mail = current_user.email from_mail = current_user.email
initvalues = {} initvalues = {}
edit = int(edit) edit = int(edit)
@ -204,7 +203,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
administrable_dept_acronyms = sorted( administrable_dept_acronyms = sorted(
set( set(
[ [
x.dept x.dept or ""
for x in UserRole.query.filter_by(user=current_user) for x in UserRole.query.filter_by(user=current_user)
if x.role.has_permission(Permission.ScoUsersAdmin) and x.dept if x.role.has_permission(Permission.ScoUsersAdmin) and x.dept
] ]
@ -249,7 +248,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
r.name + "_" + (dept or "") for (r, dept) in displayed_roles r.name + "_" + (dept or "") for (r, dept) in displayed_roles
] ]
displayed_roles_labels = [f"{dept}: {r.name}" for (r, dept) in displayed_roles] displayed_roles_labels = [f"{dept}: {r.name}" for (r, dept) in displayed_roles]
disabled_roles = {} # pour desactiver les roles que l'on ne peut pas editer disabled_roles = {} # pour désactiver les roles que l'on ne peut pas éditer
for i in range(len(displayed_roles_strings)): for i in range(len(displayed_roles_strings)):
if displayed_roles_strings[i] not in editable_roles_strings: if displayed_roles_strings[i] not in editable_roles_strings:
disabled_roles[i] = True disabled_roles[i] = True
@ -375,7 +374,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
can_choose_dept = True can_choose_dept = True
else: else:
selectable_dept_acronyms = set(administrable_dept_acronyms) selectable_dept_acronyms = set(administrable_dept_acronyms)
if edit: # ajoute dept actuel de l'utilisateur if edit and the_user.dept is not None: # ajoute dept actuel de l'utilisateur
selectable_dept_acronyms |= {the_user.dept} selectable_dept_acronyms |= {the_user.dept}
if len(selectable_dept_acronyms) > 1: if len(selectable_dept_acronyms) > 1:
can_choose_dept = True can_choose_dept = True
@ -389,6 +388,9 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
"explanation": """département de rattachement de l'utilisateur""", "explanation": """département de rattachement de l'utilisateur""",
"labels": selectable_dept_acronyms, "labels": selectable_dept_acronyms,
"allowed_values": selectable_dept_acronyms, "allowed_values": selectable_dept_acronyms,
"default": g.scodoc_dept
if g.scodoc_dept in selectable_dept_acronyms
else "",
}, },
) )
) )

View File

@ -97,6 +97,33 @@ def upgrade():
sa.PrimaryKeyConstraint("id"), sa.PrimaryKeyConstraint("id"),
) )
op.create_table(
"entreprise_envoi_offre_etudiant",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("sender_id", sa.Integer(), nullable=True),
sa.Column("receiver_id", sa.Integer(), nullable=True),
sa.Column("offre_id", sa.Integer(), nullable=True),
sa.Column(
"date_envoi",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.ForeignKeyConstraint(
["offre_id"],
["entreprise_offre.id"],
),
sa.ForeignKeyConstraint(
["sender_id"],
["user.id"],
),
sa.ForeignKeyConstraint(
["receiver_id"],
["identite.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.drop_constraint( op.drop_constraint(
"entreprise_contact_entreprise_corresp_id_fkey", "entreprise_contact_entreprise_corresp_id_fkey",
"entreprise_contact", "entreprise_contact",
@ -249,6 +276,7 @@ def downgrade():
op.drop_column("entreprise_contact", "nom") op.drop_column("entreprise_contact", "nom")
op.drop_table("entreprise_envoi_offre") op.drop_table("entreprise_envoi_offre")
op.drop_table("entreprise_envoi_offre_etudiant")
op.drop_table("entreprise_offre") op.drop_table("entreprise_offre")
op.drop_table("entreprise_etudiant") op.drop_table("entreprise_etudiant")
op.drop_table("entreprise_log") op.drop_table("entreprise_log")

View File

@ -29,7 +29,6 @@ from app.models import ModuleImpl, ModuleImplInscription
from app.models import Identite from app.models import Identite
from app.models import departements from app.models import departements
from app.models.evaluations import Evaluation from app.models.evaluations import Evaluation
from app.scodoc.sco_etud import identite_create
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.views import notes, scolar from app.views import notes, scolar
import tools import tools
@ -248,6 +247,7 @@ def edit_role(rolename, addpermissionname=None, removepermissionname=None): # e
db.session.add(role) db.session.add(role)
db.session.commit() db.session.commit()
@app.cli.command() @app.cli.command()
@click.argument("rolename") @click.argument("rolename")
def delete_role(rolename): def delete_role(rolename):
@ -259,6 +259,7 @@ def delete_role(rolename):
db.session.delete(role) db.session.delete(role)
db.session.commit() db.session.commit()
@app.cli.command() @app.cli.command()
@click.argument("username") @click.argument("username")
@click.option("-d", "--dept", "dept_acronym") @click.option("-d", "--dept", "dept_acronym")

View File

@ -42,17 +42,17 @@
# - do_formation_delete # - do_formation_delete
import json import json
import xml.dom.minidom
import flask import flask
from flask import g from flask import g
import pytest
from tests.unit import sco_fake_gen from tests.unit import sco_fake_gen
from app.scodoc import sco_edit_formation from app.scodoc import sco_edit_formation
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_exceptions
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre_edit from app.scodoc import sco_formsemestre_edit
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
@ -273,31 +273,31 @@ def test_formations(test_client):
# --- Suppression du module, matiere et ue test du semestre 2 # --- Suppression du module, matiere et ue test du semestre 2
# on doit d'abbord supprimer le semestre # on doit d'abord supprimer le semestre:
# sco_formsemestre_edit.formsemestre_delete( formsemestre_id=sem2["formsemestre_id"])
# sco_formsemestre_edit.formsemestre_createwithmodules( formsemestre_id=sem2["formsemestre_id"])
# RIEN NE SE PASSE AVEC CES FONCTIONS
sco_formsemestre_edit.do_formsemestre_delete( sco_formsemestre_edit.do_formsemestre_delete(
formsemestre_id=sem2["formsemestre_id"] formsemestre_id=sem2["formsemestre_id"]
) )
# sco_edit_module.module_delete( module_id=modt["module_id"])
# sco_edit_matiere.matiere_delete( matiere_id=matt["matiere_id"])
# sco_edit_ue.ue_delete( ue_id=uet["ue_id"])
# RIEN NE SE PASSE AVEC CES FONCTIONS
li_module = sco_edit_module.module_list() li_module = sco_edit_module.module_list()
assert len(li_module) == 4 assert len(li_module) == 4
sco_edit_module.do_module_delete(oid=modt["module_id"]) # on supprime le semestre # Suppression impossible car utilisé dans le semestre semt:
# sco_formsemestre_edit.formsemestre_delete_moduleimpls( formsemestre_id=sem2["formsemestre_id"], module_ids_to_del=[modt["module_id"]]) with pytest.raises(sco_exceptions.ScoNonEmptyFormationObject):
# deuxieme methode de supression d'un module sco_edit_module.module_delete(module_id=mi3["module_id"])
li_module2 = sco_edit_module.module_list()
assert len(li_module2) == 3 # verification de la suppression du module sco_formsemestre_edit.do_formsemestre_delete(semt["formsemestre_id"])
li_module2_before = sco_edit_module.module_list()
sco_edit_module.do_module_delete(mi3["module_id"])
sco_edit_module.do_module_delete(modt["module_id"])
# deuxieme methode de supression d'un module
li_module2_after = sco_edit_module.module_list()
assert (
len(li_module2_after) == len(li_module2_before) - 2
) # verification de la suppression
lim_sem2 = sco_moduleimpl.moduleimpl_list(formsemestre_id=sem2["formsemestre_id"]) lim_sem2 = sco_moduleimpl.moduleimpl_list(formsemestre_id=sem2["formsemestre_id"])
@ -316,10 +316,6 @@ def test_formations(test_client):
assert len(li_ue2) == 3 # verification de la suppression de l'UE assert len(li_ue2) == 3 # verification de la suppression de l'UE
# --- Suppression d'une formation # --- Suppression d'une formation
# Il faut d'abbord supprimer le semestre aussi.
sco_formsemestre_edit.do_formsemestre_delete(
formsemestre_id=semt["formsemestre_id"]
)
sco_edit_formation.do_formation_delete(oid=f2["formation_id"]) sco_edit_formation.do_formation_delete(oid=f2["formation_id"])
lif3 = notes.formation_list(format="json").get_data(as_text=True) lif3 = notes.formation_list(format="json").get_data(as_text=True)

View File

@ -64,7 +64,7 @@ fi
# ------------ LIEN VERS .env # ------------ LIEN VERS .env
# Pour conserver le .env entre les mises à jour, on le génère dans # Pour conserver le .env entre les mises à jour, on le génère dans
# /opt/scodoc-data/;env et on le lie: # /opt/scodoc-data/.env et on le lie:
if [ ! -e "$SCODOC_DIR/.env" ] && [ ! -L "$SCODOC_DIR/.env" ] if [ ! -e "$SCODOC_DIR/.env" ] && [ ! -L "$SCODOC_DIR/.env" ]
then then
ln -s "$SCODOC_VAR_DIR/.env" "$SCODOC_DIR" ln -s "$SCODOC_VAR_DIR/.env" "$SCODOC_DIR"

View File

@ -24,53 +24,74 @@ usage() {
exit 1 exit 1
} }
# analyse de la ligne de commande
# calcule:
# SRC = fichier source de la restauration
# DB_DEST = base de données destination
# KEEP_ENV = vide si restauration à l'identique (i.e. production)
if (($# < 1 || $# > 2)) if (($# < 1 || $# > 2))
then then
usage usage
elif [ $# -eq 2 -a $1 != '--keep-env' -a $2 != '--keep-env' ] ; then elif [ $# -eq 2 ] && [ "$1" != "--keep-env" ] && [ "$2" != "--keep-env" ]
then
usage usage
elif [ $# -eq 1 ] ; then elif [ $# -eq 1 ]
then
echo "restauration des données et de la configuration originale (production)" echo "restauration des données et de la configuration originale (production)"
SRC=$1 SRC="$1"
DB_DEST="SCODOC" DB_DEST="SCODOC"
else else
echo "restauration des données dans la configuration actuelle" echo "restauration des données dans la configuration actuelle"
DB_CURRENT=$(su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask scodoc-database -n)") DB_CURRENT=$(su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask scodoc-database -n)")
DB_DEST="$DB_CURRENT" DB_DEST="$DB_CURRENT"
KEEP=1 KEEP_ENV="Y"
if [ $1 = '--keep-env' ]; then if [ "$1" = "--keep-env" ]
SRC=$2 then
SRC="$2"
else else
SRC=$1 SRC="$1"
fi fi
fi fi
DB_DUMP="${SCODOC_VAR_DIR}"/SCODOC.dump DB_DUMP="${SCODOC_VAR_DIR}"/SCODOC.dump
# Safety check # Safety check
echo "Ce script va remplacer les donnees de votre installation ScoDoc par celles" echo "Ce script va remplacer les données de votre installation ScoDoc par celles"
echo "enregistrées dans le fichier fourni." echo "enregistrées dans le fichier fourni."
echo "Ce fichier doit avoir ete cree par le script save_scodoc9_data.sh." echo "Ce fichier doit avoir été créé par le script save_scodoc9_data.sh."
echo echo
echo "Attention: TOUTES LES DONNEES DE CE SCODOC SERONT REMPLACEES !" echo "Attention: TOUTES LES DONNEES DE CE SCODOC SERONT REMPLACEES !"
echo "Notamment, tous les utilisateurs et departements existants seront effaces !" echo "Notamment, tous les utilisateurs et départements existants seront effacés !"
echo echo
echo "La base SQL $DB_CURRENT sera effacée et remplacée !!!" echo "La base SQL $DB_CURRENT sera effacée et remplacée !!!"
echo echo
echo -n "Voulez vous poursuivre cette operation ? (y/n) [n]" # Préparation si une copie 'antique' doit être effacée, demander confirmation, puis effacer
read -r ans SCODOC_VAR_OLD=${SCODOC_VAR_DIR}.old
if [ ! "$(norm_ans "$ans")" = 'Y' ] if [ -e "$SCODOC_VAR_DIR" ] && [ -e "$SCODOC_VAR_OLD" ]
then then
echo "Annulation" echo "Une ancienne sauvegarde (\"$SCODOC_VAR_OLD\" en date du $(stat -c %w "$SCODOC_VAR_OLD") ) va être effacée."
echo
fi
if [ -n "$KEEP_ENV" ]
then
echo -n "Restauration des données sans changement de configuration: Assurez-vous d'avoir arrêté le serveur scodoc."
echo
fi
echo -n "Voulez-vous poursuivre la restauration ? (y/n) [n]"
read -r ans
if [ ! "$(norm_ans "$ans")" = "Y" ]
then
echo "Annulation de la restauration par l\'utilisateur"
exit 1 exit 1
fi fi
rm -rf "$SCODOC_VAR_OLD" || die "Erreur suppression $SCODOC_VAR_OLD"
# -- Stop ScoDoc # -- Stop ScoDoc
if [ $KEEP -ne 1 ]; then if [ -z "$KEEP_ENV" ]
then
echo "Arrêt de scodoc9..." echo "Arrêt de scodoc9..."
systemctl stop scodoc9 systemctl stop scodoc9
else
echo -n "Assurez-vous d'avoir arrété le serveur scodoc (validez pour continuer)"
read ans
fi fi
# Clear caches # Clear caches
@ -86,7 +107,7 @@ fi
# -- Ouverture archive # -- Ouverture archive
echo "Ouverture archive $SRC..." echo "Ouverture archive $SRC..."
(cd $(dirname "$SCODOC_VAR_DIR"); tar xfz "$SRC") || die "Error opening archive" (cd "$(dirname "$SCODOC_VAR_DIR")"; tar xfz "$SRC") || die "Error opening archive"
# -- Ckeck/fix owner # -- Ckeck/fix owner
echo "Vérification du propriétaire..." echo "Vérification du propriétaire..."
@ -103,7 +124,7 @@ su -c "createdb $DB_DEST" "$SCODOC_USER" || die "Erreur création db"
if [ ! -z $KEEP_ENV ] ; then if [ ! -z $KEEP_ENV ] ; then
echo "conservation de la configuration actuelle" echo "conservation de la configuration actuelle"
cp "$SCODOC_VAR_DIR".old/.env "$SCODOC_VAR_DIR"/.env cp -p "$SCODOC_VAR_OLD"/.env "$SCODOC_VAR_DIR"/.env
echo "récupération des données..." echo "récupération des données..."
su -c "pg_restore -f - $DB_DUMP | psql -q $DB_DEST" "$SCODOC_USER" >/dev/null || die "Erreur chargement/renommage de la base SQL" su -c "pg_restore -f - $DB_DUMP | psql -q $DB_DEST" "$SCODOC_USER" >/dev/null || die "Erreur chargement/renommage de la base SQL"
su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask db upgrade)" "$SCODOC_USER" su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask db upgrade)" "$SCODOC_USER"
@ -119,5 +140,4 @@ else
systemctl start scodoc9 systemctl start scodoc9
fi fi
echo "Terminé." echo "Terminé."