forked from ScoDoc/ScoDoc
- corrige saisi stage sur entreprise (fix #642)
- clé étrangère sur Identite dans EntrepriseStageApprentissage - nouveau mécanisme pour le choix d'étudiant via auto-completion (ajout de autoComplete.js-10.2.7) - nouveau point d'API: /etudiants/name/<string:start> (et son test unitaire)
This commit is contained in:
parent
111c400333
commit
8e1cb055f6
@ -8,16 +8,17 @@
|
||||
API : accès aux étudiants
|
||||
"""
|
||||
from datetime import datetime
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
from sqlalchemy import desc, or_
|
||||
from sqlalchemy import desc, func, or_
|
||||
from sqlalchemy.dialects.postgresql import VARCHAR
|
||||
|
||||
import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.api import tools
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
@ -31,6 +32,8 @@ from app.scodoc import sco_bulletins
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error, suppress_accents
|
||||
|
||||
|
||||
# Un exemple:
|
||||
# @bp.route("/api_function/<int:arg>")
|
||||
@ -164,12 +167,39 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
)
|
||||
if not None in allowed_depts:
|
||||
# restreint aux départements autorisés:
|
||||
etuds = etuds.join(Departement).filter(
|
||||
query = query.join(Departement).filter(
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
return [etud.to_dict_api() for etud in query]
|
||||
|
||||
|
||||
@bp.route("/etudiants/name/<string:start>")
|
||||
@api_web_bp.route("/etudiants/name/<string:start>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
||||
"""Liste des étudiants dont le nom débute par start.
|
||||
Si start fait moins de min_len=3 caractères, liste vide.
|
||||
La casse et les accents sont ignorés.
|
||||
"""
|
||||
if len(start) < min_len:
|
||||
return []
|
||||
start = suppress_accents(start).lower()
|
||||
query = Identite.query.filter(
|
||||
func.lower(func.unaccent(Identite.nom, type_=VARCHAR)).ilike(start + "%")
|
||||
)
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
||||
if not None in allowed_depts:
|
||||
# restreint aux départements autorisés:
|
||||
query = query.join(Departement).filter(
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
|
||||
# Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
|
||||
return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))]
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
||||
@bp.route("/etudiant/nip/<string:nip>/formsemestres")
|
||||
@bp.route("/etudiant/ine/<string:ine>/formsemestres")
|
||||
|
@ -122,7 +122,7 @@ class EntrepriseCreationForm(FlaskForm):
|
||||
origine = _build_string_field("Origine du correspondant", required=False)
|
||||
notes = _build_string_field("Notes sur le correspondant", required=False)
|
||||
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
@ -248,7 +248,7 @@ class SiteCreationForm(FlaskForm):
|
||||
codepostal = _build_string_field("Code postal (*)")
|
||||
ville = _build_string_field("Ville (*)")
|
||||
pays = _build_string_field("Pays", required=False)
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
@ -326,7 +326,7 @@ class OffreCreationForm(FlaskForm):
|
||||
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -458,7 +458,7 @@ class CorrespondantCreationForm(FlaskForm):
|
||||
class CorrespondantsCreationForm(FlaskForm):
|
||||
hidden_site_id = HiddenField()
|
||||
correspondants = FieldList(FormField(CorrespondantCreationForm), min_entries=1)
|
||||
submit = SubmitField("Envoyer")
|
||||
submit = SubmitField("Enregistrer")
|
||||
cancel = SubmitField("Annuler")
|
||||
|
||||
def validate(self):
|
||||
@ -566,7 +566,7 @@ class ContactCreationForm(FlaskForm):
|
||||
render_kw={"placeholder": "Tapez le nom de l'utilisateur"},
|
||||
)
|
||||
notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate_utilisateur(self, utilisateur):
|
||||
@ -613,8 +613,9 @@ class ContactModificationForm(FlaskForm):
|
||||
class StageApprentissageCreationForm(FlaskForm):
|
||||
etudiant = _build_string_field(
|
||||
"Étudiant (*)",
|
||||
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
|
||||
render_kw={"placeholder": "Tapez le nom de l'étudiant", "autocomplete": "off"},
|
||||
)
|
||||
etudid = HiddenField()
|
||||
type_offre = SelectField(
|
||||
"Type de l'offre (*)",
|
||||
choices=[("Stage"), ("Alternance")],
|
||||
@ -627,12 +628,12 @@ class StageApprentissageCreationForm(FlaskForm):
|
||||
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
notes = TextAreaField("Notes")
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
if not super().validate(extra_validators):
|
||||
validate = False
|
||||
|
||||
if (
|
||||
@ -646,64 +647,12 @@ class StageApprentissageCreationForm(FlaskForm):
|
||||
|
||||
return validate
|
||||
|
||||
def validate_etudiant(self, etudiant):
|
||||
etudiant_data = etudiant.data.upper().strip()
|
||||
stm = text(
|
||||
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||
)
|
||||
etudiant = (
|
||||
Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
|
||||
)
|
||||
def validate_etudid(self, field):
|
||||
"L'etudid doit avoit été placé par le JS"
|
||||
etudid = int(field.data) if field.data else None
|
||||
etudiant = Identite.query.get(etudid) if etudid is not None else None
|
||||
if etudiant is None:
|
||||
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
|
||||
|
||||
|
||||
class StageApprentissageModificationForm(FlaskForm):
|
||||
etudiant = _build_string_field(
|
||||
"Étudiant (*)",
|
||||
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
|
||||
)
|
||||
type_offre = SelectField(
|
||||
"Type de l'offre (*)",
|
||||
choices=[("Stage"), ("Alternance")],
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
date_debut = DateField(
|
||||
"Date début (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
date_fin = DateField(
|
||||
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
notes = TextAreaField("Notes")
|
||||
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
|
||||
if (
|
||||
self.date_debut.data
|
||||
and self.date_fin.data
|
||||
and self.date_debut.data > self.date_fin.data
|
||||
):
|
||||
self.date_debut.errors.append("Les dates sont incompatibles")
|
||||
self.date_fin.errors.append("Les dates sont incompatibles")
|
||||
validate = False
|
||||
|
||||
return validate
|
||||
|
||||
def validate_etudiant(self, etudiant):
|
||||
etudiant_data = etudiant.data.upper().strip()
|
||||
stm = text(
|
||||
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||
)
|
||||
etudiant = (
|
||||
Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
|
||||
)
|
||||
if etudiant is None:
|
||||
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
|
||||
raise ValidationError("Étudiant introuvable (sélectionnez dans la liste)")
|
||||
|
||||
|
||||
class TaxeApprentissageForm(FlaskForm):
|
||||
@ -732,7 +681,7 @@ class TaxeApprentissageForm(FlaskForm):
|
||||
default=1,
|
||||
)
|
||||
notes = TextAreaField("Notes")
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
|
@ -164,7 +164,10 @@ class EntrepriseStageApprentissage(db.Model):
|
||||
entreprise_id = db.Column(
|
||||
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||
)
|
||||
etudid = db.Column(db.Integer)
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
)
|
||||
type_offre = db.Column(db.Text)
|
||||
date_debut = db.Column(db.Date)
|
||||
date_fin = db.Column(db.Date)
|
||||
|
@ -28,7 +28,6 @@ from app.entreprises.forms import (
|
||||
ContactCreationForm,
|
||||
ContactModificationForm,
|
||||
StageApprentissageCreationForm,
|
||||
StageApprentissageModificationForm,
|
||||
EnvoiOffreForm,
|
||||
AjoutFichierForm,
|
||||
TaxeApprentissageForm,
|
||||
@ -1473,7 +1472,8 @@ def delete_contact(entreprise_id, contact_id):
|
||||
@permission_required(Permission.RelationsEntreprisesChange)
|
||||
def add_stage_apprentissage(entreprise_id):
|
||||
"""
|
||||
Permet d'ajouter un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
|
||||
Permet d'ajouter un étudiant ayant réalisé un stage ou alternance
|
||||
sur la fiche de l'entreprise
|
||||
"""
|
||||
entreprise = Entreprise.query.filter_by(
|
||||
id=entreprise_id, visible=True
|
||||
@ -1484,15 +1484,8 @@ def add_stage_apprentissage(entreprise_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
|
||||
)
|
||||
if form.validate_on_submit():
|
||||
etudiant_nomcomplet = form.etudiant.data.upper().strip()
|
||||
stm = text(
|
||||
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||
)
|
||||
etudiant = (
|
||||
Identite.query.from_statement(stm)
|
||||
.params(nom_prenom=etudiant_nomcomplet)
|
||||
.first()
|
||||
)
|
||||
etudid = form.etudid.data
|
||||
etudiant = Identite.query.get_or_404(etudid)
|
||||
formation = etudiant.inscription_courante_date(
|
||||
form.date_debut.data, form.date_fin.data
|
||||
)
|
||||
@ -1538,7 +1531,7 @@ def add_stage_apprentissage(entreprise_id):
|
||||
@permission_required(Permission.RelationsEntreprisesChange)
|
||||
def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
"""
|
||||
Permet de modifier un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
|
||||
Permet de modifier un étudiant ayant réalisé un stage ou alternance sur la fiche de l'entreprise
|
||||
"""
|
||||
stage_apprentissage = EntrepriseStageApprentissage.query.filter_by(
|
||||
id=stage_apprentissage_id, entreprise_id=entreprise_id
|
||||
@ -1548,21 +1541,14 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
etudiant = Identite.query.filter_by(id=stage_apprentissage.etudid).first_or_404(
|
||||
description=f"etudiant {stage_apprentissage.etudid} inconnue"
|
||||
)
|
||||
form = StageApprentissageModificationForm()
|
||||
form = StageApprentissageCreationForm()
|
||||
if request.method == "POST" and form.cancel.data:
|
||||
return redirect(
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
|
||||
)
|
||||
if form.validate_on_submit():
|
||||
etudiant_nomcomplet = form.etudiant.data.upper().strip()
|
||||
stm = text(
|
||||
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||
)
|
||||
etudiant = (
|
||||
Identite.query.from_statement(stm)
|
||||
.params(nom_prenom=etudiant_nomcomplet)
|
||||
.first()
|
||||
)
|
||||
etudid = form.etudid.data
|
||||
etudiant = Identite.query.get_or_404(etudid)
|
||||
formation = etudiant.inscription_courante_date(
|
||||
form.date_debut.data, form.date_fin.data
|
||||
)
|
||||
@ -1577,6 +1563,7 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
formation.formsemestre.formsemestre_id if formation else None,
|
||||
)
|
||||
stage_apprentissage.notes = form.notes.data.strip()
|
||||
db.session.add(stage_apprentissage)
|
||||
log = EntrepriseHistorique(
|
||||
authenticated_user=current_user.user_name,
|
||||
entreprise_id=stage_apprentissage.entreprise_id,
|
||||
@ -1593,7 +1580,9 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
)
|
||||
)
|
||||
elif request.method == "GET":
|
||||
form.etudiant.data = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}"
|
||||
form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} {
|
||||
sco_etud.format_prenom(etudiant.prenom)}"""
|
||||
form.etudid.data = etudiant.id
|
||||
form.type_offre.data = stage_apprentissage.type_offre
|
||||
form.date_debut.data = stage_apprentissage.date_debut
|
||||
form.date_fin.data = stage_apprentissage.date_fin
|
||||
|
@ -43,8 +43,8 @@ class Identite(db.Model):
|
||||
"optionnel (si present, affiché à la place du nom)"
|
||||
civilite = db.Column(db.String(1), nullable=False)
|
||||
|
||||
# données d'état-civil. Si présent remplace les données d'usage dans les documents officiels (bulletins, PV)
|
||||
# cf nomprenom_etat_civil()
|
||||
# données d'état-civil. Si présent remplace les données d'usage dans les documents
|
||||
# officiels (bulletins, PV): voir nomprenom_etat_civil()
|
||||
civilite_etat_civil = db.Column(db.String(1), nullable=False, server_default="X")
|
||||
prenom_etat_civil = db.Column(db.Text(), nullable=False, server_default="")
|
||||
|
||||
|
@ -3173,6 +3173,19 @@ li.tf-msg {
|
||||
/* EMO_WARNING, "⚠️" */
|
||||
}
|
||||
|
||||
p.error {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
}
|
||||
|
||||
p.error::before {
|
||||
content: "\2049 \fe0f";
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.infop {
|
||||
font-weight: normal;
|
||||
|
65
app/static/js/etud_autocomplete.js
Normal file
65
app/static/js/etud_autocomplete.js
Normal file
@ -0,0 +1,65 @@
|
||||
|
||||
// Mécanisme d'auto-complétion (choix) d'un étudiant
|
||||
// Il faut un champs #etudiant (text input) et à coté un champ hidden etudid qui sera rempli.
|
||||
// utilise autoComplete.js, source https://tarekraafat.github.io/autoComplete.js
|
||||
// EV 2023-06-01
|
||||
|
||||
function etud_autocomplete_config(with_dept = false) {
|
||||
return {
|
||||
selector: "#etudiant",
|
||||
placeHolder: "Nom...",
|
||||
threshold: 3,
|
||||
data: {
|
||||
src: async (query) => {
|
||||
try {
|
||||
// Fetch Data from external Source
|
||||
const source = await fetch(`/ScoDoc/api/etudiants/name/${query}`);
|
||||
// Data should be an array of `Objects` or `Strings`
|
||||
const data = await source.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
},
|
||||
// Data source 'Object' key to be searched
|
||||
keys: ["nom"]
|
||||
},
|
||||
events: {
|
||||
input: {
|
||||
selection: (event) => {
|
||||
const prenom = sco_capitalize(event.detail.selection.value.prenom);
|
||||
const selection = with_dept ? `${event.detail.selection.value.nom} ${prenom} (${event.detail.selection.value.dept_acronym})` : `${event.detail.selection.value.nom} ${prenom}`;
|
||||
// store etudid
|
||||
const etudidField = document.getElementById('etudid');
|
||||
etudidField.value = event.detail.selection.value.id;
|
||||
autoCompleteJS.input.value = selection;
|
||||
}
|
||||
}
|
||||
},
|
||||
resultsList: {
|
||||
element: (list, data) => {
|
||||
if (!data.results.length) {
|
||||
// Create "No Results" message element
|
||||
const message = document.createElement("div");
|
||||
// Add class to the created element
|
||||
message.setAttribute("class", "no_result");
|
||||
// Add message text content
|
||||
message.innerHTML = `<span>Pas de résultat pour "${data.query}"</span>`;
|
||||
// Append message element to the results list
|
||||
list.prepend(message);
|
||||
// Efface l'etudid
|
||||
const etudidField = document.getElementById('etudid');
|
||||
etudidField.value = "";
|
||||
}
|
||||
},
|
||||
noResults: true,
|
||||
},
|
||||
resultItem: {
|
||||
highlight: true,
|
||||
element: (item, data) => {
|
||||
const prenom = sco_capitalize(data.value.prenom);
|
||||
item.innerHTML += with_dept ? ` ${prenom} (${data.value.dept_acronym})` : ` ${prenom}`;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -67,6 +67,10 @@ $(function () {
|
||||
}
|
||||
});
|
||||
|
||||
function sco_capitalize(string) {
|
||||
return string[0].toUpperCase() + string.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
// Affiche un message transitoire (duration milliseconds, 0 means infinity)
|
||||
function sco_message(msg, className = "message_custom", duration = 0) {
|
||||
var div = document.createElement("div");
|
||||
|
1
app/static/libjs/autoComplete.js-10.2.7/dist/autoComplete.min.js
vendored
Normal file
1
app/static/libjs/autoComplete.js-10.2.7/dist/autoComplete.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
92
app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.01.css
vendored
Normal file
92
app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.01.css
vendored
Normal file
@ -0,0 +1,92 @@
|
||||
.autoComplete_wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input {
|
||||
width: 370px;
|
||||
height: 40px;
|
||||
padding-left: 20px;
|
||||
font-size: 1rem;
|
||||
color: rgba(123, 123, 123, 1);
|
||||
border-radius: 8px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input::placeholder {
|
||||
color: rgba(123, 123, 123, 0.5);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul {
|
||||
position: absolute;
|
||||
max-height: 226px;
|
||||
overflow-y: scroll;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0 0 0;
|
||||
border-radius: 0.6rem;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 3px 6px rgba(149, 157, 165, 0.15);
|
||||
border: 1px solid rgba(33, 33, 33, 0.07);
|
||||
z-index: 1000;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul[hidden],
|
||||
.autoComplete_wrapper > ul:empty {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li {
|
||||
margin: 0.3rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
list-style: none;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
color: #212121;
|
||||
transition: all 0.1s ease-in-out;
|
||||
border-radius: 0.35rem;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li::selection {
|
||||
color: rgba(#ffffff, 0);
|
||||
background-color: rgba(#ffffff, 0);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(123, 123, 123, 0.1);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li mark {
|
||||
background-color: transparent;
|
||||
color: rgba(255, 122, 122, 1);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li mark::selection {
|
||||
color: rgba(#ffffff, 0);
|
||||
background-color: rgba(#ffffff, 0);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li[aria-selected="true"] {
|
||||
background-color: rgba(123, 123, 123, 0.1);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.autoComplete_wrapper > input {
|
||||
width: 18rem;
|
||||
}
|
||||
}
|
82
app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.02.css
vendored
Normal file
82
app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.02.css
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
.autoComplete_wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input {
|
||||
width: 370px;
|
||||
height: 40px;
|
||||
padding-left: 10px;
|
||||
font-size: 1rem;
|
||||
color: rgb(116, 116, 116);
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(33, 33, 33, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input::placeholder {
|
||||
color: rgba(123, 123, 123, 0.5);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul {
|
||||
position: absolute;
|
||||
max-height: 226px;
|
||||
overflow-y: scroll;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0 0 0;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(33, 33, 33, 0.1);
|
||||
z-index: 1000;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li {
|
||||
padding: 10px 20px;
|
||||
list-style: none;
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
color: #212121;
|
||||
transition: all 0.1s ease-in-out;
|
||||
border-radius: 3px;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li::selection {
|
||||
color: rgba(#ffffff, 0);
|
||||
background-color: rgba(#ffffff, 0);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(123, 123, 123, 0.1);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li mark {
|
||||
background-color: transparent;
|
||||
color: rgba(255, 122, 122, 1);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li mark::selection {
|
||||
color: rgba(#ffffff, 0);
|
||||
background-color: rgba(#ffffff, 0);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li[aria-selected="true"] {
|
||||
background-color: rgba(123, 123, 123, 0.1);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.autoComplete_wrapper > input {
|
||||
width: 18rem;
|
||||
}
|
||||
}
|
128
app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.css
vendored
Normal file
128
app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.css
vendored
Normal file
@ -0,0 +1,128 @@
|
||||
.autoComplete_wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input {
|
||||
height: 3rem;
|
||||
width: 370px;
|
||||
margin: 0;
|
||||
padding: 0 2rem 0 3.2rem;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
font-size: 1rem;
|
||||
text-overflow: ellipsis;
|
||||
color: rgba(255, 122, 122, 0.3);
|
||||
outline: none;
|
||||
border-radius: 10rem;
|
||||
border: 0.05rem solid rgba(255, 122, 122, 0.5);
|
||||
background-image: url(./images/search.svg);
|
||||
background-size: 1.4rem;
|
||||
background-position: left 1.05rem top 0.8rem;
|
||||
background-repeat: no-repeat;
|
||||
background-origin: border-box;
|
||||
background-color: #fff;
|
||||
transition: all 0.4s ease;
|
||||
-webkit-transition: all -webkit-transform 0.4s ease;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input::placeholder {
|
||||
color: rgba(255, 122, 122, 0.5);
|
||||
transition: all 0.3s ease;
|
||||
-webkit-transition: all -webkit-transform 0.3s ease;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input:hover::placeholder {
|
||||
color: rgba(255, 122, 122, 0.6);
|
||||
transition: all 0.3s ease;
|
||||
-webkit-transition: all -webkit-transform 0.3s ease;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input:focus::placeholder {
|
||||
padding: 0.1rem 0.6rem;
|
||||
font-size: 0.95rem;
|
||||
color: rgba(255, 122, 122, 0.4);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input:focus::selection {
|
||||
background-color: rgba(255, 122, 122, 0.15);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input::selection {
|
||||
background-color: rgba(255, 122, 122, 0.15);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input:hover {
|
||||
color: rgba(255, 122, 122, 0.8);
|
||||
transition: all 0.3s ease;
|
||||
-webkit-transition: all -webkit-transform 0.3s ease;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input:focus {
|
||||
color: rgba(255, 122, 122, 1);
|
||||
border: 0.06rem solid rgba(255, 122, 122, 0.8);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul {
|
||||
position: absolute;
|
||||
max-height: 226px;
|
||||
overflow-y: scroll;
|
||||
box-sizing: border-box;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0.5rem 0 0 0;
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
list-style: none;
|
||||
border-radius: 0.6rem;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(33, 33, 33, 0.07);
|
||||
box-shadow: 0 3px 6px rgba(149, 157, 165, 0.15);
|
||||
outline: none;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
-moz-transition: opacity 0.15s ease-in-out;
|
||||
-webkit-transition: opacity 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul[hidden],
|
||||
.autoComplete_wrapper > ul:empty {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li {
|
||||
margin: 0.3rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
color: #212121;
|
||||
border-radius: 0.35rem;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li mark {
|
||||
background-color: transparent;
|
||||
color: rgba(255, 122, 122, 1);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(255, 122, 122, 0.15);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li[aria-selected="true"] {
|
||||
background-color: rgba(255, 122, 122, 0.15);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.autoComplete_wrapper > input {
|
||||
width: 18rem;
|
||||
}
|
||||
}
|
8
app/static/libjs/autoComplete.js-10.2.7/dist/css/images/search.svg
vendored
Normal file
8
app/static/libjs/autoComplete.js-10.2.7/dist/css/images/search.svg
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" focusable="false" x="0px" y="0px" width="30" height="30" viewBox="0 0 171 171" style=" fill:#000000;">
|
||||
<g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal">
|
||||
<path d="M0,171.99609v-171.99609h171.99609v171.99609z" fill="none"></path>
|
||||
<g fill="#ff7a7a">
|
||||
<path d="M74.1,17.1c-31.41272,0 -57,25.58728 -57,57c0,31.41272 25.58728,57 57,57c13.6601,0 26.20509,-4.85078 36.03692,-12.90293l34.03301,34.03301c1.42965,1.48907 3.55262,2.08891 5.55014,1.56818c1.99752,-0.52073 3.55746,-2.08067 4.07819,-4.07819c0.52073,-1.99752 -0.0791,-4.12049 -1.56818,-5.55014l-34.03301,-34.03301c8.05215,-9.83182 12.90293,-22.37682 12.90293,-36.03692c0,-31.41272 -25.58728,-57 -57,-57zM74.1,28.5c25.2517,0 45.6,20.3483 45.6,45.6c0,25.2517 -20.3483,45.6 -45.6,45.6c-25.2517,0 -45.6,-20.3483 -45.6,-45.6c0,-25.2517 20.3483,-45.6 45.6,-45.6z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -85,7 +85,15 @@
|
||||
{{ super() }}
|
||||
{{ moment.include_moment() }}
|
||||
{{ moment.lang(g.locale) }}
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/menu.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/bubble.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/jQuery/jquery.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/jQuery/jquery-migrate-1.2.0.min.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/jquery.field.min.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script>
|
||||
<script>
|
||||
|
||||
var SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)[:-11] }}";
|
||||
</script>
|
||||
{% endblock %}
|
@ -4,33 +4,27 @@
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/css/autosuggest_inquisitor.css" />
|
||||
<script src="/ScoDoc/static/libjs/AutoSuggest.js"></script>
|
||||
<link rel="stylesheet" href="/ScoDoc/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.02.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>{{ title }}</h1>
|
||||
<br>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<p>
|
||||
(*) champs requis
|
||||
</p>
|
||||
{{ wtf.quick_form(form, novalidate=True) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{super()}}
|
||||
<script src="/ScoDoc/static/libjs/autoComplete.js-10.2.7/dist/autoComplete.min.js"></script>
|
||||
<script src="/ScoDoc/static/js/etud_autocomplete.js"></script>
|
||||
<script>
|
||||
window.onload = function (e) {
|
||||
var etudiants_options = {
|
||||
script: "/ScoDoc/entreprises/etudiants?",
|
||||
varname: "term",
|
||||
json: true,
|
||||
noresults: "Valeur invalide !",
|
||||
minchars: 2,
|
||||
timeout: 60000
|
||||
};
|
||||
var as_etudiants = new bsn.AutoSuggest('etudiant', etudiants_options);
|
||||
}
|
||||
const autoCompleteJS = new autoComplete(etud_autocomplete_config(with_dept = true));
|
||||
</script>
|
||||
{% endblock %}
|
60
migrations/versions/d84bc592584e_extension_unaccent.py
Normal file
60
migrations/versions/d84bc592584e_extension_unaccent.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Extension unaccent
|
||||
|
||||
Revision ID: d84bc592584e
|
||||
Revises: b8df1b913c79
|
||||
Create Date: 2023-06-01 13:46:52.927951
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import sessionmaker # added by ev
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "d84bc592584e"
|
||||
down_revision = "b8df1b913c79"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
Session = sessionmaker()
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
# Ajout extension pour recherches sans accents:
|
||||
session.execute(sa.text("""CREATE EXTENSION IF NOT EXISTS "unaccent";"""))
|
||||
|
||||
# Clé étrangère sur identite
|
||||
session.execute(
|
||||
sa.text(
|
||||
"""UPDATE are_stages_apprentissages
|
||||
SET etudid = NULL
|
||||
WHERE are_stages_apprentissages.etudid NOT IN (
|
||||
SELECT id
|
||||
FROM Identite
|
||||
);
|
||||
"""
|
||||
)
|
||||
)
|
||||
with op.batch_alter_table("are_stages_apprentissages", schema=None) as batch_op:
|
||||
batch_op.create_foreign_key(
|
||||
"are_stages_apprentissages_etudid_fkey",
|
||||
"identite",
|
||||
["etudid"],
|
||||
["id"],
|
||||
ondelete="CASCADE",
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("are_stages_apprentissages", schema=None) as batch_op:
|
||||
batch_op.drop_constraint(
|
||||
"are_stages_apprentissages_etudid_fkey", type_="foreignkey"
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
@ -1,7 +1,7 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.4.79"
|
||||
SCOVERSION = "9.4.80"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
@ -224,6 +224,32 @@ def test_etudiants(api_headers):
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_etudiants_by_name(api_headers):
|
||||
"""
|
||||
Route: /etudiants/name/<string:start>
|
||||
"""
|
||||
r = requests.get(
|
||||
API_URL + "/etudiants/name/A",
|
||||
headers=api_headers,
|
||||
verify=CHECK_CERTIFICATE,
|
||||
timeout=scu.SCO_TEST_API_TIMEOUT,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
etuds = r.json()
|
||||
assert etuds == []
|
||||
#
|
||||
r = requests.get(
|
||||
API_URL + "/etudiants/name/REG",
|
||||
headers=api_headers,
|
||||
verify=CHECK_CERTIFICATE,
|
||||
timeout=scu.SCO_TEST_API_TIMEOUT,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
etuds = r.json()
|
||||
assert len(etuds) == 1
|
||||
assert etuds[0]["nom"] == "RÉGNIER"
|
||||
|
||||
|
||||
def test_etudiant_formsemestres(api_headers):
|
||||
"""
|
||||
Route: /etudiant/etudid/<etudid:int>/formsemestres
|
||||
|
Loading…
Reference in New Issue
Block a user