From 2ff49fc1bd6b20340579f82f555bc8410462d951 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 3 Nov 2023 16:55:26 +0100 Subject: [PATCH 01/69] WIP passage moment - vanilla js --- app/static/js/assiduites.js | 219 ++++++++++++++---- .../assiduites/pages/ajout_justificatif.j2 | 8 +- app/templates/assiduites/pages/bilan_etud.j2 | 6 +- app/templates/assiduites/pages/calendrier.j2 | 17 +- 4 files changed, 191 insertions(+), 59 deletions(-) diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 90ed21d64..f4f98cc7c 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -32,6 +32,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 () { @@ -40,6 +51,132 @@ Object.defineProperty(Array.prototype, "reversed", { enumerable: false, }); +// <= Gestion des dates => + +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} precision - La précision pour la comparaison (année, mois, jour, etc.) + * @param {String} bornes - L'inclusivité/exclusivité de la comparaison ("[]", "()", "[)", "(]") + */ + value: function (deb, fin, precision, bornes = "[]") { + // Ajuste la date actuelle, la date de début et la date de fin à la précision spécifiée + const thisPrecision = this.toPrecision(precision); + const debPrecision = deb.toPrecision(precision); + const finPrecision = fin.toPrecision(precision); + + // Vérifie les bornes en fonction de l'inclusivité/exclusivité spécifiée dans 'bornes' + const check_deb = + bornes[0] === "(" + ? thisPrecision > debPrecision + : thisPrecision >= debPrecision; + const check_fin = + bornes[1] === ")" + ? finPrecision > thisPrecision + : finPrecision >= thisPrecision; + + return check_deb && check_fin; + }, +}); + +Object.defineProperty(Date.prototype, "toPrecision", { + /** + * Ajuste la date à la précision donnée. + * @param {String} precision - La précision désirée (année, mois, jour, etc.) + */ + value: function (precision) { + const newDate = new Date(this.getTime()); + + // Trouve l'indice de la précision spécifiée dans le tableau des précisions + const precisionsIndex = + precision != undefined + ? DatePrecisions.indexOf(precision) + : DatePrecisions.length - 1; + + // Réinitialise toutes les parties de la date moins significatives que la précision spécifiée + for (let i = precisionsIndex + 1; i < DatePrecisions.length; i++) { + const p = DatePrecisions[i]; + switch (p) { + case "month": + // Les mois en JavaScript sont indexés à partir de 0, donc on met 0 pour janvier + newDate.setMonth(0); + break; + case "day": + // Les jours en JavaScript commencent à 1, donc on met 1 pour le premier jour du mois + newDate.setDate(1); + break; + case "hour": + newDate.setHours(0); + break; + case "minute": + newDate.setMinutes(0); + break; + case "second": + newDate.setSeconds(0); + break; + case "millisecond": + newDate.setMilliseconds(0); + break; + } + } + + return newDate; + }, +}); + +Object.defineProperty(Date.prototype, "isBefore", { + value: function (date) { + return this < date; + }, +}); +Object.defineProperty(Date.prototype, "isAfter", { + value: function (date) { + return this > date; + }, +}); + +Object.defineProperty(Date.prototype, "isSame", { + /** + * Retourne vrai si les dates sont les mêmes + * @param {Date} date + * @returns boolean + */ + value: function (date) { + return this == date; + }, +}); + +Object.defineProperty(Date.prototype, "toIsoUtcString", { + value: function () { + const date = this; + 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) + ); + }, +}); + /** * Ajout des évents sur les boutons d'assiduité * @param {Document | HTMLFieldSetElement} parent par défaut le document, un field sinon @@ -126,7 +263,7 @@ function validateSelectors(btn) { } function onlyAbs() { - if (getDate() > moment()) { + if (getDate() > Date.now()) { document .querySelectorAll(".rbtn.present, .rbtn.retard") .forEach((el) => el.remove()); @@ -268,8 +405,8 @@ function executeMassActionQueue() { */ const tlTimes = getTimeLineTimes(); let assiduite = { - date_debut: tlTimes.deb.format(), - date_fin: tlTimes.fin.format(), + date_debut: tlTimes.deb.toIsoUtcString(), + date_fin: tlTimes.fin.toIsoUtcString(), }; assiduite = setModuleImplId(assiduite); @@ -613,19 +750,11 @@ function getNearestWorkDay(date) { } 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, "[]"); } /** @@ -664,8 +793,8 @@ 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) { + return new Date(str).toLocaleString("fr-FR"); } /** @@ -705,8 +834,8 @@ function verifyNonWorkDays(day, nonWorkdays) { * Fonction qui vérifie si une période est dans un interval * Objet période / interval * { - * deb: moment.tz(), - * fin: moment.tz(), + * deb: Date, + * fin: Date, * } * @param {object} period * @param {object} interval @@ -718,7 +847,7 @@ 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 @@ -726,11 +855,11 @@ function getTimeLineTimes() { //On récupère la date const dateiso = document.querySelector("#tl_date").value; - //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 +873,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(conflict.date_debut), + fin: new Date(conflict.date_fin), }; return tlTimes.deb.isSame(clTimes.deb) && tlTimes.fin.isSame(clTimes.fin); } @@ -815,9 +944,9 @@ function toIsoString(date) { } /** - * 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", ":"); @@ -825,7 +954,7 @@ function numberTimeToDate(nb) { datetime = `${date}T${time}`; - return moment.tz(datetime, TIMEZONE); + return new Date(datetime); } // <<== Gestion des assiduités ==>> @@ -885,8 +1014,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.toIsoUtcString(), + date_fin: tlTimes.fin.toIsoUtcString(), etat: etat, }; @@ -1067,8 +1196,8 @@ 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(assi.date_debut), + fin: new Date(assi.date_debut), }; return hasTimeConflict(periode, interval); }); @@ -1085,21 +1214,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(assi.date_debut), + fin: new Date(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(a.date_fin); + const b_fin = new Date(b.date_fin); return b_fin < a_fin; }); @@ -1232,8 +1361,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) => { @@ -1545,8 +1674,8 @@ function getFormSemestreDates() { const dateFin = document.getElementById("formsemestre_date_fin").textContent; return { - deb: dateDeb, - fin: dateFin, + deb: new Date(dateDeb), + fin: new Date(dateFin), }; } @@ -1614,7 +1743,9 @@ function getJustificatifFromPeriod(date, etudid, update) { getUrl() + `/api/justificatifs/${etudid}/query?date_debut=${date.deb .add(1, "s") - .format()}&date_fin=${date.fin.subtract(1, "s").format()}`, + .toIsoUtcString()}&date_fin=${date.fin + .subtract(1, "s") + .toIsoUtcString()}`, success: (data) => { update(data); }, @@ -1646,8 +1777,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(assiduite.date_debut), + fin: new Date(assiduite.date_fin), }; const action = (justifs) => { //créer un nouveau justificatif @@ -1660,8 +1791,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(assiduite.date_debut).toIsoUtcString(), + date_fin: new Date(assiduite.date_fin).toIsoUtcString(), raison: raison, etat: etat, }; diff --git a/app/templates/assiduites/pages/ajout_justificatif.j2 b/app/templates/assiduites/pages/ajout_justificatif.j2 index c4885b3f8..83e7366c3 100644 --- a/app/templates/assiduites/pages/ajout_justificatif.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif.j2 @@ -118,8 +118,8 @@ return false; } - const date_debut = moment.tz(deb, TIMEZONE); - const date_fin = moment.tz(fin, TIMEZONE); + const date_debut = new Date(deb); + const date_fin = new Date(fin); if (date_fin.isBefore(date_debut)) { openAlertModal("Erreur détéctée", document.createTextNode("La date de fin doit se trouver après la date de début."), "", color = "crimson"); @@ -138,8 +138,8 @@ const raison = field.querySelector('#justi_raison').value; return { - date_debut: moment.tz(deb, TIMEZONE).format(), - date_fin: moment.tz(fin, TIMEZONE).format(), + date_debut: new Date(deb).toIsoUtcString(), + date_fin: new Date(fin).toIsoUtcString(), etat: etat, raison: raison, } diff --git a/app/templates/assiduites/pages/bilan_etud.j2 b/app/templates/assiduites/pages/bilan_etud.j2 index dad94d274..bba1dae0e 100644 --- a/app/templates/assiduites/pages/bilan_etud.j2 +++ b/app/templates/assiduites/pages/bilan_etud.j2 @@ -93,8 +93,8 @@ return; } - const date_debut = new moment.tz(dd_val + "T00:00", TIMEZONE); - const date_fin = new moment.tz(df_val + "T23:59", TIMEZONE); + const date_debut = new Date(dd_val + "T00:00"); + const date_fin = new Date(df_val + "T23:59"); if (date_debut.valueOf() > date_fin.valueOf()) { openAlertModal("Dates invalides", document.createTextNode('La date de début se situe après la date de fin.')); @@ -102,7 +102,7 @@ } - countAssiduites(date_debut.format(), date_fin.format()) + countAssiduites(date_debut.toIsoUtcString()(), date_fin.toIsoUtcString()()) } diff --git a/app/templates/assiduites/pages/calendrier.j2 b/app/templates/assiduites/pages/calendrier.j2 index d976f2cad..e4e793d39 100644 --- a/app/templates/assiduites/pages/calendrier.j2 +++ b/app/templates/assiduites/pages/calendrier.j2 @@ -126,7 +126,7 @@ let datesByMonth = {}; dates.forEach((date) => { - let month = date.format("MMMM"); // Obtenir le mois + let month = date.toLocaleString('fr-FR', { month: "short" }); // Obtenir le mois if (!datesByMonth[month]) { datesByMonth[month] = []; @@ -135,6 +135,7 @@ datesByMonth[month].push(date); }); + console.log(Object.keys(datesByMonth)) return datesByMonth; } @@ -146,18 +147,18 @@ datesByMonth[month].forEach((date) => { let dayAssiduities = assiduities.filter((assiduity) => { - return moment.tz(date, TIMEZONE).isBetween( - moment.tz(assiduity.date_debut, TIMEZONE), - moment.tz(assiduity.date_fin, TIMEZONE), + return new Date(date).isBetween( + new Date(assiduity.date_debut), + new Date(assiduity.date_fin), "day", "[]" ) }); let dayJustificatifs = justificatifs.filter((justif) => { - return moment.tz(date, TIMEZONE).isBetween( - moment.tz(justif.date_debut, TIMEZONE), - moment.tz(justif.date_fin, TIMEZONE), + return new Date(date).isBetween( + new Date(justif.date_debut), + new Date(justif.date_fin), "day", "[]" ) @@ -250,7 +251,7 @@ } else if (dayJustificatifs.some((j) => j.etat.toLowerCase() !== "valide")) { est_just = "est_just invalide"; } - const momentDate = moment.tz(date, TIMEZONE); + const momentDate = new Date(date); let dayOfMonth = momentDate.format("D"); let dayOfWeek = momentDate.format("ddd"); From e71e4b27ec12a1f122ba66b7e41406e55d45fe41 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 6 Nov 2023 22:05:38 +0100 Subject: [PATCH 02/69] EDT: ajout des edt_id dans formsemestre, groupes, modules, users --- app/auth/models.py | 20 +++- app/models/config.py | 7 +- app/models/formsemestre.py | 2 + app/models/groups.py | 42 ++++++- app/models/modules.py | 4 +- app/scodoc/sco_edt_cal.py | 5 +- app/scodoc/sco_groups.py | 125 ++++----------------- app/scodoc/sco_groups_edit.py | 69 +++++++++++- app/scodoc/sco_groups_view.py | 4 - app/static/css/partition_editor.css | 6 +- app/static/css/scodoc.css | 14 ++- app/templates/scolar/partition_editor.j2 | 13 ++- app/views/scolar.py | 2 +- flask_cas/routing.py | 36 ++++-- migrations/versions/6fb956addd69_edt_id.py | 58 ++++++++++ 15 files changed, 259 insertions(+), 148 deletions(-) create mode 100644 migrations/versions/6fb956addd69_edt_id.py diff --git a/app/auth/models.py b/app/auth/models.py index 1330b9b7d..899a5ab27 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -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 @@ -182,7 +185,18 @@ class User(UserMixin, db.Model): return self._migrate_scodoc7_password(password) return False - return check_password_hash(self.password_hash, password) + password_ok = check_password_hash(self.password_hash, password) + if password_ok and cas_enabled and 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") + log(f"Storing edt_id for {self.user_name}: '{edt_id}'") + self.edt_id = edt_id + db.session.add(self) + db.session.commit() + return password_ok def _migrate_scodoc7_password(self, password) -> bool: """After migration, rehash password.""" diff --git a/app/models/config.py b/app/models/config.py index 60ce884b4..fe76bec88 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -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 là ou contactez votre administrateur local.""" ) return klass diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 1d99342c0..d020eb92a 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -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( diff --git a/app/models/groups.py b/app/models/groups.py index a4a5792f6..8d445eee4 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -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", @@ -272,6 +274,40 @@ class GroupDescr(db.Model): return False return True + def set_name( + self, group_name: str, edt_id: str | bool = False, dest_url: str = None + ): + """Set group name, and optionally edt_id. + Check permission 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 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 + if edt_id is not False: + if isinstance(edt_id, str): + edt_id = edt_id.strip() or None + self.edt_id = edt_id + db.session.add(self) + db.session.commit() + sco_cache.invalidate_formsemestre( + formsemestre_id=self.partition.formsemestre_id + ) + 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: diff --git a/app/models/modules.py b/app/models/modules.py index b4aa00ad8..fe297ebd7 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -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: diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index 613428ed1..2174f2580 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -34,16 +34,13 @@ XXX incompatible avec les ics HyperPlanning Paris 13 (était pour GPU). """ import icalendar -import pprint -import traceback + import urllib import app.scodoc.sco_utils as scu 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 diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 6a4d77e84..46b650b72 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -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 @@ -136,7 +136,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}, ) @@ -258,14 +258,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: @@ -350,12 +350,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 +393,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 +967,8 @@ def edit_partition_form(formsemestre_id=None): for p in partitions: if p["partition_name"] is not None: H.append( - f"""{suppricon} """ ) @@ -1299,85 +1299,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"

Renommer un groupe de {group.partition.partition_name or '-'}

"] - 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 +1491,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 +1504,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 +1533,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 diff --git a/app/scodoc/sco_groups_edit.py b/app/scodoc/sco_groups_edit.py index 4fd1e103c..0a9173ec2 100644 --- a/app/scodoc/sco_groups_edit.py +++ b/app/scodoc/sco_groups_edit.py @@ -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,64 @@ 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"

Renommer un groupe de {group.partition.partition_name or '-'}

"] + 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", + }, + ), + ( + "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", + }, + ), + ), + submitlabel="Renommer", + 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 + group.set_name(tf[2]["group_name"], edt_id=tf[2]["edt_id"], dest_url=dest_url) + flash("groupe modifié") + return flask.redirect(dest_url) diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index c44e3c0cd..2f20b6ce3 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -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 diff --git a/app/static/css/partition_editor.css b/app/static/css/partition_editor.css index 09f042fef..52576891e 100644 --- a/app/static/css/partition_editor.css +++ b/app/static/css/partition_editor.css @@ -302,6 +302,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 +602,4 @@ h3 { #zoneGroupes .groupe[data-idgroupe=aucun]>div:nth-child(1) { color: red; -} \ No newline at end of file +} diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index c31d0de60..e5db153a4 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1327,7 +1327,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, @@ -3921,9 +3921,9 @@ div#update_warning>div:nth-child(2) { padding-left: 8ex; } -/* +/* Titres des tabs: - .nav-tabs li a { + .nav-tabs li a { font-variant: small-caps; font-size: 13pt; } @@ -4354,7 +4354,7 @@ button.unselect { /* Non supproté par les navigateurs (en Fev. 2023) .table_recap button:has(span a.clearreaload) { -} +} */ div.table_recap table.table_recap, @@ -4833,4 +4833,8 @@ div.cas_etat_certif_ssl { margin-bottom: 8px; font-style: italic; color: rgb(231, 0, 0); -} \ No newline at end of file +} + +.edt_id { + color: rgb(85, 255, 24); +} diff --git a/app/templates/scolar/partition_editor.j2 b/app/templates/scolar/partition_editor.j2 index 4aa03fc24..23ddc6866 100644 --- a/app/templates/scolar/partition_editor.j2 +++ b/app/templates/scolar/partition_editor.j2 @@ -8,7 +8,8 @@

Filtres

@@ -212,7 +213,7 @@
+
- +
Configuration @@ -246,15 +247,15 @@ let div = document.createElement("button"); div.classList.add("dt-button"); div.dataset.idgroupe = groupe.id; + let edt_id_str = groupe.edt_id ? `[${groupe.edt_id}]` : ""; div.innerHTML = ` || - ${groupe.group_name} - ✏️ + ${groupe.group_name} ${edt_id_str} + ✏️ `; div.addEventListener("click", filtre); div.querySelector(".move").addEventListener("mousedown", moveStart); - div.querySelector(".modif").addEventListener("click", editText); div.querySelector(".suppr").addEventListener("click", suppr); return div; @@ -945,4 +946,4 @@ } - \ No newline at end of file + diff --git a/app/views/scolar.py b/app/views/scolar.py index 0388c4c2e..ac908ce76 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -842,7 +842,7 @@ sco_publish("/setGroups", sco_groups.setGroups, Permission.ScoView, methods=["PO sco_publish( "/group_rename", - sco_groups.group_rename, + sco_groups_edit.group_rename, Permission.ScoView, methods=["GET", "POST"], ) diff --git a/flask_cas/routing.py b/flask_cas/routing.py index ec9eef639..6078b79ca 100644 --- a/flask_cas/routing.py +++ b/flask_cas/routing.py @@ -1,19 +1,21 @@ +""" +Routes for CAS authentication +Modified for ScoDoc +""" +import re import ssl +from urllib.error import URLError +from urllib.request import urlopen import flask -from xmltodict import parse from flask import current_app +from xmltodict import parse + from .cas_urls import create_cas_login_url from .cas_urls import create_cas_logout_url from .cas_urls import create_cas_validate_url -try: - from urllib import urlopen # python 2 -except ImportError: - from urllib.request import urlopen # python 3 -from urllib.error import URLError - blueprint = flask.Blueprint("cas", __name__) @@ -53,7 +55,6 @@ def login(): flask.session[cas_token_session_key] = flask.request.args["ticket"] if cas_token_session_key in flask.session: - if validate(flask.session[cas_token_session_key]): if "CAS_AFTER_LOGIN_SESSION_URL" in flask.session: redirect_url = flask.session.pop("CAS_AFTER_LOGIN_SESSION_URL") @@ -64,7 +65,7 @@ def login(): else: flask.session.pop(cas_token_session_key, None) - current_app.logger.debug("Redirecting to: {redirect_url}") + current_app.logger.debug(f"cas.login: redirecting to {redirect_url}") return flask.redirect(redirect_url) @@ -84,6 +85,7 @@ def logout(): flask.session.pop(cas_username_session_key, None) flask.session.pop(cas_attributes_session_key, None) flask.session.pop(cas_token_session_key, None) # added by EV + flask.session.pop("CAS_EDT_ID", None) # added by EV cas_after_logout = current_app.config["CAS_AFTER_LOGOUT"] if cas_after_logout is not None: @@ -102,7 +104,7 @@ def logout(): else: redirect_url = create_cas_logout_url(current_app.config["CAS_SERVER"], None) - current_app.logger.debug(f"Redirecting to: {redirect_url}") + current_app.logger.debug(f"cas.logout: redirecting to {redirect_url}") return flask.redirect(redirect_url) @@ -114,11 +116,12 @@ def validate(ticket): key `CAS_USERNAME_SESSION_KEY` while the validated attributes dictionary is saved under the key 'CAS_ATTRIBUTES_SESSION_KEY'. """ + from app.models.config import ScoDocSiteConfig cas_username_session_key = current_app.config["CAS_USERNAME_SESSION_KEY"] cas_attributes_session_key = current_app.config["CAS_ATTRIBUTES_SESSION_KEY"] cas_error_callback = current_app.config.get("CAS_ERROR_CALLBACK") - current_app.logger.debug("validating token {0}".format(ticket)) + current_app.logger.debug(f"validating token {ticket}") cas_validate_url = create_cas_validate_url( current_app.config["CAS_SERVER"], @@ -182,7 +185,7 @@ def validate(ticket): attributes = xml_from_dict.get("cas:attributes", {}) if attributes and "cas:memberOf" in attributes: - if isinstance(attributes["cas:memberOf"], basestring): + if isinstance(attributes["cas:memberOf"], str): attributes["cas:memberOf"] = ( attributes["cas:memberOf"].lstrip("[").rstrip("]").split(",") ) @@ -190,6 +193,15 @@ def validate(ticket): attributes["cas:memberOf"][group_number] = ( attributes["cas:memberOf"][group_number].lstrip(" ").rstrip(" ") ) + # Extract auxiliary informations (utilisé pour edt_id) + exp = ScoDocSiteConfig.get("cas_edt_id_from_xml_regexp") + if exp: + m = re.search(exp, xmldump) + if m and len(m.groups()) > 0: + cas_edt_id = m.group(1) + if cas_edt_id: + flask.session["CAS_EDT_ID"] = cas_edt_id + flask.session[cas_username_session_key] = username flask.session[cas_attributes_session_key] = attributes else: diff --git a/migrations/versions/6fb956addd69_edt_id.py b/migrations/versions/6fb956addd69_edt_id.py new file mode 100644 index 000000000..801ba62b5 --- /dev/null +++ b/migrations/versions/6fb956addd69_edt_id.py @@ -0,0 +1,58 @@ +"""edt_id + +Revision ID: 6fb956addd69 +Revises: fd805feb7ba8 +Create Date: 2023-11-06 12:14:42.808476 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "6fb956addd69" +down_revision = "fd805feb7ba8" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("group_descr", schema=None) as batch_op: + batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True)) + batch_op.create_index( + batch_op.f("ix_group_descr_edt_id"), ["edt_id"], unique=False + ) + + with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op: + batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True)) + batch_op.create_index( + batch_op.f("ix_notes_formsemestre_edt_id"), ["edt_id"], unique=False + ) + + with op.batch_alter_table("notes_modules", schema=None) as batch_op: + batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True)) + batch_op.create_index( + batch_op.f("ix_notes_modules_edt_id"), ["edt_id"], unique=False + ) + + with op.batch_alter_table("user", schema=None) as batch_op: + batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True)) + batch_op.create_index(batch_op.f("ix_user_edt_id"), ["edt_id"], unique=False) + + +def downgrade(): + with op.batch_alter_table("user", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_user_edt_id")) + batch_op.drop_column("edt_id") + + with op.batch_alter_table("notes_modules", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_notes_modules_edt_id")) + batch_op.drop_column("edt_id") + + with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_notes_formsemestre_edt_id")) + batch_op.drop_column("edt_id") + + with op.batch_alter_table("group_descr", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_group_descr_edt_id")) + batch_op.drop_column("edt_id") From 2c209313c6af89acb9c7488b2834e52a5212c415 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 8 Nov 2023 12:51:24 +0100 Subject: [PATCH 03/69] Corrige script anonymisation (qui violait nouvelles contraintes sur la base) --- sco_version.py | 2 +- tools/anonymize_db.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sco_version.py b/sco_version.py index 3fa1a6859..7771d1df9 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.50" +SCOVERSION = "9.6.51" SCONAME = "ScoDoc" diff --git a/tools/anonymize_db.py b/tools/anonymize_db.py index 1d95a11b0..cc7a5a042 100755 --- a/tools/anonymize_db.py +++ b/tools/anonymize_db.py @@ -55,6 +55,7 @@ def usage(): anonymize_name = "random_text_md5(8)" anonymize_date = "'1970-01-01'" +anonymize_false = "FALSE" anonymize_question_str = "'?'" anonymize_null = "NULL" @@ -69,13 +70,14 @@ ANONYMIZED_FIELDS = { "identite.nom": anonymize_name, "identite.prenom": anonymize_name, "identite.nom_usuel": anonymize_null, - "identite.civilite": "'X'", + "identite.civilite_etat_civil" : anonymize_null, + "identite.prenom_etat_civil" : anonymize_null, "identite.date_naissance": anonymize_date, "identite.lieu_naissance": anonymize_question_str, "identite.dept_naissance": anonymize_question_str, "identite.nationalite": anonymize_question_str, "identite.statut": anonymize_null, - "identite.boursier": anonymize_null, + "identite.boursier": anonymize_false, "identite.photo_filename": anonymize_null, "identite.code_nip": anonymize_null, "identite.code_ine": anonymize_null, From 37f86f02cd1fe2e3655fc3b80071b0dcb929e490 Mon Sep 17 00:00:00 2001 From: Iziram Date: Wed, 8 Nov 2023 15:09:49 +0100 Subject: [PATCH 04/69] =?UTF-8?q?Assiduit=C3=A9=20:=20bug=20fix=20saisie?= =?UTF-8?q?=20jour=20readOnly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/assiduites.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 90ed21d64..d11b5df48 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -84,7 +84,7 @@ function validateSelectors(btn) { ); }); - if (getModuleImplId() == null && window.forceModule) { + if (getModuleImplId() == null && window.forceModule && !readOnly) { const HTML = `

Attention, le module doit obligatoirement être renseigné.

Cela vient de la configuration du semestre ou plus largement du département.

From a40768e33bbc86a874bb509c111fbe8b5829e36c Mon Sep 17 00:00:00 2001 From: Iziram Date: Wed, 8 Nov 2023 15:12:42 +0100 Subject: [PATCH 05/69] =?UTF-8?q?Assiduit=C3=A9:=20suppression=20momentJS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/assiduites.js | 305 +++- app/static/libjs/moment-2.29.4.min.js | 2 - app/static/libjs/moment-timezone.js | 1597 ----------------- app/templates/assiduites/pages/bilan_etud.j2 | 3 +- app/templates/assiduites/pages/calendrier.j2 | 45 +- .../pages/signal_assiduites_diff.j2 | 2 +- app/templates/assiduites/widgets/conflict.j2 | 34 +- app/templates/assiduites/widgets/differee.j2 | 75 +- .../assiduites/widgets/minitimeline.j2 | 27 +- .../widgets/moduleimpl_dynamic_selector.j2 | 8 +- .../assiduites/widgets/tableau_assi.j2 | 10 +- .../assiduites/widgets/tableau_base.j2 | 6 +- .../assiduites/widgets/tableau_justi.j2 | 19 +- app/templates/assiduites/widgets/timeline.j2 | 5 +- app/views/assiduites.py | 23 +- 15 files changed, 391 insertions(+), 1770 deletions(-) delete mode 100644 app/static/libjs/moment-2.29.4.min.js delete mode 100644 app/static/libjs/moment-timezone.js diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index f4f98cc7c..044708249 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -53,34 +53,6 @@ Object.defineProperty(Array.prototype, "reversed", { // <= Gestion des dates => -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} precision - La précision pour la comparaison (année, mois, jour, etc.) - * @param {String} bornes - L'inclusivité/exclusivité de la comparaison ("[]", "()", "[)", "(]") - */ - value: function (deb, fin, precision, bornes = "[]") { - // Ajuste la date actuelle, la date de début et la date de fin à la précision spécifiée - const thisPrecision = this.toPrecision(precision); - const debPrecision = deb.toPrecision(precision); - const finPrecision = fin.toPrecision(precision); - - // Vérifie les bornes en fonction de l'inclusivité/exclusivité spécifiée dans 'bornes' - const check_deb = - bornes[0] === "(" - ? thisPrecision > debPrecision - : thisPrecision >= debPrecision; - const check_fin = - bornes[1] === ")" - ? finPrecision > thisPrecision - : finPrecision >= thisPrecision; - - return check_deb && check_fin; - }, -}); - Object.defineProperty(Date.prototype, "toPrecision", { /** * Ajuste la date à la précision donnée. @@ -128,12 +100,12 @@ Object.defineProperty(Date.prototype, "toPrecision", { Object.defineProperty(Date.prototype, "isBefore", { value: function (date) { - return this < date; + return this.valueOf() < date.valueOf(); }, }); Object.defineProperty(Date.prototype, "isAfter", { value: function (date) { - return this > date; + return this.valueOf() > date.valueOf(); }, }); @@ -144,11 +116,42 @@ Object.defineProperty(Date.prototype, "isSame", { * @returns boolean */ value: function (date) { - return this == date; + return this.valueOf() == date.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} precision - La précision pour la comparaison (année, mois, jour, etc.) + * @param {String} bornes - L'inclusivité/exclusivité de la comparaison ("[]", "()", "[)", "(]") + */ + value: function (deb, fin, precision, bornes = "[]") { + // Ajuste la date actuelle, la date de début et la date de fin à la précision spécifiée + const thisPrecision = this.toPrecision(precision); + const debPrecision = deb.toPrecision(precision); + const finPrecision = fin.toPrecision(precision); + + // Vérifie les bornes en fonction de l'inclusivité/exclusivité spécifiée dans 'bornes' + const check_deb = + bornes[0] === "(" + ? thisPrecision > debPrecision + : thisPrecision >= debPrecision; + const check_fin = + bornes[1] === ")" + ? finPrecision > thisPrecision + : finPrecision >= thisPrecision; + + 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 () { const date = this; var tzo = -date.getTimezoneOffset(), @@ -177,6 +180,231 @@ Object.defineProperty(Date.prototype, "toIsoUtcString", { }, }); +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) { + 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, + }); + 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, + }); + + case "YYYY-MM-DDTHH:mm": + let iso = this.toIsoUtcString(); + // slice : YYYY-MM-DDTHH + // slice + 3 : YYYY-MM-DDTHH:mm + return iso.slice(0, iso.indexOf(":") + 3); + default: + return this.toIsoUtcString(); + } + }, +}); + +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 + }, +}); + +Object.defineProperty(Date.prototype, "startOf", { + /** + * Ajuste 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"); + } + 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; + }, +}); + +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)); + } +} + /** * Ajout des évents sur les boutons d'assiduité * @param {Document | HTMLFieldSetElement} parent par défaut le document, un field sinon @@ -753,7 +981,6 @@ function verifyDateInSemester() { const date = getDate(); const periodSemester = getFormSemestreDates(); - return date.isBetween(periodSemester.deb, periodSemester.fin, "[]"); } @@ -793,8 +1020,8 @@ 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) { - return new Date(str).toLocaleString("fr-FR"); +function formatDateModal(str, separator = " ") { + return new Date(str).format("DD/MM/Y HH:mm").replace(" ", separator); } /** @@ -1197,9 +1424,10 @@ function getAssiduitesConflict(etudid, periode) { return etudAssiduites.filter((assi) => { const interval = { deb: new Date(assi.date_debut), - fin: new Date(assi.date_debut), + fin: new Date(assi.date_fin), }; - return hasTimeConflict(periode, interval); + const test = hasTimeConflict(periode, interval); + return test; }); } @@ -1506,7 +1734,6 @@ function insertEtudRow(etud, index, output = false) { date_fin: null, prevAssiduites: prevAssiduite, }; - if (conflict.length > 0) { assiduite.etatAssiduite = conflict[0].etat; @@ -1742,9 +1969,9 @@ function getJustificatifFromPeriod(date, etudid, update) { url: getUrl() + `/api/justificatifs/${etudid}/query?date_debut=${date.deb - .add(1, "s") + .add(1, "seconds") .toIsoUtcString()}&date_fin=${date.fin - .subtract(1, "s") + .add(-1, "seconds") .toIsoUtcString()}`, success: (data) => { update(data); diff --git a/app/static/libjs/moment-2.29.4.min.js b/app/static/libjs/moment-2.29.4.min.js deleted file mode 100644 index 3427886d1..000000000 --- a/app/static/libjs/moment-2.29.4.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var H;function f(){return H.apply(null,arguments)}function a(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function F(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function c(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function L(e){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(e).length;for(var t in e)if(c(e,t))return;return 1}function o(e){return void 0===e}function u(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function V(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function G(e,t){for(var n=[],s=e.length,i=0;i>>0,s=0;sAe(e)?(r=e+1,t-Ae(e)):(r=e,t);return{year:r,dayOfYear:n}}function qe(e,t,n){var s,i,r=ze(e.year(),t,n),r=Math.floor((e.dayOfYear()-r-1)/7)+1;return r<1?s=r+P(i=e.year()-1,t,n):r>P(e.year(),t,n)?(s=r-P(e.year(),t,n),i=e.year()+1):(i=e.year(),s=r),{week:s,year:i}}function P(e,t,n){var s=ze(e,t,n),t=ze(e+1,t,n);return(Ae(e)-s+t)/7}s("w",["ww",2],"wo","week"),s("W",["WW",2],"Wo","isoWeek"),t("week","w"),t("isoWeek","W"),n("week",5),n("isoWeek",5),v("w",p),v("ww",p,w),v("W",p),v("WW",p,w),Te(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=g(e)});function Be(e,t){return e.slice(t,7).concat(e.slice(0,t))}s("d",0,"do","day"),s("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),s("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),s("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),s("e",0,0,"weekday"),s("E",0,0,"isoWeekday"),t("day","d"),t("weekday","e"),t("isoWeekday","E"),n("day",11),n("weekday",11),n("isoWeekday",11),v("d",p),v("e",p),v("E",p),v("dd",function(e,t){return t.weekdaysMinRegex(e)}),v("ddd",function(e,t){return t.weekdaysShortRegex(e)}),v("dddd",function(e,t){return t.weekdaysRegex(e)}),Te(["dd","ddd","dddd"],function(e,t,n,s){s=n._locale.weekdaysParse(e,s,n._strict);null!=s?t.d=s:m(n).invalidWeekday=e}),Te(["d","e","E"],function(e,t,n,s){t[s]=g(e)});var Je="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Qe="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Xe="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),Ke=k,et=k,tt=k;function nt(){function e(e,t){return t.length-e.length}for(var t,n,s,i=[],r=[],a=[],o=[],u=0;u<7;u++)s=l([2e3,1]).day(u),t=M(this.weekdaysMin(s,"")),n=M(this.weekdaysShort(s,"")),s=M(this.weekdays(s,"")),i.push(t),r.push(n),a.push(s),o.push(t),o.push(n),o.push(s);i.sort(e),r.sort(e),a.sort(e),o.sort(e),this._weekdaysRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+r.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+i.join("|")+")","i")}function st(){return this.hours()%12||12}function it(e,t){s(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function rt(e,t){return t._meridiemParse}s("H",["HH",2],0,"hour"),s("h",["hh",2],0,st),s("k",["kk",2],0,function(){return this.hours()||24}),s("hmm",0,0,function(){return""+st.apply(this)+r(this.minutes(),2)}),s("hmmss",0,0,function(){return""+st.apply(this)+r(this.minutes(),2)+r(this.seconds(),2)}),s("Hmm",0,0,function(){return""+this.hours()+r(this.minutes(),2)}),s("Hmmss",0,0,function(){return""+this.hours()+r(this.minutes(),2)+r(this.seconds(),2)}),it("a",!0),it("A",!1),t("hour","h"),n("hour",13),v("a",rt),v("A",rt),v("H",p),v("h",p),v("k",p),v("HH",p,w),v("hh",p,w),v("kk",p,w),v("hmm",ge),v("hmmss",we),v("Hmm",ge),v("Hmmss",we),D(["H","HH"],x),D(["k","kk"],function(e,t,n){e=g(e);t[x]=24===e?0:e}),D(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),D(["h","hh"],function(e,t,n){t[x]=g(e),m(n).bigHour=!0}),D("hmm",function(e,t,n){var s=e.length-2;t[x]=g(e.substr(0,s)),t[T]=g(e.substr(s)),m(n).bigHour=!0}),D("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[x]=g(e.substr(0,s)),t[T]=g(e.substr(s,2)),t[N]=g(e.substr(i)),m(n).bigHour=!0}),D("Hmm",function(e,t,n){var s=e.length-2;t[x]=g(e.substr(0,s)),t[T]=g(e.substr(s))}),D("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[x]=g(e.substr(0,s)),t[T]=g(e.substr(s,2)),t[N]=g(e.substr(i))});k=de("Hours",!0);var at,ot={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",w:"a week",ww:"%d weeks",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Ce,monthsShort:Ue,week:{dow:0,doy:6},weekdays:Je,weekdaysMin:Xe,weekdaysShort:Qe,meridiemParse:/[ap]\.?m?\.?/i},R={},ut={};function lt(e){return e&&e.toLowerCase().replace("_","-")}function ht(e){for(var t,n,s,i,r=0;r=t&&function(e,t){for(var n=Math.min(e.length,t.length),s=0;s=t-1)break;t--}r++}return at}function dt(t){var e;if(void 0===R[t]&&"undefined"!=typeof module&&module&&module.exports&&null!=t.match("^[^/\\\\]*$"))try{e=at._abbr,require("./locale/"+t),ct(e)}catch(e){R[t]=null}return R[t]}function ct(e,t){return e&&((t=o(t)?mt(e):ft(e,t))?at=t:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),at._abbr}function ft(e,t){if(null===t)return delete R[e],null;var n,s=ot;if(t.abbr=e,null!=R[e])Q("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=R[e]._config;else if(null!=t.parentLocale)if(null!=R[t.parentLocale])s=R[t.parentLocale]._config;else{if(null==(n=dt(t.parentLocale)))return ut[t.parentLocale]||(ut[t.parentLocale]=[]),ut[t.parentLocale].push({name:e,config:t}),null;s=n._config}return R[e]=new K(X(s,t)),ut[e]&&ut[e].forEach(function(e){ft(e.name,e.config)}),ct(e),R[e]}function mt(e){var t;if(!(e=e&&e._locale&&e._locale._abbr?e._locale._abbr:e))return at;if(!a(e)){if(t=dt(e))return t;e=[e]}return ht(e)}function _t(e){var t=e._a;return t&&-2===m(e).overflow&&(t=t[O]<0||11We(t[Y],t[O])?b:t[x]<0||24P(r,u,l)?m(s)._overflowWeeks=!0:null!=h?m(s)._overflowWeekday=!0:(d=$e(r,a,o,u,l),s._a[Y]=d.year,s._dayOfYear=d.dayOfYear)),null!=e._dayOfYear&&(i=bt(e._a[Y],n[Y]),(e._dayOfYear>Ae(i)||0===e._dayOfYear)&&(m(e)._overflowDayOfYear=!0),h=Ze(i,0,e._dayOfYear),e._a[O]=h.getUTCMonth(),e._a[b]=h.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=c[t]=n[t];for(;t<7;t++)e._a[t]=c[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[x]&&0===e._a[T]&&0===e._a[N]&&0===e._a[Ne]&&(e._nextDay=!0,e._a[x]=0),e._d=(e._useUTC?Ze:je).apply(null,c),r=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[x]=24),e._w&&void 0!==e._w.d&&e._w.d!==r&&(m(e).weekdayMismatch=!0)}}function Tt(e){if(e._f===f.ISO_8601)St(e);else if(e._f===f.RFC_2822)Ot(e);else{e._a=[],m(e).empty=!0;for(var t,n,s,i,r,a=""+e._i,o=a.length,u=0,l=ae(e._f,e._locale).match(te)||[],h=l.length,d=0;de.valueOf():e.valueOf()"}),i.toJSON=function(){return this.isValid()?this.toISOString():null},i.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},i.unix=function(){return Math.floor(this.valueOf()/1e3)},i.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},i.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},i.eraName=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;nthis.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},i.isLocal=function(){return!!this.isValid()&&!this._isUTC},i.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},i.isUtc=At,i.isUTC=At,i.zoneAbbr=function(){return this._isUTC?"UTC":""},i.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},i.dates=e("dates accessor is deprecated. Use date instead.",ke),i.months=e("months accessor is deprecated. Use month instead",Ge),i.years=e("years accessor is deprecated. Use year instead",Ie),i.zone=e("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?(this.utcOffset(e="string"!=typeof e?-e:e,t),this):-this.utcOffset()}),i.isDSTShifted=e("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!o(this._isDSTShifted))return this._isDSTShifted;var e,t={};return $(t,this),(t=Nt(t))._a?(e=(t._isUTC?l:W)(t._a),this._isDSTShifted=this.isValid()&&0= 2.6.0. You are using Moment.js " + - moment.version + - ". See momentjs.com" - ); - } - - /************************************ - Unpacking - ************************************/ - - function charCodeToInt(charCode) { - if (charCode > 96) { - return charCode - 87; - } else if (charCode > 64) { - return charCode - 29; - } - return charCode - 48; - } - - function unpackBase60(string) { - var i = 0, - parts = string.split("."), - whole = parts[0], - fractional = parts[1] || "", - multiplier = 1, - num, - out = 0, - sign = 1; - - // handle negative numbers - if (string.charCodeAt(0) === 45) { - i = 1; - sign = -1; - } - - // handle digits before the decimal - for (i; i < whole.length; i++) { - num = charCodeToInt(whole.charCodeAt(i)); - out = 60 * out + num; - } - - // handle digits after the decimal - for (i = 0; i < fractional.length; i++) { - multiplier = multiplier / 60; - num = charCodeToInt(fractional.charCodeAt(i)); - out += num * multiplier; - } - - return out * sign; - } - - function arrayToInt(array) { - for (var i = 0; i < array.length; i++) { - array[i] = unpackBase60(array[i]); - } - } - - function intToUntil(array, length) { - for (var i = 0; i < length; i++) { - array[i] = Math.round((array[i - 1] || 0) + array[i] * 60000); // minutes to milliseconds - } - - array[length - 1] = Infinity; - } - - function mapIndices(source, indices) { - var out = [], - i; - - for (i = 0; i < indices.length; i++) { - out[i] = source[indices[i]]; - } - - return out; - } - - function unpack(string) { - var data = string.split("|"), - offsets = data[2].split(" "), - indices = data[3].split(""), - untils = data[4].split(" "); - - arrayToInt(offsets); - arrayToInt(indices); - arrayToInt(untils); - - intToUntil(untils, indices.length); - - return { - name: data[0], - abbrs: mapIndices(data[1].split(" "), indices), - offsets: mapIndices(offsets, indices), - untils: untils, - population: data[5] | 0, - }; - } - - /************************************ - Zone object - ************************************/ - - function Zone(packedString) { - if (packedString) { - this._set(unpack(packedString)); - } - } - - Zone.prototype = { - _set: function (unpacked) { - this.name = unpacked.name; - this.abbrs = unpacked.abbrs; - this.untils = unpacked.untils; - this.offsets = unpacked.offsets; - this.population = unpacked.population; - }, - - _index: function (timestamp) { - var target = +timestamp, - untils = this.untils, - i; - - for (i = 0; i < untils.length; i++) { - if (target < untils[i]) { - return i; - } - } - }, - - countries: function () { - var zone_name = this.name; - return Object.keys(countries).filter(function (country_code) { - return countries[country_code].zones.indexOf(zone_name) !== -1; - }); - }, - - parse: function (timestamp) { - var target = +timestamp, - offsets = this.offsets, - untils = this.untils, - max = untils.length - 1, - offset, - offsetNext, - offsetPrev, - i; - - for (i = 0; i < max; i++) { - offset = offsets[i]; - offsetNext = offsets[i + 1]; - offsetPrev = offsets[i ? i - 1 : i]; - - if (offset < offsetNext && tz.moveAmbiguousForward) { - offset = offsetNext; - } else if (offset > offsetPrev && tz.moveInvalidForward) { - offset = offsetPrev; - } - - if (target < untils[i] - offset * 60000) { - return offsets[i]; - } - } - - return offsets[max]; - }, - - abbr: function (mom) { - return this.abbrs[this._index(mom)]; - }, - - offset: function (mom) { - logError("zone.offset has been deprecated in favor of zone.utcOffset"); - return this.offsets[this._index(mom)]; - }, - - utcOffset: function (mom) { - return this.offsets[this._index(mom)]; - }, - }; - - /************************************ - Country object - ************************************/ - - function Country(country_name, zone_names) { - this.name = country_name; - this.zones = zone_names; - } - - /************************************ - Current Timezone - ************************************/ - - function OffsetAt(at) { - var timeString = at.toTimeString(); - var abbr = timeString.match(/\([a-z ]+\)/i); - if (abbr && abbr[0]) { - // 17:56:31 GMT-0600 (CST) - // 17:56:31 GMT-0600 (Central Standard Time) - abbr = abbr[0].match(/[A-Z]/g); - abbr = abbr ? abbr.join("") : undefined; - } else { - // 17:56:31 CST - // 17:56:31 GMT+0800 (台北標準時間) - abbr = timeString.match(/[A-Z]{3,5}/g); - abbr = abbr ? abbr[0] : undefined; - } - - if (abbr === "GMT") { - abbr = undefined; - } - - this.at = +at; - this.abbr = abbr; - this.offset = at.getTimezoneOffset(); - } - - function ZoneScore(zone) { - this.zone = zone; - this.offsetScore = 0; - this.abbrScore = 0; - } - - ZoneScore.prototype.scoreOffsetAt = function (offsetAt) { - this.offsetScore += Math.abs( - this.zone.utcOffset(offsetAt.at) - offsetAt.offset - ); - if (this.zone.abbr(offsetAt.at).replace(/[^A-Z]/g, "") !== offsetAt.abbr) { - this.abbrScore++; - } - }; - - function findChange(low, high) { - var mid, diff; - - while ((diff = (((high.at - low.at) / 12e4) | 0) * 6e4)) { - mid = new OffsetAt(new Date(low.at + diff)); - if (mid.offset === low.offset) { - low = mid; - } else { - high = mid; - } - } - - return low; - } - - function userOffsets() { - var startYear = new Date().getFullYear() - 2, - last = new OffsetAt(new Date(startYear, 0, 1)), - offsets = [last], - change, - next, - i; - - for (i = 1; i < 48; i++) { - next = new OffsetAt(new Date(startYear, i, 1)); - if (next.offset !== last.offset) { - change = findChange(last, next); - offsets.push(change); - offsets.push(new OffsetAt(new Date(change.at + 6e4))); - } - last = next; - } - - for (i = 0; i < 4; i++) { - offsets.push(new OffsetAt(new Date(startYear + i, 0, 1))); - offsets.push(new OffsetAt(new Date(startYear + i, 6, 1))); - } - - return offsets; - } - - function sortZoneScores(a, b) { - if (a.offsetScore !== b.offsetScore) { - return a.offsetScore - b.offsetScore; - } - if (a.abbrScore !== b.abbrScore) { - return a.abbrScore - b.abbrScore; - } - if (a.zone.population !== b.zone.population) { - return b.zone.population - a.zone.population; - } - return b.zone.name.localeCompare(a.zone.name); - } - - function addToGuesses(name, offsets) { - var i, offset; - arrayToInt(offsets); - for (i = 0; i < offsets.length; i++) { - offset = offsets[i]; - guesses[offset] = guesses[offset] || {}; - guesses[offset][name] = true; - } - } - - function guessesForUserOffsets(offsets) { - var offsetsLength = offsets.length, - filteredGuesses = {}, - out = [], - i, - j, - guessesOffset; - - for (i = 0; i < offsetsLength; i++) { - guessesOffset = guesses[offsets[i].offset] || {}; - for (j in guessesOffset) { - if (guessesOffset.hasOwnProperty(j)) { - filteredGuesses[j] = true; - } - } - } - - for (i in filteredGuesses) { - if (filteredGuesses.hasOwnProperty(i)) { - out.push(names[i]); - } - } - - return out; - } - - function rebuildGuess() { - // use Intl API when available and returning valid time zone - try { - var intlName = Intl.DateTimeFormat().resolvedOptions().timeZone; - if (intlName && intlName.length > 3) { - var name = names[normalizeName(intlName)]; - if (name) { - return name; - } - logError( - "Moment Timezone found " + - intlName + - " from the Intl api, but did not have that data loaded." - ); - } - } catch (e) { - // Intl unavailable, fall back to manual guessing. - } - - var offsets = userOffsets(), - offsetsLength = offsets.length, - guesses = guessesForUserOffsets(offsets), - zoneScores = [], - zoneScore, - i, - j; - - for (i = 0; i < guesses.length; i++) { - zoneScore = new ZoneScore(getZone(guesses[i]), offsetsLength); - for (j = 0; j < offsetsLength; j++) { - zoneScore.scoreOffsetAt(offsets[j]); - } - zoneScores.push(zoneScore); - } - - zoneScores.sort(sortZoneScores); - - return zoneScores.length > 0 ? zoneScores[0].zone.name : undefined; - } - - function guess(ignoreCache) { - if (!cachedGuess || ignoreCache) { - cachedGuess = rebuildGuess(); - } - return cachedGuess; - } - - /************************************ - Global Methods - ************************************/ - - function normalizeName(name) { - return (name || "").toLowerCase().replace(/\//g, "_"); - } - - function addZone(packed) { - var i, name, split, normalized; - - if (typeof packed === "string") { - packed = [packed]; - } - - for (i = 0; i < packed.length; i++) { - split = packed[i].split("|"); - name = split[0]; - normalized = normalizeName(name); - zones[normalized] = packed[i]; - names[normalized] = name; - addToGuesses(normalized, split[2].split(" ")); - } - } - - function getZone(name, caller) { - name = normalizeName(name); - - var zone = zones[name]; - var link; - - if (zone instanceof Zone) { - return zone; - } - - if (typeof zone === "string") { - zone = new Zone(zone); - zones[name] = zone; - return zone; - } - - // Pass getZone to prevent recursion more than 1 level deep - if ( - links[name] && - caller !== getZone && - (link = getZone(links[name], getZone)) - ) { - zone = zones[name] = new Zone(); - zone._set(link); - zone.name = names[name]; - return zone; - } - - return null; - } - - function getNames() { - var i, - out = []; - - for (i in names) { - if ( - names.hasOwnProperty(i) && - (zones[i] || zones[links[i]]) && - names[i] - ) { - out.push(names[i]); - } - } - - return out.sort(); - } - - function getCountryNames() { - return Object.keys(countries); - } - - function addLink(aliases) { - var i, alias, normal0, normal1; - - if (typeof aliases === "string") { - aliases = [aliases]; - } - - for (i = 0; i < aliases.length; i++) { - alias = aliases[i].split("|"); - - normal0 = normalizeName(alias[0]); - normal1 = normalizeName(alias[1]); - - links[normal0] = normal1; - names[normal0] = alias[0]; - - links[normal1] = normal0; - names[normal1] = alias[1]; - } - } - - function addCountries(data) { - var i, country_code, country_zones, split; - if (!data || !data.length) return; - for (i = 0; i < data.length; i++) { - split = data[i].split("|"); - country_code = split[0].toUpperCase(); - country_zones = split[1].split(" "); - countries[country_code] = new Country(country_code, country_zones); - } - } - - function getCountry(name) { - name = name.toUpperCase(); - return countries[name] || null; - } - - function zonesForCountry(country, with_offset) { - country = getCountry(country); - - if (!country) return null; - - var zones = country.zones.sort(); - - if (with_offset) { - return zones.map(function (zone_name) { - var zone = getZone(zone_name); - return { - name: zone_name, - offset: zone.utcOffset(new Date()), - }; - }); - } - - return zones; - } - - function loadData(data) { - addZone(data.zones); - addLink(data.links); - addCountries(data.countries); - tz.dataVersion = data.version; - } - - function zoneExists(name) { - if (!zoneExists.didShowError) { - zoneExists.didShowError = true; - logError( - "moment.tz.zoneExists('" + - name + - "') has been deprecated in favor of !moment.tz.zone('" + - name + - "')" - ); - } - return !!getZone(name); - } - - function needsOffset(m) { - var isUnixTimestamp = m._f === "X" || m._f === "x"; - return !!(m._a && m._tzm === undefined && !isUnixTimestamp); - } - - function logError(message) { - if (typeof console !== "undefined" && typeof console.error === "function") { - console.error(message); - } - } - - /************************************ - moment.tz namespace - ************************************/ - - function tz(input) { - var args = Array.prototype.slice.call(arguments, 0, -1), - name = arguments[arguments.length - 1], - zone = getZone(name), - out = moment.utc.apply(null, args); - - if (zone && !moment.isMoment(input) && needsOffset(out)) { - out.add(zone.parse(out), "minutes"); - } - - out.tz(name); - - return out; - } - - tz.version = VERSION; - tz.dataVersion = ""; - tz._zones = zones; - tz._links = links; - tz._names = names; - tz._countries = countries; - tz.add = addZone; - tz.link = addLink; - tz.load = loadData; - tz.zone = getZone; - tz.zoneExists = zoneExists; // deprecated in 0.1.0 - tz.guess = guess; - tz.names = getNames; - tz.Zone = Zone; - tz.unpack = unpack; - tz.unpackBase60 = unpackBase60; - tz.needsOffset = needsOffset; - tz.moveInvalidForward = true; - tz.moveAmbiguousForward = false; - tz.countries = getCountryNames; - tz.zonesForCountry = zonesForCountry; - - /************************************ - Interface with Moment.js - ************************************/ - - var fn = moment.fn; - - moment.tz = tz; - - moment.defaultZone = null; - - moment.updateOffset = function (mom, keepTime) { - var zone = moment.defaultZone, - offset; - - if (mom._z === undefined) { - if (zone && needsOffset(mom) && !mom._isUTC) { - mom._d = moment.utc(mom._a)._d; - mom.utc().add(zone.parse(mom), "minutes"); - } - mom._z = zone; - } - if (mom._z) { - offset = mom._z.utcOffset(mom); - if (Math.abs(offset) < 16) { - offset = offset / 60; - } - if (mom.utcOffset !== undefined) { - var z = mom._z; - mom.utcOffset(-offset, keepTime); - mom._z = z; - } else { - mom.zone(offset, keepTime); - } - } - }; - - fn.tz = function (name, keepTime) { - if (name) { - if (typeof name !== "string") { - throw new Error( - "Time zone name must be a string, got " + - name + - " [" + - typeof name + - "]" - ); - } - this._z = getZone(name); - if (this._z) { - moment.updateOffset(this, keepTime); - } else { - logError( - "Moment Timezone has no data for " + - name + - ". See http://momentjs.com/timezone/docs/#/data-loading/." - ); - } - return this; - } - if (this._z) { - return this._z.name; - } - }; - - function abbrWrap(old) { - return function () { - if (this._z) { - return this._z.abbr(this); - } - return old.call(this); - }; - } - - function resetZoneWrap(old) { - return function () { - this._z = null; - return old.apply(this, arguments); - }; - } - - function resetZoneWrap2(old) { - return function () { - if (arguments.length > 0) this._z = null; - return old.apply(this, arguments); - }; - } - - fn.zoneName = abbrWrap(fn.zoneName); - fn.zoneAbbr = abbrWrap(fn.zoneAbbr); - fn.utc = resetZoneWrap(fn.utc); - fn.local = resetZoneWrap(fn.local); - fn.utcOffset = resetZoneWrap2(fn.utcOffset); - - moment.tz.setDefault = function (name) { - if (major < 2 || (major === 2 && minor < 9)) { - logError( - "Moment Timezone setDefault() requires Moment.js >= 2.9.0. You are using Moment.js " + - moment.version + - "." - ); - } - moment.defaultZone = name ? getZone(name) : null; - return moment; - }; - - // Cloning a moment should include the _z property. - var momentProperties = moment.momentProperties; - if (Object.prototype.toString.call(momentProperties) === "[object Array]") { - // moment 2.8.1+ - momentProperties.push("_z"); - momentProperties.push("_a"); - } else if (momentProperties) { - // moment 2.7.0 - momentProperties._z = null; - } - - loadData({ - version: "2022g", - zones: [ - "Africa/Abidjan|GMT|0|0||48e5", - "Africa/Nairobi|EAT|-30|0||47e5", - "Africa/Algiers|CET|-10|0||26e5", - "Africa/Lagos|WAT|-10|0||17e6", - "Africa/Maputo|CAT|-20|0||26e5", - "Africa/Cairo|EET|-20|0||15e6", - "Africa/Casablanca|+00 +01|0 -10|01010101010101010101010101|1T0q0 mo0 gM0 LA0 WM0 jA0 e00 28M0 e00 2600 gM0 2600 e00 2600 gM0 2600 gM0 2600 e00 2600 gM0 2600 e00 28M0 e00|32e5", - "Europe/Paris|CET CEST|-10 -20|01010101010101010101010|1T0p0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0|11e6", - "Africa/Johannesburg|SAST|-20|0||84e5", - "Africa/Juba|EAT CAT|-30 -20|01|24nx0|", - "Africa/Khartoum|EAT CAT|-30 -20|01|1Usl0|51e5", - "Africa/Sao_Tome|GMT WAT|0 -10|010|1UQN0 2q00|", - "Africa/Windhoek|CAT WAT|-20 -10|010|1T3c0 11B0|32e4", - "America/Adak|HST HDT|a0 90|01010101010101010101010|1ST00 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|326", - "America/Anchorage|AKST AKDT|90 80|01010101010101010101010|1SSX0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|30e4", - "America/Santo_Domingo|AST|40|0||29e5", - "America/Fortaleza|-03|30|0||34e5", - "America/Asuncion|-03 -04|30 40|01010101010101010101010|1T0r0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0|28e5", - "America/Panama|EST|50|0||15e5", - "America/Mexico_City|CST CDT|60 50|0101010101010|1T3k0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0|20e6", - "America/Managua|CST|60|0||22e5", - "America/Caracas|-04|40|0||29e5", - "America/Lima|-05|50|0||11e6", - "America/Denver|MST MDT|70 60|01010101010101010101010|1SSV0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|26e5", - "America/Campo_Grande|-03 -04|30 40|010101|1SKr0 1zd0 On0 1HB0 FX0|77e4", - "America/Chicago|CST CDT|60 50|01010101010101010101010|1SSU0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|92e5", - "America/Chihuahua|MST MDT CST|70 60 60|0101010101012|1T3l0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0|81e4", - "America/Ciudad_Juarez|MST MDT CST|70 60 60|010101010101201010101010|1SSV0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1wn0 cm0 EP0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|", - "America/Phoenix|MST|70|0||42e5", - "America/Whitehorse|PST PDT MST|80 70 70|010101012|1SSW0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1z90|23e3", - "America/New_York|EST EDT|50 40|01010101010101010101010|1SST0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|21e6", - "America/Los_Angeles|PST PDT|80 70|01010101010101010101010|1SSW0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|15e6", - "America/Halifax|AST ADT|40 30|01010101010101010101010|1SSS0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|39e4", - "America/Godthab|-03 -02|30 20|01010101010101|1T0p0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0|17e3", - "America/Grand_Turk|AST EDT EST|40 40 50|012121212121212121212|1Vkv0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|37e2", - "America/Havana|CST CDT|50 40|01010101010101010101010|1SSR0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0|21e5", - "America/Mazatlan|MST MDT|70 60|0101010101010|1T3l0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0|44e4", - "America/Metlakatla|AKST AKDT PST|90 80 80|010120101010101010101010|1SSX0 1zb0 Op0 1zb0 uM0 jB0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|14e2", - "America/Miquelon|-03 -02|30 20|01010101010101010101010|1SSR0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|61e2", - "America/Noronha|-02|20|0||30e2", - "America/Ojinaga|MST MDT CST CDT|70 60 60 50|01010101010123232323232|1SSV0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1wn0 Rc0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|23e3", - "America/Santiago|-03 -04|30 40|01010101010101010101010|1Tk30 Ap0 1Nb0 Ap0 1zb0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 11B0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0|62e5", - "America/Sao_Paulo|-02 -03|20 30|010101|1SKq0 1zd0 On0 1HB0 FX0|20e6", - "Atlantic/Azores|-01 +00|10 0|01010101010101010101010|1T0p0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0|25e4", - "America/St_Johns|NST NDT|3u 2u|01010101010101010101010|1SSRu 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0|11e4", - "Antarctica/Casey|+11 +08|-b0 -80|0101010|1Vkh0 1o30 14k0 1kr0 12l0 1o01|10", - "Asia/Bangkok|+07|-70|0||15e6", - "Asia/Vladivostok|+10|-a0|0||60e4", - "Australia/Sydney|AEDT AEST|-b0 -a0|01010101010101010101010|1T340 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0|40e5", - "Asia/Tashkent|+05|-50|0||23e5", - "Pacific/Auckland|NZDT NZST|-d0 -c0|01010101010101010101010|1T320 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00|14e5", - "Europe/Istanbul|+03|-30|0||13e6", - "Antarctica/Troll|+00 +02|0 -20|01010101010101010101010|1T0p0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0|40", - "Asia/Dhaka|+06|-60|0||16e6", - "Asia/Amman|EET EEST +03|-20 -30 -30|0101010101012|1T2m0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 LA0 1C00|25e5", - "Asia/Kamchatka|+12|-c0|0||18e4", - "Asia/Dubai|+04|-40|0||39e5", - "Asia/Beirut|EET EEST|-20 -30|01010101010101010101010|1T0m0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0|22e5", - "Asia/Kuala_Lumpur|+08|-80|0||71e5", - "Asia/Kolkata|IST|-5u|0||15e6", - "Asia/Chita|+09|-90|0||33e4", - "Asia/Shanghai|CST|-80|0||23e6", - "Asia/Colombo|+0530|-5u|0||22e5", - "Asia/Damascus|EET EEST +03|-20 -30 -30|0101010101012|1T2m0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0|26e5", - "Asia/Famagusta|+03 EET EEST|-30 -20 -30|0121212121212121212121|1Urd0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0|", - "Asia/Gaza|EET EEST|-20 -30|01010101010101010101010|1SXX0 1qL0 WN0 1qL0 11c0 1on0 11B0 1o00 11A0 1qo0 XA0 1qp0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0|18e5", - "Asia/Hong_Kong|HKT|-80|0||73e5", - "Asia/Jakarta|WIB|-70|0||31e6", - "Asia/Jayapura|WIT|-90|0||26e4", - "Asia/Jerusalem|IST IDT|-20 -30|01010101010101010101010|1SXA0 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0 10N0 1oL0 10N0 1rz0|81e4", - "Asia/Kabul|+0430|-4u|0||46e5", - "Asia/Karachi|PKT|-50|0||24e6", - "Asia/Kathmandu|+0545|-5J|0||12e5", - "Asia/Sakhalin|+11|-b0|0||58e4", - "Asia/Makassar|WITA|-80|0||15e5", - "Asia/Manila|PST|-80|0||24e6", - "Europe/Athens|EET EEST|-20 -30|01010101010101010101010|1T0p0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0|35e5", - "Asia/Pyongyang|KST KST|-8u -90|01|1VGf0|29e5", - "Asia/Qyzylorda|+06 +05|-60 -50|01|1Xei0|73e4", - "Asia/Rangoon|+0630|-6u|0||48e5", - "Asia/Seoul|KST|-90|0||23e6", - "Asia/Tehran|+0330 +0430|-3u -4u|0101010101010|1SWIu 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0|14e6", - "Asia/Tokyo|JST|-90|0||38e6", - "Europe/Lisbon|WET WEST|0 -10|01010101010101010101010|1T0p0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0|27e5", - "Atlantic/Cape_Verde|-01|10|0||50e4", - "Australia/Adelaide|ACDT ACST|-au -9u|01010101010101010101010|1T34u 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0|11e5", - "Australia/Brisbane|AEST|-a0|0||20e5", - "Australia/Darwin|ACST|-9u|0||12e4", - "Australia/Eucla|+0845|-8J|0||368", - "Australia/Lord_Howe|+11 +1030|-b0 -au|01010101010101010101010|1T330 1cMu 1cLu 1fAu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1fzu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu|347", - "Australia/Perth|AWST|-80|0||18e5", - "Pacific/Easter|-05 -06|50 60|01010101010101010101010|1Tk30 Ap0 1Nb0 Ap0 1zb0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 11B0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0|30e2", - "Europe/Dublin|GMT IST|0 -10|01010101010101010101010|1T0p0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0|12e5", - "Etc/GMT-1|+01|-10|0||", - "Pacific/Fakaofo|+13|-d0|0||483", - "Pacific/Kiritimati|+14|-e0|0||51e2", - "Etc/GMT-2|+02|-20|0||", - "Pacific/Tahiti|-10|a0|0||18e4", - "Pacific/Niue|-11|b0|0||12e2", - "Etc/GMT+12|-12|c0|0||", - "Pacific/Galapagos|-06|60|0||25e3", - "Etc/GMT+7|-07|70|0||", - "Pacific/Pitcairn|-08|80|0||56", - "Pacific/Gambier|-09|90|0||125", - "Etc/UTC|UTC|0|0||", - "Europe/London|GMT BST|0 -10|01010101010101010101010|1T0p0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0|10e6", - "Europe/Chisinau|EET EEST|-20 -30|01010101010101010101010|1T0o0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0|67e4", - "Europe/Moscow|MSK|-30|0||16e6", - "Europe/Volgograd|+03 +04|-30 -40|010|1WQL0 5gn0|10e5", - "Pacific/Honolulu|HST|a0|0||37e4", - "MET|MET MEST|-10 -20|01010101010101010101010|1T0p0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0|", - "Pacific/Chatham|+1345 +1245|-dJ -cJ|01010101010101010101010|1T320 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00|600", - "Pacific/Apia|+14 +13|-e0 -d0|0101010101|1T320 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0|37e3", - "Pacific/Fiji|+13 +12|-d0 -c0|0101010101|1Swe0 1VA0 s00 1VA0 s00 20o0 pc0 2hc0 bc0|88e4", - "Pacific/Guam|ChST|-a0|0||17e4", - "Pacific/Marquesas|-0930|9u|0||86e2", - "Pacific/Pago_Pago|SST|b0|0||37e2", - "Pacific/Norfolk|+11 +12|-b0 -c0|010101010101010101|219P0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0|25e4", - "Pacific/Tongatapu|+14 +13|-e0 -d0|01|1Swd0|75e3", - ], - links: [ - "Africa/Abidjan|Africa/Accra", - "Africa/Abidjan|Africa/Bamako", - "Africa/Abidjan|Africa/Banjul", - "Africa/Abidjan|Africa/Bissau", - "Africa/Abidjan|Africa/Conakry", - "Africa/Abidjan|Africa/Dakar", - "Africa/Abidjan|Africa/Freetown", - "Africa/Abidjan|Africa/Lome", - "Africa/Abidjan|Africa/Monrovia", - "Africa/Abidjan|Africa/Nouakchott", - "Africa/Abidjan|Africa/Ouagadougou", - "Africa/Abidjan|Africa/Timbuktu", - "Africa/Abidjan|America/Danmarkshavn", - "Africa/Abidjan|Atlantic/Reykjavik", - "Africa/Abidjan|Atlantic/St_Helena", - "Africa/Abidjan|Etc/GMT", - "Africa/Abidjan|Etc/GMT+0", - "Africa/Abidjan|Etc/GMT-0", - "Africa/Abidjan|Etc/GMT0", - "Africa/Abidjan|Etc/Greenwich", - "Africa/Abidjan|GMT", - "Africa/Abidjan|GMT+0", - "Africa/Abidjan|GMT-0", - "Africa/Abidjan|GMT0", - "Africa/Abidjan|Greenwich", - "Africa/Abidjan|Iceland", - "Africa/Algiers|Africa/Tunis", - "Africa/Cairo|Africa/Tripoli", - "Africa/Cairo|Egypt", - "Africa/Cairo|Europe/Kaliningrad", - "Africa/Cairo|Libya", - "Africa/Casablanca|Africa/El_Aaiun", - "Africa/Johannesburg|Africa/Maseru", - "Africa/Johannesburg|Africa/Mbabane", - "Africa/Lagos|Africa/Bangui", - "Africa/Lagos|Africa/Brazzaville", - "Africa/Lagos|Africa/Douala", - "Africa/Lagos|Africa/Kinshasa", - "Africa/Lagos|Africa/Libreville", - "Africa/Lagos|Africa/Luanda", - "Africa/Lagos|Africa/Malabo", - "Africa/Lagos|Africa/Ndjamena", - "Africa/Lagos|Africa/Niamey", - "Africa/Lagos|Africa/Porto-Novo", - "Africa/Maputo|Africa/Blantyre", - "Africa/Maputo|Africa/Bujumbura", - "Africa/Maputo|Africa/Gaborone", - "Africa/Maputo|Africa/Harare", - "Africa/Maputo|Africa/Kigali", - "Africa/Maputo|Africa/Lubumbashi", - "Africa/Maputo|Africa/Lusaka", - "Africa/Nairobi|Africa/Addis_Ababa", - "Africa/Nairobi|Africa/Asmara", - "Africa/Nairobi|Africa/Asmera", - "Africa/Nairobi|Africa/Dar_es_Salaam", - "Africa/Nairobi|Africa/Djibouti", - "Africa/Nairobi|Africa/Kampala", - "Africa/Nairobi|Africa/Mogadishu", - "Africa/Nairobi|Indian/Antananarivo", - "Africa/Nairobi|Indian/Comoro", - "Africa/Nairobi|Indian/Mayotte", - "America/Adak|America/Atka", - "America/Adak|US/Aleutian", - "America/Anchorage|America/Juneau", - "America/Anchorage|America/Nome", - "America/Anchorage|America/Sitka", - "America/Anchorage|America/Yakutat", - "America/Anchorage|US/Alaska", - "America/Campo_Grande|America/Cuiaba", - "America/Caracas|America/Boa_Vista", - "America/Caracas|America/Guyana", - "America/Caracas|America/La_Paz", - "America/Caracas|America/Manaus", - "America/Caracas|America/Porto_Velho", - "America/Caracas|Brazil/West", - "America/Caracas|Etc/GMT+4", - "America/Chicago|America/Indiana/Knox", - "America/Chicago|America/Indiana/Tell_City", - "America/Chicago|America/Knox_IN", - "America/Chicago|America/Matamoros", - "America/Chicago|America/Menominee", - "America/Chicago|America/North_Dakota/Beulah", - "America/Chicago|America/North_Dakota/Center", - "America/Chicago|America/North_Dakota/New_Salem", - "America/Chicago|America/Rainy_River", - "America/Chicago|America/Rankin_Inlet", - "America/Chicago|America/Resolute", - "America/Chicago|America/Winnipeg", - "America/Chicago|CST6CDT", - "America/Chicago|Canada/Central", - "America/Chicago|US/Central", - "America/Chicago|US/Indiana-Starke", - "America/Denver|America/Boise", - "America/Denver|America/Cambridge_Bay", - "America/Denver|America/Edmonton", - "America/Denver|America/Inuvik", - "America/Denver|America/Shiprock", - "America/Denver|America/Yellowknife", - "America/Denver|Canada/Mountain", - "America/Denver|MST7MDT", - "America/Denver|Navajo", - "America/Denver|US/Mountain", - "America/Fortaleza|America/Araguaina", - "America/Fortaleza|America/Argentina/Buenos_Aires", - "America/Fortaleza|America/Argentina/Catamarca", - "America/Fortaleza|America/Argentina/ComodRivadavia", - "America/Fortaleza|America/Argentina/Cordoba", - "America/Fortaleza|America/Argentina/Jujuy", - "America/Fortaleza|America/Argentina/La_Rioja", - "America/Fortaleza|America/Argentina/Mendoza", - "America/Fortaleza|America/Argentina/Rio_Gallegos", - "America/Fortaleza|America/Argentina/Salta", - "America/Fortaleza|America/Argentina/San_Juan", - "America/Fortaleza|America/Argentina/San_Luis", - "America/Fortaleza|America/Argentina/Tucuman", - "America/Fortaleza|America/Argentina/Ushuaia", - "America/Fortaleza|America/Bahia", - "America/Fortaleza|America/Belem", - "America/Fortaleza|America/Buenos_Aires", - "America/Fortaleza|America/Catamarca", - "America/Fortaleza|America/Cayenne", - "America/Fortaleza|America/Cordoba", - "America/Fortaleza|America/Jujuy", - "America/Fortaleza|America/Maceio", - "America/Fortaleza|America/Mendoza", - "America/Fortaleza|America/Montevideo", - "America/Fortaleza|America/Paramaribo", - "America/Fortaleza|America/Punta_Arenas", - "America/Fortaleza|America/Recife", - "America/Fortaleza|America/Rosario", - "America/Fortaleza|America/Santarem", - "America/Fortaleza|Antarctica/Palmer", - "America/Fortaleza|Antarctica/Rothera", - "America/Fortaleza|Atlantic/Stanley", - "America/Fortaleza|Etc/GMT+3", - "America/Godthab|America/Nuuk", - "America/Halifax|America/Glace_Bay", - "America/Halifax|America/Goose_Bay", - "America/Halifax|America/Moncton", - "America/Halifax|America/Thule", - "America/Halifax|Atlantic/Bermuda", - "America/Halifax|Canada/Atlantic", - "America/Havana|Cuba", - "America/Lima|America/Bogota", - "America/Lima|America/Eirunepe", - "America/Lima|America/Guayaquil", - "America/Lima|America/Porto_Acre", - "America/Lima|America/Rio_Branco", - "America/Lima|Brazil/Acre", - "America/Lima|Etc/GMT+5", - "America/Los_Angeles|America/Ensenada", - "America/Los_Angeles|America/Santa_Isabel", - "America/Los_Angeles|America/Tijuana", - "America/Los_Angeles|America/Vancouver", - "America/Los_Angeles|Canada/Pacific", - "America/Los_Angeles|Mexico/BajaNorte", - "America/Los_Angeles|PST8PDT", - "America/Los_Angeles|US/Pacific", - "America/Managua|America/Belize", - "America/Managua|America/Costa_Rica", - "America/Managua|America/El_Salvador", - "America/Managua|America/Guatemala", - "America/Managua|America/Regina", - "America/Managua|America/Swift_Current", - "America/Managua|America/Tegucigalpa", - "America/Managua|Canada/Saskatchewan", - "America/Mazatlan|Mexico/BajaSur", - "America/Mexico_City|America/Bahia_Banderas", - "America/Mexico_City|America/Merida", - "America/Mexico_City|America/Monterrey", - "America/Mexico_City|Mexico/General", - "America/New_York|America/Detroit", - "America/New_York|America/Fort_Wayne", - "America/New_York|America/Indiana/Indianapolis", - "America/New_York|America/Indiana/Marengo", - "America/New_York|America/Indiana/Petersburg", - "America/New_York|America/Indiana/Vevay", - "America/New_York|America/Indiana/Vincennes", - "America/New_York|America/Indiana/Winamac", - "America/New_York|America/Indianapolis", - "America/New_York|America/Iqaluit", - "America/New_York|America/Kentucky/Louisville", - "America/New_York|America/Kentucky/Monticello", - "America/New_York|America/Louisville", - "America/New_York|America/Montreal", - "America/New_York|America/Nassau", - "America/New_York|America/Nipigon", - "America/New_York|America/Pangnirtung", - "America/New_York|America/Port-au-Prince", - "America/New_York|America/Thunder_Bay", - "America/New_York|America/Toronto", - "America/New_York|Canada/Eastern", - "America/New_York|EST5EDT", - "America/New_York|US/East-Indiana", - "America/New_York|US/Eastern", - "America/New_York|US/Michigan", - "America/Noronha|Atlantic/South_Georgia", - "America/Noronha|Brazil/DeNoronha", - "America/Noronha|Etc/GMT+2", - "America/Panama|America/Atikokan", - "America/Panama|America/Cancun", - "America/Panama|America/Cayman", - "America/Panama|America/Coral_Harbour", - "America/Panama|America/Jamaica", - "America/Panama|EST", - "America/Panama|Jamaica", - "America/Phoenix|America/Creston", - "America/Phoenix|America/Dawson_Creek", - "America/Phoenix|America/Fort_Nelson", - "America/Phoenix|America/Hermosillo", - "America/Phoenix|MST", - "America/Phoenix|US/Arizona", - "America/Santiago|Chile/Continental", - "America/Santo_Domingo|America/Anguilla", - "America/Santo_Domingo|America/Antigua", - "America/Santo_Domingo|America/Aruba", - "America/Santo_Domingo|America/Barbados", - "America/Santo_Domingo|America/Blanc-Sablon", - "America/Santo_Domingo|America/Curacao", - "America/Santo_Domingo|America/Dominica", - "America/Santo_Domingo|America/Grenada", - "America/Santo_Domingo|America/Guadeloupe", - "America/Santo_Domingo|America/Kralendijk", - "America/Santo_Domingo|America/Lower_Princes", - "America/Santo_Domingo|America/Marigot", - "America/Santo_Domingo|America/Martinique", - "America/Santo_Domingo|America/Montserrat", - "America/Santo_Domingo|America/Port_of_Spain", - "America/Santo_Domingo|America/Puerto_Rico", - "America/Santo_Domingo|America/St_Barthelemy", - "America/Santo_Domingo|America/St_Kitts", - "America/Santo_Domingo|America/St_Lucia", - "America/Santo_Domingo|America/St_Thomas", - "America/Santo_Domingo|America/St_Vincent", - "America/Santo_Domingo|America/Tortola", - "America/Santo_Domingo|America/Virgin", - "America/Sao_Paulo|Brazil/East", - "America/St_Johns|Canada/Newfoundland", - "America/Whitehorse|America/Dawson", - "America/Whitehorse|Canada/Yukon", - "Asia/Bangkok|Antarctica/Davis", - "Asia/Bangkok|Asia/Barnaul", - "Asia/Bangkok|Asia/Ho_Chi_Minh", - "Asia/Bangkok|Asia/Hovd", - "Asia/Bangkok|Asia/Krasnoyarsk", - "Asia/Bangkok|Asia/Novokuznetsk", - "Asia/Bangkok|Asia/Novosibirsk", - "Asia/Bangkok|Asia/Phnom_Penh", - "Asia/Bangkok|Asia/Saigon", - "Asia/Bangkok|Asia/Tomsk", - "Asia/Bangkok|Asia/Vientiane", - "Asia/Bangkok|Etc/GMT-7", - "Asia/Bangkok|Indian/Christmas", - "Asia/Chita|Asia/Dili", - "Asia/Chita|Asia/Khandyga", - "Asia/Chita|Asia/Yakutsk", - "Asia/Chita|Etc/GMT-9", - "Asia/Chita|Pacific/Palau", - "Asia/Dhaka|Antarctica/Vostok", - "Asia/Dhaka|Asia/Almaty", - "Asia/Dhaka|Asia/Bishkek", - "Asia/Dhaka|Asia/Dacca", - "Asia/Dhaka|Asia/Kashgar", - "Asia/Dhaka|Asia/Omsk", - "Asia/Dhaka|Asia/Qostanay", - "Asia/Dhaka|Asia/Thimbu", - "Asia/Dhaka|Asia/Thimphu", - "Asia/Dhaka|Asia/Urumqi", - "Asia/Dhaka|Etc/GMT-6", - "Asia/Dhaka|Indian/Chagos", - "Asia/Dubai|Asia/Baku", - "Asia/Dubai|Asia/Muscat", - "Asia/Dubai|Asia/Tbilisi", - "Asia/Dubai|Asia/Yerevan", - "Asia/Dubai|Etc/GMT-4", - "Asia/Dubai|Europe/Astrakhan", - "Asia/Dubai|Europe/Samara", - "Asia/Dubai|Europe/Saratov", - "Asia/Dubai|Europe/Ulyanovsk", - "Asia/Dubai|Indian/Mahe", - "Asia/Dubai|Indian/Mauritius", - "Asia/Dubai|Indian/Reunion", - "Asia/Gaza|Asia/Hebron", - "Asia/Hong_Kong|Hongkong", - "Asia/Jakarta|Asia/Pontianak", - "Asia/Jerusalem|Asia/Tel_Aviv", - "Asia/Jerusalem|Israel", - "Asia/Kamchatka|Asia/Anadyr", - "Asia/Kamchatka|Etc/GMT-12", - "Asia/Kamchatka|Kwajalein", - "Asia/Kamchatka|Pacific/Funafuti", - "Asia/Kamchatka|Pacific/Kwajalein", - "Asia/Kamchatka|Pacific/Majuro", - "Asia/Kamchatka|Pacific/Nauru", - "Asia/Kamchatka|Pacific/Tarawa", - "Asia/Kamchatka|Pacific/Wake", - "Asia/Kamchatka|Pacific/Wallis", - "Asia/Kathmandu|Asia/Katmandu", - "Asia/Kolkata|Asia/Calcutta", - "Asia/Kuala_Lumpur|Asia/Brunei", - "Asia/Kuala_Lumpur|Asia/Choibalsan", - "Asia/Kuala_Lumpur|Asia/Irkutsk", - "Asia/Kuala_Lumpur|Asia/Kuching", - "Asia/Kuala_Lumpur|Asia/Singapore", - "Asia/Kuala_Lumpur|Asia/Ulaanbaatar", - "Asia/Kuala_Lumpur|Asia/Ulan_Bator", - "Asia/Kuala_Lumpur|Etc/GMT-8", - "Asia/Kuala_Lumpur|Singapore", - "Asia/Makassar|Asia/Ujung_Pandang", - "Asia/Rangoon|Asia/Yangon", - "Asia/Rangoon|Indian/Cocos", - "Asia/Sakhalin|Asia/Magadan", - "Asia/Sakhalin|Asia/Srednekolymsk", - "Asia/Sakhalin|Etc/GMT-11", - "Asia/Sakhalin|Pacific/Bougainville", - "Asia/Sakhalin|Pacific/Efate", - "Asia/Sakhalin|Pacific/Guadalcanal", - "Asia/Sakhalin|Pacific/Kosrae", - "Asia/Sakhalin|Pacific/Noumea", - "Asia/Sakhalin|Pacific/Pohnpei", - "Asia/Sakhalin|Pacific/Ponape", - "Asia/Seoul|ROK", - "Asia/Shanghai|Asia/Chongqing", - "Asia/Shanghai|Asia/Chungking", - "Asia/Shanghai|Asia/Harbin", - "Asia/Shanghai|Asia/Macao", - "Asia/Shanghai|Asia/Macau", - "Asia/Shanghai|Asia/Taipei", - "Asia/Shanghai|PRC", - "Asia/Shanghai|ROC", - "Asia/Tashkent|Antarctica/Mawson", - "Asia/Tashkent|Asia/Aqtau", - "Asia/Tashkent|Asia/Aqtobe", - "Asia/Tashkent|Asia/Ashgabat", - "Asia/Tashkent|Asia/Ashkhabad", - "Asia/Tashkent|Asia/Atyrau", - "Asia/Tashkent|Asia/Dushanbe", - "Asia/Tashkent|Asia/Oral", - "Asia/Tashkent|Asia/Samarkand", - "Asia/Tashkent|Asia/Yekaterinburg", - "Asia/Tashkent|Etc/GMT-5", - "Asia/Tashkent|Indian/Kerguelen", - "Asia/Tashkent|Indian/Maldives", - "Asia/Tehran|Iran", - "Asia/Tokyo|Japan", - "Asia/Vladivostok|Antarctica/DumontDUrville", - "Asia/Vladivostok|Asia/Ust-Nera", - "Asia/Vladivostok|Etc/GMT-10", - "Asia/Vladivostok|Pacific/Chuuk", - "Asia/Vladivostok|Pacific/Port_Moresby", - "Asia/Vladivostok|Pacific/Truk", - "Asia/Vladivostok|Pacific/Yap", - "Atlantic/Azores|America/Scoresbysund", - "Atlantic/Cape_Verde|Etc/GMT+1", - "Australia/Adelaide|Australia/Broken_Hill", - "Australia/Adelaide|Australia/South", - "Australia/Adelaide|Australia/Yancowinna", - "Australia/Brisbane|Australia/Lindeman", - "Australia/Brisbane|Australia/Queensland", - "Australia/Darwin|Australia/North", - "Australia/Lord_Howe|Australia/LHI", - "Australia/Perth|Australia/West", - "Australia/Sydney|Antarctica/Macquarie", - "Australia/Sydney|Australia/ACT", - "Australia/Sydney|Australia/Canberra", - "Australia/Sydney|Australia/Currie", - "Australia/Sydney|Australia/Hobart", - "Australia/Sydney|Australia/Melbourne", - "Australia/Sydney|Australia/NSW", - "Australia/Sydney|Australia/Tasmania", - "Australia/Sydney|Australia/Victoria", - "Etc/UTC|Etc/UCT", - "Etc/UTC|Etc/Universal", - "Etc/UTC|Etc/Zulu", - "Etc/UTC|UCT", - "Etc/UTC|UTC", - "Etc/UTC|Universal", - "Etc/UTC|Zulu", - "Europe/Athens|Asia/Nicosia", - "Europe/Athens|EET", - "Europe/Athens|Europe/Bucharest", - "Europe/Athens|Europe/Helsinki", - "Europe/Athens|Europe/Kiev", - "Europe/Athens|Europe/Kyiv", - "Europe/Athens|Europe/Mariehamn", - "Europe/Athens|Europe/Nicosia", - "Europe/Athens|Europe/Riga", - "Europe/Athens|Europe/Sofia", - "Europe/Athens|Europe/Tallinn", - "Europe/Athens|Europe/Uzhgorod", - "Europe/Athens|Europe/Vilnius", - "Europe/Athens|Europe/Zaporozhye", - "Europe/Chisinau|Europe/Tiraspol", - "Europe/Dublin|Eire", - "Europe/Istanbul|Antarctica/Syowa", - "Europe/Istanbul|Asia/Aden", - "Europe/Istanbul|Asia/Baghdad", - "Europe/Istanbul|Asia/Bahrain", - "Europe/Istanbul|Asia/Istanbul", - "Europe/Istanbul|Asia/Kuwait", - "Europe/Istanbul|Asia/Qatar", - "Europe/Istanbul|Asia/Riyadh", - "Europe/Istanbul|Etc/GMT-3", - "Europe/Istanbul|Europe/Kirov", - "Europe/Istanbul|Europe/Minsk", - "Europe/Istanbul|Turkey", - "Europe/Lisbon|Atlantic/Canary", - "Europe/Lisbon|Atlantic/Faeroe", - "Europe/Lisbon|Atlantic/Faroe", - "Europe/Lisbon|Atlantic/Madeira", - "Europe/Lisbon|Portugal", - "Europe/Lisbon|WET", - "Europe/London|Europe/Belfast", - "Europe/London|Europe/Guernsey", - "Europe/London|Europe/Isle_of_Man", - "Europe/London|Europe/Jersey", - "Europe/London|GB", - "Europe/London|GB-Eire", - "Europe/Moscow|Europe/Simferopol", - "Europe/Moscow|W-SU", - "Europe/Paris|Africa/Ceuta", - "Europe/Paris|Arctic/Longyearbyen", - "Europe/Paris|Atlantic/Jan_Mayen", - "Europe/Paris|CET", - "Europe/Paris|Europe/Amsterdam", - "Europe/Paris|Europe/Andorra", - "Europe/Paris|Europe/Belgrade", - "Europe/Paris|Europe/Berlin", - "Europe/Paris|Europe/Bratislava", - "Europe/Paris|Europe/Brussels", - "Europe/Paris|Europe/Budapest", - "Europe/Paris|Europe/Busingen", - "Europe/Paris|Europe/Copenhagen", - "Europe/Paris|Europe/Gibraltar", - "Europe/Paris|Europe/Ljubljana", - "Europe/Paris|Europe/Luxembourg", - "Europe/Paris|Europe/Madrid", - "Europe/Paris|Europe/Malta", - "Europe/Paris|Europe/Monaco", - "Europe/Paris|Europe/Oslo", - "Europe/Paris|Europe/Podgorica", - "Europe/Paris|Europe/Prague", - "Europe/Paris|Europe/Rome", - "Europe/Paris|Europe/San_Marino", - "Europe/Paris|Europe/Sarajevo", - "Europe/Paris|Europe/Skopje", - "Europe/Paris|Europe/Stockholm", - "Europe/Paris|Europe/Tirane", - "Europe/Paris|Europe/Vaduz", - "Europe/Paris|Europe/Vatican", - "Europe/Paris|Europe/Vienna", - "Europe/Paris|Europe/Warsaw", - "Europe/Paris|Europe/Zagreb", - "Europe/Paris|Europe/Zurich", - "Europe/Paris|Poland", - "Pacific/Auckland|Antarctica/McMurdo", - "Pacific/Auckland|Antarctica/South_Pole", - "Pacific/Auckland|NZ", - "Pacific/Chatham|NZ-CHAT", - "Pacific/Easter|Chile/EasterIsland", - "Pacific/Fakaofo|Etc/GMT-13", - "Pacific/Fakaofo|Pacific/Enderbury", - "Pacific/Fakaofo|Pacific/Kanton", - "Pacific/Galapagos|Etc/GMT+6", - "Pacific/Gambier|Etc/GMT+9", - "Pacific/Guam|Pacific/Saipan", - "Pacific/Honolulu|HST", - "Pacific/Honolulu|Pacific/Johnston", - "Pacific/Honolulu|US/Hawaii", - "Pacific/Kiritimati|Etc/GMT-14", - "Pacific/Niue|Etc/GMT+11", - "Pacific/Pago_Pago|Pacific/Midway", - "Pacific/Pago_Pago|Pacific/Samoa", - "Pacific/Pago_Pago|US/Samoa", - "Pacific/Pitcairn|Etc/GMT+8", - "Pacific/Tahiti|Etc/GMT+10", - "Pacific/Tahiti|Pacific/Rarotonga", - ], - countries: [ - "AD|Europe/Andorra", - "AE|Asia/Dubai", - "AF|Asia/Kabul", - "AG|America/Puerto_Rico America/Antigua", - "AI|America/Puerto_Rico America/Anguilla", - "AL|Europe/Tirane", - "AM|Asia/Yerevan", - "AO|Africa/Lagos Africa/Luanda", - "AQ|Antarctica/Casey Antarctica/Davis Antarctica/Mawson Antarctica/Palmer Antarctica/Rothera Antarctica/Troll Asia/Urumqi Pacific/Auckland Pacific/Port_Moresby Asia/Riyadh Antarctica/McMurdo Antarctica/DumontDUrville Antarctica/Syowa Antarctica/Vostok", - "AR|America/Argentina/Buenos_Aires America/Argentina/Cordoba America/Argentina/Salta America/Argentina/Jujuy America/Argentina/Tucuman America/Argentina/Catamarca America/Argentina/La_Rioja America/Argentina/San_Juan America/Argentina/Mendoza America/Argentina/San_Luis America/Argentina/Rio_Gallegos America/Argentina/Ushuaia", - "AS|Pacific/Pago_Pago", - "AT|Europe/Vienna", - "AU|Australia/Lord_Howe Antarctica/Macquarie Australia/Hobart Australia/Melbourne Australia/Sydney Australia/Broken_Hill Australia/Brisbane Australia/Lindeman Australia/Adelaide Australia/Darwin Australia/Perth Australia/Eucla", - "AW|America/Puerto_Rico America/Aruba", - "AX|Europe/Helsinki Europe/Mariehamn", - "AZ|Asia/Baku", - "BA|Europe/Belgrade Europe/Sarajevo", - "BB|America/Barbados", - "BD|Asia/Dhaka", - "BE|Europe/Brussels", - "BF|Africa/Abidjan Africa/Ouagadougou", - "BG|Europe/Sofia", - "BH|Asia/Qatar Asia/Bahrain", - "BI|Africa/Maputo Africa/Bujumbura", - "BJ|Africa/Lagos Africa/Porto-Novo", - "BL|America/Puerto_Rico America/St_Barthelemy", - "BM|Atlantic/Bermuda", - "BN|Asia/Kuching Asia/Brunei", - "BO|America/La_Paz", - "BQ|America/Puerto_Rico America/Kralendijk", - "BR|America/Noronha America/Belem America/Fortaleza America/Recife America/Araguaina America/Maceio America/Bahia America/Sao_Paulo America/Campo_Grande America/Cuiaba America/Santarem America/Porto_Velho America/Boa_Vista America/Manaus America/Eirunepe America/Rio_Branco", - "BS|America/Toronto America/Nassau", - "BT|Asia/Thimphu", - "BW|Africa/Maputo Africa/Gaborone", - "BY|Europe/Minsk", - "BZ|America/Belize", - "CA|America/St_Johns America/Halifax America/Glace_Bay America/Moncton America/Goose_Bay America/Toronto America/Iqaluit America/Winnipeg America/Resolute America/Rankin_Inlet America/Regina America/Swift_Current America/Edmonton America/Cambridge_Bay America/Yellowknife America/Inuvik America/Dawson_Creek America/Fort_Nelson America/Whitehorse America/Dawson America/Vancouver America/Panama America/Puerto_Rico America/Phoenix America/Blanc-Sablon America/Atikokan America/Creston", - "CC|Asia/Yangon Indian/Cocos", - "CD|Africa/Maputo Africa/Lagos Africa/Kinshasa Africa/Lubumbashi", - "CF|Africa/Lagos Africa/Bangui", - "CG|Africa/Lagos Africa/Brazzaville", - "CH|Europe/Zurich", - "CI|Africa/Abidjan", - "CK|Pacific/Rarotonga", - "CL|America/Santiago America/Punta_Arenas Pacific/Easter", - "CM|Africa/Lagos Africa/Douala", - "CN|Asia/Shanghai Asia/Urumqi", - "CO|America/Bogota", - "CR|America/Costa_Rica", - "CU|America/Havana", - "CV|Atlantic/Cape_Verde", - "CW|America/Puerto_Rico America/Curacao", - "CX|Asia/Bangkok Indian/Christmas", - "CY|Asia/Nicosia Asia/Famagusta", - "CZ|Europe/Prague", - "DE|Europe/Zurich Europe/Berlin Europe/Busingen", - "DJ|Africa/Nairobi Africa/Djibouti", - "DK|Europe/Berlin Europe/Copenhagen", - "DM|America/Puerto_Rico America/Dominica", - "DO|America/Santo_Domingo", - "DZ|Africa/Algiers", - "EC|America/Guayaquil Pacific/Galapagos", - "EE|Europe/Tallinn", - "EG|Africa/Cairo", - "EH|Africa/El_Aaiun", - "ER|Africa/Nairobi Africa/Asmara", - "ES|Europe/Madrid Africa/Ceuta Atlantic/Canary", - "ET|Africa/Nairobi Africa/Addis_Ababa", - "FI|Europe/Helsinki", - "FJ|Pacific/Fiji", - "FK|Atlantic/Stanley", - "FM|Pacific/Kosrae Pacific/Port_Moresby Pacific/Guadalcanal Pacific/Chuuk Pacific/Pohnpei", - "FO|Atlantic/Faroe", - "FR|Europe/Paris", - "GA|Africa/Lagos Africa/Libreville", - "GB|Europe/London", - "GD|America/Puerto_Rico America/Grenada", - "GE|Asia/Tbilisi", - "GF|America/Cayenne", - "GG|Europe/London Europe/Guernsey", - "GH|Africa/Abidjan Africa/Accra", - "GI|Europe/Gibraltar", - "GL|America/Nuuk America/Danmarkshavn America/Scoresbysund America/Thule", - "GM|Africa/Abidjan Africa/Banjul", - "GN|Africa/Abidjan Africa/Conakry", - "GP|America/Puerto_Rico America/Guadeloupe", - "GQ|Africa/Lagos Africa/Malabo", - "GR|Europe/Athens", - "GS|Atlantic/South_Georgia", - "GT|America/Guatemala", - "GU|Pacific/Guam", - "GW|Africa/Bissau", - "GY|America/Guyana", - "HK|Asia/Hong_Kong", - "HN|America/Tegucigalpa", - "HR|Europe/Belgrade Europe/Zagreb", - "HT|America/Port-au-Prince", - "HU|Europe/Budapest", - "ID|Asia/Jakarta Asia/Pontianak Asia/Makassar Asia/Jayapura", - "IE|Europe/Dublin", - "IL|Asia/Jerusalem", - "IM|Europe/London Europe/Isle_of_Man", - "IN|Asia/Kolkata", - "IO|Indian/Chagos", - "IQ|Asia/Baghdad", - "IR|Asia/Tehran", - "IS|Africa/Abidjan Atlantic/Reykjavik", - "IT|Europe/Rome", - "JE|Europe/London Europe/Jersey", - "JM|America/Jamaica", - "JO|Asia/Amman", - "JP|Asia/Tokyo", - "KE|Africa/Nairobi", - "KG|Asia/Bishkek", - "KH|Asia/Bangkok Asia/Phnom_Penh", - "KI|Pacific/Tarawa Pacific/Kanton Pacific/Kiritimati", - "KM|Africa/Nairobi Indian/Comoro", - "KN|America/Puerto_Rico America/St_Kitts", - "KP|Asia/Pyongyang", - "KR|Asia/Seoul", - "KW|Asia/Riyadh Asia/Kuwait", - "KY|America/Panama America/Cayman", - "KZ|Asia/Almaty Asia/Qyzylorda Asia/Qostanay Asia/Aqtobe Asia/Aqtau Asia/Atyrau Asia/Oral", - "LA|Asia/Bangkok Asia/Vientiane", - "LB|Asia/Beirut", - "LC|America/Puerto_Rico America/St_Lucia", - "LI|Europe/Zurich Europe/Vaduz", - "LK|Asia/Colombo", - "LR|Africa/Monrovia", - "LS|Africa/Johannesburg Africa/Maseru", - "LT|Europe/Vilnius", - "LU|Europe/Brussels Europe/Luxembourg", - "LV|Europe/Riga", - "LY|Africa/Tripoli", - "MA|Africa/Casablanca", - "MC|Europe/Paris Europe/Monaco", - "MD|Europe/Chisinau", - "ME|Europe/Belgrade Europe/Podgorica", - "MF|America/Puerto_Rico America/Marigot", - "MG|Africa/Nairobi Indian/Antananarivo", - "MH|Pacific/Tarawa Pacific/Kwajalein Pacific/Majuro", - "MK|Europe/Belgrade Europe/Skopje", - "ML|Africa/Abidjan Africa/Bamako", - "MM|Asia/Yangon", - "MN|Asia/Ulaanbaatar Asia/Hovd Asia/Choibalsan", - "MO|Asia/Macau", - "MP|Pacific/Guam Pacific/Saipan", - "MQ|America/Martinique", - "MR|Africa/Abidjan Africa/Nouakchott", - "MS|America/Puerto_Rico America/Montserrat", - "MT|Europe/Malta", - "MU|Indian/Mauritius", - "MV|Indian/Maldives", - "MW|Africa/Maputo Africa/Blantyre", - "MX|America/Mexico_City America/Cancun America/Merida America/Monterrey America/Matamoros America/Chihuahua America/Ciudad_Juarez America/Ojinaga America/Mazatlan America/Bahia_Banderas America/Hermosillo America/Tijuana", - "MY|Asia/Kuching Asia/Singapore Asia/Kuala_Lumpur", - "MZ|Africa/Maputo", - "NA|Africa/Windhoek", - "NC|Pacific/Noumea", - "NE|Africa/Lagos Africa/Niamey", - "NF|Pacific/Norfolk", - "NG|Africa/Lagos", - "NI|America/Managua", - "NL|Europe/Brussels Europe/Amsterdam", - "NO|Europe/Berlin Europe/Oslo", - "NP|Asia/Kathmandu", - "NR|Pacific/Nauru", - "NU|Pacific/Niue", - "NZ|Pacific/Auckland Pacific/Chatham", - "OM|Asia/Dubai Asia/Muscat", - "PA|America/Panama", - "PE|America/Lima", - "PF|Pacific/Tahiti Pacific/Marquesas Pacific/Gambier", - "PG|Pacific/Port_Moresby Pacific/Bougainville", - "PH|Asia/Manila", - "PK|Asia/Karachi", - "PL|Europe/Warsaw", - "PM|America/Miquelon", - "PN|Pacific/Pitcairn", - "PR|America/Puerto_Rico", - "PS|Asia/Gaza Asia/Hebron", - "PT|Europe/Lisbon Atlantic/Madeira Atlantic/Azores", - "PW|Pacific/Palau", - "PY|America/Asuncion", - "QA|Asia/Qatar", - "RE|Asia/Dubai Indian/Reunion", - "RO|Europe/Bucharest", - "RS|Europe/Belgrade", - "RU|Europe/Kaliningrad Europe/Moscow Europe/Simferopol Europe/Kirov Europe/Volgograd Europe/Astrakhan Europe/Saratov Europe/Ulyanovsk Europe/Samara Asia/Yekaterinburg Asia/Omsk Asia/Novosibirsk Asia/Barnaul Asia/Tomsk Asia/Novokuznetsk Asia/Krasnoyarsk Asia/Irkutsk Asia/Chita Asia/Yakutsk Asia/Khandyga Asia/Vladivostok Asia/Ust-Nera Asia/Magadan Asia/Sakhalin Asia/Srednekolymsk Asia/Kamchatka Asia/Anadyr", - "RW|Africa/Maputo Africa/Kigali", - "SA|Asia/Riyadh", - "SB|Pacific/Guadalcanal", - "SC|Asia/Dubai Indian/Mahe", - "SD|Africa/Khartoum", - "SE|Europe/Berlin Europe/Stockholm", - "SG|Asia/Singapore", - "SH|Africa/Abidjan Atlantic/St_Helena", - "SI|Europe/Belgrade Europe/Ljubljana", - "SJ|Europe/Berlin Arctic/Longyearbyen", - "SK|Europe/Prague Europe/Bratislava", - "SL|Africa/Abidjan Africa/Freetown", - "SM|Europe/Rome Europe/San_Marino", - "SN|Africa/Abidjan Africa/Dakar", - "SO|Africa/Nairobi Africa/Mogadishu", - "SR|America/Paramaribo", - "SS|Africa/Juba", - "ST|Africa/Sao_Tome", - "SV|America/El_Salvador", - "SX|America/Puerto_Rico America/Lower_Princes", - "SY|Asia/Damascus", - "SZ|Africa/Johannesburg Africa/Mbabane", - "TC|America/Grand_Turk", - "TD|Africa/Ndjamena", - "TF|Asia/Dubai Indian/Maldives Indian/Kerguelen", - "TG|Africa/Abidjan Africa/Lome", - "TH|Asia/Bangkok", - "TJ|Asia/Dushanbe", - "TK|Pacific/Fakaofo", - "TL|Asia/Dili", - "TM|Asia/Ashgabat", - "TN|Africa/Tunis", - "TO|Pacific/Tongatapu", - "TR|Europe/Istanbul", - "TT|America/Puerto_Rico America/Port_of_Spain", - "TV|Pacific/Tarawa Pacific/Funafuti", - "TW|Asia/Taipei", - "TZ|Africa/Nairobi Africa/Dar_es_Salaam", - "UA|Europe/Simferopol Europe/Kyiv", - "UG|Africa/Nairobi Africa/Kampala", - "UM|Pacific/Pago_Pago Pacific/Tarawa Pacific/Honolulu Pacific/Midway Pacific/Wake", - "US|America/New_York America/Detroit America/Kentucky/Louisville America/Kentucky/Monticello America/Indiana/Indianapolis America/Indiana/Vincennes America/Indiana/Winamac America/Indiana/Marengo America/Indiana/Petersburg America/Indiana/Vevay America/Chicago America/Indiana/Tell_City America/Indiana/Knox America/Menominee America/North_Dakota/Center America/North_Dakota/New_Salem America/North_Dakota/Beulah America/Denver America/Boise America/Phoenix America/Los_Angeles America/Anchorage America/Juneau America/Sitka America/Metlakatla America/Yakutat America/Nome America/Adak Pacific/Honolulu", - "UY|America/Montevideo", - "UZ|Asia/Samarkand Asia/Tashkent", - "VA|Europe/Rome Europe/Vatican", - "VC|America/Puerto_Rico America/St_Vincent", - "VE|America/Caracas", - "VG|America/Puerto_Rico America/Tortola", - "VI|America/Puerto_Rico America/St_Thomas", - "VN|Asia/Bangkok Asia/Ho_Chi_Minh", - "VU|Pacific/Efate", - "WF|Pacific/Tarawa Pacific/Wallis", - "WS|Pacific/Apia", - "YE|Asia/Riyadh Asia/Aden", - "YT|Africa/Nairobi Indian/Mayotte", - "ZA|Africa/Johannesburg", - "ZM|Africa/Maputo Africa/Lusaka", - "ZW|Africa/Maputo Africa/Harare", - ], - }); - - return moment; -}); diff --git a/app/templates/assiduites/pages/bilan_etud.j2 b/app/templates/assiduites/pages/bilan_etud.j2 index bba1dae0e..304adf221 100644 --- a/app/templates/assiduites/pages/bilan_etud.j2 +++ b/app/templates/assiduites/pages/bilan_etud.j2 @@ -101,8 +101,7 @@ return; } - - countAssiduites(date_debut.toIsoUtcString()(), date_fin.toIsoUtcString()()) + countAssiduites(date_debut.toIsoUtcString(), date_fin.toIsoUtcString()) } diff --git a/app/templates/assiduites/pages/calendrier.j2 b/app/templates/assiduites/pages/calendrier.j2 index e4e793d39..f9a949136 100644 --- a/app/templates/assiduites/pages/calendrier.j2 +++ b/app/templates/assiduites/pages/calendrier.j2 @@ -111,10 +111,11 @@ + + + +
+ + +
+ + + \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 7d87ac679..38eadef16 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -1297,6 +1297,16 @@ def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: ) +@bp.route("/test", methods=["GET", "POST"]) +@scodoc +@permission_required(Permission.ScoView) +def test(): + """XXX fonction de test a retirer""" + if request.method == "POST": + print("test date_utils : ", request.form) + return render_template("assiduites/pages/test.j2") + + # --- Fonctions internes --- From d1bc546d7b2a636f0354726782e1e6a8cea1ba8a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 11 Nov 2023 18:13:18 +0100 Subject: [PATCH 11/69] WIP: Affichage de l'emploi du temps du semestre. --- app/api/formsemestres.py | 16 + app/models/formsemestre.py | 16 + app/models/groups.py | 14 +- app/models/moduleimpls.py | 6 + app/models/modules.py | 8 + app/scodoc/sco_edt_cal.py | 318 +++++++++--------- app/scodoc/sco_formsemestre_custommenu.py | 5 - app/scodoc/sco_formsemestre_status.py | 29 +- app/scodoc/sco_groups.py | 6 +- app/scodoc/sco_groups_edit.py | 6 +- app/scodoc/sco_preferences.py | 15 +- app/static/css/edt.css | 16 + .../tui.calendar/toastui-calendar.min.css | 6 + .../tui.calendar/toastui-calendar.min.js | 9 + app/templates/formsemestre/edt.j2 | 96 ++++++ app/views/notes_formsemestre.py | 13 + sco_version.py | 2 +- 17 files changed, 393 insertions(+), 188 deletions(-) create mode 100644 app/static/css/edt.css create mode 100644 app/static/libjs/tui.calendar/toastui-calendar.min.css create mode 100644 app/static/libjs/tui.calendar/toastui-calendar.min.js create mode 100644 app/templates/formsemestre/edt.j2 diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 40fc81bb7..87a640194 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -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,18 @@ 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//edt") +@api_web_bp.route("/formsemestre//edt") +@login_required +@scodoc +@permission_required(Permission.ScoView) +@as_json +def formsemestre_edt(formsemestre_id: int): + """l'emploi du temps du semestre""" + 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) + return sco_edt_cal.formsemestre_edt_dict(formsemestre) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index d020eb92a..705e26248 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -259,6 +259,22 @@ 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_id(self) -> str: + "l'id pour l'emploi du temps: à défaut, le 1er code étape Apogée" + return ( + self.edt_id or "" or (self.etapes[0].etape_apo if len(self.etapes) else "") + ) + def get_infos_dict(self) -> dict: """Un dict avec des informations sur le semestre pour les bulletins et autres templates diff --git a/app/models/groups.py b/app/models/groups.py index 8d445eee4..33eac2604 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -231,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: @@ -243,10 +247,14 @@ class GroupDescr(db.Model): d["partition"] = self.partition.to_dict(with_groups=False) return d + def get_edt_id(self) -> str: + "l'id pour l'emploi du temps: à défaut, le nom scodoc du groupe" + return 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 ré-inscription. + aux groupes pour faciliter une éventuelle ré-inscription. """ from app.models.formsemestre import FormSemestreInscription diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 75672c1b9..741abda1b 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -45,6 +45,12 @@ class ModuleImpl(db.Model): def __repr__(self): return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>" + def get_edt_id(self) -> str: + "l'id pour l'emploi du temps: actuellement celui du module" + return ( + self.module.get_edt_id() + ) # TODO à décliner pour autoriser des codes différents ? + 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) diff --git a/app/models/modules.py b/app/models/modules.py index fe297ebd7..5a96de3a9 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -285,6 +285,14 @@ class Module(db.Model): return {x.strip() for x in self.code_apogee.split(",") if x} return set() + def get_edt_id(self) -> str: + "l'id pour l'emploi du temps: à défaut, le 1er code Apogée" + return ( + self.edt_id + or (self.code_apogee.split(",")[0] if self.code_apogee else "") + or "" + ) + def get_parcours(self) -> list[ApcParcours]: """Les parcours utilisant ce module. Si tous les parcours, liste vide (!). diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index 2174f2580..cd4601408 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -29,175 +29,191 @@ XXX usage uniquement experimental pour tests implémentations -XXX incompatible avec les ics HyperPlanning Paris 13 (était pour GPU). - """ - +import re import icalendar -import urllib - -import app.scodoc.sco_utils as scu +from flask import flash from app import log -from app.scodoc import sco_formsemestre -from app.scodoc import sco_groups -from app.scodoc import sco_preferences +from app.models import FormSemestre, GroupDescr, ModuleImpl, ScoDocSiteConfig +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: - 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}' - """ +def formsemestre_load_calendar( + formsemestre: FormSemestre, +) -> icalendar.cal.Calendar | None: + """Load ics data, return calendar or None if not configured or not available""" + edt_id = formsemestre.get_edt_id() + if not edt_id: + flash( + "accès aux emplois du temps non configuré pour ce semestre (pas d'edt_id)" ) - log(traceback.format_exc()) return None - return ics_url + edt_ics_path = ScoDocSiteConfig.get("edt_ics_path") + if not edt_ics_path.strip(): + return None + ics_filename = edt_ics_path.format(edt_id=edt_id) + try: + with open(ics_filename, "rb") as file: + log(f"Loading edt from {ics_filename}") + calendar = icalendar.Calendar.from_ical(file.read()) + except FileNotFoundError: + flash("erreur chargement du calendrier") + log( + f"formsemestre_load_calendar: ics not found for {formsemestre}\npath='{ics_filename}'" + ) + return None + + return 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 +_COLOR_PALETTE = [ + "#ff6961", + "#ffb480", + "#f8f38d", + "#42d6a4", + "#08cad1", + "#59adf6", + "#9d94ff", + "#c780e8", +] -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) -> list[dict]: + """EDT complet du semestre, comme une liste de dict serialisable en json. + Fonction appellée par l'API /formsemestre//edt + TODO: spécifier intervalle de dates start et end + TODO: cacher ? """ - 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"]) + # 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) - edt_group_name = sco2edt.get(group["group_name"], group["group_name"]) - log("group scodoc=%s : edt=%s" % (group["group_name"], edt_group_name)) - - 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: + # Chargement du calendier ics + calendar = formsemestre_load_calendar(formsemestre) + if not calendar: + return [] + # Génération des événements, avec titre et champs utiles pour l'affichage dans ScoDoc + events = [e for e in calendar.walk() if e.name == "VEVENT"] + events_dict = [] + for event in events: + if "DESCRIPTION" in event: + # --- Group + edt_group = extract_event_group(event) + # si pas de groupe dans l'event, prend toute la promo ("tous") + group: GroupDescr = ( + edt2group.get(edt_group, None) if edt_group else default_group + ) + background_color = ( + group_colors.get(edt_group, "rgb(214, 233, 248)") + if group + else "lightgrey" + ) + group_disp = ( + f"""
{group.get_nom_with_part(default="promo")}
""" + if group + else f"""
{edt_group} + + {scu.EMO_WARNING} non reconnu +
""" + ) + # --- ModuleImpl + edt_module = extract_event_module(event) + modimpl: ModuleImpl = edt2modimpl.get(edt_module, None) + mod_disp = ( + f"""
{modimpl.module.code} +
""" + if modimpl + else f"""
{scu.EMO_WARNING} {edt_module}
""" + ) d = { - "title": e.decoded("DESCRIPTION"), # + '/' + e['X-GROUP-ID'], - "start": e.decoded("dtstart").isoformat(), - "end": e.decoded("dtend").isoformat(), + # Champs utilisés par tui.calendar + "calendarId": "cal1", + "title": extract_event_title(event) + group_disp + mod_disp, + "start": event.decoded("dtstart").isoformat(), + "end": event.decoded("dtend").isoformat(), + "backgroundColor": background_color, + # Infos brutes pour usage API éventuel + "group_id": group.id if group else None, + "group_edt_id": edt_group, + "moduleimpl_id": modimpl.id if modimpl else None, } - J.append(d) + events_dict.append(d) - return scu.sendJSON(J) + return events_dict -# 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' -# ], -# ), -# """ -# """, -# """
-# Emplois du temps du groupe""", -# sco_groups_view.menu_group_choice( -# group_id=group_id, formsemestre_id=formsemestre_id -# ), -# """
loading...
-#
-# """, -# html_sco_header.sco_footer(), -# """ -# """, -# ] -# ) +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.""" + # TODO: fonction ajustée à l'USPN, devra être paramétrable d'une façon ou d'une autre: regexp ? + # 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 = {modimpl.get_edt_id(): modimpl for modimpl in formsemestre.modimpls} + edt2modimpl.pop("", None) + 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: + edt2group.update({g.get_edt_id(): g for g in partition.groups}) + edt2group.pop("", None) + return edt2group diff --git a/app/scodoc/sco_formsemestre_custommenu.py b/app/scodoc/sco_formsemestre_custommenu.py index a98c07ac1..86c5bb391 100644 --- a/app/scodoc/sco_formsemestre_custommenu.py +++ b/app/scodoc/sco_formsemestre_custommenu.py @@ -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(): diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index c716cd098..767f5a2fa 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -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"""
- indiquer les évaluations - indiquer les parcours BUT """ @@ -836,7 +843,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str: 'Tous les étudiants'}
{ - "Gestion de l'assiduité" if not partition_is_empty else "" + "Gestion de l'assiduité" if not partition_is_empty else "" }
""" ) @@ -925,8 +932,8 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str: if formsemestre.can_change_groups(): H.append( f""" (créer)""" ) @@ -937,8 +944,8 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str: H.append( f"""

Ajouter une partition

""" ) @@ -1310,13 +1317,13 @@ def formsemestre_tableau_modules( {mod.code} - {mod.abbrev or mod.titre or ""} {len(mod_inscrits)} { - sco_users.user_info(modimpl["responsable_id"])["prenomnom"] + sco_users.user_info(modimpl["responsable_id"])["prenomnom"] } @@ -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"} - {"lui" if nb_sans_notes == 1 else "leur"} + affecter des notes. """ diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 6e2123757..b0967835d 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -208,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 diff --git a/app/scodoc/sco_groups_edit.py b/app/scodoc/sco_groups_edit.py index 0a9173ec2..792efab2b 100644 --- a/app/scodoc/sco_groups_edit.py +++ b/app/scodoc/sco_groups_edit.py @@ -96,11 +96,13 @@ def group_rename(group_id): "default": group.edt_id or "", "size": 12, "allow_null": True, - "explanation": "optionnel : identifiant du groupe dans le logiciel d'emploi du temps", + "explanation": """optionnel : identifiant du groupe dans le logiciel + d'emploi du temps, pour le cas où les noms de gropupes ne seraient pas + les mêmes dans ScoDoc et dans l'emploi du temps.""", }, ), ), - submitlabel="Renommer", + submitlabel="Enregistrer", cancelbutton="Annuler", ) dest_url = url_for( diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index cab6feaee..557b73b0c 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -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", }, diff --git a/app/static/css/edt.css b/app/static/css/edt.css new file mode 100644 index 000000000..79a3430b0 --- /dev/null +++ b/app/static/css/edt.css @@ -0,0 +1,16 @@ + +.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; +} +.group-name { + color:rgb(25, 113, 25); +} +.group-edt { + color: red; + background-color: yellow; +} diff --git a/app/static/libjs/tui.calendar/toastui-calendar.min.css b/app/static/libjs/tui.calendar/toastui-calendar.min.css new file mode 100644 index 000000000..bd85e12cd --- /dev/null +++ b/app/static/libjs/tui.calendar/toastui-calendar.min.css @@ -0,0 +1,6 @@ +/*! + * TOAST UI Calendar 2nd Edition + * @version 2.1.3 | Tue Aug 16 2022 + * @author NHN Cloud FE Development Lab + * @license MIT + */.toastui-calendar-holiday{color:red;font-size:15px}.toastui-calendar-layout{box-sizing:border-box;position:relative;white-space:nowrap}.toastui-calendar-layout *{box-sizing:border-box}.toastui-calendar-layout.toastui-calendar-dragging--move-event *{cursor:move}.toastui-calendar-layout.toastui-calendar-dragging--resize-horizontal-event *{cursor:col-resize}.toastui-calendar-layout.toastui-calendar-dragging--resize-vertical-event *{cursor:row-resize}.toastui-calendar-layout .toastui-calendar-panel-resizer{user-select:none}.toastui-calendar-layout .toastui-calendar-panel-resizer:hover{border-color:#999}.toastui-calendar-layout .toastui-calendar-panel-resizer-guide{position:absolute}.toastui-calendar-icon,.toastui-calendar-layout.toastui-calendar-horizontal .toastui-calendar-panel,.toastui-calendar-layout.toastui-calendar-horizontal .toastui-calendar-panel-resizer{display:inline-block;vertical-align:middle}.toastui-calendar-icon{height:14px;width:14px}.toastui-calendar-icon.toastui-calendar-ic-title{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAO5JREFUKBVjYCATMOLSFxkZqfHnz5+1QHktNDVbV69e7cOCJgjmQjXtB3IksMh7g8SY0CXQNTEyMlYD1fBCabhyFI3omkCq/v//PwnotC8gGq4LyIBrxKYJpBBoU15oaCgPiEbWCPYjUEIFGBBY/QS0qRWooRVIg/UBDXgMYoBtBHJSgWxsAQFWjET8BBqQBuLDNM4Can6GpAAb8ydQMADo3x0gSbDGlStX3gVqdMSjGUUTSCNKAggPD1cDOmU/EEuBJKEAQxNIHEUjSABNM1ZNIHXMIAIZXL169a2+vv5moK18QKeXAv20B1meYjYAr7xrljpOzc0AAAAASUVORK5CYII=) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-location{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAR1JREFUKBWdUTtPg1AUBiT8CydHJtv/0MTJRWAgcTK1bq0/pO3k4E4IYLo2Me46tS4wumjSpV07kAb6HXLPzaGPRZLL+c73uE/D+OdnHuaCIOhVVTUEf620pWVZ0yRJ3qW3FfR9f1zX9UgaGJumOUnT9Fn3DDzPuwPOuD9TvSzL3kizhOFJ4LnjOJc0wM0FP2Asgx0mEehHUfRHgzDzqF3GOogzbJg8V6XHFqYv4Cvqy7J8DcOwWYmw8Hwy1kHMRjcaKuEGgV82caWbZay3indagJyxcKLOlKeRdJA627YfUVaN0v6tlKbZVjCO4zW2cw91px3AxJEmOONCNoTzPP9xXZfOd6u0Bzz60RGOgmQuiuIb4S3gB0IvaoJW2QMDs1bBoH1CAQAAAABJRU5ErkJggg==) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-date{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAGpJREFUKBVjYKAGCA0N/Q/C6GZhE2cEKQoLC9v6//9/L3QN2PiMjIzbVq1a5c0EkiRWE7JasEZsJhMSI1sjC7LJq1evBvsZWQyZjRxwZNs4hDSiBA6y55EDBRsb7EdQasAmiUNsKw5x4oQBkUAeDPJ53KsAAAAASUVORK5CYII=) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-state{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAIxJREFUKBVjYCATMKLrCw8P9/z3798soLgMVO4JExNT2sqVK7cjq2VC5oDYME2MjIyNIAwUkoGKoShlQeFBOGCbVq1a1QDihoaG1gMpmO0gITAAOzUsLGzr////vWCC+GigK7YBDfUGO5VYTSADYWox/IjPNmS5UY3IoYHGBgcOKG7QxPFxt+KTJCgHAGcZJbGLRuJ2AAAAAElFTkSuQmCC) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-private{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAKpJREFUKBVjYCATMKLri46Olvn9+3fX////HUByjIyMB1hZWcuWLl36BFktikaQpl+/fl0EKhBCVgRkv2NjY9NH1syErABkE1TTdqBCWRAG8reDxKBycOUoGmHOA2pIA5kOwiA2SDVMDqYTRSNQUBIkgewkJDZYDqYR7sewsLCtQFO9YBLYaGBAbVu1apU3SA5uIyFNIMXIauAaQRKkgBGlERTURATOVpgaABRQQOK46wEAAAAAAElFTkSuQmCC) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-public{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAKdJREFUKBVjYCATMKLri46Olvn9+3fX////HUByjIyMB1hZWcuWLl36BFktikaQpl+/fl0EKhBCVgRkv2NjY9NH1syErABkE1TTdqBCWRAG8reDxKBycOUoGmHOA2pIA5kOwiA2SDVMDq4TmREaGvofhJHFcLHhfgwLC9sKNNULl0KQODCgtq1atcobxIY7lZAmkGJkNXCNIAlSwIjSCApqIgJnK0wNALoOPwSpOcq0AAAAAElFTkSuQmCC) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-close{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAJRJREFUKBXFkNERhCAMREUbuEKohzq0Eq2DDq6Da4B60KezDORkxj+ZwchmX0IYhtdWCGFl9y5g82NtzDnPdzAaudo76ZBS+nrvPxiInMkJcs5tMcZFfqcfxdqIRiELof+BiIJPg+mExmpmvKRn3zKj7OrG9Y79szPL14A1xEP0Hgy4gBZS5R7czHj3ehSgOzkVeyfuGrBw/WLm0hsAAAAASUVORK5CYII=) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-user-b{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAAJpJREFUKBVjYKA1YES3wMTExBMoNgsqnnbmzJntyGqYkDlQ9qz////LgDCQD9MIV4ZNA1wSGwObhjRGRsYnIAzUkIZNE0licE+bm5tr/fnzJx1osjPQBFmoKY+BftnLwsIy8+TJk9dAYmANxsbGoUD2YiBmBwliAT+BYrFnz55dDfNDO1AAl2KQfpAcSA0DTIMyiEMAEKMG0wgAolIjcM7Tjm8AAAAASUVORK5CYII=) no-repeat;top:-4px}.toastui-calendar-icon.toastui-calendar-ic-edit{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAAMdJREFUKBVjYCARMOJTb2xsLMfIyBjLysq64Pjx409BapnwaQDKzf7//3/L79+/D1tbW0uB1LJg02BmZqYIVPgdKBf/79+//UC2xs+fP8OB/H4MG0CK//79ewCkEGQYExOTI5DawMnJuQTER/EDTDFQXA4kCQQ3QBpOnz79AsJF8gMWxTA1KDTYBhyKMUwH6WSysrKSB7kZyIY5AySOVTFIggno+5VAmijFYA1AwhzEgAKcJsMUwIMVGKPH2NnZ7ZFDBKYImQYAuO5YIMgk39gAAAAASUVORK5CYII=) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-delete{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAAFhJREFUKBVjYCARMKKrNzEx2fr//38vkDgjI+O2M2fOeCOrAWtAVoQsicyGaWZCFsTHBtr6H588Tjm4H4yNjfGacPbsWbBaop0Es3JYaQBFDMxjWOitMDEA3EEZfFEISwUAAAAASUVORK5CYII=) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-arrow-solid-top{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAIFJREFUKBVjYBj+gBmXF2NiYiTV1dV5rl279gWbGiZsgg0NDSw/f/5cCZRbBWJjU4PVRjExsR6g4nAgln/z5g3v1atXd6JrxtAYHh4e+v///z4khZa6urrXgJqvIYkxMCJzgJo0//37dwooxoMsDmR/YWJiMlu5cuV1NPFRLrYQAADMVCaUtbG7XwAAAABJRU5ErkJggg==) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-milestone{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAAGFJREFUKBVjYCARMILU/3dw+I+hj5FxG+P+/d7o4rg1IKtE0syELI6T/f+/F0yOOA0w1UCa9hpYkGxjYDxwABwIILH/jo5bGWBuZ2TcClOHogEmCKKxBSlInPZ+ANlCEgAA37EU4AHbBQUAAAAASUVORK5CYII=) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-arrow-left{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAHCAYAAAAvZezQAAAAAXNSR0IArs4c6QAAAFZJREFUCB1jZICCyspK83///hUxgvhVVVV6f//+3c3ExJTMVFNTowqU2cHMzJzf3t6+hen379/zgIp2t7W1rQCpZmJlZU0C0q5AbREgAQwzwAIgGZgtADMCJqH/QyvhAAAAAElFTkSuQmCC) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-arrow-right{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAHCAYAAAAvZezQAAAAAXNSR0IArs4c6QAAAFxJREFUCB1jKC8vX1lZWWnOAAVMQLD4379/m6qqqvRAYowgAsiJAAr2sbCw2IMFQIIVFRUL////r8SCpMKVlZXVnhFooA9Q+VxmZmbXtra2S0xATizQYD8QB6QaAJMLJ9BqE9yTAAAAAElFTkSuQmCC) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-handle-y{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAICAMAAADp7a43AAAACVBMVEX///////////+OSuX+AAAAA3RSTlMrQJG5H4EIAAAAEUlEQVR4AWNgYmRiZABB/CwAAtgAIUTUNkMAAAAASUVORK5CYII=) 50% no-repeat}.toastui-calendar-icon.toastui-calendar-ic-checkbox-normal{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAADpJREFUKBVjPHfu3O5///65MJAAmJiY9jCcOXPmP6kApIeJBItQlI5qRAkOVM5o4KCGBwqPkcxEvhsAbzRE+Jhb9IwAAAAASUVORK5CYII=) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-checkbox-checked{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAMBJREFUKBWVkjEOwjAMRe2WgZW7IIHEDdhghhuwcQ42rlJugAQS54Cxa5cq1QM5TUpByZfS2j9+dlJVt/tX5ZxbS4ZU9VLkQvSHKTIGRaVJYFmKrBbTCJxE2UgCdDzMZDkHrOV6b95V0US6UmgKodujEZbJg0B0ZgEModO5lrY1TMQf1TpyJGBEjD+E2NPN7ukIUDiF/BfEXgRiGEw8NgkffYGYwCi808fpn/6OvfUfsDr/Vc1IfRf8sKnFVqeiVQfDu0tf/nWH9gAAAABJRU5ErkJggg==) no-repeat}.toastui-calendar-icon.toastui-calendar-ic-dropdown-arrow{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAHlJREFUKBVjYBgFOEOAEVkmPDxc89+/f6eAYjzI4kD2FyYmJrOVK1deh4kzwRggGiQBVJCELAZig8SQNYHEmEEEMrh69eo1HR0dfqCYJUickZGxf9WqVf3IakBsFBthklpaWmVA9mEQhrJhUoTp0NBQCRAmrHL4qgAAuu4cWZOZIGsAAAAASUVORK5CYII=) no-repeat}.toastui-calendar-icon.toastui-calendar-open.toastui-calendar-ic-dropdown-arrow{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAIFJREFUKBVjYBj+gBmXF2NiYiTV1dV5rl279gWbGiZsgg0NDSw/f/5cCZRbBWJjU4PVRjExsR6g4nAgln/z5g3v1atXd6JrxtAYHh4e+v///z4khZa6urrXgJqvIYkxMCJzgJo0//37dwooxoMsDmR/YWJiMlu5cuV1NPFRLrYQAADMVCaUtbG7XwAAAABJRU5ErkJggg==) no-repeat}.toastui-calendar-ic-location-b{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAAOZJREFUKBWVUT0KwjAUbkzGTuJWPYNDpV0cXD2ETuIRBK+gs4ubp3BwcXBp6eINBKWDgydoid9X8oKCggYeee/7S9IGwZ9LverTNO3Wdb2y1o6IK6WOWutFlmU30XmDE58hbgvpTA+Y+mJqCemS20jdG2N6LPYMICc6b5BrIG3ONBZ7CoVj7w0cfllGRDj+gKQpjt/iPU0ye/LkROcNANaoCUzjqqquIsBuHddAWoiyLO9RFHUwJ4JxR/qmKIqdYG9vCMNwCeIiJHuHecj/B0GSJBng7ifO+ErDPM8L4b7ucRzPWJ8ET1E7YC7tmi9qAAAAAElFTkSuQmCC) no-repeat;top:-4px}.toastui-calendar-ic-state-b{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAAHlJREFUKBVjYCARMCKrNzEx8QTyZ/3//18GJM7IyPgESKWdOXNmO4gPAkwQCk6CFQMVNoIwVOMsuCw6w9jY+D8Iw8TR+SBxsJOATtkKNM0LphAbDbRxG9Bp3mAnEVIMMgCmBt0P2AxHERusGkAhgOJQ7Jyt2IUJiAIAwwIn24FgmhkAAAAASUVORK5CYII=) no-repeat;top:-4px}.toastui-calendar-ic-repeat-b{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAAQpJREFUKBVjYCARMCKrNzU1tf/3718lUMwQiP8yMjKeYWJi6j516tRhExOThjNnzjTANQAFqv///98CVHQPSO8A0ixATa5AtjyQPgDETmfPngULMpiZmbn//fu3BSg4B6ggCyjxG8hm8PT0ZH/9+vUJoJgTiA8CTCACqDgXaOJ9Xl7eTJhikDhQcSVQsQGITT8A9rSxsfF/mJVApzWCQgPGd3BwYPny5cstoNOuAZ3rAwoJOAAqviAqKtoOEwAaxPr58+dpQL4iEGeAxJFt2AfkOwA1PQTSu4Em/gGyPYC0EpCuAdraCtIADiWgQCPQOmdmZmYHoNgVoCJfIB0CpG8DI84BphgoRjoAAAzgdELI91E5AAAAAElFTkSuQmCC) no-repeat;top:-4px}.toastui-calendar-timegrid-time-column{font-size:11px;height:100%}.toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-hour-rows{display:inline-block;height:100%;position:relative}.toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-time{color:#333;position:absolute;right:5px;text-align:right}.toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-time.toastui-calendar-timegrid-time-past{font-weight:400}.toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-time.toastui-calendar-timegrid-time-first{line-height:normal;visibility:hidden}.toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-time.toastui-calendar-timegrid-time-last{height:0;visibility:hidden}.toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-time .toastui-calendar-timegrid-time-label,.toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-time span{position:absolute;right:0;transform:translateY(-50%)}.toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-current-time .toastui-calendar-timegrid-day-difference{bottom:100%;position:absolute;right:0}.toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-time-hidden{visibility:hidden}.toastui-calendar-timegrid-time-column .toastui-calendar-timegrid-current-time{font-size:11px;font-weight:400;position:absolute;right:5px;text-align:right;transform:translateY(-50%)}.toastui-calendar-timezone-labels-slot{background-color:#fff;border-bottom:1px solid #e9e9e9;display:table;height:40px;position:absolute;table-layout:fixed}.toastui-calendar-timezone-labels-slot .toastui-calendar-timegrid-timezone-label{background-color:#fff;border-right:1px solid #e5e5e5;display:table-cell;font-size:11px;padding-right:5px;text-align:right;vertical-align:middle}.toastui-calendar-timezone-labels-slot .toastui-calendar-timegrid-timezone-collapse-button{background:transparent;border:1px solid #ddd;border-left:none;bottom:2px;cursor:pointer;position:absolute;top:2px;width:10px}.toastui-calendar-timezone-labels-slot .toastui-calendar-timegrid-timezone-collapse-button .toastui-calendar-icon{height:7px;transform:translateX(-50%);width:4px}.toastui-calendar-column{position:relative}.toastui-calendar-column .toastui-calendar-gridline-half{position:absolute;width:100%}.toastui-calendar-column .toastui-calendar-grid-selection{left:1px;padding:3px;position:absolute;right:10px}.toastui-calendar-column .toastui-calendar-grid-selection .toastui-calendar-grid-selection-label{font-size:11px;font-weight:700}.toastui-calendar-column .toastui-calendar-events{bottom:0;left:0;position:absolute;right:0;top:0}.toastui-calendar-panel.toastui-calendar-time{overflow-y:auto}.toastui-calendar-timegrid{height:200%;min-height:900px;position:relative;user-select:none}.toastui-calendar-timegrid .toastui-calendar-timegrid-scroll-area{height:100%;position:relative}.toastui-calendar-timegrid .toastui-calendar-columns{bottom:0;overflow:hidden;position:absolute;right:0;top:0}.toastui-calendar-timegrid .toastui-calendar-columns .toastui-calendar-gridline-half{position:absolute;width:100%}.toastui-calendar-timegrid .toastui-calendar-columns .toastui-calendar-column{display:inline-block;height:100%}.toastui-calendar-timegrid .toastui-calendar-timegrid-now-indicator{left:0;position:absolute;right:0}.toastui-calendar-timegrid .toastui-calendar-timegrid-now-indicator .toastui-calendar-timegrid-now-indicator-left{position:absolute}.toastui-calendar-timegrid .toastui-calendar-timegrid-now-indicator .toastui-calendar-timegrid-now-indicator-marker{border-radius:50%;height:9px;margin:-4px 0 0 -5px;position:absolute;width:9px}.toastui-calendar-timegrid .toastui-calendar-timegrid-now-indicator .toastui-calendar-timegrid-now-indicator-today{position:absolute}.toastui-calendar-timegrid .toastui-calendar-timegrid-now-indicator .toastui-calendar-timegrid-now-indicator-right{position:absolute;right:0}.toastui-calendar-event-background{position:absolute}.toastui-calendar-event-time{cursor:pointer;overflow:hidden;position:absolute}.toastui-calendar-event-time .toastui-calendar-event-time-content,.toastui-calendar-event-time .toastui-calendar-travel-time{font-size:12px;overflow:hidden;padding:1px 0 0 3px}.toastui-calendar-resize-handler-x{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAECAMAAACEE47CAAAACVBMVEX///////////+OSuX+AAAAA3RSTlMrQJG5H4EIAAAAEUlEQVR4AWNgYoRABhjEFAEAArQAIcHQcPsAAAAASUVORK5CYII=) no-repeat bottom;bottom:1px;color:#fff;cursor:row-resize;height:8px;left:0;position:absolute;right:0;text-align:center}.toastui-calendar-weekday-event-title{display:block;font-size:12px;font-weight:700;overflow:hidden;padding-left:3px;text-overflow:ellipsis;white-space:nowrap}.toastui-calendar-weekday-event-dot{border-radius:50%;display:inline-block;float:left;height:8px;position:relative;top:8px;width:8px}.toastui-calendar-weekday-event-dot+.toastui-calendar-weekday-event-title{color:#333}.toastui-calendar-weekday-resize-handle{position:absolute;right:5px;top:0}.toastui-calendar-weekday-resize-handle.toastui-calendar-handle-y{cursor:col-resize}.toastui-calendar-grid-cell-date .toastui-calendar-weekday-grid-date.toastui-calendar-weekday-grid-date-decorator{background-color:#135de6;border-radius:50%;display:inline-block;font-weight:700;height:26px;line-height:26px;margin-left:2px;text-align:center;width:26px}.toastui-calendar-panel-title{display:table;float:left;height:100%;padding-right:5px}.toastui-calendar-panel-title .toastui-calendar-left-content{display:table-cell;font-size:11px;text-align:right;vertical-align:middle}.toastui-calendar-panel-grid-wrapper{overflow-y:hidden;position:relative}.toastui-calendar-panel .toastui-calendar-panel-grid-wrapper,.toastui-calendar-panel .toastui-calendar-panel-title{height:100%}.toastui-calendar-allday-panel{height:100%;overflow-y:hidden;position:relative}.toastui-calendar-allday-panel .toastui-calendar-grid-selection{position:absolute;right:10px;top:0;z-index:1}.toastui-calendar-panel-grid{height:100%;position:absolute}.toastui-calendar-panel-event-wrapper{height:100%;left:0;overflow-y:scroll;position:absolute;top:0;width:100%}.toastui-calendar-panel-event-wrapper .toastui-calendar-weekday-event-block{position:absolute}.toastui-calendar-panel-event-wrapper .toastui-calendar-weekday-event{background-color:rgba(218,27,27,.2);border-left:3px solid;border-color:#da1b1b;border-radius:0;color:#9a1313;cursor:pointer;height:18px;margin:0 10px 0 1px;position:relative}.toastui-calendar-panel-event-wrapper .toastui-calendar-weekday-exceed-right .toastui-calendar-weekday-event{margin-right:0}.toastui-calendar-panel-event{border:1px solid #333;position:absolute}.toastui-calendar-weekday-exceed-in-week{background-color:#fff;border:1px solid #ddd;bottom:5px;color:#000;cursor:pointer;font-size:12px;line-height:14px;margin-right:5px;padding:1px 5px;position:absolute;right:5px;z-index:1}.toastui-calendar-collapse-btn-icon{border-bottom:5px solid #4f5959;border-left:4px solid transparent;border-right:4px solid transparent;display:inline-block;height:0;margin:-1px -14px 0 -4px;vertical-align:middle;width:0}.toastui-calendar-day-view .toastui-calendar-panel:not(.toastui-calendar-time),.toastui-calendar-week-view .toastui-calendar-panel:not(.toastui-calendar-time){overflow-y:scroll}.toastui-calendar-floating-layer{z-index:1}.toastui-calendar-floating-layer *{box-sizing:border-box}.toastui-calendar-popup-overlay{height:100%;left:0;position:absolute;top:0;width:100%}.toastui-calendar-popup-container{box-shadow:0 2px 6px 0 rgba(0,0,0,.1);clear:both;font-weight:2.5;position:absolute;z-index:2}.toastui-calendar-popup-section{font-size:0;min-height:40px}.toastui-calendar-popup-button.toastui-calendar-popup-close{background-color:#fff;border:none;padding:0;position:absolute;right:10px;top:10px}.toastui-calendar-popup-button.toastui-calendar-popup-confirm{background-color:#ff6618;border:none;border-radius:40px;color:#fff;float:right;font-size:12px;font-weight:700;height:36px;width:96px}.toastui-calendar-dropdown-menu{background-color:#fff;border:1px solid #d5d5d5;border-radius:0 0 2px 2px;border-top:none;padding:4px 0;position:absolute;top:31px;width:100%;z-index:1}.toastui-calendar-dropdown-menu.toastui-calendar-open{display:block}.toastui-calendar-dropdown-menu-item{border:none;border-radius:2px;cursor:pointer;font-size:0;height:30px;padding:0 9px 0 12px;width:100%}.toastui-calendar-popup-arrow-border,.toastui-calendar-popup-arrow-fill{position:absolute}.toastui-calendar-see-more-container{display:block;position:absolute;z-index:1}.toastui-calendar-see-more{height:inherit;padding:5px}.toastui-calendar-more-title-date{color:#333;font-size:23px}.toastui-calendar-more-title-day{color:#333;font-size:12px}.toastui-calendar-month-more-list{overflow:auto;padding:0 17px}.toastui-calendar-see-more-header{border-bottom:none;position:relative}.toastui-calendar-form-container{background-color:#fff;border:1px solid #d5d5d5;border-radius:2px;box-shadow:0 2px 6px 0 rgba(0,0,0,.1);min-width:474px;padding:17px}.toastui-calendar-form-container .toastui-calendar-hidden-input{display:none}.toastui-calendar-form-container .toastui-calendar-grid-selection{font-size:11px;font-weight:700}.toastui-calendar-popup-section-item{border:1px solid #d5d5d5;border-radius:2px;display:inline-block;font-size:0;height:32px;padding:0 9px 0 12px}.toastui-calendar-popup-section-item input{border:none;display:inline-block;height:30px;outline:none}.toastui-calendar-popup-section-item .toastui-calendar-content{display:inline-block;font-size:12px;padding-left:8px;position:relative;text-align:left;vertical-align:middle}.toastui-calendar-popup-date-picker .toastui-calendar-content{max-width:125px}.toastui-calendar-dropdown-section{position:relative}.toastui-calendar-dropdown-section.toastui-calendar-calendar-section{width:176px}.toastui-calendar-dropdown-section .toastui-calendar-content{line-height:30px}.toastui-calendar-popup-section-title input{width:365px}.toastui-calendar-dot{border-radius:8px;height:12px;margin:1px;width:12px}.toastui-calendar-content.toastui-calendar-event-calendar{overflow:hidden;text-overflow:ellipsis;top:-1px;white-space:nowrap;width:125px}.toastui-calendar-popup-section-location .toastui-calendar-content{width:400px}.toastui-calendar-popup-section-allday{border:none;cursor:pointer;padding:0 0 0 8px}.toastui-calendar-popup-section-allday .toastui-calendar-ic-checkbox-normal{cursor:pointer;display:inline-block;height:14px;line-height:14px;margin:0;vertical-align:middle;width:14px}.toastui-calendar-popup-section-allday .toastui-calendar-content{padding-left:4px}.toastui-calendar-popup-date-picker{width:176px}.toastui-calendar-datepicker-container>div{z-index:1}.toastui-calendar-popup-date-dash{color:#d5d5d5;font-size:12px;height:32px;padding:0 4px;vertical-align:middle}.toastui-calendar-popup-button{background:#fff;border:1px solid #d5d5d5;border-radius:2px;color:#333;cursor:pointer;font-size:12px;outline:none;text-align:center}.toastui-calendar-popup-button.toastui-calendar-popup-section-private{font-size:0;height:32px;margin-left:4px;padding:8px}.toastui-calendar-popup-button .toastui-calendar-event-state{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:58px}.toastui-calendar-dropdown-section.toastui-calendar-state-section{width:109px}.toastui-calendar-dropdown-section.toastui-calendar-state-section .toastui-calendar-popup-button{width:100%}.toastui-calendar-state-section .toastui-calendar-content{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:58px}.toastui-calendar-popup-section-item.toastui-calendar-dropdown-menu-item{border:none;cursor:pointer;display:block;height:30px}.toastui-calendar-dropdown-menu-item .toastui-calendar-content{display:inline-block;font-size:12px;padding-left:8px;position:relative;text-align:left;vertical-align:middle}.toastui-calendar-popup-section-item.toastui-calendar-popup-button{font-size:0;height:32px;top:-1px}.toastui-calendar-popup-arrow.toastui-calendar-top .toastui-calendar-popup-arrow-border{border:8px solid transparent;border-bottom:8px solid #d5d5d5;border-top:none;left:calc(50% - 8px);top:-7px}.toastui-calendar-popup-arrow.toastui-calendar-top .toastui-calendar-popup-arrow-fill{border:7px solid transparent;border-bottom:7px solid #fff;border-top:none;left:-7px;top:1px}.toastui-calendar-popup-arrow.toastui-calendar-bottom .toastui-calendar-popup-arrow-border{border:8px solid transparent;border-bottom:none;border-top-color:#d5d5d5;bottom:-7px}.toastui-calendar-popup-arrow.toastui-calendar-bottom .toastui-calendar-popup-arrow-fill{border:7px solid transparent;border-bottom:none;border-top-color:#fff;bottom:1px;left:-7px}.toastui-calendar-detail-container{background-color:#fff;border:1px solid #d5d5d5;border-radius:2px;box-shadow:0 2px 6px 0 rgba(0,0,0,.1);min-width:301px;padding:17px 17px 0;width:301px}.toastui-calendar-detail-container .toastui-calendar-section-header{margin-bottom:6px}.toastui-calendar-detail-container .toastui-calendar-section-detail{margin-bottom:16px}.toastui-calendar-detail-container .toastui-calendar-section-button{border-top:1px solid #e5e5e5;font-size:0}.toastui-calendar-detail-container .toastui-calendar-content{font-size:12px;height:24px;line-height:2}.toastui-calendar-detail-container .toastui-calendar-icon{background-size:12px;height:12px;margin-right:8px;position:relative;width:12px}.toastui-calendar-detail-container .toastui-calendar-calendar-dot{border-radius:50%;height:10px;margin-right:10px;top:-4px;width:10px}.toastui-calendar-event-title{font-size:15px;font-weight:700;line-height:1.6;word-break:break-all}.toastui-calendar-detail-item-indent{padding-left:20px;text-indent:-20px}.toastui-calendar-delete-button,.toastui-calendar-edit-button{background:none;border:none;cursor:pointer;display:inline-block;outline:none;padding:7px 9px 11px;width:calc(50% - 1px)}.toastui-calendar-vertical-line{background:#e5e5e5;display:inline-block;height:14px;margin-top:-7px;vertical-align:middle;width:1px}.toastui-calendar-section-button .toastui-calendar-icon{margin-right:4px;top:-3px}.toastui-calendar-section-button .toastui-calendar-content{position:relative;top:2px}.toastui-calendar-popup-top-line{border:none;border-radius:2px 2px 0 0;height:4px;position:absolute;top:0;width:100%}.toastui-calendar-popup-arrow.toastui-calendar-left .toastui-calendar-popup-arrow-border{border:8px solid transparent;border-left:none;border-right-color:#d5d5d5;left:-7px}.toastui-calendar-popup-arrow.toastui-calendar-left .toastui-calendar-popup-arrow-fill{border:7px solid transparent;border-left:none;border-right-color:#fff;left:1px;top:-7px}.toastui-calendar-popup-arrow.toastui-calendar-right .toastui-calendar-popup-arrow-border{border:8px solid transparent;border-left:8px solid #d5d5d5;border-right:none;right:-7px}.toastui-calendar-popup-arrow.toastui-calendar-right .toastui-calendar-popup-arrow-fill{border:7px solid transparent;border-left:7px solid #fff;border-right:none;right:1px;top:-7px}.toastui-calendar-day-name-container,.toastui-calendar-day-names{position:relative}.toastui-calendar-day-name-item{font-size:12px;font-weight:400;padding:0 10px;position:absolute;text-align:left}.toastui-calendar-day-name-item.toastui-calendar-week{height:42px;line-height:38px}.toastui-calendar-day-name-item.toastui-calendar-month{height:31px;line-height:31px}.toastui-calendar-day-view-day-names,.toastui-calendar-week-view-day-names{border-bottom:1px solid #e5e5e5}.toastui-calendar-day-names.toastui-calendar-week{height:42px;padding-left:0;text-align:left}.toastui-calendar-day-names.toastui-calendar-month{font-size:12px;font-weight:400;height:31px;padding:0 10px;text-align:left}.toastui-calendar-day-name__date{font-size:26px}.toastui-calendar-day-name__name{font-size:12px}.toastui-calendar-layout.toastui-calendar-month{height:100%}.toastui-calendar-month .toastui-calendar-day-names{height:31px}.toastui-calendar-month .toastui-calendar-month-daygrid{height:calc(100% - 31px);position:relative}.toastui-calendar-month-week-item{position:relative}.toastui-calendar-weekday-grid{height:100%;min-height:inherit;position:absolute;width:100%}.toastui-calendar-daygrid-cell{height:100%;min-height:inherit;padding:3px 0;position:absolute}.toastui-calendar-daygrid-cell+.toastui-calendar-daygrid-cell{border-left:1px solid #e5e5e5}.toastui-calendar-grid-cell-date{display:inline-block;height:27px;line-height:1.7;text-align:center;width:27px}.toastui-calendar-grid-cell-footer{bottom:0;position:absolute;width:100%}.toastui-calendar-grid-cell-more-events{background-color:transparent;border:none;color:#aaa;cursor:pointer;float:right;font-size:11px;font-weight:700;height:27px;line-height:27px;padding:0 5px;text-align:center}.toastui-calendar-weekday-events{font-size:12px;left:0;position:absolute;top:0;width:100%}.toastui-calendar-weekday-event{cursor:pointer}.toastui-calendar-weekday{height:100%}.toastui-calendar-weekday .toastui-calendar-grid-selection{position:absolute} \ No newline at end of file diff --git a/app/static/libjs/tui.calendar/toastui-calendar.min.js b/app/static/libjs/tui.calendar/toastui-calendar.min.js new file mode 100644 index 000000000..d3554d540 --- /dev/null +++ b/app/static/libjs/tui.calendar/toastui-calendar.min.js @@ -0,0 +1,9 @@ +/*! + * TOAST UI Calendar 2nd Edition + * @version 2.1.3 | Tue Aug 16 2022 + * @author NHN Cloud FE Development Lab + * @license MIT + */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("tui-date-picker")):"function"==typeof define&&define.amd?define(["tui-date-picker"],t):"object"==typeof exports?exports.tui=t(require("tui-date-picker")):(e.tui=e.tui||{},e.tui.Calendar=t(e.tui.DatePicker))}(this,(function(e){return function(){var t={7111:function(e,t,n){var r=n(6733),o=n(9821),i=TypeError;e.exports=function(e){if(r(e))return e;throw i(o(e)+" is not a function")}},8505:function(e,t,n){var r=n(6733),o=String,i=TypeError;e.exports=function(e){if("object"==typeof e||r(e))return e;throw i("Can't set "+o(e)+" as a prototype")}},9736:function(e,t,n){var r=n(95),o=n(2391),i=n(1787).f,a=r("unscopables"),l=Array.prototype;null==l[a]&&i(l,a,{configurable:!0,value:o(null)}),e.exports=function(e){l[a][e]=!0}},6637:function(e,t,n){"use strict";var r=n(966).charAt;e.exports=function(e,t,n){return t+(n?r(e,t).length:1)}},1176:function(e,t,n){var r=n(5052),o=String,i=TypeError;e.exports=function(e){if(r(e))return e;throw i(o(e)+" is not an object")}},9540:function(e,t,n){var r=n(905),o=n(3231),i=n(9646),a=function(e){return function(t,n,a){var l,c=r(t),s=i(c),u=o(a,s);if(e&&n!=n){for(;s>u;)if((l=c[u++])!=l)return!0}else for(;s>u;u++)if((e||u in c)&&c[u]===n)return e||u||0;return!e&&-1}};e.exports={includes:a(!0),indexOf:a(!1)}},7079:function(e,t,n){var r=n(5968),o=r({}.toString),i=r("".slice);e.exports=function(e){return i(o(e),8,-1)}},1589:function(e,t,n){var r=n(1601),o=n(6733),i=n(7079),a=n(95)("toStringTag"),l=Object,c="Arguments"==i(function(){return arguments}());e.exports=r?i:function(e){var t,n,r;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=l(e),a))?n:c?i(t):"Object"==(r=i(t))&&o(t.callee)?"Arguments":r}},1590:function(e,t,n){var r=n(5968),o=Error,i=r("".replace),a=String(o("zxcasd").stack),l=/\n\s*at [^:]*:[^\n]*/,c=l.test(a);e.exports=function(e,t){if(c&&"string"==typeof e&&!o.prepareStackTrace)for(;t--;)e=i(e,l,"");return e}},7081:function(e,t,n){var r=n(8270),o=n(4826),i=n(7933),a=n(1787);e.exports=function(e,t,n){for(var l=o(t),c=a.f,s=i.f,u=0;u0&&r[0]<4?1:+(r[0]+r[1])),!o&&a&&(!(r=a.match(/Edge\/(\d+)/))||r[1]>=74)&&(r=a.match(/Chrome\/(\d+)/))&&(o=+r[1]),e.exports=o},3837:function(e){e.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},373:function(e,t,n){var r=n(4229),o=n(5358);e.exports=!r((function(){var e=Error("a");return!("stack"in e)||(Object.defineProperty(e,"stack",o(1,7)),7!==e.stack)}))},3103:function(e,t,n){var r=n(9859),o=n(7933).f,i=n(5762),a=n(4768),l=n(8400),c=n(7081),s=n(6541);e.exports=function(e,t){var n,u,d,f,p,h=e.target,m=e.global,g=e.stat;if(n=m?r:g?r[h]||l(h,{}):(r[h]||{}).prototype)for(u in t){if(f=t[u],d=e.dontCallGetSet?(p=o(n,u))&&p.value:n[u],!s(m?u:h+(g?".":"#")+u,e.forced)&&void 0!==d){if(typeof f==typeof d)continue;c(f,d)}(e.sham||d&&d.sham)&&i(f,"sham",!0),a(n,u,f,e)}}},4229:function(e){e.exports=function(e){try{return!!e()}catch(e){return!0}}},4954:function(e,t,n){"use strict";n(7950);var r=n(5968),o=n(4768),i=n(3466),a=n(4229),l=n(95),c=n(5762),s=l("species"),u=RegExp.prototype;e.exports=function(e,t,n,d){var f=l(e),p=!a((function(){var t={};return t[f]=function(){return 7},7!=""[e](t)})),h=p&&!a((function(){var t=!1,n=/a/;return"split"===e&&((n={}).constructor={},n.constructor[s]=function(){return n},n.flags="",n[f]=/./[f]),n.exec=function(){return t=!0,null},n[f](""),!t}));if(!p||!h||n){var m=r(/./[f]),g=t(f,""[e],(function(e,t,n,o,a){var l=r(e),c=t.exec;return c===i||c===u.exec?p&&!a?{done:!0,value:m(t,n,o)}:{done:!0,value:l(n,t,o)}:{done:!1}}));o(String.prototype,e,g[0]),o(u,f,g[1])}d&&c(u[f],"sham",!0)}},3171:function(e,t,n){var r=n(7188),o=Function.prototype,i=o.apply,a=o.call;e.exports="object"==typeof Reflect&&Reflect.apply||(r?a.bind(i):function(){return a.apply(i,arguments)})},7188:function(e,t,n){var r=n(4229);e.exports=!r((function(){var e=function(){}.bind();return"function"!=typeof e||e.hasOwnProperty("prototype")}))},266:function(e,t,n){var r=n(7188),o=Function.prototype.call;e.exports=r?o.bind(o):function(){return o.apply(o,arguments)}},1805:function(e,t,n){var r=n(7400),o=n(8270),i=Function.prototype,a=r&&Object.getOwnPropertyDescriptor,l=o(i,"name"),c=l&&"something"===function(){}.name,s=l&&(!r||r&&a(i,"name").configurable);e.exports={EXISTS:l,PROPER:c,CONFIGURABLE:s}},5968:function(e,t,n){var r=n(7188),o=Function.prototype,i=o.bind,a=o.call,l=r&&i.bind(a,a);e.exports=r?function(e){return e&&l(e)}:function(e){return e&&function(){return a.apply(e,arguments)}}},1333:function(e,t,n){var r=n(9859),o=n(6733),i=function(e){return o(e)?e:void 0};e.exports=function(e,t){return arguments.length<2?i(r[e]):r[e]&&r[e][t]}},5300:function(e,t,n){var r=n(7111);e.exports=function(e,t){var n=e[t];return null==n?void 0:r(n)}},17:function(e,t,n){var r=n(5968),o=n(2991),i=Math.floor,a=r("".charAt),l=r("".replace),c=r("".slice),s=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,u=/\$([$&'`]|\d{1,2})/g;e.exports=function(e,t,n,r,d,f){var p=n+e.length,h=r.length,m=u;return void 0!==d&&(d=o(d),m=s),l(f,m,(function(o,l){var s;switch(a(l,0)){case"$":return"$";case"&":return e;case"`":return c(t,0,n);case"'":return c(t,p);case"<":s=d[c(l,1,-1)];break;default:var u=+l;if(0===u)return o;if(u>h){var f=i(u/10);return 0===f?o:f<=h?void 0===r[f-1]?a(l,1):r[f-1]+a(l,1):o}s=r[u-1]}return void 0===s?"":s}))}},9859:function(e,t,n){var r=function(e){return e&&e.Math==Math&&e};e.exports=r("object"==typeof globalThis&&globalThis)||r("object"==typeof window&&window)||r("object"==typeof self&&self)||r("object"==typeof n.g&&n.g)||function(){return this}()||Function("return this")()},8270:function(e,t,n){var r=n(5968),o=n(2991),i=r({}.hasOwnProperty);e.exports=Object.hasOwn||function(e,t){return i(o(e),t)}},5977:function(e){e.exports={}},3777:function(e,t,n){var r=n(1333);e.exports=r("document","documentElement")},4394:function(e,t,n){var r=n(7400),o=n(4229),i=n(2635);e.exports=!r&&!o((function(){return 7!=Object.defineProperty(i("div"),"a",{get:function(){return 7}}).a}))},9337:function(e,t,n){var r=n(5968),o=n(4229),i=n(7079),a=Object,l=r("".split);e.exports=o((function(){return!a("z").propertyIsEnumerable(0)}))?function(e){return"String"==i(e)?l(e,""):a(e)}:a},835:function(e,t,n){var r=n(6733),o=n(5052),i=n(6540);e.exports=function(e,t,n){var a,l;return i&&r(a=t.constructor)&&a!==n&&o(l=a.prototype)&&l!==n.prototype&&i(e,l),e}},8511:function(e,t,n){var r=n(5968),o=n(6733),i=n(5353),a=r(Function.toString);o(i.inspectSource)||(i.inspectSource=function(e){return a(e)}),e.exports=i.inspectSource},9679:function(e,t,n){var r=n(5052),o=n(5762);e.exports=function(e,t){r(t)&&"cause"in t&&o(e,"cause",t.cause)}},6407:function(e,t,n){var r,o,i,a=n(8694),l=n(9859),c=n(5968),s=n(5052),u=n(5762),d=n(8270),f=n(5353),p=n(4399),h=n(5977),m="Object already initialized",g=l.TypeError,v=l.WeakMap;if(a||f.state){var y=f.state||(f.state=new v),w=c(y.get),_=c(y.has),b=c(y.set);r=function(e,t){if(_(y,e))throw new g(m);return t.facade=e,b(y,e,t),t},o=function(e){return w(y,e)||{}},i=function(e){return _(y,e)}}else{var x=p("state");h[x]=!0,r=function(e,t){if(d(e,x))throw new g(m);return t.facade=e,u(e,x,t),t},o=function(e){return d(e,x)?e[x]:{}},i=function(e){return d(e,x)}}e.exports={set:r,get:o,has:i,enforce:function(e){return i(e)?o(e):r(e,{})},getterFor:function(e){return function(t){var n;if(!s(t)||(n=o(t)).type!==e)throw g("Incompatible receiver, "+e+" required");return n}}}},6733:function(e){e.exports=function(e){return"function"==typeof e}},6541:function(e,t,n){var r=n(4229),o=n(6733),i=/#|\.prototype\./,a=function(e,t){var n=c[l(e)];return n==u||n!=s&&(o(t)?r(t):!!t)},l=a.normalize=function(e){return String(e).replace(i,".").toLowerCase()},c=a.data={},s=a.NATIVE="N",u=a.POLYFILL="P";e.exports=a},5052:function(e,t,n){var r=n(6733);e.exports=function(e){return"object"==typeof e?null!==e:r(e)}},4231:function(e){e.exports=!1},9395:function(e,t,n){var r=n(1333),o=n(6733),i=n(1321),a=n(6969),l=Object;e.exports=a?function(e){return"symbol"==typeof e}:function(e){var t=r("Symbol");return o(t)&&i(t.prototype,l(e))}},693:function(e,t,n){"use strict";var r,o,i,a=n(4229),l=n(6733),c=n(2391),s=n(7567),u=n(4768),d=n(95),f=n(4231),p=d("iterator"),h=!1;[].keys&&("next"in(i=[].keys())?(o=s(s(i)))!==Object.prototype&&(r=o):h=!0),null==r||a((function(){var e={};return r[p].call(e)!==e}))?r={}:f&&(r=c(r)),l(r[p])||u(r,p,(function(){return this})),e.exports={IteratorPrototype:r,BUGGY_SAFARI_ITERATORS:h}},5495:function(e){e.exports={}},9646:function(e,t,n){var r=n(4237);e.exports=function(e){return r(e.length)}},6039:function(e,t,n){var r=n(4229),o=n(6733),i=n(8270),a=n(7400),l=n(1805).CONFIGURABLE,c=n(8511),s=n(6407),u=s.enforce,d=s.get,f=Object.defineProperty,p=a&&!r((function(){return 8!==f((function(){}),"length",{value:8}).length})),h=String(String).split("String"),m=e.exports=function(e,t,n){"Symbol("===String(t).slice(0,7)&&(t="["+String(t).replace(/^Symbol\(([^)]*)\)/,"$1")+"]"),n&&n.getter&&(t="get "+t),n&&n.setter&&(t="set "+t),(!i(e,"name")||l&&e.name!==t)&&(a?f(e,"name",{value:t,configurable:!0}):e.name=t),p&&n&&i(n,"arity")&&e.length!==n.arity&&f(e,"length",{value:n.arity});try{n&&i(n,"constructor")&&n.constructor?a&&f(e,"prototype",{writable:!1}):e.prototype&&(e.prototype=void 0)}catch(e){}var r=u(e);return i(r,"source")||(r.source=h.join("string"==typeof t?t:"")),e};Function.prototype.toString=m((function(){return o(this)&&d(this).source||c(this)}),"toString")},917:function(e){var t=Math.ceil,n=Math.floor;e.exports=Math.trunc||function(e){var r=+e;return(r>0?n:t)(r)}},3839:function(e,t,n){var r=n(6358),o=n(4229);e.exports=!!Object.getOwnPropertySymbols&&!o((function(){var e=Symbol();return!String(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&r&&r<41}))},8694:function(e,t,n){var r=n(9859),o=n(6733),i=n(8511),a=r.WeakMap;e.exports=o(a)&&/native code/.test(i(a))},635:function(e,t,n){var r=n(3326);e.exports=function(e,t){return void 0===e?arguments.length<2?"":t:r(e)}},2391:function(e,t,n){var r,o=n(1176),i=n(219),a=n(3837),l=n(5977),c=n(3777),s=n(2635),u=n(4399),d=u("IE_PROTO"),f=function(){},p=function(e){return" + +{% endblock scripts %} diff --git a/app/views/notes_formsemestre.py b/app/views/notes_formsemestre.py index 843370422..eac987603 100644 --- a/app/views/notes_formsemestre.py +++ b/app/views/notes_formsemestre.py @@ -105,3 +105,16 @@ def formsemestre_change_formation(formsemestre_id: int): formsemestre=formsemestre, sco=ScoData(formsemestre=formsemestre), ) + + +@bp.route("/formsemestre/edt/") +@scodoc +@permission_required(Permission.ScoView) +def formsemestre_edt(formsemestre_id: int): + """Expérimental: affiche emploi du temps du semestre""" + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + return render_template( + "formsemestre/edt.j2", + formsemestre=formsemestre, + sco=ScoData(formsemestre=formsemestre), + ) diff --git a/sco_version.py b/sco_version.py index 7771d1df9..08f5d05eb 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.51" +SCOVERSION = "9.6.52" SCONAME = "ScoDoc" From 0f61b0874aab631a6d2cf7701dc650f07bb06b5b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 12 Nov 2023 19:58:55 +0100 Subject: [PATCH 12/69] =?UTF-8?q?WIP:=20codes=20Apo=20et=20EDT=20sur=20cha?= =?UTF-8?q?que=20modimpl.=20(pas=20encore=20utilis=C3=A9=20dans=20exports?= =?UTF-8?q?=20Apo).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../formsemestre/edit_modimpls_codes_apo.py | 52 +++++++++++ app/forms/main/config_personalized_links.py | 3 +- app/models/moduleimpls.py | 22 ++++- app/scodoc/sco_formsemestre_edit.py | 4 + .../formsemestre/change_formation.j2 | 8 +- .../formsemestre/edit_modimpls_codes.j2 | 91 +++++++++++++++++++ app/views/notes_formsemestre.py | 47 +++++++++- .../c8f66652c77f_code_apo_sur_modimpls.py | 38 ++++++++ 8 files changed, 255 insertions(+), 10 deletions(-) create mode 100644 app/forms/formsemestre/edit_modimpls_codes_apo.py create mode 100644 app/templates/formsemestre/edit_modimpls_codes.j2 create mode 100644 migrations/versions/c8f66652c77f_code_apo_sur_modimpls.py diff --git a/app/forms/formsemestre/edit_modimpls_codes_apo.py b/app/forms/formsemestre/edit_modimpls_codes_apo.py new file mode 100644 index 000000000..bdd84a18d --- /dev/null +++ b/app/forms/formsemestre/edit_modimpls_codes_apo.py @@ -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 BooleanField, 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() diff --git a/app/forms/main/config_personalized_links.py b/app/forms/main/config_personalized_links.py index b2293a2c2..2a261e766 100644 --- a/app/forms/main/config_personalized_links.py +++ b/app/forms/main/config_personalized_links.py @@ -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 diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 741abda1b..468888e6a 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -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( @@ -45,11 +50,21 @@ class ModuleImpl(db.Model): 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_id(self) -> str: - "l'id pour l'emploi du temps: actuellement celui du module" + "l'id pour l'emploi du temps: à défaut, le 1er code Apogée" return ( - self.module.get_edt_id() - ) # TODO à décliner pour autoriser des codes différents ? + self.edt_id + or (self.code_apogee.split(",")[0] if self.code_apogee else "") + or self.module.get_edt_id() + ) def get_evaluations_poids(self) -> pd.DataFrame: """Les poids des évaluations vers les UE (accès via cache)""" @@ -102,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: diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 4f5ae61de..bff18edb3 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -464,6 +464,10 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id) }">Modifier les coefficients des UE capitalisées

+

Modifier les codes Apogée et emploi du temps des modules +

Sélectionner les modules, leurs responsables et les étudiants à inscrire:

""" diff --git a/app/templates/formsemestre/change_formation.j2 b/app/templates/formsemestre/change_formation.j2 index 0b3820cae..8c0b8b5b3 100644 --- a/app/templates/formsemestre/change_formation.j2 +++ b/app/templates/formsemestre/change_formation.j2 @@ -11,11 +11,11 @@

Changement de la formation du semestre

On ne peut pas changer la formation d'un semestre existant car -elle défini son organisation (modules, ...), SAUF si la nouvelle formation a -exactement le même contenu que l'existante. +elle définit son organisation (modules, ...), SAUF si la nouvelle formation a +exactement le même contenu que l'existante. Cela peut arriver par exemple lorsqu'on crée une nouvelle version (pas encore modifiée) et que l'on a oublié d'y rattacher un semestre. -

+

{% if formations %}
@@ -27,4 +27,4 @@ et que l'on a oublié d'y rattacher un semestre.
Aucune formation ne peut se substituer à celle de ce semestre.
{% endif %}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/formsemestre/edit_modimpls_codes.j2 b/app/templates/formsemestre/edit_modimpls_codes.j2 new file mode 100644 index 000000000..d70b6e280 --- /dev/null +++ b/app/templates/formsemestre/edit_modimpls_codes.j2 @@ -0,0 +1,91 @@ +{% extends "sco_page.j2" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block styles %} +{{super()}} + +{% endblock %} + +{% macro render_text_field(field_apo, field_edt, codes_apo_module) %} +
+ + {{ field_apo.label(class_="form-label") }} + {{codes_apo_module|join(", ") or ("non défini"|safe)}} + + {{field_apo(class_="form-field")}} + {{field_edt(class_="field-edt")}} + {%- for error in field_apo.errors %} + {{ error }} + {% endfor %} + {%- for error in field_edt.errors %} + {{ error }} + {% endfor %} +
+{% endmacro %} + + +{% block app_content %} + +
+

Codes Apogée et emploi du temps des modules du semestre

+ +

Les codes élément Apogée sont utilisés pour les exports des +résultats et peuvent aussi l'être pour connecter l'emploi du temps. Si votre +logiciel d'emploi du temps utilise des codes différents, vous pouvez aussi +indiquer un code EDT spécifique. +

+ +

Les codes Apogée modules rappelés à gauche sont ceux définis +dans la formation: il sont utilisés sauf si on spécifie un code ici. +Pour les modifier, aller dans l'édition de la formation. +

+ + +
+
+ + {{ form.hidden_tag() }} + {{ wtf.form_errors(form, hiddens="only") }} +
+ + + Code Apo. Module + Code(s) Apogée + Code EDT + +
+ {% for modimpl in formsemestre.modimpls_sorted %} + {{ render_text_field(form["modimpl_apo_" ~ modimpl.id], form["modimpl_edt_" ~ modimpl.id], modimpl.module.get_codes_apogee()) }} + {% endfor %} +
+ {{ wtf.form_field(form.submit) }} + {{ wtf.form_field(form.cancel) }} +
+ +
+
+ +
+{% endblock %} diff --git a/app/views/notes_formsemestre.py b/app/views/notes_formsemestre.py index eac987603..84d1509a7 100644 --- a/app/views/notes_formsemestre.py +++ b/app/views/notes_formsemestre.py @@ -32,11 +32,12 @@ Emmanuel Viennet, 2023 from flask import flash, redirect, render_template, url_for from flask import g, request +from app import db, log from app.decorators import ( scodoc, permission_required, ) -from app.forms.formsemestre import change_formation +from app.forms.formsemestre import change_formation, edit_modimpls_codes_apo from app.models import Formation, FormSemestre from app.scodoc import sco_formations, sco_formation_versions from app.scodoc.sco_permissions import Permission @@ -107,6 +108,50 @@ def formsemestre_change_formation(formsemestre_id: int): ) +@bp.route( + "/formsemestre_edit_modimpls_codes/", methods=["GET", "POST"] +) +@scodoc +@permission_required(Permission.EditFormSemestre) +def formsemestre_edit_modimpls_codes(formsemestre_id: int): + """Edition des codes Apogée et EDT""" + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + form = edit_modimpls_codes_apo.EditModimplsCodesForm(formsemestre) + + if request.method == "POST" and form.validate: + if not form.cancel.data: + # record codes + for modimpl in formsemestre.modimpls_sorted: + field_apo = getattr(form, f"modimpl_apo_{modimpl.id}") + field_edt = getattr(form, f"modimpl_edt_{modimpl.id}") + if field_apo and field_edt: + modimpl.code_apogee = field_apo.data.strip() or None + modimpl.edt_id = field_edt.data.strip() or None + log(f"setting codes for {modimpl}: apo={field_apo} edt={field_edt}") + db.session.add(modimpl) + db.session.commit() + flash("Codes enregistrés") + return redirect( + url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + # GET + for modimpl in formsemestre.modimpls_sorted: + field_apo = getattr(form, f"modimpl_apo_{modimpl.id}") + field_edt = getattr(form, f"modimpl_edt_{modimpl.id}") + field_apo.data = modimpl.code_apogee or "" + field_edt.data = modimpl.edt_id or "" + return render_template( + "formsemestre/edit_modimpls_codes.j2", + form=form, + formsemestre=formsemestre, + sco=ScoData(formsemestre=formsemestre), + ) + + @bp.route("/formsemestre/edt/") @scodoc @permission_required(Permission.ScoView) diff --git a/migrations/versions/c8f66652c77f_code_apo_sur_modimpls.py b/migrations/versions/c8f66652c77f_code_apo_sur_modimpls.py new file mode 100644 index 000000000..5d818b767 --- /dev/null +++ b/migrations/versions/c8f66652c77f_code_apo_sur_modimpls.py @@ -0,0 +1,38 @@ +"""code apo sur modimpls + +Revision ID: c8f66652c77f +Revises: 6fb956addd69 +Create Date: 2023-11-12 10:01:42.424734 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "c8f66652c77f" +down_revision = "6fb956addd69" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("notes_moduleimpl", schema=None) as batch_op: + batch_op.add_column( + sa.Column("code_apogee", sa.String(length=512), nullable=True) + ) + batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True)) + batch_op.create_index( + batch_op.f("ix_notes_moduleimpl_code_apogee"), ["code_apogee"], unique=False + ) + batch_op.create_index( + batch_op.f("ix_notes_moduleimpl_edt_id"), ["edt_id"], unique=False + ) + + +def downgrade(): + with op.batch_alter_table("notes_moduleimpl", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_notes_moduleimpl_edt_id")) + batch_op.drop_index(batch_op.f("ix_notes_moduleimpl_code_apogee")) + batch_op.drop_column("edt_id") + batch_op.drop_column("code_apogee") From bf096d4648f996c5c30221f538c733656d4e9696 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 12 Nov 2023 21:45:06 +0100 Subject: [PATCH 13/69] WIP: lien saisie abs sur EDT. --- app/scodoc/sco_edt_cal.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index cd4601408..ee50e2dd7 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -33,7 +33,7 @@ XXX usage uniquement experimental pour tests implémentations import re import icalendar -from flask import flash +from flask import flash, g, url_for from app import log from app.models import FormSemestre, GroupDescr, ModuleImpl, ScoDocSiteConfig import app.scodoc.sco_utils as scu @@ -126,15 +126,30 @@ def formsemestre_edt_dict(formsemestre: FormSemestre) -> list[dict]: edt_module = extract_event_module(event) modimpl: ModuleImpl = edt2modimpl.get(edt_module, None) mod_disp = ( - f"""
{modimpl.module.code} -
""" + f"""
{ + modimpl.module.code}
""" if modimpl - else f"""
{scu.EMO_WARNING} {edt_module}
""" + else f"""
{ + scu.EMO_WARNING} {edt_module}
""" + ) + # --- Lien saisie abs + link_abs = ( + f"""""" + if modimpl and group + else "" ) d = { # Champs utilisés par tui.calendar "calendarId": "cal1", - "title": extract_event_title(event) + group_disp + mod_disp, + "title": extract_event_title(event) + group_disp + mod_disp + link_abs, "start": event.decoded("dtstart").isoformat(), "end": event.decoded("dtend").isoformat(), "backgroundColor": background_color, From 9f357e1a2fd82ff777434683ae6f982197c3e121 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 12 Nov 2023 21:50:07 +0100 Subject: [PATCH 14/69] EDT: fix typo lien saisie abs --- app/scodoc/sco_edt_cal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index ee50e2dd7..e8ba3110e 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -135,7 +135,7 @@ def formsemestre_edt_dict(formsemestre: FormSemestre) -> list[dict]: # --- Lien saisie abs link_abs = ( f"""""" From 83765b584e5488408c91be6e1607d8e5e03f87a5 Mon Sep 17 00:00:00 2001 From: Iziram Date: Mon, 13 Nov 2023 08:35:19 +0100 Subject: [PATCH 16/69] Assiduites : champs "heure_deb" "heure_fin" timeline --- app/templates/assiduites/widgets/timeline.j2 | 29 +++++++++++++++++--- app/views/assiduites.py | 19 +++++++++---- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/app/templates/assiduites/widgets/timeline.j2 b/app/templates/assiduites/widgets/timeline.j2 index 3a4507085..9d614195f 100644 --- a/app/templates/assiduites/widgets/timeline.j2 +++ b/app/templates/assiduites/widgets/timeline.j2 @@ -131,12 +131,12 @@ document.addEventListener( "mouseup", mouseUp, - {once:true} + { once: true } ); document.addEventListener( "touchend", mouseUp, - {once:true} + { once: true } ); } else if (event.target === periodTimeLine) { @@ -217,6 +217,14 @@ } function setPeriodValues(deb, fin) { + if (fin < deb) { + throw new RangeError(`le paramètre 'deb' doit être inférieur au paramètre 'fin' ([${deb};${fin}])`) + } + + if (deb < 0 || fin < 0) { + throw new RangeError(`Les paramètres doivent être des entiers positifis ([${deb};${fin}])`) + } + deb = snapToQuarter(deb); fin = snapToQuarter(fin); let leftPercentage = (deb - t_start) / (t_end - t_start) * 100; @@ -231,13 +239,13 @@ function snapHandlesToQuarters() { const periodValues = getPeriodValues(); - let lef = Math.min(computePercentage(periodValues[0], t_start), computePercentage(t_end, tick_delay)); + let lef = Math.min(computePercentage(Math.abs(periodValues[0]), t_start), computePercentage(Math.abs(t_end), tick_delay)); if (lef < 0) { lef = 0; } const left = `${lef}%`; - let wid = Math.max(computePercentage(periodValues[1], periodValues[0]), computePercentage(tick_delay, 0)); + let wid = Math.max(computePercentage(Math.abs(periodValues[1]), Math.abs(periodValues[0])), computePercentage(tick_delay, 0)); if (wid > 100) { wid = 100; } @@ -251,10 +259,23 @@ function computePercentage(a, b) { return ((a - b) / (t_end - t_start)) * 100; } + function fromTime(time, separator = ":") { + const [hours, minutes] = time.split(separator).map((el) => Number(el)) + return hours + minutes / 60 + } createTicks(); setPeriodValues(t_start, t_start + period_default); + {% if heures %} + let [heure_deb, heure_fin] = [{{ heures | safe }}] + if (heure_deb != '' && heure_fin != '') { + heure_deb = fromTime(heure_deb); + heure_fin = fromTime(heure_fin); + setPeriodValues(heure_deb, heure_fin) + } + {% endif %} + +{% endblock %} + + {% block app_content %}
@@ -33,6 +46,25 @@ affectent notamment les comptages d'absences de tous les bulletins des {{ wtf.form_field(form.edt_ics_path) }}
+
Extraction des identifiants depuis les calendriers
+
+ Indiquer ici comment récupérer les informations (titre, groupe, module) + dans les calendriers publiés par votre logiciel d'emploi du temps. +
+
+ {{ wtf.form_field(form.edt_ics_title_field) }} + {{ wtf.form_field(form.edt_ics_title_regexp) }} +
+
+ {{ wtf.form_field(form.edt_ics_group_field) }} + {{ wtf.form_field(form.edt_ics_group_regexp) }} +
+
+ {{ wtf.form_field(form.edt_ics_mod_field) }} + {{ wtf.form_field(form.edt_ics_mod_regexp) }} +
+ +
{{ wtf.form_field(form.submit) }} {{ wtf.form_field(form.cancel) }} @@ -41,8 +73,4 @@ affectent notamment les comptages d'absences de tous les bulletins des
- - - - {% endblock %} diff --git a/app/templates/formsemestre/edt.j2 b/app/templates/formsemestre/edt.j2 index b6ee3ad90..2dbdaadb1 100644 --- a/app/templates/formsemestre/edt.j2 +++ b/app/templates/formsemestre/edt.j2 @@ -51,6 +51,13 @@ document.addEventListener('DOMContentLoaded', function() { return `${start} ${event.title}`; }, }, + timezone: { + zones: [ + { + timezoneName: 'CET', // TODO récupérer timezone serveur + }, + ], + }, usageStatistics: false, week: { dayNames: [ "Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"], diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 4ecd9f607..4d5af816f 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -324,6 +324,16 @@ def config_assiduites(): if request.method == "POST" and form.cancel.data: # cancel button return redirect(url_for("scodoc.index")) + edt_options = ( + ("edt_ics_path", "Chemin vers les calendriers ics"), + ("edt_ics_title_field", "Champ contenant titre"), + ("edt_ics_title_regexp", "Expression extraction titre"), + ("edt_ics_group_field", "Champ contenant groupe"), + ("edt_ics_group_regexp", "Expression extraction groupe"), + ("edt_ics_mod_field", "Champ contenant module"), + ("edt_ics_mod_regexp", "Expression extraction module"), + ) + if form.validate_on_submit(): if ScoDocSiteConfig.set("assi_morning_time", form.data["morning_time"]): flash("Heure du début de la journée enregistrée") @@ -333,8 +343,11 @@ def config_assiduites(): flash("Heure de fin de la journée enregistrée") if ScoDocSiteConfig.set("assi_tick_time", float(form.data["tick_time"])): flash("Granularité de la timeline enregistrée") - if ScoDocSiteConfig.set("edt_ics_path", form.data["edt_ics_path"]): - flash("Chemin vers les calendriers ics enregistré") + # --- Calendriers emploi du temps + for opt_name, message in edt_options: + if ScoDocSiteConfig.set(opt_name, form.data[opt_name]): + flash(f"{message} enregistré") + return redirect(url_for("scodoc.configuration")) if request.method == "GET": @@ -352,7 +365,9 @@ def config_assiduites(): except ValueError: form.tick_time.data = 15.0 ScoDocSiteConfig.set("assi_tick_time", 15.0) - form.edt_ics_path.data = ScoDocSiteConfig.get("edt_ics_path") + # --- Emplois du temps + for opt_name, _ in edt_options: + getattr(form, opt_name).data = ScoDocSiteConfig.get(opt_name) return render_template( "assiduites/pages/config_assiduites.j2", From 456d570c9b691e2ac681d22e18b557fac89f3710 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 13 Nov 2023 23:16:23 +0100 Subject: [PATCH 20/69] oups --- app/scodoc/sco_edt_cal.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index 9fccc0658..81c525097 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -256,39 +256,7 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]: "end": event.decoded("dtend").isoformat(), } ) -<<<<<<< HEAD - # --- Lien saisie abs - link_abs = ( - f"""""" - if modimpl and group - else "" - ) - d = { - # Champs utilisés par tui.calendar - "calendarId": "cal1", - "title": extract_event_title(event) + group_disp + mod_disp + link_abs, - "start": event.decoded("dtstart").isoformat(), - "end": event.decoded("dtend").isoformat(), - "backgroundColor": background_color, - # Infos brutes pour usage API éventuel - "group_id": group.id if group else None, - "group_edt_id": edt_group, - "moduleimpl_id": modimpl.id if modimpl else None, - } - events_dict.append(d) - - return events_dict -======= return events_sco ->>>>>>> edt def extract_event_data( From 324488f9c4a52652226e416c00bc174642b7327a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 13 Nov 2023 23:43:54 +0100 Subject: [PATCH 21/69] EDT: lien saisie abs avec heure slocales serveur --- app/scodoc/sco_edt_cal.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index 81c525097..0c7f89b44 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -30,6 +30,7 @@ XXX usage uniquement experimental pour tests implémentations """ +from datetime import timezone import re import icalendar @@ -249,8 +250,15 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]: "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 - "heure_deb": event.decoded("dtstart").strftime("%H:%M"), - "heure_fin": event.decoded("dtend").strftime("%H:%M"), + # 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").isoformat(), "start": event.decoded("dtstart").isoformat(), "end": event.decoded("dtend").isoformat(), From 098502f2d345855782de756ec3eeea1306eb21fd Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 14 Nov 2023 14:06:47 +0100 Subject: [PATCH 22/69] =?UTF-8?q?EDT:=20am=C3=A9liore=20gestion=20erreur?= =?UTF-8?q?=20chargement=20ics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/formsemestres.py | 4 +++- app/scodoc/sco_edt_cal.py | 31 ++++++++++++++++++++++++++----- app/templates/formsemestre/edt.j2 | 7 ++++++- sco_version.py | 2 +- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 87a640194..3aaef1c78 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -565,7 +565,9 @@ def save_groups_auto_assignment(formsemestre_id: int): @permission_required(Permission.ScoView) @as_json def formsemestre_edt(formsemestre_id: int): - """l'emploi du temps du semestre""" + """l'emploi du temps du semestre + Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur. + """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index 0c7f89b44..7d63add01 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -58,13 +58,31 @@ def formsemestre_load_calendar( try: with open(ics_filename, "rb") as file: log(f"Loading edt from {ics_filename}") - calendar = icalendar.Calendar.from_ical(file.read()) - except FileNotFoundError: - flash("erreur chargement du calendrier") + try: + calendar = icalendar.Calendar.from_ical(file.read()) + except ValueError as exc: + log( + f"""formsemestre_load_calendar: error importing ics for { + formsemestre}\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}\npath='{ics_filename}'" ) - return None + raise ScoValueError( + f"Fichier ics introuvable (filename={ics_filename})" + ) from exc + except PermissionError as exc: + log( + f"""formsemestre_load_calendar: permission denied for {formsemestre + }\npath='{ics_filename}'""" + ) + raise ScoValueError( + f"Fichier ics inaccessible: vérifier permissions (filename={ics_filename})" + ) from exc return calendar @@ -88,7 +106,10 @@ def formsemestre_edt_dict(formsemestre: FormSemestre) -> list[dict]: Fonction appellée par l'API /formsemestre//edt TODO: spécifier intervalle de dates start et end """ - events_scodoc = _load_and_convert_ics(formsemestre) + 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: diff --git a/app/templates/formsemestre/edt.j2 b/app/templates/formsemestre/edt.j2 index 2dbdaadb1..d5bb1debf 100644 --- a/app/templates/formsemestre/edt.j2 +++ b/app/templates/formsemestre/edt.j2 @@ -95,7 +95,12 @@ document.addEventListener('DOMContentLoaded', function() { fetch(`${SCO_URL}/../api/formsemestre/{{formsemestre.id}}/edt`) .then(r=>{return r.json()}) .then(events=>{ - calendar.createEvents(events); + if (typeof events == 'string') { + // error ! + alert("error: " + events) + } else { + calendar.createEvents(events); + } }); }); diff --git a/sco_version.py b/sco_version.py index 1fd49c111..e8ed7d5f8 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.53" +SCOVERSION = "9.6.54" SCONAME = "ScoDoc" From cc579c59f7afb67b6409a49a5468706431e994b3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 14 Nov 2023 14:20:36 +0100 Subject: [PATCH 23/69] =?UTF-8?q?Import=20Apo:=20tol=C3=A8re=20erreur=20si?= =?UTF-8?q?=20etudiant=20apo=20dupliqu=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_portal_apogee.py | 47 ++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/app/scodoc/sco_portal_apogee.py b/app/scodoc/sco_portal_apogee.py index 95ad30d99..ae58aa737 100644 --- a/app/scodoc/sco_portal_apogee.py +++ b/app/scodoc/sco_portal_apogee.py @@ -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: From 9a0e64effdc00d08b92f40c438fdc316ffd53503 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 14 Nov 2023 15:55:52 +0100 Subject: [PATCH 24/69] EDT: config.: chargement d'un sample ics pour aider --- app/scodoc/sco_edt_cal.py | 45 +++++++++------ .../assiduites/pages/config_assiduites.j2 | 57 ++++++++++++++++++- app/views/scodoc.py | 20 ++++++- 3 files changed, 101 insertions(+), 21 deletions(-) diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index 7d63add01..476546f9b 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -41,50 +41,58 @@ from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu -def formsemestre_load_calendar( - formsemestre: FormSemestre, -) -> icalendar.cal.Calendar | None: - """Load ics data, return calendar or None if not configured or not available""" - edt_id = formsemestre.get_edt_id() - if not edt_id: - flash( - "accès aux emplois du temps non configuré pour ce semestre (pas d'edt_id)" - ) - return None +def get_ics_filename(edt_id: str) -> str: + "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 - ics_filename = edt_ics_path.format(edt_id=edt_id) + 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. + """ + if edt_id is None and formsemestre: + edt_id = formsemestre.get_edt_id() + if not edt_id: + raise ScoValueError( + "accès aux emplois du temps non configuré pour ce semestre (pas d'edt_id)" + ) + ics_filename = get_ics_filename(edt_id) try: with open(ics_filename, "rb") as file: log(f"Loading edt from {ics_filename}") + data = file.read() try: - calendar = icalendar.Calendar.from_ical(file.read()) + calendar = icalendar.Calendar.from_ical(data) except ValueError as exc: log( f"""formsemestre_load_calendar: error importing ics for { - formsemestre}\npath='{ics_filename}'""" + 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}\npath='{ics_filename}'" + 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 + 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 calendar + return data, calendar # --- Couleurs des évènements emploi du temps @@ -178,7 +186,7 @@ def formsemestre_edt_dict(formsemestre: FormSemestre) -> list[dict]: 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) + _, calendar = formsemestre_load_calendar(formsemestre) if not calendar: return [] # --- Paramètres d'extraction @@ -236,7 +244,8 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]: edt_group = extract_event_data( event, edt_ics_group_field, edt_ics_group_pattern ) - # si pas de groupe dans l'event, oi si groupe non reconnu, prend toute la promo ("tous") + # 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 diff --git a/app/templates/assiduites/pages/config_assiduites.j2 b/app/templates/assiduites/pages/config_assiduites.j2 index 7522968a9..3d75e9b8c 100644 --- a/app/templates/assiduites/pages/config_assiduites.j2 +++ b/app/templates/assiduites/pages/config_assiduites.j2 @@ -10,9 +10,50 @@ div.config-section { margin-right: -15px; margin-left: -15px; } - -{% endblock %} +#zone-test { + margin-bottom: 12px; +} +#raw-ics-sample-zone { + display: none; +} +#raw-ics-sample-zone>div { + font-style: italic; +} +#test_load_ics:disabled { + background-color: gray; + color: white; /* Optional: change text color if needed */ + cursor: not-allowed; +} + +{% endblock styles %} + + +{% block scripts %} +{{ super() }} + + +{% endblock scripts %} {% block app_content %}
@@ -46,6 +87,18 @@ affectent notamment les comptages d'absences de tous les bulletins des {{ wtf.form_field(form.edt_ics_path) }}
+
+ Pour essayer, indiquer un edt_id : + + +
+
Voici un évènement chargé au milieu de ce calendrier: +
+ +
+
Extraction des identifiants depuis les calendriers
Indiquer ici comment récupérer les informations (titre, groupe, module) diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 4d5af816f..6c30eec34 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -81,7 +81,7 @@ from app.models import ( from app.models import departements from app.models.config import PersonalizedLink - +from app.scodoc import sco_edt_cal from app.scodoc import sco_find_etud from app.scodoc import sco_logos from app.scodoc import sco_utils as scu @@ -376,6 +376,24 @@ def config_assiduites(): ) +@bp.route("/ScoDoc/ics_raw_sample/") +@admin_required +def ics_raw_sample(edt_id: str): + "Renvoie un extrait de l'ics brut, pour aider à configurer les extractions" + try: + raw_ics, _ = sco_edt_cal.formsemestre_load_calendar(edt_id=edt_id) + except ScoValueError as exc: + return exc.args[0] + try: + ics = raw_ics.decode(scu.SCO_ENCODING) + except SyntaxError: + return f"Erreur lors de la conversion vers {scu.SCO_ENCODING}" + evs = ics.split("BEGIN:VEVENT") + if len(evs) < 1: + return "pas d'évènements VEVENT détectés dans ce fichier" + return "BEGIN:VEVENT" + evs[len(evs) // 2] + + @bp.route("/ScoDoc/config_codes_decisions", methods=["GET", "POST"]) @admin_required def config_codes_decisions(): From 338d2f7ec8229c1d623733bdf4f4f14f0c194999 Mon Sep 17 00:00:00 2001 From: Iziram Date: Tue, 14 Nov 2023 16:38:07 +0100 Subject: [PATCH 25/69] Assiduites : fix journee entiere + utilisation scodoc-datetime --- app/scodoc/sco_utils.py | 2 +- app/static/js/date_utils.js | 84 ++++++++++++++++--- .../assiduites/pages/ajout_justificatif.j2 | 58 ++++++------- 3 files changed, 100 insertions(+), 44 deletions(-) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index bb26b58ad..6e04677dc 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -231,7 +231,7 @@ def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or No try: 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 diff --git a/app/static/js/date_utils.js b/app/static/js/date_utils.js index d22487d32..486de19ee 100644 --- a/app/static/js/date_utils.js +++ b/app/static/js/date_utils.js @@ -1,3 +1,8 @@ +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. @@ -338,6 +343,7 @@ class ScoDocDateTimePicker extends HTMLElement { const timeInput = document.createElement("input"); timeInput.type = "time"; timeInput.id = "time"; + timeInput.step = 60; // Ajouter les inputs dans le shadow DOM shadow.appendChild(dateInput); @@ -362,6 +368,10 @@ class ScoDocDateTimePicker extends HTMLElement { 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"); @@ -386,6 +396,34 @@ class ScoDocDateTimePicker extends HTMLElement { : ""; } }); + 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 @@ -418,17 +456,13 @@ class ScoDocDateTimePicker extends HTMLElement { return 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; - - if ((time.match(/0/g) || []).length > 1) { - time = time.slice(0, time.indexOf(":") + 3); - } - - this.shadowRoot.querySelector("#time").value = time; - this._value = val; + 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. @@ -436,6 +470,34 @@ class ScoDocDateTimePicker extends HTMLElement { 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. diff --git a/app/templates/assiduites/pages/ajout_justificatif.j2 b/app/templates/assiduites/pages/ajout_justificatif.j2 index 0c102cb13..0935ed60b 100644 --- a/app/templates/assiduites/pages/ajout_justificatif.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif.j2 @@ -11,15 +11,11 @@
-
- - -
Date de début - Journée(s) entière(s) + Journée entière
Date de fin @@ -55,6 +51,10 @@
+
+ + +
@@ -112,15 +112,14 @@ function validateFields() { const field = document.querySelector('.justi-form') const { deb, fin } = getDates() + const date_debut = new Date(deb); + const date_fin = new Date(fin); - if (deb == "" || fin == "") { + if (deb == "" || fin == "" || !date_debut.isValid() || !date_fin.isValid()) { openAlertModal("Erreur détéctée", document.createTextNode("Il faut indiquer une date de début et une date de fin valide."), "", color = "crimson"); return false; } - const date_debut = new Date(deb); - const date_fin = new Date(fin); - if (date_fin.isBefore(date_debut)) { openAlertModal("Erreur détéctée", document.createTextNode("La date de fin doit se trouver après la date de début."), "", color = "crimson"); return false; @@ -219,39 +218,31 @@ } function dayOnly() { - const { deb, fin } = getDates(); + const date_deb = document.getElementById("justi_date_debut"); + const date_fin = document.getElementById("justi_date_fin"); if (document.getElementById('justi_journee').checked) { - document.getElementById("justi_date_debut").type = "date" - document.getElementById("justi_date_debut").value = deb.slice(0, deb.indexOf('T')) - - document.getElementById("justi_date_fin").type = "date" - document.getElementById("justi_date_fin").value = fin.slice(0, fin.indexOf('T')) + date_deb.setAttribute("show", "date") + date_fin.setAttribute("show", "date") + document.getElementById("date_fin").classList.add("hidden"); } else { - document.getElementById("justi_date_debut").type = "datetime-local" - document.getElementById("justi_date_debut").value = `${deb}T${assi_morning}` + date_deb.removeAttribute("show") + date_fin.removeAttribute("show") + document.getElementById("date_fin").classList.remove("hidden"); - document.getElementById("justi_date_fin").type = "datetime-local" - document.getElementById("justi_date_fin").value = `${fin}T${assi_evening}` } } function getDates() { - if (document.querySelector('.page #justi_journee').checked) { - const date_str_deb = document.querySelector(".page #justi_date_debut").value - const date_str_fin = document.querySelector(".page #justi_date_fin").value - - - - return { - "deb": date_str_deb ? `${date_str_deb}T${assi_morning}` : "", - "fin": date_str_fin ? `${date_str_fin}T${assi_evening}` : "", - } - } + const date_deb = document.querySelector(".page #justi_date_debut") + const date_fin = document.querySelector(".page #justi_date_fin") + const journee = document.querySelector('.page #justi_journee').checked + const deb = date_deb.valueAsObject.date + "T" + (journee ? assi_morning : date_deb.valueAsObject.time) + const fin = (journee ? date_deb.valueAsObject.date : date_fin.valueAsObject.date) + "T" + (journee ? assi_evening : date_fin.valueAsObject.time) return { - "deb": document.querySelector(".page #justi_date_debut").value, - "fin": document.querySelector(".page #justi_date_fin").value, + "deb": deb, + "fin": fin, } } @@ -265,6 +256,9 @@ loadAll(); document.getElementById('justi_journee').addEventListener('click', () => { dayOnly() }); dayOnly() + + document.getElementById("justi_date_debut").valueAsObject = { time: assi_morning } + document.getElementById("justi_date_fin").valueAsObject = { time: assi_evening } } {% endblock pageContent %} \ No newline at end of file From e551ac5124cb0ee449bcf3383c3b436e23ccd480 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 14 Nov 2023 19:26:44 +0100 Subject: [PATCH 26/69] =?UTF-8?q?ajout=20l=C3=A9gende?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/templates/assiduites/pages/config_assiduites.j2 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/templates/assiduites/pages/config_assiduites.j2 b/app/templates/assiduites/pages/config_assiduites.j2 index 3d75e9b8c..620e3b760 100644 --- a/app/templates/assiduites/pages/config_assiduites.j2 +++ b/app/templates/assiduites/pages/config_assiduites.j2 @@ -93,7 +93,9 @@ affectent notamment les comptages d'absences de tous les bulletins des
-
Voici un évènement chargé au milieu de ce calendrier: +
Voici un évènement chargé au milieu de ce calendrier. + Utilisez cet exemple pour configurer les expressions d'extraction + en bas de ce formulaire.
From a043cb26edb8dc6d6abd9344bc02be983d53f370 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 14 Nov 2023 22:48:28 +0100 Subject: [PATCH 27/69] Fix: invalidation cache sur modif evaluation --- app/scodoc/sco_evaluation_edit.py | 8 +++++--- app/static/css/edt.css | 9 ++++++++- app/templates/formsemestre/edt.j2 | 13 +++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/scodoc/sco_evaluation_edit.py b/app/scodoc/sco_evaluation_edit.py index c24be275c..ed63817cd 100644 --- a/app/scodoc/sco_evaluation_edit.py +++ b/app/scodoc/sco_evaluation_edit.py @@ -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()}

Opération non autorisée

Modification évaluation impossible pour {current_user.get_nomplogin()}

-

Revenir

@@ -131,7 +132,7 @@ def evaluation_create_form( H = [ f"""

{action} en {scu.MODULE_TYPE_NAMES[mod["module_type"]]} {mod["code"] or "module sans code"} {mod["titre"]} {link}

""" @@ -299,7 +300,7 @@ def evaluation_create_form( "type": "float", "explanation": f""" ({ - "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" }) {ue.titre} @@ -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) diff --git a/app/static/css/edt.css b/app/static/css/edt.css index b0990d8f1..135be6530 100644 --- a/app/static/css/edt.css +++ b/app/static/css/edt.css @@ -23,5 +23,12 @@ height: calc(100% - 44px) !important; } .toastui-calendar-week-view-day-names, .toastui-calendar-time { - overflow: hidden !important; + overflow: hidden !important; +} + +.ic-arrow-line-left { + background: url('../icons/ic-arrow-line-left.png') no-repeat; +} +.ic-arrow-line-right { + background: url('../icons/ic-arrow-line-right.png') no-repeat; } diff --git a/app/templates/formsemestre/edt.j2 b/app/templates/formsemestre/edt.j2 index d5bb1debf..d8c92eb48 100644 --- a/app/templates/formsemestre/edt.j2 +++ b/app/templates/formsemestre/edt.j2 @@ -13,6 +13,19 @@

Expérimental: emploi du temps

+
+ + + + + + +
From d87432e8ac929bdfc00e918d589ff0bf8173d35e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 14 Nov 2023 23:16:25 +0100 Subject: [PATCH 28/69] EDT: navigation prev/next week --- app/static/css/edt.css | 64 +++++++++++++++++--------- app/templates/formsemestre/edt.j2 | 75 ++++++++++++++++++++++--------- 2 files changed, 98 insertions(+), 41 deletions(-) diff --git a/app/static/css/edt.css b/app/static/css/edt.css index 135be6530..f97c91e85 100644 --- a/app/static/css/edt.css +++ b/app/static/css/edt.css @@ -1,34 +1,58 @@ - .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; + 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; } .group-name { - color:rgb(25, 113, 25); + color: rgb(25, 113, 25); } .group-edt { - color: red; - background-color: yellow; + color: red; + background-color: yellow; } +#renderRange { + margin-left: 16px; +} .toastui-calendar-timegrid { - height: 100% !important; - min-height: auto !important; + height: 100% !important; + min-height: auto !important; } -.toastui-calendar-time{ - height: calc(100% - 44px) !important; +.toastui-calendar-time { + height: calc(100% - 44px) !important; } -.toastui-calendar-week-view-day-names, .toastui-calendar-time { - overflow: hidden !important; +.toastui-calendar-week-view-day-names, +.toastui-calendar-time { + overflow: hidden !important; } -.ic-arrow-line-left { - background: url('../icons/ic-arrow-line-left.png') no-repeat; +.btn { + border-radius: 25px; + border-color: #ddd; } -.ic-arrow-line-right { - background: url('../icons/ic-arrow-line-right.png') no-repeat; + +.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; } diff --git a/app/templates/formsemestre/edt.j2 b/app/templates/formsemestre/edt.j2 index d8c92eb48..267d05e81 100644 --- a/app/templates/formsemestre/edt.j2 +++ b/app/templates/formsemestre/edt.j2 @@ -18,10 +18,10 @@ @@ -41,7 +41,12 @@ let hm_formatter = new Intl.DateTimeFormat('default', { hour12: false }); +function getDataAction(target) { + return target.dataset ? target.dataset.action : target.getAttribute('data-action'); +} + document.addEventListener('DOMContentLoaded', function() { + document.getElementById('menu-navi').addEventListener('click', onClickNavi); const Calendar = tui.Calendar; const container = document.getElementById('calendar'); const options = { @@ -86,25 +91,7 @@ document.addEventListener('DOMContentLoaded', function() { }; const calendar = new Calendar(container, options); - //let events = [ - // { - // id: "12456", - // start:"2023-11-10T09:30", - // end:"2023-11-10T11:30", - // backgroundColor:"lightblue", - // color: "red", // couleur du texte - // location: "quelque part", - // title:'Essai saisir', - // }, - // { - // id: "12457", - // start:"2023-11-10T09:30", - // end:"2023-11-10T11:50", - // backgroundColor:"lightgreen", - // color: "blue", // couleur du texte - // title:'TD groupe 2', - // }, - //]; + fetch(`${SCO_URL}/../api/formsemestre/{{formsemestre.id}}/edt`) .then(r=>{return r.json()}) .then(events=>{ @@ -115,6 +102,52 @@ document.addEventListener('DOMContentLoaded', function() { calendar.createEvents(events); } }); + + function formatDate(date) { + let year = date.getFullYear(); + let month = (date.getMonth() + 1).toString().padStart(2, '0'); // Months are zero-indexed in JavaScript + let day = date.getDate().toString().padStart(2, '0'); + return `${day}/${month}/${year}`; + } + + function setRenderRangeText() { + var renderRange = document.getElementById('renderRange'); + var options = calendar.getOptions(); + var viewName = calendar.getViewName(); + + var html = []; + if (viewName === 'day') { + html.push(currentCalendarDate('YYYY.MM.DD')); + } else if (viewName === 'month' && + (!options.month.visibleWeeksCount || options.month.visibleWeeksCount > 4)) { + html.push(currentCalendarDate('YYYY.MM')); + } else { + html.push(formatDate(calendar.getDateRangeStart())); + html.push(' - '); + html.push(formatDate(calendar.getDateRangeEnd())); + } + renderRange.innerHTML = html.join(''); + } + function onClickNavi(e) { + var action = getDataAction(e.target); + + switch (action) { + case 'move-prev': + calendar.prev(); + break; + case 'move-next': + calendar.next(); + break; + case 'move-today': + calendar.today(); + break; + default: + return; + } + + setRenderRangeText(); + // setSchedules(); + } }); From 676ae47d69eb69c6590a0b58c1a470bd383040c3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 14 Nov 2023 23:24:29 +0100 Subject: [PATCH 29/69] Serve locally xlsx-populate (no CDN) --- app/static/libjs/xlsx-populate-1.21.0.min.js | 2 ++ app/templates/scolar/partition_editor.j2 | 2 +- app/templates/scolar/students_groups_auto_assignment.j2 | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 app/static/libjs/xlsx-populate-1.21.0.min.js diff --git a/app/static/libjs/xlsx-populate-1.21.0.min.js b/app/static/libjs/xlsx-populate-1.21.0.min.js new file mode 100644 index 000000000..8a7afb033 --- /dev/null +++ b/app/static/libjs/xlsx-populate-1.21.0.min.js @@ -0,0 +1,2 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).XlsxPopulate=e()}}(function(){var define,module,exports;return function a(o,s,f){function u(t,e){if(!s[t]){if(!o[t]){var r="function"==typeof require&&require;if(!e&&r)return r(t,!0);if(c)return c(t,!0);var n=new Error("Cannot find module '"+t+"'");throw n.code="MODULE_NOT_FOUND",n}var i=s[t]={exports:{}};o[t][0].call(i.exports,function(e){return u(o[t][1][e]||e)},i,i.exports,a,o,s,f)}return s[t].exports}for(var c="function"==typeof require&&require,e=0;es.length&&(l=s.length);var d=s.slice(h+u,l+u),p=d.length%i;p&&(d=g.concat([d,g.alloc(i-p)]));var b=this._createIV(n,a,i,c),m=this._crypt(e,t,r,o,b,d);f.push(m),c++}var v=g.concat(f);if(e)v=g.concat([this._createUInt32LEBuffer(s.length,8),v]);else{var y=s.readUInt32LE(0);v=v.slice(0,y)}return v}},{key:"_createUInt32LEBuffer",value:function(e,t){var r=1c&&(s=s.slice(0,c));return s}},{key:"_createIV",value:function(e,t,r,n){"number"==typeof n&&(n=this._createUInt32LEBuffer(n));var i=this._hash(e,t,n);if(i.lengthr&&(i=i.slice(0,r));return i}}])&&i(t.prototype,r),n&&i(t,n),e}();r.exports=e}).call(this,t("buffer").Buffer)},{"./XmlBuilder":21,"./XmlParser":22,"./externals":27,"./xmlq":29,buffer:77,cfb:78,crypto:86,lodash:170}],8:[function(e,t,r){"use strict";function i(e,t){for(var r=0;r=r._nextId&&(r._nextId=t+1)})}},{key:"_init",value:function(e){e=e||{name:"Relationships",attributes:{xmlns:"http://schemas.openxmlformats.org/package/2006/relationships"},children:[]},this._node=e}}])&&i(e.prototype,r),n&&i(e,n),t}();t.exports=n},{lodash:170}],12:[function(e,t,r){"use strict";function i(e,t){for(var r=0;rt){var o=u.cloneDeep(n);o.attributes.min=t+1;for(var s=o.attributes.min;s<=o.attributes.max;s++)this._colNodes[s]=o}}else r={name:"col",attributes:{min:t,max:t},children:[]},this._colNodes[t]=r;var f=new h(this,r);return this._columns[t]=f}},{key:"definedName",value:function(){var r=this;return new b("Workbook.definedName").case("string",function(e){return r.workbook().scopedDefinedName(r,e)}).case(["string","*"],function(e,t){return r.workbook().scopedDefinedName(r,e,t),r}).handle(arguments)}},{key:"delete",value:function(){return this.workbook().deleteSheet(this),this.workbook()}},{key:"find",value:function(t,r){t=f(t);var n=[];return this._rows.forEach(function(e){e&&(n=n.concat(e.find(t,r)))}),n}},{key:"gridLinesVisible",value:function(){var t=this,r=this._getOrCreateSheetViewNode();return new b("Sheet.gridLinesVisible").case(function(){return 1===r.attributes.showGridLines||void 0===r.attributes.showGridLines}).case("boolean",function(e){return r.attributes.showGridLines=e?1:0,t}).handle(arguments)}},{key:"hidden",value:function(){var r=this;return new b("Sheet.hidden").case(function(){return"hidden"===r._idNode.attributes.state||"veryHidden"===r._idNode.attributes.state&&"very"}).case("*",function(e){if(e){var t=u.filter(r.workbook().sheets(),function(e){return!e.hidden()});if(1===t.length&&t[0]===r)throw new Error("This sheet may not be hidden as a workbook must contain at least one visible sheet.");if(r.active())t[t[0]===r?1:0].active(!0)}return"very"===e?r._idNode.attributes.state="veryHidden":e?r._idNode.attributes.state="hidden":delete r._idNode.attributes.state,r}).handle(arguments)}},{key:"move",value:function(e){return this.workbook().moveSheet(this,e),this}},{key:"name",value:function(){var t=this;return new b("Sheet.name").case(function(){return"".concat(t._idNode.attributes.name)}).case("string",function(e){return t._idNode.attributes.name=e,t}).handle(arguments)}},{key:"range",value:function(){var i=this;return new b("Sheet.range").case("string",function(e){var t=p.fromAddress(e);if("range"!==t.type)throw new Error("Sheet.range: Invalid address");return i.range(t.startRowNumber,t.startColumnNumber,t.endRowNumber,t.endColumnNumber)}).case(["*","*"],function(e,t){return"string"==typeof e&&(e=i.cell(e)),"string"==typeof t&&(t=i.cell(t)),new o(e,t)}).case(["number","*","number","*"],function(e,t,r,n){return i.range(i.cell(e,t),i.cell(r,n))}).handle(arguments)}},{key:"autoFilter",value:function(e){return this._autoFilter=e,this}},{key:"row",value:function(e){if(e<1)throw new RangeError("Invalid row number ".concat(e,". Remember that spreadsheets use 1-based indexing."));if(this._rows[e])return this._rows[e];var t=new c(this,{name:"row",attributes:{r:e},children:[]});return this._rows[e]=t}},{key:"tabColor",value:function(){var r=this;return new b("Sheet.tabColor").case(function(){var e=d.findChild(r._sheetPrNode,"tabColor");if(e){var t={};return e.attributes.hasOwnProperty("rgb")?t.rgb=e.attributes.rgb:e.attributes.hasOwnProperty("theme")?t.theme=e.attributes.theme:e.attributes.hasOwnProperty("indexed")&&(t.rgb=m[e.attributes.indexed]),e.attributes.hasOwnProperty("tint")&&(t.tint=e.attributes.tint),t}}).case("string",function(e){return r.tabColor({rgb:e})}).case("integer",function(e){return r.tabColor({theme:e})}).case("nil",function(){return d.removeChild(r._sheetPrNode,"tabColor"),r}).case("object",function(e){var t=d.appendChildIfNotFound(r._sheetPrNode,"tabColor");return d.setAttributes(t,{rgb:e.rgb&&e.rgb.toUpperCase(),indexed:null,theme:e.theme,tint:e.tint}),r}).handle(arguments)}},{key:"tabSelected",value:function(){var t=this,r=this._getOrCreateSheetViewNode();return new b("Sheet.tabSelected").case(function(){return 1===r.attributes.tabSelected}).case("boolean",function(e){return e?r.attributes.tabSelected=1:delete r.attributes.tabSelected,t}).handle(arguments)}},{key:"rightToLeft",value:function(){var t=this,r=this._getOrCreateSheetViewNode();return new b("Sheet.rightToLeft").case(function(){return r.attributes.rightToLeft}).case("boolean",function(e){return e?r.attributes.rightToLeft=!0:delete r.attributes.rightToLeft,t}).handle(arguments)}},{key:"usedRange",value:function(){for(var e=u.findIndex(this._rows),t=this._rows.length-1,r=0,n=0,i=0;ithis._maxSharedFormulaId&&(this._maxSharedFormulaId=e)}},{key:"printOptions",value:function(){var r=this,n=this._getCheckAttributeNameHelper("printOptions",["gridLines","gridLinesSet","headings","horizontalCentered","verticalCentered"]);return new b("Sheet.printOptions").case(["string"],function(e){return n(e),1===r._printOptionsNode.attributes[e]}).case(["string","nil"],function(e){return n(e),delete r._printOptionsNode.attributes[e],r}).case(["string","boolean"],function(e,t){return n(e),t?(r._printOptionsNode.attributes[e]=1,r):r.printOptions(e,void 0)}).handle(arguments)}},{key:"printGridLines",value:function(){var t=this;return new b("Sheet.gridLines").case(function(){return t.printOptions("gridLines")&&t.printOptions("gridLinesSet")}).case(["nil"],function(){return t.printOptions("gridLines",void 0),t.printOptions("gridLinesSet",void 0),t}).case(["boolean"],function(e){return t.printOptions("gridLines",e),t.printOptions("gridLinesSet",e),t}).handle(arguments)}},{key:"pageMargins",value:function(){var r=this;if(void 0===this.pageMarginsPreset())throw new Error("Sheet.pageMargins: preset is undefined.");var n=this._getCheckAttributeNameHelper("pageMargins",["left","right","top","bottom","header","footer"]),i=this._getCheckRangeHelper("pageMargins",0,void 0);return new b("Sheet.pageMargins").case(["string"],function(e){n(e);var t=r._pageMarginsNode.attributes[e];return void 0!==t?parseFloat(t):r._pageMarginsPresetName?parseFloat(r._pageMarginsPresets[r._pageMarginsPresetName][e]):void 0}).case(["string","nil"],function(e){return n(e),delete r._pageMarginsNode.attributes[e],r}).case(["string","number"],function(e,t){return n(e),i(t),r._pageMarginsNode.attributes[e]=t,r}).case(["string","string"],function(e,t){return r.pageMargins(e,parseFloat(t))}).handle(arguments)}},{key:"pageMarginsPreset",value:function(){var r=this;return new b("Sheet.pageMarginsPreset").case(function(){return r._pageMarginsPresetName}).case(["nil"],function(){return r._pageMarginsPresetName=void 0,r._pageMarginsNode.attributes={},r}).case(["string"],function(e){return r._getCheckAttributeNameHelper("pageMarginsPreset",Object.keys(r._pageMarginsPresets))(e),r._pageMarginsPresetName=e,r._pageMarginsNode.attributes={},r}).case(["string","object"],function(e,t){if(r._pageMarginsPresets.hasOwnProperty(e))throw new Error("Sheet.pageMarginsPreset: The preset ".concat(e," already exists!"));if(!1===u.isEqual(u.sortBy(["left","right","top","bottom","header","footer"]),u.sortBy(Object.keys(t))))throw new Error('Sheet.pageMarginsPreset: Invalid preset attributes for one or key(s)! - "'.concat(Object.keys(t),'"'));return u.forEach(function(e,t){var r=parseFloat(e);if(u.isNaN(r)||!1===u.isNumber(r))throw new Error('Sheet.pageMarginsPreset: Invalid preset attribute value! - "'.concat(e,'"'))}),r._pageMarginsPresetName=e,r._pageMarginsNode.attributes={},r._pageMarginsPresets[e]=t,r}).handle(arguments)}},{key:"panes",value:function(){var r=this,n=this._getCheckAttributeNameHelper("pane.state",["split","frozen","frozenSplit"]),i=this._getCheckAttributeNameHelper("pane.activePane",["bottomLeft","bottomRight","topLeft","topRight"]),a=this._getOrCreateSheetViewNode(),o=d.findChild(a,"pane");return new b("Sheet.pane").case(function(){if(o){var e=u.cloneDeep(o.attributes);return e.state||(e.state="split"),e}}).case(["nil"],function(){return d.removeChild(a,"pane"),r}).case(["object"],function(e){var t=u.assign({activePane:"bottomRight"},e);return n(t.state),i(t.activePane),o?o.attributes=t:(o={name:"pane",attributes:t,children:[]},d.appendChild(a,o)),r}).handle(arguments)}},{key:"freezePanes",value:function(){var a=this;return new b("Sheet.feezePanes").case(["integer","integer"],function(e,t){var r=p.columnNumberToName(e+1)+(t+1),n=0===e?"bottomLeft":"bottomRight";return n=0===t?"topRight":n,a.panes({state:"frozen",topLeftCell:r,xSplit:e,ySplit:t,activePane:n})}).case(["string"],function(e){var t=p.fromAddress(e),r=t.columnNumber-1,n=t.rowNumber-1,i=0==r?"bottomLeft":"bottomRight";return i=0==n?"topRight":i,a.panes({state:"frozen",topLeftCell:e,xSplit:r,ySplit:n,activePane:i})}).handle(arguments)}},{key:"splitPanes",value:function(e,t){return this.panes({state:"split",xSplit:e,ySplit:t})}},{key:"resetPanes",value:function(){return this.panes(null)}},{key:"_getCheckAttributeNameHelper",value:function(t,r){return function(e){if(!u.includes(r,e))throw new Error("Sheet.".concat(t,': "').concat(e,'" is not supported.'))}}},{key:"_getCheckTypeHelper",value:function(t,r){return function(e){if(n(e)!==r)throw new TypeError("Sheet.".concat(t,": invalid type - value must be of type ").concat(r,"."))}}},{key:"_getCheckRangeHelper",value:function(t,r,n){var i=this._getCheckTypeHelper(t,"number");return function(e){if(i(e),void 0!==r&&e=n._nextNumFormatId&&(n._nextNumFormatId=t+1)})}},{key:"_init",value:function(e){this._node=e,this._numFmtsNode=c.findChild(this._node,"numFmts"),this._fontsNode=c.findChild(this._node,"fonts"),this._fillsNode=c.findChild(this._node,"fills"),this._bordersNode=c.findChild(this._node,"borders"),this._cellXfsNode=c.findChild(this._node,"cellXfs"),this._numFmtsNode||(this._numFmtsNode={name:"numFmts",attributes:{},children:[]},c.insertBefore(this._node,this._numFmtsNode,this._fontsNode)),delete this._numFmtsNode.attributes.count,delete this._fontsNode.attributes.count,delete this._fillsNode.attributes.count,delete this._bordersNode.attributes.count,delete this._cellXfsNode.attributes.count}}])&&i(e.prototype,r),n&&i(e,n),t}();t.exports=n},{"./Style":17,"./xmlq":29,lodash:170}],19:[function(M,T,e){(function(i,a){"use strict";function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function s(e,t){for(var r=0;r
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/auth/user_info_page.j2 b/app/templates/auth/user_info_page.j2 index ff008361e..8a6cb7f6e 100644 --- a/app/templates/auth/user_info_page.j2 +++ b/app/templates/auth/user_info_page.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends "base.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %} @@ -45,14 +45,14 @@ or current_user.has_permission(Permission.UsersAdmin, user.dept) ) %}
  • modifier le mot de passe ou l'adresse mail
  • {% endif %} {% if current_user.has_permission(Permission.UsersAdmin, dept) %}
  • modifier ce compte et ses rôles
  • @@ -62,7 +62,7 @@ or current_user.has_permission(Permission.UsersAdmin, user.dept) ) %}
  • {{"désactiver" if user.active else "activer"}} ce compte
  • @@ -105,4 +105,4 @@ {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/babase.j2 b/app/templates/babase.j2 new file mode 100644 index 000000000..96591cc29 --- /dev/null +++ b/app/templates/babase.j2 @@ -0,0 +1,34 @@ +{% block doc -%} + + +{%- block html %} + + {%- block head %} + {% block title %}{{title|default}}{% endblock title %} + + {%- block metas %} + + {%- endblock metas %} + + {%- block styles %} + + + {%- endblock styles %} + {%- endblock head %} + + + {% block body -%} + {% block navbar %} + {%- endblock navbar %} + {% block content -%} + {%- endblock content %} + + {% block scripts %} + + + {%- endblock scripts %} + {%- endblock body %} + +{%- endblock html %} + +{% endblock doc -%} diff --git a/app/templates/base.j2 b/app/templates/base.j2 index c0cfbe821..ab7207539 100644 --- a/app/templates/base.j2 +++ b/app/templates/base.j2 @@ -1,5 +1,5 @@ {# -*- mode: jinja-html -*- #} -{% extends 'bootstrap/base.html' %} +{% extends 'babase.j2' %} {% block styles %} {{super()}} @@ -32,8 +32,8 @@
  • Configuration
  • {% endif %} {% if g.scodoc_dept %} -
  • Dept. {{ g.scodoc_dept }}
  • {% endif %} {% if not current_user.is_anonymous and @@ -92,6 +92,6 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/but/formsemestre_validation_auto_but.j2 b/app/templates/but/formsemestre_validation_auto_but.j2 index 0f8e827b6..72570ae53 100644 --- a/app/templates/but/formsemestre_validation_auto_but.j2 +++ b/app/templates/but/formsemestre_validation_auto_but.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends "sco_page.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} @@ -53,4 +53,4 @@
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/but/refcomp_assoc.j2 b/app/templates/but/refcomp_assoc.j2 index 527a93bb8..8eb4c075d 100644 --- a/app/templates/but/refcomp_assoc.j2 +++ b/app/templates/but/refcomp_assoc.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends "base.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %}

    Associer un référentiel de compétences

    @@ -15,7 +15,7 @@ Référentiel actuellement associé: {% if formation.referentiel_competence is not none %} {{ formation.referentiel_competence.specialite_long }} - supprimer {% else %} @@ -35,4 +35,4 @@ }}">passer par cette page. -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/but/refcomp_load.j2 b/app/templates/but/refcomp_load.j2 index 18cf30ae0..b6c88cefa 100644 --- a/app/templates/but/refcomp_load.j2 +++ b/app/templates/but/refcomp_load.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends "base.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %}

    Charger un référentiel de compétences

    @@ -28,4 +28,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/but/refcomp_table.j2 b/app/templates/but/refcomp_table.j2 index b1b1a5ca5..0c7585dc0 100644 --- a/app/templates/but/refcomp_table.j2 +++ b/app/templates/but/refcomp_table.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends "sco_page.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %}

    Référentiels de compétences chargés

    @@ -32,4 +32,4 @@ ScoDoc importe le format XML généré par Orébut, et peut exporter une représentation JSON.

    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/config_cas.j2 b/app/templates/config_cas.j2 index 05fb21586..b1d532f88 100644 --- a/app/templates/config_cas.j2 +++ b/app/templates/config_cas.j2 @@ -1,5 +1,5 @@ {% extends "base.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %}

    Configuration du Service d'Authentification Central (CAS)

    diff --git a/app/templates/config_codes_decisions.j2 b/app/templates/config_codes_decisions.j2 index 8bdd338d3..352f98354 100644 --- a/app/templates/config_codes_decisions.j2 +++ b/app/templates/config_codes_decisions.j2 @@ -1,5 +1,5 @@ {% extends "base.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %}

    Configuration des codes de décision exportés vers Apogée

    @@ -20,4 +20,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/config_logos.j2 b/app/templates/config_logos.j2 index 918c1f9ab..7d0fdd016 100644 --- a/app/templates/config_logos.j2 +++ b/app/templates/config_logos.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% macro render_field(field, with_label=True) %}
    @@ -130,4 +130,4 @@ {% endfor %}
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/config_personalized_links.j2 b/app/templates/config_personalized_links.j2 index ff3a7f61e..cea3e6122 100644 --- a/app/templates/config_personalized_links.j2 +++ b/app/templates/config_personalized_links.j2 @@ -1,5 +1,5 @@ {% extends "base.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} @@ -81,4 +81,4 @@ div.validation-buttons { -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/configuration.j2 b/app/templates/configuration.j2 index 7c06908ac..fe7743035 100644 --- a/app/templates/configuration.j2 +++ b/app/templates/configuration.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% macro render_field(field, with_label=True) %}
    diff --git a/app/templates/confirm_dialog.j2 b/app/templates/confirm_dialog.j2 index 4755ed104..d99425200 100644 --- a/app/templates/confirm_dialog.j2 +++ b/app/templates/confirm_dialog.j2 @@ -1,5 +1,5 @@ {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %} @@ -18,4 +18,4 @@
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/create_dept.j2 b/app/templates/create_dept.j2 index 75887a4c2..858acc86e 100644 --- a/app/templates/create_dept.j2 +++ b/app/templates/create_dept.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends "base.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %}

    Créer un département

    @@ -12,4 +12,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/entreprises/form.j2 b/app/templates/entreprises/form.j2 index e53117782..d5a8c37e4 100644 --- a/app/templates/entreprises/form.j2 +++ b/app/templates/entreprises/form.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} @@ -59,4 +59,4 @@ var as_utilisateurs = new bsn.AutoSuggest('utilisateur', responsables_options); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/entreprises/form_ajout_correspondants.j2 b/app/templates/entreprises/form_ajout_correspondants.j2 index ada71b155..9bc6cff62 100644 --- a/app/templates/entreprises/form_ajout_correspondants.j2 +++ b/app/templates/entreprises/form_ajout_correspondants.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} @@ -88,4 +88,4 @@ } } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/entreprises/form_ajout_entreprise.j2 b/app/templates/entreprises/form_ajout_entreprise.j2 index bdd32aef2..89f362015 100644 --- a/app/templates/entreprises/form_ajout_entreprise.j2 +++ b/app/templates/entreprises/form_ajout_entreprise.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %}

    Ajout entreprise

    @@ -55,4 +55,4 @@ document.getElementById("ville").value = '' } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/entreprises/form_ajout_stage_apprentissage.j2 b/app/templates/entreprises/form_ajout_stage_apprentissage.j2 index 071e4d1fc..a8881d4b3 100644 --- a/app/templates/entreprises/form_ajout_stage_apprentissage.j2 +++ b/app/templates/entreprises/form_ajout_stage_apprentissage.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} diff --git a/app/templates/entreprises/form_envoi_offre.j2 b/app/templates/entreprises/form_envoi_offre.j2 index ab1be632f..1490d88af 100644 --- a/app/templates/entreprises/form_envoi_offre.j2 +++ b/app/templates/entreprises/form_envoi_offre.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} @@ -72,7 +72,7 @@
    Retirer
    - + `); var as_r = new bsn.AutoSuggest(newFieldName, responsables_options); }); @@ -85,4 +85,4 @@ } } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/entreprises/form_modification_entreprise.j2 b/app/templates/entreprises/form_modification_entreprise.j2 index 8f325ff86..3c6907777 100644 --- a/app/templates/entreprises/form_modification_entreprise.j2 +++ b/app/templates/entreprises/form_modification_entreprise.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} @@ -65,4 +65,4 @@ document.getElementById("ville").value = response.etablissement.libelle_commune } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/entreprises/form_validate_confirmation.j2 b/app/templates/entreprises/form_validate_confirmation.j2 index d41f19d66..9fa4272cb 100644 --- a/app/templates/entreprises/form_validate_confirmation.j2 +++ b/app/templates/entreprises/form_validate_confirmation.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %}

    Validation entreprise

    @@ -12,4 +12,4 @@ {{ wtf.quick_form(form) }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/entreprises/import_donnees.j2 b/app/templates/entreprises/import_donnees.j2 index be3a7dfab..3344a6297 100644 --- a/app/templates/entreprises/import_donnees.j2 +++ b/app/templates/entreprises/import_donnees.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} @@ -195,7 +195,7 @@ Code postal : {{ entreprise.codepostal }}
    Ville : {{ entreprise.ville }}
    Pays : {{ entreprise.pays }}
    - Fiche entreprise @@ -222,7 +222,7 @@ Code postal : {{ site.codepostal }}
    Ville : {{ site.ville }}
    Pays : {{ site.pays }}
    - Fiche entreprise @@ -264,4 +264,4 @@ {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/entreprises/preferences.j2 b/app/templates/entreprises/preferences.j2 index 8fb5a8612..2658f1ca7 100644 --- a/app/templates/entreprises/preferences.j2 +++ b/app/templates/entreprises/preferences.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %} {% include 'entreprises/nav.j2' %} @@ -14,4 +14,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/error_500.j2 b/app/templates/error_500.j2 index 790aff3e1..9a02d0efb 100644 --- a/app/templates/error_500.j2 +++ b/app/templates/error_500.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block title %}Une erreur est survenue !{% endblock %} @@ -12,14 +12,14 @@

    {{date}}

    Si le problème persiste, contacter l'administrateur de votre site, - ou l'assistance sur le canal Discord.

    {% if 'scodoc_dept' in g %}

    Pour aider à corriger le problème, nous vous remercions d'envoyer ce rapport d'erreur (qui contient des données anonymisées sur votre configuration): -

    @@ -38,4 +38,4 @@ retour à la page d'accueil

    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/error_access_denied.j2 b/app/templates/error_access_denied.j2 index 92cf9ec8a..c14a2c8a1 100644 --- a/app/templates/error_access_denied.j2 +++ b/app/templates/error_access_denied.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %} @@ -17,4 +17,4 @@ {% endif %}

    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/error_csrf.j2 b/app/templates/error_csrf.j2 index 2633c86ed..83e5f736a 100644 --- a/app/templates/error_csrf.j2 +++ b/app/templates/error_csrf.j2 @@ -1,5 +1,5 @@ {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %} @@ -15,4 +15,4 @@
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/form_confirmation.j2 b/app/templates/form_confirmation.j2 index 47b6d315c..5b6e31b3b 100644 --- a/app/templates/form_confirmation.j2 +++ b/app/templates/form_confirmation.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %}

    {{ title }}

    @@ -12,4 +12,4 @@ {{ wtf.quick_form(form) }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/formation/ue_assoc_parcours_ects.j2 b/app/templates/formation/ue_assoc_parcours_ects.j2 index d7ea10ba9..290d0d169 100644 --- a/app/templates/formation/ue_assoc_parcours_ects.j2 +++ b/app/templates/formation/ue_assoc_parcours_ects.j2 @@ -1,6 +1,6 @@ {# Association d'ECTS à une UE par parcours #} {% extends "sco_page.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} diff --git a/app/templates/formsemestre/change_formation.j2 b/app/templates/formsemestre/change_formation.j2 index 8c0b8b5b3..b0e8c9e02 100644 --- a/app/templates/formsemestre/change_formation.j2 +++ b/app/templates/formsemestre/change_formation.j2 @@ -1,5 +1,5 @@ {% extends "sco_page.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} diff --git a/app/templates/formsemestre/edit_modimpls_codes.j2 b/app/templates/formsemestre/edit_modimpls_codes.j2 index d70b6e280..a663207d3 100644 --- a/app/templates/formsemestre/edit_modimpls_codes.j2 +++ b/app/templates/formsemestre/edit_modimpls_codes.j2 @@ -1,5 +1,5 @@ {% extends "sco_page.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} diff --git a/app/templates/formsemestre/edt.j2 b/app/templates/formsemestre/edt.j2 index 267d05e81..0c4edce75 100644 --- a/app/templates/formsemestre/edt.j2 +++ b/app/templates/formsemestre/edt.j2 @@ -1,5 +1,5 @@ {% extends "sco_page.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} diff --git a/app/templates/main/index.j2 b/app/templates/main/index.j2 index 9298d86b8..ca2408bbf 100644 --- a/app/templates/main/index.j2 +++ b/app/templates/main/index.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %} @@ -54,4 +54,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/sco_page.j2 b/app/templates/sco_page.j2 index 7cbfd96b0..6cc4a863a 100644 --- a/app/templates/sco_page.j2 +++ b/app/templates/sco_page.j2 @@ -1,5 +1,5 @@ {# -*- mode: jinja-html -*- #} -{% extends 'bootstrap/base.html' %} +{% extends 'babase.j2' %} {% block styles %} {{super()}} @@ -52,7 +52,7 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/scodoc.j2 b/app/templates/scodoc.j2 index e62102ffd..235835d23 100644 --- a/app/templates/scodoc.j2 +++ b/app/templates/scodoc.j2 @@ -1,6 +1,6 @@ {# -*- mode: jinja-html -*- #} {% extends 'base.j2' %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %}

    ScoDoc 9 - suivi scolarité

    @@ -53,7 +53,7 @@ {% endif %} - @@ -63,4 +63,4 @@ le logiciel libre ScoDoc. -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/scodoc/forms/placement.j2 b/app/templates/scodoc/forms/placement.j2 index 7631b743c..7cb5b482e 100644 --- a/app/templates/scodoc/forms/placement.j2 +++ b/app/templates/scodoc/forms/placement.j2 @@ -1,4 +1,4 @@ -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% macro render_field(field) %} @@ -77,4 +77,4 @@ - \ No newline at end of file + diff --git a/app/templates/scodoc/role_create.j2 b/app/templates/scodoc/role_create.j2 index 9c21b5d68..4f2627e7d 100644 --- a/app/templates/scodoc/role_create.j2 +++ b/app/templates/scodoc/role_create.j2 @@ -1,12 +1,12 @@ {# -*- mode: jinja-html -*- #} {% extends "base.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block app_content %}

    Créer un rôle

    -Un rôle est associé à un ensemble de permissions. +Un rôle est associé à un ensemble de permissions. Les utilisateurs peuvent avoir un ou plusieurs rôles dans chaque département.
    @@ -19,4 +19,4 @@ Les utilisateurs peuvent avoir un ou plusieurs rôles dans chaque département. -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/scodoc/role_editor.j2 b/app/templates/scodoc/role_editor.j2 index 9656291ca..9439887b8 100644 --- a/app/templates/scodoc/role_editor.j2 +++ b/app/templates/scodoc/role_editor.j2 @@ -1,7 +1,7 @@ {# -*- mode: jinja-html -*- #} {# -*- Edition des rôles/permissions -*- #} {% extends "base.j2" %} -{% import 'bootstrap/wtf.html' as wtf %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} @@ -14,7 +14,7 @@
    Les rôles sont associés à un ensemble de permissions. Chaque utilisateur peut avoir un nombre quelconque de rôles dans chaque -département. +département. Sur cette page vous pouvez modifier les permissions associée à chaque rôle, ou créer de nouveaux rôles. Les rôles en gras sont les rôles standards de ScoDoc. @@ -38,7 +38,7 @@ Les rôles en gras sont les rôles standards de ScoDoc.
    {% for permission_name in permissions_names %}
    -
    +
    `.\n@font-family-monospace:   Menlo, Monaco, Consolas, \"Courier New\", monospace;\n@font-family-base:        @font-family-sans-serif;\n\n@font-size-base:          14px;\n@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px\n@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px\n\n@font-size-h1:            floor((@font-size-base * 2.6)); // ~36px\n@font-size-h2:            floor((@font-size-base * 2.15)); // ~30px\n@font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px\n@font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px\n@font-size-h5:            @font-size-base;\n@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px\n\n//** Unit-less `line-height` for use in components like buttons.\n@line-height-base:        1.428571429; // 20/14\n//** Computed \"line-height\" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.\n@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px\n\n//** By default, this inherits from the ``.\n@headings-font-family:    inherit;\n@headings-font-weight:    500;\n@headings-line-height:    1.1;\n@headings-color:          inherit;\n\n\n//-- Iconography\n//\n//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower.\n\n@icon-font-path:          \"../fonts/\";\n@icon-font-name:          \"glyphicons-halflings-regular\";\n@icon-font-svg-id:        \"glyphicons_halflingsregular\";\n\n//== Components\n//\n//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).\n\n@padding-base-vertical:     6px;\n@padding-base-horizontal:   12px;\n\n@padding-large-vertical:    10px;\n@padding-large-horizontal:  16px;\n\n@padding-small-vertical:    5px;\n@padding-small-horizontal:  10px;\n\n@padding-xs-vertical:       1px;\n@padding-xs-horizontal:     5px;\n\n@line-height-large:         1.33;\n@line-height-small:         1.5;\n\n@border-radius-base:        4px;\n@border-radius-large:       6px;\n@border-radius-small:       3px;\n\n//** Global color for active items (e.g., navs or dropdowns).\n@component-active-color:    #fff;\n//** Global background color for active items (e.g., navs or dropdowns).\n@component-active-bg:       @brand-primary;\n\n//** Width of the `border` for generating carets that indicator dropdowns.\n@caret-width-base:          4px;\n//** Carets increase slightly in size for larger components.\n@caret-width-large:         5px;\n\n\n//== Tables\n//\n//## Customizes the `.table` component with basic values, each used across all table variations.\n\n//** Padding for ``s and ``s.\n@table-cell-padding:            8px;\n//** Padding for cells in `.table-condensed`.\n@table-condensed-cell-padding:  5px;\n\n//** Default background color used for all tables.\n@table-bg:                      transparent;\n//** Background color used for `.table-striped`.\n@table-bg-accent:               #f9f9f9;\n//** Background color used for `.table-hover`.\n@table-bg-hover:                #f5f5f5;\n@table-bg-active:               @table-bg-hover;\n\n//** Border color for table and cell borders.\n@table-border-color:            #ddd;\n\n\n//== Buttons\n//\n//## For each of Bootstrap's buttons, define text, background and border color.\n\n@btn-font-weight:                normal;\n\n@btn-default-color:              #333;\n@btn-default-bg:                 #fff;\n@btn-default-border:             #ccc;\n\n@btn-primary-color:              #fff;\n@btn-primary-bg:                 @brand-primary;\n@btn-primary-border:             darken(@btn-primary-bg, 5%);\n\n@btn-success-color:              #fff;\n@btn-success-bg:                 @brand-success;\n@btn-success-border:             darken(@btn-success-bg, 5%);\n\n@btn-info-color:                 #fff;\n@btn-info-bg:                    @brand-info;\n@btn-info-border:                darken(@btn-info-bg, 5%);\n\n@btn-warning-color:              #fff;\n@btn-warning-bg:                 @brand-warning;\n@btn-warning-border:             darken(@btn-warning-bg, 5%);\n\n@btn-danger-color:               #fff;\n@btn-danger-bg:                  @brand-danger;\n@btn-danger-border:              darken(@btn-danger-bg, 5%);\n\n@btn-link-disabled-color:        @gray-light;\n\n\n//== Forms\n//\n//##\n\n//** `` background color\n@input-bg:                       #fff;\n//** `` background color\n@input-bg-disabled:              @gray-lighter;\n\n//** Text color for ``s\n@input-color:                    @gray;\n//** `` border color\n@input-border:                   #ccc;\n//** `` border radius\n@input-border-radius:            @border-radius-base;\n//** Border color for inputs on focus\n@input-border-focus:             #66afe9;\n\n//** Placeholder text color\n@input-color-placeholder:        @gray-light;\n\n//** Default `.form-control` height\n@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);\n//** Large `.form-control` height\n@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);\n//** Small `.form-control` height\n@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);\n\n@legend-color:                   @gray-dark;\n@legend-border-color:            #e5e5e5;\n\n//** Background color for textual input addons\n@input-group-addon-bg:           @gray-lighter;\n//** Border color for textual input addons\n@input-group-addon-border-color: @input-border;\n\n\n//== Dropdowns\n//\n//## Dropdown menu container and contents.\n\n//** Background for the dropdown menu.\n@dropdown-bg:                    #fff;\n//** Dropdown menu `border-color`.\n@dropdown-border:                rgba(0,0,0,.15);\n//** Dropdown menu `border-color` **for IE8**.\n@dropdown-fallback-border:       #ccc;\n//** Divider color for between dropdown items.\n@dropdown-divider-bg:            #e5e5e5;\n\n//** Dropdown link text color.\n@dropdown-link-color:            @gray-dark;\n//** Hover color for dropdown links.\n@dropdown-link-hover-color:      darken(@gray-dark, 5%);\n//** Hover background for dropdown links.\n@dropdown-link-hover-bg:         #f5f5f5;\n\n//** Active dropdown menu item text color.\n@dropdown-link-active-color:     @component-active-color;\n//** Active dropdown menu item background color.\n@dropdown-link-active-bg:        @component-active-bg;\n\n//** Disabled dropdown menu item background color.\n@dropdown-link-disabled-color:   @gray-light;\n\n//** Text color for headers within dropdown menus.\n@dropdown-header-color:          @gray-light;\n\n// Note: Deprecated @dropdown-caret-color as of v3.1.0\n@dropdown-caret-color:           #000;\n\n\n//-- Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n//\n// Note: These variables are not generated into the Customizer.\n\n@zindex-navbar:            1000;\n@zindex-dropdown:          1000;\n@zindex-popover:           1010;\n@zindex-tooltip:           1030;\n@zindex-navbar-fixed:      1030;\n@zindex-modal-background:  1040;\n@zindex-modal:             1050;\n\n\n//== Media queries breakpoints\n//\n//## Define the breakpoints at which your layout will change, adapting to different screen sizes.\n\n// Extra small screen / phone\n// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1\n@screen-xs:                  480px;\n@screen-xs-min:              @screen-xs;\n@screen-phone:               @screen-xs-min;\n\n// Small screen / tablet\n// Note: Deprecated @screen-sm and @screen-tablet as of v3.0.1\n@screen-sm:                  768px;\n@screen-sm-min:              @screen-sm;\n@screen-tablet:              @screen-sm-min;\n\n// Medium screen / desktop\n// Note: Deprecated @screen-md and @screen-desktop as of v3.0.1\n@screen-md:                  992px;\n@screen-md-min:              @screen-md;\n@screen-desktop:             @screen-md-min;\n\n// Large screen / wide desktop\n// Note: Deprecated @screen-lg and @screen-lg-desktop as of v3.0.1\n@screen-lg:                  1200px;\n@screen-lg-min:              @screen-lg;\n@screen-lg-desktop:          @screen-lg-min;\n\n// So media queries don't overlap when required, provide a maximum\n@screen-xs-max:              (@screen-sm-min - 1);\n@screen-sm-max:              (@screen-md-min - 1);\n@screen-md-max:              (@screen-lg-min - 1);\n\n\n//== Grid system\n//\n//## Define your custom responsive grid.\n\n//** Number of columns in the grid.\n@grid-columns:              12;\n//** Padding between columns. Gets divided in half for the left and right.\n@grid-gutter-width:         30px;\n// Navbar collapse\n//** Point at which the navbar becomes uncollapsed.\n@grid-float-breakpoint:     @screen-sm-min;\n//** Point at which the navbar begins collapsing.\n@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);\n\n\n//== Container sizes\n//\n//## Define the maximum width of `.container` for different screen sizes.\n\n// Small screen / tablet\n@container-tablet:             ((720px + @grid-gutter-width));\n//** For `@screen-sm-min` and up.\n@container-sm:                 @container-tablet;\n\n// Medium screen / desktop\n@container-desktop:            ((940px + @grid-gutter-width));\n//** For `@screen-md-min` and up.\n@container-md:                 @container-desktop;\n\n// Large screen / wide desktop\n@container-large-desktop:      ((1140px + @grid-gutter-width));\n//** For `@screen-lg-min` and up.\n@container-lg:                 @container-large-desktop;\n\n\n//== Navbar\n//\n//##\n\n// Basics of a navbar\n@navbar-height:                    50px;\n@navbar-margin-bottom:             @line-height-computed;\n@navbar-border-radius:             @border-radius-base;\n@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));\n@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);\n@navbar-collapse-max-height:       340px;\n\n@navbar-default-color:             #777;\n@navbar-default-bg:                #f8f8f8;\n@navbar-default-border:            darken(@navbar-default-bg, 6.5%);\n\n// Navbar links\n@navbar-default-link-color:                #777;\n@navbar-default-link-hover-color:          #333;\n@navbar-default-link-hover-bg:             transparent;\n@navbar-default-link-active-color:         #555;\n@navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);\n@navbar-default-link-disabled-color:       #ccc;\n@navbar-default-link-disabled-bg:          transparent;\n\n// Navbar brand label\n@navbar-default-brand-color:               @navbar-default-link-color;\n@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);\n@navbar-default-brand-hover-bg:            transparent;\n\n// Navbar toggle\n@navbar-default-toggle-hover-bg:           #ddd;\n@navbar-default-toggle-icon-bar-bg:        #888;\n@navbar-default-toggle-border-color:       #ddd;\n\n\n// Inverted navbar\n// Reset inverted navbar basics\n@navbar-inverse-color:                      @gray-light;\n@navbar-inverse-bg:                         #222;\n@navbar-inverse-border:                     darken(@navbar-inverse-bg, 10%);\n\n// Inverted navbar links\n@navbar-inverse-link-color:                 @gray-light;\n@navbar-inverse-link-hover-color:           #fff;\n@navbar-inverse-link-hover-bg:              transparent;\n@navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;\n@navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 10%);\n@navbar-inverse-link-disabled-color:        #444;\n@navbar-inverse-link-disabled-bg:           transparent;\n\n// Inverted navbar brand label\n@navbar-inverse-brand-color:                @navbar-inverse-link-color;\n@navbar-inverse-brand-hover-color:          #fff;\n@navbar-inverse-brand-hover-bg:             transparent;\n\n// Inverted navbar toggle\n@navbar-inverse-toggle-hover-bg:            #333;\n@navbar-inverse-toggle-icon-bar-bg:         #fff;\n@navbar-inverse-toggle-border-color:        #333;\n\n\n//== Navs\n//\n//##\n\n//=== Shared nav styles\n@nav-link-padding:                          10px 15px;\n@nav-link-hover-bg:                         @gray-lighter;\n\n@nav-disabled-link-color:                   @gray-light;\n@nav-disabled-link-hover-color:             @gray-light;\n\n@nav-open-link-hover-color:                 #fff;\n\n//== Tabs\n@nav-tabs-border-color:                     #ddd;\n\n@nav-tabs-link-hover-border-color:          @gray-lighter;\n\n@nav-tabs-active-link-hover-bg:             @body-bg;\n@nav-tabs-active-link-hover-color:          @gray;\n@nav-tabs-active-link-hover-border-color:   #ddd;\n\n@nav-tabs-justified-link-border-color:            #ddd;\n@nav-tabs-justified-active-link-border-color:     @body-bg;\n\n//== Pills\n@nav-pills-border-radius:                   @border-radius-base;\n@nav-pills-active-link-hover-bg:            @component-active-bg;\n@nav-pills-active-link-hover-color:         @component-active-color;\n\n\n//== Pagination\n//\n//##\n\n@pagination-color:                     @link-color;\n@pagination-bg:                        #fff;\n@pagination-border:                    #ddd;\n\n@pagination-hover-color:               @link-hover-color;\n@pagination-hover-bg:                  @gray-lighter;\n@pagination-hover-border:              #ddd;\n\n@pagination-active-color:              #fff;\n@pagination-active-bg:                 @brand-primary;\n@pagination-active-border:             @brand-primary;\n\n@pagination-disabled-color:            @gray-light;\n@pagination-disabled-bg:               #fff;\n@pagination-disabled-border:           #ddd;\n\n\n//== Pager\n//\n//##\n\n@pager-bg:                             @pagination-bg;\n@pager-border:                         @pagination-border;\n@pager-border-radius:                  15px;\n\n@pager-hover-bg:                       @pagination-hover-bg;\n\n@pager-active-bg:                      @pagination-active-bg;\n@pager-active-color:                   @pagination-active-color;\n\n@pager-disabled-color:                 @pagination-disabled-color;\n\n\n//== Jumbotron\n//\n//##\n\n@jumbotron-padding:              30px;\n@jumbotron-color:                inherit;\n@jumbotron-bg:                   @gray-lighter;\n@jumbotron-heading-color:        inherit;\n@jumbotron-font-size:            ceil((@font-size-base * 1.5));\n\n\n//== Form states and alerts\n//\n//## Define colors for form feedback states and, by default, alerts.\n\n@state-success-text:             #3c763d;\n@state-success-bg:               #dff0d8;\n@state-success-border:           darken(spin(@state-success-bg, -10), 5%);\n\n@state-info-text:                #31708f;\n@state-info-bg:                  #d9edf7;\n@state-info-border:              darken(spin(@state-info-bg, -10), 7%);\n\n@state-warning-text:             #8a6d3b;\n@state-warning-bg:               #fcf8e3;\n@state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);\n\n@state-danger-text:              #a94442;\n@state-danger-bg:                #f2dede;\n@state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);\n\n\n//== Tooltips\n//\n//##\n\n//** Tooltip max width\n@tooltip-max-width:           200px;\n//** Tooltip text color\n@tooltip-color:               #fff;\n//** Tooltip background color\n@tooltip-bg:                  #000;\n@tooltip-opacity:             .9;\n\n//** Tooltip arrow width\n@tooltip-arrow-width:         5px;\n//** Tooltip arrow color\n@tooltip-arrow-color:         @tooltip-bg;\n\n\n//== Popovers\n//\n//##\n\n//** Popover body background color\n@popover-bg:                          #fff;\n//** Popover maximum width\n@popover-max-width:                   276px;\n//** Popover border color\n@popover-border-color:                rgba(0,0,0,.2);\n//** Popover fallback border color\n@popover-fallback-border-color:       #ccc;\n\n//** Popover title background color\n@popover-title-bg:                    darken(@popover-bg, 3%);\n\n//** Popover arrow width\n@popover-arrow-width:                 10px;\n//** Popover arrow color\n@popover-arrow-color:                 #fff;\n\n//** Popover outer arrow width\n@popover-arrow-outer-width:           (@popover-arrow-width + 1);\n//** Popover outer arrow color\n@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);\n//** Popover outer arrow fallback color\n@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);\n\n\n//== Labels\n//\n//##\n\n//** Default label background color\n@label-default-bg:            @gray-light;\n//** Primary label background color\n@label-primary-bg:            @brand-primary;\n//** Success label background color\n@label-success-bg:            @brand-success;\n//** Info label background color\n@label-info-bg:               @brand-info;\n//** Warning label background color\n@label-warning-bg:            @brand-warning;\n//** Danger label background color\n@label-danger-bg:             @brand-danger;\n\n//** Default label text color\n@label-color:                 #fff;\n//** Default text color of a linked label\n@label-link-hover-color:      #fff;\n\n\n//== Modals\n//\n//##\n\n//** Padding applied to the modal body\n@modal-inner-padding:         20px;\n\n//** Padding applied to the modal title\n@modal-title-padding:         15px;\n//** Modal title line-height\n@modal-title-line-height:     @line-height-base;\n\n//** Background color of modal content area\n@modal-content-bg:                             #fff;\n//** Modal content border color\n@modal-content-border-color:                   rgba(0,0,0,.2);\n//** Modal content border color **for IE8**\n@modal-content-fallback-border-color:          #999;\n\n//** Modal backdrop background color\n@modal-backdrop-bg:           #000;\n//** Modal backdrop opacity\n@modal-backdrop-opacity:      .5;\n//** Modal header border color\n@modal-header-border-color:   #e5e5e5;\n//** Modal footer border color\n@modal-footer-border-color:   @modal-header-border-color;\n\n@modal-lg:                    900px;\n@modal-md:                    600px;\n@modal-sm:                    300px;\n\n\n//== Alerts\n//\n//## Define alert colors, border radius, and padding.\n\n@alert-padding:               15px;\n@alert-border-radius:         @border-radius-base;\n@alert-link-font-weight:      bold;\n\n@alert-success-bg:            @state-success-bg;\n@alert-success-text:          @state-success-text;\n@alert-success-border:        @state-success-border;\n\n@alert-info-bg:               @state-info-bg;\n@alert-info-text:             @state-info-text;\n@alert-info-border:           @state-info-border;\n\n@alert-warning-bg:            @state-warning-bg;\n@alert-warning-text:          @state-warning-text;\n@alert-warning-border:        @state-warning-border;\n\n@alert-danger-bg:             @state-danger-bg;\n@alert-danger-text:           @state-danger-text;\n@alert-danger-border:         @state-danger-border;\n\n\n//== Progress bars\n//\n//##\n\n//** Background color of the whole progress component\n@progress-bg:                 #f5f5f5;\n//** Progress bar text color\n@progress-bar-color:          #fff;\n\n//** Default progress bar color\n@progress-bar-bg:             @brand-primary;\n//** Success progress bar color\n@progress-bar-success-bg:     @brand-success;\n//** Warning progress bar color\n@progress-bar-warning-bg:     @brand-warning;\n//** Danger progress bar color\n@progress-bar-danger-bg:      @brand-danger;\n//** Info progress bar color\n@progress-bar-info-bg:        @brand-info;\n\n\n//== List group\n//\n//##\n\n//** Background color on `.list-group-item`\n@list-group-bg:                 #fff;\n//** `.list-group-item` border color\n@list-group-border:             #ddd;\n//** List group border radius\n@list-group-border-radius:      @border-radius-base;\n\n//** Background color of single list elements on hover\n@list-group-hover-bg:           #f5f5f5;\n//** Text color of active list elements\n@list-group-active-color:       @component-active-color;\n//** Background color of active list elements\n@list-group-active-bg:          @component-active-bg;\n//** Border color of active list elements\n@list-group-active-border:      @list-group-active-bg;\n@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);\n\n@list-group-link-color:         #555;\n@list-group-link-heading-color: #333;\n\n\n//== Panels\n//\n//##\n\n@panel-bg:                    #fff;\n@panel-body-padding:          15px;\n@panel-border-radius:         @border-radius-base;\n\n//** Border color for elements within panels\n@panel-inner-border:          #ddd;\n@panel-footer-bg:             #f5f5f5;\n\n@panel-default-text:          @gray-dark;\n@panel-default-border:        #ddd;\n@panel-default-heading-bg:    #f5f5f5;\n\n@panel-primary-text:          #fff;\n@panel-primary-border:        @brand-primary;\n@panel-primary-heading-bg:    @brand-primary;\n\n@panel-success-text:          @state-success-text;\n@panel-success-border:        @state-success-border;\n@panel-success-heading-bg:    @state-success-bg;\n\n@panel-info-text:             @state-info-text;\n@panel-info-border:           @state-info-border;\n@panel-info-heading-bg:       @state-info-bg;\n\n@panel-warning-text:          @state-warning-text;\n@panel-warning-border:        @state-warning-border;\n@panel-warning-heading-bg:    @state-warning-bg;\n\n@panel-danger-text:           @state-danger-text;\n@panel-danger-border:         @state-danger-border;\n@panel-danger-heading-bg:     @state-danger-bg;\n\n\n//== Thumbnails\n//\n//##\n\n//** Padding around the thumbnail image\n@thumbnail-padding:           4px;\n//** Thumbnail background color\n@thumbnail-bg:                @body-bg;\n//** Thumbnail border color\n@thumbnail-border:            #ddd;\n//** Thumbnail border radius\n@thumbnail-border-radius:     @border-radius-base;\n\n//** Custom text color for thumbnail captions\n@thumbnail-caption-color:     @text-color;\n//** Padding around the thumbnail caption\n@thumbnail-caption-padding:   9px;\n\n\n//== Wells\n//\n//##\n\n@well-bg:                     #f5f5f5;\n@well-border:                 darken(@well-bg, 7%);\n\n\n//== Badges\n//\n//##\n\n@badge-color:                 #fff;\n//** Linked badge text color on hover\n@badge-link-hover-color:      #fff;\n@badge-bg:                    @gray-light;\n\n//** Badge text color in active nav link\n@badge-active-color:          @link-color;\n//** Badge background color in active nav link\n@badge-active-bg:             #fff;\n\n@badge-font-weight:           bold;\n@badge-line-height:           1;\n@badge-border-radius:         10px;\n\n\n//== Breadcrumbs\n//\n//##\n\n@breadcrumb-padding-vertical:   8px;\n@breadcrumb-padding-horizontal: 15px;\n//** Breadcrumb background color\n@breadcrumb-bg:                 #f5f5f5;\n//** Breadcrumb text color\n@breadcrumb-color:              #ccc;\n//** Text color of current page in the breadcrumb\n@breadcrumb-active-color:       @gray-light;\n//** Textual separator for between breadcrumb elements\n@breadcrumb-separator:          \"/\";\n\n\n//== Carousel\n//\n//##\n\n@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);\n\n@carousel-control-color:                      #fff;\n@carousel-control-width:                      15%;\n@carousel-control-opacity:                    .5;\n@carousel-control-font-size:                  20px;\n\n@carousel-indicator-active-bg:                #fff;\n@carousel-indicator-border-color:             #fff;\n\n@carousel-caption-color:                      #fff;\n\n\n//== Close\n//\n//##\n\n@close-font-weight:           bold;\n@close-color:                 #000;\n@close-text-shadow:           0 1px 0 #fff;\n\n\n//== Code\n//\n//##\n\n@code-color:                  #c7254e;\n@code-bg:                     #f9f2f4;\n\n@kbd-color:                   #fff;\n@kbd-bg:                      #333;\n\n@pre-bg:                      #f5f5f5;\n@pre-color:                   @gray-dark;\n@pre-border-color:            #ccc;\n@pre-scrollable-max-height:   340px;\n\n\n//== Type\n//\n//##\n\n//** Text muted color\n@text-muted:                  @gray-light;\n//** Abbreviations and acronyms border color\n@abbr-border-color:           @gray-light;\n//** Headings small color\n@headings-small-color:        @gray-light;\n//** Blockquote small color\n@blockquote-small-color:      @gray-light;\n//** Blockquote font size\n@blockquote-font-size:        (@font-size-base * 1.25);\n//** Blockquote border color\n@blockquote-border-color:     @gray-lighter;\n//** Page header border color\n@page-header-border-color:    @gray-lighter;\n\n\n//== Miscellaneous\n//\n//##\n\n//** Horizontal line color.\n@hr-border:                   @gray-lighter;\n\n//** Horizontal offset for forms and lists.\n@component-offset-horizontal: 180px;\n","//\n// Thumbnails\n// --------------------------------------------------\n\n\n// Mixin and adjust the regular image class\n.thumbnail {\n  display: block;\n  padding: @thumbnail-padding;\n  margin-bottom: @line-height-computed;\n  line-height: @line-height-base;\n  background-color: @thumbnail-bg;\n  border: 1px solid @thumbnail-border;\n  border-radius: @thumbnail-border-radius;\n  .transition(all .2s ease-in-out);\n\n  > img,\n  a > img {\n    &:extend(.img-responsive);\n    margin-left: auto;\n    margin-right: auto;\n  }\n\n  // Add a hover state for linked versions only\n  a&:hover,\n  a&:focus,\n  a&.active {\n    border-color: @link-color;\n  }\n\n  // Image captions\n  .caption {\n    padding: @thumbnail-caption-padding;\n    color: @thumbnail-caption-color;\n  }\n}\n","//\n// Carousel\n// --------------------------------------------------\n\n\n// Wrapper for the slide container and indicators\n.carousel {\n  position: relative;\n}\n\n.carousel-inner {\n  position: relative;\n  overflow: hidden;\n  width: 100%;\n\n  > .item {\n    display: none;\n    position: relative;\n    .transition(.6s ease-in-out left);\n\n    // Account for jankitude on images\n    > img,\n    > a > img {\n      &:extend(.img-responsive);\n      line-height: 1;\n    }\n  }\n\n  > .active,\n  > .next,\n  > .prev { display: block; }\n\n  > .active {\n    left: 0;\n  }\n\n  > .next,\n  > .prev {\n    position: absolute;\n    top: 0;\n    width: 100%;\n  }\n\n  > .next {\n    left: 100%;\n  }\n  > .prev {\n    left: -100%;\n  }\n  > .next.left,\n  > .prev.right {\n    left: 0;\n  }\n\n  > .active.left {\n    left: -100%;\n  }\n  > .active.right {\n    left: 100%;\n  }\n\n}\n\n// Left/right controls for nav\n// ---------------------------\n\n.carousel-control {\n  position: absolute;\n  top: 0;\n  left: 0;\n  bottom: 0;\n  width: @carousel-control-width;\n  .opacity(@carousel-control-opacity);\n  font-size: @carousel-control-font-size;\n  color: @carousel-control-color;\n  text-align: center;\n  text-shadow: @carousel-text-shadow;\n  // We can't have this transition here because WebKit cancels the carousel\n  // animation if you trip this while in the middle of another animation.\n\n  // Set gradients for backgrounds\n  &.left {\n    #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001));\n  }\n  &.right {\n    left: auto;\n    right: 0;\n    #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5));\n  }\n\n  // Hover/focus state\n  &:hover,\n  &:focus {\n    outline: none;\n    color: @carousel-control-color;\n    text-decoration: none;\n    .opacity(.9);\n  }\n\n  // Toggles\n  .icon-prev,\n  .icon-next,\n  .glyphicon-chevron-left,\n  .glyphicon-chevron-right {\n    position: absolute;\n    top: 50%;\n    z-index: 5;\n    display: inline-block;\n  }\n  .icon-prev,\n  .glyphicon-chevron-left {\n    left: 50%;\n  }\n  .icon-next,\n  .glyphicon-chevron-right {\n    right: 50%;\n  }\n  .icon-prev,\n  .icon-next {\n    width:  20px;\n    height: 20px;\n    margin-top: -10px;\n    margin-left: -10px;\n    font-family: serif;\n  }\n\n  .icon-prev {\n    &:before {\n      content: '\\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039)\n    }\n  }\n  .icon-next {\n    &:before {\n      content: '\\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A)\n    }\n  }\n}\n\n// Optional indicator pips\n//\n// Add an unordered list with the following class and add a list item for each\n// slide your carousel holds.\n\n.carousel-indicators {\n  position: absolute;\n  bottom: 10px;\n  left: 50%;\n  z-index: 15;\n  width: 60%;\n  margin-left: -30%;\n  padding-left: 0;\n  list-style: none;\n  text-align: center;\n\n  li {\n    display: inline-block;\n    width:  10px;\n    height: 10px;\n    margin: 1px;\n    text-indent: -999px;\n    border: 1px solid @carousel-indicator-border-color;\n    border-radius: 10px;\n    cursor: pointer;\n\n    // IE8-9 hack for event handling\n    //\n    // Internet Explorer 8-9 does not support clicks on elements without a set\n    // `background-color`. We cannot use `filter` since that's not viewed as a\n    // background color by the browser. Thus, a hack is needed.\n    //\n    // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we\n    // set alpha transparency for the best results possible.\n    background-color: #000 \\9; // IE8\n    background-color: rgba(0,0,0,0); // IE9\n  }\n  .active {\n    margin: 0;\n    width:  12px;\n    height: 12px;\n    background-color: @carousel-indicator-active-bg;\n  }\n}\n\n// Optional captions\n// -----------------------------\n// Hidden by default for smaller viewports\n.carousel-caption {\n  position: absolute;\n  left: 15%;\n  right: 15%;\n  bottom: 20px;\n  z-index: 10;\n  padding-top: 20px;\n  padding-bottom: 20px;\n  color: @carousel-caption-color;\n  text-align: center;\n  text-shadow: @carousel-text-shadow;\n  & .btn {\n    text-shadow: none; // No shadow for button elements in carousel-caption\n  }\n}\n\n\n// Scale up controls for tablets and up\n@media screen and (min-width: @screen-sm-min) {\n\n  // Scale up the controls a smidge\n  .carousel-control {\n    .glyphicon-chevron-left,\n    .glyphicon-chevron-right,\n    .icon-prev,\n    .icon-next {\n      width: 30px;\n      height: 30px;\n      margin-top: -15px;\n      margin-left: -15px;\n      font-size: 30px;\n    }\n  }\n\n  // Show and left align the captions\n  .carousel-caption {\n    left: 20%;\n    right: 20%;\n    padding-bottom: 30px;\n  }\n\n  // Move up the indicators\n  .carousel-indicators {\n    bottom: 20px;\n  }\n}\n","//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n  font-family: @headings-font-family;\n  font-weight: @headings-font-weight;\n  line-height: @headings-line-height;\n  color: @headings-color;\n\n  small,\n  .small {\n    font-weight: normal;\n    line-height: 1;\n    color: @headings-small-color;\n  }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n  margin-top: @line-height-computed;\n  margin-bottom: (@line-height-computed / 2);\n\n  small,\n  .small {\n    font-size: 65%;\n  }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n  margin-top: (@line-height-computed / 2);\n  margin-bottom: (@line-height-computed / 2);\n\n  small,\n  .small {\n    font-size: 75%;\n  }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n  margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n  margin-bottom: @line-height-computed;\n  font-size: floor((@font-size-base * 1.15));\n  font-weight: 200;\n  line-height: 1.4;\n\n  @media (min-width: @screen-sm-min) {\n    font-size: (@font-size-base * 1.5);\n  }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: 14px base font * 85% = about 12px\nsmall,\n.small  { font-size: 85%; }\n\n// Undo browser default styling\ncite    { font-style: normal; }\n\n// Alignment\n.text-left           { text-align: left; }\n.text-right          { text-align: right; }\n.text-center         { text-align: center; }\n.text-justify        { text-align: justify; }\n\n// Contextual colors\n.text-muted {\n  color: @text-muted;\n}\n.text-primary {\n  .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n  .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n  .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n  .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n  .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n  // Given the contrast here, this is the only class to have its color inverted\n  // automatically.\n  color: #fff;\n  .bg-variant(@brand-primary);\n}\n.bg-success {\n  .bg-variant(@state-success-bg);\n}\n.bg-info {\n  .bg-variant(@state-info-bg);\n}\n.bg-warning {\n  .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n  .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n  padding-bottom: ((@line-height-computed / 2) - 1);\n  margin: (@line-height-computed * 2) 0 @line-height-computed;\n  border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// --------------------------------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n  margin-top: 0;\n  margin-bottom: (@line-height-computed / 2);\n  ul,\n  ol {\n    margin-bottom: 0;\n  }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n  padding-left: 0;\n  list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n  .list-unstyled();\n  margin-left: -5px;\n\n  > li {\n    display: inline-block;\n    padding-left: 5px;\n    padding-right: 5px;\n  }\n}\n\n// Description Lists\ndl {\n  margin-top: 0; // Remove browser default\n  margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n  line-height: @line-height-base;\n}\ndt {\n  font-weight: bold;\n}\ndd {\n  margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n@media (min-width: @grid-float-breakpoint) {\n  .dl-horizontal {\n    dt {\n      float: left;\n      width: (@component-offset-horizontal - 20);\n      clear: left;\n      text-align: right;\n      .text-overflow();\n    }\n    dd {\n      margin-left: @component-offset-horizontal;\n      &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n    }\n  }\n}\n\n// MISC\n// ----\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n  cursor: help;\n  border-bottom: 1px dotted @abbr-border-color;\n}\n.initialism {\n  font-size: 90%;\n  text-transform: uppercase;\n}\n\n// Blockquotes\nblockquote {\n  padding: (@line-height-computed / 2) @line-height-computed;\n  margin: 0 0 @line-height-computed;\n  font-size: @blockquote-font-size;\n  border-left: 5px solid @blockquote-border-color;\n\n  p,\n  ul,\n  ol {\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  // Note: Deprecated small and .small as of v3.1.0\n  // Context: https://github.com/twbs/bootstrap/issues/11660\n  footer,\n  small,\n  .small {\n    display: block;\n    font-size: 80%; // back to default font-size\n    line-height: @line-height-base;\n    color: @blockquote-small-color;\n\n    &:before {\n      content: '\\2014 \\00A0'; // em dash, nbsp\n    }\n  }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n  padding-right: 15px;\n  padding-left: 0;\n  border-right: 5px solid @blockquote-border-color;\n  border-left: 0;\n  text-align: right;\n\n  // Account for citation\n  footer,\n  small,\n  .small {\n    &:before { content: ''; }\n    &:after {\n      content: '\\00A0 \\2014'; // nbsp, em dash\n    }\n  }\n}\n\n// Quotes\nblockquote:before,\nblockquote:after {\n  content: \"\";\n}\n\n// Addresses\naddress {\n  margin-bottom: @line-height-computed;\n  font-style: normal;\n  line-height: @line-height-base;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n  font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: @code-color;\n  background-color: @code-bg;\n  white-space: nowrap;\n  border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: @kbd-color;\n  background-color: @kbd-bg;\n  border-radius: @border-radius-small;\n  box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);\n}\n\n// Blocks of code\npre {\n  display: block;\n  padding: ((@line-height-computed - 1) / 2);\n  margin: 0 0 (@line-height-computed / 2);\n  font-size: (@font-size-base - 1); // 14px to 13px\n  line-height: @line-height-base;\n  word-break: break-all;\n  word-wrap: break-word;\n  color: @pre-color;\n  background-color: @pre-bg;\n  border: 1px solid @pre-border-color;\n  border-radius: @border-radius-base;\n\n  // Account for some code outputs that place code tags in pre tags\n  code {\n    padding: 0;\n    font-size: inherit;\n    color: inherit;\n    white-space: pre-wrap;\n    background-color: transparent;\n    border-radius: 0;\n  }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n  max-height: @pre-scrollable-max-height;\n  overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n  .container-fixed();\n\n  @media (min-width: @screen-sm-min) {\n    width: @container-sm;\n  }\n  @media (min-width: @screen-md-min) {\n    width: @container-md;\n  }\n  @media (min-width: @screen-lg-min) {\n    width: @container-lg;\n  }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n  .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n  .make-row();\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n  .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n  .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n  .make-grid(lg);\n}\n","//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n  max-width: 100%;\n  background-color: @table-bg;\n}\nth {\n  text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n  width: 100%;\n  margin-bottom: @line-height-computed;\n  // Cells\n  > thead,\n  > tbody,\n  > tfoot {\n    > tr {\n      > th,\n      > td {\n        padding: @table-cell-padding;\n        line-height: @line-height-base;\n        vertical-align: top;\n        border-top: 1px solid @table-border-color;\n      }\n    }\n  }\n  // Bottom align for column headings\n  > thead > tr > th {\n    vertical-align: bottom;\n    border-bottom: 2px solid @table-border-color;\n  }\n  // Remove top border from thead by default\n  > caption + thead,\n  > colgroup + thead,\n  > thead:first-child {\n    > tr:first-child {\n      > th,\n      > td {\n        border-top: 0;\n      }\n    }\n  }\n  // Account for multiple tbody instances\n  > tbody + tbody {\n    border-top: 2px solid @table-border-color;\n  }\n\n  // Nesting\n  .table {\n    background-color: @body-bg;\n  }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n  > thead,\n  > tbody,\n  > tfoot {\n    > tr {\n      > th,\n      > td {\n        padding: @table-condensed-cell-padding;\n      }\n    }\n  }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n  border: 1px solid @table-border-color;\n  > thead,\n  > tbody,\n  > tfoot {\n    > tr {\n      > th,\n      > td {\n        border: 1px solid @table-border-color;\n      }\n    }\n  }\n  > thead > tr {\n    > th,\n    > td {\n      border-bottom-width: 2px;\n    }\n  }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n  > tbody > tr:nth-child(odd) {\n    > td,\n    > th {\n      background-color: @table-bg-accent;\n    }\n  }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n  > tbody > tr:hover {\n    > td,\n    > th {\n      background-color: @table-bg-hover;\n    }\n  }\n}\n\n\n// Table cell sizing\n//\n// Reset default table behavior\n\ntable col[class*=\"col-\"] {\n  position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n  float: none;\n  display: table-column;\n}\ntable {\n  td,\n  th {\n    &[class*=\"col-\"] {\n      position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n      float: none;\n      display: table-cell;\n    }\n  }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n@media (max-width: @screen-xs-max) {\n  .table-responsive {\n    width: 100%;\n    margin-bottom: (@line-height-computed * 0.75);\n    overflow-y: hidden;\n    overflow-x: scroll;\n    -ms-overflow-style: -ms-autohiding-scrollbar;\n    border: 1px solid @table-border-color;\n    -webkit-overflow-scrolling: touch;\n\n    // Tighten up spacing\n    > .table {\n      margin-bottom: 0;\n\n      // Ensure the content doesn't wrap\n      > thead,\n      > tbody,\n      > tfoot {\n        > tr {\n          > th,\n          > td {\n            white-space: nowrap;\n          }\n        }\n      }\n    }\n\n    // Special overrides for the bordered tables\n    > .table-bordered {\n      border: 0;\n\n      // Nuke the appropriate borders so that the parent can handle them\n      > thead,\n      > tbody,\n      > tfoot {\n        > tr {\n          > th:first-child,\n          > td:first-child {\n            border-left: 0;\n          }\n          > th:last-child,\n          > td:last-child {\n            border-right: 0;\n          }\n        }\n      }\n\n      // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n      // chances are there will be only one `tr` in a `thead` and that would\n      // remove the border altogether.\n      > tbody,\n      > tfoot {\n        > tr:last-child {\n          > th,\n          > td {\n            border-bottom: 0;\n          }\n        }\n      }\n\n    }\n  }\n}\n","//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n  padding: 0;\n  margin: 0;\n  border: 0;\n  // Chrome and Firefox set a `min-width: -webkit-min-content;` on fieldsets,\n  // so we reset that to ensure it behaves more like a standard block element.\n  // See https://github.com/twbs/bootstrap/issues/12359.\n  min-width: 0;\n}\n\nlegend {\n  display: block;\n  width: 100%;\n  padding: 0;\n  margin-bottom: @line-height-computed;\n  font-size: (@font-size-base * 1.5);\n  line-height: inherit;\n  color: @legend-color;\n  border: 0;\n  border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n  display: inline-block;\n  margin-bottom: 5px;\n  font-weight: bold;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\n// Override content-box in Normalize (* isn't specific enough)\ninput[type=\"search\"] {\n  .box-sizing(border-box);\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n  margin: 4px 0 0;\n  margin-top: 1px \\9; /* IE8-9 */\n  line-height: normal;\n}\n\n// Set the height of file controls to match text inputs\ninput[type=\"file\"] {\n  display: block;\n}\n\n// Make range inputs behave like textual form controls\ninput[type=\"range\"] {\n  display: block;\n  width: 100%;\n}\n\n// Make multiple select elements height not fixed\nselect[multiple],\nselect[size] {\n  height: auto;\n}\n\n// Focus for file, radio, and checkbox\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n  .tab-focus();\n}\n\n// Adjust output element\noutput {\n  display: block;\n  padding-top: (@padding-base-vertical + 1);\n  font-size: @font-size-base;\n  line-height: @line-height-base;\n  color: @input-color;\n}\n\n\n// Common form controls\n//\n// Shared size and type resets for form controls. Apply `.form-control` to any\n// of the following form controls:\n//\n// select\n// textarea\n// input[type=\"text\"]\n// input[type=\"password\"]\n// input[type=\"datetime\"]\n// input[type=\"datetime-local\"]\n// input[type=\"date\"]\n// input[type=\"month\"]\n// input[type=\"time\"]\n// input[type=\"week\"]\n// input[type=\"number\"]\n// input[type=\"email\"]\n// input[type=\"url\"]\n// input[type=\"search\"]\n// input[type=\"tel\"]\n// input[type=\"color\"]\n\n.form-control {\n  display: block;\n  width: 100%;\n  height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n  padding: @padding-base-vertical @padding-base-horizontal;\n  font-size: @font-size-base;\n  line-height: @line-height-base;\n  color: @input-color;\n  background-color: @input-bg;\n  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n  border: 1px solid @input-border;\n  border-radius: @input-border-radius;\n  .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));\n  .transition(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n\n  // Customize the `:focus` state to imitate native WebKit styles.\n  .form-control-focus();\n\n  // Placeholder\n  .placeholder();\n\n  // Disabled and read-only inputs\n  //\n  // HTML5 says that controls under a fieldset > legend:first-child won't be\n  // disabled if the fieldset is disabled. Due to implementation difficulty, we\n  // don't honor that edge case; we style them as disabled anyway.\n  &[disabled],\n  &[readonly],\n  fieldset[disabled] & {\n    cursor: not-allowed;\n    background-color: @input-bg-disabled;\n    opacity: 1; // iOS fix for unreadable disabled content\n  }\n\n  // Reset height for `textarea`s\n  textarea& {\n    height: auto;\n  }\n}\n\n\n// Search inputs in iOS\n//\n// This overrides the extra rounded corners on search inputs in iOS so that our\n// `.form-control` class can properly style them. Note that this cannot simply\n// be added to `.form-control` as it's not specific enough. For details, see\n// https://github.com/twbs/bootstrap/issues/11586.\n\ninput[type=\"search\"] {\n  -webkit-appearance: none;\n}\n\n\n// Special styles for iOS date input\n//\n// In Mobile Safari, date inputs require a pixel line-height that matches the\n// given height of the input.\n\ninput[type=\"date\"] {\n  line-height: @input-height-base;\n}\n\n\n// Form groups\n//\n// Designed to help with the organization and spacing of vertical forms. For\n// horizontal forms, use the predefined grid classes.\n\n.form-group {\n  margin-bottom: 15px;\n}\n\n\n// Checkboxes and radios\n//\n// Indent the labels to position radios/checkboxes as hanging controls.\n\n.radio,\n.checkbox {\n  display: block;\n  min-height: @line-height-computed; // clear the floating input if there is no label text\n  margin-top: 10px;\n  margin-bottom: 10px;\n  padding-left: 20px;\n  label {\n    display: inline;\n    font-weight: normal;\n    cursor: pointer;\n  }\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n  float: left;\n  margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n  margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing\n}\n\n// Radios and checkboxes on same line\n.radio-inline,\n.checkbox-inline {\n  display: inline-block;\n  padding-left: 20px;\n  margin-bottom: 0;\n  vertical-align: middle;\n  font-weight: normal;\n  cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n  margin-top: 0;\n  margin-left: 10px; // space out consecutive inline controls\n}\n\n// Apply same disabled cursor tweak as for inputs\n//\n// Note: Neither radios nor checkboxes can be readonly.\ninput[type=\"radio\"],\ninput[type=\"checkbox\"],\n.radio,\n.radio-inline,\n.checkbox,\n.checkbox-inline {\n  &[disabled],\n  fieldset[disabled] & {\n    cursor: not-allowed;\n  }\n}\n\n\n// Form control sizing\n//\n// Build on `.form-control` with modifier classes to decrease or increase the\n// height and font-size of form controls.\n\n.input-sm {\n  .input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n\n.input-lg {\n  .input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);\n}\n\n\n// Form control feedback states\n//\n// Apply contextual and semantic states to individual form controls.\n\n.has-feedback {\n  // Enable absolute positioning\n  position: relative;\n\n  // Ensure icons don't overlap text\n  .form-control {\n    padding-right: (@input-height-base * 1.25);\n  }\n\n  // Feedback icon (requires .glyphicon classes)\n  .form-control-feedback {\n    position: absolute;\n    top: (@line-height-computed + 5); // Height of the `label` and its margin\n    right: 0;\n    display: block;\n    width: @input-height-base;\n    height: @input-height-base;\n    line-height: @input-height-base;\n    text-align: center;\n  }\n}\n\n// Feedback states\n.has-success {\n  .form-control-validation(@state-success-text; @state-success-text; @state-success-bg);\n}\n.has-warning {\n  .form-control-validation(@state-warning-text; @state-warning-text; @state-warning-bg);\n}\n.has-error {\n  .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg);\n}\n\n\n// Static form control text\n//\n// Apply class to a `p` element to make any string of text align with labels in\n// a horizontal form layout.\n\n.form-control-static {\n  margin-bottom: 0; // Remove default margin from `p`\n}\n\n\n// Help text\n//\n// Apply to any element you wish to create light text for placement immediately\n// below a form control. Use for general help, formatting, or instructional text.\n\n.help-block {\n  display: block; // account for any element using help-block\n  margin-top: 5px;\n  margin-bottom: 10px;\n  color: lighten(@text-color, 25%); // lighten the text some for contrast\n}\n\n\n\n// Inline forms\n//\n// Make forms appear inline(-block) by adding the `.form-inline` class. Inline\n// forms begin stacked on extra small (mobile) devices and then go inline when\n// viewports reach <768px.\n//\n// Requires wrapping inputs and labels with `.form-group` for proper display of\n// default HTML form controls and our custom form controls (e.g., input groups).\n//\n// Heads up! This is mixin-ed into `.navbar-form` in navbars.less.\n\n.form-inline {\n\n  // Kick in the inline\n  @media (min-width: @screen-sm-min) {\n    // Inline-block all the things for \"inline\"\n    .form-group {\n      display: inline-block;\n      margin-bottom: 0;\n      vertical-align: middle;\n    }\n\n    // In navbar-form, allow folks to *not* use `.form-group`\n    .form-control {\n      display: inline-block;\n      width: auto; // Prevent labels from stacking above inputs in `.form-group`\n      vertical-align: middle;\n    }\n    // Input groups need that 100% width though\n    .input-group > .form-control {\n      width: 100%;\n    }\n\n    .control-label {\n      margin-bottom: 0;\n      vertical-align: middle;\n    }\n\n    // Remove default margin on radios/checkboxes that were used for stacking, and\n    // then undo the floating of radios and checkboxes to match (which also avoids\n    // a bug in WebKit: https://github.com/twbs/bootstrap/issues/1969).\n    .radio,\n    .checkbox {\n      display: inline-block;\n      margin-top: 0;\n      margin-bottom: 0;\n      padding-left: 0;\n      vertical-align: middle;\n    }\n    .radio input[type=\"radio\"],\n    .checkbox input[type=\"checkbox\"] {\n      float: none;\n      margin-left: 0;\n    }\n\n    // Validation states\n    //\n    // Reposition the icon because it's now within a grid column and columns have\n    // `position: relative;` on them. Also accounts for the grid gutter padding.\n    .has-feedback .form-control-feedback {\n      top: 0;\n    }\n  }\n}\n\n\n// Horizontal forms\n//\n// Horizontal forms are built on grid classes and allow you to create forms with\n// labels on the left and inputs on the right.\n\n.form-horizontal {\n\n  // Consistent vertical alignment of labels, radios, and checkboxes\n  .control-label,\n  .radio,\n  .checkbox,\n  .radio-inline,\n  .checkbox-inline {\n    margin-top: 0;\n    margin-bottom: 0;\n    padding-top: (@padding-base-vertical + 1); // Default padding plus a border\n  }\n  // Account for padding we're adding to ensure the alignment and of help text\n  // and other content below items\n  .radio,\n  .checkbox {\n    min-height: (@line-height-computed + (@padding-base-vertical + 1));\n  }\n\n  // Make form groups behave like rows\n  .form-group {\n    .make-row();\n  }\n\n  .form-control-static {\n    padding-top: (@padding-base-vertical + 1);\n  }\n\n  // Only right align form labels here when the columns stop stacking\n  @media (min-width: @screen-sm-min) {\n    .control-label {\n      text-align: right;\n    }\n  }\n\n  // Validation states\n  //\n  // Reposition the icon because it's now within a grid column and columns have\n  // `position: relative;` on them. Also accounts for the grid gutter padding.\n  .has-feedback .form-control-feedback {\n    top: 0;\n    right: (@grid-gutter-width / 2);\n  }\n}\n","//\n// Buttons\n// --------------------------------------------------\n\n\n// Base styles\n// --------------------------------------------------\n\n.btn {\n  display: inline-block;\n  margin-bottom: 0; // For input.btn\n  font-weight: @btn-font-weight;\n  text-align: center;\n  vertical-align: middle;\n  cursor: pointer;\n  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n  border: 1px solid transparent;\n  white-space: nowrap;\n  .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @border-radius-base);\n  .user-select(none);\n\n  &,\n  &:active,\n  &.active {\n    &:focus {\n      .tab-focus();\n    }\n  }\n\n  &:hover,\n  &:focus {\n    color: @btn-default-color;\n    text-decoration: none;\n  }\n\n  &:active,\n  &.active {\n    outline: 0;\n    background-image: none;\n    .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n  }\n\n  &.disabled,\n  &[disabled],\n  fieldset[disabled] & {\n    cursor: not-allowed;\n    pointer-events: none; // Future-proof disabling of clicks\n    .opacity(.65);\n    .box-shadow(none);\n  }\n}\n\n\n// Alternate buttons\n// --------------------------------------------------\n\n.btn-default {\n  .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border);\n}\n.btn-primary {\n  .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border);\n}\n// Success appears as green\n.btn-success {\n  .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border);\n}\n// Info appears as blue-green\n.btn-info {\n  .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border);\n}\n// Warning appears as orange\n.btn-warning {\n  .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border);\n}\n// Danger and error appear as red\n.btn-danger {\n  .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border);\n}\n\n\n// Link buttons\n// -------------------------\n\n// Make a button look and behave like a link\n.btn-link {\n  color: @link-color;\n  font-weight: normal;\n  cursor: pointer;\n  border-radius: 0;\n\n  &,\n  &:active,\n  &[disabled],\n  fieldset[disabled] & {\n    background-color: transparent;\n    .box-shadow(none);\n  }\n  &,\n  &:hover,\n  &:focus,\n  &:active {\n    border-color: transparent;\n  }\n  &:hover,\n  &:focus {\n    color: @link-hover-color;\n    text-decoration: underline;\n    background-color: transparent;\n  }\n  &[disabled],\n  fieldset[disabled] & {\n    &:hover,\n    &:focus {\n      color: @btn-link-disabled-color;\n      text-decoration: none;\n    }\n  }\n}\n\n\n// Button Sizes\n// --------------------------------------------------\n\n.btn-lg {\n  // line-height: ensure even-numbered height of button next to large input\n  .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);\n}\n.btn-sm {\n  // line-height: ensure proper height of button next to small input\n  .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n.btn-xs {\n  .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n\n\n// Block button\n// --------------------------------------------------\n\n.btn-block {\n  display: block;\n  width: 100%;\n  padding-left: 0;\n  padding-right: 0;\n}\n\n// Vertically space out multiple block buttons\n.btn-block + .btn-block {\n  margin-top: 5px;\n}\n\n// Specificity overrides\ninput[type=\"submit\"],\ninput[type=\"reset\"],\ninput[type=\"button\"] {\n  &.btn-block {\n    width: 100%;\n  }\n}\n","//\n// Button groups\n// --------------------------------------------------\n\n// Make the div behave like a button\n.btn-group,\n.btn-group-vertical {\n  position: relative;\n  display: inline-block;\n  vertical-align: middle; // match .btn alignment given font-size hack above\n  > .btn {\n    position: relative;\n    float: left;\n    // Bring the \"active\" button to the front\n    &:hover,\n    &:focus,\n    &:active,\n    &.active {\n      z-index: 2;\n    }\n    &:focus {\n      // Remove focus outline when dropdown JS adds it after closing the menu\n      outline: none;\n    }\n  }\n}\n\n// Prevent double borders when buttons are next to each other\n.btn-group {\n  .btn + .btn,\n  .btn + .btn-group,\n  .btn-group + .btn,\n  .btn-group + .btn-group {\n    margin-left: -1px;\n  }\n}\n\n// Optional: Group multiple button groups together for a toolbar\n.btn-toolbar {\n  margin-left: -5px; // Offset the first child's margin\n  &:extend(.clearfix all);\n\n  .btn-group,\n  .input-group {\n    float: left;\n  }\n  > .btn,\n  > .btn-group,\n  > .input-group {\n    margin-left: 5px;\n  }\n}\n\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n  border-radius: 0;\n}\n\n// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match\n.btn-group > .btn:first-child {\n  margin-left: 0;\n  &:not(:last-child):not(.dropdown-toggle) {\n    .border-right-radius(0);\n  }\n}\n// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n  .border-left-radius(0);\n}\n\n// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group)\n.btn-group > .btn-group {\n  float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group > .btn-group:first-child {\n  > .btn:last-child,\n  > .dropdown-toggle {\n    .border-right-radius(0);\n  }\n}\n.btn-group > .btn-group:last-child > .btn:first-child {\n  .border-left-radius(0);\n}\n\n// On active and open, don't show outline\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n  outline: 0;\n}\n\n\n// Sizing\n//\n// Remix the default button sizing classes into new ones for easier manipulation.\n\n.btn-group-xs > .btn { &:extend(.btn-xs); }\n.btn-group-sm > .btn { &:extend(.btn-sm); }\n.btn-group-lg > .btn { &:extend(.btn-lg); }\n\n\n// Split button dropdowns\n// ----------------------\n\n// Give the line between buttons some depth\n.btn-group > .btn + .dropdown-toggle {\n  padding-left: 8px;\n  padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n  padding-left: 12px;\n  padding-right: 12px;\n}\n\n// The clickable button for toggling the menu\n// Remove the gradient and set the same inset shadow as the :active state\n.btn-group.open .dropdown-toggle {\n  .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n\n  // Show no shadow for `.btn-link` since it has no other button styles.\n  &.btn-link {\n    .box-shadow(none);\n  }\n}\n\n\n// Reposition the caret\n.btn .caret {\n  margin-left: 0;\n}\n// Carets in other button sizes\n.btn-lg .caret {\n  border-width: @caret-width-large @caret-width-large 0;\n  border-bottom-width: 0;\n}\n// Upside down carets for .dropup\n.dropup .btn-lg .caret {\n  border-width: 0 @caret-width-large @caret-width-large;\n}\n\n\n// Vertical button groups\n// ----------------------\n\n.btn-group-vertical {\n  > .btn,\n  > .btn-group,\n  > .btn-group > .btn {\n    display: block;\n    float: none;\n    width: 100%;\n    max-width: 100%;\n  }\n\n  // Clear floats so dropdown menus can be properly placed\n  > .btn-group {\n    &:extend(.clearfix all);\n    > .btn {\n      float: none;\n    }\n  }\n\n  > .btn + .btn,\n  > .btn + .btn-group,\n  > .btn-group + .btn,\n  > .btn-group + .btn-group {\n    margin-top: -1px;\n    margin-left: 0;\n  }\n}\n\n.btn-group-vertical > .btn {\n  &:not(:first-child):not(:last-child) {\n    border-radius: 0;\n  }\n  &:first-child:not(:last-child) {\n    border-top-right-radius: @border-radius-base;\n    .border-bottom-radius(0);\n  }\n  &:last-child:not(:first-child) {\n    border-bottom-left-radius: @border-radius-base;\n    .border-top-radius(0);\n  }\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) {\n  > .btn:last-child,\n  > .dropdown-toggle {\n    .border-bottom-radius(0);\n  }\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n  .border-top-radius(0);\n}\n\n\n\n// Justified button groups\n// ----------------------\n\n.btn-group-justified {\n  display: table;\n  width: 100%;\n  table-layout: fixed;\n  border-collapse: separate;\n  > .btn,\n  > .btn-group {\n    float: none;\n    display: table-cell;\n    width: 1%;\n  }\n  > .btn-group .btn {\n    width: 100%;\n  }\n}\n\n\n// Checkbox and radio options\n[data-toggle=\"buttons\"] > .btn > input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn > input[type=\"checkbox\"] {\n  display: none;\n}\n","//\n// Component animations\n// --------------------------------------------------\n\n// Heads up!\n//\n// We don't use the `.opacity()` mixin here since it causes a bug with text\n// fields in IE7-8. Source: https://github.com/twitter/bootstrap/pull/3552.\n\n.fade {\n  opacity: 0;\n  .transition(opacity .15s linear);\n  &.in {\n    opacity: 1;\n  }\n}\n\n.collapse {\n  display: none;\n  &.in {\n    display: block;\n  }\n}\n.collapsing {\n  position: relative;\n  height: 0;\n  overflow: hidden;\n  .transition(height .35s ease);\n}\n","//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n//  Star\n\n// Import the fonts\n@font-face {\n  font-family: 'Glyphicons Halflings';\n  src: ~\"url('@{icon-font-path}@{icon-font-name}.eot')\";\n  src: ~\"url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype')\",\n       ~\"url('@{icon-font-path}@{icon-font-name}.woff') format('woff')\",\n       ~\"url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype')\",\n       ~\"url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg')\";\n}\n\n// Catchall baseclass\n.glyphicon {\n  position: relative;\n  top: 1px;\n  display: inline-block;\n  font-family: 'Glyphicons Halflings';\n  font-style: normal;\n  font-weight: normal;\n  line-height: 1;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk               { &:before { content: \"\\2a\"; } }\n.glyphicon-plus                   { &:before { content: \"\\2b\"; } }\n.glyphicon-euro                   { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus                  { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud                  { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope               { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil                 { &:before { content: \"\\270f\"; } }\n.glyphicon-glass                  { &:before { content: \"\\e001\"; } }\n.glyphicon-music                  { &:before { content: \"\\e002\"; } }\n.glyphicon-search                 { &:before { content: \"\\e003\"; } }\n.glyphicon-heart                  { &:before { content: \"\\e005\"; } }\n.glyphicon-star                   { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty             { &:before { content: \"\\e007\"; } }\n.glyphicon-user                   { &:before { content: \"\\e008\"; } }\n.glyphicon-film                   { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large               { &:before { content: \"\\e010\"; } }\n.glyphicon-th                     { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list                { &:before { content: \"\\e012\"; } }\n.glyphicon-ok                     { &:before { content: \"\\e013\"; } }\n.glyphicon-remove                 { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in                { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out               { &:before { content: \"\\e016\"; } }\n.glyphicon-off                    { &:before { content: \"\\e017\"; } }\n.glyphicon-signal                 { &:before { content: \"\\e018\"; } }\n.glyphicon-cog                    { &:before { content: \"\\e019\"; } }\n.glyphicon-trash                  { &:before { content: \"\\e020\"; } }\n.glyphicon-home                   { &:before { content: \"\\e021\"; } }\n.glyphicon-file                   { &:before { content: \"\\e022\"; } }\n.glyphicon-time                   { &:before { content: \"\\e023\"; } }\n.glyphicon-road                   { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt           { &:before { content: \"\\e025\"; } }\n.glyphicon-download               { &:before { content: \"\\e026\"; } }\n.glyphicon-upload                 { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox                  { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle            { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat                 { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh                { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt               { &:before { content: \"\\e032\"; } }\n.glyphicon-lock                   { &:before { content: \"\\e033\"; } }\n.glyphicon-flag                   { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones             { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off             { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down            { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up              { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode                 { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode                { &:before { content: \"\\e040\"; } }\n.glyphicon-tag                    { &:before { content: \"\\e041\"; } }\n.glyphicon-tags                   { &:before { content: \"\\e042\"; } }\n.glyphicon-book                   { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark               { &:before { content: \"\\e044\"; } }\n.glyphicon-print                  { &:before { content: \"\\e045\"; } }\n.glyphicon-camera                 { &:before { content: \"\\e046\"; } }\n.glyphicon-font                   { &:before { content: \"\\e047\"; } }\n.glyphicon-bold                   { &:before { content: \"\\e048\"; } }\n.glyphicon-italic                 { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height            { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width             { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left             { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center           { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right            { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify          { &:before { content: \"\\e055\"; } }\n.glyphicon-list                   { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left            { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right           { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video         { &:before { content: \"\\e059\"; } }\n.glyphicon-picture                { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker             { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust                 { &:before { content: \"\\e063\"; } }\n.glyphicon-tint                   { &:before { content: \"\\e064\"; } }\n.glyphicon-edit                   { &:before { content: \"\\e065\"; } }\n.glyphicon-share                  { &:before { content: \"\\e066\"; } }\n.glyphicon-check                  { &:before { content: \"\\e067\"; } }\n.glyphicon-move                   { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward          { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward          { &:before { content: \"\\e070\"; } }\n.glyphicon-backward               { &:before { content: \"\\e071\"; } }\n.glyphicon-play                   { &:before { content: \"\\e072\"; } }\n.glyphicon-pause                  { &:before { content: \"\\e073\"; } }\n.glyphicon-stop                   { &:before { content: \"\\e074\"; } }\n.glyphicon-forward                { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward           { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward           { &:before { content: \"\\e077\"; } }\n.glyphicon-eject                  { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left           { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right          { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign              { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign             { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign            { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign                { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign          { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign              { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot             { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle          { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle              { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle             { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left             { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right            { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up               { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down             { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt              { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full            { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small           { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign       { &:before { content: \"\\e101\"; } }\n.glyphicon-gift                   { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf                   { &:before { content: \"\\e103\"; } }\n.glyphicon-fire                   { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open               { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close              { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign           { &:before { content: \"\\e107\"; } }\n.glyphicon-plane                  { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar               { &:before { content: \"\\e109\"; } }\n.glyphicon-random                 { &:before { content: \"\\e110\"; } }\n.glyphicon-comment                { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet                 { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up             { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down           { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet                { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart          { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close           { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open            { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical        { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal      { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd                    { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn               { &:before { content: \"\\e122\"; } }\n.glyphicon-bell                   { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate            { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up              { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down            { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right             { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left              { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up                { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down              { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right     { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left      { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up        { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down      { &:before { content: \"\\e134\"; } }\n.glyphicon-globe                  { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench                 { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks                  { &:before { content: \"\\e137\"; } }\n.glyphicon-filter                 { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase              { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen             { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard              { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip              { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty            { &:before { content: \"\\e143\"; } }\n.glyphicon-link                   { &:before { content: \"\\e144\"; } }\n.glyphicon-phone                  { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin                { &:before { content: \"\\e146\"; } }\n.glyphicon-usd                    { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp                    { &:before { content: \"\\e149\"; } }\n.glyphicon-sort                   { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet       { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt   { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order          { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt      { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes     { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked              { &:before { content: \"\\e157\"; } }\n.glyphicon-expand                 { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down          { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up            { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in                 { &:before { content: \"\\e161\"; } }\n.glyphicon-flash                  { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out                { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window             { &:before { content: \"\\e164\"; } }\n.glyphicon-record                 { &:before { content: \"\\e165\"; } }\n.glyphicon-save                   { &:before { content: \"\\e166\"; } }\n.glyphicon-open                   { &:before { content: \"\\e167\"; } }\n.glyphicon-saved                  { &:before { content: \"\\e168\"; } }\n.glyphicon-import                 { &:before { content: \"\\e169\"; } }\n.glyphicon-export                 { &:before { content: \"\\e170\"; } }\n.glyphicon-send                   { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk            { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved           { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove          { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save            { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open            { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card            { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer               { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery                { &:before { content: \"\\e179\"; } }\n.glyphicon-header                 { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed             { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone               { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt              { &:before { content: \"\\e183\"; } }\n.glyphicon-tower                  { &:before { content: \"\\e184\"; } }\n.glyphicon-stats                  { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video               { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video               { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles              { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo           { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby            { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1              { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1              { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1              { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark         { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark      { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download         { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload           { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer           { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous         { &:before { content: \"\\e200\"; } }\n","//\n// Dropdown menus\n// --------------------------------------------------\n\n\n// Dropdown arrow/caret\n.caret {\n  display: inline-block;\n  width: 0;\n  height: 0;\n  margin-left: 2px;\n  vertical-align: middle;\n  border-top:   @caret-width-base solid;\n  border-right: @caret-width-base solid transparent;\n  border-left:  @caret-width-base solid transparent;\n}\n\n// The dropdown wrapper (div)\n.dropdown {\n  position: relative;\n}\n\n// Prevent the focus on the dropdown toggle when closing dropdowns\n.dropdown-toggle:focus {\n  outline: 0;\n}\n\n// The dropdown menu (ul)\n.dropdown-menu {\n  position: absolute;\n  top: 100%;\n  left: 0;\n  z-index: @zindex-dropdown;\n  display: none; // none by default, but block on \"open\" of the menu\n  float: left;\n  min-width: 160px;\n  padding: 5px 0;\n  margin: 2px 0 0; // override default ul\n  list-style: none;\n  font-size: @font-size-base;\n  background-color: @dropdown-bg;\n  border: 1px solid @dropdown-fallback-border; // IE8 fallback\n  border: 1px solid @dropdown-border;\n  border-radius: @border-radius-base;\n  .box-shadow(0 6px 12px rgba(0,0,0,.175));\n  background-clip: padding-box;\n\n  // Aligns the dropdown menu to right\n  //\n  // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]`\n  &.pull-right {\n    right: 0;\n    left: auto;\n  }\n\n  // Dividers (basically an hr) within the dropdown\n  .divider {\n    .nav-divider(@dropdown-divider-bg);\n  }\n\n  // Links within the dropdown menu\n  > li > a {\n    display: block;\n    padding: 3px 20px;\n    clear: both;\n    font-weight: normal;\n    line-height: @line-height-base;\n    color: @dropdown-link-color;\n    white-space: nowrap; // prevent links from randomly breaking onto new lines\n  }\n}\n\n// Hover/Focus state\n.dropdown-menu > li > a {\n  &:hover,\n  &:focus {\n    text-decoration: none;\n    color: @dropdown-link-hover-color;\n    background-color: @dropdown-link-hover-bg;\n  }\n}\n\n// Active state\n.dropdown-menu > .active > a {\n  &,\n  &:hover,\n  &:focus {\n    color: @dropdown-link-active-color;\n    text-decoration: none;\n    outline: 0;\n    background-color: @dropdown-link-active-bg;\n  }\n}\n\n// Disabled state\n//\n// Gray out text and ensure the hover/focus state remains gray\n\n.dropdown-menu > .disabled > a {\n  &,\n  &:hover,\n  &:focus {\n    color: @dropdown-link-disabled-color;\n  }\n}\n// Nuke hover/focus effects\n.dropdown-menu > .disabled > a {\n  &:hover,\n  &:focus {\n    text-decoration: none;\n    background-color: transparent;\n    background-image: none; // Remove CSS gradient\n    .reset-filter();\n    cursor: not-allowed;\n  }\n}\n\n// Open state for the dropdown\n.open {\n  // Show the menu\n  > .dropdown-menu {\n    display: block;\n  }\n\n  // Remove the outline when :focus is triggered\n  > a {\n    outline: 0;\n  }\n}\n\n// Menu positioning\n//\n// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown\n// menu with the parent.\n.dropdown-menu-right {\n  left: auto; // Reset the default from `.dropdown-menu`\n  right: 0;\n}\n// With v3, we enabled auto-flipping if you have a dropdown within a right\n// aligned nav component. To enable the undoing of that, we provide an override\n// to restore the default dropdown menu alignment.\n//\n// This is only for left-aligning a dropdown menu within a `.navbar-right` or\n// `.pull-right` nav component.\n.dropdown-menu-left {\n  left: 0;\n  right: auto;\n}\n\n// Dropdown section headers\n.dropdown-header {\n  display: block;\n  padding: 3px 20px;\n  font-size: @font-size-small;\n  line-height: @line-height-base;\n  color: @dropdown-header-color;\n}\n\n// Backdrop to catch body clicks on mobile, etc.\n.dropdown-backdrop {\n  position: fixed;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  top: 0;\n  z-index: (@zindex-dropdown - 10);\n}\n\n// Right aligned dropdowns\n.pull-right > .dropdown-menu {\n  right: 0;\n  left: auto;\n}\n\n// Allow for dropdowns to go bottom up (aka, dropup-menu)\n//\n// Just add .dropup after the standard .dropdown class and you're set, bro.\n// TODO: abstract this so that the navbar fixed styles are not placed here?\n\n.dropup,\n.navbar-fixed-bottom .dropdown {\n  // Reverse the caret\n  .caret {\n    border-top: 0;\n    border-bottom: @caret-width-base solid;\n    content: \"\";\n  }\n  // Different positioning for bottom up menu\n  .dropdown-menu {\n    top: auto;\n    bottom: 100%;\n    margin-bottom: 1px;\n  }\n}\n\n\n// Component alignment\n//\n// Reiterate per navbar.less and the modified component alignment there.\n\n@media (min-width: @grid-float-breakpoint) {\n  .navbar-right {\n    .dropdown-menu {\n      .dropdown-menu-right();\n    }\n    // Necessary for overrides of the default right aligned menu.\n    // Will remove come v4 in all likelihood.\n    .dropdown-menu-left {\n      .dropdown-menu-left();\n    }\n  }\n}\n\n","//\n// Input groups\n// --------------------------------------------------\n\n// Base styles\n// -------------------------\n.input-group {\n  position: relative; // For dropdowns\n  display: table;\n  border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table\n\n  // Undo padding and float of grid classes\n  &[class*=\"col-\"] {\n    float: none;\n    padding-left: 0;\n    padding-right: 0;\n  }\n\n  .form-control {\n    // Ensure that the input is always above the *appended* addon button for\n    // proper border colors.\n    position: relative;\n    z-index: 2;\n\n    // IE9 fubars the placeholder attribute in text inputs and the arrows on\n    // select elements in input groups. To fix it, we float the input. Details:\n    // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855\n    float: left;\n\n    width: 100%;\n    margin-bottom: 0;\n  }\n}\n\n// Sizing options\n//\n// Remix the default form control sizing classes into new ones for easier\n// manipulation.\n\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn { .input-lg(); }\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn { .input-sm(); }\n\n\n// Display as table-cell\n// -------------------------\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n  display: table-cell;\n\n  &:not(:first-child):not(:last-child) {\n    border-radius: 0;\n  }\n}\n// Addon and addon wrapper for buttons\n.input-group-addon,\n.input-group-btn {\n  width: 1%;\n  white-space: nowrap;\n  vertical-align: middle; // Match the inputs\n}\n\n// Text input groups\n// -------------------------\n.input-group-addon {\n  padding: @padding-base-vertical @padding-base-horizontal;\n  font-size: @font-size-base;\n  font-weight: normal;\n  line-height: 1;\n  color: @input-color;\n  text-align: center;\n  background-color: @input-group-addon-bg;\n  border: 1px solid @input-group-addon-border-color;\n  border-radius: @border-radius-base;\n\n  // Sizing\n  &.input-sm {\n    padding: @padding-small-vertical @padding-small-horizontal;\n    font-size: @font-size-small;\n    border-radius: @border-radius-small;\n  }\n  &.input-lg {\n    padding: @padding-large-vertical @padding-large-horizontal;\n    font-size: @font-size-large;\n    border-radius: @border-radius-large;\n  }\n\n  // Nuke default margins from checkboxes and radios to vertically center within.\n  input[type=\"radio\"],\n  input[type=\"checkbox\"] {\n    margin-top: 0;\n  }\n}\n\n// Reset rounded corners\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n  .border-right-radius(0);\n}\n.input-group-addon:first-child {\n  border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n  .border-left-radius(0);\n}\n.input-group-addon:last-child {\n  border-left: 0;\n}\n\n// Button input groups\n// -------------------------\n.input-group-btn {\n  position: relative;\n  // Jankily prevent input button groups from wrapping with `white-space` and\n  // `font-size` in combination with `inline-block` on buttons.\n  font-size: 0;\n  white-space: nowrap;\n\n  // Negative margin for spacing, position for bringing hovered/focused/actived\n  // element above the siblings.\n  > .btn {\n    position: relative;\n    + .btn {\n      margin-left: -1px;\n    }\n    // Bring the \"active\" button to the front\n    &:hover,\n    &:focus,\n    &:active {\n      z-index: 2;\n    }\n  }\n\n  // Negative margin to only have a 1px border between the two\n  &:first-child {\n    > .btn,\n    > .btn-group {\n      margin-right: -1px;\n    }\n  }\n  &:last-child {\n    > .btn,\n    > .btn-group {\n      margin-left: -1px;\n    }\n  }\n}\n","//\n// Navs\n// --------------------------------------------------\n\n\n// Base class\n// --------------------------------------------------\n\n.nav {\n  margin-bottom: 0;\n  padding-left: 0; // Override default ul/ol\n  list-style: none;\n  &:extend(.clearfix all);\n\n  > li {\n    position: relative;\n    display: block;\n\n    > a {\n      position: relative;\n      display: block;\n      padding: @nav-link-padding;\n      &:hover,\n      &:focus {\n        text-decoration: none;\n        background-color: @nav-link-hover-bg;\n      }\n    }\n\n    // Disabled state sets text to gray and nukes hover/tab effects\n    &.disabled > a {\n      color: @nav-disabled-link-color;\n\n      &:hover,\n      &:focus {\n        color: @nav-disabled-link-hover-color;\n        text-decoration: none;\n        background-color: transparent;\n        cursor: not-allowed;\n      }\n    }\n  }\n\n  // Open dropdowns\n  .open > a {\n    &,\n    &:hover,\n    &:focus {\n      background-color: @nav-link-hover-bg;\n      border-color: @link-color;\n    }\n  }\n\n  // Nav dividers (deprecated with v3.0.1)\n  //\n  // This should have been removed in v3 with the dropping of `.nav-list`, but\n  // we missed it. We don't currently support this anywhere, but in the interest\n  // of maintaining backward compatibility in case you use it, it's deprecated.\n  .nav-divider {\n    .nav-divider();\n  }\n\n  // Prevent IE8 from misplacing imgs\n  //\n  // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989\n  > li > a > img {\n    max-width: none;\n  }\n}\n\n\n// Tabs\n// -------------------------\n\n// Give the tabs something to sit on\n.nav-tabs {\n  border-bottom: 1px solid @nav-tabs-border-color;\n  > li {\n    float: left;\n    // Make the list-items overlay the bottom border\n    margin-bottom: -1px;\n\n    // Actual tabs (as links)\n    > a {\n      margin-right: 2px;\n      line-height: @line-height-base;\n      border: 1px solid transparent;\n      border-radius: @border-radius-base @border-radius-base 0 0;\n      &:hover {\n        border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color;\n      }\n    }\n\n    // Active state, and its :hover to override normal :hover\n    &.active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @nav-tabs-active-link-hover-color;\n        background-color: @nav-tabs-active-link-hover-bg;\n        border: 1px solid @nav-tabs-active-link-hover-border-color;\n        border-bottom-color: transparent;\n        cursor: default;\n      }\n    }\n  }\n  // pulling this in mainly for less shorthand\n  &.nav-justified {\n    .nav-justified();\n    .nav-tabs-justified();\n  }\n}\n\n\n// Pills\n// -------------------------\n.nav-pills {\n  > li {\n    float: left;\n\n    // Links rendered as pills\n    > a {\n      border-radius: @nav-pills-border-radius;\n    }\n    + li {\n      margin-left: 2px;\n    }\n\n    // Active state\n    &.active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @nav-pills-active-link-hover-color;\n        background-color: @nav-pills-active-link-hover-bg;\n      }\n    }\n  }\n}\n\n\n// Stacked pills\n.nav-stacked {\n  > li {\n    float: none;\n    + li {\n      margin-top: 2px;\n      margin-left: 0; // no need for this gap between nav items\n    }\n  }\n}\n\n\n// Nav variations\n// --------------------------------------------------\n\n// Justified nav links\n// -------------------------\n\n.nav-justified {\n  width: 100%;\n\n  > li {\n    float: none;\n     > a {\n      text-align: center;\n      margin-bottom: 5px;\n    }\n  }\n\n  > .dropdown .dropdown-menu {\n    top: auto;\n    left: auto;\n  }\n\n  @media (min-width: @screen-sm-min) {\n    > li {\n      display: table-cell;\n      width: 1%;\n      > a {\n        margin-bottom: 0;\n      }\n    }\n  }\n}\n\n// Move borders to anchors instead of bottom of list\n//\n// Mixin for adding on top the shared `.nav-justified` styles for our tabs\n.nav-tabs-justified {\n  border-bottom: 0;\n\n  > li > a {\n    // Override margin from .nav-tabs\n    margin-right: 0;\n    border-radius: @border-radius-base;\n  }\n\n  > .active > a,\n  > .active > a:hover,\n  > .active > a:focus {\n    border: 1px solid @nav-tabs-justified-link-border-color;\n  }\n\n  @media (min-width: @screen-sm-min) {\n    > li > a {\n      border-bottom: 1px solid @nav-tabs-justified-link-border-color;\n      border-radius: @border-radius-base @border-radius-base 0 0;\n    }\n    > .active > a,\n    > .active > a:hover,\n    > .active > a:focus {\n      border-bottom-color: @nav-tabs-justified-active-link-border-color;\n    }\n  }\n}\n\n\n// Tabbable tabs\n// -------------------------\n\n// Hide tabbable panes to start, show them when `.active`\n.tab-content {\n  > .tab-pane {\n    display: none;\n  }\n  > .active {\n    display: block;\n  }\n}\n\n\n// Dropdowns\n// -------------------------\n\n// Specific dropdowns\n.nav-tabs .dropdown-menu {\n  // make dropdown border overlap tab border\n  margin-top: -1px;\n  // Remove the top rounded corners here since there is a hard edge above the menu\n  .border-top-radius(0);\n}\n","//\n// Navbars\n// --------------------------------------------------\n\n\n// Wrapper and base class\n//\n// Provide a static navbar from which we expand to create full-width, fixed, and\n// other navbar variations.\n\n.navbar {\n  position: relative;\n  min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode)\n  margin-bottom: @navbar-margin-bottom;\n  border: 1px solid transparent;\n\n  // Prevent floats from breaking the navbar\n  &:extend(.clearfix all);\n\n  @media (min-width: @grid-float-breakpoint) {\n    border-radius: @navbar-border-radius;\n  }\n}\n\n\n// Navbar heading\n//\n// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy\n// styling of responsive aspects.\n\n.navbar-header {\n  &:extend(.clearfix all);\n\n  @media (min-width: @grid-float-breakpoint) {\n    float: left;\n  }\n}\n\n\n// Navbar collapse (body)\n//\n// Group your navbar content into this for easy collapsing and expanding across\n// various device sizes. By default, this content is collapsed when <768px, but\n// will expand past that for a horizontal display.\n//\n// To start (on mobile devices) the navbar links, forms, and buttons are stacked\n// vertically and include a `max-height` to overflow in case you have too much\n// content for the user's viewport.\n\n.navbar-collapse {\n  max-height: @navbar-collapse-max-height;\n  overflow-x: visible;\n  padding-right: @navbar-padding-horizontal;\n  padding-left:  @navbar-padding-horizontal;\n  border-top: 1px solid transparent;\n  box-shadow: inset 0 1px 0 rgba(255,255,255,.1);\n  &:extend(.clearfix all);\n  -webkit-overflow-scrolling: touch;\n\n  &.in {\n    overflow-y: auto;\n  }\n\n  @media (min-width: @grid-float-breakpoint) {\n    width: auto;\n    border-top: 0;\n    box-shadow: none;\n\n    &.collapse {\n      display: block !important;\n      height: auto !important;\n      padding-bottom: 0; // Override default setting\n      overflow: visible !important;\n    }\n\n    &.in {\n      overflow-y: visible;\n    }\n\n    // Undo the collapse side padding for navbars with containers to ensure\n    // alignment of right-aligned contents.\n    .navbar-fixed-top &,\n    .navbar-static-top &,\n    .navbar-fixed-bottom & {\n      padding-left: 0;\n      padding-right: 0;\n    }\n  }\n}\n\n\n// Both navbar header and collapse\n//\n// When a container is present, change the behavior of the header and collapse.\n\n.container,\n.container-fluid {\n  > .navbar-header,\n  > .navbar-collapse {\n    margin-right: -@navbar-padding-horizontal;\n    margin-left:  -@navbar-padding-horizontal;\n\n    @media (min-width: @grid-float-breakpoint) {\n      margin-right: 0;\n      margin-left:  0;\n    }\n  }\n}\n\n\n//\n// Navbar alignment options\n//\n// Display the navbar across the entirety of the page or fixed it to the top or\n// bottom of the page.\n\n// Static top (unfixed, but 100% wide) navbar\n.navbar-static-top {\n  z-index: @zindex-navbar;\n  border-width: 0 0 1px;\n\n  @media (min-width: @grid-float-breakpoint) {\n    border-radius: 0;\n  }\n}\n\n// Fix the top/bottom navbars when screen real estate supports it\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n  position: fixed;\n  right: 0;\n  left: 0;\n  z-index: @zindex-navbar-fixed;\n\n  // Undo the rounded corners\n  @media (min-width: @grid-float-breakpoint) {\n    border-radius: 0;\n  }\n}\n.navbar-fixed-top {\n  top: 0;\n  border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n  bottom: 0;\n  margin-bottom: 0; // override .navbar defaults\n  border-width: 1px 0 0;\n}\n\n\n// Brand/project name\n\n.navbar-brand {\n  float: left;\n  padding: @navbar-padding-vertical @navbar-padding-horizontal;\n  font-size: @font-size-large;\n  line-height: @line-height-computed;\n  height: @navbar-height;\n\n  &:hover,\n  &:focus {\n    text-decoration: none;\n  }\n\n  @media (min-width: @grid-float-breakpoint) {\n    .navbar > .container &,\n    .navbar > .container-fluid & {\n      margin-left: -@navbar-padding-horizontal;\n    }\n  }\n}\n\n\n// Navbar toggle\n//\n// Custom button for toggling the `.navbar-collapse`, powered by the collapse\n// JavaScript plugin.\n\n.navbar-toggle {\n  position: relative;\n  float: right;\n  margin-right: @navbar-padding-horizontal;\n  padding: 9px 10px;\n  .navbar-vertical-align(34px);\n  background-color: transparent;\n  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n  border: 1px solid transparent;\n  border-radius: @border-radius-base;\n\n  // We remove the `outline` here, but later compensate by attaching `:hover`\n  // styles to `:focus`.\n  &:focus {\n    outline: none;\n  }\n\n  // Bars\n  .icon-bar {\n    display: block;\n    width: 22px;\n    height: 2px;\n    border-radius: 1px;\n  }\n  .icon-bar + .icon-bar {\n    margin-top: 4px;\n  }\n\n  @media (min-width: @grid-float-breakpoint) {\n    display: none;\n  }\n}\n\n\n// Navbar nav links\n//\n// Builds on top of the `.nav` components with its own modifier class to make\n// the nav the full height of the horizontal nav (above 768px).\n\n.navbar-nav {\n  margin: (@navbar-padding-vertical / 2) -@navbar-padding-horizontal;\n\n  > li > a {\n    padding-top:    10px;\n    padding-bottom: 10px;\n    line-height: @line-height-computed;\n  }\n\n  @media (max-width: @grid-float-breakpoint-max) {\n    // Dropdowns get custom display when collapsed\n    .open .dropdown-menu {\n      position: static;\n      float: none;\n      width: auto;\n      margin-top: 0;\n      background-color: transparent;\n      border: 0;\n      box-shadow: none;\n      > li > a,\n      .dropdown-header {\n        padding: 5px 15px 5px 25px;\n      }\n      > li > a {\n        line-height: @line-height-computed;\n        &:hover,\n        &:focus {\n          background-image: none;\n        }\n      }\n    }\n  }\n\n  // Uncollapse the nav\n  @media (min-width: @grid-float-breakpoint) {\n    float: left;\n    margin: 0;\n\n    > li {\n      float: left;\n      > a {\n        padding-top:    @navbar-padding-vertical;\n        padding-bottom: @navbar-padding-vertical;\n      }\n    }\n\n    &.navbar-right:last-child {\n      margin-right: -@navbar-padding-horizontal;\n    }\n  }\n}\n\n\n// Component alignment\n//\n// Repurpose the pull utilities as their own navbar utilities to avoid specificity\n// issues with parents and chaining. Only do this when the navbar is uncollapsed\n// though so that navbar contents properly stack and align in mobile.\n\n@media (min-width: @grid-float-breakpoint) {\n  .navbar-left  { .pull-left(); }\n  .navbar-right { .pull-right(); }\n}\n\n\n// Navbar form\n//\n// Extension of the `.form-inline` with some extra flavor for optimum display in\n// our navbars.\n\n.navbar-form {\n  margin-left: -@navbar-padding-horizontal;\n  margin-right: -@navbar-padding-horizontal;\n  padding: 10px @navbar-padding-horizontal;\n  border-top: 1px solid transparent;\n  border-bottom: 1px solid transparent;\n  @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);\n  .box-shadow(@shadow);\n\n  // Mixin behavior for optimum display\n  .form-inline();\n\n  .form-group {\n    @media (max-width: @grid-float-breakpoint-max) {\n      margin-bottom: 5px;\n    }\n  }\n\n  // Vertically center in expanded, horizontal navbar\n  .navbar-vertical-align(@input-height-base);\n\n  // Undo 100% width for pull classes\n  @media (min-width: @grid-float-breakpoint) {\n    width: auto;\n    border: 0;\n    margin-left: 0;\n    margin-right: 0;\n    padding-top: 0;\n    padding-bottom: 0;\n    .box-shadow(none);\n\n    // Outdent the form if last child to line up with content down the page\n    &.navbar-right:last-child {\n      margin-right: -@navbar-padding-horizontal;\n    }\n  }\n}\n\n\n// Dropdown menus\n\n// Menu position and menu carets\n.navbar-nav > li > .dropdown-menu {\n  margin-top: 0;\n  .border-top-radius(0);\n}\n// Menu position and menu caret support for dropups via extra dropup class\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n  .border-bottom-radius(0);\n}\n\n\n// Buttons in navbars\n//\n// Vertically center a button within a navbar (when *not* in a form).\n\n.navbar-btn {\n  .navbar-vertical-align(@input-height-base);\n\n  &.btn-sm {\n    .navbar-vertical-align(@input-height-small);\n  }\n  &.btn-xs {\n    .navbar-vertical-align(22);\n  }\n}\n\n\n// Text in navbars\n//\n// Add a class to make any element properly align itself vertically within the navbars.\n\n.navbar-text {\n  .navbar-vertical-align(@line-height-computed);\n\n  @media (min-width: @grid-float-breakpoint) {\n    float: left;\n    margin-left: @navbar-padding-horizontal;\n    margin-right: @navbar-padding-horizontal;\n\n    // Outdent the form if last child to line up with content down the page\n    &.navbar-right:last-child {\n      margin-right: 0;\n    }\n  }\n}\n\n// Alternate navbars\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n  background-color: @navbar-default-bg;\n  border-color: @navbar-default-border;\n\n  .navbar-brand {\n    color: @navbar-default-brand-color;\n    &:hover,\n    &:focus {\n      color: @navbar-default-brand-hover-color;\n      background-color: @navbar-default-brand-hover-bg;\n    }\n  }\n\n  .navbar-text {\n    color: @navbar-default-color;\n  }\n\n  .navbar-nav {\n    > li > a {\n      color: @navbar-default-link-color;\n\n      &:hover,\n      &:focus {\n        color: @navbar-default-link-hover-color;\n        background-color: @navbar-default-link-hover-bg;\n      }\n    }\n    > .active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-default-link-active-color;\n        background-color: @navbar-default-link-active-bg;\n      }\n    }\n    > .disabled > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-default-link-disabled-color;\n        background-color: @navbar-default-link-disabled-bg;\n      }\n    }\n  }\n\n  .navbar-toggle {\n    border-color: @navbar-default-toggle-border-color;\n    &:hover,\n    &:focus {\n      background-color: @navbar-default-toggle-hover-bg;\n    }\n    .icon-bar {\n      background-color: @navbar-default-toggle-icon-bar-bg;\n    }\n  }\n\n  .navbar-collapse,\n  .navbar-form {\n    border-color: @navbar-default-border;\n  }\n\n  // Dropdown menu items\n  .navbar-nav {\n    // Remove background color from open dropdown\n    > .open > a {\n      &,\n      &:hover,\n      &:focus {\n        background-color: @navbar-default-link-active-bg;\n        color: @navbar-default-link-active-color;\n      }\n    }\n\n    @media (max-width: @grid-float-breakpoint-max) {\n      // Dropdowns get custom display when collapsed\n      .open .dropdown-menu {\n        > li > a {\n          color: @navbar-default-link-color;\n          &:hover,\n          &:focus {\n            color: @navbar-default-link-hover-color;\n            background-color: @navbar-default-link-hover-bg;\n          }\n        }\n        > .active > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-default-link-active-color;\n            background-color: @navbar-default-link-active-bg;\n          }\n        }\n        > .disabled > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-default-link-disabled-color;\n            background-color: @navbar-default-link-disabled-bg;\n          }\n        }\n      }\n    }\n  }\n\n\n  // Links in navbars\n  //\n  // Add a class to ensure links outside the navbar nav are colored correctly.\n\n  .navbar-link {\n    color: @navbar-default-link-color;\n    &:hover {\n      color: @navbar-default-link-hover-color;\n    }\n  }\n\n}\n\n// Inverse navbar\n\n.navbar-inverse {\n  background-color: @navbar-inverse-bg;\n  border-color: @navbar-inverse-border;\n\n  .navbar-brand {\n    color: @navbar-inverse-brand-color;\n    &:hover,\n    &:focus {\n      color: @navbar-inverse-brand-hover-color;\n      background-color: @navbar-inverse-brand-hover-bg;\n    }\n  }\n\n  .navbar-text {\n    color: @navbar-inverse-color;\n  }\n\n  .navbar-nav {\n    > li > a {\n      color: @navbar-inverse-link-color;\n\n      &:hover,\n      &:focus {\n        color: @navbar-inverse-link-hover-color;\n        background-color: @navbar-inverse-link-hover-bg;\n      }\n    }\n    > .active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-inverse-link-active-color;\n        background-color: @navbar-inverse-link-active-bg;\n      }\n    }\n    > .disabled > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-inverse-link-disabled-color;\n        background-color: @navbar-inverse-link-disabled-bg;\n      }\n    }\n  }\n\n  // Darken the responsive nav toggle\n  .navbar-toggle {\n    border-color: @navbar-inverse-toggle-border-color;\n    &:hover,\n    &:focus {\n      background-color: @navbar-inverse-toggle-hover-bg;\n    }\n    .icon-bar {\n      background-color: @navbar-inverse-toggle-icon-bar-bg;\n    }\n  }\n\n  .navbar-collapse,\n  .navbar-form {\n    border-color: darken(@navbar-inverse-bg, 7%);\n  }\n\n  // Dropdowns\n  .navbar-nav {\n    > .open > a {\n      &,\n      &:hover,\n      &:focus {\n        background-color: @navbar-inverse-link-active-bg;\n        color: @navbar-inverse-link-active-color;\n      }\n    }\n\n    @media (max-width: @grid-float-breakpoint-max) {\n      // Dropdowns get custom display\n      .open .dropdown-menu {\n        > .dropdown-header {\n          border-color: @navbar-inverse-border;\n        }\n        .divider {\n          background-color: @navbar-inverse-border;\n        }\n        > li > a {\n          color: @navbar-inverse-link-color;\n          &:hover,\n          &:focus {\n            color: @navbar-inverse-link-hover-color;\n            background-color: @navbar-inverse-link-hover-bg;\n          }\n        }\n        > .active > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-inverse-link-active-color;\n            background-color: @navbar-inverse-link-active-bg;\n          }\n        }\n        > .disabled > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-inverse-link-disabled-color;\n            background-color: @navbar-inverse-link-disabled-bg;\n          }\n        }\n      }\n    }\n  }\n\n  .navbar-link {\n    color: @navbar-inverse-link-color;\n    &:hover {\n      color: @navbar-inverse-link-hover-color;\n    }\n  }\n\n}\n","//\n// Utility classes\n// --------------------------------------------------\n\n\n// Floats\n// -------------------------\n\n.clearfix {\n  .clearfix();\n}\n.center-block {\n  .center-block();\n}\n.pull-right {\n  float: right !important;\n}\n.pull-left {\n  float: left !important;\n}\n\n\n// Toggling content\n// -------------------------\n\n// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1\n.hide {\n  display: none !important;\n}\n.show {\n  display: block !important;\n}\n.invisible {\n  visibility: hidden;\n}\n.text-hide {\n  .text-hide();\n}\n\n\n// Hide from screenreaders and browsers\n//\n// Credit: HTML5 Boilerplate\n\n.hidden {\n  display: none !important;\n  visibility: hidden !important;\n}\n\n\n// For Affix plugin\n// -------------------------\n\n.affix {\n  position: fixed;\n}\n","//\n// Breadcrumbs\n// --------------------------------------------------\n\n\n.breadcrumb {\n  padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal;\n  margin-bottom: @line-height-computed;\n  list-style: none;\n  background-color: @breadcrumb-bg;\n  border-radius: @border-radius-base;\n\n  > li {\n    display: inline-block;\n\n    + li:before {\n      content: \"@{breadcrumb-separator}\\00a0\"; // Unicode space added since inline-block means non-collapsing white-space\n      padding: 0 5px;\n      color: @breadcrumb-color;\n    }\n  }\n\n  > .active {\n    color: @breadcrumb-active-color;\n  }\n}\n","//\n// Pagination (multiple pages)\n// --------------------------------------------------\n.pagination {\n  display: inline-block;\n  padding-left: 0;\n  margin: @line-height-computed 0;\n  border-radius: @border-radius-base;\n\n  > li {\n    display: inline; // Remove list-style and block-level defaults\n    > a,\n    > span {\n      position: relative;\n      float: left; // Collapse white-space\n      padding: @padding-base-vertical @padding-base-horizontal;\n      line-height: @line-height-base;\n      text-decoration: none;\n      color: @pagination-color;\n      background-color: @pagination-bg;\n      border: 1px solid @pagination-border;\n      margin-left: -1px;\n    }\n    &:first-child {\n      > a,\n      > span {\n        margin-left: 0;\n        .border-left-radius(@border-radius-base);\n      }\n    }\n    &:last-child {\n      > a,\n      > span {\n        .border-right-radius(@border-radius-base);\n      }\n    }\n  }\n\n  > li > a,\n  > li > span {\n    &:hover,\n    &:focus {\n      color: @pagination-hover-color;\n      background-color: @pagination-hover-bg;\n      border-color: @pagination-hover-border;\n    }\n  }\n\n  > .active > a,\n  > .active > span {\n    &,\n    &:hover,\n    &:focus {\n      z-index: 2;\n      color: @pagination-active-color;\n      background-color: @pagination-active-bg;\n      border-color: @pagination-active-border;\n      cursor: default;\n    }\n  }\n\n  > .disabled {\n    > span,\n    > span:hover,\n    > span:focus,\n    > a,\n    > a:hover,\n    > a:focus {\n      color: @pagination-disabled-color;\n      background-color: @pagination-disabled-bg;\n      border-color: @pagination-disabled-border;\n      cursor: not-allowed;\n    }\n  }\n}\n\n// Sizing\n// --------------------------------------------------\n\n// Large\n.pagination-lg {\n  .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @border-radius-large);\n}\n\n// Small\n.pagination-sm {\n  .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @border-radius-small);\n}\n","//\n// Pager pagination\n// --------------------------------------------------\n\n\n.pager {\n  padding-left: 0;\n  margin: @line-height-computed 0;\n  list-style: none;\n  text-align: center;\n  &:extend(.clearfix all);\n  li {\n    display: inline;\n    > a,\n    > span {\n      display: inline-block;\n      padding: 5px 14px;\n      background-color: @pager-bg;\n      border: 1px solid @pager-border;\n      border-radius: @pager-border-radius;\n    }\n\n    > a:hover,\n    > a:focus {\n      text-decoration: none;\n      background-color: @pager-hover-bg;\n    }\n  }\n\n  .next {\n    > a,\n    > span {\n      float: right;\n    }\n  }\n\n  .previous {\n    > a,\n    > span {\n      float: left;\n    }\n  }\n\n  .disabled {\n    > a,\n    > a:hover,\n    > a:focus,\n    > span {\n      color: @pager-disabled-color;\n      background-color: @pager-bg;\n      cursor: not-allowed;\n    }\n  }\n\n}\n","//\n// Labels\n// --------------------------------------------------\n\n.label {\n  display: inline;\n  padding: .2em .6em .3em;\n  font-size: 75%;\n  font-weight: bold;\n  line-height: 1;\n  color: @label-color;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: baseline;\n  border-radius: .25em;\n\n  // Add hover effects, but only for links\n  &[href] {\n    &:hover,\n    &:focus {\n      color: @label-link-hover-color;\n      text-decoration: none;\n      cursor: pointer;\n    }\n  }\n\n  // Empty labels collapse automatically (not available in IE8)\n  &:empty {\n    display: none;\n  }\n\n  // Quick fix for labels in buttons\n  .btn & {\n    position: relative;\n    top: -1px;\n  }\n}\n\n// Colors\n// Contextual variations (linked labels get darker on :hover)\n\n.label-default {\n  .label-variant(@label-default-bg);\n}\n\n.label-primary {\n  .label-variant(@label-primary-bg);\n}\n\n.label-success {\n  .label-variant(@label-success-bg);\n}\n\n.label-info {\n  .label-variant(@label-info-bg);\n}\n\n.label-warning {\n  .label-variant(@label-warning-bg);\n}\n\n.label-danger {\n  .label-variant(@label-danger-bg);\n}\n","//\n// Badges\n// --------------------------------------------------\n\n\n// Base classes\n.badge {\n  display: inline-block;\n  min-width: 10px;\n  padding: 3px 7px;\n  font-size: @font-size-small;\n  font-weight: @badge-font-weight;\n  color: @badge-color;\n  line-height: @badge-line-height;\n  vertical-align: baseline;\n  white-space: nowrap;\n  text-align: center;\n  background-color: @badge-bg;\n  border-radius: @badge-border-radius;\n\n  // Empty badges collapse automatically (not available in IE8)\n  &:empty {\n    display: none;\n  }\n\n  // Quick fix for badges in buttons\n  .btn & {\n    position: relative;\n    top: -1px;\n  }\n  .btn-xs & {\n    top: 0;\n    padding: 1px 5px;\n  }\n}\n\n// Hover state, but only for links\na.badge {\n  &:hover,\n  &:focus {\n    color: @badge-link-hover-color;\n    text-decoration: none;\n    cursor: pointer;\n  }\n}\n\n// Account for counters in navs\na.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n  color: @badge-active-color;\n  background-color: @badge-active-bg;\n}\n.nav-pills > li > a > .badge {\n  margin-left: 3px;\n}\n","//\n// Jumbotron\n// --------------------------------------------------\n\n\n.jumbotron {\n  padding: @jumbotron-padding;\n  margin-bottom: @jumbotron-padding;\n  color: @jumbotron-color;\n  background-color: @jumbotron-bg;\n\n  h1,\n  .h1 {\n    color: @jumbotron-heading-color;\n  }\n  p {\n    margin-bottom: (@jumbotron-padding / 2);\n    font-size: @jumbotron-font-size;\n    font-weight: 200;\n  }\n\n  .container & {\n    border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container\n  }\n\n  .container {\n    max-width: 100%;\n  }\n\n  @media screen and (min-width: @screen-sm-min) {\n    padding-top:    (@jumbotron-padding * 1.6);\n    padding-bottom: (@jumbotron-padding * 1.6);\n\n    .container & {\n      padding-left:  (@jumbotron-padding * 2);\n      padding-right: (@jumbotron-padding * 2);\n    }\n\n    h1,\n    .h1 {\n      font-size: (@font-size-base * 4.5);\n    }\n  }\n}\n","//\n// Alerts\n// --------------------------------------------------\n\n\n// Base styles\n// -------------------------\n\n.alert {\n  padding: @alert-padding;\n  margin-bottom: @line-height-computed;\n  border: 1px solid transparent;\n  border-radius: @alert-border-radius;\n\n  // Headings for larger alerts\n  h4 {\n    margin-top: 0;\n    // Specified for the h4 to prevent conflicts of changing @headings-color\n    color: inherit;\n  }\n  // Provide class for links that match alerts\n  .alert-link {\n    font-weight: @alert-link-font-weight;\n  }\n\n  // Improve alignment and spacing of inner content\n  > p,\n  > ul {\n    margin-bottom: 0;\n  }\n  > p + p {\n    margin-top: 5px;\n  }\n}\n\n// Dismissable alerts\n//\n// Expand the right padding and account for the close button's positioning.\n\n.alert-dismissable {\n padding-right: (@alert-padding + 20);\n\n  // Adjust close link position\n  .close {\n    position: relative;\n    top: -2px;\n    right: -21px;\n    color: inherit;\n  }\n}\n\n// Alternate styles\n//\n// Generate contextual modifier classes for colorizing the alert.\n\n.alert-success {\n  .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text);\n}\n.alert-info {\n  .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text);\n}\n.alert-warning {\n  .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text);\n}\n.alert-danger {\n  .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);\n}\n","//\n// Progress bars\n// --------------------------------------------------\n\n\n// Bar animations\n// -------------------------\n\n// WebKit\n@-webkit-keyframes progress-bar-stripes {\n  from  { background-position: 40px 0; }\n  to    { background-position: 0 0; }\n}\n\n// Spec and IE10+\n@keyframes progress-bar-stripes {\n  from  { background-position: 40px 0; }\n  to    { background-position: 0 0; }\n}\n\n\n\n// Bar itself\n// -------------------------\n\n// Outer container\n.progress {\n  overflow: hidden;\n  height: @line-height-computed;\n  margin-bottom: @line-height-computed;\n  background-color: @progress-bg;\n  border-radius: @border-radius-base;\n  .box-shadow(inset 0 1px 2px rgba(0,0,0,.1));\n}\n\n// Bar of progress\n.progress-bar {\n  float: left;\n  width: 0%;\n  height: 100%;\n  font-size: @font-size-small;\n  line-height: @line-height-computed;\n  color: @progress-bar-color;\n  text-align: center;\n  background-color: @progress-bar-bg;\n  .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));\n  .transition(width .6s ease);\n}\n\n// Striped bars\n.progress-striped .progress-bar {\n  #gradient > .striped();\n  background-size: 40px 40px;\n}\n\n// Call animation for the active one\n.progress.active .progress-bar {\n  .animation(progress-bar-stripes 2s linear infinite);\n}\n\n\n\n// Variations\n// -------------------------\n\n.progress-bar-success {\n  .progress-bar-variant(@progress-bar-success-bg);\n}\n\n.progress-bar-info {\n  .progress-bar-variant(@progress-bar-info-bg);\n}\n\n.progress-bar-warning {\n  .progress-bar-variant(@progress-bar-warning-bg);\n}\n\n.progress-bar-danger {\n  .progress-bar-variant(@progress-bar-danger-bg);\n}\n","// Media objects\n// Source: http://stubbornella.org/content/?p=497\n// --------------------------------------------------\n\n\n// Common styles\n// -------------------------\n\n// Clear the floats\n.media,\n.media-body {\n  overflow: hidden;\n  zoom: 1;\n}\n\n// Proper spacing between instances of .media\n.media,\n.media .media {\n  margin-top: 15px;\n}\n.media:first-child {\n  margin-top: 0;\n}\n\n// For images and videos, set to block\n.media-object {\n  display: block;\n}\n\n// Reset margins on headings for tighter default spacing\n.media-heading {\n  margin: 0 0 5px;\n}\n\n\n// Media image alignment\n// -------------------------\n\n.media {\n  > .pull-left {\n    margin-right: 10px;\n  }\n  > .pull-right {\n    margin-left: 10px;\n  }\n}\n\n\n// Media list variation\n// -------------------------\n\n// Undo default ul/ol styles\n.media-list {\n  padding-left: 0;\n  list-style: none;\n}\n","//\n// List groups\n// --------------------------------------------------\n\n\n// Base class\n//\n// Easily usable on 
    diff --git a/app/static/js/sco_ue_external.js b/app/static/js/sco_ue_external.js index fb57807f4..e63815550 100644 --- a/app/static/js/sco_ue_external.js +++ b/app/static/js/sco_ue_external.js @@ -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); } - - - - diff --git a/app/static/js/scolar_index.js b/app/static/js/scolar_index.js index 90c20b45f..9b75eb0df 100644 --- a/app/static/js/scolar_index.js +++ b/app/static/js/scolar_index.js @@ -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); + } }); - - diff --git a/app/static/js/table_editor.js b/app/static/js/table_editor.js index 603e0792b..18b4bd686 100644 --- a/app/static/js/table_editor.js +++ b/app/static/js/table_editor.js @@ -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 += ` -
    { + output += ` +
    ${cellule.data}
    `; // 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 += ` -
    { + output += ` +
    ${value / 100}
    `; - }) + }); - Object.entries(sumsRessources).forEach(([num, value]) => { - output += ` -
    { + output += ` +
    ${value / 100}
    `; - }) + }); - 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); } diff --git a/app/static/js/trombino.js b/app/static/js/trombino.js index c6a560bc7..f6bf989db 100644 --- a/app/static/js/trombino.js +++ b/app/static/js/trombino.js @@ -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); + } }); - diff --git a/app/static/js/ue_list.js b/app/static/js/ue_list.js index f6e9ea757..35e79ac61 100644 --- a/app/static/js/ue_list.js +++ b/app/static/js/ue_list.js @@ -1,5 +1,5 @@ // Edition elements programme "en place" -$(function() { - $('.span_apo_edit').jinplace(); -}); \ No newline at end of file +$(function () { + $(".span_apo_edit").jinplace(); +}); diff --git a/app/static/js/user_form.js b/app/static/js/user_form.js index 4ad06672a..c92617903 100644 --- a/app/static/js/user_form.js +++ b/app/static/js/user_form.js @@ -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(); +}); diff --git a/sco_version.py b/sco_version.py index 5be4f7743..67d9697b6 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.55" +SCOVERSION = "9.6.56" SCONAME = "ScoDoc" From ce3452df73ddaeb95a439f5edfdabb9ddea07637 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 17 Nov 2023 17:00:15 +0100 Subject: [PATCH 56/69] Fix: groups_view tabs html --- app/scodoc/sco_groups_view.py | 83 ++++++++++++++++------------------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index b04741349..0e4b8ac23 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -106,57 +106,52 @@ 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("""
    """) - 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( - ( - """ + return f""" + { html_sco_header.sco_header( + javascripts=JAVASCRIPTS, + cssstyles=CSSSTYLES, + init_qtip=True, + ) + } +
    + + {form_groups_choice(groups_infos, submit_on_change=True)} +
    -
    - """, - 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, - ), - "
    ", - """
    """, - tab_photos_html(groups_infos, etat=etat), - #'

    hello

    ', - "
    ", - '
    ', - tab_absences_html(groups_infos, etat=etat), - "
    ", - ) - ) - - H.append(html_sco_header.sco_footer()) - return "\n".join(H) +
    + { + 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, + ) + } +
    +
    + { tab_photos_html(groups_infos, etat=etat) } +
    +
    + { tab_absences_html(groups_infos, etat=etat) } +
    +
    + { html_sco_header.sco_footer() } + """ def form_groups_choice( @@ -751,7 +746,7 @@ def groups_table( H.append("") - return "".join(H) + "
    " + return "".join(H) elif ( fmt == "pdf" From 093ab253f33ac2d950b014ff945f8e4508b06e05 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 19 Nov 2023 22:06:36 +0100 Subject: [PATCH 57/69] =?UTF-8?q?Am=C3=A9liorer=20=C3=A9dition=20et=20clon?= =?UTF-8?q?age=20des=20formations.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/formsemestre.py | 27 +++++++++++++++++++++++++++ app/scodoc/sco_edit_formation.py | 27 +++++++++++++++++---------- app/scodoc/sco_formations.py | 12 ++++++++++-- app/static/css/scodoc.css | 5 ++++- sco_version.py | 2 +- 5 files changed, 59 insertions(+), 14 deletions(-) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index b44498007..9aa6cdd4b 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -1040,6 +1040,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( diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index 2327071ba..0857656df 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -80,7 +80,7 @@ def formation_delete(formation_id=None, dialog_confirmed=False): f"""

    Confirmer la suppression de la formation {formation.titre} ({formation.acronyme}) ?

    -

    Attention: la suppression d'une formation est irréversible +

    Attention: la suppression d'une formation est irréversible et implique la supression de toutes les UE, matières et modules de la formation !

    """, @@ -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à une formation 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): diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index dbd864f53..d18109871 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -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(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", diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index da2dfe36d..0f9072c81 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -2319,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 { diff --git a/sco_version.py b/sco_version.py index 67d9697b6..26bbad3c4 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.56" +SCOVERSION = "9.6.57" SCONAME = "ScoDoc" From 77c9a48d0219f543c558b6fe358bb29d452c9ebe Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 19 Nov 2023 22:35:04 +0100 Subject: [PATCH 58/69] =?UTF-8?q?EDT:=20am=C3=A9liore=20apparence=20+=20ac?= =?UTF-8?q?cepote=20plusieurs=20codes=20edt=20par=20modimpl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/formsemestre.py | 8 ++-- app/models/moduleimpls.py | 10 ++--- app/models/modules.py | 10 ++--- app/scodoc/sco_edt_cal.py | 85 ++++++++++++++++++++++++-------------- app/scodoc/sco_utils.py | 12 +++++- app/static/css/edt.css | 9 ++++ 6 files changed, 86 insertions(+), 48 deletions(-) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 9aa6cdd4b..6bfbbdbb2 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -269,10 +269,12 @@ class FormSemestre(db.Model): return default_partition.groups.first() raise ScoValueError("Le semestre n'a pas de groupe par défaut") - def get_edt_id(self) -> str: - "l'id pour l'emploi du temps: à défaut, le 1er code étape Apogée" + def get_edt_ids(self) -> list[str]: + "l'ids pour l'emploi du temps: à défaut, les codes étape Apogée" return ( - self.edt_id or "" or (self.etapes[0].etape_apo if len(self.etapes) else "") + 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: diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 468888e6a..77eba7da8 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -58,12 +58,12 @@ class ModuleImpl(db.Model): return {x.strip() for x in self.code_apogee.split(",") if x} return self.module.get_codes_apogee() - def get_edt_id(self) -> str: - "l'id pour l'emploi du temps: à défaut, le 1er code Apogée" + def get_edt_ids(self) -> list[str]: + "les ids pour l'emploi du temps: à défaut, les codes Apogée" return ( - self.edt_id - or (self.code_apogee.split(",")[0] if self.code_apogee else "") - or self.module.get_edt_id() + 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: diff --git a/app/models/modules.py b/app/models/modules.py index 5a96de3a9..891a476dc 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -285,13 +285,9 @@ class Module(db.Model): return {x.strip() for x in self.code_apogee.split(",") if x} return set() - def get_edt_id(self) -> str: - "l'id pour l'emploi du temps: à défaut, le 1er code Apogée" - return ( - self.edt_id - or (self.code_apogee.split(",")[0] if self.code_apogee else "") - or "" - ) + 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. diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index 9bfaebf6c..7aebfce7f 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -34,7 +34,7 @@ from datetime import timezone import re import icalendar -from flask import flash, g, url_for +from flask import g, url_for from app import log from app.models import FormSemestre, GroupDescr, ModuleImpl, ScoDocSiteConfig from app.scodoc.sco_exceptions import ScoValueError @@ -56,12 +56,14 @@ def formsemestre_load_calendar( Raises ScoValueError if not configured or not available or invalid format. """ if edt_id is None and formsemestre: - edt_id = formsemestre.get_edt_id() - if not edt_id: + 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)" ) - ics_filename = get_ics_filename(edt_id) + # 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: @@ -147,40 +149,51 @@ def formsemestre_edt_dict( if group and group_ids_set and group.id not in group_ids_set: continue # ignore cet évènement modimpl: ModuleImpl | bool = event["modimpl"] - if modimpl is False: - mod_disp = f"""
    - {scu.EMO_WARNING} non configuré -
    """ - else: - mod_disp = ( - f"""
    { - modimpl.module.code}
    """ - if modimpl - else f"""
    { - scu.EMO_WARNING} {event['edt_module']}
    """ + 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"""{scu.EMO_WARNING} non configuré""" + bubble = "extraction emploi du temps non configurée" + case None: # Module edt non trouvé dans ScoDoc + mod_disp = f"""{ + scu.EMO_WARNING} {event['edt_module']}""" + bubble = "code module non trouvé dans ScoDoc. Vérifier configuration." + case _: # module EDT bien retrouvé dans ScoDoc + mod_disp = f"""{ + modimpl.module.code}""" + bubble = f"{modimpl.module.abbrev or ''} ({event['edt_module']})" + + title = f""" + """ + # --- Lien saisie abs link_abs = ( f"""""" - if modimpl and group + if url_abs else "" ) d = { # Champs utilisés par tui.calendar "calendarId": "cal1", - "title": event["title"] + group_disp + mod_disp + link_abs, + "title": f"""{title} {group_disp} {link_abs}""", "start": event["start"], "end": event["end"], "backgroundColor": event["group_bg_color"], @@ -245,11 +258,13 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]: for event in events: if "DESCRIPTION" in event: # --- Titre de l'évènement - title = ( + 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( @@ -278,6 +293,8 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]: 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 = "" @@ -285,7 +302,8 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]: # events_sco.append( { - "title": title, + "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 @@ -376,8 +394,11 @@ def formsemestre_retreive_modimpls_from_edt_id( formsemestre: FormSemestre, ) -> dict[str, ModuleImpl]: """Construit un dict donnant le moduleimpl de chaque edt_id""" - edt2modimpl = {modimpl.get_edt_id(): modimpl for modimpl in formsemestre.modimpls} - edt2modimpl.pop("", None) + edt2modimpl = {} + for modimpl in formsemestre.modimpls: + for edt_id in modimpl.get_edt_ids(): + if edt_id: + edt2modimpl[edt_id] = modimpl return edt2modimpl diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 6e04677dc..1523c8e82 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -764,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: diff --git a/app/static/css/edt.css b/app/static/css/edt.css index f97c91e85..e60408299 100644 --- a/app/static/css/edt.css +++ b/app/static/css/edt.css @@ -6,8 +6,17 @@ 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; From 988f577f6eb6e944247ecd0fcdea8f24ee51bab5 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 20 Nov 2023 11:23:40 +0100 Subject: [PATCH 59/69] Fix formation_import_xml (typo) --- app/scodoc/sco_formations.py | 6 +++--- sco_version.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index d18109871..e17b4945a 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -322,13 +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 "" + 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( dept_id=f_dict["dept_id"] ).filter( db.func.lower(Formation.acronyme) == acronyme_lower, - db.func(Formation.titre) == titre_lower, + db.func.lower(Formation.titre) == titre_lower, ) if formations.count(): version = max(f.version or 0 for f in formations) diff --git a/sco_version.py b/sco_version.py index 26bbad3c4..e8a195ffc 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.57" +SCOVERSION = "9.6.58" SCONAME = "ScoDoc" From 441a893f12609f28f70942f301e07381878ad787 Mon Sep 17 00:00:00 2001 From: Iziram Date: Mon, 20 Nov 2023 11:01:22 +0100 Subject: [PATCH 60/69] Assiduites : fix delete justificatif en double --- app/scodoc/sco_archives_justificatifs.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index 125569d01..60c3bd8db 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -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): From 552095b979963f2c059013a8cbee6c4b6706a78b Mon Sep 17 00:00:00 2001 From: Iziram Date: Mon, 20 Nov 2023 16:55:26 +0100 Subject: [PATCH 61/69] Assiduites : dates naives front-end --- app/scodoc/html_sco_header.py | 1 + app/static/js/assiduites.js | 126 +++++++++--------- app/static/js/date_utils.js | 31 ++++- .../assiduites/pages/ajout_justificatif.j2 | 6 +- app/templates/assiduites/pages/bilan_dept.j2 | 2 +- app/templates/assiduites/pages/bilan_etud.j2 | 4 +- app/templates/assiduites/pages/calendrier.j2 | 9 +- .../pages/signal_assiduites_diff.j2 | 4 +- .../pages/signal_assiduites_etud.j2 | 14 +- app/templates/assiduites/widgets/conflict.j2 | 8 +- app/templates/assiduites/widgets/differee.j2 | 16 ++- .../assiduites/widgets/minitimeline.j2 | 16 +-- .../widgets/moduleimpl_dynamic_selector.j2 | 4 +- .../assiduites/widgets/tableau_assi.j2 | 10 +- .../assiduites/widgets/tableau_base.j2 | 6 +- .../assiduites/widgets/tableau_justi.j2 | 18 +-- app/templates/base.j2 | 5 +- 17 files changed, 153 insertions(+), 127 deletions(-) diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py index 7b8466977..1b9e45194 100644 --- a/app/scodoc/html_sco_header.py +++ b/app/scodoc/html_sco_header.py @@ -213,6 +213,7 @@ def sco_header( window.onload=function(){{enableTooltips("gtrcontent")}}; const SCO_URL="{scu.ScoURL()}"; + const SCO_TIMEZONE="{scu.TIME_ZONE}"; """ ) diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index fd4892b41..aadd04cae 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -305,8 +305,8 @@ function executeMassActionQueue() { */ const tlTimes = getTimeLineTimes(); let assiduite = { - date_debut: tlTimes.deb.toIsoUtcString(), - date_fin: tlTimes.fin.toIsoUtcString(), + date_debut: tlTimes.deb.toFakeIso(), + date_fin: tlTimes.fin.toFakeIso(), }; assiduite = setModuleImplId(assiduite); @@ -601,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); } /** @@ -610,19 +613,25 @@ function formatDate(date, styles = { dateStyle: "full" }) { function updateDate() { const dateInput = document.querySelector("#tl_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 (!isNonWorkDay(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); @@ -630,22 +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"); - const date_fra = Date.toFRA(lastWorkDay.toIsoUtcString().split("T")[0]); $(dateInput).datepicker("setDate", date_fra); dateInput.value = date_fra; date = lastWorkDay; - dateStr = formatDate(lastWorkDay).capitalize(); + dateStr = formatDate(lastWorkDay, { + dateStyle: "full", + timeZone: SCO_TIMEZONE, + }).capitalize(); } - console.warn(dateStr, date, date.toIsoUtcString()); - document.querySelector("#datestr").textContent = dateStr; return true; } @@ -654,7 +664,7 @@ function getNearestWorkDay(date) { const aDay = 86400000; // 24 * 3600 * 1000 | H * s * ms let day = date; let count = 0; - while (isNonWorkDay(day.getDay(), nonWorkDays) && count++ < 7) { + while (isNonWorkDay(day, nonWorkDays) && count++ < 7) { day = new Date(day - aDay); } return day; @@ -712,31 +722,12 @@ function formatDateModal(str, separator = " ") { * Renvoie Vrai si le jour est non travaillé */ function isNonWorkDay(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; - } - + const d = Intl.DateTimeFormat("fr-FR", { + timeZone: SCO_TIMEZONE, + weekday: "short", + }) + .format(day) + .replace(".", ""); return nonWorkdays.indexOf(d) != -1; } @@ -783,8 +774,8 @@ function getTimeLineTimes() { function isConflictSameAsPeriod(conflict, period = undefined) { const tlTimes = period == undefined ? getTimeLineTimes() : period; const clTimes = { - deb: new Date(conflict.date_debut), - fin: new Date(conflict.date_fin), + 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); } @@ -850,8 +841,8 @@ function numberTimeToDate(nb) { function getAssiduitesFromEtuds(clear, deb, fin) { const etudIds = Object.keys(etuds).join(","); - const date_debut = deb ? deb : getPrevDate().toIsoUtcString(); - const date_fin = fin ? fin : getNextDate().toIsoUtcString(); + const date_debut = deb ? deb : getPrevDate().toFakeIso(); + const date_fin = fin ? fin : getNextDate().toFakeIso(); if (clear) { assiduites = {}; @@ -894,8 +885,8 @@ function getAssiduitesFromEtuds(clear, deb, fin) { function createAssiduite(etat, etudid) { const tlTimes = getTimeLineTimes(); let assiduite = { - date_debut: tlTimes.deb.toIsoUtcString(), - date_fin: tlTimes.fin.toIsoUtcString(), + date_debut: tlTimes.deb.toFakeIso(), + date_fin: tlTimes.fin.toFakeIso(), etat: etat, }; @@ -937,6 +928,19 @@ 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 = ` +

    Attention, l'étudiant n'est pas inscrit à ce module.

    +

    Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.

    + `; + + const content = document.createElement("div"); + content.innerHTML = HTML; + + openAlertModal("Sélection du module", content); + } with_errors = true; } }, @@ -1076,8 +1080,8 @@ function getAssiduitesConflict(etudid, periode) { return etudAssiduites.filter((assi) => { const interval = { - deb: new Date(assi.date_debut), - fin: new Date(assi.date_fin), + deb: new Date(Date.removeUTC(assi.date_debut)), + fin: new Date(Date.removeUTC(assi.date_fin)), }; const test = hasTimeConflict(periode, interval); return test; @@ -1101,15 +1105,15 @@ function getLastAssiduiteOfPrevDate(etudid) { const prevAssiduites = etudAssiduites .filter((assi) => { const interval = { - deb: new Date(assi.date_debut), - fin: new Date(assi.date_fin), + 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 = new Date(a.date_fin); - const b_fin = new Date(b.date_fin); + 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; }); @@ -1144,8 +1148,8 @@ function getAssiduiteValue(field) { * @param {String | Number} etudid identifiant de l'étudiant */ function actualizeEtudAssiduite(etudid) { - const date_debut = getPrevDate().toIsoUtcString(); - const date_fin = getNextDate().toIsoUtcString(); + const date_debut = getPrevDate().toFakeIso(); + const date_fin = getNextDate().toFakeIso(); const url_api = getUrl() + @@ -1391,8 +1395,8 @@ function insertEtudRow(etud, index, output = false) { 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 { @@ -1623,9 +1627,7 @@ function getJustificatifFromPeriod(date, etudid, update) { getUrl() + `/api/justificatifs/${etudid}/query?date_debut=${date.deb .add(1, "seconds") - .toIsoUtcString()}&date_fin=${date.fin - .add(-1, "seconds") - .toIsoUtcString()}`, + .toFakeIso()}&date_fin=${date.fin.add(-1, "seconds").toFakeIso()}`, success: (data) => { update(data); }, @@ -1657,8 +1659,8 @@ function fastJustify(assiduite) { } const period = { - deb: new Date(assiduite.date_debut), - fin: new Date(assiduite.date_fin), + deb: new Date(Date.removeUTC(assiduite.date_debut)), + fin: new Date(Date.removeUTC(assiduite.date_fin)), }; const action = (justifs) => { //créer un nouveau justificatif @@ -1671,8 +1673,8 @@ function fastJustify(assiduite) { //créer justificatif const justif = { - date_debut: new Date(assiduite.date_debut).toIsoUtcString(), - date_fin: new Date(assiduite.date_fin).toIsoUtcString(), + date_debut: new Date(Date.removeUTC(assiduite.date_debut)).toFakeIso(), + date_fin: new Date(Date.removeUTC(assiduite.date_fin)).toFakeIso(), raison: raison, etat: etat, }; diff --git a/app/static/js/date_utils.js b/app/static/js/date_utils.js index ee9ed1503..05babeede 100644 --- a/app/static/js/date_utils.js +++ b/app/static/js/date_utils.js @@ -59,6 +59,11 @@ 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()); @@ -198,13 +203,29 @@ 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() + "-" + @@ -216,11 +237,7 @@ Object.defineProperty(Date.prototype, "toIsoUtcString", { ":" + pad(date.getMinutes()) + ":" + - pad(date.getSeconds()) + - dif + - pad(Math.floor(Math.abs(tzo) / 60)) + - ":" + - pad(Math.abs(tzo) % 60) + pad(date.getSeconds()) ); }, }); @@ -246,6 +263,7 @@ Object.defineProperty(Date.prototype, "format", { hour: "2-digit", minute: "2-digit", hour12: false, + timeZone: SCO_TIMEZONE, }); case "DD/MM/YYYY HH:mm": return this.toLocaleString("fr-FR", { @@ -255,6 +273,7 @@ Object.defineProperty(Date.prototype, "format", { hour: "2-digit", minute: "2-digit", hour12: false, + timeZone: SCO_TIMEZONE, }); case "YYYY-MM-DDTHH:mm": @@ -264,7 +283,7 @@ Object.defineProperty(Date.prototype, "format", { case "YYYY-MM-DD": return iso.slice(0, iso.indexOf("T")); default: - return this.toIsoUtcString(); + return this.toFakeIso(); } }, }); diff --git a/app/templates/assiduites/pages/ajout_justificatif.j2 b/app/templates/assiduites/pages/ajout_justificatif.j2 index 445379ca4..fac1b11ed 100644 --- a/app/templates/assiduites/pages/ajout_justificatif.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif.j2 @@ -103,7 +103,7 @@ [required]::after { content: "*"; - color: crimson; + color: var(--color-error); } -{% endblock %} +{% endblock %} \ No newline at end of file From 77388e2d874b0d44acd29c05d5468e9063d943b7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 20 Nov 2023 17:57:10 +0100 Subject: [PATCH 62/69] Add check in bul_head --- app/templates/bul_head.j2 | 12 ++++++++---- sco_version.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/templates/bul_head.j2 b/app/templates/bul_head.j2 index 91acfb3d9..8c725f012 100644 --- a/app/templates/bul_head.j2 +++ b/app/templates/bul_head.j2 @@ -18,8 +18,12 @@ Bulletin {{formsemestre.html_link_status() | safe}} - {% if formsemestre.etuds_inscriptions[etud.id].parcour %} - Parcours {{formsemestre.etuds_inscriptions[etud.id].parcour.code}} + {% if etud.id in formsemestre.etuds_inscriptions %} + {% if formsemestre.etuds_inscriptions[etud.id].parcour %} + Parcours {{formsemestre.etuds_inscriptions[etud.id].parcour.code}} + {% endif %} + {% else %} + {{scu.EMO_WARNING|safe}} non inscrit !? {% endif %}
    @@ -67,8 +71,8 @@ )}}">version courte spéciale BUT {% endif %} visualiser les compétences BUT
    diff --git a/sco_version.py b/sco_version.py index e8a195ffc..12c81cd07 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.58" +SCOVERSION = "9.6.59" SCONAME = "ScoDoc" From 6bc4d6dbb49cd293efdf756d696330673798bb2a Mon Sep 17 00:00:00 2001 From: Iziram Date: Mon, 20 Nov 2023 20:59:40 +0100 Subject: [PATCH 63/69] Assiduites : bugfix conflict fakeiso --- app/templates/assiduites/widgets/conflict.j2 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/templates/assiduites/widgets/conflict.j2 b/app/templates/assiduites/widgets/conflict.j2 index 15cf64440..987cdfb21 100644 --- a/app/templates/assiduites/widgets/conflict.j2 +++ b/app/templates/assiduites/widgets/conflict.j2 @@ -270,14 +270,14 @@ ) { const assiduite_avant = { etat: this.selectedAssiduite.etat, - date_debut: assiduite_debut.toFakeIso()(), - date_fin: separtorDate.toFakeIso()(), + date_debut: assiduite_debut.toFakeIso(), + date_fin: separtorDate.toFakeIso(), }; const assiduite_apres = { etat: this.selectedAssiduite.etat, - date_debut: separtorDate.toFakeIso()(), - date_fin: assiduite_fin.toFakeIso()(), + date_debut: separtorDate.toFakeIso(), + date_fin: assiduite_fin.toFakeIso(), }; if (this.selectedAssiduite.moduleimpl_id) { From 7672c3e3c5f62ae514fc5f2ebbf0c9633ccae098 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 20 Nov 2023 17:57:10 +0100 Subject: [PATCH 64/69] Add check in bul_head --- app/templates/bul_head.j2 | 12 ++++++++---- sco_version.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/templates/bul_head.j2 b/app/templates/bul_head.j2 index 91acfb3d9..8c725f012 100644 --- a/app/templates/bul_head.j2 +++ b/app/templates/bul_head.j2 @@ -18,8 +18,12 @@ Bulletin {{formsemestre.html_link_status() | safe}} - {% if formsemestre.etuds_inscriptions[etud.id].parcour %} - Parcours {{formsemestre.etuds_inscriptions[etud.id].parcour.code}} + {% if etud.id in formsemestre.etuds_inscriptions %} + {% if formsemestre.etuds_inscriptions[etud.id].parcour %} + Parcours {{formsemestre.etuds_inscriptions[etud.id].parcour.code}} + {% endif %} + {% else %} + {{scu.EMO_WARNING|safe}} non inscrit !? {% endif %}
    @@ -67,8 +71,8 @@ )}}">version courte spéciale BUT {% endif %} visualiser les compétences BUT
    diff --git a/sco_version.py b/sco_version.py index e8a195ffc..12c81cd07 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.58" +SCOVERSION = "9.6.59" SCONAME = "ScoDoc" From 58c5d61648a643d07b57a66fe18dfc5213b5c4e2 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 21 Nov 2023 11:31:22 +0100 Subject: [PATCH 65/69] EDT: plusieurs edt_id de groupes par groupe scodoc --- app/models/groups.py | 6 +++--- app/scodoc/sco_edt_cal.py | 5 +++-- app/scodoc/sco_groups_edit.py | 3 ++- sco_version.py | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/models/groups.py b/app/models/groups.py index 8e8791ca0..72a54acf6 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -247,9 +247,9 @@ class GroupDescr(db.Model): d["partition"] = self.partition.to_dict(with_groups=False) return d - def get_edt_id(self) -> str: - "l'id pour l'emploi du temps: à défaut, le nom scodoc du groupe" - return self.edt_id or self.group_name or "" + 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. diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index 7aebfce7f..7f60dda93 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -408,6 +408,7 @@ def formsemestre_retreive_groups_from_edt_id( """Construit un dict donnant le groupe de chaque edt_id""" edt2group = {} for partition in formsemestre.partitions: - edt2group.update({g.get_edt_id(): g for g in partition.groups}) - edt2group.pop("", None) + for g in partition.groups: + for edt_id in g.get_edt_ids(): + edt2group[edt_id] = g return edt2group diff --git a/app/scodoc/sco_groups_edit.py b/app/scodoc/sco_groups_edit.py index f30d2ab2c..6c6cfd422 100644 --- a/app/scodoc/sco_groups_edit.py +++ b/app/scodoc/sco_groups_edit.py @@ -101,7 +101,8 @@ def group_rename(group_id): "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.""", + les mêmes dans ScoDoc et dans l'emploi du temps (si plusieurs ids, + les séparer par des virgules).""", }, ), ), diff --git a/sco_version.py b/sco_version.py index 12c81cd07..21b533cd9 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.59" +SCOVERSION = "9.6.60" SCONAME = "ScoDoc" From ecd1b1f80b20cf873ad3aba58d2cd44c0bdb8d2b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 21 Nov 2023 11:34:52 +0100 Subject: [PATCH 66/69] EDT: fix typo --- app/scodoc/sco_edt_cal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index 7f60dda93..eaa172a0f 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -55,6 +55,7 @@ def formsemestre_load_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: From 54906c1bde6f2233171fa1aecdacaef7073435b0 Mon Sep 17 00:00:00 2001 From: Iziram Date: Wed, 22 Nov 2023 09:13:00 +0100 Subject: [PATCH 67/69] =?UTF-8?q?Assiduites=20:=20mises=20=C3=A0=20jour=20?= =?UTF-8?q?couleurs=20listes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/css/assiduites.css | 6 ++++-- app/templates/assiduites/widgets/tableau_assi.j2 | 1 + app/templates/assiduites/widgets/tableau_base.j2 | 13 +++++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/static/css/assiduites.css b/app/static/css/assiduites.css index fe429ecfc..ed5daa259 100644 --- a/app/static/css/assiduites.css +++ b/app/static/css/assiduites.css @@ -1,15 +1,17 @@ :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: #FF0000; + --color-error: #e62a11; --color-warning: #eec660; --color-information: #658ef0; @@ -21,7 +23,7 @@ --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); diff --git a/app/templates/assiduites/widgets/tableau_assi.j2 b/app/templates/assiduites/widgets/tableau_assi.j2 index 1b72ebbf1..92e6aef81 100644 --- a/app/templates/assiduites/widgets/tableau_assi.j2 +++ b/app/templates/assiduites/widgets/tableau_assi.j2 @@ -88,6 +88,7 @@ td.textContent = getModuleImpl(assiduite); } else if (k.indexOf('est_just') != -1) { td.textContent = assiduite[k] ? "Oui" : "Non" + if (assiduite[k]) row.classList.add("est_just") } else if (k.indexOf('etudid') != -1) { const e = getEtudiant(assiduite.etudid); diff --git a/app/templates/assiduites/widgets/tableau_base.j2 b/app/templates/assiduites/widgets/tableau_base.j2 index 2eb5e34e5..71580a14e 100644 --- a/app/templates/assiduites/widgets/tableau_base.j2 +++ b/app/templates/assiduites/widgets/tableau_base.j2 @@ -456,6 +456,7 @@ td { border: 1px solid #dddddd; padding: 8px; + color: var(--color-default-text); } th { @@ -498,17 +499,25 @@ .l-absent, .l-invalid { - background-color: var(--color-absent); + background-color: var(--color-absent-clair); } .l-valid { - background-color: var(--color-primary); + background-color: var(--color-justi-clair); } .l-retard { background-color: var(--color-retard); } + .l-absent.est_just { + background-color: var(--color-absent-justi); + } + + .l-retard.est_just { + background-color: var(--color-retard-justi); + } + /* Ajoutez des styles pour le conteneur de pagination et les boutons */ .pagination-container { display: flex; From 33c9a606b0cd50c553399c67b56aea8e7f44fc7a Mon Sep 17 00:00:00 2001 From: Iziram Date: Wed, 22 Nov 2023 15:31:35 +0100 Subject: [PATCH 68/69] Assiduites : SaisieAssiduiteEtud rework --- app/static/js/assiduites.js | 83 +++++++ .../assiduites/pages/ajout_assiduites.j2 | 234 ++++++++++++++++++ .../assiduites/pages/ajout_justificatif.j2 | 23 +- app/templates/assiduites/pages/calendrier.j2 | 4 +- .../widgets/moduleimpl_dynamic_selector.j2 | 17 +- app/views/assiduites.py | 45 ++-- 6 files changed, 378 insertions(+), 28 deletions(-) create mode 100644 app/templates/assiduites/pages/ajout_assiduites.j2 diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index aadd04cae..8b4df342c 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -953,6 +953,89 @@ function createAssiduite(etat, etudid) { ); 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 = ` +

    Aucun module n'a été spécifié

    + `; + 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 = ` +

    Attention, le module doit obligatoirement être renseigné.

    +

    Cela vient de la configuration du semestre ou plus largement du département.

    +

    Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.

    + `; + + 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 = ` +

    Attention, l'étudiant n'est pas inscrit à ce module.

    +

    Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.

    + `; + + 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 = ` +

    L'assiduité n'a pas pu être enregistrée car une autre assiduité existe sur la période sélectionnée

    +

    Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.

    + `; + + const content = document.createElement("div"); + content.innerHTML = HTML; + + openAlertModal("Période conflictuelle", content); + } + with_errors = true; + } + }, + (data, status) => { + //error + console.error(data, status); + errorAlert(); + with_errors = true; + } + ); + return !with_errors; +} /** * Suppression d'une assiduité diff --git a/app/templates/assiduites/pages/ajout_assiduites.j2 b/app/templates/assiduites/pages/ajout_assiduites.j2 new file mode 100644 index 000000000..311b24a55 --- /dev/null +++ b/app/templates/assiduites/pages/ajout_assiduites.j2 @@ -0,0 +1,234 @@ +{% include "assiduites/widgets/toast.j2" %} +{% block pageContent %} +
    +

    Ajouter une assiduité

    + {% include "assiduites/widgets/tableau_base.j2" %} + {% if saisie_eval %} +
    +
    +

    + La saisie de l'assiduité a été préconfigurée en fonction de l'évaluation.
    + Une fois la saisie finie, cliquez sur le lien si dessous pour revenir sur la gestion de l'évaluation +

    + retourner sur la page de l'évaluation +
    + {% endif %} +
    +
    +
    +
    + Date de début + + Journée entière +
    +
    + Date de fin + +
    +
    + +
    +
    + Etat de l'assiduité + +
    +
    +
    +
    + Module + {% with moduleid="ajout_assiduite_module_impl",label=false %} + {% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %} + {% endwith %} +
    +
    + +
    +
    + Raison + +
    +
    + +
    + + +
    + + +
    + +
    +
    + + {% include "assiduites/widgets/tableau_assi.j2" %} +
    + +
    + + + +{% endblock pageContent %} \ No newline at end of file diff --git a/app/templates/assiduites/pages/ajout_justificatif.j2 b/app/templates/assiduites/pages/ajout_justificatif.j2 index fac1b11ed..d74737146 100644 --- a/app/templates/assiduites/pages/ajout_justificatif.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif.j2 @@ -3,10 +3,7 @@

    Justifier des absences ou retards

    {% include "assiduites/widgets/tableau_base.j2" %} -
    - - {% include "assiduites/widgets/tableau_justi.j2" %} -
    +
    @@ -60,6 +57,10 @@
    +
    + + {% include "assiduites/widgets/tableau_justi.j2" %} +
    @@ -224,12 +225,12 @@ if (document.getElementById('justi_journee').checked) { date_deb.setAttribute("show", "date") date_fin.setAttribute("show", "date") - document.getElementById("date_fin").classList.add("hidden"); + document.querySelector(`legend[for="justi_date_fin"]`).removeAttribute("required") + } else { date_deb.removeAttribute("show") date_fin.removeAttribute("show") - document.getElementById("date_fin").classList.remove("hidden"); - + document.querySelector(`legend[for="justi_date_fin"]`).setAttribute("required", "") } } @@ -238,8 +239,12 @@ const date_fin = document.querySelector(".page #justi_date_fin") const journee = document.querySelector('.page #justi_journee').checked const deb = date_deb.valueAsObject.date + "T" + (journee ? assi_morning : date_deb.valueAsObject.time) - const fin = (journee ? date_deb.valueAsObject.date : date_fin.valueAsObject.date) + "T" + (journee ? assi_evening : date_fin.valueAsObject.time) - + let fin = "T" + (journee ? assi_evening : date_fin.valueAsObject.time) + if (journee) { + fin = (date_fin.valueAsObject.date || date_deb.valueAsObject.date) + fin + } else { + fin = date_fin.valueAsObject.date + fin + } return { "deb": deb, "fin": fin, diff --git a/app/templates/assiduites/pages/calendrier.j2 b/app/templates/assiduites/pages/calendrier.j2 index 0f9ef84bd..1114f628f 100644 --- a/app/templates/assiduites/pages/calendrier.j2 +++ b/app/templates/assiduites/pages/calendrier.j2 @@ -476,7 +476,7 @@ const matin = [new Date(date), new Date(date)] color = "sans_etat" matin[0].setHours(0, 0, 0, 0) - matin[1].setHours(12, 59, 59) + matin[1].setHours(12, 59, 59) // TODO Utiliser heure pivot (config) @@ -515,7 +515,7 @@ span_aprem.classList.add("color"); const aprem = [new Date(date), new Date(date)] color = "sans_etat" - aprem[0].setHours(13, 0, 0, 0) + aprem[0].setHours(13, 0, 0, 0) // TODO Utiliser heure pivot (config) aprem[1].setHours(23, 59, 59) diff --git a/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 b/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 index 84366dc62..cdb6c5585 100644 --- a/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 +++ b/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 @@ -1,13 +1,24 @@ -