Merge branch 'main96' into liste_assi

This commit is contained in:
Iziram 2023-11-22 16:05:41 +01:00
commit 1e98dd125e
210 changed files with 10542 additions and 8069 deletions

View File

@ -19,11 +19,7 @@ from flask import current_app, g, request
from flask import Flask
from flask import abort, flash, has_request_context
from flask import render_template
# from flask.json import JSONEncoder
from flask.logging import default_handler
from flask_bootstrap import Bootstrap
from flask_caching import Cache
from flask_json import FlaskJSON, json_response
from flask_login import LoginManager, current_user
@ -34,6 +30,7 @@ from flask_sqlalchemy import SQLAlchemy
from jinja2 import select_autoescape
import sqlalchemy as sa
import werkzeug.debug
from wtforms.fields import HiddenField
from flask_cas import CAS
@ -59,8 +56,6 @@ login.login_view = "auth.login"
login.login_message = "Identifiez-vous pour accéder à cette page."
mail = Mail()
bootstrap = Bootstrap()
# moment = Moment()
CACHE_TYPE = os.environ.get("CACHE_TYPE")
cache = Cache(
@ -304,7 +299,6 @@ def create_app(config_class=DevConfig):
login.init_app(app)
mail.init_app(app)
app.extensions["mail"].debug = 0 # disable copy of mails to stderr
bootstrap.init_app(app)
cache.init_app(app)
sco_cache.CACHE = cache
if CACHE_TYPE: # non default
@ -320,6 +314,12 @@ def create_app(config_class=DevConfig):
app.register_error_handler(503, postgresql_server_error)
app.register_error_handler(APIInvalidParams, handle_invalid_usage)
# Add some globals
# previously in Flask-Bootstrap:
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
field, HiddenField
)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix="/auth")
@ -549,8 +549,8 @@ def truncate_database():
CREATE OR REPLACE FUNCTION reset_sequences(username IN VARCHAR) RETURNS void AS $$
DECLARE
statements CURSOR FOR
SELECT sequence_name
FROM information_schema.sequences
SELECT sequence_name
FROM information_schema.sequences
ORDER BY sequence_name ;
BEGIN
FOR stmt IN statements LOOP

View File

@ -1014,7 +1014,7 @@ def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
else assiduite_unique.external_data
)
if force and not external_data.get("module", False):
if force and not (external_data is not None and external_data.get("module", False) != ""):
errors.append(
"param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul"
)

View File

@ -33,6 +33,7 @@ from app.models import (
)
from app.models.formsemestre import GROUPS_AUTO_ASSIGNMENT_DATA_MAX
from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json
from app.scodoc import sco_edt_cal
from app.scodoc import sco_groups
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import ModuleType
@ -555,3 +556,23 @@ def save_groups_auto_assignment(formsemestre_id: int):
formsemestre.groups_auto_assignment_data = request.data
db.session.add(formsemestre)
db.session.commit()
@bp.route("/formsemestre/<int:formsemestre_id>/edt")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/edt")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formsemestre_edt(formsemestre_id: int):
"""l'emploi du temps du semestre.
Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
group_ids permet de filtrer sur les groupes ScoDoc.
"""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
group_ids = request.args.getlist("group_ids", int)
return sco_edt_cal.formsemestre_edt_dict(formsemestre, group_ids=group_ids)

View File

@ -39,6 +39,15 @@ def after_cas_login():
"scodoc_cas_login_date"
] = datetime.datetime.now().isoformat()
user.cas_last_login = datetime.datetime.utcnow()
if flask.session.get("CAS_EDT_ID"):
# essaie de récupérer l'edt_id s'il est présent
# cet ID peut être renvoyé par le CAS et extrait par ScoDoc
# via l'expression `cas_edt_id_from_xml_regexp`
# voir flask_cas.routing
edt_id = flask.session.get("CAS_EDT_ID")
current_app.logger.info(f"""after_cas_login: storing edt_id for {
user.user_name}: '{edt_id}'""")
user.edt_id = edt_id
db.session.add(user)
db.session.commit()
return flask.redirect(url_for("scodoc.index"))

View File

@ -12,6 +12,7 @@ from typing import Optional
import cracklib # pylint: disable=import-error
import flask
from flask import current_app, g
from flask_login import UserMixin, AnonymousUserMixin
@ -88,7 +89,8 @@ class User(UserMixin, db.Model):
"""
cas_last_login = db.Column(db.DateTime, nullable=True)
"""date du dernier login via CAS"""
edt_id = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
password_hash = db.Column(db.String(128))
password_scodoc7 = db.Column(db.String(42))
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
@ -172,7 +174,8 @@ class User(UserMixin, db.Model):
return False
# if CAS activated and forced, allow only super-user and users with cas_allow_scodoc_login
if ScoDocSiteConfig.is_cas_enabled() and ScoDocSiteConfig.get("cas_force"):
cas_enabled = ScoDocSiteConfig.is_cas_enabled()
if cas_enabled and ScoDocSiteConfig.get("cas_force"):
if (not self.is_administrator()) and not self.cas_allow_scodoc_login:
return False

View File

@ -307,7 +307,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
return ues_ids
def etud_has_decision(self, etudid) -> bool:
def etud_has_decision(self, etudid, include_rcues=True) -> bool:
"""True s'il y a une décision (quelconque) de jury
émanant de ce formsemestre pour cet étudiant.
prend aussi en compte les autorisations de passage.
@ -318,9 +318,12 @@ class ResultatsSemestreBUT(NotesTableCompat):
or ApcValidationAnnee.query.filter_by(
formsemestre_id=self.formsemestre.id, etudid=etudid
).count()
or ApcValidationRCUE.query.filter_by(
formsemestre_id=self.formsemestre.id, etudid=etudid
).count()
or (
include_rcues
and ApcValidationRCUE.query.filter_by(
formsemestre_id=self.formsemestre.id, etudid=etudid
).count()
)
)
def get_validations_annee(self) -> dict[int, ApcValidationAnnee]:

View File

@ -290,9 +290,10 @@ class NotesTableCompat(ResultatsSemestre):
]
return etudids
def etud_has_decision(self, etudid) -> bool:
def etud_has_decision(self, etudid, include_rcues=True) -> bool:
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
prend aussi en compte les autorisations de passage.
Si include_rcues, prend en compte les validation d'RCUEs en BUT (pas d'effet en classic).
Sous-classée en BUT pour les RCUEs et années.
"""
return bool(

View File

@ -0,0 +1,52 @@
"""
Formulaire configuration des codes Apo et EDT des modimps d'un formsemestre
"""
from flask_wtf import FlaskForm
from wtforms import validators
from wtforms.fields.simple import StringField, SubmitField
from app.models import FormSemestre, ModuleImpl
class _EditModimplsCodesForm(FlaskForm):
"form. définition des liens personnalisés"
# construit dynamiquement ci-dessous
def EditModimplsCodesForm(formsemestre: FormSemestre) -> _EditModimplsCodesForm:
"Création d'un formulaire pour éditer les codes"
# Formulaire dynamique, on créé une classe ad-hoc
class F(_EditModimplsCodesForm):
pass
def _gen_mod_form(modimpl: ModuleImpl):
field = StringField(
modimpl.module.code,
validators=[
validators.Optional(),
validators.Length(min=1, max=80),
],
default="",
render_kw={"size": 32},
)
setattr(F, f"modimpl_apo_{modimpl.id}", field)
field = StringField(
"",
validators=[
validators.Optional(),
validators.Length(min=1, max=80),
],
default="",
render_kw={"size": 12},
)
setattr(F, f"modimpl_edt_{modimpl.id}", field)
for modimpl in formsemestre.modimpls_sorted:
_gen_mod_form(modimpl)
F.submit = SubmitField("Valider")
F.cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
return F()

View File

@ -28,12 +28,15 @@
"""
Formulaire configuration Module Assiduités
"""
import datetime
import re
from flask_wtf import FlaskForm
from wtforms import SubmitField, DecimalField
from wtforms import DecimalField, SubmitField, ValidationError
from wtforms.fields.simple import StringField
from wtforms.validators import Optional
from wtforms.widgets import TimeInput
import datetime
class TimeField(StringField):
@ -72,9 +75,45 @@ class TimeField(StringField):
else:
raise ValueError
self.data = datetime.time(hour, minutes, seconds)
except ValueError:
except ValueError as exc:
self.data = None
raise ValueError(self.gettext("Not a valid time string"))
raise ValueError(self.gettext("Not a valid time string")) from exc
def check_tick_time(form, field):
"""Le tick_time doit être entre 0 et 60 minutes"""
if field.data < 1 or field.data > 59:
raise ValidationError("Valeur de granularité invalide (entre 1 et 59)")
def check_ics_path(form, field):
"""Vérifie que le chemin est bien un chemin absolu
et qu'il contient edt_id
"""
data = field.data.strip()
if not data:
return
if not data.startswith("/"):
raise ValidationError("Le chemin vers les ics doit commencer par /")
if not "{edt_id}" in data:
raise ValidationError("Le chemin vers les ics doit utiliser {edt_id}")
def check_ics_field(form, field):
"""Vérifie que c'est un nom de champ crédible: un mot alphanumérique"""
if not re.match(r"^[a-zA-Z\-_0-9]+$", field.data):
raise ValidationError("nom de champ ics invalide")
def check_ics_regexp(form, field):
"""Vérifie que field est une expresssion régulière"""
value = field.data.strip()
# check that it compiles
try:
_ = re.compile(value)
except re.error as exc:
raise ValidationError("expression invalide") from exc
return True
class ConfigAssiduitesForm(FlaskForm):
@ -84,7 +123,60 @@ class ConfigAssiduitesForm(FlaskForm):
lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)")
afternoon_time = TimeField("Fin de la journée")
tick_time = DecimalField("Granularité de la Time Line (temps en minutes)", places=0)
tick_time = DecimalField(
"Granularité de la timeline (temps en minutes)",
places=0,
validators=[check_tick_time],
)
edt_ics_path = StringField(
label="Chemin vers les ics",
description="""Chemin absolu unix sur le serveur vers le fichier ics donnant l'emploi
du temps d'un semestre. La balise <tt>{edt_id}</tt> sera remplacée par l'edt_id du
semestre (par défaut, son code étape Apogée).
Si ce champ n'est pas renseigné, les emplois du temps ne seront pas utilisés.""",
validators=[Optional(), check_ics_path],
)
edt_ics_title_field = StringField(
label="Champs contenant le titre",
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
validators=[Optional(), check_ics_field],
)
edt_ics_title_regexp = StringField(
label="Extraction du titre",
description=r"""expression régulière python dont le premier groupe doit
sera le titre de l'évènement affcihé dans le calendrier ScoDoc.
Exemple: <tt>Matière : \w+ - ([\w\.\s']+)</tt>
""",
validators=[Optional(), check_ics_regexp],
)
edt_ics_group_field = StringField(
label="Champs contenant le groupe",
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
validators=[Optional(), check_ics_field],
)
edt_ics_group_regexp = StringField(
label="Extraction du groupe",
description=r"""expression régulière python dont le premier groupe doit
correspondre à l'identifiant de groupe de l'emploi du temps.
Exemple: <tt>.*- ([\w\s]+)$</tt>
""",
validators=[Optional(), check_ics_regexp],
)
edt_ics_mod_field = StringField(
label="Champs contenant le module",
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
validators=[Optional(), check_ics_field],
)
edt_ics_mod_regexp = StringField(
label="Extraction du module",
description=r"""expression régulière python dont le premier groupe doit
correspondre à l'identifiant (code) du module de l'emploi du temps.
Exemple: <tt>Matière : ([A-Z][A-Z0-9]+)</tt>
""",
validators=[Optional(), check_ics_regexp],
)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -38,11 +38,17 @@ from app.models import ScoDocSiteConfig
def check_cas_uid_from_mail_regexp(form, field):
"Vérifie la regexp fournie pur l'extraction du CAS id"
"Vérifie la regexp fournie pour l'extraction du CAS id"
if not ScoDocSiteConfig.cas_uid_from_mail_regexp_is_valid(field.data):
raise ValidationError("expression régulière invalide")
def check_cas_edt_id_from_xml_regexp(form, field):
"Vérifie la regexp fournie pour l'extraction du CAS id"
if not ScoDocSiteConfig.cas_edt_id_from_xml_regexp_is_valid(field.data):
raise ValidationError("expression régulière pour edt_id invalide")
class ConfigCASForm(FlaskForm):
"Formulaire paramétrage CAS"
cas_enable = BooleanField("Activer le CAS")
@ -58,18 +64,18 @@ class ConfigCASForm(FlaskForm):
description="""url complète. Commence en général par <tt>https://</tt>.""",
)
cas_login_route = StringField(
label="Route du login CAS",
label="Optionnel: route du login CAS",
description="""ajouté à l'URL du serveur: exemple <tt>/cas</tt>
(si commence par <tt>/</tt>, part de la racine)""",
default="/cas",
)
cas_logout_route = StringField(
label="Route du logout CAS",
label="Optionnel: route du logout CAS",
description="""ajouté à l'URL du serveur: exemple <tt>/cas/logout</tt>""",
default="/cas/logout",
)
cas_validate_route = StringField(
label="Route de validation CAS",
label="Optionnel: route de validation CAS",
description="""ajouté à l'URL du serveur: exemple <tt>/cas/serviceValidate</tt>""",
default="/cas/serviceValidate",
)
@ -81,7 +87,7 @@ class ConfigCASForm(FlaskForm):
)
cas_uid_from_mail_regexp = StringField(
label="Expression pour extraire l'identifiant utilisateur",
label="Optionnel: expression pour extraire l'identifiant utilisateur",
description="""regexp python appliquée au mail institutionnel de l'utilisateur,
dont le premier groupe doit donner l'identifiant CAS.
Si non fournie, le super-admin devra saisir cet identifiant pour chaque compte.
@ -92,6 +98,17 @@ class ConfigCASForm(FlaskForm):
validators=[Optional(), check_cas_uid_from_mail_regexp],
)
cas_edt_id_from_xml_regexp = StringField(
label="Optionnel: expression pour extraire l'identifiant edt",
description="""regexp python appliquée à la réponse XML du serveur CAS pour
retrouver l'id de l'utilisateur sur le SI de l'institution, et notamment sur les
calendrier d'emploi du temps. Par exemple, si cet id est renvoyé dans le champ
<b>supannEmpId</b>, utiliser:
<tt>&lt;cas:supannEmpId&gt;(.*?)&lt;/cas:supannEmpId&gt;</tt>
""",
validators=[Optional(), check_cas_edt_id_from_xml_regexp],
)
cas_ssl_verify = BooleanField("Vérification du certificat SSL")
cas_ssl_certificate_file = FileField(
label="Certificat (PEM)",

View File

@ -2,9 +2,8 @@
Formulaire configuration liens personalisés (menu "Liens")
"""
from flask import g, url_for
from flask_wtf import FlaskForm
from wtforms import FieldList, Form, validators
from wtforms import validators
from wtforms.fields.simple import BooleanField, StringField, SubmitField
from app.models import ScoDocSiteConfig
@ -29,7 +28,7 @@ def PersonalizedLinksForm() -> _PersonalizedLinksForm:
F,
f"link_{idx}",
StringField(
f"Titre",
"Titre",
validators=[
validators.Optional(),
validators.Length(min=1, max=80),
@ -42,7 +41,7 @@ def PersonalizedLinksForm() -> _PersonalizedLinksForm:
F,
f"link_url_{idx}",
StringField(
f"URL",
"URL",
description="adresse, incluant le http.",
validators=[
validators.Optional(),
@ -56,7 +55,7 @@ def PersonalizedLinksForm() -> _PersonalizedLinksForm:
F,
f"link_with_args_{idx}",
BooleanField(
f"ajouter arguments",
"ajouter arguments",
description="query string avec ids",
),
)

View File

@ -7,7 +7,6 @@ from app import db
from app.models import CODE_STR_LEN
from app.models.but_refcomp import ApcNiveau
from app.models.etudiants import Identite
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns

View File

@ -4,8 +4,8 @@
"""
import json
import urllib.parse
import re
import urllib.parse
from flask import flash
from app import current_app, db, log
@ -13,8 +13,6 @@ from app.comp import bonus_spo
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_utils as scu
from datetime import time
from app.scodoc.codes_cursus import (
ABAN,
ABL,
@ -105,6 +103,7 @@ class ScoDocSiteConfig(db.Model):
"cas_validate_route": str,
"cas_attribute_id": str,
"cas_uid_from_mail_regexp": str,
"cas_edt_id_from_xml_regexp": str,
# Assiduité
"morning_time": str,
"lunch_time": str,
@ -174,7 +173,7 @@ class ScoDocSiteConfig(db.Model):
klass = bonus_spo.get_bonus_class_dict().get(class_name)
if klass is None:
flash(
f"""Fonction de calcul bonus sport inexistante: {class_name}.
f"""Fonction de calcul bonus sport inexistante: {class_name}.
Changez ou contactez votre administrateur local."""
)
return klass
@ -287,7 +286,7 @@ class ScoDocSiteConfig(db.Model):
@classmethod
def set(cls, name: str, value: str) -> bool:
"Set parameter, returns True if change. Commit session."
value_str = str(value or "")
value_str = str(value or "").strip()
if (cls.get(name) or "") != value_str:
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
if cfg is None:
@ -430,7 +429,17 @@ class ScoDocSiteConfig(db.Model):
return False
# and returns at least one group on a simple cannonical address
match = pattern.search("emmanuel@exemple.fr")
return len(match.groups()) > 0
return match is not None and len(match.groups()) > 0
@classmethod
def cas_edt_id_from_xml_regexp_is_valid(cls, exp: str) -> bool:
"True si l'expression régulière semble valide"
# check that it compiles
try:
_ = re.compile(exp)
except re.error:
return False
return True
@classmethod
def assi_get_rounded_time(cls, label: str, default: str) -> float:

View File

@ -64,6 +64,8 @@ class FormSemestre(db.Model):
titre = db.Column(db.Text(), nullable=False)
date_debut = db.Column(db.Date(), nullable=False)
date_fin = db.Column(db.Date(), nullable=False)
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
"False si verrouillé"
modalite = db.Column(
@ -257,6 +259,24 @@ class FormSemestre(db.Model):
d["session_id"] = self.session_id()
return d
def get_default_group(self) -> GroupDescr:
"""default ('tous') group.
Le groupe par défaut contient tous les étudiants et existe toujours.
C'est l'unique groupe de la partition sans nom.
"""
default_partition = self.partitions.filter_by(partition_name=None).first()
if default_partition:
return default_partition.groups.first()
raise ScoValueError("Le semestre n'a pas de groupe par défaut")
def get_edt_ids(self) -> list[str]:
"l'ids pour l'emploi du temps: à défaut, les codes étape Apogée"
return (
scu.split_id(self.edt_id)
or [e.etape_apo.strip() for e in self.etapes if e.etape_apo]
or []
)
def get_infos_dict(self) -> dict:
"""Un dict avec des informations sur le semestre
pour les bulletins et autres templates
@ -613,7 +633,7 @@ class FormSemestre(db.Model):
def can_change_groups(self, user: User = None) -> bool:
"""Vrai si l'utilisateur (par def. current) peut changer les groupes dans
ce semestre: vérifie permission et verrouillage.
ce semestre: vérifie permission et verrouillage (mais pas si la partition est éditable).
"""
if not self.etat:
return False # semestre verrouillé
@ -1022,6 +1042,33 @@ class FormSemestre(db.Model):
nb_recorded += 1
return nb_recorded
def change_formation(self, formation_dest: Formation):
"""Associe ce formsemestre à une autre formation.
Ce n'est possible que si la formation destination possède des modules de
même code que ceux utilisés dans la formation d'origine du formsemestre.
S'il manque un module, l'opération est annulée.
Commit (or rollback) session.
"""
ok = True
for mi in self.modimpls:
dest_modules = formation_dest.modules.filter_by(code=mi.module.code).all()
match len(dest_modules):
case 1:
mi.module = dest_modules[0]
db.session.add(mi)
case 0:
print(f"Argh ! no module found with code={mi.module.code}")
ok = False
case _:
print(f"Arg ! several modules found with code={mi.module.code}")
ok = False
if ok:
self.formation_id = formation_dest.id
db.session.commit()
else:
db.session.rollback()
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
notes_formsemestre_responsables = db.Table(

View File

@ -180,7 +180,7 @@ class Partition(db.Model):
"Crée un groupe dans cette partition"
if not self.formsemestre.can_change_groups():
raise AccessDenied(
"""Vous n'avez pas le droit d'effectuer cette opération,
"""Vous n'avez pas le droit d'effectuer cette opération,
ou bien le semestre est verrouillé !"""
)
if group_name:
@ -213,10 +213,12 @@ class GroupDescr(db.Model):
id = db.Column(db.Integer, primary_key=True)
group_id = db.synonym("id")
partition_id = db.Column(db.Integer, db.ForeignKey("partition.id"))
# "A", "C2", ... (NULL for 'all'):
group_name = db.Column(db.String(GROUPNAME_STR_LEN))
# Numero = ordre de presentation
"""nom du groupe: "A", "C2", ... (NULL for 'all')"""
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
numero = db.Column(db.Integer, nullable=False, default=0)
"Numero = ordre de presentation"
etuds = db.relationship(
"Identite",
@ -229,8 +231,12 @@ class GroupDescr(db.Model):
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
)
def get_nom_with_part(self) -> str:
"Nom avec partition: 'TD A'"
def get_nom_with_part(self, default="-") -> str:
"""Nom avec partition: 'TD A'
Si groupe par défaut (tous), utilise default ou "-"
"""
if self.partition.partition_name is None:
return default
return f"{self.partition.partition_name or ''} {self.group_name or '-'}"
def to_dict(self, with_partition=True) -> dict:
@ -241,10 +247,14 @@ class GroupDescr(db.Model):
d["partition"] = self.partition.to_dict(with_groups=False)
return d
def get_edt_ids(self) -> list[str]:
"les ids pour l'emploi du temps: à défaut, le nom scodoc du groupe"
return scu.split_id(self.edt_id) or [self.group_name] or []
def get_nb_inscrits(self) -> int:
"""Nombre inscrits à ce group et au formsemestre.
C'est nécessaire car lors d'une désinscription, on conserve l'appartenance
aux groupes pour facilier une éventuelle -inscription.
aux groupes pour faciliter une éventuelle -inscription.
"""
from app.models.formsemestre import FormSemestreInscription
@ -272,6 +282,46 @@ class GroupDescr(db.Model):
return False
return True
def set_name(self, group_name: str, dest_url: str = None):
"""Set group name, and optionally edt_id.
Check permission (partition must be groups_editable)
and invalidate caches. Commit session.
dest_url is used for error messages.
"""
if not self.partition.formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
if self.group_name is None:
raise ValueError("can't set a name to default group")
if not self.partition.groups_editable:
raise AccessDenied("Partition non éditable")
if group_name:
group_name = group_name.strip()
if not group_name:
raise ScoValueError("nom de groupe vide !", dest_url=dest_url)
if group_name != self.group_name and not GroupDescr.check_name(
self.partition, group_name
):
raise ScoValueError(
"Le nom de groupe existe déjà dans la partition", dest_url=dest_url
)
self.group_name = group_name
db.session.add(self)
db.session.commit()
sco_cache.invalidate_formsemestre(
formsemestre_id=self.partition.formsemestre_id
)
def set_edt_id(self, edt_id: str):
"Set edt_id. Check permission. Commit session."
if not self.partition.formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
if isinstance(edt_id, str):
edt_id = edt_id.strip() or None
self.edt_id = edt_id
db.session.add(self)
db.session.commit()
def remove_etud(self, etud: "Identite"):
"Enlève l'étudiant de ce groupe s'il en fait partie (ne fait rien sinon)"
if etud in self.etuds:

View File

@ -7,6 +7,7 @@ from flask_sqlalchemy.query import Query
from app import db
from app.auth.models import User
from app.comp import df_cache
from app.models import APO_CODE_STR_LEN
from app.models.etudiants import Identite
from app.models.modules import Module
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
@ -21,6 +22,10 @@ class ModuleImpl(db.Model):
__table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),)
id = db.Column(db.Integer, primary_key=True)
code_apogee = db.Column(db.String(APO_CODE_STR_LEN), index=True, nullable=True)
"id de l'element pedagogique Apogee correspondant"
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
moduleimpl_id = db.synonym("id")
module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False)
formsemestre_id = db.Column(
@ -42,12 +47,25 @@ class ModuleImpl(db.Model):
viewonly=True,
)
def __init__(self, **kwargs):
super(ModuleImpl, self).__init__(**kwargs)
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
def get_codes_apogee(self) -> set[str]:
"""Les codes Apogée (codés en base comme "VRT1,VRT2").
(si non renseigné, ceux du module)
"""
if self.code_apogee:
return {x.strip() for x in self.code_apogee.split(",") if x}
return self.module.get_codes_apogee()
def get_edt_ids(self) -> list[str]:
"les ids pour l'emploi du temps: à défaut, les codes Apogée"
return (
scu.split_id(self.edt_id)
or scu.split_id(self.code_apogee)
or self.module.get_edt_ids()
)
def get_evaluations_poids(self) -> pd.DataFrame:
"""Les poids des évaluations vers les UE (accès via cache)"""
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
@ -99,6 +117,7 @@ class ModuleImpl(db.Model):
d["module"] = self.module.to_dict(convert_objects=convert_objects)
else:
d.pop("module", None)
d["code_apogee"] = d["code_apogee"] or "" # pas de None
return d
def can_edit_evaluation(self, user) -> bool:

View File

@ -34,8 +34,10 @@ class Module(db.Model):
# note: en APC, le semestre qui fait autorité est celui de l'UE
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
# id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
"id de l'element pedagogique Apogee correspondant"
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
# Relations:
@ -283,6 +285,10 @@ class Module(db.Model):
return {x.strip() for x in self.code_apogee.split(",") if x}
return set()
def get_edt_ids(self) -> list[str]:
"les ids pour l'emploi du temps: à défaut, le 1er code Apogée"
return scu.split_id(self.edt_id) or scu.split_id(self.code_apogee) or []
def get_parcours(self) -> list[ApcParcours]:
"""Les parcours utilisant ce module.
Si tous les parcours, liste vide (!).

View File

@ -44,15 +44,15 @@ import sco_version
# Multiselect menus are used on a few pages and not loaded by default
BOOTSTRAP_MULTISELECT_JS = [
"libjs/bootstrap-3.1.1-dist/js/bootstrap.min.js",
"libjs/bootstrap-multiselect/bootstrap-multiselect.js",
"libjs/bootstrap/js/bootstrap.min.js",
"libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.js",
"libjs/purl.js",
]
BOOTSTRAP_MULTISELECT_CSS = [
"libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css",
"libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css",
"libjs/bootstrap-multiselect/bootstrap-multiselect.css",
"libjs/bootstrap/css/bootstrap.min.css",
"libjs/bootstrap/css/bootstrap-theme.min.css",
"libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css",
]
@ -95,7 +95,7 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
<title>%(page_title)s</title>
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
@ -212,7 +212,8 @@ def sco_header(
<script>
window.onload=function(){{enableTooltips("gtrcontent")}};
var SCO_URL="{scu.ScoURL()}";
const SCO_URL="{scu.ScoURL()}";
const SCO_TIMEZONE="{scu.TIME_ZONE}";
</script>"""
)

View File

@ -157,10 +157,15 @@ class JustificatifArchiver(BaseArchiver):
Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s)
dans la trace de l'étudiant
"""
print("debug : ", archive_name, filename, has_trace)
if str(etud.id) not in self.list_oids(etud.dept_id):
raise ValueError(f"Aucune archive pour etudid[{etud.id}]")
archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id)
try:
archive_id = self.get_id_from_name(
etud.id, archive_name, dept_id=etud.dept_id
)
except ScoValueError:
raise ValueError(f"Archive Inconnue [{archive_name}]")
if filename is not None:
if filename not in self.list_archive(archive_id, dept_id=etud.dept_id):

View File

@ -987,7 +987,7 @@ def formsemestre_has_decisions(formsemestre_id):
"""
cnx = ndb.GetDBConnexion()
validations = scolar_formsemestre_validation_list(
cnx, args={"formsemestre_id": formsemestre_id}
cnx, args={"formsemestre_id": formsemestre_id, "is_external": False}
)
return len(validations) > 0

View File

@ -80,7 +80,7 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
f"""<h2>Confirmer la suppression de la formation
{formation.titre} ({formation.acronyme}) ?
</h2>
<p><b>Attention:</b> la suppression d'une formation est <b>irréversible</b>
<p><b>Attention:</b> la suppression d'une formation est <b>irréversible</b>
et implique la supression de toutes les UE, matières et modules de la formation !
</p>
""",
@ -273,7 +273,8 @@ def formation_edit(formation_id=None, create=False):
"\n".join(H)
+ tf_error_message(
f"""Valeurs incorrectes: il existe déjà <a href="{
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=other_formations[0].id)
url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept, formation_id=other_formations[0].id)
}">une formation</a> avec même titre,
acronyme et version.
"""
@ -285,11 +286,11 @@ def formation_edit(formation_id=None, create=False):
if create:
formation = do_formation_create(tf[2])
else:
do_formation_edit(tf[2])
flash(
f"""Création de la formation {
formation.titre} ({formation.acronyme}) version {formation.version}"""
)
if do_formation_edit(tf[2]):
flash(
f"""Modification de la formation {
formation.titre} ({formation.acronyme}) version {formation.version}"""
)
return flask.redirect(
url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id
@ -335,8 +336,8 @@ def do_formation_create(args: dict) -> Formation:
return formation
def do_formation_edit(args):
"edit a formation"
def do_formation_edit(args) -> bool:
"edit a formation, returns True if modified"
# On ne peut jamais supprimer le code formation:
if "formation_code" in args and not args["formation_code"]:
@ -350,11 +351,16 @@ def do_formation_edit(args):
if "type_parcours" in args:
del args["type_parcours"]
modified = False
for field in formation.__dict__:
if field in args:
value = args[field].strip() if isinstance(args[field], str) else args[field]
if field and field[0] != "_":
if field and field[0] != "_" and getattr(formation, field, None) != value:
setattr(formation, field, value)
modified = True
if not modified:
return False
db.session.add(formation)
try:
@ -370,6 +376,7 @@ def do_formation_edit(args):
),
) from exc
formation.invalidate_cached_sems()
return True
def module_move(module_id, after=0, redirect=True):

View File

@ -74,7 +74,8 @@ _moduleEditor = ndb.EditableTable(
"semestre_id",
"numero",
"code_apogee",
"module_type"
"module_type",
"edt_id",
#'ects'
),
sortkey="numero, code, titre",
@ -171,7 +172,7 @@ def do_module_delete(oid):
d'en créer une nouvelle version pour la modifier sans affecter
les semestres déjà en place.
</p>
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=mod["formation_id"])}">reprendre</a>
"""
raise ScoGenError(err_page)
@ -645,7 +646,7 @@ def module_edit(
"title": "Code Apogée",
"size": 25,
"explanation": """(optionnel) code élément pédagogique Apogée ou liste de codes ELP
séparés par des virgules (ce code est propre à chaque établissement, se rapprocher
séparés par des virgules (ce code est propre à chaque établissement, se rapprocher
du référent Apogée).
""",
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
@ -682,7 +683,7 @@ def module_edit(
]
+ ["-1"],
"explanation": """Parcours dans lesquels est utilisé ce module.<br>
Attention: si le module ne doit pas avoir les mêmes coefficients suivant le parcours,
Attention: si le module ne doit pas avoir les mêmes coefficients suivant le parcours,
il faut en créer plusieurs versions, car dans ScoDoc chaque module a ses coefficients.""",
},
)
@ -746,7 +747,7 @@ def module_edit(
"input_type": "separator",
"title": f"""<span class="fontred">{scu.EMO_WARNING }
Pas de parcours:
<a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
<a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=formation.id)
}">associer un référentiel de compétence</a>
</span>""",

View File

@ -29,178 +29,387 @@
XXX usage uniquement experimental pour tests implémentations
XXX incompatible avec les ics HyperPlanning Paris 13 (était pour GPU).
"""
from datetime import timezone
import re
import icalendar
import pprint
import traceback
import urllib
import app.scodoc.sco_utils as scu
from flask import g, url_for
from app import log
from app.scodoc import html_sco_header
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_preferences
from app.models import FormSemestre, GroupDescr, ModuleImpl, ScoDocSiteConfig
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu
def formsemestre_get_ics_url(sem):
"""
edt_sem_ics_url est un template
utilisé avec .format(sem=sem)
Par exemple:
https://example.fr/agenda/{sem[etapes][0]}
"""
ics_url_tmpl = sco_preferences.get_preference(
"edt_sem_ics_url", sem["formsemestre_id"]
)
if not ics_url_tmpl:
def get_ics_filename(edt_id: str) -> str | None:
"Le chemin vers l'ics de cet edt_id"
edt_ics_path = ScoDocSiteConfig.get("edt_ics_path")
if not edt_ics_path.strip():
return None
try:
ics_url = ics_url_tmpl.format(sem=sem)
except:
log(
f"""Exception in formsemestre_get_ics_url(formsemestre_id={sem["formsemestre_id"]})
ics_url_tmpl='{ics_url_tmpl}'
"""
return edt_ics_path.format(edt_id=edt_id)
def formsemestre_load_calendar(
formsemestre: FormSemestre = None, edt_id: str = None
) -> tuple[bytes, icalendar.cal.Calendar]:
"""Load ics data, return raw ics and decoded calendar.
Raises ScoValueError if not configured or not available or invalid format.
"""
edt_ids = []
if edt_id is None and formsemestre:
edt_ids = formsemestre.get_edt_ids()
if not edt_ids:
raise ScoValueError(
"accès aux emplois du temps non configuré pour ce semestre (pas d'edt_id)"
)
log(traceback.format_exc())
return None
return ics_url
# Ne charge qu'un seul ics pour le semestre, prend uniquement
# le premier edt_id
ics_filename = get_ics_filename(edt_ids[0])
if ics_filename is None:
raise ScoValueError("accès aux emplois du temps non configuré (pas de chemin)")
try:
with open(ics_filename, "rb") as file:
log(f"Loading edt from {ics_filename}")
data = file.read()
try:
calendar = icalendar.Calendar.from_ical(data)
except ValueError as exc:
log(
f"""formsemestre_load_calendar: error importing ics for {
formsemestre or ''}\npath='{ics_filename}'"""
)
raise ScoValueError(
f"calendrier ics illisible (edt_id={edt_id})"
) from exc
except FileNotFoundError as exc:
log(
f"formsemestre_load_calendar: ics not found for {formsemestre or ''}\npath='{ics_filename}'"
)
raise ScoValueError(
f"Fichier ics introuvable (filename={ics_filename})"
) from exc
except PermissionError as exc:
log(
f"""formsemestre_load_calendar: permission denied for {formsemestre or ''
}\npath='{ics_filename}'"""
)
raise ScoValueError(
f"Fichier ics inaccessible: vérifier permissions (filename={ics_filename})"
) from exc
return data, calendar
def formsemestre_load_ics(sem):
"""Load ics data, from our cache or, when necessary, from external provider"""
# TODO: cacher le résultat
ics_url = formsemestre_get_ics_url(sem)
if not ics_url:
ics_data = ""
else:
log(f"Loading edt from {ics_url}")
# 5s TODO: add config parameter, eg for slow networks
f = urllib.request.urlopen(ics_url, timeout=5)
ics_data = f.read()
f.close()
cal = icalendar.Calendar.from_ical(ics_data)
return cal
# --- Couleurs des évènements emploi du temps
_COLOR_PALETTE = [
"#ff6961",
"#ffb480",
"#f8f38d",
"#42d6a4",
"#08cad1",
"#59adf6",
"#9d94ff",
"#c780e8",
]
_EVENT_DEFAULT_COLOR = "rgb(214, 233, 248)"
def get_edt_transcodage_groups(formsemestre_id):
"""-> { nom_groupe_edt : nom_groupe_scodoc }"""
# TODO: valider ces données au moment où on enregistre les préférences
edt2sco = {}
sco2edt = {}
msg = "" # message erreur, '' si ok
txt = sco_preferences.get_preference("edt_groups2scodoc", formsemestre_id)
if not txt:
return edt2sco, sco2edt, msg
line_num = 1
for line in txt.split("\n"):
fs = [s.strip() for s in line.split(";")]
if len(fs) == 1: # groupe 'tous'
edt2sco[fs[0]] = None
sco2edt[None] = fs[0]
elif len(fs) == 2:
edt2sco[fs[0]] = fs[1]
sco2edt[fs[1]] = fs[0]
else:
msg = f"ligne {line_num} invalide"
line_num += 1
log(f"sco2edt={pprint.pformat(sco2edt)}")
return edt2sco, sco2edt, msg
def group_edt_json(group_id, start="", end=""): # actuellement inutilisé
"""EDT complet du semestre, au format JSON
TODO: indiquer un groupe
TODO: utiliser start et end (2 dates au format ISO YYYY-MM-DD)
TODO: cacher
def formsemestre_edt_dict(
formsemestre: FormSemestre, group_ids: list[int] = None
) -> list[dict]:
"""EDT complet du semestre, comme une liste de dict serialisable en json.
Fonction appelée par l'API /formsemestre/<int:formsemestre_id>/edt
group_ids indiquer les groupes ScoDoc à afficher (les autres sont filtrés).
Les évènements pour lesquels le groupe ScoDoc n'est pas reconnu sont
toujours présents.
TODO: spécifier intervalle de dates start et end
"""
group = sco_groups.get_group(group_id)
sem = sco_formsemestre.get_formsemestre(group["formsemestre_id"])
edt2sco, sco2edt, msg = get_edt_transcodage_groups(group["formsemestre_id"])
group_ids_set = set(group_ids) if group_ids else set()
try:
events_scodoc = _load_and_convert_ics(formsemestre)
except ScoValueError as exc:
return exc.args[0]
# Génération des événements pour le calendrier html
events_cal = []
for event in events_scodoc:
group: GroupDescr | bool = event["group"]
if group is False:
group_disp = f"""<div class="group-edt">
<span title="extraction emploi du temps non configurée">
{scu.EMO_WARNING} non configuré</span>
</div>"""
else:
group_disp = (
f"""<div class="group-name">{group.get_nom_with_part(default="promo")}</div>"""
if group
else f"""<div class="group-edt">{event['edt_group']}
<span title="vérifier noms de groupe ou configuration extraction edt">
{scu.EMO_WARNING} non reconnu</span>
</div>"""
)
if group and group_ids_set and group.id not in group_ids_set:
continue # ignore cet évènement
modimpl: ModuleImpl | bool = event["modimpl"]
url_abs = (
url_for(
"assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
heure_deb=event["heure_deb"],
heure_fin=event["heure_fin"],
moduleimpl_id=modimpl.id,
jour=event["jour"],
)
if modimpl and group
else None
)
match modimpl:
case False: # EDT non configuré
mod_disp = f"""<span>{scu.EMO_WARNING} non configuré</span>"""
bubble = "extraction emploi du temps non configurée"
case None: # Module edt non trouvé dans ScoDoc
mod_disp = f"""<span class="mod-etd">{
scu.EMO_WARNING} {event['edt_module']}</span>"""
bubble = "code module non trouvé dans ScoDoc. Vérifier configuration."
case _: # module EDT bien retrouvé dans ScoDoc
mod_disp = f"""<span class="mod-name mod-code" title="{
modimpl.module.abbrev or ""} ({event['edt_module']})">{
modimpl.module.code}</span>"""
bubble = f"{modimpl.module.abbrev or ''} ({event['edt_module']})"
edt_group_name = sco2edt.get(group["group_name"], group["group_name"])
log("group scodoc=%s : edt=%s" % (group["group_name"], edt_group_name))
title = f"""<div class = "module-edt" title="{bubble} {event['title_edt']}">
<a class="discretelink" href="{url_abs or ''}">{mod_disp} <span>{event['title']}</span></a>
</div>
"""
cal = formsemestre_load_ics(sem)
events = [e for e in cal.walk() if e.name == "VEVENT"]
J = []
for e in events:
# if e['X-GROUP-ID'].strip() == edt_group_name:
if "DESCRIPTION" in e:
d = {
"title": e.decoded("DESCRIPTION"), # + '/' + e['X-GROUP-ID'],
"start": e.decoded("dtstart").isoformat(),
"end": e.decoded("dtend").isoformat(),
}
J.append(d)
# --- Lien saisie abs
link_abs = (
f"""<div class="module-edt link-abs"><a class="stdlink" href="{
url_abs}">absences</a>
</div>"""
if url_abs
else ""
)
d = {
# Champs utilisés par tui.calendar
"calendarId": "cal1",
"title": f"""{title} {group_disp} {link_abs}""",
"start": event["start"],
"end": event["end"],
"backgroundColor": event["group_bg_color"],
# Infos brutes pour usage API éventuel
"group_id": group.id if group else None,
"group_edt_id": event["edt_group"],
"moduleimpl_id": modimpl.id if modimpl else None,
}
events_cal.append(d)
return scu.sendJSON(J)
return events_cal
# def experimental_calendar(group_id=None, formsemestre_id=None): # inutilisé
# """experimental page"""
# return "\n".join(
# [
# html_sco_header.sco_header(
# javascripts=[
# "libjs/purl.js",
# "libjs/moment.min.js",
# "libjs/fullcalendar/fullcalendar.min.js",
# ],
# cssstyles=[
# # 'libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css',
# # 'libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css',
# # 'libjs/bootstrap-multiselect/bootstrap-multiselect.css'
# "libjs/fullcalendar/fullcalendar.css",
# # media='print' 'libjs/fullcalendar/fullcalendar.print.css'
# ],
# ),
# """<style>
# #loading {
# display: none;
# position: absolute;
# top: 10px;
# right: 10px;
# }
# </style>
# """,
# """<form id="group_selector" method="get">
# <span style="font-weight: bold; font-size:120%">Emplois du temps du groupe</span>""",
# sco_groups_view.menu_group_choice(
# group_id=group_id, formsemestre_id=formsemestre_id
# ),
# """</form><div id="loading">loading...</div>
# <div id="calendar"></div>
# """,
# html_sco_header.sco_footer(),
# """<script>
# $(document).ready(function() {
def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]:
"chargement fichier, filtrage et extraction des identifiants."
# Chargement du calendier ics
_, calendar = formsemestre_load_calendar(formsemestre)
if not calendar:
return []
# --- Paramètres d'extraction
edt_ics_title_field = ScoDocSiteConfig.get("edt_ics_title_field")
edt_ics_title_regexp = ScoDocSiteConfig.get("edt_ics_title_regexp")
try:
edt_ics_title_pattern = (
re.compile(edt_ics_title_regexp) if edt_ics_title_regexp else None
)
except re.error as exc:
raise ScoValueError(
"expression d'extraction du titre depuis l'emploi du temps invalide"
) from exc
edt_ics_group_field = ScoDocSiteConfig.get("edt_ics_group_field")
edt_ics_group_regexp = ScoDocSiteConfig.get("edt_ics_group_regexp")
try:
edt_ics_group_pattern = (
re.compile(edt_ics_group_regexp) if edt_ics_group_regexp else None
)
except re.error as exc:
raise ScoValueError(
"expression d'extraction du groupe depuis l'emploi du temps invalide"
) from exc
edt_ics_mod_field = ScoDocSiteConfig.get("edt_ics_mod_field")
edt_ics_mod_regexp = ScoDocSiteConfig.get("edt_ics_mod_regexp")
try:
edt_ics_mod_pattern = (
re.compile(edt_ics_mod_regexp) if edt_ics_mod_regexp else None
)
except re.error as exc:
raise ScoValueError(
"expression d'extraction du module depuis l'emploi du temps invalide"
) from exc
# --- Correspondances id edt -> id scodoc pour groupes, modules et enseignants
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
group_colors = {
group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1]
for i, group_name in enumerate(edt2group)
}
default_group = formsemestre.get_default_group()
edt2modimpl = formsemestre_retreive_modimpls_from_edt_id(formsemestre)
# ---
events = [e for e in calendar.walk() if e.name == "VEVENT"]
events_sco = []
for event in events:
if "DESCRIPTION" in event:
# --- Titre de l'évènement
title_edt = (
extract_event_data(event, edt_ics_title_field, edt_ics_title_pattern)
if edt_ics_title_pattern
else "non configuré"
)
# title remplacé par le nom du module scodoc quand il est trouvé
title = title_edt
# --- Group
if edt_ics_group_pattern:
edt_group = extract_event_data(
event, edt_ics_group_field, edt_ics_group_pattern
)
# si pas de groupe dans l'event, ou si groupe non reconnu,
# prend toute la promo ("tous")
group: GroupDescr = (
edt2group.get(edt_group, default_group)
if edt_group
else default_group
)
group_bg_color = (
group_colors.get(edt_group, _EVENT_DEFAULT_COLOR)
if group
else "lightgrey"
)
else:
edt_group = ""
group = False
group_bg_color = _EVENT_DEFAULT_COLOR
# var group_id = $.url().param()['group_id'];
# --- ModuleImpl
if edt_ics_mod_pattern:
edt_module = extract_event_data(
event, edt_ics_mod_field, edt_ics_mod_pattern
)
modimpl: ModuleImpl = edt2modimpl.get(edt_module, None)
if modimpl:
title = modimpl.module.titre_str()
else:
modimpl = False
edt_module = ""
# --- TODO: enseignant
#
events_sco.append(
{
"title": title, # titre event ou nom module
"title_edt": title_edt, # titre event
"edt_group": edt_group, # id group edt non traduit
"group": group, # False si extracteur non configuré
"group_bg_color": group_bg_color, # associée au groupe
"modimpl": modimpl, # False si extracteur non configuré
"edt_module": edt_module, # id module edt non traduit
# heures pour saisie abs: en heure LOCALE DU SERVEUR
"heure_deb": event.decoded("dtstart")
.replace(tzinfo=timezone.utc)
.astimezone(tz=None)
.strftime("%H:%M"),
"heure_fin": event.decoded("dtend")
.replace(tzinfo=timezone.utc)
.astimezone(tz=None)
.strftime("%H:%M"),
"jour": event.decoded("dtstart").date().isoformat(),
"start": event.decoded("dtstart").isoformat(),
"end": event.decoded("dtend").isoformat(),
}
)
return events_sco
# $('#calendar').fullCalendar({
# events: {
# url: 'group_edt_json?group_id=' + group_id,
# error: function() {
# $('#script-warning').show();
# }
# },
# timeFormat: 'HH:mm',
# timezone: 'local', // heure locale du client
# loading: function(bool) {
# $('#loading').toggle(bool);
# }
# });
# });
# </script>
# """,
# ]
# )
def extract_event_data(
event: icalendar.cal.Event, ics_field: str, pattern: re.Pattern
) -> str:
"""Extrait la chaine (id) de l'évènement."""
if not event.has_key(ics_field):
return "-"
data = event.decoded(ics_field).decode("utf-8") # assume ics in utf8
m = pattern.search(data)
if m and len(m.groups()) > 0:
return m.group(1)
# fallback: ics field, complete
return data
# def extract_event_title(event: icalendar.cal.Event) -> str:
# """Extrait le titre à afficher dans nos calendriers.
# En effet, le titre présent dans l'ics emploi du temps est souvent complexe et peu parlant.
# Par exemple, à l'USPN, Hyperplanning nous donne:
# 'Matière : VRETR113 - Mathematiques du sig (VRETR113\nEnseignant : 1234 - M. DUPONT PIERRE\nTD : TDB\nSalle : L112 (IUTV) - L112\n'
# """
# # TODO: fonction ajustée à l'USPN, devra être paramétrable d'une façon ou d'une autre: regexp ?
# if not event.has_key("DESCRIPTION"):
# return "-"
# description = event.decoded("DESCRIPTION").decode("utf-8") # assume ics in utf8
# # ici on prend le nom du module
# m = re.search(r"Matière : \w+ - ([\w\.\s']+)", description)
# if m and len(m.groups()) > 0:
# return m.group(1)
# # fallback: full description
# return description
# def extract_event_module(event: icalendar.cal.Event) -> str:
# """Extrait le code module de l'emplois du temps.
# Chaine vide si ne le trouve pas.
# Par exemple, à l'USPN, Hyperplanning nous donne le code 'VRETR113' dans DESCRIPTION
# 'Matière : VRETR113 - Mathematiques du sig (VRETR113\nEnseignant : 1234 - M. DUPONT PIERRE\nTD : TDB\nSalle : L112 (IUTV) - L112\n'
# """
# # TODO: fonction ajustée à l'USPN, devra être paramétrable d'une façon ou d'une autre: regexp ?
# if not event.has_key("DESCRIPTION"):
# return "-"
# description = event.decoded("DESCRIPTION").decode("utf-8") # assume ics in utf8
# # extraction du code:
# m = re.search(r"Matière : ([A-Z][A-Z0-9]+)", description)
# if m and len(m.groups()) > 0:
# return m.group(1)
# return ""
# def extract_event_group(event: icalendar.cal.Event) -> str:
# """Extrait le nom du groupe (TD, ...). "" si pas de match."""
# # Utilise ici le SUMMARY
# # qui est de la forme
# # SUMMARY;LANGUAGE=fr:TP2 GPR1 - VCYR303 - Services reseaux ava (VCYR303) - 1234 - M. VIENNET EMMANUEL - V2ROM - BUT2 RT pa. ROM - Groupe 1
# if not event.has_key("SUMMARY"):
# return "-"
# summary = event.decoded("SUMMARY").decode("utf-8") # assume ics in utf8
# # extraction du code:
# m = re.search(r".*- ([\w\s]+)$", summary)
# if m and len(m.groups()) > 0:
# return m.group(1).strip()
# return ""
def formsemestre_retreive_modimpls_from_edt_id(
formsemestre: FormSemestre,
) -> dict[str, ModuleImpl]:
"""Construit un dict donnant le moduleimpl de chaque edt_id"""
edt2modimpl = {}
for modimpl in formsemestre.modimpls:
for edt_id in modimpl.get_edt_ids():
if edt_id:
edt2modimpl[edt_id] = modimpl
return edt2modimpl
def formsemestre_retreive_groups_from_edt_id(
formsemestre: FormSemestre,
) -> dict[str, GroupDescr]:
"""Construit un dict donnant le groupe de chaque edt_id"""
edt2group = {}
for partition in formsemestre.partitions:
for g in partition.groups:
for edt_id in g.get_edt_ids():
edt2group[edt_id] = g
return edt2group

View File

@ -45,6 +45,7 @@ from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_evaluations
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
@ -87,7 +88,7 @@ def evaluation_create_form(
{html_sco_header.sco_header()}
<h2>Opération non autorisée</h2>
<p>Modification évaluation impossible pour {current_user.get_nomplogin()}</p>
<p><a href="{url_for('notes.moduleimpl_status',
<p><a href="{url_for('notes.moduleimpl_status',
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
}" class="stdlink">Revenir</a>
</p>
@ -131,7 +132,7 @@ def evaluation_create_form(
H = [
f"""<h3>{action} en
{scu.MODULE_TYPE_NAMES[mod["module_type"]]} <a class="stdlink" href="{
url_for("notes.moduleimpl_status",
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
}">{mod["code"] or "module sans code"} {mod["titre"]}</a> {link}</h3>
"""
@ -299,7 +300,7 @@ def evaluation_create_form(
"type": "float",
"explanation": f"""
<span class="eval_coef_ue" title="coef. du module dans cette UE">({
"coef. mod.:" +str(coef_ue) if coef_ue
"coef. mod.:" +str(coef_ue) if coef_ue
else "ce module n'a pas de coef. dans cette UE"
})</span>
<span class="eval_coef_ue_titre">{ue.titre}</span>
@ -382,4 +383,5 @@ def evaluation_create_form(
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
db.session.add(evaluation)
db.session.commit()
sco_cache.invalidate_formsemestre(evaluation.moduleimpl.formsemestre.id)
return flask.redirect(dest_url)

View File

@ -307,7 +307,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
D = sco_xml.xml_to_dicts(f)
except Exception as exc:
raise ScoFormatError(
"""Ce document xml ne correspond pas à un programme exporté par ScoDoc.
"""Ce document xml ne correspond pas à un programme exporté par ScoDoc.
(élément 'formation' inexistant par exemple)."""
) from exc
assert D[0] == "formation"
@ -322,8 +322,13 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
referentiel_competence_id = _formation_retreive_refcomp(f_dict)
f_dict["referentiel_competence_id"] = referentiel_competence_id
# find new version number
acronyme_lower = f_dict["acronyme"].lower() if f_dict["acronyme"] else ""
titre_lower = f_dict["titre"].lower() if f_dict["titre"] else ""
formations: list[Formation] = Formation.query.filter_by(
acronyme=f_dict["acronyme"], titre=f_dict["titre"], dept_id=f_dict["dept_id"]
dept_id=f_dict["dept_id"]
).filter(
db.func.lower(Formation.acronyme) == acronyme_lower,
db.func.lower(Formation.titre) == titre_lower,
)
if formations.count():
version = max(f.version or 0 for f in formations)
@ -518,6 +523,7 @@ def formation_list_table() -> GenTable:
"_titre_link_class": "stdlink",
"_titre_id": f"""titre-{acronyme_no_spaces}""",
"version": formation.version or 0,
"commentaire": formation.commentaire or "",
}
# Ajoute les semestres associés à chaque formation:
row["formsemestres"] = formation.formsemestres.order_by(
@ -594,10 +600,12 @@ def formation_list_table() -> GenTable:
"formation_code",
"version",
"titre",
"commentaire",
"sems_list_txt",
)
titles = {
"buttons": "",
"commentaire": "Commentaire",
"acronyme": "Acro.",
"parcours_name": "Type",
"titre": "Titre",

View File

@ -68,6 +68,7 @@ _formsemestreEditor = ndb.EditableTable(
"ens_can_edit_eval",
"elt_sem_apo",
"elt_annee_apo",
"edt_id",
),
filter_dept=True,
sortkey="date_debut",
@ -571,7 +572,7 @@ def view_formsemestre_by_etape(etape_apo=None, fmt="html"):
),
html_title=html_title,
html_next_section="""<form action="view_formsemestre_by_etape">
Etape: <input name="etape_apo" type="text" size="8"></input>
Etape: <input name="etape_apo" type="text" size="8"></input>
</form>""",
)
tab.base_url = "%s?etape_apo=%s" % (request.base_url, etape_apo or "")

View File

@ -86,11 +86,6 @@ def build_context_dict(formsemestre_id: int) -> dict:
def formsemestre_custommenu_html(formsemestre_id):
"HTML code for custom menu"
menu = []
# Calendrier électronique ?
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
ics_url = sco_edt_cal.formsemestre_get_ics_url(sem)
if ics_url:
menu.append({"title": "Emploi du temps (ics)", "url": ics_url})
# Liens globaux (config. générale)
params = build_context_dict(formsemestre_id)
for link in ScoDocSiteConfig.get_perso_links():

View File

@ -40,6 +40,7 @@ from app.models import (
ModuleImpl,
Evaluation,
UniteEns,
ScoDocSiteConfig,
ScolarFormSemestreValidation,
ScolarAutorisationInscription,
ApcValidationAnnee,
@ -213,6 +214,9 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
)
for index, resp in enumerate(formsemestre.responsables):
initvalues[resp_fields[index]] = uid2display.get(resp.id)
group_tous = formsemestre.get_default_group()
if group_tous:
initvalues["edt_promo_id"] = group_tous.edt_id or ""
# Liste des ID de semestres
if formation.type_parcours is not None:
@ -321,7 +325,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
"size": 40,
"title": "Nom de ce semestre",
"explanation": f"""n'indiquez pas les dates, ni le semestre, ni la modalité dans
le titre: ils seront automatiquement ajoutés <input type="button"
le titre: ils seront automatiquement ajoutés <input type="button"
value="remettre titre par défaut" onClick="document.tf.titre.value='{
_default_sem_title(formation)}';"/>""",
"allow_null": False,
@ -445,13 +449,41 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
},
)
)
if ScoDocSiteConfig.get("edt_ics_path"):
modform.append(
(
"edt_id",
{
"size": 32,
"title": "Identifiant EDT",
"explanation": "optionnel, identifiant sur le logiciel emploi du temps (par défaut, utilise la première étape Apogée).",
"allow_null": True,
},
)
)
modform.append(
(
"edt_promo_id",
{
"size": 32,
"title": "Identifiant EDT promo",
"explanation": """optionnel, identifiant du groupe "tous"
(promotion complète) dans l'emploi du temps.""",
"allow_null": True,
},
)
)
if edit:
formtit = f"""
<p><a class="stdlink" href="{url_for("notes.formsemestre_edit_uecoefs",
<p><a class="stdlink" href="{url_for("notes.formsemestre_edit_uecoefs",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Modifier les coefficients des UE capitalisées</a>
</p>
<h3>Sélectionner les modules, leurs responsables et les étudiants
<p><a class="stdlink" href="{url_for("notes.formsemestre_edit_modimpls_codes",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Modifier les codes Apogée et emploi du temps des modules</a>
</p>
<h3>Sélectionner les modules, leurs responsables et les étudiants
à inscrire:</h3>
"""
else:
@ -510,7 +542,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
{
"input_type": "boolcheckbox",
"title": "",
"explanation": """Autoriser tous les enseignants associés
"explanation": """Autoriser tous les enseignants associés
à un module à y créer des évaluations""",
},
),
@ -585,8 +617,8 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
{
"input_type": "separator",
"title": f"""<span class="fontred">{scu.EMO_WARNING }
Pas de parcours:
<a class="stdlink" href="{ url_for('notes.ue_table',
Pas de parcours:
<a class="stdlink" href="{ url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept, formation_id=formation.id)
}">vérifier la formation</a>
</span>""",
@ -784,7 +816,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
if tf[0] == 0 or msg:
return f"""<p>Formation <a class="discretelink" href="{
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept,
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept,
formation_id=formation.id)
}"><em>{formation.titre}</em> ({formation.acronyme}), version {
formation.version}, code {formation.formation_code}</a>
@ -953,8 +985,16 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
db.session.get(ApcParcours, int(parcour_id_str))
for parcour_id_str in tf[2]["parcours"]
]
# --- Id edt du groupe par défault
if "edt_promo_id" in tf[2]:
group_tous = formsemestre.get_default_group()
if group_tous:
group_tous.edt_id = tf[2]["edt_promo_id"]
db.session.add(group_tous)
db.session.add(formsemestre)
db.session.commit()
# --- Crée ou met à jour les groupes de parcours BUT
formsemestre.setup_parcours_groups()
# peut être nécessaire dans certains cas:
@ -969,11 +1009,11 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
</li>
</ul></span>
</div>
{"<p>Modification effectuée</p>" if ok
{"<p>Modification effectuée</p>" if ok
else "<p>Modules non modifiés</p>"
}
<a class="stdlink" href="{
url_for('notes.formsemestre_status',
url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">retour au tableau de bord</a>
"""
@ -1309,11 +1349,11 @@ def formsemestre_delete(formsemestre_id):
html_sco_header.html_sem_header("Suppression du semestre"),
"""<div class="ue_warning"><span>Attention !</span>
<p class="help">A n'utiliser qu'en cas d'erreur lors de la saisie d'une formation. Normalement,
<b>un semestre ne doit jamais être supprimé</b>
<b>un semestre ne doit jamais être supprimé</b>
(on perd la mémoire des notes et de tous les événements liés à ce semestre !).
</p>
<p class="help">Tous les modules de ce semestre seront supprimés.
<p class="help">Tous les modules de ce semestre seront supprimés.
Ceci n'est possible que si :
</p>
<ol>
@ -1497,24 +1537,24 @@ def do_formsemestre_delete(formsemestre_id):
req = "DELETE FROM sco_prefs WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
# --- Suppression des groupes et partitions
req = """DELETE FROM group_membership
WHERE group_id IN
req = """DELETE FROM group_membership
WHERE group_id IN
(SELECT gm.group_id FROM group_membership gm, partition p, group_descr gd
WHERE gm.group_id = gd.id AND gd.partition_id = p.id
WHERE gm.group_id = gd.id AND gd.partition_id = p.id
AND p.formsemestre_id=%(formsemestre_id)s)
"""
cursor.execute(req, {"formsemestre_id": formsemestre_id})
req = """DELETE FROM group_descr
WHERE id IN
(SELECT gd.id FROM group_descr gd, partition p
WHERE gd.partition_id = p.id
req = """DELETE FROM group_descr
WHERE id IN
(SELECT gd.id FROM group_descr gd, partition p
WHERE gd.partition_id = p.id
AND p.formsemestre_id=%(formsemestre_id)s)
"""
cursor.execute(req, {"formsemestre_id": formsemestre_id})
req = "DELETE FROM partition WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
# --- Responsables
req = """DELETE FROM notes_formsemestre_responsables
req = """DELETE FROM notes_formsemestre_responsables
WHERE formsemestre_id=%(formsemestre_id)s"""
cursor.execute(req, {"formsemestre_id": formsemestre_id})
# --- Etapes
@ -1606,7 +1646,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
Indiquez "auto" (ou laisser vide) pour que ScoDoc calcule automatiquement le coefficient,
ou bien entrez une valeur (nombre réel).
</p>
<p class="help">Dans le doute, si le mode auto n'est pas applicable et que tous les étudiants sont inscrits aux mêmes modules de ce semestre, prenez comme coefficient la somme indiquée.
<p class="help">Dans le doute, si le mode auto n'est pas applicable et que tous les étudiants sont inscrits aux mêmes modules de ce semestre, prenez comme coefficient la somme indiquée.
Sinon, référez vous au programme pédagogique. Les lignes en <font color="red">rouge</font>
sont à changer.
</p>
@ -1734,7 +1774,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
return f"""{html_sco_header.html_sem_header("Coefficients des UE du semestre")}
{" ".join(message)}
<p><a class="stdlink" href="{url_for("notes.formsemestre_status",
<p><a class="stdlink" href="{url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">Revenir au tableau de bord</a>
</p>

View File

@ -257,6 +257,13 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
"enabled": current_user.has_permission(Permission.EditFormSemestre),
"helpmsg": "",
},
{
"title": "Expérimental: emploi du temps",
"endpoint": "notes.formsemestre_edt",
"args": {"formsemestre_id": formsemestre_id},
"enabled": True,
"helpmsg": "",
},
]
# debug :
if current_app.config["DEBUG"]:
@ -796,10 +803,10 @@ def formsemestre_description(
tab.html_before_table = f"""
<form name="f" method="get" action="{request.base_url}">
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
<input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()"
<input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()"
{ "checked" if with_evals else "" }
>indiquer les évaluations</input>
<input type="checkbox" name="with_parcours" value="1" onchange="document.f.submit()"
<input type="checkbox" name="with_parcours" value="1" onchange="document.f.submit()"
{ "checked" if with_parcours else "" }
>indiquer les parcours BUT</input>
"""
@ -836,7 +843,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
'Tous les étudiants'}
</div>
<div class="sem-groups-partition-titre">{
"Gestion de l'assiduité" if not partition_is_empty else ""
"Gestion de l'assiduité" if not partition_is_empty else ""
}</div>
"""
)
@ -925,8 +932,8 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
if formsemestre.can_change_groups():
H.append(
f""" (<a href="{url_for("scolar.partition_editor",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
edit_partition=1)
}" class="stdlink">créer</a>)"""
)
@ -937,8 +944,8 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
H.append(
f"""<h4><a class="stdlink"
href="{url_for("scolar.partition_editor",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
edit_partition=1)
}">Ajouter une partition</a></h4>"""
)
@ -1310,13 +1317,13 @@ def formsemestre_tableau_modules(
<td class="formsemestre_status_code""><a
href="{moduleimpl_status_url}"
title="{mod_descr}" class="stdlink">{mod.code}</a></td>
<td class="scotext"><a href="{moduleimpl_status_url}" title="{mod_descr}"
<td class="scotext"><a href="{moduleimpl_status_url}" title="{mod_descr}"
class="formsemestre_status_link">{mod.abbrev or mod.titre or ""}</a>
</td>
<td class="formsemestre_status_inscrits">{len(mod_inscrits)}</td>
<td class="resp scotext">
<a class="discretelink" href="{moduleimpl_status_url}" title="{mod_ens}">{
sco_users.user_info(modimpl["responsable_id"])["prenomnom"]
sco_users.user_info(modimpl["responsable_id"])["prenomnom"]
}</a>
</td>
<td>
@ -1457,8 +1464,8 @@ def formsemestre_warning_etuds_sans_note(
"notes.formsemestre_note_etuds_sans_notes",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)}">{"lui" if nb_sans_notes == 1 else "leur"}
<span title="pour ne pas bloquer les autres étudiants, il est souvent préférable
)}">{"lui" if nb_sans_notes == 1 else "leur"}
<span title="pour ne pas bloquer les autres étudiants, il est souvent préférable
que les nouveaux aient des notes provisoires">affecter des notes</a>.
</div>
"""

View File

@ -42,7 +42,7 @@ from app import cache, db, log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre, Identite, Scolog
from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN
from app.models import SHORT_STR_LEN
from app.models.groups import GroupDescr, Partition
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
@ -79,7 +79,9 @@ partitionEditor = ndb.EditableTable(
)
groupEditor = ndb.EditableTable(
"group_descr", "group_id", ("group_id", "partition_id", "group_name", "numero")
"group_descr",
"group_id",
("group_id", "partition_id", "group_name", "numero", "edt_id"),
)
group_list = groupEditor.list
@ -136,7 +138,7 @@ def get_partitions_list(formsemestre_id, with_default=True) -> list[dict]:
partitions = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.*
FROM partition p
WHERE formsemestre_id=%(formsemestre_id)s
WHERE formsemestre_id=%(formsemestre_id)s
ORDER BY numero""",
{"formsemestre_id": formsemestre_id},
)
@ -206,8 +208,10 @@ def get_partition_groups(partition): # OBSOLETE !
)
def get_default_group(formsemestre_id, fix_if_missing=False):
"""Returns group_id for default ('tous') group"""
def get_default_group(formsemestre_id, fix_if_missing=False) -> int:
"""Returns group_id for default ('tous') group
XXX remplacé par formsemestre.get_default_group
"""
r = ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id
FROM group_descr gd, partition p
@ -258,14 +262,14 @@ def get_group_members(group_id, etat=None):
Trié par nom_usuel (ou nom) puis prénom
"""
req = """SELECT i.id as etudid, i.*, a.*, gm.*, ins.etat
FROM identite i, adresse a, group_membership gm,
group_descr gd, partition p, notes_formsemestre_inscription ins
WHERE i.id = gm.etudid
and a.etudid = i.id
and ins.etudid = i.id
and ins.formsemestre_id = p.formsemestre_id
and p.id = gd.partition_id
and gd.id = gm.group_id
FROM identite i, adresse a, group_membership gm,
group_descr gd, partition p, notes_formsemestre_inscription ins
WHERE i.id = gm.etudid
and a.etudid = i.id
and ins.etudid = i.id
and ins.formsemestre_id = p.formsemestre_id
and p.id = gd.partition_id
and gd.id = gm.group_id
and gm.group_id=%(group_id)s
"""
if etat is not None:
@ -286,8 +290,9 @@ def get_group_members(group_id, etat=None):
return r
def get_group_infos(group_id, etat=None): # was _getlisteetud
"""legacy code: used by group_list and trombino"""
def get_group_infos(group_id, etat: str | None = None): # was _getlisteetud
"""legacy code: used by group_list and trombino.
etat: état de l'inscription."""
from app.scodoc import sco_formsemestre
cnx = ndb.GetDBConnexion()
@ -350,12 +355,12 @@ def get_etud_groups(etudid: int, formsemestre_id: int, exclude_default=False):
"""Infos sur groupes de l'etudiant dans ce semestre
[ group + partition_name ]
"""
req = """SELECT p.id AS partition_id, p.*,
req = """SELECT p.id AS partition_id, p.*,
g.id AS group_id, g.numero as group_numero, g.group_name
FROM group_descr g, partition p, group_membership gm
WHERE gm.etudid=%(etudid)s
and gm.group_id = g.id
and g.partition_id = p.id
FROM group_descr g, partition p, group_membership gm
WHERE gm.etudid=%(etudid)s
and gm.group_id = g.id
and g.partition_id = p.id
and p.formsemestre_id = %(formsemestre_id)s
"""
if exclude_default:
@ -393,7 +398,7 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
p.id AS partition_id,
gd.group_name,
gd.id AS group_id
FROM
FROM
notes_formsemestre_inscription i,
partition p,
group_descr gd,
@ -967,8 +972,8 @@ def edit_partition_form(formsemestre_id=None):
for p in partitions:
if p["partition_name"] is not None:
H.append(
f"""<tr><td class="epnav"><a class="stdlink"
href="{url_for("scolar.partition_delete",
f"""<tr><td class="epnav"><a class="stdlink"
href="{url_for("scolar.partition_delete",
scodoc_dept=g.scodoc_dept, partition_id=p["partition_id"])
}">{suppricon}</a>&nbsp;</td><td class="epnav">"""
)
@ -1299,85 +1304,6 @@ def partition_set_name(partition_id, partition_name, redirect=1):
)
def group_set_name(group: GroupDescr, group_name: str, redirect=True):
"""Set group name"""
if not group.partition.formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
if group.group_name is None:
raise ValueError("can't set a name to default group")
destination = url_for(
"scolar.affect_groups",
scodoc_dept=g.scodoc_dept,
partition_id=group.partition_id,
)
if group_name:
group_name = group_name.strip()
if not group_name:
raise ScoValueError("nom de groupe vide !", dest_url=destination)
if not GroupDescr.check_name(group.partition, group_name):
raise ScoValueError(
"Le nom de groupe existe déjà dans la partition", dest_url=destination
)
redirect = int(redirect)
group.group_name = group_name
db.session.add(group)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=group.partition.formsemestre_id)
# redirect to partition edit page:
if redirect:
return flask.redirect(destination)
def group_rename(group_id):
"""Form to rename a group"""
group = GroupDescr.query.get_or_404(group_id)
formsemestre_id = group.partition.formsemestre_id
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if not formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
H = [f"<h2>Renommer un groupe de {group.partition.partition_name or '-'}</h2>"]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("group_id", {"default": group_id, "input_type": "hidden"}),
(
"group_name",
{
"title": "Nouveau nom",
"default": group.group_name,
"size": 12,
"allow_null": False,
"validator": lambda val, _: len(val) < GROUPNAME_STR_LEN,
},
),
),
submitlabel="Renommer",
cancelbutton="Annuler",
)
if tf[0] == 0:
return (
html_sco_header.sco_header()
+ "\n".join(H)
+ "\n"
+ tf[1]
+ html_sco_header.sco_footer()
)
elif tf[0] == -1:
return flask.redirect(
url_for(
"scolar.affect_groups",
scodoc_dept=g.scodoc_dept,
partition_id=group.partition_id,
)
)
else:
# form submission
return group_set_name(group, tf[2]["group_name"])
def groups_auto_repartition(partition: Partition):
"""Réparti les etudiants dans des groupes dans une partition, en respectant le niveau
et la mixité.
@ -1570,7 +1496,7 @@ def do_evaluation_listeetuds_groups(
return [] # no groups, so no students
rg = ["gm.group_id = '%(group_id)s'" % g for g in groups]
rq = """and Isem.etudid = gm.etudid
and gd.partition_id = p.id
and gd.partition_id = p.id
and p.formsemestre_id = Isem.formsemestre_id
"""
r = rq + " AND (" + " or ".join(rg) + " )"
@ -1583,9 +1509,9 @@ def do_evaluation_listeetuds_groups(
"SELECT distinct Im.etudid, Isem.etat FROM "
+ ", ".join(fromtables)
+ """ WHERE Isem.etudid = Im.etudid
and Im.moduleimpl_id = M.id
and Isem.formsemestre_id = M.formsemestre_id
and E.moduleimpl_id = M.id
and Im.moduleimpl_id = M.id
and Isem.formsemestre_id = M.formsemestre_id
and E.moduleimpl_id = M.id
and E.id = %(evaluation_id)s
"""
)
@ -1612,7 +1538,7 @@ def do_evaluation_listegroupes(evaluation_id, include_default=False):
cursor = cnx.cursor()
cursor.execute(
"""SELECT DISTINCT gd.id AS group_id
FROM group_descr gd, group_membership gm, partition p,
FROM group_descr gd, group_membership gm, partition p,
notes_moduleimpl m, notes_evaluation e
WHERE gm.group_id = gd.id
and gd.partition_id = p.id

View File

@ -27,11 +27,15 @@
"""Formulaires gestion des groupes
"""
from flask import render_template
import flask
from flask import flash, g, render_template, request, url_for
from app.models import Partition
from app.models import FormSemestre, GroupDescr, Partition
from app.models import GROUPNAME_STR_LEN
from app.scodoc import html_sco_header
from app.scodoc.sco_exceptions import AccessDenied
import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator
def affect_groups(partition_id):
@ -59,3 +63,73 @@ def affect_groups(partition_id):
),
formsemestre_id=formsemestre.id,
)
def group_rename(group_id):
"""Form to rename a group"""
group: GroupDescr = GroupDescr.query.get_or_404(group_id)
formsemestre_id = group.partition.formsemestre_id
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if not formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
H = [f"<h2>Renommer un groupe de {group.partition.partition_name or '-'}</h2>"]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("group_id", {"default": group_id, "input_type": "hidden"}),
(
"group_name",
{
"title": "Nouveau nom",
"default": group.group_name,
"size": 12,
"allow_null": False,
"validator": lambda val, _: len(val) < GROUPNAME_STR_LEN,
"explanation": "doit être unique dans cette partition"
if group.partition.groups_editable
else "groupes non modifiables dans cette partition",
"enabled": group.partition.groups_editable,
},
),
(
"edt_id",
{
"title": "Id EDT",
"default": group.edt_id or "",
"size": 12,
"allow_null": True,
"explanation": """optionnel : identifiant du groupe dans le logiciel
d'emploi du temps, pour le cas où les noms de groupes ne seraient pas
les mêmes dans ScoDoc et dans l'emploi du temps (si plusieurs ids,
les séparer par des virgules).""",
},
),
),
submitlabel="Enregistrer",
cancelbutton="Annuler",
)
dest_url = url_for(
"scolar.partition_editor",
scodoc_dept=g.scodoc_dept,
formsemestre_id=group.partition.formsemestre_id,
edit_partition=1,
)
if tf[0] == 0:
return (
html_sco_header.sco_header()
+ "\n".join(H)
+ "\n"
+ tf[1]
+ html_sco_header.sco_footer()
)
elif tf[0] == -1:
return flask.redirect(dest_url)
else:
# form submission
# Si la partition n'est pas editable, on ne peut changer que l'edt_id
group.set_edt_id(tf[2]["edt_id"])
if group.partition.groups_editable:
group.set_name(tf[2]["group_name"], dest_url=dest_url)
flash("groupe modifié")
return flask.redirect(dest_url)

View File

@ -31,11 +31,8 @@
# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code)
import collections
import datetime
import urllib
from urllib.parse import parse_qs
import time
from flask import url_for, g, request
@ -45,7 +42,6 @@ from app import db
from app.models import FormSemestre
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_cal
from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
@ -71,18 +67,24 @@ CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
def groups_view(
group_ids=(),
fmt="html",
# Options pour listes:
with_codes=0,
etat=None,
with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail)
with_archives=0, # ajoute colonne avec noms fichiers archivés
with_paiement=0,
with_archives=0,
with_annotations=0,
with_bourse=0,
formsemestre_id=None, # utilise si aucun groupe selectionné
formsemestre_id=None,
):
"""Affichage des étudiants des groupes indiqués
group_ids: liste de group_id
fmt: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf
Options pour listes:
with_paiement: si vrai, ajoute colonnes infos paiement droits
et finalisation inscription (lent car interrogation portail)
with_archives: ajoute colonne avec noms fichiers archivés
formsemestre_id est utilisé si aucun groupe selectionné pour construire la liste des groupes.
"""
# Informations sur les groupes à afficher:
groups_infos = DisplayedGroupsInfos(
@ -104,117 +106,127 @@ def groups_view(
with_bourse=with_bourse,
)
H = [
html_sco_header.sco_header(
javascripts=JAVASCRIPTS,
cssstyles=CSSSTYLES,
init_qtip=True,
)
]
# Menu choix groupe
H.append("""<div id="group-tabs">""")
H.append(form_groups_choice(groups_infos, submit_on_change=True))
# Note: le formulaire est soumis a chaque modif des groupes
# on pourrait faire comme pour le form de saisie des notes. Il faudrait pour cela:
# - charger tous les etudiants au debut, quels que soient les groupes selectionnés
# - ajouter du JS pour modifier les liens (arguments group_ids) quand le menu change
# Tabs
# H.extend( ("""<span>toto</span><ul id="toto"><li>item 1</li><li>item 2</li></ul>""",) )
H.extend(
(
"""<ul class="nav nav-tabs">
<li class="active"><a href="#tab-listes" data-toggle="tab">Listes</a></li>
<li><a href="#tab-photos" data-toggle="tab">Photos</a></li>
<li><a href="#tab-abs" data-toggle="tab">Absences et feuilles...</a></li>
</ul>
return f"""
{ html_sco_header.sco_header(
javascripts=JAVASCRIPTS,
cssstyles=CSSSTYLES,
init_qtip=True,
)
}
<div id="group-tabs">
<!-- Menu choix groupe -->
{form_groups_choice(groups_infos, submit_on_change=True)}
<ul class="nav nav-tabs">
<li class="active"><a href="#tab-listes" data-toggle="tab">Listes</a></li>
<li><a href="#tab-photos" data-toggle="tab">Photos</a></li>
<li><a href="#tab-abs" data-toggle="tab">Absences et feuilles...</a></li>
</ul>
</div>
<!-- Tab panes -->
<div class="tab-content">
<div class="tab-pane active" id="tab-listes">
""",
groups_table(
groups_infos=groups_infos,
fmt=fmt,
with_codes=with_codes,
etat=etat,
with_paiement=with_paiement,
with_archives=with_archives,
with_annotations=with_annotations,
with_bourse=with_bourse,
),
"</div>",
"""<div class="tab-pane" id="tab-photos">""",
tab_photos_html(groups_infos, etat=etat),
#'<p>hello</p>',
"</div>",
'<div class="tab-pane" id="tab-abs">',
tab_absences_html(groups_infos, etat=etat),
"</div>",
)
)
H.append(html_sco_header.sco_footer())
return "\n".join(H)
<div class="tab-pane active" id="tab-listes">
{
groups_table(
groups_infos=groups_infos,
fmt=fmt,
with_codes=with_codes,
etat=etat,
with_paiement=with_paiement,
with_archives=with_archives,
with_annotations=with_annotations,
with_bourse=with_bourse,
)
}
</div>
<div class="tab-pane" id="tab-photos">
{ tab_photos_html(groups_infos, etat=etat) }
</div>
<div class="tab-pane" id="tab-abs">
{ tab_absences_html(groups_infos, etat=etat) }
</div>
</div>
{ html_sco_header.sco_footer() }
"""
def form_groups_choice(groups_infos, with_selectall_butt=False, submit_on_change=False):
def form_groups_choice(
groups_infos,
with_selectall_butt=False,
with_deselect_butt=False,
submit_on_change=False,
default_deselect_others=True,
):
"""form pour selection groupes
group_ids est la liste des groupes actuellement sélectionnés
et doit comporter au moins un élément, sauf si formsemestre_id est spécifié.
(utilisé pour retrouver le semestre et proposer la liste des autres groupes)
Si submit_on_change, ajoute une classe "submit_on_change" qui est utilisee en JS
Si submit_on_change, soumet (recharge la page) à chaque modif.
Si default_deselect_others, désélectionne le groupe "Tous" quand on sélectionne un autre groupe.
Ces deux options ajoutent des classes utilisées en JS pour la gestion du formulaire.
"""
default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id)
H = [
"""<form id="group_selector" method="get">
<input type="hidden" name="formsemestre_id" id="formsemestre_id" value="%s"/>
<input type="hidden" name="default_group_id" id="default_group_id" value="%s"/>
f"""
<form id="group_selector" method="get">
<input type="hidden" name="formsemestre_id" id="formsemestre_id"
value="{groups_infos.formsemestre_id}"/>
<input type="hidden" name="default_group_id" id="default_group_id"
value="{default_group_id}"/>
Groupes:
{
menu_groups_choice(
groups_infos,
submit_on_change=submit_on_change,
default_deselect_others=default_deselect_others,
)
}
"""
% (groups_infos.formsemestre_id, default_group_id)
]
H.append(menu_groups_choice(groups_infos, submit_on_change=submit_on_change))
if with_selectall_butt:
H.append(
"""<input type="button" value="sélectionner tous" onmousedown="select_tous();"/>"""
"""<input type="button" value="sélectionner tous" onmousedown="select_groupe_tous();"/>"""
)
if with_deselect_butt:
H.append(
"""<input type="button" value="ne pas filtrer" onmousedown="remove_group_filter();"/>"""
)
H.append("</form>")
return "\n".join(H)
def menu_groups_choice(groups_infos, submit_on_change=False):
def menu_groups_choice(
groups_infos, submit_on_change=False, default_deselect_others=True
):
"""menu pour selection groupes
group_ids est la liste des groupes actuellement sélectionnés
et doit comporter au moins un élément, sauf si formsemestre_id est spécifié.
(utilisé pour retrouver le semestre et proposer la liste des autres groupes)
"""
default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id)
if submit_on_change:
klass = "submit_on_change"
else:
klass = ""
H = [
"""<select name="group_ids" id="group_ids_sel" class="multiselect %s" multiple="multiple">
"""
% (klass,)
]
n_members = len(sco_groups.get_group_members(default_group_id))
if default_group_id in groups_infos.group_ids:
selected = "selected"
else:
selected = ""
H.append(
'<option class="default_group" value="%s" %s>%s (%s)</option>'
% (default_group_id, selected, "Tous", n_members)
)
H = [
f"""<select name="group_ids" id="group_ids_sel"
class="multiselect
{'submit_on_change' if submit_on_change else ''}
{'default_deselect_others' if default_deselect_others else ''}
"
multiple="multiple">
<option class="default_group"
value="{default_group_id}"
{'selected' if default_group_id in groups_infos.group_ids else ''}
>Tous ({n_members})</option>
"""
]
for partition in groups_infos.partitions:
H.append('<optgroup label="%s">' % partition["partition_name"])
@ -294,14 +306,19 @@ class DisplayedGroupsInfos:
.formsemestre_id : semestre "principal" (en fait celui du 1er groupe de la liste)
.members
.groups_titles
etat: filtrage selon l'état de l'inscription
select_all_when_unspecified : sélectionne le groupe "tous" si aucun groupe n'est indiqué.
empty_list_select_all: si vrai (défaut) on sélectionne le groupe tous si aucun groupe indiqué.
"""
def __init__(
self,
group_ids=(), # groupes specifies dans l'URL, ou un seul int
formsemestre_id=None,
etat=None,
formsemestre_id: int | None = None,
etat: str | None = None,
select_all_when_unspecified=False,
empty_list_select_all=True,
moduleimpl_id=None, # used to find formsemestre when unspecified
):
if isinstance(group_ids, int):
@ -322,21 +339,26 @@ class DisplayedGroupsInfos:
if not group_ids: # appel sans groupe (eg page accueil)
if not formsemestre_id:
raise Exception("missing parameter formsemestre_id or group_ids")
if select_all_when_unspecified:
group_ids = [
sco_groups.get_default_group(formsemestre_id, fix_if_missing=True)
]
else:
# selectionne le premier groupe trouvé, s'il y en a un
partition = sco_groups.get_partitions_list(
formsemestre_id, with_default=True
)[0]
groups = sco_groups.get_partition_groups(partition)
if groups:
group_ids = [groups[0]["group_id"]]
raise ValueError("missing parameter formsemestre_id or group_ids")
if empty_list_select_all:
if select_all_when_unspecified:
group_ids = [
sco_groups.get_default_group(
formsemestre_id, fix_if_missing=True
)
]
else:
group_ids = [sco_groups.get_default_group(formsemestre_id)]
# selectionne le premier groupe trouvé, s'il y en a un
partition = sco_groups.get_partitions_list(
formsemestre_id, with_default=True
)[0]
groups = sco_groups.get_partition_groups(partition)
if groups:
group_ids = [groups[0]["group_id"]]
else:
group_ids = [sco_groups.get_default_group(formsemestre_id)]
else:
group_ids = []
gq = []
for group_id in group_ids:
@ -380,7 +402,8 @@ class DisplayedGroupsInfos:
if not self.formsemestre: # aucun groupe selectionne
self.formsemestre = sco_formsemestre.get_formsemestre(formsemestre_id)
if formsemestre_id not in self.sems:
self.sems[formsemestre_id] = self.formsemestre
self.sortuniq()
if len(self.sems) > 1:
@ -635,9 +658,10 @@ def groups_table(
else:
htitle = "Aucun étudiant !"
H = [
'<div class="tab-content"><form>' '<h3 class="formsemestre"><span>',
htitle,
"</span>",
f"""<div class="tab-content">
<form>
<span style="font-weight:bold; font-size:120%;">{htitle}</span>
"""
]
if groups_infos.members:
Of = []
@ -679,7 +703,7 @@ def groups_table(
""",
]
)
H.append("</h3></form>")
H.append("</div></form>")
if groups_infos.members:
H.extend(
[
@ -722,7 +746,7 @@ def groups_table(
H.append("</ul>")
return "".join(H) + "</div>"
return "".join(H)
elif (
fmt == "pdf"

View File

@ -163,7 +163,7 @@ def _ue_coefs_html(coefs_lst) -> str:
"""
+ "\n".join(
[
f"""<div style="--coef:{coef};
f"""<div style="--coef:{coef};
{'background-color: ' + ue.color + ';' if ue.color else ''}
"><div>{coef}</div>{ue.acronyme}</div>"""
for ue, coef in coefs_lst
@ -331,15 +331,16 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
>Saisie Absences journée</a></span>
"""
)
year, week, day = datetime.date.today().isocalendar()
semaine: str = f"{year}-W{week}"
H.append(
f"""
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
url_for(
"assiduites.signal_assiduites_group",
"assiduites.signal_assiduites_diff",
scodoc_dept=g.scodoc_dept,
group_ids=group_id,
jour=datetime.date.today().isoformat(),
semaine=semaine,
formsemestre_id=formsemestre.id,
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
)}"
@ -363,8 +364,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
if formsemestre_has_decisions(formsemestre_id):
H.append(
"""<ul class="tf-msg">
<li class="tf-msg warning">Décisions de jury saisies: seul le responsable du
semestre peut saisir des notes (il devra modifier les décisions de jury).
<li class="tf-msg warning">Décisions de jury saisies: seul le ou la responsable du
semestre peut saisir des notes (elle devra modifier les décisions de jury).
</li>
</ul>"""
)
@ -419,7 +420,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
if nb_evaluations > 0:
top_table_links += f"""
<a class="stdlink" style="margin-left:2em;" href="{
url_for("notes.moduleimpl_evaluation_renumber",
url_for("notes.moduleimpl_evaluation_renumber",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}">Trier par date</a>
"""

View File

@ -35,6 +35,8 @@ import xml
import xml.sax.saxutils
import xml.dom.minidom
from flask import flash
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc import sco_cache
@ -67,7 +69,7 @@ class PortalInterface(object):
portal_url += "/"
if self.first_time:
if portal_url:
log("Portal URL=%s" % portal_url)
log(f"Portal URL={portal_url}")
else:
log("Portal not configured")
self.first_time = False
@ -338,25 +340,27 @@ def get_etud_apogee(code_nip):
if not d:
return None
if len(d) > 1:
raise ValueError("invalid XML response from Etudiant Web Service\n%s" % doc)
log(f"get_etud_apogee({code_nip}): {len(d)} etudiants !\n{doc}")
flash("Attention: plusieurs étudiants inscrits avec le NIP {code_nip}")
# dans ce cas, renvoie le premier étudiant
return d[0]
def get_default_etapes():
"""Liste par défaut, lue du fichier de config"""
filename = scu.SCO_TOOLS_DIR + "/default-etapes.txt"
log("get_default_etapes: reading %s" % filename)
f = open(filename)
log(f"get_default_etapes: reading {filename}")
etapes = {}
for line in f.readlines():
line = line.strip()
if line and line[0] != "#":
dept, code, intitule = [x.strip() for x in line.split(":")]
if dept and code:
if dept in etapes:
etapes[dept][code] = intitule
else:
etapes[dept] = {code: intitule}
with open(filename, encoding=scu.SCO_ENCODING) as f:
for line in f.readlines():
line = line.strip()
if line and line[0] != "#":
dept, code, intitule = [x.strip() for x in line.split(":")]
if dept and code:
if dept in etapes:
etapes[dept][code] = intitule
else:
etapes[dept] = {code: intitule}
return etapes
@ -369,7 +373,7 @@ def _parse_etapes_from_xml(doc):
dom = xml.dom.minidom.parseString(doc)
infos = {}
if dom.childNodes[0].nodeName != "etapes":
raise ValueError
raise ValueError("élément 'etapes' attendu")
if xml_etapes_by_dept:
# Ancien format XML avec des sections par departement:
for d in dom.childNodes[0].childNodes:
@ -394,8 +398,7 @@ def get_etapes_apogee():
if etapes_url:
portal_timeout = sco_preferences.get_preference("portal_timeout")
log(
"get_etapes_apogee: requesting '%s' with timeout=%s"
% (etapes_url, portal_timeout)
f"""get_etapes_apogee: requesting '{etapes_url}' with timeout={portal_timeout}"""
)
doc = scu.query_portal(etapes_url, timeout=portal_timeout)
try:
@ -403,15 +406,17 @@ def get_etapes_apogee():
# cache le resultat (utile si le portail repond de façon intermitente)
if infos:
log("get_etapes_apogee: caching result")
with open(SCO_CACHE_ETAPE_FILENAME, "w") as f:
with open(
SCO_CACHE_ETAPE_FILENAME, "w", encoding=scu.SCO_ENCODING
) as f:
f.write(doc)
except:
log("invalid XML response from getEtapes Web Service\n%s" % etapes_url)
# Avons nous la copie d'une réponse récente ?
log(f"invalid XML response from getEtapes Web Service\n{etapes_url}")
# Avons-nous la copie d'une réponse récente ?
try:
doc = open(SCO_CACHE_ETAPE_FILENAME).read()
doc = open(SCO_CACHE_ETAPE_FILENAME, encoding=scu.SCO_ENCODING).read()
infos = _parse_etapes_from_xml(doc)
log("using last saved version from " + SCO_CACHE_ETAPE_FILENAME)
log(f"using last saved version from {SCO_CACHE_ETAPE_FILENAME}")
except:
infos = {}
else:

View File

@ -2033,24 +2033,13 @@ class BasePreferences:
"category": "edt",
},
),
(
"edt_groups2scodoc",
{
"input_type": "textarea",
"initvalue": "",
"title": "Noms Groupes",
"explanation": "Transcodage: nom de groupe EDT ; non de groupe ScoDoc (sur plusieurs lignes)",
"rows": 8,
"cols": 16,
"category": "edt",
},
),
# Divers
(
"ImputationDept",
{
"title": "Département d'imputation",
"initvalue": "",
"explanation": "préfixe id de session (optionnel, remplace nom département)",
"explanation": "optionnel: préfixe id de formsemestre (par défaut, le nom du département). Pour usages API avancés.",
"size": 10,
"category": "edt",
},

View File

@ -294,7 +294,7 @@ def do_evaluation_upload_xls():
len(absents)} absents, {nb_suppress} note supprimées)
</p>"""
if etudids_with_decisions:
msg += """<p class="warning">Important: il y avait déjà des décisions de jury
msg += """<p class="warning">Important: il y avait déjà des décisions de jury
enregistrées, qui sont peut-être à revoir suite à cette modification !</p>
"""
return 1, msg
@ -642,7 +642,7 @@ def notes_add(
)
cursor.execute(
"""DELETE FROM notes_notes
WHERE etudid = %(etudid)s
WHERE etudid = %(etudid)s
AND evaluation_id = %(evaluation_id)s
""",
args,
@ -660,7 +660,7 @@ def notes_add(
nb_suppress += 1
if changed:
etudids_changed.append(etudid)
if res.etud_has_decision(etudid):
if res.etud_has_decision(etudid, include_rcues=False):
etudids_with_decision.append(etudid)
except Exception as exc:
log("*** exception in notes_add")
@ -691,7 +691,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
avez l'autorisation d'effectuer cette opération)
</p>
<p><a class="stdlink" href="{
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id)
}">Continuer</a></p>
"""
@ -740,7 +740,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
</ul>
</div>
<form>
<input type="hidden" name="evaluation_id" id="formnotes_evaluation_id"
<input type="hidden" name="evaluation_id" id="formnotes_evaluation_id"
value="{evaluation_id}"/>
</form>
"""
@ -1168,7 +1168,11 @@ def _form_saisie_notes(
return '<div class="ue_warning"><span>Aucun étudiant sélectionné !</span></div>'
# Décisions de jury existantes ?
decisions_jury = {etudid: res.etud_has_decision(etudid) for etudid in etudids}
# en BUT on ne considère pas les RCUEs car ils peuvenut avoir été validés depuis
# d'autres semestres (les validations de RCUE n'indiquent pas si elles sont "externes")
decisions_jury = {
etudid: res.etud_has_decision(etudid, include_rcues=False) for etudid in etudids
}
# Nb de décisions de jury (pour les inscrits à l'évaluation):
nb_decisions = sum(decisions_jury.values())
@ -1308,7 +1312,7 @@ def _form_saisie_notes(
)
H.append(tf.getform()) # check and init
H.append(
f"""<a href="{url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
f"""<a href="{url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl.id)
}" class="btn btn-primary">Terminer</a>
"""

View File

@ -53,7 +53,6 @@ import requests
from pytz import timezone
import dateutil.parser as dtparser
import flask
from flask import g, request, Response
@ -230,9 +229,9 @@ def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or No
"""
try:
date: datetime.datetime = dtparser.isoparse(date)
date: datetime.datetime = datetime.datetime.fromisoformat(date)
return date if convert else True
except (dtparser.ParserError, ValueError, TypeError):
except (ValueError, TypeError):
return None if convert else False
@ -765,13 +764,23 @@ FORBIDDEN_CHARS_EXP = re.compile(r"[*\|~\(\)\\]")
ALPHANUM_EXP = re.compile(r"^[\w-]+$", re.UNICODE)
def is_valid_code_nip(s):
def is_valid_code_nip(s: str) -> bool:
"""True si s peut être un code NIP: au moins 6 chiffres décimaux"""
if not s:
return False
return re.match(r"^[0-9]{6,32}$", s)
def split_id(ident: str) -> list[str]:
"""ident est une chaine 'X, Y, Z'
Renvoie ['X','Y', 'Z']
"""
if ident:
ident = ident.strip()
return [x.strip() for x in ident.strip().split(",")] if ident else []
return []
def strnone(s):
"convert s to string, '' if s is false"
if s:
@ -1467,3 +1476,14 @@ def is_assiduites_module_forced(
except (TypeError, ValueError):
retour = sco_preferences.get_preference("forcer_module", dept_id=dept_id)
return retour
def get_assiduites_time_config(config_type: str) -> str:
from app.models import ScoDocSiteConfig
match config_type:
case "matin":
return ScoDocSiteConfig.get("assi_morning_time", "08:00:00")
case "aprem":
return ScoDocSiteConfig.get("assi_afternoon_time", "18:00:00")
case "pivot":
return ScoDocSiteConfig.get("assi_lunch_time", "13:00:00")

View File

@ -1,3 +1,35 @@
:root {
--color-present: #6bdb83;
--color-absent: #e62a11;
--color-absent-clair: #F25D4A;
--color-retard: #f0c865;
--color-justi: #7059FF;
--color-justi-clair: #6885E3;
--color-justi-invalide: #a84476;
--color-nonwork: #badfff;
--color-absent-justi: #e65ab7;
--color-retard-justi: #ffef7a;
--color-error: #e62a11;
--color-warning: #eec660;
--color-information: #658ef0;
--color-def: #d61616;
--color-conflit: #ff00009c;
--color-bg-def: #c8c8c8;
--color-primary: #7059FF;
--color-secondary: #6f9fff;
--color-defaut: #FFF;
--color-defaut-dark: #444;
--color-default-text: #1F1F1F;
--motif-justi: repeating-linear-gradient(135deg, transparent, transparent 4px, var(--color-justi) 4px, var(--color-justi) 8px);
--motif-justi-invalide: repeating-linear-gradient(-135deg, transparent, transparent 4px, var(--color-justi-invalide) 4px, var(--color-justi-invalide) 8px);
}
* {
box-sizing: border-box;
}
@ -36,10 +68,10 @@
.infos {
position: relative;
width: fit-content;
display: flex;
justify-content: space-evenly;
justify-content: start;
align-content: center;
gap: 10px;
}
#datestr {
@ -48,7 +80,7 @@
border: 1px #444 solid;
border-radius: 5px;
padding: 5px;
min-width: 100px;
min-width: 250px;
display: inline-block;
min-height: 20px;
}
@ -87,7 +119,7 @@
}
.ui-slider-range.ui-widget-header.ui-corner-all {
background-color: #F9C768;
background-color: var(--color-warning);
background-image: none;
opacity: 0.50;
visibility: visible;
@ -122,7 +154,7 @@
.etud_row.def,
.etud_row.dem {
background-color: #c8c8c8;
background-color: var(--color-bg-def);
}
/* --- Index --- */
@ -149,7 +181,7 @@
.tr.def .td.sticky span::after {
display: block;
content: " (Déf.)";
color: #d61616;
color: var(--color-def);
margin-left: 2px;
}
@ -157,7 +189,7 @@
.tr.dem .td.sticky span::after {
display: block;
content: " (Dém.)";
color: #d61616;
color: var(--color-def);
margin-left: 2px;
}
@ -213,32 +245,36 @@
}
.etud_row.conflit {
background-color: #ff0000c2;
background-color: var(--color-conflit);
}
.etud_row .assiduites_bar .absent,
.demo.absent {
background-color: #F1A69C !important;
background-color: var(--color-absent) !important;
}
.etud_row .assiduites_bar .present,
.demo.present {
background-color: #9CF1AF !important;
background-color: var(--color-present) !important;
}
.etud_row .assiduites_bar .retard,
.demo.retard {
background-color: #F1D99C !important;
background-color: var(--color-retard) !important;
}
.demo.nonwork {
background-color: var(--color-nonwork) !important;
}
.etud_row .assiduites_bar .justified,
.demo.justified {
background-image: repeating-linear-gradient(135deg, transparent, transparent 4px, #7059FF 4px, #7059FF 8px);
background-image: var(--motif-justi);
}
.etud_row .assiduites_bar .invalid_justified,
.demo.invalid_justified {
background-image: repeating-linear-gradient(225deg, transparent, transparent 4px, #d61616 4px, #d61616 8px);
background-image: var(--motif-justi-invalide);
}
@ -273,27 +309,35 @@
height: 35px;
background-position: center;
background-size: cover;
border-radius: 5px;
border: 1px solid var(--color-defaut-dark);
}
.rbtn.present::before {
background-image: url(../icons/present.svg);
background-color: var(--color-present);
}
.rbtn.absent::before {
background-color: var(--color-absent);
background-image: url(../icons/absent.svg);
}
.rbtn.aucun::before {
background-image: url(../icons/aucun.svg);
background-color: var(--color-defaut-dark);
}
.rbtn.retard::before {
background-color: var(--color-retard);
background-image: url(../icons/retard.svg);
}
.rbtn:checked:before {
outline: 5px solid #7059FF;
outline: 5px solid var(--color-primary);
border-radius: 50%;
}
@ -486,7 +530,7 @@
.loader {
border: 6px solid #f3f3f3;
border-radius: 50%;
border-top: 6px solid #3498db;
border-top: 6px solid var(--color-primary);
width: 60px;
height: 60px;
position: absolute;
@ -532,7 +576,7 @@
}
.rouge {
color: crimson;
color: var(--color-error);
}
.legende {
@ -588,7 +632,7 @@
#forcemodule {
border-radius: 8px;
background: crimson;
background: var(--color-error);
max-width: fit-content;
padding: 5px;
color: white;

67
app/static/css/edt.css Normal file
View File

@ -0,0 +1,67 @@
.toastui-calendar-template-time {
padding: 4px;
word-break: break-all;
white-space: normal !important;
align-items: normal !important;
font-size: 12pt;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.module-edt {
display: inline;
}
.mod-code {
font-weight: bold;
color: rgb(21, 21, 116);
font-size: 110%;
}
.group-name {
color: rgb(25, 113, 25);
display: inline;
}
.group-edt {
color: red;
background-color: yellow;
}
#renderRange {
margin-left: 16px;
}
.toastui-calendar-timegrid {
height: 100% !important;
min-height: auto !important;
}
.toastui-calendar-time {
height: calc(100% - 44px) !important;
}
.toastui-calendar-week-view-day-names,
.toastui-calendar-time {
overflow: hidden !important;
}
.btn {
border-radius: 25px;
border-color: #ddd;
}
.btn:hover {
border: solid 1px #bbb;
background-color: #fff;
}
.btn:active {
background-color: #f9f9f9;
border: solid 1px #bbb;
outline: none;
}
.btn:disabled {
background-color: #f9f9f9;
border: solid 1px #ddd;
color: #bbb;
}
.btn:focus:active,
.btn:focus,
.btn:active {
outline: none;
}

View File

@ -91,6 +91,10 @@ body:not(.editionActivated) .editing {
.nonEditable .editing {
display: none;
}
.nonEditable .editing.rename {
display: inline;
}
.editionActivated #zoneChoix,
.editionActivated #zoneGroupes {
@ -302,6 +306,10 @@ body.editionActivated .filtres>div>div>div>div {
display: none;
}
#zonePartitions span.editing a {
text-decoration: none;
}
.editionActivated #zonePartitions .filtres .config {
display: block;
}
@ -598,4 +606,4 @@ h3 {
#zoneGroupes .groupe[data-idgroupe=aucun]>div:nth-child(1) {
color: red;
}
}

View File

@ -39,6 +39,41 @@ h3 {
font-weight: bold;
}
/* customization of multiselect style */
.multiselect-container.dropdown-menu {
background-color: #e9e9e9;
}
.multiselect-container label.form-check-label {
font-weight: normal;
margin-left: 8px;
}
button.multiselect-option {
width: 100%;
text-align: left;
border: none;
}
.multiselect-container button.multiselect-option span.form-check {
padding-left: 2px;
}
.multiselect-container span.multiselect-group {
font-weight: bold;
}
.multiselect-container
.multiselect-all.active:not(.multiselect-active-item-fallback),
.multiselect-container
.multiselect-all:not(.multiselect-active-item-fallback):active,
.multiselect-container
.multiselect-group.active:not(.multiselect-active-item-fallback),
.multiselect-container
.multiselect-group:not(.multiselect-active-item-fallback):active,
.multiselect-container
.multiselect-option.active:not(.multiselect-active-item-fallback),
.multiselect-container
.multiselect-option:not(.multiselect-active-item-fallback):active {
background-color: #e9e9e9;
}
/* Legacy ScoDoc pages */
div#gtrcontent {
margin-bottom: 16px;
}
@ -61,7 +96,8 @@ div#gtrcontent {
}
.scotext {
font-family: TimesNewRoman, "Times New Roman", Times, Baskerville, Georgia, serif;
font-family: TimesNewRoman, "Times New Roman", Times, Baskerville, Georgia,
serif;
}
.sco-hidden {
@ -176,7 +212,7 @@ tr.bandeaugtr {
text-decoration: underline;
}
.navbar-default .navbar-nav>li.logout a {
.navbar-default .navbar-nav > li.logout a {
color: rgb(255, 0, 0);
}
@ -212,7 +248,6 @@ div.about-logo {
padding-top: 10px;
}
div.head_message {
margin-top: 12px;
margin-bottom: 8px;
@ -289,7 +324,6 @@ p.footer {
border-top: 1px solid rgb(60, 60, 60);
}
/* ---- (left) SIDEBAR ----- */
div.sidebar {
@ -403,7 +437,6 @@ div.table_etud_in_dept table.gt_table {
font-size: medium;
}
.etud-insidebar ul {
padding-left: 1.5em;
margin-left: 0;
@ -571,7 +604,7 @@ table.semlist tbody tr td.modalite {
.sco_modified {
font-weight: bold;
color: indigo
color: indigo;
}
/***************************/
@ -586,7 +619,7 @@ table.semlist tbody tr td.modalite {
border-radius: 0 0 10px 10px;
background: #ec7068;
background: #90c;
color: #FFF;
color: #fff;
font-size: 24px;
animation: message 3s;
transform: translate(-50%, 0);
@ -594,15 +627,14 @@ table.semlist tbody tr td.modalite {
@keyframes message {
20% {
transform: translate(-50%, 100%)
transform: translate(-50%, 100%);
}
80% {
transform: translate(-50%, 100%)
transform: translate(-50%, 100%);
}
}
div#gtrcontent table.semlist tbody tr.css_S-1 td {
background-color: rgb(251, 250, 216);
}
@ -643,7 +675,8 @@ div.news {
border-radius: 8px;
}
div.news a, div.news a.stdlink {
div.news a,
div.news a.stdlink {
color: black;
text-decoration: none;
}
@ -672,7 +705,6 @@ span.newstext {
font-style: normal;
}
span.gt_export_icons {
margin-left: 1.5em;
}
@ -683,7 +715,7 @@ div.scoinfos {
margin-bottom: 0px;
padding: 2px;
padding-bottom: 0px;
background-color: #F4F4B2;
background-color: #f4f4b2;
}
/* ----- fiches etudiants ------ */
@ -752,17 +784,17 @@ div.etudarchive ul {
div.etudarchive ul li {
background-image: url(/ScoDoc/static/icons/bullet_arrow.png);
background-repeat: no-repeat;
background-position: 0 .4em;
padding-left: .6em;
background-position: 0 0.4em;
padding-left: 0.6em;
}
div.etudarchive ul li.addetudarchive {
background-image: url(/ScoDoc/static/icons/bullet_plus.png);
padding-left: 1.2em
padding-left: 1.2em;
}
span.etudarchive_descr {
margin-right: .4em;
margin-right: 0.4em;
}
span.deletudarchive {
@ -871,7 +903,6 @@ div.ficheinscriptions {
color: red;
}
td.photocell {
padding-left: 32px;
}
@ -989,7 +1020,6 @@ span.linktitresem a:visited {
color: red;
}
a.stdlink,
a.stdlink:visited {
color: blue;
@ -1070,7 +1100,6 @@ span.trombi_box a img {
margin-bottom: 0px;
}
/* markup non semantique pour les cas simples */
.fontred {
@ -1126,7 +1155,8 @@ a.discretelink:hover {
text-align: center;
}
.expl, .help {
.expl,
.help {
max-width: var(--sco-content-max-width);
}
.help {
@ -1138,7 +1168,8 @@ a.discretelink:hover {
color: red;
}
div.sco_box, div.sco_help {
div.sco_box,
div.sco_help {
margin-top: 12px;
margin-bottom: 4px;
margin-left: 0px;
@ -1208,7 +1239,7 @@ span.wtf-field ul.errors li {
display: inline-block;
}
.configuration_logo details>*:not(summary) {
.configuration_logo details > *:not(summary) {
margin-left: 32px;
}
@ -1327,7 +1358,7 @@ table.gt_table tr.etuddem td a {
table.gt_table tr.etuddem td.etudinfo:first-child::after {
color: red;
content: " (dém.)";
}
}
td.etudabs,
td.etudabs a.discretelink,
@ -1349,7 +1380,7 @@ table.notes_evaluation th.eval_incomplete {
font-size: 80%;
}
table.notes_evaluation td.eval_incomplete>a {
table.notes_evaluation td.eval_incomplete > a {
font-size: 80%;
color: rgb(166, 50, 159);
}
@ -1369,7 +1400,6 @@ table.notes_evaluation td.exc a {
color: rgb(0, 131, 0);
}
table.notes_evaluation tr td a.discretelink:hover {
text-decoration: none;
}
@ -1418,7 +1448,7 @@ div.jury_footer {
justify-content: space-evenly;
}
div.jury_footer>span {
div.jury_footer > span {
border: 2px solid rgb(90, 90, 90);
border-radius: 4px;
padding: 4px;
@ -1444,7 +1474,6 @@ div.jury_footer>span {
color: red;
}
span.eval_info {
font-style: italic;
}
@ -1592,7 +1621,7 @@ formsemestre_page_title .lock img {
}
#formnotes td.tf-ro-fieldlabel:after {
content: '';
content: "";
}
#formnotes .tf-ro-field.formnote_bareme {
@ -1639,7 +1668,7 @@ formsemestre_page_title .lock img {
margin-left: -1px;
}
#sco_menu>li {
#sco_menu > li {
float: left;
width: auto;
/* 120px !important; */
@ -1648,20 +1677,20 @@ formsemestre_page_title .lock img {
text-transform: uppercase;
}
#sco_menu>li li {
#sco_menu > li li {
text-transform: none;
font-size: 14px;
font-family: Arial, Helvetica, sans-serif;
}
#sco_menu>li>a {
#sco_menu > li > a {
font-weight: bold !important;
padding-left: 15px;
padding-right: 15px;
}
#sco_menu>li>a.ui-menu-item,
#sco_menu>li>a.ui-menu-item:visited {
#sco_menu > li > a.ui-menu-item,
#sco_menu > li > a.ui-menu-item:visited {
text-decoration: none;
}
@ -1669,14 +1698,14 @@ formsemestre_page_title .lock img {
width: 200px;
}
.sco_dropdown_menu>li {
.sco_dropdown_menu > li {
width: auto;
/* 120px !important; */
font-size: 12px;
font-family: Arial, Helvetica, sans-serif;
}
.menu-etudiant>li {
.menu-etudiant > li {
width: 200px !important;
}
@ -1731,7 +1760,7 @@ tr.formsemestre_status {
}
tr.formsemestre_status_green {
background-color: #EFF7F2;
background-color: #eff7f2;
}
tr.formsemestre_status_ue {
@ -1814,7 +1843,6 @@ span.mod_coef_indicator_zero {
border: 1px solid rgb(156, 156, 156);
}
span.status_ue_acro {
font-weight: bold;
}
@ -1851,7 +1879,7 @@ ul.ue_inscr_list li.etud {
}
.sem-groups-abs {
background-color: rgb(137,137,137);
background-color: rgb(137, 137, 137);
border-radius: 16px;
padding: 16px;
width: fit-content;
@ -1867,7 +1895,7 @@ ul.ue_inscr_list li.etud {
font-size: 110%;
}
.sem-groups-partition {
background-color: rgb(213,203,183);
background-color: rgb(213, 203, 183);
border-radius: 12px;
margin-bottom: 8px;
padding: 12px;
@ -1875,7 +1903,8 @@ ul.ue_inscr_list li.etud {
grid-template-columns: 240px auto;
}
.sem-groups-list, .sem-groups-assi {
.sem-groups-list,
.sem-groups-assi {
background-color: white;
border-radius: 6px;
margin: 4px;
@ -1936,7 +1965,7 @@ div#modimpl_coefs {
font-size: 60%;
}
.coefs_histo>div {
.coefs_histo > div {
--height: calc(32px * var(--coef) / max(var(--max), 1));
height: var(--height);
padding: var(--height) 4px 0 4px;
@ -1944,7 +1973,7 @@ div#modimpl_coefs {
box-sizing: border-box;
}
.coefs_histo>div:nth-child(odd) {
.coefs_histo > div:nth-child(odd) {
background-color: #9c0;
}
@ -1963,7 +1992,7 @@ div.evaluation_titre {
margin-left: 4px;
}
.evaluation_poids>div {
.evaluation_poids > div {
display: inline-flex;
height: 12px;
width: 12px;
@ -1974,7 +2003,7 @@ div.evaluation_titre {
justify-content: center;
}
.evaluation_poids>div>div {
.evaluation_poids > div > div {
height: var(--size);
width: var(--size);
background: #09c;
@ -2028,7 +2057,6 @@ span.mievr_rattr {
margin-left: 2em;
margin-top: 1px;
margin-bottom: 2px;
;
border: 1px solid red;
padding: 1px 3px 1px 3px;
}
@ -2054,15 +2082,16 @@ tr.mievr td {
background-color: white;
}
tr.mievr.non_visible_inter td, tr.mievr.non_visible_inter th {
tr.mievr.non_visible_inter td,
tr.mievr.non_visible_inter th {
/* background-color: #d2cdc5; */
background: repeating-linear-gradient(
45deg,
#f0f0f0,
#f0f0f0 10px,
#e0e0e0 10px,
#e0e0e0 20px
);
45deg,
#f0f0f0,
#f0f0f0 10px,
#e0e0e0 10px,
#e0e0e0 20px
);
}
tr.mievr th {
@ -2145,7 +2174,8 @@ span.eval_coef_ue {
margin-right: 2em;
}
span.eval_coef_ue_titre {}
span.eval_coef_ue_titre {
}
/* Inscriptions modules/UE */
div.list_but_ue_inscriptions {
@ -2201,7 +2231,6 @@ form.list_but_ue_inscriptions td {
text-align: center;
}
table#but_ue_inscriptions {
margin-left: 16px;
width: auto;
@ -2290,7 +2319,10 @@ table.formation_list_table td.buttons span.but_placeholder {
}
.formation_list_table td.titre {
width: 50%;
width: 45%;
}
.formation_list_table td.commentaire {
font-style: italic;
}
.formation_list_table td.sems_list_txt {
@ -2355,10 +2387,11 @@ div.formation_list_ues {
div.formation_list_ues {
background-color: #b7d2fa;
margin-top: 20px
margin-top: 20px;
}
div.formation_list_ues_content {}
div.formation_list_ues_content {
}
div.formation_list_modules {
margin-top: 20px;
@ -2428,7 +2461,7 @@ div.formation_parcs {
column-gap: 8px;
}
div.formation_parcs>div {
div.formation_parcs > div {
font-size: 100%;
color: white;
background-color: #09c;
@ -2438,24 +2471,23 @@ div.formation_parcs>div {
padding: 4px 8px;
}
div.formation_parcs>div.focus {
div.formation_parcs > div.focus {
opacity: 1;
}
div.formation_parcs>div>a:hover {
div.formation_parcs > div > a:hover {
color: #ccc;
}
div.formation_parcs>div>a,
div.formation_parcs>div>a:visited {
div.formation_parcs > div > a,
div.formation_parcs > div > a:visited {
color: white;
}
div.ue_choix_niveau>div.formation_parcs>div {
div.ue_choix_niveau > div.formation_parcs > div {
font-size: 80%;
}
div.ue_list_tit {
font-weight: bold;
margin-top: 8px;
@ -2516,7 +2548,7 @@ span.ue_type {
}
table.formsemestre_description td.ue_coef_nul {
background-color: yellow!important;
background-color: yellow !important;
color: red;
font-weight: bold;
}
@ -2652,7 +2684,7 @@ div.cont_ue_choix_niveau {
flex-wrap: wrap;
}
div.cont_ue_choix_niveau>div {
div.cont_ue_choix_niveau > div {
display: inline-flex;
margin-left: 8px;
align-items: center;
@ -2688,7 +2720,6 @@ div#ue_list_modules {
margin-right: 15px;
}
span.ue_share {
font-weight: bold;
}
@ -2734,11 +2765,11 @@ span.code_parcours.no_parcours {
background-color: firebrick;
}
tr#tf_module_parcours>td {
tr#tf_module_parcours > td {
background-color: rgb(229, 229, 229);
}
tr#tf_module_app_critiques>td {
tr#tf_module_app_critiques > td {
background-color: rgb(194, 209, 228);
}
@ -3074,7 +3105,6 @@ a.bull_link:hover {
text-decoration: underline;
}
div.bulletin_menubar {
padding-left: 25px;
}
@ -3122,9 +3152,9 @@ div.eval_description {
div.bul_foot {
max-width: 1000px;
background: #FFE7D5;
background: #ffe7d5;
border-radius: 16px;
border: 1px solid #AAA;
border: 1px solid #aaa;
padding: 16px 32px;
margin-left: 16px;
}
@ -3155,7 +3185,6 @@ span.titredivsaisienote {
font-size: 115%;
}
.etud_dem {
color: rgb(130, 130, 130);
}
@ -3293,7 +3322,6 @@ mark {
padding-right: 5px;
}
form.sco_pref table.tf {
border-spacing: 5px 15px;
}
@ -3305,7 +3333,7 @@ td.tf-ro-fieldlabel {
}
td.tf-ro-fieldlabel:after {
content: ' :';
content: " :";
}
td.tf-ro-field {
@ -3416,7 +3444,7 @@ P.gtr_devel:before {
/* ---- Sortable tables --- */
/* Sortable tables */
table.sortable a.sortheader {
background-color: #E6E6E6;
background-color: #e6e6e6;
color: black;
font-weight: bold;
text-decoration: none;
@ -3502,7 +3530,6 @@ table.recap_parcours td {
td.rcp_dec {
color: rgb(0%, 0%, 50%);
;
}
td.rcp_nonass,
@ -3541,7 +3568,13 @@ table.recap_hide_details tr.sem_precedent td.ue_acro span {
}
.recap_parcours tr.sem_autre_formation td.rcp_titre_sem {
background-image: repeating-linear-gradient(-45deg, rgb(100, 205, 193), rgb(100, 205, 193) 2px, transparent 5px, transparent 40px);
background-image: repeating-linear-gradient(
-45deg,
rgb(100, 205, 193),
rgb(100, 205, 193) 2px,
transparent 5px,
transparent 40px
);
}
.rcp_l2 td {
@ -3612,7 +3645,6 @@ th.sfv_subtitle {
font-style: italic;
}
tr.sfv_ass {
background-color: rgb(90%, 90%, 80%);
}
@ -3659,7 +3691,7 @@ span.finalisationinscription {
.pas_sembox_title a {
font-weight: bold;
font-size: 100%;
color: #1C721C;
color: #1c721c;
}
.pas_sembox_subtitle {
@ -3773,20 +3805,19 @@ div.module_check_absences ul {
z-index: 1;
}
.scoplement>div {
.scoplement > div {
text-align: left;
display: inline-block;
white-space: nowrap;
}
.scoplement>div:nth-child(1),
.scoplement>div:nth-child(7) {
.scoplement > div:nth-child(1),
.scoplement > div:nth-child(7) {
margin-bottom: 8px;
}
/* ----------------------------------------------- */
/* ----------------------------- */
/* TABLES generees par gen_table */
/* ----------------------------- */
@ -3811,7 +3842,6 @@ table.table_coldate tr td:first-child {
color: rgb(0%, 0%, 50%);
}
table.table_listegroupe tr td {
padding-left: 0.5em;
padding-right: 0.5em;
@ -3827,7 +3857,6 @@ table.list_users th.roles_string {
overflow-wrap: break-word;
}
table.formsemestre_description tr.table_row_ue td {
font-weight: bold;
}
@ -3847,15 +3876,15 @@ table.formsemestre_description tbody tr.evaluation td {
}
/* --- */
tr#tf_extue_decl>td,
tr#tf_extue_note>td {
tr#tf_extue_decl > td,
tr#tf_extue_note > td {
padding-top: 20px;
}
tr#tf_extue_titre>td,
tr#tf_extue_acronyme>td,
tr#tf_extue_type>td,
tr#tf_extue_ects>td {
tr#tf_extue_titre > td,
tr#tf_extue_acronyme > td,
tr#tf_extue_type > td,
tr#tf_extue_ects > td {
padding-left: 20px;
}
@ -3866,7 +3895,6 @@ div.form_rename_partition {
margin-bottom: 2em;
}
td.calday {
text-align: right;
vertical-align: top;
@ -3877,7 +3905,6 @@ div.cal_evaluations table.monthcalendar td.calcell {
width: 6em;
}
div.cal_evaluations table.monthcalendar td a {
color: rgb(128, 0, 0);
}
@ -3895,12 +3922,10 @@ div.othersemlist {
border: 1px solid gray;
}
div.othersemlist input {
margin-left: 20px;
}
div#update_warning {
display: none;
border: 1px solid red;
@ -3911,19 +3936,19 @@ div#update_warning {
padding-bottom: 1ex;
}
div#update_warning>div:first-child:before {
div#update_warning > div:first-child:before {
content: url(/ScoDoc/static/icons/warning_img.png);
vertical-align: -80%;
}
div#update_warning>div:nth-child(2) {
div#update_warning > div:nth-child(2) {
font-size: 80%;
padding-left: 8ex;
}
/*
/*
Titres des tabs:
.nav-tabs li a {
.nav-tabs li a {
font-variant: small-caps;
font-size: 13pt;
}
@ -3959,9 +3984,8 @@ ul.main li {
padding-bottom: 2ex;
}
#scodoc_admin {
background-color: #EEFFFF;
background-color: #eeffff;
}
#message,
@ -4027,11 +4051,13 @@ div.apo_csv_status span {
}
div.apo_csv_status_nok {
background: url(/ScoDoc/static/icons/bullet_warning_img.png) no-repeat left top 0px;
background: url(/ScoDoc/static/icons/bullet_warning_img.png) no-repeat left
top 0px;
}
div.apo_csv_status_missing_elems {
background: url(/ScoDoc/static/icons/bullet_warning_img.png) no-repeat left top 0px;
background: url(/ScoDoc/static/icons/bullet_warning_img.png) no-repeat left
top 0px;
padding-left: 22px;
}
@ -4054,7 +4080,6 @@ div.apo_csv_jury_nok li {
color: red;
}
pre.small_pre_acc {
font-size: 60%;
width: 90%;
@ -4063,7 +4088,7 @@ pre.small_pre_acc {
overflow: scroll;
}
.apo_csv_jury_ok input[type=submit] {
.apo_csv_jury_ok input[type="submit"] {
color: green;
}
@ -4354,7 +4379,7 @@ button.unselect {
/* Non supproté par les navigateurs (en Fev. 2023)
.table_recap button:has(span a.clearreaload) {
}
}
*/
div.table_recap table.table_recap,
@ -4610,7 +4635,6 @@ table.table_recap th.col_malus {
color: rgb(165, 0, 0);
}
table.table_recap tr.ects td {
color: rgb(160, 86, 3);
font-weight: bold;
@ -4688,7 +4712,6 @@ table.table_recap th.evaluation.first_of_mod {
border-left: 1px dashed rgb(4, 16, 159);
}
table.table_recap td.evaluation.att {
color: rgb(255, 0, 217);
font-weight: bold;
@ -4760,7 +4783,6 @@ table.evaluations_recap tr.sae td {
background-color: #d8fcc8;
}
table.evaluations_recap tr.module td {
font-weight: bold;
}
@ -4823,7 +4845,7 @@ div.cas_settings {
background-color: #feb4e54f;
}
div.cas_settings>div,
div.cas_settings > div,
div.cas_settings div.form-group {
margin-left: 8px;
}
@ -4833,4 +4855,8 @@ div.cas_etat_certif_ssl {
margin-bottom: 8px;
font-style: italic;
color: rgb(231, 0, 0);
}
}
.edt_id {
color: rgb(85, 255, 24);
}

16
app/static/icons/absent.svg Executable file → Normal file

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 547 B

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,11 @@
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="85" height="85" rx="15" fill=""/>
<g opacity="0.7" clip-path="url(#clip0_120_4425)">
<path d="M67.2116 70L43 45.707L18.7885 70L15.0809 66.3043L39.305 41.9995L15.0809 17.6939L18.7885 14L43 38.2922L67.2116 14L70.9191 17.6939L46.695 41.9995L70.9191 66.3043L67.2116 70Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_120_4425">
<rect width="56" height="56" fill="white" transform="matrix(1 0 0 -1 15 70)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 540 B

View File

@ -1,5 +1,5 @@
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="85" height="85" rx="15" fill="#BBB"/>
<rect width="85" height="85" rx="15" fill=""/>
<defs>
<clipPath id="clip0_120_4425">
<rect width="56" height="56" fill="white" transform="matrix(1 0 0 -1 15 70)"/>

Before

Width:  |  Height:  |  Size: 291 B

After

Width:  |  Height:  |  Size: 287 B

View File

@ -1,7 +1,7 @@
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="85" height="85" rx="15" fill="#9CF1AF"/>
<rect width="85" height="85" rx="15" fill=""/>
<g clip-path="url(#clip0_120_4405)">
<g opacity="0.5">
<g opacity="0.7">
<path d="M70.7713 27.5875L36.0497 62.3091C35.7438 62.6149 35.2487 62.6149 34.9435 62.3091L15.2286 42.5935C14.9235 42.2891 14.9235 41.7939 15.2286 41.488L20.0191 36.6976C20.3249 36.3924 20.8201 36.3924 21.1252 36.6976L35.4973 51.069L64.8754 21.6909C65.1819 21.3858 65.6757 21.3858 65.9815 21.6909L70.7713 26.4814C71.0771 26.7865 71.0771 27.281 70.7713 27.5875Z" fill="black"/>
</g>
</g>

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 722 B

View File

@ -1,6 +1,6 @@
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="85" height="85" rx="15" fill="#F1D99C"/>
<g opacity="0.5" clip-path="url(#clip0_120_4407)">
<rect width="85" height="85" rx="15" fill=""/>
<g opacity="0.7" clip-path="url(#clip0_120_4407)">
<path d="M55.2901 49.1836L44.1475 41.3918V28C44.1475 27.3688 43.6311 26.8524 43 26.8524C42.3688 26.8524 41.8524 27.3688 41.8524 28V42C41.8524 42.3787 42.036 42.7229 42.3459 42.941L53.9819 51.077C54.177 51.2147 54.4065 51.2836 54.636 51.2836C54.9918 51.2836 55.3475 51.1115 55.577 50.7787C55.9327 50.2623 55.8065 49.5508 55.2901 49.1836Z" fill="black"/>
<path d="M62.7836 22.2164C57.482 16.9148 50.459 14 43 14C35.541 14 28.518 16.9148 23.2164 22.2164C17.9148 27.518 15 34.541 15 42C15 49.459 17.9148 56.482 23.2164 61.7836C28.518 67.0852 35.541 70 43 70C50.459 70 57.482 67.0852 62.7836 61.7836C68.0852 56.482 71 49.459 71 42C71 34.541 68.0852 27.518 62.7836 22.2164ZM44.1475 67.682V63C44.1475 62.3689 43.6311 61.8525 43 61.8525C42.3689 61.8525 41.8525 62.3689 41.8525 63V67.682C28.5869 67.0967 17.9033 56.4131 17.318 43.1475H22C22.6311 43.1475 23.1475 42.6311 23.1475 42C23.1475 41.3689 22.6311 40.8525 22 40.8525H17.318C17.9033 27.5869 28.5869 16.9033 41.8525 16.318V21C41.8525 21.6311 42.3689 22.1475 43 22.1475C43.6311 22.1475 44.1475 21.6311 44.1475 21V16.318C57.4131 16.9033 68.0967 27.5869 68.682 40.8525H64C63.3689 40.8525 62.8525 41.3689 62.8525 42C62.8525 42.6311 63.3689 43.1475 64 43.1475H68.682C68.0967 56.4131 57.4131 67.0967 44.1475 67.682Z" fill="black"/>
</g>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,10 +1,9 @@
$(function () {
$("div#export_help").accordion({
heightStyle: "content",
collapsible: true,
active: false,
});
$("div#export_help").accordion({
heightStyle: "content",
collapsible: true,
active: false,
});
});
// Affichage des listes par type
@ -14,77 +13,89 @@ $(function () {
// -> surligne le cas sélectionné
function display(r, c, row, col) {
if ((row != r) && (row != '*')) return 'none';
if ((col != c) && (col != '*')) return 'none';
return '';
if (row != r && row != "*") return "none";
if (col != c && col != "*") return "none";
return "";
}
function show_tag(all_rows, all_cols, tag) {
// Filtrer tous les étudiants
all_rows.split(',').forEach(function (r) {
all_cols.split(',').forEach(function (c) {
etudiants = r + c.substring(1);
$(etudiants).css("display", "none");
})
})
// sauf le tag
$('.' + tag).css('display', '');
// Filtrer tous les étudiants
all_rows.split(",").forEach(function (r) {
all_cols.split(",").forEach(function (c) {
etudiants = r + c.substring(1);
$(etudiants).css("display", "none");
});
});
// sauf le tag
$("." + tag).css("display", "");
}
function show_filtres(effectifs, filtre_row, filtre_col) {
$("#compte").html(effectifs);
if ((filtre_row == '') && (filtre_col == '')) {
$("#sans_filtre").css("display", "");
$("#filtre_row").css("display", "none");
$("#filtre_col").css("display", "none");
$("#compte").html(effectifs);
if (filtre_row == "" && filtre_col == "") {
$("#sans_filtre").css("display", "");
$("#filtre_row").css("display", "none");
$("#filtre_col").css("display", "none");
} else {
$("#sans_filtre").css("display", "none");
if (filtre_row == "") {
$("#filtre_row").css("display", "none");
$("#filtre_col").css("display", "");
$("#filtre_col").html("Filtre sur code étape: " + filtre_col);
} else if (filtre_col == "") {
$("#filtre_row").css("display", "");
$("#filtre_col").css("display", "none");
$("#filtre_row").html("Filtre sur semestre: " + filtre_row);
} else {
$("#sans_filtre").css("display", "none");
if (filtre_row == '') {
$("#filtre_row").css("display", "none");
$("#filtre_col").css("display", "");
$("#filtre_col").html("Filtre sur code étape: " + filtre_col);
} else if (filtre_col == '') {
$("#filtre_row").css("display", "");
$("#filtre_col").css("display", "none");
$("#filtre_row").html("Filtre sur semestre: " + filtre_row);
} else {
$("#filtre_row").css("display", "");
$("#filtre_col").css("display", "");
$("#filtre_row").html("Filtre sur semestre: " + filtre_row);
$("#filtre_col").html("Filtre sur code étape: " + filtre_col);
}
$("#filtre_row").css("display", "");
$("#filtre_col").css("display", "");
$("#filtre_row").html("Filtre sur semestre: " + filtre_row);
$("#filtre_col").html("Filtre sur code étape: " + filtre_col);
}
}
}
function doFiltrage(all_rows, all_cols, row, col, effectifs, filtre_row, filtre_col) {
show_filtres(effectifs, filtre_row, filtre_col)
all_rows.split(',').forEach(function (r) {
all_cols.split(',').forEach(function (c) {
etudiants = r + c.substring(1);
$(etudiants).css("display", display(r, c, row, col));
});
function doFiltrage(
all_rows,
all_cols,
row,
col,
effectifs,
filtre_row,
filtre_col
) {
show_filtres(effectifs, filtre_row, filtre_col);
all_rows.split(",").forEach(function (r) {
all_cols.split(",").forEach(function (c) {
etudiants = r + c.substring(1);
$(etudiants).css("display", display(r, c, row, col));
});
});
$('.repartition td').css("background-color", "");
$('.repartition th').css("background-color", "");
$(".repartition td").css("background-color", "");
$(".repartition th").css("background-color", "");
if (row == '*' && col == '*') { // Aucun filtre
} else if (row == '*') { // filtrage sur 1 colonne
$(col).css("background-color", "lightblue");
} else if (col == '*') { // Filtrage sur 1 ligne
$(row + '>td').css("background-color", "lightblue");
$(row + '>th').css("background-color", "lightblue");
} else { // filtrage sur 1 case
$(row + '>td' + col).css("background-color", "lightblue");
}
if (row == "*" && col == "*") {
// Aucun filtre
} else if (row == "*") {
// filtrage sur 1 colonne
$(col).css("background-color", "lightblue");
} else if (col == "*") {
// Filtrage sur 1 ligne
$(row + ">td").css("background-color", "lightblue");
$(row + ">th").css("background-color", "lightblue");
} else {
// filtrage sur 1 case
$(row + ">td" + col).css("background-color", "lightblue");
}
// Modifie le titre de la section pour indiquer la sélection:
// elt est le lien cliqué
// var td_class = elt.parentNode.className.trim();
// if (td_class) {
// var titre_col = $("table.repartition th.")[0].textContent.trim();
// if (titre_col) {
// $("h4#effectifs").html("Liste des étudiants de " + titre_col);
// }
// }
// Modifie le titre de la section pour indiquer la sélection:
// elt est le lien cliqué
// var td_class = elt.parentNode.className.trim();
// if (td_class) {
// var titre_col = $("table.repartition th.")[0].textContent.trim();
// if (titre_col) {
// $("h4#effectifs").html("Liste des étudiants de " + titre_col);
// }
// }
}

View File

@ -1,6 +1,5 @@
// <=== CONSTANTS and GLOBALS ===>
const TIMEZONE = "Europe/Paris";
let url;
function getUrl() {
@ -32,6 +31,17 @@ Object.defineProperty(String.prototype, "capitalize", {
},
enumerable: false,
});
const DatePrecisions = [
"year",
"month",
"day",
"hour",
"minute",
"second",
"millisecond",
];
// <<== Outils ==>>
Object.defineProperty(Array.prototype, "reversed", {
value: function () {
@ -84,7 +94,7 @@ function validateSelectors(btn) {
);
});
if (getModuleImplId() == null && window.forceModule) {
if (getModuleImplId() == null && window.forceModule && !readOnly) {
const HTML = `
<p>Attention, le module doit obligatoirement être renseigné.</p>
<p>Cela vient de la configuration du semestre ou plus largement du département.</p>
@ -101,6 +111,7 @@ function validateSelectors(btn) {
getAssiduitesFromEtuds(true);
document.querySelector(".selectors").disabled = true;
$("#tl_date").datepicker("option", "disabled", true);
generateMassAssiduites();
generateAllEtudRow();
btn.remove();
@ -126,7 +137,7 @@ function validateSelectors(btn) {
}
function onlyAbs() {
if (getDate() > moment()) {
if (getDate() > Date.now()) {
document
.querySelectorAll(".rbtn.present, .rbtn.retard")
.forEach((el) => el.remove());
@ -162,6 +173,7 @@ function uniqueCheckBox(box) {
* @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/
function sync_get(path, success, errors) {
//TODO Optimiser : rendre asynchrone + sans jquery
console.log("sync_get " + path);
$.ajax({
async: false,
@ -177,16 +189,25 @@ function sync_get(path, success, errors) {
* @param {CallableFunction} success fonction à effectuer en cas de succès
* @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/
function async_get(path, success, errors) {
async function async_get(path, success, errors) {
console.log("async_get " + path);
$.ajax({
async: true,
type: "GET",
url: path,
success: success,
error: errors,
});
let response;
try {
response = await fetch(path);
if (response.ok) {
const data = await response.json();
success(data);
} else {
throw new Error("Network response was not ok.");
}
} catch (error) {
console.error(error);
if (errors) errors(error);
}
return response;
}
/**
* Fait une requête POST de façon synchrone
* @param {String} path adresse distante
@ -195,6 +216,7 @@ function async_get(path, success, errors) {
* @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/
function sync_post(path, data, success, errors) {
//TODO Optimiser : rendre asynchrone + sans jquery
console.log("sync_post " + path);
$.ajax({
async: false,
@ -212,17 +234,32 @@ function sync_post(path, data, success, errors) {
* @param {CallableFunction} success fonction à effectuer en cas de succès
* @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/
function async_post(path, data, success, errors) {
console.log("sync_post " + path);
return $.ajax({
async: true,
type: "POST",
url: path,
data: JSON.stringify(data),
success: success,
error: errors,
});
async function async_post(path, data, success, errors) {
console.log("async_post " + path);
let response;
try {
response = await fetch(path, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (response.ok) {
const responseData = await response.json();
success(responseData);
} else {
throw new Error("Network response was not ok.");
}
} catch (error) {
console.error(error);
if (errors) errors(error);
}
return response;
}
// <<== Gestion des actions de masse ==>>
const massActionQueue = new Map();
@ -268,8 +305,8 @@ function executeMassActionQueue() {
*/
const tlTimes = getTimeLineTimes();
let assiduite = {
date_debut: tlTimes.deb.format(),
date_fin: tlTimes.fin.format(),
date_debut: tlTimes.deb.toFakeIso(),
date_fin: tlTimes.fin.toFakeIso(),
};
assiduite = setModuleImplId(assiduite);
@ -564,7 +601,10 @@ function toTime(time) {
* @returns
*/
function formatDate(date, styles = { dateStyle: "full" }) {
return new Intl.DateTimeFormat("fr-FR", styles).format(date);
return new Intl.DateTimeFormat("fr-FR", {
...{ timeZone: SCO_TIMEZONE },
...styles,
}).format(date);
}
/**
@ -572,17 +612,26 @@ function formatDate(date, styles = { dateStyle: "full" }) {
*/
function updateDate() {
const dateInput = document.querySelector("#tl_date");
const date = dateInput.valueAsDate ?? new Date();
let date = $(dateInput).datepicker("getDate");
if (date == null) {
date = new Date(Date.fromFRA(dateInput.value));
}
const intlOptions = {
dateStyle: "full",
timeZone: SCO_TIMEZONE,
};
let dateStr = "";
if (!verifyNonWorkDays(date.getDay(), nonWorkDays)) {
dateStr = formatDate(date).capitalize();
if (!isNonWorkDay(date, nonWorkDays)) {
dateStr = formatDate(date, intlOptions).capitalize();
} else {
// On se rend au dernier jour travaillé disponible
const lastWorkDay = getNearestWorkDay(date);
const att = document.createTextNode(
`Le jour sélectionné (${formatDate(date)}) n'est pas un jour travaillé.`
`Le jour sélectionné (${formatDate(
date,
intlOptions
)}) n'est pas un jour travaillé.`
);
const div = document.createElement("div");
div.appendChild(att);
@ -590,14 +639,23 @@ function updateDate() {
div.appendChild(
document.createTextNode(
`Le dernier jour travaillé disponible a été sélectionné : ${formatDate(
lastWorkDay
lastWorkDay,
intlOptions
)}.`
)
);
openAlertModal("Attention", div, "", "#eec660");
dateInput.value = lastWorkDay.toISOString().split("T")[0];
dateStr = formatDate(lastWorkDay).capitalize();
$(dateInput).datepicker("setDate", date_fra);
dateInput.value = date_fra;
date = lastWorkDay;
dateStr = formatDate(lastWorkDay, {
dateStyle: "full",
timeZone: SCO_TIMEZONE,
}).capitalize();
}
document.querySelector("#datestr").textContent = dateStr;
return true;
}
@ -606,26 +664,17 @@ function getNearestWorkDay(date) {
const aDay = 86400000; // 24 * 3600 * 1000 | H * s * ms
let day = date;
let count = 0;
while (verifyNonWorkDays(day.getDay(), nonWorkDays) && count++ < 7) {
while (isNonWorkDay(day, nonWorkDays) && count++ < 7) {
day = new Date(day - aDay);
}
return day;
}
function verifyDateInSemester() {
const date = new moment.tz(
document.querySelector("#tl_date").value,
TIMEZONE
);
const date = getDate();
const periodSemester = getFormSemestreDates();
return date.isBetween(
periodSemester.deb,
periodSemester.fin,
undefined,
"[]"
);
return date.isBetween(periodSemester.deb, periodSemester.fin, "[]");
}
/**
@ -637,15 +686,15 @@ function setupDate(onchange = null) {
const input = document.querySelector("#tl_date");
datestr.addEventListener("click", () => {
if (!input.disabled) {
if (!document.querySelector(".selectors").disabled) {
try {
input.showPicker();
document.querySelector(".infos .ui-datepicker-trigger").click();
} catch {}
}
});
if (onchange != null) {
input.addEventListener("change", onchange);
$(input).change(onchange);
}
}
@ -664,40 +713,21 @@ function getAssiduitesOnDateChange() {
* @param {String} separator le séparateur de la date intelligible (01/01/2000 {separtor} 10:00)
* @returns {String} la date intelligible
*/
function formatDateModal(str, separator = "·") {
return new moment.tz(str, TIMEZONE).format(`DD/MM/Y ${separator} HH:mm`);
function formatDateModal(str, separator = " ") {
return new Date(str).format("DD/MM/Y HH:mm").replace(" ", separator);
}
/**
* Vérifie si la date sélectionnée n'est pas un jour non travaillé
* Renvoie Vrai si le jour est non travaillé
*/
function verifyNonWorkDays(day, nonWorkdays) {
let d = "";
switch (day) {
case 0:
d = "dim";
break;
case 1:
d = "lun";
break;
case 2:
d = "mar";
break;
case 3:
d = "mer";
break;
case 4:
d = "jeu";
break;
case 5:
d = "ven";
break;
case 6:
d = "sam";
break;
}
function isNonWorkDay(day, nonWorkdays) {
const d = Intl.DateTimeFormat("fr-FR", {
timeZone: SCO_TIMEZONE,
weekday: "short",
})
.format(day)
.replace(".", "");
return nonWorkdays.indexOf(d) != -1;
}
@ -705,8 +735,8 @@ function verifyNonWorkDays(day, nonWorkdays) {
* Fonction qui vérifie si une période est dans un interval
* Objet période / interval
* {
* deb: moment.tz(<Date>),
* fin: moment.tz(<Date>),
* deb: Date,
* fin: Date,
* }
* @param {object} period
* @param {object} interval
@ -718,19 +748,19 @@ function hasTimeConflict(period, interval) {
/**
* On récupère la période de la timeline
* @returns {deb : moment.tz(), fin: moment.tz()}
* @returns {deb : Date, fin: Date)}
*/
function getTimeLineTimes() {
//getPeriodValues() -> retourne la position de la timeline [a,b] avec a et b des number
let values = getPeriodValues();
//On récupère la date
const dateiso = document.querySelector("#tl_date").value;
const dateiso = getDate().format("YYYY-MM-DD");
//On génère des objets temps (moment.tz)
//On génère des objets temps
values = values.map((el) => {
el = toTime(el).replace("h", ":");
el = `${dateiso}T${el}`;
return moment.tz(el, TIMEZONE);
return new Date(el);
});
return { deb: values[0], fin: values[1] };
@ -744,8 +774,8 @@ function getTimeLineTimes() {
function isConflictSameAsPeriod(conflict, period = undefined) {
const tlTimes = period == undefined ? getTimeLineTimes() : period;
const clTimes = {
deb: moment.tz(conflict.date_debut, TIMEZONE),
fin: moment.tz(conflict.date_fin, TIMEZONE),
deb: new Date(Date.removeUTC(conflict.date_debut)),
fin: new Date(Date.removeUTC(conflict.date_fin)),
};
return tlTimes.deb.isSame(clTimes.deb) && tlTimes.fin.isSame(clTimes.fin);
}
@ -755,9 +785,10 @@ function isConflictSameAsPeriod(conflict, period = undefined) {
* @returns {Date} la date sélectionnée
*/
function getDate() {
const date = new Date(document.querySelector("#tl_date").value);
date.setHours(0, 0, 0, 0);
return date;
const date =
$("#tl_date").datepicker("getDate") ??
new Date(Date.fromFRA(document.querySelector("#tl_date").value));
return date.startOf("day");
}
/**
@ -766,10 +797,7 @@ function getDate() {
*/
function getNextDate() {
const date = getDate();
const next = new Date(date.valueOf());
next.setDate(date.getDate() + 1);
next.setHours(0, 0, 0, 0);
return next;
return date.clone().add(1, "days");
}
/**
* Retourne un objet date représentant le jour précédent
@ -777,10 +805,7 @@ function getNextDate() {
*/
function getPrevDate() {
const date = getDate();
const next = new Date(date.valueOf());
next.setDate(date.getDate() - 1);
next.setHours(0, 0, 0, 0);
return next;
return date.clone().add(-1, "days");
}
/**
@ -788,44 +813,19 @@ function getPrevDate() {
* @param {Date} date
* @returns {string} la date iso avec le timezone
*/
function toIsoString(date) {
var tzo = -date.getTimezoneOffset(),
dif = tzo >= 0 ? "+" : "-",
pad = function (num) {
return (num < 10 ? "0" : "") + num;
};
return (
date.getFullYear() +
"-" +
pad(date.getMonth() + 1) +
"-" +
pad(date.getDate()) +
"T" +
pad(date.getHours()) +
":" +
pad(date.getMinutes()) +
":" +
pad(date.getSeconds()) +
dif +
pad(Math.floor(Math.abs(tzo) / 60)) +
":" +
pad(Math.abs(tzo) % 60)
);
}
/**
* Transforme un temps numérique en une date moment.tz
* Transforme un temps numérique en une date
* @param {number} nb
* @returns {moment.tz} Une date formée du temps donné et de la date courante
* @returns {Date} Une date formée du temps donné et de la date courante
*/
function numberTimeToDate(nb) {
time = toTime(nb).replace("h", ":");
date = document.querySelector("#tl_date").value;
date = getDate().format("YYYY-MM-DD");
datetime = `${date}T${time}`;
return moment.tz(datetime, TIMEZONE);
return new Date(datetime);
}
// <<== Gestion des assiduités ==>>
@ -841,8 +841,8 @@ function numberTimeToDate(nb) {
function getAssiduitesFromEtuds(clear, deb, fin) {
const etudIds = Object.keys(etuds).join(",");
const date_debut = deb ? deb : toIsoString(getPrevDate());
const date_fin = fin ? fin : toIsoString(getNextDate());
const date_debut = deb ? deb : getPrevDate().toFakeIso();
const date_fin = fin ? fin : getNextDate().toFakeIso();
if (clear) {
assiduites = {};
@ -885,8 +885,8 @@ function getAssiduitesFromEtuds(clear, deb, fin) {
function createAssiduite(etat, etudid) {
const tlTimes = getTimeLineTimes();
let assiduite = {
date_debut: tlTimes.deb.format(),
date_fin: tlTimes.fin.format(),
date_debut: tlTimes.deb.toFakeIso(),
date_fin: tlTimes.fin.toFakeIso(),
etat: etat,
};
@ -928,6 +928,102 @@ function createAssiduite(etat, etudid) {
openAlertModal("Sélection du module", content);
}
if (
data.errors["0"].message == "L'étudiant n'est pas inscrit au module"
) {
const HTML = `
<p>Attention, l'étudiant n'est pas inscrit à ce module.</p>
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
`;
const content = document.createElement("div");
content.innerHTML = HTML;
openAlertModal("Sélection du module", content);
}
with_errors = true;
}
},
(data, status) => {
//error
console.error(data, status);
errorAlert();
with_errors = true;
}
);
return !with_errors;
}
/**
* Création d'une assiduité pour un étudiant
* @param {String} etat l'état de l'étudiant
* @param {Number | String} etudid l'identifiant de l'étudiant
*
* TODO : Rendre asynchrone
*/
function createAssiduiteComplete(assiduite, etudid) {
if (!hasModuleImpl(assiduite) && window.forceModule) {
const html = `
<h3>Aucun module n'a été spécifié</h3>
`;
const div = document.createElement("div");
div.innerHTML = html;
openAlertModal("Erreur Module", div);
return false;
}
const path = getUrl() + `/api/assiduite/${etudid}/create`;
let with_errors = false;
sync_post(
path,
[assiduite],
(data, status) => {
//success
if (data.success.length > 0) {
let obj = data.success["0"].message.assiduite_id;
}
if (data.errors.length > 0) {
console.error(data.errors["0"].message);
if (data.errors["0"].message == "Module non renseigné") {
const HTML = `
<p>Attention, le module doit obligatoirement être renseigné.</p>
<p>Cela vient de la configuration du semestre ou plus largement du département.</p>
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
`;
const content = document.createElement("div");
content.innerHTML = HTML;
openAlertModal("Sélection du module", content);
}
if (
data.errors["0"].message == "L'étudiant n'est pas inscrit au module"
) {
const HTML = `
<p>Attention, l'étudiant n'est pas inscrit à ce module.</p>
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
`;
const content = document.createElement("div");
content.innerHTML = HTML;
openAlertModal("Sélection du module", content);
}
if (
data.errors["0"].message ==
"Duplication: la période rentre en conflit avec une plage enregistrée"
) {
const HTML = `
<p>L'assiduité n'a pas pu être enregistrée car une autre assiduité existe sur la période sélectionnée</p>
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
`;
const content = document.createElement("div");
content.innerHTML = HTML;
openAlertModal("Période conflictuelle", content);
}
with_errors = true;
}
},
@ -1052,7 +1148,7 @@ function editAssiduite(assiduite_id, etat, assi) {
}
/**
* Récupération des assiduités conflictuelles avec la période de la time line
* Récupération des assiduités conflictuelles avec la période de la timeline
* @param {String | Number} etudid identifiant de l'étudiant
* @returns {Array[Assiduité]} un tableau d'assiduité
*/
@ -1067,10 +1163,11 @@ function getAssiduitesConflict(etudid, periode) {
return etudAssiduites.filter((assi) => {
const interval = {
deb: moment.tz(assi.date_debut, TIMEZONE),
fin: moment.tz(assi.date_fin, TIMEZONE),
deb: new Date(Date.removeUTC(assi.date_debut)),
fin: new Date(Date.removeUTC(assi.date_fin)),
};
return hasTimeConflict(periode, interval);
const test = hasTimeConflict(periode, interval);
return test;
});
}
@ -1085,21 +1182,21 @@ function getLastAssiduiteOfPrevDate(etudid) {
return "";
}
const period = {
deb: moment.tz(getPrevDate(), TIMEZONE),
fin: moment.tz(getDate(), TIMEZONE),
deb: getPrevDate(),
fin: getDate(),
};
const prevAssiduites = etudAssiduites
.filter((assi) => {
const interval = {
deb: moment.tz(assi.date_debut, TIMEZONE),
fin: moment.tz(assi.date_fin, TIMEZONE),
deb: new Date(Date.removeUTC(assi.date_debut)),
fin: new Date(Date.removeUTC(assi.date_fin)),
};
return hasTimeConflict(period, interval);
})
.sort((a, b) => {
const a_fin = moment.tz(a.date_fin, TIMEZONE);
const b_fin = moment.tz(b.date_fin, TIMEZONE);
const a_fin = new Date(Date.removeUTC(a.date_fin));
const b_fin = new Date(Date.removeUTC(b.date_fin));
return b_fin < a_fin;
});
@ -1134,8 +1231,8 @@ function getAssiduiteValue(field) {
* @param {String | Number} etudid identifiant de l'étudiant
*/
function actualizeEtudAssiduite(etudid) {
const date_debut = toIsoString(getPrevDate());
const date_fin = toIsoString(getNextDate());
const date_debut = getPrevDate().toFakeIso();
const date_fin = getNextDate().toFakeIso();
const url_api =
getUrl() +
@ -1163,7 +1260,7 @@ function getAllAssiduitesFromEtud(
.replace("°", courant ? "&courant" : "")
: ""
}`;
//TODO Utiliser async_get au lieu de jquery
$.ajax({
async: true,
type: "GET",
@ -1232,8 +1329,8 @@ function assiduiteAction(element) {
assiduites[etudid],
getTimeLineTimes(),
{
deb: new moment.tz(getDate(), TIMEZONE),
fin: new moment.tz(getNextDate(), TIMEZONE),
deb: getDate(),
fin: getNextDate(),
}
);
const update = (assi) => {
@ -1328,17 +1425,17 @@ function generateEtudRow(
const HTML = `<div class="etud_row ${conflit} ${defdem}" id="etud_row_${
etud.id
}">
<div class="index">${index}</div>
<div class="name_field">
<img class="pdp" src="${pdp_url}">
<a class="name_set" href="BilanEtud?etudid=${etud.id}">
<h4 class="nom">${etud.nom}</h4>
<h5 class="prenom">${etud.prenom}</h5>
</a>
</div>
<div class="assiduites_bar">
<div id="prevDateAssi" class="${assiduite.prevAssiduites?.etat?.toLowerCase()}">
@ -1347,12 +1444,12 @@ function generateEtudRow(
<fieldset class="btns_field single" etudid="${etud.id}" assiduite_id="${
assiduite.id
}" type="${assiduite.type}">
${assi}
</fieldset>
</div>`;
return HTML;
@ -1377,13 +1474,12 @@ function insertEtudRow(etud, index, output = false) {
date_fin: null,
prevAssiduites: prevAssiduite,
};
if (conflict.length > 0) {
assiduite.etatAssiduite = conflict[0].etat;
assiduite.id = conflict[0].assiduite_id;
assiduite.date_debut = conflict[0].date_debut;
assiduite.date_fin = conflict[0].date_fin;
assiduite.date_debut = Date.removeUTC(conflict[0].date_debut);
assiduite.date_fin = Date.removeUTC(conflict[0].date_fin);
if (isConflictSameAsPeriod(conflict[0])) {
assiduite.type = "édition";
} else {
@ -1545,8 +1641,8 @@ function getFormSemestreDates() {
const dateFin = document.getElementById("formsemestre_date_fin").textContent;
return {
deb: dateDeb,
fin: dateFin,
deb: new Date(dateDeb),
fin: new Date(dateFin),
};
}
@ -1613,8 +1709,8 @@ function getJustificatifFromPeriod(date, etudid, update) {
url:
getUrl() +
`/api/justificatifs/${etudid}/query?date_debut=${date.deb
.add(1, "s")
.format()}&date_fin=${date.fin.subtract(1, "s").format()}`,
.add(1, "seconds")
.toFakeIso()}&date_fin=${date.fin.add(-1, "seconds").toFakeIso()}`,
success: (data) => {
update(data);
},
@ -1646,8 +1742,8 @@ function fastJustify(assiduite) {
}
const period = {
deb: new moment.tz(assiduite.date_debut, TIMEZONE),
fin: new moment.tz(assiduite.date_fin, TIMEZONE),
deb: new Date(Date.removeUTC(assiduite.date_debut)),
fin: new Date(Date.removeUTC(assiduite.date_fin)),
};
const action = (justifs) => {
//créer un nouveau justificatif
@ -1660,8 +1756,8 @@ function fastJustify(assiduite) {
//créer justificatif
const justif = {
date_debut: new moment.tz(assiduite.date_debut, TIMEZONE).format(),
date_fin: new moment.tz(assiduite.date_fin, TIMEZONE).format(),
date_debut: new Date(Date.removeUTC(assiduite.date_debut)).toFakeIso(),
date_fin: new Date(Date.removeUTC(assiduite.date_fin)).toFakeIso(),
raison: raison,
etat: etat,
};
@ -1694,7 +1790,7 @@ function fastJustify(assiduite) {
content,
success,
() => {},
"#7059FF"
"var(--color-primary)"
);
};
if (assiduite.etudid) {
@ -1744,6 +1840,8 @@ function getAllJustificatifsFromEtud(
`/api/justificatifs/${etudid}${
order ? "/query?order°".replace("°", courant ? "&courant" : "") : ""
}`;
//TODO Utiliser async_get au lieu de jquery
$.ajax({
async: true,
type: "GET",

View File

@ -1,7 +1,6 @@
// Affichage anciens (non BUT) bulletin de notes
// (uses jQuery)
// Change visibility of UE details (les <tr> de classe "notes_bulletin_row_mod" suivant)
// La table a la structure suivante:
// <tr class="notes_bulletin_row_ue"><td><span class="toggle_ue">+/-</span>...</td>...</tr>
@ -11,40 +10,44 @@
// On change la visi de tous les <tr> jusqu'au notes_bulletin_row_ue suivant.
//
function toggle_vis_ue(e, new_state) {
// e is the span containg the clicked +/- icon
var tr = e.parentNode.parentNode;
if (new_state == undefined) {
// current state: use alt attribute of current image
if (e.childNodes[0].alt == '+') {
new_state = false;
} else {
new_state = true;
}
}
// find next tr in siblings
var tr = tr.nextSibling;
//while ((tr != null) && sibl.tagName == 'TR') {
var current = true;
while ((tr != null) && current) {
if ((tr.nodeType == 1) && (tr.tagName == 'TR')) {
for (var i = 0; i < tr.classList.length; i++) {
if ((tr.classList[i] == 'notes_bulletin_row_ue') || (tr.classList[i] == 'notes_bulletin_row_sum_ects'))
current = false;
}
if (current) {
if (new_state) {
tr.style.display = 'none';
} else {
tr.style.display = 'table-row';
}
}
}
tr = tr.nextSibling;
}
if (new_state) {
e.innerHTML = '<img width="13" height="13" border="0" title="" alt="+" src="/ScoDoc/static/icons/plus_img.png"/>';
} else {
e.innerHTML = '<img width="13" height="13" border="0" title="" alt="-" src="/ScoDoc/static/icons/minus_img.png"/>';
}
// e is the span containg the clicked +/- icon
var tr = e.parentNode.parentNode;
if (new_state == undefined) {
// current state: use alt attribute of current image
if (e.childNodes[0].alt == "+") {
new_state = false;
} else {
new_state = true;
}
}
// find next tr in siblings
var tr = tr.nextSibling;
//while ((tr != null) && sibl.tagName == 'TR') {
var current = true;
while (tr != null && current) {
if (tr.nodeType == 1 && tr.tagName == "TR") {
for (var i = 0; i < tr.classList.length; i++) {
if (
tr.classList[i] == "notes_bulletin_row_ue" ||
tr.classList[i] == "notes_bulletin_row_sum_ects"
)
current = false;
}
if (current) {
if (new_state) {
tr.style.display = "none";
} else {
tr.style.display = "table-row";
}
}
}
tr = tr.nextSibling;
}
if (new_state) {
e.innerHTML =
'<img width="13" height="13" border="0" title="" alt="+" src="/ScoDoc/static/icons/plus_img.png"/>';
} else {
e.innerHTML =
'<img width="13" height="13" border="0" title="" alt="-" src="/ScoDoc/static/icons/minus_img.png"/>';
}
}

View File

@ -12,13 +12,12 @@ var CURRENTWEEKCOLOR = "yellow";
// get all tr elements from this class
// (no getElementBuClassName)
function getTRweek( week ) {
var tablecal = document.getElementById('maincalendar');
var all = tablecal.getElementsByTagName('tr');
var res = [] ;
for(var i=0; i < all.length; i++) {
if (all[i].className == week)
res[res.length] = all[i];
function getTRweek(week) {
var tablecal = document.getElementById("maincalendar");
var all = tablecal.getElementsByTagName("tr");
var res = [];
for (var i = 0; i < all.length; i++) {
if (all[i].className == week) res[res.length] = all[i];
}
return res;
}
@ -26,14 +25,13 @@ function getTRweek( week ) {
var HIGHLIGHTEDCELLS = [];
function deselectweeks() {
for(var i=0; i < HIGHLIGHTEDCELLS.length; i++) {
for (var i = 0; i < HIGHLIGHTEDCELLS.length; i++) {
var row = rows[i];
if (row) {
if (row.className.match('currentweek')) {
row.style.backgroundColor = CURRENTWEEKCOLOR;
if (row.className.match("currentweek")) {
row.style.backgroundColor = CURRENTWEEKCOLOR;
} else {
row.style.backgroundColor = WEEKDAYCOLOR;
row.style.backgroundColor = WEEKDAYCOLOR;
}
rows[i] = null;
}
@ -44,11 +42,11 @@ function deselectweeks() {
function highlightweek(el) {
deselectweeks();
var week = el.className;
if ((week == 'wkend') || (week.substring(0,2) != 'wk')) {
if (week == "wkend" || week.substring(0, 2) != "wk") {
return; /* does not hightlight weekends */
}
rows = getTRweek(week);
for (var i=0; i < rows.length; i++) {
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
row.style.backgroundColor = DAYHIGHLIGHT;
HIGHLIGHTEDCELLS[HIGHLIGHTEDCELLS.length] = row;
@ -58,7 +56,7 @@ function highlightweek(el) {
// click on a day
function wclick(el) {
monday = el.className;
form = document.getElementById('formw');
form.datelundi.value = monday.substr(2).replace(/_/g,'/').split(' ')[0];
form = document.getElementById("formw");
form.datelundi.value = monday.substr(2).replace(/_/g, "/").split(" ")[0];
form.submit();
}

View File

@ -1,6 +1,5 @@
function submit_form() {
$("#config_logos_form").submit();
$("#config_logos_form").submit();
}
$(function () {
})
$(function () {});

595
app/static/js/date_utils.js Normal file
View File

@ -0,0 +1,595 @@
/**
* Transforme une date du format français (DD/MM/YYYY) au format iso (YYYY-MM-DD)
* Exemple d'utilisation :
* new Date(Date.fromFRA("30/06/2024")) -> new Date("2024-06-30")
* @param {string} dateFra
* @returns {string} dateIso
*/
Date.fromFRA = function (dateFra) {
if (dateFra == "") return "";
// Expression régulière pour valider le format de date ISO (YYYY-MM-DD)
const regexDateFra = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/;
// Vérification du format de la date ISO
if (!regexDateFra.test(dateFra)) {
throw new Error(
`La date (format français) passée en paramètre [${dateFra}] n'est pas valide.`
);
}
// Conversion du format français (DD/MM/YYYY) au format ISO (YYYY-MM-DD)
return `${dateFra.substring(6, 10)}-${dateFra.substring(
3,
5
)}-${dateFra.substring(0, 2)}`;
};
/**
* Transforme une date du format iso (YYYY-MM-DD) au format français (DD/MM/YYYY)
* Exemple d'utilisation :
* Date.toFRA("2024-06-30") -> "30/06/2024"
* @param {string} dateIso
* @returns {string} dateFra
*/
Date.toFRA = function (dateIso) {
if (dateIso == "") return "";
// Expression régulière pour valider le format de date ISO (YYYY-MM-DD)
const regexDateIso = /^\d{4}-(0\d|1[0-2])-([0-2]\d|3[01])$/;
// Vérification du format de la date ISO
if (!regexDateIso.test(dateIso)) {
throw new Error(
`La date ISO passée en paramètre [${dateIso}] n'est pas valide.`
);
}
// Conversion du format ISO (YYYY-MM-DD) en format français (DD/MM/YYYY)
return `${dateIso.substring(8, 10)}/${dateIso.substring(
5,
7
)}/${dateIso.substring(0, 4)}`;
};
/**
* Vérifie si le début de l'une des périodes est avant la fin de l'autre
* et si la fin de cette période est après le début de l'autre.
* @param {Object} period {deb:Object, fin:Object}
* @param {Object} interval {deb:Object, fin:Object}
* @returns vrai si la periode et l'interval ont une intersection commune
*/
Date.intersect = function (period, interval) {
return period.deb <= interval.fin && period.fin >= interval.deb;
};
Date.removeUTC = function (isoString) {
const reg = new RegExp(/[+-][\d:]+$/);
return isoString.replace(reg, "");
};
Object.defineProperty(Date.prototype, "isValid", {
value: function () {
return !Number.isNaN(this.getTime());
},
});
Object.defineProperty(Date.prototype, "startOf", {
/**
* Génère u la date à la plus petite valeur pour la précision donnée.
* @param {string} precision - La précision souhaitée (year, month, day, hours, minutes, seconds, milliseconds).
* @returns {Date} - Une nouvelle date ajustée.
*/
value: function (precision) {
const newDate = this.clone();
switch (precision) {
case "year":
newDate.setMonth(0);
case "month":
newDate.setDate(1);
case "day":
newDate.setHours(0);
case "hours":
newDate.setMinutes(0);
case "minutes":
newDate.setSeconds(0);
case "seconds":
newDate.setMilliseconds(0);
break;
case "milliseconds":
break;
default:
throw new Error(
`Invalid precision for startOf function [${precision}]`
);
}
return newDate;
},
});
Object.defineProperty(Date.prototype, "endOf", {
/**
* Ajuste la date à la plus grande valeur pour la précision donnée.
* @param {string} precision - La précision souhaitée (year, month, day, hours, minutes, seconds, milliseconds).
* @returns {Date} - Une nouvelle date ajustée.
*/
value: function (precision) {
const newDate = this.clone();
switch (precision) {
case "year":
newDate.setMonth(11); // Décembre est le 11ème mois (0-indexé)
case "month":
newDate.setDate(0); // Le jour 0 du mois suivant est le dernier jour du mois courant
newDate.setMonth(newDate.getMonth() + 1);
case "day":
newDate.setHours(23); // 23 heures est la dernière heure de la journée
case "hours":
newDate.setMinutes(59); // 59 minutes est la dernière minute de l'heure
case "minutes":
newDate.setSeconds(59); // 59 secondes est la dernière seconde de la minute
case "seconds":
newDate.setMilliseconds(999); // 999 millisecondes est la dernière milliseconde de la seconde
break;
case "milliseconds":
// Rien à faire pour les millisecondes
break;
default:
throw new Error("Invalid precision for endOf function");
}
return newDate;
},
});
Object.defineProperty(Date.prototype, "isBefore", {
/**
* Retourne vrai si la date est située avant la date fournie
* @param {Date} date
* @returns {boolean}
*/
value: function (date) {
return this.valueOf() < date.valueOf();
},
});
Object.defineProperty(Date.prototype, "isAfter", {
/**
* Retourne vrai si la date est située après la date fournie
* @param {Date} date
* @returns {boolean}
*/
value: function (date) {
return this.valueOf() > date.valueOf();
},
});
Object.defineProperty(Date.prototype, "isSame", {
/**
* Retourne vrai si les dates sont les mêmes
* @param {Date} date
* @param {string} precision default : "milliseconds"
* @returns boolean
*/
value: function (date, precision = "milliseconds") {
return (
this.startOf(precision).valueOf() == date.startOf(precision).valueOf()
);
},
});
Object.defineProperty(Date.prototype, "isBetween", {
/**
* Vérifie si la date est comprise dans une période avec une précision et une inclusivité optionnelles
* @param {Date} deb - La date de début de la période
* @param {Date} fin - La date de fin de la période
* @param {String} bornes - L'inclusivité/exclusivité de la comparaison ("[]", "()", "[)", "(]")
* - bornes incluses : []
* - bornes excluses : ()
* - borne gauche incluse et borne droit excluse : [)
* - borne gauche excluse et borne droit incluse : (]
*/
value: function (deb, fin, bornes = "[]") {
// Ajuste la date actuelle, la date de début et la date de fin à la précision spécifiée
// Vérifie les bornes en fonction de l'inclusivité/exclusivité spécifiée dans 'bornes'
const check_deb =
bornes[0] === "("
? this.valueOf() > deb.valueOf()
: this.valueOf() >= deb.valueOf();
const check_fin =
bornes[1] === ")"
? fin.valueOf() > this.valueOf()
: fin.valueOf() >= this.valueOf();
return check_deb && check_fin;
},
});
Object.defineProperty(Date.prototype, "toIsoUtcString", {
/**
* @returns date au format iso utc (yyyy-mm-ddThh:MM±oo:oo:oo)
*/
value: function () {
// Formater la date et l'heure
const date = this;
var tzo = -date.getTimezoneOffset(),
dif = tzo >= 0 ? "+" : "-",
pad = function (num) {
return (num < 10 ? "0" : "") + num;
};
return (
this.toFakeIso() +
dif +
pad(Math.floor(Math.abs(tzo) / 60)) +
":" +
pad(Math.abs(tzo) % 60)
);
},
});
Object.defineProperty(Date.prototype, "toFakeIso", {
value: function () {
const date = this;
pad = function (num) {
return (num < 10 ? "0" : "") + num;
};
return (
date.getFullYear() +
"-" +
pad(date.getMonth() + 1) +
"-" +
pad(date.getDate()) +
"T" +
pad(date.getHours()) +
":" +
pad(date.getMinutes()) +
":" +
pad(date.getSeconds())
);
},
});
Object.defineProperty(Date.prototype, "clone", {
/**
* @returns Retourne une copie de la date (copie non liée)
*/
value: function () {
return structuredClone(this);
},
});
Object.defineProperty(Date.prototype, "format", {
value: function (formatString) {
let iso = this.toIsoUtcString();
switch (formatString) {
case "DD/MM/Y HH:mm":
return this.toLocaleString("fr-FR", {
day: "2-digit",
month: "2-digit",
year: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: SCO_TIMEZONE,
});
case "DD/MM/YYYY HH:mm":
return this.toLocaleString("fr-FR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: SCO_TIMEZONE,
});
case "YYYY-MM-DDTHH:mm":
// slice : YYYY-MM-DDTHH
// slice + 3 : YYYY-MM-DDTHH:mm
return iso.slice(0, iso.indexOf(":") + 3);
case "YYYY-MM-DD":
return iso.slice(0, iso.indexOf("T"));
default:
return this.toFakeIso();
}
},
});
Object.defineProperty(Date.prototype, "add", {
/**
* Ajoute une valeur spécifiée à un élément de la date.
* @param {number} value - La valeur à ajouter.
* @param {string} type - Le type de la valeur (year, month, day, hours, minutes, seconds).
*/
value: function (value, type) {
switch (type) {
case "years":
this.setFullYear(this.getFullYear() + value);
break;
case "months":
this.setMonth(this.getMonth() + value);
break;
case "days":
this.setDate(this.getDate() + value);
break;
case "hours":
this.setHours(this.getHours() + value);
break;
case "minutes":
this.setMinutes(this.getMinutes() + value);
break;
case "seconds":
this.setSeconds(this.getSeconds() + value);
break;
default:
throw new Error(
`Invalid type for adding to date | type : ${type} value : ${value}`
);
}
return this; // Return the modified date
},
});
class Duration {
/**
* Constructeur de la classe Duration.
* @param {Date} start - La date de début de la période.
* @param {Date} end - La date de fin de la période.
*/
constructor(start, end) {
this.start = start; // Stocke la date de début.
this.end = end; // Stocke la date de fin.
this.duration = end - start; // Calcule la durée en millisecondes entre les deux dates.
}
/**
* Calcule le nombre d'années entre les deux dates et arrondit le résultat à quatre décimales.
* @return {number} Le nombre d'années arrondi à quatre décimales.
*/
get years() {
const startYear = this.start.getFullYear(); // Obtient l'année de la date de début.
const endYear = this.end.getFullYear(); // Obtient l'année de la date de fin.
// Calcule la différence en années et arrondit à quatre décimales.
return parseFloat((endYear - startYear).toFixed(4));
}
/**
* Calcule le nombre de mois entre les deux dates, en tenant compte des années et des jours, et arrondit le résultat à quatre décimales.
* @return {number} Le nombre de mois arrondi à quatre décimales.
*/
get months() {
const years = this.years; // Nombre d'années complètes.
// Calcule la différence en mois, en ajoutant la différence en jours divisée par 30 pour une approximation.
const months =
years * 12 +
(this.end.getMonth() - this.start.getMonth()) +
(this.end.getDate() - this.start.getDate()) / 30;
// Arrondit à quatre décimales.
return parseFloat(months.toFixed(4));
}
/**
* Calcule le nombre de jours entre les deux dates et arrondit le résultat à quatre décimales.
* @return {number} Le nombre de jours arrondi à quatre décimales.
*/
get days() {
// Convertit la durée en millisecondes en jours et arrondit à quatre décimales.
return parseFloat((this.duration / (24 * 60 * 60 * 1000)).toFixed(4));
}
/**
* Calcule le nombre d'heures entre les deux dates et arrondit le résultat à quatre décimales.
* @return {number} Le nombre d'heures arrondi à quatre décimales.
*/
get hours() {
// Convertit la durée en millisecondes en heures et arrondit à quatre décimales.
return parseFloat((this.duration / (60 * 60 * 1000)).toFixed(4));
}
/**
* Calcule le nombre de minutes entre les deux dates et arrondit le résultat à quatre décimales.
* @return {number} Le nombre de minutes arrondi à quatre décimales.
*/
get minutes() {
// Convertit la durée en millisecondes en minutes et arrondit à quatre décimales.
return parseFloat((this.duration / (60 * 1000)).toFixed(4));
}
/**
* Calcule le nombre de secondes entre les deux dates et arrondit le résultat à quatre décimales.
* @return {number} Le nombre de secondes arrondi à quatre décimales.
*/
get seconds() {
// Convertit la durée en millisecondes en secondes et arrondit à quatre décimales.
return parseFloat((this.duration / 1000).toFixed(4));
}
/**
* Obtient le nombre de millisecondes entre les deux dates et arrondit le résultat à quatre décimales.
* @return {number} Le nombre de millisecondes arrondi à quatre décimales.
*/
get milliseconds() {
// Arrondit la durée totale en millisecondes à quatre décimales.
return parseFloat(this.duration.toFixed(4));
}
}
class ScoDocDateTimePicker extends HTMLElement {
constructor() {
super();
// Définir si le champ est requis
this.required = this.hasAttribute("required");
// Initialiser le shadow DOM
const shadow = this.attachShadow({ mode: "open" });
// Créer l'input pour la date
const dateInput = document.createElement("input");
dateInput.type = "date";
dateInput.id = "date";
// Créer l'input pour l'heure
const timeInput = document.createElement("input");
timeInput.type = "time";
timeInput.id = "time";
timeInput.step = 60;
// Ajouter les inputs dans le shadow DOM
shadow.appendChild(dateInput);
shadow.appendChild(timeInput);
// Gestionnaires d'événements pour la mise à jour de la valeur
dateInput.addEventListener("change", () => this.updateValue());
timeInput.addEventListener("change", () => this.updateValue());
// Style CSS pour les inputs
const style = document.createElement("style");
style.textContent = `
input {
display: inline-block;
}
input:invalid {
border: 1px solid red;
}
`;
// Ajouter le style au shadow DOM
shadow.appendChild(style);
}
static get observedAttributes() {
return ["show"]; // Ajoute 'show' à la liste des attributs observés
}
connectedCallback() {
// Récupérer l'attribut 'name'
this.name = this.getAttribute("name");
// Créer un input caché pour la valeur datetime
this.hiddenInput = document.createElement("input");
this.hiddenInput.type = "hidden";
this.hiddenInput.name = this.name;
this.appendChild(this.hiddenInput);
// Gérer la soumission du formulaire
this.closest("form")?.addEventListener("submit", (e) => {
if (!this.validate()) {
e.preventDefault(); // Empêcher la soumission si non valide
this.dispatchEvent(
new Event("invalid", { bubbles: true, cancelable: true })
);
} else {
// Mettre à jour la valeur de l'input caché avant la soumission
this.hiddenInput.value = this.isValid()
? this.valueAsDate.toIsoUtcString()
: "";
}
});
this.updateDisplay();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "show") {
this.updateDisplay(); // Met à jour l'affichage si l'attribut 'show' change
}
}
updateDisplay() {
const mode = this.getAttribute("show") || "both";
const dateInput = this.shadowRoot.querySelector("#date");
const timeInput = this.shadowRoot.querySelector("#time");
switch (mode) {
case "date":
dateInput.style.display = "inline-block";
timeInput.style.display = "none";
break;
case "time":
dateInput.style.display = "none";
timeInput.style.display = "inline-block";
break;
case "both":
default:
dateInput.style.display = "inline-block";
timeInput.style.display = "inline-block";
}
}
// Vérifier si la valeur forme une date valide
isValid() {
return !Number.isNaN(this.valueAsDate.getTime());
}
// Valider l'élément
validate() {
if (this.required && !this.isValid()) {
return false;
}
return true;
}
// Mettre à jour la valeur interne
updateValue() {
const dateInput = this.shadowRoot.querySelector("#date");
const timeInput = this.shadowRoot.querySelector("#time");
this._value = `${dateInput.value}T${timeInput.value}`;
this.dispatchEvent(new Event("change", { bubbles: true }));
// Appliquer le style 'invalid' si nécessaire
dateInput.classList.toggle("invalid", this.required && !this.isValid());
timeInput.classList.toggle("invalid", this.required && !this.isValid());
}
// Getter pour obtenir la valeur actuelle.
get value() {
return this._value;
}
get valueAsObject() {
const dateInput = this.shadowRoot.querySelector("#date");
const timeInput = this.shadowRoot.querySelector("#time");
return {
date: dateInput.value,
time: timeInput.value,
};
}
// Getter pour obtenir la valeur en tant qu'objet Date.
get valueAsDate() {
return new Date(this._value);
}
// Setter pour définir la valeur. Sépare la valeur en date et heure et les définit individuellement.
set value(val) {
let [date, time] = val.split("T");
this.shadowRoot.querySelector("#date").value = date;
time = time.substring(0, 5);
this.shadowRoot.querySelector("#time").value = time;
this._value = val;
}
// Setter pour définir la valeur à partir d'un objet avec les propriétés 'date' et 'time'.
set valueAsObject(obj) {
const dateInput = this.shadowRoot.querySelector("#date");
const timeInput = this.shadowRoot.querySelector("#time");
if (obj.hasOwnProperty("date")) {
dateInput.value = obj.date || ""; // Définit la valeur de l'input de date si elle est fournie
}
if (obj.hasOwnProperty("time")) {
timeInput.value = obj.time.substring(0, 5) || ""; // Définit la valeur de l'input d'heure si elle est fournie
}
// Met à jour la valeur interne en fonction des nouvelles valeurs des inputs
this.updateValue();
}
// Setter pour définir la valeur à partir d'un objet Date.
set valueAsDate(dateVal) {
// Formatage de l'objet Date en string et mise à jour de la valeur.
this.value = `${dateVal.getFullYear()}-${String(
dateVal.getMonth() + 1
).padStart(2, "0")}-${String(dateVal.getDate()).padStart(2, "0")}T${String(
dateVal.getHours()
).padStart(2, "0")}:${String(dateVal.getMinutes()).padStart(2, "0")}`;
}
}
// Définition du nouvel élément personnalisé 'scodoc-datetime'.
customElements.define("scodoc-datetime", ScoDocDateTimePicker);

View File

@ -13,70 +13,70 @@ les balises (fermées par défaut sauf si attribut open déjà activé dans le c
*/
const ID_ATTRIBUTE = "ds_id"
const ID_ATTRIBUTE = "ds_id";
function genere_id(detail, idnum) {
let id = "ds_" + idnum;
if (detail.getAttribute("id")) {
id = "#" + detail.getAttribute("id");
}
detail.setAttribute(ID_ATTRIBUTE, id);
return id;
let id = "ds_" + idnum;
if (detail.getAttribute("id")) {
id = "#" + detail.getAttribute("id");
}
detail.setAttribute(ID_ATTRIBUTE, id);
return id;
}
// remise à l'état initial. doit être exécuté dès le chargement de la page pour que l'état 'open'
// des balises soit celui indiqué par le serveur (et donc indépendant du localstorage)
function reset_detail(detail, id) {
let opened = detail.getAttribute("open");
if (opened) {
detail.setAttribute("open", true);
localStorage.setItem(id, true);
} else {
detail.removeAttribute("open");
localStorage.setItem(id, false);
}
let opened = detail.getAttribute("open");
if (opened) {
detail.setAttribute("open", true);
localStorage.setItem(id, true);
} else {
detail.removeAttribute("open");
localStorage.setItem(id, false);
}
}
function restore_detail(detail, id) {
let status = localStorage.getItem(id);
if (status == "true") {
detail.setAttribute("open", true);
} else {
detail.removeAttribute("open");
}
let status = localStorage.getItem(id);
if (status == "true") {
detail.setAttribute("open", true);
} else {
detail.removeAttribute("open");
}
}
function add_listener(detail) {
detail.addEventListener('toggle', (e) => {
let id = e.target.getAttribute(ID_ATTRIBUTE);
let ante = e.target.getAttribute("open");
if (ante == null) {
localStorage.setItem(id, false);
} else {
localStorage.setItem(id, true);
}
e.stopPropagation();
})
detail.addEventListener("toggle", (e) => {
let id = e.target.getAttribute(ID_ATTRIBUTE);
let ante = e.target.getAttribute("open");
if (ante == null) {
localStorage.setItem(id, false);
} else {
localStorage.setItem(id, true);
}
e.stopPropagation();
});
}
function reset_ds() {
let idnum = 0;
keepDetails = true;
details = document.querySelectorAll("details")
details.forEach(function (detail) {
let id = genere_id(detail, idnum);
console.log("Processing " + id)
if (keepDetails) {
restore_detail(detail, id);
} else {
reset_detail(detail, id);
}
add_listener(detail);
idnum++;
});
let idnum = 0;
keepDetails = true;
details = document.querySelectorAll("details");
details.forEach(function (detail) {
let id = genere_id(detail, idnum);
console.log("Processing " + id);
if (keepDetails) {
restore_detail(detail, id);
} else {
reset_detail(detail, id);
}
add_listener(detail);
idnum++;
});
}
window.addEventListener('load', function() {
console.log("details/summary persistence ON");
reset_ds();
})
window.addEventListener("load", function () {
console.log("details/summary persistence ON");
reset_ds();
});

View File

@ -1,38 +1,32 @@
function _partition_set_attr(partition_id, attr_name, attr_value) {
$.post(SCO_URL + '/partition_set_attr',
{
'partition_id': partition_id,
'attr': attr_name,
'value': attr_value
},
function (result) {
sco_message(result);
});
return;
$.post(
SCO_URL + "/partition_set_attr",
{
partition_id: partition_id,
attr: attr_name,
value: attr_value,
},
function (result) {
sco_message(result);
}
);
return;
}
// Met à jour bul_show_rank lorsque checkbox modifiees:
function update_rk(e) {
var partition_id = $(e).attr('data-partition_id');
var v;
if (e.checked)
v = '1';
else
v = '0';
_partition_set_attr(partition_id, 'bul_show_rank', v);
var partition_id = $(e).attr("data-partition_id");
var v;
if (e.checked) v = "1";
else v = "0";
_partition_set_attr(partition_id, "bul_show_rank", v);
}
function update_show_in_list(e) {
var partition_id = $(e).attr('data-partition_id');
var v;
if (e.checked)
v = '1';
else
v = '0';
var partition_id = $(e).attr("data-partition_id");
var v;
if (e.checked) v = "1";
else v = "0";
_partition_set_attr(partition_id, 'show_in_lists', v);
_partition_set_attr(partition_id, "show_in_lists", v);
}

View File

@ -1,63 +1,72 @@
// Affiche et met a jour la liste des UE partageant le meme code
$().ready(function () {
if (document.querySelector("#tf_ue_id")) {
/* fonctions spécifiques pour edition UE */
update_ue_list();
$("#tf_ue_code").bind("keyup", update_ue_list);
if (document.querySelector("#tf_ue_id")) {
/* fonctions spécifiques pour edition UE */
update_ue_list();
$("#tf_ue_code").bind("keyup", update_ue_list);
$("select#tf_type").change(function () {
update_bonus_description();
});
update_bonus_description();
}
$("select#tf_type").change(function () {
update_bonus_description();
});
update_bonus_description();
}
});
function update_bonus_description() {
var ue_type = $("#tf_type")[0].value;
if (ue_type == "1") { /* UE SPORT */
$("#bonus_description").show();
var query = "/ScoDoc/get_bonus_description/default";
$.get(query, '', function (data) {
$("#bonus_description").html(data);
});
} else {
$("#bonus_description").html("");
$("#bonus_description").hide();
}
var ue_type = $("#tf_type")[0].value;
if (ue_type == "1") {
/* UE SPORT */
$("#bonus_description").show();
var query = "/ScoDoc/get_bonus_description/default";
$.get(query, "", function (data) {
$("#bonus_description").html(data);
});
} else {
$("#bonus_description").html("");
$("#bonus_description").hide();
}
}
function update_ue_list() {
let ue_id = $("#tf_ue_id")[0].value;
let ue_code = $("#tf_ue_code")[0].value;
let query = SCO_URL + "/Notes/ue_sharing_code?ue_code=" + ue_code + "&hide_ue_id=" + ue_id + "&ue_id=" + ue_id;
$.get(query, '', function (data) {
$("#ue_list_code").html(data);
});
let ue_id = $("#tf_ue_id")[0].value;
let ue_code = $("#tf_ue_code")[0].value;
let query =
SCO_URL +
"/Notes/ue_sharing_code?ue_code=" +
ue_code +
"&hide_ue_id=" +
ue_id +
"&ue_id=" +
ue_id;
$.get(query, "", function (data) {
$("#ue_list_code").html(data);
});
}
function set_ue_parcour(checkbox) {
let url = checkbox.dataset.setter;
const checkboxes = document.querySelectorAll('#choix_parcours input[type="checkbox"]:checked');
const parcours_ids = [];
checkboxes.forEach(function (checkbox) {
parcours_ids.push(checkbox.value);
let url = checkbox.dataset.setter;
const checkboxes = document.querySelectorAll(
'#choix_parcours input[type="checkbox"]:checked'
);
const parcours_ids = [];
checkboxes.forEach(function (checkbox) {
parcours_ids.push(checkbox.value);
});
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(parcours_ids),
})
.then((response) => response.json())
.then((data) => {
if (data.status == 404) {
sco_error_message(data.message);
} else {
sco_message(data.message);
}
});
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(parcours_ids)
})
.then(response => response.json())
.then(data => {
if (data.status == 404) {
sco_error_message(data.message);
} else {
sco_message(data.message);
}
});
}

View File

@ -1,65 +1,68 @@
// 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"]
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;
},
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}`;
},
},
}
},
},
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

@ -1,38 +1,37 @@
// Tableau recap evaluations du semestre
$(function () {
$('table.evaluations_recap').DataTable(
{
paging: false,
searching: true,
info: false,
autoWidth: false,
fixedHeader: {
header: true,
footer: false
},
orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting
colReorder: true,
"columnDefs": [
{
// colonne date, triable (XXX ne fonctionne pas)
targets: ["date"],
"type": "string",
},
],
dom: 'Bfrtip',
buttons: [
{
extend: 'copyHtml5',
text: 'Copier',
exportOptions: { orthogonal: 'export' }
},
{
extend: 'excelHtml5',
exportOptions: { orthogonal: 'export' },
title: document.querySelector('table.evaluations_recap').dataset.filename
},
],
})
$("table.evaluations_recap").DataTable({
paging: false,
searching: true,
info: false,
autoWidth: false,
fixedHeader: {
header: true,
footer: false,
},
orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting
colReorder: true,
columnDefs: [
{
// colonne date, triable (XXX ne fonctionne pas)
targets: ["date"],
type: "string",
},
],
dom: "Bfrtip",
buttons: [
{
extend: "copyHtml5",
text: "Copier",
exportOptions: { orthogonal: "export" },
},
{
extend: "excelHtml5",
exportOptions: { orthogonal: "export" },
title: document.querySelector("table.evaluations_recap").dataset
.filename,
},
],
});
});

View File

@ -1,14 +1,11 @@
// Export table tous les resultats
// Menu choix parcours:
$(function() {
$('#parcours_sel').multiselect(
{
includeSelectAllOption: true,
nonSelectedText:'Choisir le(s) parcours...',
selectAllValue: '',
numberDisplayed: 3,
}
);
$(function () {
$("#parcours_sel").multiselect({
includeSelectAllOption: true,
nonSelectedText: "Choisir le(s) parcours...",
selectAllValue: "",
numberDisplayed: 3,
});
});

View File

@ -3,26 +3,38 @@ var apo_ue_editor = null;
var apo_mod_editor = null;
$(document).ready(function () {
var table_options = {
"paging": false,
"searching": false,
"info": false,
/* "autoWidth" : false, */
"fixedHeader": {
"header": true,
"footer": true
},
"orderCellsTop": true, // cellules ligne 1 pour tri
"aaSorting": [], // Prevent initial sorting
};
$('table#formation_table_recap').DataTable(table_options);
let table_editable = document.querySelector("table#formation_table_recap.apo_editable");
if (table_editable) {
let apo_ue_save_url = document.querySelector("table#formation_table_recap.apo_editable").dataset.apo_ue_save_url;
apo_ue_editor = new ScoFieldEditor("table#formation_table_recap tr.ue td.apo", apo_ue_save_url, false);
let apo_mod_save_url = document.querySelector("table#formation_table_recap.apo_editable").dataset.apo_mod_save_url;
apo_mod_editor = new ScoFieldEditor("table#formation_table_recap tr.mod td.apo", apo_mod_save_url, false);
}
var table_options = {
paging: false,
searching: false,
info: false,
/* "autoWidth" : false, */
fixedHeader: {
header: true,
footer: true,
},
orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting
};
$("table#formation_table_recap").DataTable(table_options);
let table_editable = document.querySelector(
"table#formation_table_recap.apo_editable"
);
if (table_editable) {
let apo_ue_save_url = document.querySelector(
"table#formation_table_recap.apo_editable"
).dataset.apo_ue_save_url;
apo_ue_editor = new ScoFieldEditor(
"table#formation_table_recap tr.ue td.apo",
apo_ue_save_url,
false
);
let apo_mod_save_url = document.querySelector(
"table#formation_table_recap.apo_editable"
).dataset.apo_mod_save_url;
apo_mod_editor = new ScoFieldEditor(
"table#formation_table_recap tr.mod td.apo",
apo_mod_save_url,
false
);
}
});

View File

@ -1,14 +1,13 @@
// Formulaire formsemestre_createwithmodules
function change_semestre_id() {
var semestre_id = $("#tf_semestre_id")[0].value;
for (var i = -1; i < 12; i++) {
$(".sem" + i).hide();
}
$(".sem" + semestre_id).show();
var semestre_id = $("#tf_semestre_id")[0].value;
for (var i = -1; i < 12; i++) {
$(".sem" + i).hide();
}
$(".sem" + semestre_id).show();
}
$(window).on('load', function () {
change_semestre_id();
});
$(window).on("load", function () {
change_semestre_id();
});

View File

@ -1,81 +1,83 @@
function compute_moyenne() {
var notes = $(".tf_field_note input").map(
function () { return parseFloat($(this).val()); }
).get();
// les coefs sont donnes (ECTS en BUT)
let coefs = $("form.tf_ext_edit_ue_validations").data("ue_coefs");
// ou saisis (formations classiques)
if (coefs == 'undefined') {
coefs = $(".tf_field_coef input").map(
function () { return parseFloat($(this).val()); }
).get();
var notes = $(".tf_field_note input")
.map(function () {
return parseFloat($(this).val());
})
.get();
// les coefs sont donnes (ECTS en BUT)
let coefs = $("form.tf_ext_edit_ue_validations").data("ue_coefs");
// ou saisis (formations classiques)
if (coefs == "undefined") {
coefs = $(".tf_field_coef input")
.map(function () {
return parseFloat($(this).val());
})
.get();
}
var N = notes.length;
var dp = 0;
var sum_coefs = 0;
for (var i = 0; i < N; i++) {
if (!(isNaN(notes[i]) || isNaN(coefs[i]))) {
dp += notes[i] * coefs[i];
sum_coefs += coefs[i];
}
var N = notes.length;
var dp = 0.;
var sum_coefs = 0.;
for (var i = 0; i < N; i++) {
if (!(isNaN(notes[i]) || isNaN(coefs[i]))) {
dp += notes[i] * coefs[i];
sum_coefs += coefs[i];
}
}
let moy = dp / sum_coefs;
if (isNaN(moy)) {
moy = "-";
}
if (typeof moy == "number") {
moy = moy.toFixed(2);
}
return moy;
}
let moy = dp / sum_coefs;
if (isNaN(moy)) {
moy = "-";
}
if (typeof moy == "number") {
moy = moy.toFixed(2);
}
return moy;
}
// Callback select menu (UE code)
function enable_disable_fields_cb() {
enable_disable_fields(this);
enable_disable_fields(this);
}
function enable_disable_fields(select_elt) {
// input fields controled by this menu
var input_fields = $(select_elt).parent().parent().find('input:not(.ext_coef_disabled)');
var disabled = false;
if ($(select_elt).val() === "None") {
disabled = true;
// input fields controled by this menu
var input_fields = $(select_elt)
.parent()
.parent()
.find("input:not(.ext_coef_disabled)");
var disabled = false;
if ($(select_elt).val() === "None") {
disabled = true;
}
input_fields.each(function () {
if (disabled) {
let cur_value = $(this).val();
$(this).data("saved-value", cur_value);
$(this).val("");
} else {
let saved_value = $(this).data("saved-value");
if (typeof saved_value == "undefined") {
saved_value = "";
}
if (saved_value) {
$(this).val(saved_value);
}
}
input_fields.each(function () {
if (disabled) {
let cur_value = $(this).val();
$(this).data('saved-value', cur_value);
$(this).val("");
} else {
let saved_value = $(this).data('saved-value');
if (typeof saved_value == 'undefined') {
saved_value = '';
}
if (saved_value) {
$(this).val(saved_value);
}
}
});
input_fields.prop('disabled', disabled);
});
input_fields.prop("disabled", disabled);
}
function setup_text_fields() {
$(".ueext_valid_select").each(
function () {
enable_disable_fields(this);
}
);
$(".ueext_valid_select").each(function () {
enable_disable_fields(this);
});
}
$().ready(function () {
$(".tf_ext_edit_ue_validations").change(function () {
$(".ext_sem_moy_val")[0].innerHTML = compute_moyenne();
});
$("form.tf_ext_edit_ue_validations input").blur(function () {
$(".ext_sem_moy_val")[0].innerHTML = compute_moyenne();
});
$(".ueext_valid_select").change(enable_disable_fields_cb);
$(".tf_ext_edit_ue_validations").change(function () {
$(".ext_sem_moy_val")[0].innerHTML = compute_moyenne();
});
$("form.tf_ext_edit_ue_validations input").blur(function () {
$(".ext_sem_moy_val")[0].innerHTML = compute_moyenne();
});
$(".ueext_valid_select").change(enable_disable_fields_cb);
setup_text_fields();
setup_text_fields();
});

View File

@ -1,186 +1,213 @@
// Affichage progressif du trombinoscope html
$().ready(function () {
var spans = $(".unloaded_img");
for (var i = 0; i < spans.length; i++) {
var sp = spans[i];
var etudid = sp.id;
$(sp).load(SCO_URL + '/etud_photo_html?etudid=' + etudid);
}
var spans = $(".unloaded_img");
for (var i = 0; i < spans.length; i++) {
var sp = spans[i];
var etudid = sp.id;
$(sp).load(SCO_URL + "/etud_photo_html?etudid=" + etudid);
}
});
// L'URL pour recharger l'état courant de la page (groupes et tab selectionnes)
// (ne fonctionne que pour les requetes GET: manipule la query string)
function groups_view_url() {
var url = $.url();
delete url.param()['group_ids']; // retire anciens groupes de l'URL
delete url.param()['curtab']; // retire ancien tab actif
if (CURRENT_TAB_HASH) {
url.param()['curtab'] = CURRENT_TAB_HASH;
}
delete url.param()['formsemestre_id'];
url.param()['formsemestre_id'] = $("#group_selector")[0].formsemestre_id.value;
var url = $.url();
delete url.param()["group_ids"]; // retire anciens groupes de l'URL
delete url.param()["curtab"]; // retire ancien tab actif
if (CURRENT_TAB_HASH) {
url.param()["curtab"] = CURRENT_TAB_HASH;
}
delete url.param()["formsemestre_id"];
url.param()["formsemestre_id"] =
$("#group_selector")[0].formsemestre_id.value;
var selected_groups = $("#group_selector select").val();
url.param()['group_ids'] = selected_groups; // remplace par groupes selectionnes
var selected_groups = $("#group_selector select").val();
url.param()["group_ids"] = selected_groups; // remplace par groupes selectionnes
return url;
return url;
}
// Selectionne tous les etudiants et recharge la page:
function select_tous() {
var url = groups_view_url();
var default_group_id = $("#group_selector")[0].default_group_id.value;
delete url.param()['group_ids'];
url.param()['group_ids'] = [default_group_id];
// Sélectionne le groupe "tous" et recharge la page:
function select_groupe_tous() {
var url = groups_view_url();
var default_group_id = $("#group_selector")[0].default_group_id.value;
delete url.param()["group_ids"];
url.param()["group_ids"] = [default_group_id];
var query_string = $.param(url.param(), traditional = true);
window.location = url.attr('base') + url.attr('path') + '?' + query_string;
var query_string = $.param(url.param(), (traditional = true));
window.location = url.attr("base") + url.attr("path") + "?" + query_string;
}
// Recharge la page sans arguments group_ids
function remove_group_filter() {
var url = groups_view_url();
delete url.param()["group_ids"];
var query_string = $.param(url.param(), (traditional = true));
window.location = url.attr("base") + url.attr("path") + "?" + query_string;
}
// L'URL pour l'état courant de la page:
function get_current_url() {
var url = groups_view_url();
var query_string = $.param(url.param(), traditional = true);
return url.attr('base') + url.attr('path') + '?' + query_string;
var url = groups_view_url();
var query_string = $.param(url.param(), (traditional = true));
return url.attr("base") + url.attr("path") + "?" + query_string;
}
// Recharge la page en changeant les groupes selectionnés et en conservant le tab actif:
function submit_group_selector() {
window.location = get_current_url();
window.location = get_current_url();
}
function show_current_tab() {
$('.nav-tabs [href="#' + CURRENT_TAB_HASH + '"]').tab('show');
if (document.getElementsByClassName("nav-tabs").length > 0) {
$('.nav-tabs [href="#' + CURRENT_TAB_HASH + '"]').tab("show");
}
}
var CURRENT_TAB_HASH = $.url().param()['curtab'];
var CURRENT_TAB_HASH = $.url().param()["curtab"];
$().ready(function () {
$('.nav-tabs a').on('shown.bs.tab', function (e) {
CURRENT_TAB_HASH = e.target.hash.slice(1); // sans le #
});
$(".nav-tabs a").on("shown.bs.tab", function (e) {
CURRENT_TAB_HASH = e.target.hash.slice(1); // sans le #
});
show_current_tab();
show_current_tab();
});
function change_list_options() {
var url = groups_view_url();
var selected_options = $("#group_list_options").val();
var options = ["with_paiement", "with_archives", "with_annotations", "with_codes", "with_bourse"];
for (var i = 0; i < options.length; i++) {
var option = options[i];
delete url.param()[option];
if ($.inArray(option, selected_options) >= 0) {
url.param()[option] = 1;
}
var url = groups_view_url();
var selected_options = $("#group_list_options").val();
var options = [
"with_paiement",
"with_archives",
"with_annotations",
"with_codes",
"with_bourse",
];
for (var i = 0; i < options.length; i++) {
var option = options[i];
delete url.param()[option];
if ($.inArray(option, selected_options) >= 0) {
url.param()[option] = 1;
}
var query_string = $.param(url.param(), traditional = true);
window.location = url.attr('base') + url.attr('path') + '?' + query_string;
}
var query_string = $.param(url.param(), (traditional = true));
window.location = url.attr("base") + url.attr("path") + "?" + query_string;
}
// Menu choix groupe:
function toggle_visible_etuds() {
//
$(".etud_elem").hide();
var qargs = "";
$("#group_ids_sel option:selected").each(function (index, opt) {
var group_id = opt.value;
$(".group-" + group_id).show();
qargs += "&group_ids=" + group_id;
});
// Update url saisie tableur:
var input_eval = $("#formnotes_evaluation_id");
if (input_eval.length > 0) {
var evaluation_id = input_eval[0].value;
$("#menu_saisie_tableur a").attr("href", "saisie_notes_tableur?evaluation_id=" + evaluation_id + qargs);
// lien feuille excel:
$("#lnk_feuille_saisie").attr("href", "feuille_saisie_notes?evaluation_id=" + evaluation_id + qargs);
}
// Update champs form group_ids_str
let group_ids_str = Array.from(
document.querySelectorAll("#group_ids_sel option:checked")
).map(
function (elem) { return elem.value; }
).join();
document.querySelectorAll("input.group_ids_str").forEach(elem => elem.value = group_ids_str);
//
$(".etud_elem").hide();
var qargs = "";
$("#group_ids_sel option:selected").each(function (index, opt) {
var group_id = opt.value;
$(".group-" + group_id).show();
qargs += "&group_ids=" + group_id;
});
// Update url saisie tableur:
var input_eval = $("#formnotes_evaluation_id");
if (input_eval.length > 0) {
var evaluation_id = input_eval[0].value;
$("#menu_saisie_tableur a").attr(
"href",
"saisie_notes_tableur?evaluation_id=" + evaluation_id + qargs
);
// lien feuille excel:
$("#lnk_feuille_saisie").attr(
"href",
"feuille_saisie_notes?evaluation_id=" + evaluation_id + qargs
);
}
// Update champs form group_ids_str
let group_ids_str = Array.from(
document.querySelectorAll("#group_ids_sel option:checked")
)
.map(function (elem) {
return elem.value;
})
.join();
document
.querySelectorAll("input.group_ids_str")
.forEach((elem) => (elem.value = group_ids_str));
}
$().ready(function () {
$('#group_ids_sel').multiselect(
{
includeSelectAllOption: false,
nonSelectedText: 'choisir...',
// buttonContainer: '<div id="group_ids_sel_container"/>',
onChange: function (element, checked) {
if (checked == true) {
var default_group_id = $(".default_group")[0].value;
$("#group_ids_sel").multiselect({
includeSelectAllOption: false,
nonSelectedText: "choisir...",
// buttonContainer: '<div id="group_ids_sel_container"/>',
onChange: function (element, checked) {
// Gestion du groupe "tous"
if (
checked == true &&
$("#group_ids_sel").hasClass("default_deselect_others")
) {
var default_group_id = $(".default_group")[0].value;
if (element.hasClass("default_group")) {
// click sur groupe "tous"
// deselectionne les autres
$("#group_ids_sel option:selected").each(function (index, opt) {
if (opt.value != default_group_id) {
$("#group_ids_sel").multiselect('deselect', opt.value);
}
});
} else {
// click sur un autre item
// si le groupe "tous" est selectionne et que l'on coche un autre, le deselectionner
var default_is_selected = false;
$("#group_ids_sel option:selected").each(function (index, opt) {
if (opt.value == default_group_id) {
default_is_selected = true;
return false;
}
});
if (default_is_selected) {
$("#group_ids_sel").multiselect('deselect', default_group_id);
}
}
}
toggle_visible_etuds();
// referme le menu apres chaque choix:
$("#group_selector .btn-group").removeClass('open');
if ($("#group_ids_sel").hasClass("submit_on_change")) {
submit_group_selector();
}
if (element.hasClass("default_group")) {
// click sur groupe "tous"
// deselectionne les autres
$("#group_ids_sel option:selected").each(function (index, opt) {
if (opt.value != default_group_id) {
$("#group_ids_sel").multiselect("deselect", opt.value);
}
});
} else {
// click sur un autre item
// si le groupe "tous" est selectionne et que l'on coche un autre, le deselectionner
var default_is_selected = false;
$("#group_ids_sel option:selected").each(function (index, opt) {
if (opt.value == default_group_id) {
default_is_selected = true;
return false;
}
});
if (default_is_selected) {
$("#group_ids_sel").multiselect("deselect", default_group_id);
}
}
);
}
// initial setup
toggle_visible_etuds();
toggle_visible_etuds();
// referme le menu apres chaque choix:
$("#group_selector .btn-group").removeClass("open");
if ($("#group_ids_sel").hasClass("submit_on_change")) {
submit_group_selector();
}
},
});
// initial setup
toggle_visible_etuds();
});
// Trombinoscope
$().ready(function () {
var elems = $(".trombi-photo");
for (var i = 0; i < elems.length; i++) {
$(elems[i]).qtip(
{
content: {
ajax: {
url: SCO_URL + "/etud_info_html?with_photo=0&etudid=" + get_etudid_from_elem(elems[i])
},
text: "Loading..."
},
position: {
at: "right",
my: "left top"
},
style: {
classes: 'qtip-etud'
},
// utile pour debugguer le css:
// hide: { event: 'unfocus' }
}
);
}
var elems = $(".trombi-photo");
for (var i = 0; i < elems.length; i++) {
$(elems[i]).qtip({
content: {
ajax: {
url:
SCO_URL +
"/etud_info_html?with_photo=0&etudid=" +
get_etudid_from_elem(elems[i]),
},
text: "Loading...",
},
position: {
at: "right",
my: "left top",
},
style: {
classes: "qtip-etud",
},
// utile pour debugguer le css:
// hide: { event: 'unfocus' }
});
}
});

View File

@ -1,88 +1,90 @@
// active les menus des codes "manuels" (année, RCUEs)
function enable_manual_codes(elt) {
$(".jury_but select.manual").prop("disabled", !elt.checked);
$(".jury_but select.manual").prop("disabled", !elt.checked);
}
// changement d'un menu code:
function change_menu_code(elt) {
// Ajuste styles pour visualiser codes enregistrés/modifiés
if (elt.value != elt.dataset.orig_code) {
elt.parentElement.parentElement.classList.add("modified");
} else {
elt.parentElement.parentElement.classList.remove("modified");
}
if (elt.value == elt.dataset.orig_recorded) {
elt.parentElement.parentElement.classList.add("recorded");
} else {
elt.parentElement.parentElement.classList.remove("recorded");
}
// Si RCUE passant en ADJ, change les menus des UEs associées ADJR
if (elt.classList.contains("code_rcue")
&& elt.dataset.niveau_id
&& elt.value == "ADJ"
&& elt.value != elt.dataset.orig_recorded) {
let ue_selects = elt.parentElement.parentElement.parentElement.querySelectorAll(
"select.ue_rcue_" + elt.dataset.niveau_id);
ue_selects.forEach(select => {
if (select.value != "ADM") {
select.value = "ADJR";
change_menu_code(select); // pour changer les styles
}
});
}
// Ajuste styles pour visualiser codes enregistrés/modifiés
if (elt.value != elt.dataset.orig_code) {
elt.parentElement.parentElement.classList.add("modified");
} else {
elt.parentElement.parentElement.classList.remove("modified");
}
if (elt.value == elt.dataset.orig_recorded) {
elt.parentElement.parentElement.classList.add("recorded");
} else {
elt.parentElement.parentElement.classList.remove("recorded");
}
// Si RCUE passant en ADJ, change les menus des UEs associées ADJR
if (
elt.classList.contains("code_rcue") &&
elt.dataset.niveau_id &&
elt.value == "ADJ" &&
elt.value != elt.dataset.orig_recorded
) {
let ue_selects =
elt.parentElement.parentElement.parentElement.querySelectorAll(
"select.ue_rcue_" + elt.dataset.niveau_id
);
ue_selects.forEach((select) => {
if (select.value != "ADM") {
select.value = "ADJR";
change_menu_code(select); // pour changer les styles
}
});
}
}
$(function () {
// Recupère la liste ordonnées des etudids
// pour avoir le "suivant" et le "précédent"
// (liens de navigation)
const url = new URL(document.URL);
const frags = url.pathname.split("/"); // .../formsemestre_validation_but/formsemestre_id/etudid
const etudid = frags[frags.length - 1];
const formsemestre_id = frags[frags.length - 2];
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
const etudids_str = localStorage.getItem(etudids_key);
const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
const noms_str = localStorage.getItem(noms_key);
if (etudids_str && noms_str) {
const etudids = JSON.parse(etudids_str);
const noms = JSON.parse(noms_str);
const cur_idx = etudids.indexOf(etudid);
let prev_idx = -1;
let next_idx = -1
if (cur_idx != -1) {
if (cur_idx > 0) {
prev_idx = cur_idx - 1;
}
if (cur_idx < etudids.length - 1) {
next_idx = cur_idx + 1;
}
}
if (prev_idx != -1) {
let elem = document.querySelector("div.prev a");
if (elem) {
elem.href = elem.href.replace("PREV", etudids[prev_idx]);
elem.innerHTML = noms[prev_idx];
}
} else {
document.querySelector("div.prev").innerHTML = "";
}
if (next_idx != -1) {
let elem = document.querySelector("div.next a");
if (elem) {
elem.href = elem.href.replace("NEXT", etudids[next_idx]);
elem.innerHTML = noms[next_idx];
}
} else {
document.querySelector("div.next").innerHTML = "";
}
} else {
// Supprime les liens de navigation
document.querySelector("div.prev").innerHTML = "";
document.querySelector("div.next").innerHTML = "";
// Recupère la liste ordonnées des etudids
// pour avoir le "suivant" et le "précédent"
// (liens de navigation)
const url = new URL(document.URL);
const frags = url.pathname.split("/"); // .../formsemestre_validation_but/formsemestre_id/etudid
const etudid = frags[frags.length - 1];
const formsemestre_id = frags[frags.length - 2];
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
const etudids_str = localStorage.getItem(etudids_key);
const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
const noms_str = localStorage.getItem(noms_key);
if (etudids_str && noms_str) {
const etudids = JSON.parse(etudids_str);
const noms = JSON.parse(noms_str);
const cur_idx = etudids.indexOf(etudid);
let prev_idx = -1;
let next_idx = -1;
if (cur_idx != -1) {
if (cur_idx > 0) {
prev_idx = cur_idx - 1;
}
if (cur_idx < etudids.length - 1) {
next_idx = cur_idx + 1;
}
}
if (prev_idx != -1) {
let elem = document.querySelector("div.prev a");
if (elem) {
elem.href = elem.href.replace("PREV", etudids[prev_idx]);
elem.innerHTML = noms[prev_idx];
}
} else {
document.querySelector("div.prev").innerHTML = "";
}
if (next_idx != -1) {
let elem = document.querySelector("div.next a");
if (elem) {
elem.href = elem.href.replace("NEXT", etudids[next_idx]);
elem.innerHTML = noms[next_idx];
}
} else {
document.querySelector("div.next").innerHTML = "";
}
} else {
// Supprime les liens de navigation
document.querySelector("div.prev").innerHTML = "";
document.querySelector("div.next").innerHTML = "";
}
});
// ----- Etat du formulaire jury pour éviter sortie sans enregistrer
@ -91,29 +93,31 @@ let IS_SUBMITTING = false;
// Une chaine décrivant l'état du form
function get_form_state() {
let codes = [];
// il n'y a que des <select>
document.querySelectorAll("select").forEach(sel => codes.push(sel.value));
return codes.join();
let codes = [];
// il n'y a que des <select>
document.querySelectorAll("select").forEach((sel) => codes.push(sel.value));
return codes.join();
}
$('document').ready(function () {
FORM_STATE = get_form_state();
document.querySelector("form#jury_but").addEventListener('submit', jury_form_submit);
$("document").ready(function () {
FORM_STATE = get_form_state();
document
.querySelector("form#jury_but")
.addEventListener("submit", jury_form_submit);
});
function is_modified() {
return FORM_STATE != get_form_state();
return FORM_STATE != get_form_state();
}
function jury_form_submit(event) {
IS_SUBMITTING = true;
IS_SUBMITTING = true;
}
window.addEventListener("beforeunload", function (e) {
if ((!IS_SUBMITTING) && is_modified()) {
var confirmationMessage = 'Changements non enregistrés !';
(e || window.event).returnValue = confirmationMessage;
return confirmationMessage;
}
if (!IS_SUBMITTING && is_modified()) {
var confirmationMessage = "Changements non enregistrés !";
(e || window.event).returnValue = confirmationMessage;
return confirmationMessage;
}
});

View File

@ -2,33 +2,42 @@
var ScoMarkerIcons = {};
$().ready(function(){
$('#lyc_map_canvas').gmap(
{ 'center': '48.955741,2.34141',
'zoom' : 8,
'mapTypeId': google.maps.MapTypeId.ROADMAP
}).bind('init', function(event, map) {
for (var i =0; i < lycees_coords.length; i++) {
var lycee = lycees_coords[i];
var nb = lycee['number'];
var icon;
if (nb in ScoMarkerIcons) {
icon = ScoMarkerIcons[nb];
} else {
icon = new google.maps.MarkerImage( 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=' + nb + '|FF0000|000000' );
ScoMarkerIcons[nb] = icon; // cache
}
$('#lyc_map_canvas').gmap(
'addMarker',
{'position': lycee['position'], 'bounds': true, 'nomlycee' : lycee['name'], 'icon' : icon }
).click(
function() {
$('#lyc_map_canvas').gmap('openInfoWindow', {'content': this.nomlycee}, this);
}
);
}
});
$().ready(function () {
$("#lyc_map_canvas")
.gmap({
center: "48.955741,2.34141",
zoom: 8,
mapTypeId: google.maps.MapTypeId.ROADMAP,
})
.bind("init", function (event, map) {
for (var i = 0; i < lycees_coords.length; i++) {
var lycee = lycees_coords[i];
var nb = lycee["number"];
var icon;
if (nb in ScoMarkerIcons) {
icon = ScoMarkerIcons[nb];
} else {
icon = new google.maps.MarkerImage(
"https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=" +
nb +
"|FF0000|000000"
);
ScoMarkerIcons[nb] = icon; // cache
}
$("#lyc_map_canvas")
.gmap("addMarker", {
position: lycee["position"],
bounds: true,
nomlycee: lycee["name"],
icon: icon,
})
.click(function () {
$("#lyc_map_canvas").gmap(
"openInfoWindow",
{ content: this.nomlycee },
this
);
});
}
});
});

View File

@ -1,8 +1,3 @@
/* Page édition module */
$(document).ready(function () {
});
$(document).ready(function () {});

View File

@ -1,35 +1,31 @@
// Edition tags sur modules
$(function () {
$(".module_tag_editor").tagEditor({
initialTags: "",
placeholder: "Tags du module ...",
forceLowercase: false,
onChange: function (field, editor, tags) {
$.post("module_tag_set", {
module_id: field.data("module_id"),
taglist: tags.join(),
});
},
autocomplete: {
delay: 200, // ms before suggest
position: { collision: "flip" }, // automatic menu position up/down
source: "module_tag_search",
},
});
$(function() {
$('.module_tag_editor').tagEditor({
initialTags: '',
placeholder: 'Tags du module ...',
forceLowercase: false,
onChange: function(field, editor, tags) {
$.post('module_tag_set',
{
module_id: field.data("module_id"),
taglist: tags.join()
});
},
autocomplete: {
delay: 200, // ms before suggest
position: { collision: 'flip' }, // automatic menu position up/down
source: "module_tag_search"
},
});
// version readonly
readOnlyTags($('.module_tag_editor_ro'));
$('.sco_tag_checkbox').click(function() {
if( $(this).is(':checked')) {
$(".sco_tag_edit").show();
} else {
$(".sco_tag_edit").hide();
}
});
// version readonly
readOnlyTags($(".module_tag_editor_ro"));
$(".sco_tag_checkbox").click(function () {
if ($(this).is(":checked")) {
$(".sco_tag_edit").show();
} else {
$(".sco_tag_edit").hide();
}
});
});

View File

@ -2,36 +2,31 @@
*/
function change_ue_inscr(elt) {
let url = "";
if (elt.checked) {
url = elt.dataset.url_inscr;
} else {
url = elt.dataset.url_desinscr;
}
$.post(url,
{},
function (result) {
sco_message("changement inscription UE enregistré");
}
);
let url = "";
if (elt.checked) {
url = elt.dataset.url_inscr;
} else {
url = elt.dataset.url_desinscr;
}
$.post(url, {}, function (result) {
sco_message("changement inscription UE enregistré");
});
}
$(function () {
$("table#but_ue_inscriptions").DataTable(
{
paging: false,
searching: true,
info: false,
autoWidth: false,
fixedHeader: {
header: true,
footer: false
},
orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting
"oLanguage": {
"sSearch": "Chercher :"
}
}
);
$("table#but_ue_inscriptions").DataTable({
paging: false,
searching: true,
info: false,
autoWidth: false,
fixedHeader: {
header: true,
footer: false,
},
orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting
oLanguage: {
sSearch: "Chercher :",
},
});
});

View File

@ -25,7 +25,7 @@ var NB_TICS = R_TICS.length;
function get_notes_and_draw(formsemestre_id, etudid) {
console.log("get_notes(" + formsemestre_id + ", " + etudid + " )");
/* Recupère le bulletin de note et extrait tableau de notes */
/*
/*
var notes = [
{ 'module' : 'E1',
'note' : 13,

View File

@ -1,59 +1,67 @@
// Affichage parcours etudiant
// (uses jQuery)
function toggle_vis(e, new_state) { // change visibility of tr (UE in tr and next tr)
// e is the span containg the clicked +/- icon
var formsemestre_class = e.classList[1];
var tr = e.parentNode.parentNode;
if (new_state == undefined) {
// current state: use alt attribute of current image
if (e.childNodes[0].alt == '+') {
new_state=false;
} else {
new_state=true;
}
}
if (new_state) {
new_tr_display = 'none';
function toggle_vis(e, new_state) {
// change visibility of tr (UE in tr and next tr)
// e is the span containg the clicked +/- icon
var formsemestre_class = e.classList[1];
var tr = e.parentNode.parentNode;
if (new_state == undefined) {
// current state: use alt attribute of current image
if (e.childNodes[0].alt == "+") {
new_state = false;
} else {
new_tr_display = 'table-row';
new_state = true;
}
$("tr."+formsemestre_class+":not(.rcp_l1)").css('display', new_tr_display)
// find next tr in siblings (xxx legacy code, could be optimized)
var sibl = tr.nextSibling;
while ((sibl != null) && sibl.nodeType != 1 && sibl.tagName != 'TR') {
sibl = sibl.nextSibling;
}
if (new_state) {
new_tr_display = "none";
} else {
new_tr_display = "table-row";
}
$("tr." + formsemestre_class + ":not(.rcp_l1)").css(
"display",
new_tr_display
);
// find next tr in siblings (xxx legacy code, could be optimized)
var sibl = tr.nextSibling;
while (sibl != null && sibl.nodeType != 1 && sibl.tagName != "TR") {
sibl = sibl.nextSibling;
}
if (sibl) {
var td_disp = "none";
if (new_state) {
e.innerHTML =
'<img width="13" height="13" border="0" title="" alt="+" src="/ScoDoc/static/icons/plus_img.png"/>';
} else {
e.innerHTML =
'<img width="13" height="13" border="0" title="" alt="-" src="/ScoDoc/static/icons/minus_img.png"/>';
td_disp = "inline";
}
if (sibl) {
var td_disp = 'none';
if (new_state) {
e.innerHTML = '<img width="13" height="13" border="0" title="" alt="+" src="/ScoDoc/static/icons/plus_img.png"/>';
} else {
e.innerHTML = '<img width="13" height="13" border="0" title="" alt="-" src="/ScoDoc/static/icons/minus_img.png"/>';
td_disp = 'inline';
}
// acronymes d'UE
sibl = e.parentNode.nextSibling;
while (sibl != null) {
if (sibl.nodeType == 1 && sibl.className == 'ue_acro')
sibl.childNodes[0].style.display = td_disp;
sibl = sibl.nextSibling;
}
// acronymes d'UE
sibl = e.parentNode.nextSibling;
while (sibl != null) {
if (sibl.nodeType == 1 && sibl.className == "ue_acro")
sibl.childNodes[0].style.display = td_disp;
sibl = sibl.nextSibling;
}
}
}
var sems_state = false;
function toggle_all_sems(e) {
var elems = $("span.toggle_sem");
for (var i=0; i < elems.length; i++) {
toggle_vis(elems[i], sems_state);
}
sems_state = !sems_state;
if (sems_state) {
e.innerHTML = '<img width="18" height="18" border="0" title="" alt="-" src="/ScoDoc/static/icons/minus18_img.png"/>';
} else {
e.innerHTML = '<img width="18" height="18" border="0" title="" alt="+" src="/ScoDoc/static/icons/plus18_img.png"/>';
}
}
var elems = $("span.toggle_sem");
for (var i = 0; i < elems.length; i++) {
toggle_vis(elems[i], sems_state);
}
sems_state = !sems_state;
if (sems_state) {
e.innerHTML =
'<img width="18" height="18" border="0" title="" alt="-" src="/ScoDoc/static/icons/minus18_img.png"/>';
} else {
e.innerHTML =
'<img width="18" height="18" border="0" title="" alt="+" src="/ScoDoc/static/icons/plus18_img.png"/>';
}
}

View File

@ -1,106 +1,131 @@
class ref_competences extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open' });
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
/* Template de base */
this.shadow.innerHTML = `
/* Template de base */
this.shadow.innerHTML = `
<div class=titre>Cliquer sur un parcours pour afficher ses niveaux de compétences</div>
<div class=parcours></div>
<div class=competences></div>
<div class=ACs></div>
`;
/* Style du module */
const styles = document.createElement('link');
styles.setAttribute('rel', 'stylesheet');
styles.setAttribute('href', removeLastTwoComponents(getCurrentScriptPath()) + '/css/ref-competences.css');
/* Style du module */
const styles = document.createElement("link");
styles.setAttribute("rel", "stylesheet");
styles.setAttribute(
"href",
removeLastTwoComponents(getCurrentScriptPath()) +
"/css/ref-competences.css"
);
this.shadow.appendChild(styles);
}
this.shadow.appendChild(styles);
}
set setData(data) {
this.data = data;
this.parcours();
}
set setData(data) {
this.data = data;
this.parcours();
}
parcours() {
let parcoursDIV = this.shadow.querySelector(".parcours");
Object.entries(this.data.parcours).forEach(([cle, parcours]) => {
let div = document.createElement("div");
div.innerHTML = `<a title="${parcours.libelle}">${parcours.code}</a>`;
div.addEventListener("click", (event) => { this.competences(event, cle) })
parcoursDIV.appendChild(div);
})
this.initCompetences();
}
parcours() {
let parcoursDIV = this.shadow.querySelector(".parcours");
Object.entries(this.data.parcours).forEach(([cle, parcours]) => {
let div = document.createElement("div");
div.innerHTML = `<a title="${parcours.libelle}">${parcours.code}</a>`;
div.addEventListener("click", (event) => {
this.competences(event, cle);
});
parcoursDIV.appendChild(div);
});
this.initCompetences();
}
initCompetences() {
this.competencesNumber = {};
let i = 0;
Object.keys(this.data.competences).forEach(competence => {
this.competencesNumber[competence] = 1 + i++ % 6;
})
}
initCompetences() {
this.competencesNumber = {};
let i = 0;
Object.keys(this.data.competences).forEach((competence) => {
this.competencesNumber[competence] = 1 + (i++ % 6);
});
}
competences(event, cle) {
this.shadow.querySelector(".parcours>.focus")?.classList.remove("focus");
event.currentTarget.classList.add("focus");
let divCompetences = this.shadow.querySelector(".competences");
competences(event, cle) {
this.shadow.querySelector(".parcours>.focus")?.classList.remove("focus");
event.currentTarget.classList.add("focus");
let divCompetences = this.shadow.querySelector(".competences");
this.shadow.querySelector(".competences").innerHTML = "";
this.shadow.querySelector(".competences").innerHTML = "";
/* Création des compétences */
let competencesBucket = [];
Object.entries(this.data.parcours[cle].annees).forEach(([annee, dataAnnee]) => {
Object.entries(dataAnnee.competences).forEach(([competence, niveauCle]) => {
let numComp = this.competencesNumber[competence];
let divCompetence = document.createElement("div");
divCompetence.innerText = `${competence} ${niveauCle.niveau}`;
divCompetence.style.gridRowStart = annee;
divCompetence.style.gridColumnStart = competence.replaceAll(" ", "_");
divCompetence.className = "comp" + numComp;
divCompetence.dataset.competence = `${competence} ${niveauCle.niveau}`;
divCompetence.addEventListener("click", (event) => { this.AC(event, competence, niveauCle.niveau, annee, numComp) })
divCompetences.appendChild(divCompetence);
/* Création des compétences */
let competencesBucket = [];
Object.entries(this.data.parcours[cle].annees).forEach(
([annee, dataAnnee]) => {
Object.entries(dataAnnee.competences).forEach(
([competence, niveauCle]) => {
let numComp = this.competencesNumber[competence];
let divCompetence = document.createElement("div");
divCompetence.innerText = `${competence} ${niveauCle.niveau}`;
divCompetence.style.gridRowStart = annee;
divCompetence.style.gridColumnStart = competence.replaceAll(
" ",
"_"
);
divCompetence.className = "comp" + numComp;
divCompetence.dataset.competence = `${competence} ${niveauCle.niveau}`;
divCompetence.addEventListener("click", (event) => {
this.AC(event, competence, niveauCle.niveau, annee, numComp);
});
divCompetences.appendChild(divCompetence);
competencesBucket.push(competence);
})
})
competencesBucket.push(competence);
}
);
}
);
/* Affectation de la taille des éléments */
//divCompetences.style.setProperty("--competence-size", `calc(${100 / competencesBucket.length}% )`);
let gridTemplate = "";
Object.keys(this.data.competences).forEach(competence => {
if (competencesBucket.indexOf(competence) == -1) {
gridTemplate += `[${competence.replaceAll(" ", "_")}] 0`;
} else {
gridTemplate += `[${competence.replaceAll(" ", "_")}] 1fr`;
}
})
this.shadow.querySelector(".competences").style.gridTemplateColumns = gridTemplate;
/* Affectation de la taille des éléments */
//divCompetences.style.setProperty("--competence-size", `calc(${100 / competencesBucket.length}% )`);
let gridTemplate = "";
Object.keys(this.data.competences).forEach((competence) => {
if (competencesBucket.indexOf(competence) == -1) {
gridTemplate += `[${competence.replaceAll(" ", "_")}] 0`;
} else {
gridTemplate += `[${competence.replaceAll(" ", "_")}] 1fr`;
}
});
this.shadow.querySelector(".competences").style.gridTemplateColumns =
gridTemplate;
/* Réaffectation des focus */
this.shadow.querySelectorAll(".AC").forEach(ac => {
this.shadow.querySelector(`[data-competence="${ac.dataset.competence}"]`).classList.add("focus");
});
}
/* Réaffectation des focus */
this.shadow.querySelectorAll(".AC").forEach((ac) => {
this.shadow
.querySelector(`[data-competence="${ac.dataset.competence}"]`)
.classList.add("focus");
});
}
AC(event, competence, niveau, annee, numComp) {
event.currentTarget.classList.toggle("focus");
if (this.shadow.querySelector(`.ACs [data-competence="${competence} ${niveau}"]`)) {
this.shadow.querySelector(`.ACs [data-competence="${competence} ${niveau}"]`).remove();
} else {
let output = `
AC(event, competence, niveau, annee, numComp) {
event.currentTarget.classList.toggle("focus");
if (
this.shadow.querySelector(
`.ACs [data-competence="${competence} ${niveau}"]`
)
) {
this.shadow
.querySelector(`.ACs [data-competence="${competence} ${niveau}"]`)
.remove();
} else {
let output = `
<ul class=AC data-competence="${competence} ${niveau}">
<h2 class=comp${numComp}>${competence} ${niveau}</h2>
`;
Object.entries(this.data.competences[competence].niveaux["BUT" + annee].app_critiques).forEach(([num, contenu]) => {
output += `<li><div class=comp${numComp}>${num}</div><div>${contenu.libelle}</div></li>`;
})
this.shadow.querySelector(".ACs").innerHTML += output + "</ul>";
}
}
Object.entries(
this.data.competences[competence].niveaux["BUT" + annee].app_critiques
).forEach(([num, contenu]) => {
output += `<li><div class=comp${numComp}>${num}</div><div>${contenu.libelle}</div></li>`;
});
this.shadow.querySelector(".ACs").innerHTML += output + "</ul>";
}
}
}
customElements.define('ref-competences', ref_competences);
customElements.define("ref-competences", ref_competences);

View File

@ -74,10 +74,10 @@ class releveBUT extends HTMLElement {
template() {
return `
<div>
<div>
<div class="wait"></div>
<main class="releve">
<!--------------------------------------------------------------------------------------->
<!-- Zone spéciale pour que les IUT puisse ajouter des infos locales sur la passerelle -->
@ -99,7 +99,7 @@ class releveBUT extends HTMLElement {
<em>Les moyennes ci-dessus servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em>
</div>
</div>
</section>
<!--------------------------->
@ -181,7 +181,7 @@ class releveBUT extends HTMLElement {
output += `
</div>
<div class=numerosEtudiant>
Numéro étudiant : ${data.etudiant.code_nip || "~"} -
Numéro étudiant : ${data.etudiant.code_nip || "~"} -
Code INE : ${data.etudiant.code_ine || "~"}
</div>
<div>${data.formation.titre}</div>
@ -376,12 +376,14 @@ class releveBUT extends HTMLElement {
}</div>
<div class=info>`;
if (!dataUE.date_capitalisation) {
output += ` Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;- `;
if(dataUE.malus >= 0) {
output += `Malus&nbsp;:&nbsp;${dataUE.malus || 0}`;
} else {
output += `Bonus&nbsp;complémentaire&nbsp;:&nbsp;${-dataUE.malus || 0}`;
}
output += ` Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;- `;
if (dataUE.malus >= 0) {
output += `Malus&nbsp;:&nbsp;${dataUE.malus || 0}`;
} else {
output += `Bonus&nbsp;complémentaire&nbsp;:&nbsp;${
-dataUE.malus || 0
}`;
}
} else {
output += ` le ${this.ISOToDate(
dataUE.date_capitalisation.split("T")[0]
@ -468,9 +470,9 @@ class releveBUT extends HTMLElement {
content.moyenne.value
}</div>
<div class=info>
Classe&nbsp;:&nbsp;${content.moyenne.moy}&nbsp;-
Classe&nbsp;:&nbsp;${content.moyenne.moy}&nbsp;-
Max&nbsp;:&nbsp;${content.moyenne.max}&nbsp;-
Min&nbsp;:&nbsp;${content.moyenne.min}
Min&nbsp;:&nbsp;${content.moyenne.min}
</div>
</div>
<div class=absences>

View File

@ -1,33 +1,28 @@
// Gestion formulaire UE externes
function toggle_new_ue_form(state) {
// active/desactive le formulaire "nouvelle UE"
var text_color;
if (state) {
text_color = 'rgb(180,160,160)';
} else {
text_color = 'rgb(0,0,0)';
}
// active/desactive le formulaire "nouvelle UE"
var text_color;
if (state) {
text_color = "rgb(180,160,160)";
} else {
text_color = "rgb(0,0,0)";
}
$("#tf_extue_titre td:eq(1) input").prop("disabled", state);
$("#tf_extue_titre").css('color', text_color)
$("#tf_extue_titre td:eq(1) input").prop("disabled", state);
$("#tf_extue_titre").css("color", text_color);
$("#tf_extue_acronyme td:eq(1) input").prop("disabled", state);
$("#tf_extue_acronyme").css('color', text_color)
$("#tf_extue_acronyme td:eq(1) input").prop("disabled", state);
$("#tf_extue_acronyme").css("color", text_color);
$("#tf_extue_type td:eq(1) select").prop("disabled", state);
$("#tf_extue_type").css('color', text_color)
$("#tf_extue_type td:eq(1) select").prop("disabled", state);
$("#tf_extue_type").css("color", text_color);
$("#tf_extue_ects td:eq(1) input").prop("disabled", state);
$("#tf_extue_ects").css('color', text_color)
$("#tf_extue_ects td:eq(1) input").prop("disabled", state);
$("#tf_extue_ects").css("color", text_color);
}
function update_external_ue_form() {
var state = (tf.existing_ue.value != "");
toggle_new_ue_form(state);
var state = tf.existing_ue.value != "";
toggle_new_ue_form(state);
}

View File

@ -4,30 +4,35 @@ var elt_annee_apo_editor = null;
var elt_sem_apo_editor = null;
$(document).ready(function () {
var table_options = {
"paging": false,
"searching": false,
"info": false,
/* "autoWidth" : false, */
"fixedHeader": {
"header": true,
"footer": true
},
"orderCellsTop": true, // cellules ligne 1 pour tri
"aaSorting": [], // Prevent initial sorting
};
$('table.semlist').DataTable(table_options);
let table_editable = document.querySelector("table#semlist.apo_editable");
if (table_editable) {
let save_url = document.querySelector("table#semlist.apo_editable").dataset.apo_save_url;
apo_editor = new ScoFieldEditor(".etapes_apo_str", save_url, false);
var table_options = {
paging: false,
searching: false,
info: false,
/* "autoWidth" : false, */
fixedHeader: {
header: true,
footer: true,
},
orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting
};
$("table.semlist").DataTable(table_options);
let table_editable = document.querySelector("table#semlist.apo_editable");
if (table_editable) {
let save_url = document.querySelector("table#semlist.apo_editable").dataset
.apo_save_url;
apo_editor = new ScoFieldEditor(".etapes_apo_str", save_url, false);
save_url = document.querySelector("table#semlist.apo_editable").dataset.elt_annee_apo_save_url;
elt_annee_apo_editor = new ScoFieldEditor(".elt_annee_apo", save_url, false);
save_url = document.querySelector("table#semlist.apo_editable").dataset
.elt_annee_apo_save_url;
elt_annee_apo_editor = new ScoFieldEditor(
".elt_annee_apo",
save_url,
false
);
save_url = document.querySelector("table#semlist.apo_editable").dataset.elt_sem_apo_save_url;
elt_sem_apo_editor = new ScoFieldEditor(".elt_sem_apo", save_url, false);
}
save_url = document.querySelector("table#semlist.apo_editable").dataset
.elt_sem_apo_save_url;
elt_sem_apo_editor = new ScoFieldEditor(".elt_sem_apo", save_url, false);
}
});

View File

@ -8,14 +8,14 @@ let lastX;
let lastY;
function build_table(data) {
let output = "";
let sumsUE = {};
let sumsRessources = {};
let value;
let output = "";
let sumsUE = {};
let sumsRessources = {};
let value;
data.forEach((cellule) => {
output += `
<div
data.forEach((cellule) => {
output += `
<div
class="${cellule.style || ""}"
data-editable="${cellule.editable || "false"}"
data-module_id="${cellule.module_id}"
@ -34,31 +34,31 @@ function build_table(data) {
--nbY: ${cellule.nbY || 1};
">${cellule.data}</div>`; // ne pas mettre d'espace car c'est utilisé par :not(:empty) après
if (cellule.style.includes("champs")) {
if (cellule.editable == true && cellule.data) {
value = parseFloat(cellule.data) * 100;
} else {
value = 0;
}
sumsRessources[cellule.y] = (sumsRessources[cellule.y] ?? 0) + value;
sumsUE[cellule.x] = (sumsUE[cellule.x] ?? 0) + value;
}
})
if (cellule.style.includes("champs")) {
if (cellule.editable == true && cellule.data) {
value = parseFloat(cellule.data) * 100;
} else {
value = 0;
}
sumsRessources[cellule.y] = (sumsRessources[cellule.y] ?? 0) + value;
sumsUE[cellule.x] = (sumsUE[cellule.x] ?? 0) + value;
}
});
output += showSums(sumsRessources, sumsUE);
document.querySelector(".tableau").innerHTML = output;
installListeners();
output += showSums(sumsRessources, sumsUE);
document.querySelector(".tableau").innerHTML = output;
installListeners();
}
function showSums(sumsRessources, sumsUE) {
lastX = Object.keys(sumsUE).length + 2;
lastY = Object.keys(sumsRessources).length + 2;
lastX = Object.keys(sumsUE).length + 2;
lastY = Object.keys(sumsRessources).length + 2;
let output = "";
let output = "";
Object.entries(sumsUE).forEach(([num, value]) => {
output += `
<div
Object.entries(sumsUE).forEach(([num, value]) => {
output += `
<div
class="sums"
data-editable="false"
data-x="${num}"
@ -71,11 +71,11 @@ function showSums(sumsRessources, sumsUE) {
">
${value / 100}
</div>`;
})
});
Object.entries(sumsRessources).forEach(([num, value]) => {
output += `
<div
Object.entries(sumsRessources).forEach(([num, value]) => {
output += `
<div
class="sums"
data-editable="false"
data-x="${lastX}"
@ -88,9 +88,9 @@ function showSums(sumsRessources, sumsUE) {
">
${value / 100}
</div>`;
})
});
return output;
return output;
}
/*****************************/
@ -98,125 +98,158 @@ function showSums(sumsRessources, sumsUE) {
/*****************************/
function installListeners() {
if (read_only) {
return;
}
document.body.addEventListener("keydown", key);
document.querySelectorAll("[data-editable=true]").forEach(cellule => {
cellule.addEventListener("click", function () { selectCell(this) });
cellule.addEventListener("dblclick", function () { modifCell(this) });
cellule.addEventListener("blur", function () {
let currentModif = document.querySelector(".modifying");
if (currentModif) {
if (!save(currentModif)) {
return;
}
}
});
cellule.addEventListener("input", processSums);
if (read_only) {
return;
}
document.body.addEventListener("keydown", key);
document.querySelectorAll("[data-editable=true]").forEach((cellule) => {
cellule.addEventListener("click", function () {
selectCell(this);
});
cellule.addEventListener("dblclick", function () {
modifCell(this);
});
cellule.addEventListener("blur", function () {
let currentModif = document.querySelector(".modifying");
if (currentModif) {
if (!save(currentModif)) {
return;
}
}
});
cellule.addEventListener("input", processSums);
});
}
/*********************************/
/* Interaction avec les cellules */
/*********************************/
function selectCell(obj) {
if (obj.classList.contains("modifying")) {
return; // Cellule en cours de modification, ne pas sélectionner.
}
let currentModif = document.querySelector(".modifying");
if (currentModif) {
if (!save(currentModif)) {
return;
}
if (obj.classList.contains("modifying")) {
return; // Cellule en cours de modification, ne pas sélectionner.
}
let currentModif = document.querySelector(".modifying");
if (currentModif) {
if (!save(currentModif)) {
return;
}
}
document.querySelectorAll(".selected, .modifying").forEach(cellule => {
cellule.classList.remove("selected", "modifying");
cellule.removeAttribute("contentEditable");
cellule.removeEventListener("keydown", keyCell);
})
obj.classList.add("selected");
document.querySelectorAll(".selected, .modifying").forEach((cellule) => {
cellule.classList.remove("selected", "modifying");
cellule.removeAttribute("contentEditable");
cellule.removeEventListener("keydown", keyCell);
});
obj.classList.add("selected");
}
function modifCell(obj) {
if (obj) {
obj.classList.add("modifying");
obj.contentEditable = true;
obj.addEventListener("keydown", keyCell);
obj.focus();
}
if (obj) {
obj.classList.add("modifying");
obj.contentEditable = true;
obj.addEventListener("keydown", keyCell);
obj.focus();
}
}
function key(event) {
switch (event.key) {
case "Enter": modifCell(document.querySelector(".selected")); event.preventDefault(); break;
case "ArrowRight": ArrowMove(1, 0); break;
case "ArrowLeft": ArrowMove(-1, 0); break;
case "ArrowUp": ArrowMove(0, -1); break;
case "ArrowDown": ArrowMove(0, 1); break;
}
switch (event.key) {
case "Enter":
modifCell(document.querySelector(".selected"));
event.preventDefault();
break;
case "ArrowRight":
ArrowMove(1, 0);
break;
case "ArrowLeft":
ArrowMove(-1, 0);
break;
case "ArrowUp":
ArrowMove(0, -1);
break;
case "ArrowDown":
ArrowMove(0, 1);
break;
}
}
function ArrowMove(x, y) {
if (document.querySelector(".modifying") || !document.querySelector(".selected")) {
return; // S'il n'y a aucune cellule selectionnée ou si une cellule est encours de modification, on ne change pas
}
if (
document.querySelector(".modifying") ||
!document.querySelector(".selected")
) {
return; // S'il n'y a aucune cellule selectionnée ou si une cellule est encours de modification, on ne change pas
}
let selected = document.querySelector(".selected");
let next = document.querySelector(`[data-x="${parseInt(selected.dataset.x) + x}"][data-y="${parseInt(selected.dataset.y) + y}"][data-editable="true"]`);
let selected = document.querySelector(".selected");
let next = document.querySelector(
`[data-x="${parseInt(selected.dataset.x) + x}"][data-y="${
parseInt(selected.dataset.y) + y
}"][data-editable="true"]`
);
if (next) {
selectCell(next);
}
if (next) {
selectCell(next);
}
}
function keyCell(event) {
if (event.key == "Enter") {
event.preventDefault();
event.stopPropagation();
if (!save(this)) {
return
}
this.classList.remove("modifying");
let selected = document.querySelector(".selected");
ArrowMove(0, 1);
if (selected != document.querySelector(".selected")) {
modifCell(document.querySelector(".selected"));
}
if (event.key == "Enter") {
event.preventDefault();
event.stopPropagation();
if (!save(this)) {
return;
}
this.classList.remove("modifying");
let selected = document.querySelector(".selected");
ArrowMove(0, 1);
if (selected != document.querySelector(".selected")) {
modifCell(document.querySelector(".selected"));
}
}
}
function processSums() {
let sum = 0;
document.querySelectorAll(`[data-editable="true"][data-x="${this.dataset.x}"]:not(:empty)`).forEach(e => {
let val = parseFloat(e.innerText);
if (!isNaN(val)) {
sum += val * 100;
}
})
document.querySelector(`.sums[data-x="${this.dataset.x}"][data-y="${lastY}"]`).innerText = sum / 100;
let sum = 0;
document
.querySelectorAll(
`[data-editable="true"][data-x="${this.dataset.x}"]:not(:empty)`
)
.forEach((e) => {
let val = parseFloat(e.innerText);
if (!isNaN(val)) {
sum += val * 100;
}
});
document.querySelector(
`.sums[data-x="${this.dataset.x}"][data-y="${lastY}"]`
).innerText = sum / 100;
sum = 0;
document.querySelectorAll(`[data-editable="true"][data-y="${this.dataset.y}"]:not(:empty)`).forEach(e => {
let val = parseFloat(e.innerText);
if (!isNaN(val)) {
sum += val * 100;
}
})
document.querySelector(`.sums[data-x="${lastX}"][data-y="${this.dataset.y}"]`).innerText = sum / 100;
sum = 0;
document
.querySelectorAll(
`[data-editable="true"][data-y="${this.dataset.y}"]:not(:empty)`
)
.forEach((e) => {
let val = parseFloat(e.innerText);
if (!isNaN(val)) {
sum += val * 100;
}
});
document.querySelector(
`.sums[data-x="${lastX}"][data-y="${this.dataset.y}"]`
).innerText = sum / 100;
}
/******************************/
/* Affichage d'un message */
/******************************/
function message(msg) {
var div = document.createElement("div");
div.className = "message";
div.innerHTML = msg;
document.querySelector("body").appendChild(div);
setTimeout(() => {
div.remove();
}, 3000);
var div = document.createElement("div");
div.className = "message";
div.innerHTML = msg;
document.querySelector("body").appendChild(div);
setTimeout(() => {
div.remove();
}, 3000);
}

View File

@ -1,11 +1,10 @@
// Affichage progressif du trombinoscope html
$().ready(function () {
var spans = $(".unloaded_img");
for (var i = 0; i < spans.size(); i++) {
var sp = spans[i];
var etudid = sp.id;
$(sp).load(SCO_URL + "/etud_photo_html?etudid=" + etudid);
}
var spans = $(".unloaded_img");
for (var i = 0; i < spans.size(); i++) {
var sp = spans[i];
var etudid = sp.id;
$(sp).load(SCO_URL + "/etud_photo_html?etudid=" + etudid);
}
});

View File

@ -1,5 +1,5 @@
// Edition elements programme "en place"
$(function() {
$('.span_apo_edit').jinplace();
});
$(function () {
$(".span_apo_edit").jinplace();
});

View File

@ -1,30 +1,30 @@
function refresh() {
if ($("input[name='welcome:list']").is(":checked")) {
$("input[name='reset_password:list']").closest("tr").css("display", "table-row")
if ($("input[name='reset_password:list']").is(":checked")) {
$("#tf_password").closest('tr').css("display", "none");
$("#tf_password2").closest('tr').css("display", "none");
} else {
// Le mot de passe doit être saisi
$("#tf_password").closest('tr').css("display", "table-row");
$("#tf_password2").closest('tr').css("display", "table-row");
}
if ($("input[name='welcome:list']").is(":checked")) {
$("input[name='reset_password:list']")
.closest("tr")
.css("display", "table-row");
if ($("input[name='reset_password:list']").is(":checked")) {
$("#tf_password").closest("tr").css("display", "none");
$("#tf_password2").closest("tr").css("display", "none");
} else {
// Le mot de passe doit être saisi
$("input[name='reset_password:list']").closest("tr").css("display", "none")
$("#tf_password").closest('tr').css("display", "table-row");
$("#tf_password2").closest('tr').css("display", "table-row");
// Le mot de passe doit être saisi
$("#tf_password").closest("tr").css("display", "table-row");
$("#tf_password2").closest("tr").css("display", "table-row");
}
} else {
// Le mot de passe doit être saisi
$("input[name='reset_password:list']").closest("tr").css("display", "none");
$("#tf_password").closest("tr").css("display", "table-row");
$("#tf_password2").closest("tr").css("display", "table-row");
}
}
$(function () {
$("input[name='welcome:list']").click(function () {
refresh();
})
$("input[name='reset_password:list']").click(function () {
refresh();
})
$("input[name='welcome:list']").click(function () {
refresh();
})
});
$("input[name='reset_password:list']").click(function () {
refresh();
});
refresh();
});

1
app/static/libjs/bootstrap Symbolic link
View File

@ -0,0 +1 @@
bootstrap-3.3.7-dist

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,229 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata></metadata>
<defs>
<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
<font-face units-per-em="1200" ascent="960" descent="-240" />
<missing-glyph horiz-adv-x="500" />
<glyph />
<glyph />
<glyph unicode="&#xd;" />
<glyph unicode=" " />
<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" />
<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" />
<glyph unicode="&#xa0;" />
<glyph unicode="&#x2000;" horiz-adv-x="652" />
<glyph unicode="&#x2001;" horiz-adv-x="1304" />
<glyph unicode="&#x2002;" horiz-adv-x="652" />
<glyph unicode="&#x2003;" horiz-adv-x="1304" />
<glyph unicode="&#x2004;" horiz-adv-x="434" />
<glyph unicode="&#x2005;" horiz-adv-x="326" />
<glyph unicode="&#x2006;" horiz-adv-x="217" />
<glyph unicode="&#x2007;" horiz-adv-x="217" />
<glyph unicode="&#x2008;" horiz-adv-x="163" />
<glyph unicode="&#x2009;" horiz-adv-x="260" />
<glyph unicode="&#x200a;" horiz-adv-x="72" />
<glyph unicode="&#x202f;" horiz-adv-x="260" />
<glyph unicode="&#x205f;" horiz-adv-x="326" />
<glyph unicode="&#x20ac;" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" />
<glyph unicode="&#x2212;" d="M200 400h900v300h-900v-300z" />
<glyph unicode="&#x25fc;" horiz-adv-x="500" d="M0 0z" />
<glyph unicode="&#x2601;" d="M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z" />
<glyph unicode="&#x2709;" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" />
<glyph unicode="&#x270f;" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" />
<glyph unicode="&#xe001;" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" />
<glyph unicode="&#xe002;" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q18 -55 86 -75.5t147 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" />
<glyph unicode="&#xe003;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
<glyph unicode="&#xe005;" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" />
<glyph unicode="&#xe006;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" />
<glyph unicode="&#xe007;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" />
<glyph unicode="&#xe008;" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" />
<glyph unicode="&#xe009;" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" />
<glyph unicode="&#xe010;" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe011;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe012;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe013;" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" />
<glyph unicode="&#xe014;" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" />
<glyph unicode="&#xe015;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" />
<glyph unicode="&#xe016;" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" />
<glyph unicode="&#xe017;" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" />
<glyph unicode="&#xe018;" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" />
<glyph unicode="&#xe019;" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" />
<glyph unicode="&#xe020;" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
<glyph unicode="&#xe021;" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" />
<glyph unicode="&#xe022;" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" />
<glyph unicode="&#xe023;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z" />
<glyph unicode="&#xe024;" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" />
<glyph unicode="&#xe025;" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" />
<glyph unicode="&#xe026;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z" />
<glyph unicode="&#xe027;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z" />
<glyph unicode="&#xe028;" d="M0 25v475l200 700h800l199 -700l1 -475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" />
<glyph unicode="&#xe029;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z" />
<glyph unicode="&#xe030;" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" />
<glyph unicode="&#xe031;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" />
<glyph unicode="&#xe032;" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" />
<glyph unicode="&#xe033;" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
<glyph unicode="&#xe034;" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" />
<glyph unicode="&#xe035;" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" />
<glyph unicode="&#xe036;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" />
<glyph unicode="&#xe037;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
<glyph unicode="&#xe038;" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" />
<glyph unicode="&#xe039;" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" />
<glyph unicode="&#xe040;" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" />
<glyph unicode="&#xe041;" d="M0 700l1 475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" />
<glyph unicode="&#xe042;" d="M1 700l1 475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" />
<glyph unicode="&#xe043;" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" />
<glyph unicode="&#xe044;" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
<glyph unicode="&#xe045;" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" />
<glyph unicode="&#xe046;" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" />
<glyph unicode="&#xe047;" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z" />
<glyph unicode="&#xe048;" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v71l471 -1q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" />
<glyph unicode="&#xe049;" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" />
<glyph unicode="&#xe050;" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " />
<glyph unicode="&#xe051;" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" />
<glyph unicode="&#xe052;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
<glyph unicode="&#xe053;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
<glyph unicode="&#xe054;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe055;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe056;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe057;" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" />
<glyph unicode="&#xe058;" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" />
<glyph unicode="&#xe059;" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" />
<glyph unicode="&#xe060;" d="M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z " />
<glyph unicode="&#xe062;" d="M148 745q0 124 60.5 231.5t165 172t226.5 64.5q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262zM342 772q0 -107 75.5 -182.5t181.5 -75.5 q107 0 182.5 75.5t75.5 182.5t-75.5 182t-182.5 75t-182 -75.5t-75 -181.5z" />
<glyph unicode="&#xe063;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM173 600q0 -177 125.5 -302t301.5 -125v854q-176 0 -301.5 -125 t-125.5 -302z" />
<glyph unicode="&#xe064;" d="M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 139t-64 210zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z" />
<glyph unicode="&#xe065;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM995 1015l113 -113l113 113l-21 85l-92 28z" />
<glyph unicode="&#xe066;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z" />
<glyph unicode="&#xe067;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q61 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l567 567l-137 137l-430 -431l-146 147z" />
<glyph unicode="&#xe068;" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" />
<glyph unicode="&#xe069;" d="M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe070;" d="M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe071;" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" />
<glyph unicode="&#xe072;" d="M200 0l900 550l-900 550v-1100z" />
<glyph unicode="&#xe073;" d="M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
<glyph unicode="&#xe074;" d="M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
<glyph unicode="&#xe075;" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" />
<glyph unicode="&#xe076;" d="M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z" />
<glyph unicode="&#xe077;" d="M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z" />
<glyph unicode="&#xe078;" d="M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z" />
<glyph unicode="&#xe079;" d="M185 599l592 -592l240 240l-353 353l353 353l-240 240z" />
<glyph unicode="&#xe080;" d="M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z" />
<glyph unicode="&#xe081;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z" />
<glyph unicode="&#xe082;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h600v200h-600v-200z" />
<glyph unicode="&#xe083;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM246 459l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141l-141 142l-212 -213l141 -141 z" />
<glyph unicode="&#xe084;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z" />
<glyph unicode="&#xe085;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM364 700h143q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5 q19 0 30 -10t11 -26q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-50 0 -90.5 -12t-75 -38.5t-53.5 -74.5t-19 -114zM500 300h200v100h-200 v-100z" />
<glyph unicode="&#xe086;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200v100h-200v-100z" />
<glyph unicode="&#xe087;" d="M0 500v200h195q31 125 98.5 199.5t206.5 100.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206h200v-206 q149 48 201 206h-201v200h200q-25 74 -75.5 127t-124.5 77v-204h-200v203q-75 -23 -130 -77t-79 -126h209v-200h-210z" />
<glyph unicode="&#xe088;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM356 465l135 135 l-135 135l109 109l135 -135l135 135l109 -109l-135 -135l135 -135l-109 -109l-135 135l-135 -135z" />
<glyph unicode="&#xe089;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM322 537l141 141 l87 -87l204 205l142 -142l-346 -345z" />
<glyph unicode="&#xe090;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -115 62 -215l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5zM391 245q97 -59 209 -59q171 0 292.5 121.5t121.5 292.5 q0 112 -59 209z" />
<glyph unicode="&#xe091;" d="M0 547l600 453v-300h600v-300h-600v-301z" />
<glyph unicode="&#xe092;" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" />
<glyph unicode="&#xe093;" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" />
<glyph unicode="&#xe094;" d="M104 600h296v600h300v-600h298l-449 -600z" />
<glyph unicode="&#xe095;" d="M0 200q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453l-600 -448v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5z" />
<glyph unicode="&#xe096;" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" />
<glyph unicode="&#xe097;" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z" />
<glyph unicode="&#xe101;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5h-207q-21 0 -33 -14.5t-8 -34.5zM500 300h200v100h-200v-100z" />
<glyph unicode="&#xe102;" d="M0 800h100v-200h400v300h200v-300h400v200h100v100h-111q1 1 1 6.5t-1.5 15t-3.5 17.5l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6 h-111v-100zM100 0h400v400h-400v-400zM200 900q-3 0 14 48t36 96l18 47l213 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z" />
<glyph unicode="&#xe103;" d="M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z" />
<glyph unicode="&#xe104;" d="M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5 t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5t8 -43t6 -39.5 t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z" />
<glyph unicode="&#xe105;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z" />
<glyph unicode="&#xe106;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM120 600q210 -282 393 -336l37 141q-107 18 -178.5 101.5t-71.5 193.5 q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68l-14 26zM780 161l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40 q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z" />
<glyph unicode="&#xe107;" d="M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 34 -48 36.5t-48 -29.5l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z" />
<glyph unicode="&#xe108;" d="M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -20 -13 -28.5t-32 0.5l-94 78h-222l-94 -78q-19 -9 -32 -0.5t-13 28.5 v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z" />
<glyph unicode="&#xe109;" d="M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z" />
<glyph unicode="&#xe110;" d="M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z" />
<glyph unicode="&#xe111;" d="M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z" />
<glyph unicode="&#xe112;" d="M100 600v200h300v-250q0 -113 6 -145q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5 t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z" />
<glyph unicode="&#xe113;" d="M-30 411l227 -227l352 353l353 -353l226 227l-578 579z" />
<glyph unicode="&#xe114;" d="M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z" />
<glyph unicode="&#xe115;" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z" />
<glyph unicode="&#xe116;" d="M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15 t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z" />
<glyph unicode="&#xe117;" d="M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z" />
<glyph unicode="&#xe118;" d="M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z" />
<glyph unicode="&#xe119;" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
<glyph unicode="&#xe120;" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" />
<glyph unicode="&#xe121;" d="M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z" />
<glyph unicode="&#xe122;" d="M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM100 500v250v8v8v7t0.5 7t1.5 5.5t2 5t3 4t4.5 3.5t6 1.5t7.5 0.5h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35 q-55 337 -55 351zM1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe123;" d="M74 350q0 21 13.5 35.5t33.5 14.5h18l117 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5q-18 -36 -18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-8 -3 -23 -8.5 t-65 -20t-103 -25t-132.5 -19.5t-158.5 -9q-125 0 -245.5 20.5t-178.5 40.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z" />
<glyph unicode="&#xe124;" d="M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z" />
<glyph unicode="&#xe125;" d="M0 200h200v600h-200v-600zM300 275q0 -75 100 -75h61q124 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400z M400 300v375l150 213l100 212h50v-175l-50 -225h450v-125l-250 -375h-214l-136 100h-100z" />
<glyph unicode="&#xe126;" d="M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q124 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63zM400 525l150 -212l100 -213h50v175l-50 225h450v125l-250 375h-214l-136 -100h-100v-375z" />
<glyph unicode="&#xe127;" d="M8 200v600h200v-600h-200zM308 275v525q0 17 14 35.5t28 28.5l14 9l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341 q-7 0 -90 81t-83 94zM408 289l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83l-339 -236v-503z" />
<glyph unicode="&#xe128;" d="M-101 651q0 72 54 110t139 38l302 -1l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6l365 -230q7 -4 17 -10.5t26.5 -26t16.5 -36.5v-526q0 -13 -86 -93.5t-94 -80.5h-341q-16 0 -29.5 20t-19.5 41l-130 339h-107q-84 0 -139 39t-55 111zM-1 601h222 q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l107 89v502l-343 237l-87 -83l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100zM1000 201v600h200v-600h-200z" />
<glyph unicode="&#xe129;" d="M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6.5v7.5v6.5v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294l-89 -100h-503zM400 0v200h600v-200h-600z" />
<glyph unicode="&#xe130;" d="M2 585q-16 -31 6 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85q0 -51 -0.5 -153.5t-0.5 -148.5q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM77 565l236 339h503 l89 -100v-294l-340 -130q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z" />
<glyph unicode="&#xe131;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM298 701l2 -201h300l-2 -194l402 294l-402 298v-197h-300z" />
<glyph unicode="&#xe132;" d="M0 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5zM200 600l402 -294l-2 194h300l2 201h-300v197z" />
<glyph unicode="&#xe133;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600h200v-300h200v300h200l-300 400z" />
<glyph unicode="&#xe134;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600l300 -400l300 400h-200v300h-200v-300h-200z" />
<glyph unicode="&#xe135;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM254 780q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60 q12 0 23 -5.5t23 -15t20 -13.5q24 -12 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q104 -3 221 112q30 29 47 47t34.5 49t20.5 62q-14 9 -37 9.5t-36 7.5q-14 7 -49 15t-52 19q-9 0 -39.5 -0.5 t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5 q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 39 2 44q31 -13 58 -14.5t39 3.5l11 4q7 36 -16.5 53.5t-64.5 28.5t-56 23q-19 -3 -37 0 q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6q-15 -3 -45.5 0.5t-45.5 -2.5q-21 -7 -52 -26.5t-34 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5zM518 916q3 12 16 30t16 25q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -24 17 -66.5t17 -43.5 q-9 2 -31 5t-36 5t-32 8t-30 14zM692 1003h1h-1z" />
<glyph unicode="&#xe136;" d="M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z" />
<glyph unicode="&#xe137;" horiz-adv-x="1220" d="M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z" />
<glyph unicode="&#xe138;" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
<glyph unicode="&#xe139;" d="M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z" />
<glyph unicode="&#xe140;" d="M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z" />
<glyph unicode="&#xe141;" d="M0 596q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 596q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM291 655 q0 23 15.5 38.5t38.5 15.5t39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM514 609q0 32 20.5 56.5t51.5 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5 q22 0 38 -16t16 -39t-16 -39t-38 -16q-14 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5zM800 655q0 22 16 38t39 16t38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39z" />
<glyph unicode="&#xe142;" d="M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -78.5 -16.5t-67.5 -51.5l-389 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23 q38 0 53 -36q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60 l517 511q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163z" />
<glyph unicode="&#xe143;" d="M80 784q0 131 98.5 229.5t230.5 98.5q143 0 241 -129q103 129 246 129q129 0 226 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-197 -191 -293 -322l-17 -23l-16 23q-43 58 -100 122.5t-92 99.5t-101 100q-71 70 -104.5 105.5t-77 89.5t-61 99 t-17.5 91zM250 784q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203l12 12q64 62 97.5 97t64.5 79t31 72q0 71 -48 119.5t-105 48.5q-74 0 -132 -83l-118 -171l-114 174q-51 80 -123 80q-60 0 -109.5 -49.5t-49.5 -118.5z" />
<glyph unicode="&#xe144;" d="M57 353q0 -95 66 -159l141 -142q68 -66 159 -66q93 0 159 66l283 283q66 66 66 159t-66 159l-141 141q-8 9 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141q7 -7 19 -17l105 105 l-212 212l389 389l247 -247l-95 -96l18 -17q47 -49 77 -100l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z" />
<glyph unicode="&#xe145;" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z" />
<glyph unicode="&#xe146;" d="M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z" />
<glyph unicode="&#xe148;" d="M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335q-6 1 -15.5 4t-11.5 3q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5 v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5 zM700 237q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z" />
<glyph unicode="&#xe149;" d="M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -28 16.5 -69.5t28 -62.5t41.5 -72h241v-100h-197q8 -50 -2.5 -115 t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q33 1 103 -16t103 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221z" />
<glyph unicode="&#xe150;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" />
<glyph unicode="&#xe151;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z" />
<glyph unicode="&#xe152;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z" />
<glyph unicode="&#xe153;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" />
<glyph unicode="&#xe154;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" />
<glyph unicode="&#xe155;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" />
<glyph unicode="&#xe156;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" />
<glyph unicode="&#xe157;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z" />
<glyph unicode="&#xe158;" d="M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z" />
<glyph unicode="&#xe159;" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z" />
<glyph unicode="&#xe160;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z" />
<glyph unicode="&#xe161;" d="M0 400v300h300v200l400 -350l-400 -350v200h-300zM500 0v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400z" />
<glyph unicode="&#xe162;" d="M217 519q8 -19 31 -19h302q-155 -438 -160 -458q-5 -21 4 -32l9 -8h9q14 0 26 15q11 13 274.5 321.5t264.5 308.5q14 19 5 36q-8 17 -31 17l-301 -1q1 4 78 219.5t79 227.5q2 15 -5 27l-9 9h-9q-15 0 -25 -16q-4 -6 -98 -111.5t-228.5 -257t-209.5 -237.5q-16 -19 -6 -41 z" />
<glyph unicode="&#xe163;" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300zM600 400v300h300v200l400 -350l-400 -350v200h-300z " />
<glyph unicode="&#xe164;" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98l-78 73l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5 v-300zM496 709l353 342l-149 149h500v-500l-149 149l-342 -353z" />
<glyph unicode="&#xe165;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM406 600 q0 80 57 137t137 57t137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137z" />
<glyph unicode="&#xe166;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 800l445 -500l450 500h-295v400h-300v-400h-300zM900 150h100v50h-100v-50z" />
<glyph unicode="&#xe167;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z" />
<glyph unicode="&#xe168;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 705l305 -305l596 596l-154 155l-442 -442l-150 151zM900 150h100v50h-100v-50z" />
<glyph unicode="&#xe169;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 988l97 -98l212 213l-97 97zM200 400l697 1l3 699l-250 -239l-149 149l-212 -212l149 -149zM900 150h100v50h-100v-50z" />
<glyph unicode="&#xe170;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM200 612l212 -212l98 97l-213 212zM300 1200l239 -250l-149 -149l212 -212l149 148l249 -237l-1 697zM900 150h100v50h-100v-50z" />
<glyph unicode="&#xe171;" d="M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z" />
<glyph unicode="&#xe172;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200z" />
<glyph unicode="&#xe173;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120l-126 -127h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM581 306l123 123l120 -120l353 352l123 -123l-475 -476zM600 1000h100v200h-100v-200z" />
<glyph unicode="&#xe174;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170l-298 -298h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200zM700 133l170 170l-170 170l127 127l170 -170l170 170l127 -128l-170 -169l170 -170 l-127 -127l-170 170l-170 -170z" />
<glyph unicode="&#xe175;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300l300 -300l300 300h-200v300h-200v-300h-200zM600 1000v200h100v-200h-100z" />
<glyph unicode="&#xe176;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200l-298 -298h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300h200v-300h200v300h200l-300 300zM600 1000v200h100v-200h-100z" />
<glyph unicode="&#xe177;" d="M0 250q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200v-550zM0 900h1200v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 300v200h400v-200h-400z" />
<glyph unicode="&#xe178;" d="M0 400l300 298v-198h400v-200h-400v-198zM100 800v200h100v-200h-100zM300 800v200h100v-200h-100zM500 800v200h400v198l300 -298l-300 -298v198h-400zM800 300v200h100v-200h-100zM1000 300h100v200h-100v-200z" />
<glyph unicode="&#xe179;" d="M100 700v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300l50 100l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447zM800 597q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5 t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v1106q0 31 -18 40.5t-44 -7.5l-276 -116q-25 -17 -43.5 -51.5t-18.5 -65.5v-359z" />
<glyph unicode="&#xe180;" d="M100 0h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5 t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56z" />
<glyph unicode="&#xe181;" d="M0 300q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM100 100h400l200 200h105l295 98v-298h-425l-100 -100h-375zM100 300v200h300v-200h-300zM100 600v200h300v-200h-300z M100 1000h400l200 -200v-98l295 98h105v200h-425l-100 100h-375zM700 402v163l400 133v-163z" />
<glyph unicode="&#xe182;" d="M16.5 974.5q0.5 -21.5 16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q17 18 13.5 41t-22.5 37l-192 136q-19 14 -45 12t-42 -19l-118 -118q-142 101 -268 227t-227 268l118 118q17 17 20 41.5t-11 44.5 l-139 194q-14 19 -36.5 22t-40.5 -14l-162 -162q-1 -11 -0.5 -32.5z" />
<glyph unicode="&#xe183;" d="M0 50v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5t30 -27.5t12 -24l1 -10v-50l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-20 0 -35 14.5t-15 35.5zM0 712 q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40 t-53.5 -36.5t-31 -27.5l-9 -10v-200z" />
<glyph unicode="&#xe184;" d="M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z" />
<glyph unicode="&#xe185;" d="M100 0h300v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400zM500 0v1000q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300zM900 0v700q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300z" />
<glyph unicode="&#xe186;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
<glyph unicode="&#xe187;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h100v200h100v-200h100v500h-100v-200h-100v200h-100v-500zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
<glyph unicode="&#xe188;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v100h-200v300h200v100h-300v-500zM600 300h300v100h-200v300h200v100h-300v-500z" />
<glyph unicode="&#xe189;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 550l300 -150v300zM600 400l300 150l-300 150v-300z" />
<glyph unicode="&#xe190;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300v500h700v-500h-700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM575 549 q0 -65 27 -107t68 -42h130v300h-130q-38 0 -66.5 -43t-28.5 -108z" />
<glyph unicode="&#xe191;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
<glyph unicode="&#xe192;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v400h-200v100h-100v-500zM301 400v200h100v-200h-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
<glyph unicode="&#xe193;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 700v100h300v-300h-99v-100h-100v100h99v200h-200zM201 300v100h100v-100h-100zM601 300v100h100v-100h-100z M700 700v100h200v-500h-100v400h-100z" />
<glyph unicode="&#xe194;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 500v200 l100 100h300v-100h-300v-200h300v-100h-300z" />
<glyph unicode="&#xe195;" d="M0 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 400v400h300 l100 -100v-100h-100v100h-200v-100h200v-100h-200v-100h-100zM700 400v100h100v-100h-100z" />
<glyph unicode="&#xe197;" d="M-14 494q0 -80 56.5 -137t135.5 -57h222v300h400v-300h128q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200h200v300h200v-300h200 l-300 -300z" />
<glyph unicode="&#xe198;" d="M-14 494q0 -80 56.5 -137t135.5 -57h8l414 414l403 -403q94 26 154.5 104.5t60.5 178.5q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200l300 300 l300 -300h-200v-300h-200v300h-200z" />
<glyph unicode="&#xe199;" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" />
<glyph unicode="&#xe200;" d="M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -11.5t1 -11.5q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z" />
</font>
</defs></svg>

Before

Width:  |  Height:  |  Size: 62 KiB

Some files were not shown because too many files have changed in this diff Show More