forked from ScoDoc/ScoDoc
Nouvelle fonction pour copier un étudiant vers un autre département.
This commit is contained in:
parent
4656d9547e
commit
548a881d59
@ -353,8 +353,8 @@ class User(UserMixin, db.Model):
|
|||||||
return mails
|
return mails
|
||||||
|
|
||||||
# Permissions management:
|
# Permissions management:
|
||||||
def has_permission(self, perm: int, dept=False):
|
def has_permission(self, perm: int, dept: str = False):
|
||||||
"""Check if user has permission `perm` in given `dept`.
|
"""Check if user has permission `perm` in given `dept` (acronym).
|
||||||
Similar to Zope ScoDoc7 `has_permission``
|
Similar to Zope ScoDoc7 `has_permission``
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -80,8 +80,6 @@ class Departement(db.Model):
|
|||||||
|
|
||||||
def create_dept(acronym: str, visible=True) -> Departement:
|
def create_dept(acronym: str, visible=True) -> Departement:
|
||||||
"Create new departement"
|
"Create new departement"
|
||||||
from app.models import ScoPreference
|
|
||||||
|
|
||||||
if Departement.invalid_dept_acronym(acronym):
|
if Departement.invalid_dept_acronym(acronym):
|
||||||
raise ScoValueError("acronyme departement invalide")
|
raise ScoValueError("acronyme departement invalide")
|
||||||
existing = Departement.query.filter_by(acronym=acronym).count()
|
existing = Departement.query.filter_by(acronym=acronym).count()
|
||||||
|
@ -86,6 +86,31 @@ class Identite(db.Model):
|
|||||||
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clone(self, not_copying=(), new_dept_id: int = None):
|
||||||
|
"""Clone, not copying the given attrs
|
||||||
|
Clone aussi les adresses.
|
||||||
|
Si new_dept_id est None, le nouvel étudiant n'a pas de département.
|
||||||
|
Attention: la copie n'a pas d'id avant le prochain flush ou commit.
|
||||||
|
"""
|
||||||
|
if new_dept_id == self.dept_id:
|
||||||
|
raise ScoValueError(
|
||||||
|
"clonage étudiant: le département destination est identique à celui de départ"
|
||||||
|
)
|
||||||
|
d = dict(self.__dict__)
|
||||||
|
d.pop("id", None) # get rid of id
|
||||||
|
d.pop("_sa_instance_state", None) # get rid of SQLAlchemy special attr
|
||||||
|
d.pop("departement", None) # relationship
|
||||||
|
d["dept_id"] = new_dept_id
|
||||||
|
for k in not_copying:
|
||||||
|
d.pop(k, None)
|
||||||
|
copy = self.__class__(**d)
|
||||||
|
copy.adresses = [adr.clone() for adr in self.adresses]
|
||||||
|
db.session.add(copy)
|
||||||
|
log(
|
||||||
|
f"cloning etud <{self.id} {self.nom!r} {self.prenom!r}> in dept_id={new_dept_id}"
|
||||||
|
)
|
||||||
|
return copy
|
||||||
|
|
||||||
def html_link_fiche(self) -> str:
|
def html_link_fiche(self) -> str:
|
||||||
"lien vers la fiche"
|
"lien vers la fiche"
|
||||||
return f"""<a class="stdlink" href="{
|
return f"""<a class="stdlink" href="{
|
||||||
@ -660,6 +685,19 @@ class Adresse(db.Model):
|
|||||||
)
|
)
|
||||||
description = db.Column(db.Text)
|
description = db.Column(db.Text)
|
||||||
|
|
||||||
|
def clone(self, not_copying=()):
|
||||||
|
"""Clone, not copying the given attrs
|
||||||
|
Attention: la copie n'a pas d'id avant le prochain flush ou commit.
|
||||||
|
"""
|
||||||
|
d = dict(self.__dict__)
|
||||||
|
d.pop("id", None) # get rid of id
|
||||||
|
d.pop("_sa_instance_state", None) # get rid of SQLAlchemy special attr
|
||||||
|
for k in not_copying:
|
||||||
|
d.pop(k, None)
|
||||||
|
copy = self.__class__(**d)
|
||||||
|
db.session.add(copy)
|
||||||
|
return copy
|
||||||
|
|
||||||
def to_dict(self, convert_nulls_to_str=False):
|
def to_dict(self, convert_nulls_to_str=False):
|
||||||
"""Représentation dictionnaire,"""
|
"""Représentation dictionnaire,"""
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
|
@ -177,11 +177,15 @@ class FormSemestre(db.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_formsemestre(cls, formsemestre_id: int) -> "FormSemestre":
|
def get_formsemestre(
|
||||||
""" "FormSemestre ou 404, cherche uniquement dans le département courant"""
|
cls, formsemestre_id: int, dept_id: int = None
|
||||||
|
) -> "FormSemestre":
|
||||||
|
""" "FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant"""
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
|
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
||||||
|
if dept_id is not None:
|
||||||
return cls.query.filter_by(
|
return cls.query.filter_by(
|
||||||
id=formsemestre_id, dept_id=g.scodoc_dept_id
|
id=formsemestre_id, dept_id=dept_id
|
||||||
).first_or_404()
|
).first_or_404()
|
||||||
return cls.query.filter_by(id=formsemestre_id).first_or_404()
|
return cls.query.filter_by(id=formsemestre_id).first_or_404()
|
||||||
|
|
||||||
|
@ -275,14 +275,16 @@ def do_formsemestre_inscription_with_modules(
|
|||||||
etat=scu.INSCRIT,
|
etat=scu.INSCRIT,
|
||||||
etape=None,
|
etape=None,
|
||||||
method="inscription_with_modules",
|
method="inscription_with_modules",
|
||||||
|
dept_id: int = None,
|
||||||
):
|
):
|
||||||
"""Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
|
"""Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
|
||||||
(donc sauf le sport)
|
(donc sauf le sport)
|
||||||
|
Si dept_id est spécifié, utilise ce département au lieu du courant.
|
||||||
"""
|
"""
|
||||||
group_ids = group_ids or []
|
group_ids = group_ids or []
|
||||||
if isinstance(group_ids, int):
|
if isinstance(group_ids, int):
|
||||||
group_ids = [group_ids]
|
group_ids = [group_ids]
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id)
|
||||||
# inscription au semestre
|
# inscription au semestre
|
||||||
args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
|
args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
|
||||||
if etat is not None:
|
if etat is not None:
|
||||||
|
@ -642,6 +642,12 @@ def menus_etud(etudid):
|
|||||||
"args": {"etudid": etud["etudid"]},
|
"args": {"etudid": etud["etudid"]},
|
||||||
"enabled": authuser.has_permission(Permission.ScoEtudInscrit),
|
"enabled": authuser.has_permission(Permission.ScoEtudInscrit),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Copier dans un autre département...",
|
||||||
|
"endpoint": "scolar.etud_copy_in_other_dept",
|
||||||
|
"args": {"etudid": etud["etudid"]},
|
||||||
|
"enabled": authuser.has_permission(Permission.ScoEtudInscrit),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Supprimer cet étudiant...",
|
"title": "Supprimer cet étudiant...",
|
||||||
"endpoint": "scolar.etudident_delete",
|
"endpoint": "scolar.etudident_delete",
|
||||||
@ -656,7 +662,9 @@ def menus_etud(etudid):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return htmlutils.make_menu("Étudiant", menuEtud, alone=True)
|
return htmlutils.make_menu(
|
||||||
|
"Étudiant", menuEtud, alone=True, css_class="menu-etudiant"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def etud_info_html(etudid, with_photo="1", debug=False):
|
def etud_info_html(etudid, with_photo="1", debug=False):
|
||||||
|
@ -10,12 +10,12 @@
|
|||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--sco-color-background);
|
background-color: var(--sco-color-background);
|
||||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
font-size: 12pt;
|
font-size: 12pt;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
@ -24,6 +24,10 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.container {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3 {
|
h3 {
|
||||||
@ -1672,6 +1676,10 @@ formsemestre_page_title .lock img {
|
|||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-etudiant>li {
|
||||||
|
width: 200px !important;
|
||||||
|
}
|
||||||
|
|
||||||
span.inscr_addremove_menu {
|
span.inscr_addremove_menu {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
99
app/templates/scolar/etud_copy_in_other_dept.j2
Normal file
99
app/templates/scolar/etud_copy_in_other_dept.j2
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
{# -*- mode: jinja-html -*- #}
|
||||||
|
{% extends 'base.j2' %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{super()}}
|
||||||
|
<style>
|
||||||
|
.dept-name {
|
||||||
|
font-size: 120%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.dept {
|
||||||
|
background-color: bisque;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.dept label {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
button[name="action"] {
|
||||||
|
margin-right: 32px;
|
||||||
|
}
|
||||||
|
#submit-button:disabled {
|
||||||
|
background-color: #CCCCCC;
|
||||||
|
color: #888888;
|
||||||
|
cursor: not-allowed;
|
||||||
|
border: 1px solid #AAAAAA;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
|
||||||
|
<h2>Création d'une copie de {{ etud.html_link_fiche() | safe }}</h2>
|
||||||
|
|
||||||
|
<div class="help">
|
||||||
|
|
||||||
|
<p>Utiliser cette page lorsqu'un étudinat change de département. ScoDoc gère
|
||||||
|
séparéement les étudiants des départements. Il faut donc dans ce cas
|
||||||
|
exceptionnel créer une copie de l'étudiant et l'inscrire dans un semestre de son
|
||||||
|
nouveau département. Seules les donénes sur l'identité de l'étudiant (état
|
||||||
|
civil, adresse, ...) sont dupliquées. Dans le noveau département, les résultats
|
||||||
|
obtenus dans le département d'origine ne seront pas visibles.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Si des UEs ou compétences de l'ancien département doivent être validées dans
|
||||||
|
le nouveau, il faudra utiliser ensuite une "validation d'UE antérieure".
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Attention: seuls les départements dans lesquels vous avez la permission
|
||||||
|
d'inscrire des étudiants sont présentés ici. Il faudra peut-être solliciter
|
||||||
|
l'administrateur de ce ScoDoc.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Dans chaque département autorisés, seuls les semestres non verrouillés sont
|
||||||
|
montrés. Choisir le semestre destination et valider le formulaire.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Ensuite, ne pas oublier d'inscrire l'étudiant à ses groupes, notamment son
|
||||||
|
parcours si besoin.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{% for dept in departements.values() %}
|
||||||
|
<div class="dept">
|
||||||
|
<div class="dept-name">Département {{ dept.acronym }}</div>
|
||||||
|
{% for sem in formsemestres_by_dept[dept.id]%}
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type="radio" class="formsemestre" name="formsemestre_id" value="{{ sem.id }}">
|
||||||
|
{{ sem.html_link_status() | safe }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit" name="action" value="submit" disabled id="submit-button">Créer une copie de l'étudiant et l'inscrire au semestre choisi</button>
|
||||||
|
<button type="submit" name="action" value="cancel">Annuler</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const radioButtons = document.querySelectorAll('input.formsemestre');
|
||||||
|
const submitButton = document.getElementById('submit-button');
|
||||||
|
|
||||||
|
radioButtons.forEach(radioButton => {
|
||||||
|
radioButton.addEventListener('change', () => {
|
||||||
|
const isAnyRadioButtonChecked = [...radioButtons].some(radioButton => radioButton.checked);
|
||||||
|
if (isAnyRadioButtonChecked) {
|
||||||
|
submitButton.removeAttribute('disabled');
|
||||||
|
} else {
|
||||||
|
submitButton.setAttribute('disabled', 'disabled');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -31,11 +31,12 @@ issu de ScoDoc7 / ZScolar.py
|
|||||||
Emmanuel Viennet, 2021
|
Emmanuel Viennet, 2021
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import requests
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import url_for, flash, render_template, make_response
|
from flask import abort, flash, make_response, render_template, url_for
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
@ -43,6 +44,7 @@ from flask_wtf import FlaskForm
|
|||||||
from flask_wtf.file import FileField, FileAllowed
|
from flask_wtf.file import FileField, FileAllowed
|
||||||
from wtforms import SubmitField
|
from wtforms import SubmitField
|
||||||
|
|
||||||
|
import app
|
||||||
from app import db
|
from app import db
|
||||||
from app import log
|
from app import log
|
||||||
from app.decorators import (
|
from app.decorators import (
|
||||||
@ -52,6 +54,7 @@ from app.decorators import (
|
|||||||
permission_required_compat_scodoc7,
|
permission_required_compat_scodoc7,
|
||||||
)
|
)
|
||||||
from app.models import (
|
from app.models import (
|
||||||
|
Departement,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
Identite,
|
Identite,
|
||||||
Partition,
|
Partition,
|
||||||
@ -69,6 +72,7 @@ from app.scodoc.scolog import logdb
|
|||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_exceptions import (
|
from app.scodoc.sco_exceptions import (
|
||||||
AccessDenied,
|
AccessDenied,
|
||||||
|
ScoPermissionDenied,
|
||||||
ScoValueError,
|
ScoValueError,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1770,6 +1774,77 @@ def _etudident_create_or_edit_form(edit):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/etud_copy_in_other_dept/<int:etudid>", methods=["GET", "POST"])
|
||||||
|
@scodoc
|
||||||
|
@permission_required(
|
||||||
|
Permission.ScoView
|
||||||
|
) # il faut aussi ScoEtudInscrit dans le nouveau dept
|
||||||
|
def etud_copy_in_other_dept(etudid: int):
|
||||||
|
"""Crée une copie de l'étudiant (avec ses adresses et codes) dans un autre département
|
||||||
|
et l'inscrit à un formsemestre
|
||||||
|
"""
|
||||||
|
etud = Identite.get_etud(etudid)
|
||||||
|
if request.method == "POST":
|
||||||
|
action = request.form.get("action")
|
||||||
|
if action == "cancel":
|
||||||
|
return flask.redirect(
|
||||||
|
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
formsemestre_id = int(request.form.get("formsemestre_id"))
|
||||||
|
except ValueError:
|
||||||
|
log("etud_copy_in_other_dept: invalid formsemestre_id")
|
||||||
|
abort(404, description="formsemestre_id invalide")
|
||||||
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
if not current_user.has_permission(
|
||||||
|
Permission.ScoEtudInscrit, formsemestre.departement.acronym
|
||||||
|
):
|
||||||
|
raise ScoPermissionDenied("non autorisé")
|
||||||
|
new_etud = etud.clone(new_dept_id=formsemestre.dept_id)
|
||||||
|
db.session.commit()
|
||||||
|
# Attention: change le département pour opérer dans le nouveau
|
||||||
|
# avec les anciennes fonctions ScoDoc7
|
||||||
|
orig_dept = g.scodoc_dept
|
||||||
|
try:
|
||||||
|
app.set_sco_dept(formsemestre.departement.acronym, open_cnx=False)
|
||||||
|
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
|
||||||
|
formsemestre.id,
|
||||||
|
new_etud.id,
|
||||||
|
method="etud_copy_in_other_dept",
|
||||||
|
dept_id=formsemestre.dept_id,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
app.set_sco_dept(orig_dept, open_cnx=False)
|
||||||
|
flash(f"Etudiant dupliqué et inscrit en {formsemestre.departement.acronym}")
|
||||||
|
# Attention, ce redirect change de département !
|
||||||
|
return flask.redirect(
|
||||||
|
url_for(
|
||||||
|
"scolar.ficheEtud",
|
||||||
|
scodoc_dept=formsemestre.departement.acronym,
|
||||||
|
etudid=new_etud.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
departements = {
|
||||||
|
dept.id: dept
|
||||||
|
for dept in Departement.query.order_by(Departement.acronym)
|
||||||
|
if current_user.has_permission(Permission.ScoEtudInscrit, dept.acronym)
|
||||||
|
and dept.id != etud.dept_id
|
||||||
|
}
|
||||||
|
formsemestres_by_dept = {
|
||||||
|
dept.id: dept.formsemestres.filter_by(etat=True)
|
||||||
|
.filter(FormSemestre.modalite != "EXT")
|
||||||
|
.order_by(FormSemestre.date_debut, FormSemestre.semestre_id)
|
||||||
|
.all()
|
||||||
|
for dept in departements.values()
|
||||||
|
}
|
||||||
|
return render_template(
|
||||||
|
"scolar/etud_copy_in_other_dept.j2",
|
||||||
|
departements=departements,
|
||||||
|
etud=etud,
|
||||||
|
formsemestres_by_dept=formsemestres_by_dept,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudident_delete", methods=["GET", "POST"])
|
@bp.route("/etudident_delete", methods=["GET", "POST"])
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoEtudInscrit)
|
@permission_required(Permission.ScoEtudInscrit)
|
||||||
|
Loading…
Reference in New Issue
Block a user