1
0
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:
Emmanuel Viennet 2023-06-01 17:58:30 +02:00
parent 111c400333
commit 8e1cb055f6
18 changed files with 565 additions and 113 deletions

View File

@ -8,16 +8,17 @@
API : accès aux étudiants API : accès aux étudiants
""" """
from datetime import datetime from datetime import datetime
from operator import attrgetter
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
from flask_login import login_required 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 import app
from app.api import api_bp as bp, api_web_bp 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.api import tools
from app.decorators import scodoc, permission_required from app.decorators import scodoc, permission_required
from app.models import ( from app.models import (
@ -31,6 +32,8 @@ from app.scodoc import sco_bulletins
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error, suppress_accents
# Un exemple: # Un exemple:
# @bp.route("/api_function/<int:arg>") # @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: if not None in allowed_depts:
# restreint aux départements autorisés: # 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) or_(Departement.acronym == acronym for acronym in allowed_depts)
) )
return [etud.to_dict_api() for etud in query] 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/etudid/<int:etudid>/formsemestres")
@bp.route("/etudiant/nip/<string:nip>/formsemestres") @bp.route("/etudiant/nip/<string:nip>/formsemestres")
@bp.route("/etudiant/ine/<string:ine>/formsemestres") @bp.route("/etudiant/ine/<string:ine>/formsemestres")

View File

@ -122,7 +122,7 @@ class EntrepriseCreationForm(FlaskForm):
origine = _build_string_field("Origine du correspondant", required=False) origine = _build_string_field("Origine du correspondant", required=False)
notes = _build_string_field("Notes sur le 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) cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def validate(self): def validate(self):
@ -248,7 +248,7 @@ class SiteCreationForm(FlaskForm):
codepostal = _build_string_field("Code postal (*)") codepostal = _build_string_field("Code postal (*)")
ville = _build_string_field("Ville (*)") ville = _build_string_field("Ville (*)")
pays = _build_string_field("Pays", required=False) 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) cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def validate(self): def validate(self):
@ -326,7 +326,7 @@ class OffreCreationForm(FlaskForm):
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"), 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) cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -458,7 +458,7 @@ class CorrespondantCreationForm(FlaskForm):
class CorrespondantsCreationForm(FlaskForm): class CorrespondantsCreationForm(FlaskForm):
hidden_site_id = HiddenField() hidden_site_id = HiddenField()
correspondants = FieldList(FormField(CorrespondantCreationForm), min_entries=1) correspondants = FieldList(FormField(CorrespondantCreationForm), min_entries=1)
submit = SubmitField("Envoyer") submit = SubmitField("Enregistrer")
cancel = SubmitField("Annuler") cancel = SubmitField("Annuler")
def validate(self): def validate(self):
@ -566,7 +566,7 @@ class ContactCreationForm(FlaskForm):
render_kw={"placeholder": "Tapez le nom de l'utilisateur"}, render_kw={"placeholder": "Tapez le nom de l'utilisateur"},
) )
notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)]) 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) cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def validate_utilisateur(self, utilisateur): def validate_utilisateur(self, utilisateur):
@ -613,8 +613,9 @@ class ContactModificationForm(FlaskForm):
class StageApprentissageCreationForm(FlaskForm): class StageApprentissageCreationForm(FlaskForm):
etudiant = _build_string_field( etudiant = _build_string_field(
"Étudiant (*)", "É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_offre = SelectField(
"Type de l'offre (*)", "Type de l'offre (*)",
choices=[("Stage"), ("Alternance")], choices=[("Stage"), ("Alternance")],
@ -627,12 +628,12 @@ class StageApprentissageCreationForm(FlaskForm):
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)] "Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
) )
notes = TextAreaField("Notes") notes = TextAreaField("Notes")
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE) cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def validate(self): def validate(self, extra_validators=None):
validate = True validate = True
if not FlaskForm.validate(self): if not super().validate(extra_validators):
validate = False validate = False
if ( if (
@ -646,64 +647,12 @@ class StageApprentissageCreationForm(FlaskForm):
return validate return validate
def validate_etudiant(self, etudiant): def validate_etudid(self, field):
etudiant_data = etudiant.data.upper().strip() "L'etudid doit avoit été placé par le JS"
stm = text( etudid = int(field.data) if field.data else None
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom" etudiant = Identite.query.get(etudid) if etudid is not None else None
)
etudiant = (
Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
)
if etudiant is None: if etudiant is None:
raise ValidationError("Champ incorrect (selectionnez dans la liste)") raise ValidationError("Étudiant introuvable (sélectionnez 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)")
class TaxeApprentissageForm(FlaskForm): class TaxeApprentissageForm(FlaskForm):
@ -732,7 +681,7 @@ class TaxeApprentissageForm(FlaskForm):
default=1, default=1,
) )
notes = TextAreaField("Notes") notes = TextAreaField("Notes")
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE) submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE) cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def validate(self): def validate(self):

View File

@ -164,7 +164,10 @@ class EntrepriseStageApprentissage(db.Model):
entreprise_id = db.Column( entreprise_id = db.Column(
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade") 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) type_offre = db.Column(db.Text)
date_debut = db.Column(db.Date) date_debut = db.Column(db.Date)
date_fin = db.Column(db.Date) date_fin = db.Column(db.Date)

View File

@ -28,7 +28,6 @@ from app.entreprises.forms import (
ContactCreationForm, ContactCreationForm,
ContactModificationForm, ContactModificationForm,
StageApprentissageCreationForm, StageApprentissageCreationForm,
StageApprentissageModificationForm,
EnvoiOffreForm, EnvoiOffreForm,
AjoutFichierForm, AjoutFichierForm,
TaxeApprentissageForm, TaxeApprentissageForm,
@ -1473,7 +1472,8 @@ def delete_contact(entreprise_id, contact_id):
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def add_stage_apprentissage(entreprise_id): 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( entreprise = Entreprise.query.filter_by(
id=entreprise_id, visible=True id=entreprise_id, visible=True
@ -1484,15 +1484,8 @@ def add_stage_apprentissage(entreprise_id):
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id) url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
) )
if form.validate_on_submit(): if form.validate_on_submit():
etudiant_nomcomplet = form.etudiant.data.upper().strip() etudid = form.etudid.data
stm = text( etudiant = Identite.query.get_or_404(etudid)
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
)
etudiant = (
Identite.query.from_statement(stm)
.params(nom_prenom=etudiant_nomcomplet)
.first()
)
formation = etudiant.inscription_courante_date( formation = etudiant.inscription_courante_date(
form.date_debut.data, form.date_fin.data form.date_debut.data, form.date_fin.data
) )
@ -1538,7 +1531,7 @@ def add_stage_apprentissage(entreprise_id):
@permission_required(Permission.RelationsEntreprisesChange) @permission_required(Permission.RelationsEntreprisesChange)
def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id): 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( stage_apprentissage = EntrepriseStageApprentissage.query.filter_by(
id=stage_apprentissage_id, entreprise_id=entreprise_id 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( etudiant = Identite.query.filter_by(id=stage_apprentissage.etudid).first_or_404(
description=f"etudiant {stage_apprentissage.etudid} inconnue" description=f"etudiant {stage_apprentissage.etudid} inconnue"
) )
form = StageApprentissageModificationForm() form = StageApprentissageCreationForm()
if request.method == "POST" and form.cancel.data: if request.method == "POST" and form.cancel.data:
return redirect( return redirect(
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id) url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
) )
if form.validate_on_submit(): if form.validate_on_submit():
etudiant_nomcomplet = form.etudiant.data.upper().strip() etudid = form.etudid.data
stm = text( etudiant = Identite.query.get_or_404(etudid)
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
)
etudiant = (
Identite.query.from_statement(stm)
.params(nom_prenom=etudiant_nomcomplet)
.first()
)
formation = etudiant.inscription_courante_date( formation = etudiant.inscription_courante_date(
form.date_debut.data, form.date_fin.data 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, formation.formsemestre.formsemestre_id if formation else None,
) )
stage_apprentissage.notes = form.notes.data.strip() stage_apprentissage.notes = form.notes.data.strip()
db.session.add(stage_apprentissage)
log = EntrepriseHistorique( log = EntrepriseHistorique(
authenticated_user=current_user.user_name, authenticated_user=current_user.user_name,
entreprise_id=stage_apprentissage.entreprise_id, entreprise_id=stage_apprentissage.entreprise_id,
@ -1593,7 +1580,9 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
) )
) )
elif request.method == "GET": 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.type_offre.data = stage_apprentissage.type_offre
form.date_debut.data = stage_apprentissage.date_debut form.date_debut.data = stage_apprentissage.date_debut
form.date_fin.data = stage_apprentissage.date_fin form.date_fin.data = stage_apprentissage.date_fin

View File

@ -43,8 +43,8 @@ class Identite(db.Model):
"optionnel (si present, affiché à la place du nom)" "optionnel (si present, affiché à la place du nom)"
civilite = db.Column(db.String(1), nullable=False) 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) # données d'état-civil. Si présent remplace les données d'usage dans les documents
# cf nomprenom_etat_civil() # officiels (bulletins, PV): voir nomprenom_etat_civil()
civilite_etat_civil = db.Column(db.String(1), nullable=False, server_default="X") civilite_etat_civil = db.Column(db.String(1), nullable=False, server_default="X")
prenom_etat_civil = db.Column(db.Text(), nullable=False, server_default="") prenom_etat_civil = db.Column(db.Text(), nullable=False, server_default="")

View File

@ -3173,6 +3173,19 @@ li.tf-msg {
/* EMO_WARNING, "&#9888;&#65039;" */ /* EMO_WARNING, "&#9888;&#65039;" */
} }
p.error {
font-weight: bold;
color: red;
}
p.error::before {
content: "\2049 \fe0f";
margin-right: 8px;
}
mark {
padding-right: 0px;
}
.infop { .infop {
font-weight: normal; font-weight: normal;

View 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}`;
},
},
}
}

View File

@ -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) // Affiche un message transitoire (duration milliseconds, 0 means infinity)
function sco_message(msg, className = "message_custom", duration = 0) { function sco_message(msg, className = "message_custom", duration = 0) {
var div = document.createElement("div"); var div = document.createElement("div");

File diff suppressed because one or more lines are too long

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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

View File

@ -85,7 +85,15 @@
{{ super() }} {{ super() }}
{{ moment.include_moment() }} {{ moment.include_moment() }}
{{ moment.lang(g.locale) }} {{ 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> <script>
var SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)[:-11] }}";
</script> </script>
{% endblock %} {% endblock %}

View File

@ -4,33 +4,27 @@
{% block styles %} {% block styles %}
{{super()}} {{super()}}
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/css/autosuggest_inquisitor.css" /> <link rel="stylesheet" href="/ScoDoc/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.02.css">
<script src="/ScoDoc/static/libjs/AutoSuggest.js"></script>
{% endblock %} {% endblock %}
{% block app_content %} {% block app_content %}
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
<br> <br>
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-3">
<p> <p>
(*) champs requis (*) champs requis
</p> </p>
{{ wtf.quick_form(form, novalidate=True) }} {{ wtf.quick_form(form, novalidate=True) }}
</div> </div>
</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> <script>
window.onload = function (e) { const autoCompleteJS = new autoComplete(etud_autocomplete_config(with_dept = true));
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);
}
</script> </script>
{% endblock %} {% endblock %}

View 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 ###

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.4.79" SCOVERSION = "9.4.80"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -224,6 +224,32 @@ def test_etudiants(api_headers):
assert r.status_code == 404 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): def test_etudiant_formsemestres(api_headers):
""" """
Route: /etudiant/etudid/<etudid:int>/formsemestres Route: /etudiant/etudid/<etudid:int>/formsemestres