forked from ScoDoc/ScoDoc
Update opolka/ScoDoc from ScoDoc/ScoDoc #2
@ -353,8 +353,8 @@ class User(UserMixin, db.Model):
|
||||
return mails
|
||||
|
||||
# Permissions management:
|
||||
def has_permission(self, perm: int, dept=False):
|
||||
"""Check if user has permission `perm` in given `dept`.
|
||||
def has_permission(self, perm: int, dept: str = False):
|
||||
"""Check if user has permission `perm` in given `dept` (acronym).
|
||||
Similar to Zope ScoDoc7 `has_permission``
|
||||
|
||||
Args:
|
||||
|
@ -80,8 +80,6 @@ class Departement(db.Model):
|
||||
|
||||
def create_dept(acronym: str, visible=True) -> Departement:
|
||||
"Create new departement"
|
||||
from app.models import ScoPreference
|
||||
|
||||
if Departement.invalid_dept_acronym(acronym):
|
||||
raise ScoValueError("acronyme departement invalide")
|
||||
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}>"
|
||||
)
|
||||
|
||||
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:
|
||||
"lien vers la fiche"
|
||||
return f"""<a class="stdlink" href="{
|
||||
@ -660,6 +685,19 @@ class Adresse(db.Model):
|
||||
)
|
||||
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):
|
||||
"""Représentation dictionnaire,"""
|
||||
e = dict(self.__dict__)
|
||||
|
@ -177,11 +177,15 @@ class FormSemestre(db.Model):
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_formsemestre(cls, formsemestre_id: int) -> "FormSemestre":
|
||||
""" "FormSemestre ou 404, cherche uniquement dans le département courant"""
|
||||
def get_formsemestre(
|
||||
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:
|
||||
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(
|
||||
id=formsemestre_id, dept_id=g.scodoc_dept_id
|
||||
id=formsemestre_id, dept_id=dept_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,
|
||||
etape=None,
|
||||
method="inscription_with_modules",
|
||||
dept_id: int = None,
|
||||
):
|
||||
"""Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
|
||||
(donc sauf le sport)
|
||||
Si dept_id est spécifié, utilise ce département au lieu du courant.
|
||||
"""
|
||||
group_ids = group_ids or []
|
||||
if isinstance(group_ids, int):
|
||||
group_ids = [group_ids]
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id)
|
||||
# inscription au semestre
|
||||
args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
|
||||
if etat is not None:
|
||||
|
@ -642,6 +642,12 @@ def menus_etud(etudid):
|
||||
"args": {"etudid": etud["etudid"]},
|
||||
"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...",
|
||||
"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):
|
||||
|
@ -10,12 +10,12 @@
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
background-color: var(--sco-color-background);
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 12pt;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media print {
|
||||
@ -24,6 +24,10 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
div.container {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
@ -1672,6 +1676,10 @@ formsemestre_page_title .lock img {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.menu-etudiant>li {
|
||||
width: 200px !important;
|
||||
}
|
||||
|
||||
span.inscr_addremove_menu {
|
||||
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
|
||||
"""
|
||||
import datetime
|
||||
import requests
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
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_json import as_json
|
||||
from flask_login import current_user
|
||||
@ -43,6 +44,7 @@ from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed
|
||||
from wtforms import SubmitField
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app import log
|
||||
from app.decorators import (
|
||||
@ -52,6 +54,7 @@ from app.decorators import (
|
||||
permission_required_compat_scodoc7,
|
||||
)
|
||||
from app.models import (
|
||||
Departement,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
Partition,
|
||||
@ -69,6 +72,7 @@ from app.scodoc.scolog import logdb
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
ScoPermissionDenied,
|
||||
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"])
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
|
Loading…
Reference in New Issue
Block a user