diff --git a/.gitignore b/.gitignore index e073a39..05878a8 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,7 @@ config/config.json # Sass output public/stylesheets + +# Floobits +.floo +.flooignore diff --git a/agenda/index.js b/agenda/index.js new file mode 100644 index 0000000..813cb12 --- /dev/null +++ b/agenda/index.js @@ -0,0 +1,18 @@ +const { Worker } = require("worker_threads"); +let nb = 0; + + +function startWorker(workerArgs) { + let worker = new Worker(...workerArgs); + worker.on("error", (err) => { + console.error(err); + nb--; + if (nb > 0) + startWorker(workerArgs); + }); +} + +module.exports = (app) => { + const workerArgs = ["./agenda/worker.js", {workerData: app.get("config")}]; + startWorker(workerArgs); +}; diff --git a/agenda/worker.js b/agenda/worker.js new file mode 100644 index 0000000..366dc31 --- /dev/null +++ b/agenda/worker.js @@ -0,0 +1,227 @@ +const https = require("https"); +const config = require("worker_threads").workerData; +const models = require("../models"); + +const reg_event = /(?:(?:BEGIN:VEVENT\nDTSTAMP:(?:[A-Z0-9]*?)\nDTSTART:([A-Z0-9]*?)\nDTEND:([A-Z0-9]*?)\nSUMMARY: {0,}([a-zéèàA-Z0-9-. \, \\/ô]*?)\nLOCATION:([a-zA-Zéèà0-9-. \,\\]*?)\nDESCRIPTION:(?:\\n){0,}((?:(?:LP(?:[ a-zA-Z0-9\\]*?))\\n){1,})((?:(?:[A-Z]*) (?:[A-Z]*)(?: (?:[A-Z]*)){0,}\\n){0,})(?:.*?)\nEND:VEVENT)|(?:BEGIN:VEVENT\nDTSTAMP:(?:[A-Z0-9]*?)\nDTSTART:([A-Z0-9]*?)\nDTEND:([A-Z0-9]*?)\nSUMMARY: {0,}((?:S(?:[A-Z0-9-]*)|M(?:[A-Z0-9-]*)(?:\/M(?:[A-Z0-9-]*)){0,}|Conférence)[a-zéèàA-Z0-9-. \, \\/]*?)\nLOCATION:([a-zA-Zéèà0-9-. \,\\]*?)\nDESCRIPTION:(?:\\n){0,}((?:(?:G[0-9]S[0-9]|S[0-9]|ASPE)\\n){0,})((?:(?:[A-Z]*) (?:[A-Z]*)(?: (?:[A-Z]*)){0,}\\n){0,})(?:.*?)\nEND:VEVENT))/gs; +const reg_location = /((?:[SH0-9][0-9]{2})|(?:(?:Préfa |Amphi)[0-9]))/g; +const reg_teachers = /^(?:((?:[a-zA-Z]*[ -]{0,}){0,}) ([a-zA-Z]*))$/m; +const reg_date = /([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z/; +const reg_group = /(?:(LP)[ -]{0,}(.*)|(?:(G[0-9])(S[0-9])))/; +const base_url = config["edt"]; +const capitalizeFirstLetter = (string) => string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); + + +function fetchEvents(days = 1, TS_Start = new Date()) { + return new Promise((resolve, reject) => { + let start = new Date(TS_Start); + if (start == "Invalid Date") + start = new Date(); + let end = new Date(start); + end.setDate(end.getDate() + days); + + let url = base_url + start.getFullYear() + "-" + start.getMonth() + "-" + start.getDate() + "&lastDate=" + end.getFullYear() + "-" + end.getMonth() + "-" + end.getDate(); + https.get(url, (resp) => { + let data = ""; + resp.on("data", (chunk) => { + data += chunk; + }); + resp.on("end", () => { + let output = []; + data = data.replace(/\r/g, ""); + let m; + while ((m = reg_event.exec(data)) !== null) { + if (m.index === reg_event.lastIndex) { + reg_event.lastIndex++; + } + let event = []; + let semester = []; + let group = []; + /* + m = [ + FullMatch, + StartTime, + EndTime, + EventDescription ( SubjectID,Name,Class?) , + Location, + ClassGroup, + Teacher(s) + ] + */ + if (m[1] !== undefined) { + // LPXXXX + } else { + m.splice(1, 6); + // GXSX | SX | ASPE + } + let csplit = m[5].split("\\n"); + csplit.pop(); + csplit.forEach(e => { + if (e === "ASPE") { + semester.push(e); + }else if(/^S[0-9]$/.test(e)) { + semester.push(e); + }else{ + let groupSplit = reg_group.exec(e); + if (groupSplit[1] === 'LP') { + group.push([groupSplit[2], groupSplit[1]]); + } else { + group.push([groupSplit[3], groupSplit[4]]); + } + } + }); + + event["title"] = m[3]; + event["semesters"] = semester; + event["groups"] = group; + /* + Date + */ + let d1 = reg_date.exec(m[1]); + let d2 = reg_date.exec(m[2]); + event["startDate"] = new Date(d1[1] + "-" + d1[2] + "-" + d1[3] + "T" + d1[4] + ":" + d1[5] + ":" + d1[6] + ".00Z"); + event["endDate"] = new Date(d2[1] + "-" + d2[2] + "-" + d2[3] + "T" + d2[4] + ":" + d2[5] + ":" + d2[6] + ".00Z"); + /* + Location + */ + event["locations"] = []; + let loc; + while ((loc = reg_location.exec(m[4])) !== null) { + if (loc.index === reg_location.lastIndex) { + reg_location.lastIndex++; + } + event["locations"].push(loc[1]); + } + /* + Teachers + */ + event['teachers'] = []; + let fullTeachers = m[6].split('\\n'); + fullTeachers.forEach(e => { + if(e !== ""){ + let splittedTeachers = reg_teachers.exec(e); + event['teachers'].push([splittedTeachers[1],splittedTeachers[2]]); + } + }); + output.push(event); + } + resolve(output); + }); + }).on("error", (err) => { + reject(err); + }); + }); +} + +function compare(a, b) { + if (a.length !== b.length) { + return false; + } + let set = {}; + a.forEach((i) => { + if (set[i] !== undefined) { + set[i]++; + } else { + set[i] = 1; + } + }); + let difference = b.every((i) => { + if (set[i] === undefined) { + return false; + } else { + set[i]--; + if (set[i] === 0) { + delete set[i]; + } + return true; + } + }); + return Object.keys(set) == 0 && difference; +} + +function compareGroups(list1, list2) { + return compare(list1.map(g => g[0] + " " + g[1]), list2.map(g => g.number + " " + g.Semester.name)); +} + +function compareTeachers(list1, list2) { + return compare(list1.map(t => t[0].toUpperCase() + " " + t[1].toUpperCase()), list2.map(t => t.lastName.toUpperCase() + " " + t.firstName.toUpperCase())); +} + +function compareSemesters(list1, list2) { + return compare(list1, list2.map(s => s.name)); +} + +async function updateDatabase() { + let events = await fetchEvents(365, new Date(2020, 9, 1)); + for (let event of await models.Event.findAll({ + include: [{ + model: models.Group, + include: {model: models.Semester, required: true} + }, models.User, models.Semester] + })) { + let ev = events.find(e => (e.title === event.name && e.startDate.getTime() === event.startDate.getTime() && + e.endDate.getTime() === event.endDate.getTime() && e.locations.join(", ") === event.locations && + compareGroups(e.groups, event.Groups) && compareTeachers(e.teachers, event.Users) && + compareSemesters(e.semesters, event.Semesters))); + if (!ev) + await event.destroy(); + else + events = events.filter(e => e !== ev); + } + + for (let event of events) { + let e = await models.Event.create({ + name: event.title, + startDate: event.startDate, + endDate: event.endDate, + locations: event.locations.join(", ") + }); + + let teachers = []; + for (let teacher of event.teachers) { + let t = await models.User.findOne({where: {permissions: 2, lastName: teacher[0].toUpperCase(), firstName: capitalizeFirstLetter(teacher[1])}}); + if (!t) + t = await models.User.create({ + email: teacher[1].toLowerCase().replaceAll(" ", "-") + "." + teacher[0].toLowerCase().replaceAll(" ", "-") + "@univ-lyon1.fr", + firstName: capitalizeFirstLetter(teacher[1]), + lastName: teacher[0].toUpperCase(), + permissions: 2, + passwordHash: Math.round((Math.pow(36, 12 + 1) - Math.random() * Math.pow(36, 12))).toString(36).slice(1) + }); + teachers.push(t); + } + await e.addUsers(teachers); + + let semesters = []; + for (let semester of event.semesters) { + let s = await models.Semester.findOne({where: {name: semester, year: event.startDate.getFullYear()}}); + if (!s) + s = await models.Semester.create({ + name: semester, + year: event.startDate.getFullYear() + }); + semesters.push(s); + } + await e.addSemesters(semesters); + + let groups = []; + for (let group of event.groups) { + let s = await models.Semester.findOne({where: {name: group[1], year: event.startDate.getFullYear()}}); + if (!s) + s = await models.Semester.create({ + name: group[1], + year: event.startDate.getFullYear() + }); + + let g = await models.Group.findOne({where: {number: group[0], SemesterId: s.id}}); + if (!g) + g = await models.Group.create({ + number: group[0], + SemesterId: s.id + }); + groups.push(g); + } + await e.addGroups(groups); + } +} + +updateDatabase().then(() => setInterval(updateDatabase, 30000)); diff --git a/app.js b/app.js index 5d204c4..3b56354 100644 --- a/app.js +++ b/app.js @@ -9,11 +9,10 @@ let config = process.env.NODE_ENV === "test" ? {} : require("./config/config.jso let indexRouter = require("./routes/index"); const loginRouter = require("./routes/login"); const edtRouter = require("./routes/edt"); -const homeRouter = require("./routes/home"); const marksRouter = require("./routes/marks"); const registerRouter = require("./routes/register"); -const viescolRouter = require("./routes/viescol") -const profilRouter = require("./routes/profil") +const viescolRouter = require("./routes/viescol"); +const profilRouter = require("./routes/profil"); let app = express(); const sessionMiddleware = session({ @@ -41,7 +40,6 @@ app.use("/", indexRouter); app.use("/login", loginRouter); app.use("/email", require("./routes/email")); app.use("/edt", edtRouter); -app.use("/home", homeRouter); app.use("/marks", marksRouter); app.use('/register', registerRouter); app.use('/viescol', viescolRouter); diff --git a/bin/www b/bin/www index e8cd115..aad9016 100755 --- a/bin/www +++ b/bin/www @@ -17,7 +17,7 @@ const mailClient = new SMTPClient(process.env.NODE_ENV === "test" ? {} : require /** * Render all Sass to css */ -css = sass.renderSync({ +let css = sass.renderSync({ file: "sass/style.sass", includePaths: ["sass/"], outputStyle: "compressed" @@ -30,7 +30,7 @@ fs.writeFileSync("public/stylesheets/style.css", css.css); * Get port from environment and store in Express. */ -let port = normalizePort(process.env.PORT || "3000"); +const port = normalizePort(process.env.PORT || "3000"); app.set("port", port); app.set("mailClient", mailClient); @@ -38,7 +38,7 @@ app.set("mailClient", mailClient); * Create HTTP server. */ -let server = http.createServer(app); +const server = http.createServer(app); /** * Create socket.io server @@ -66,7 +66,7 @@ models.sequelize.sync().then(() => { */ function normalizePort(val) { - let port = parseInt(val, 10); + const port = parseInt(val, 10); if (isNaN(port)) { // named pipe @@ -90,7 +90,7 @@ function onError(error) { throw error; } - let bind = typeof port === "string" + const bind = typeof port === "string" ? "Pipe " + port : "Port " + port; @@ -114,9 +114,10 @@ function onError(error) { */ function onListening() { - let addr = server.address(); - let bind = typeof addr === "string" + const addr = server.address(); + const bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; debug("Listening on " + bind); + require("../agenda")(app); } diff --git a/config/config_example.json b/config/config_example.json index 6e93448..702ca17 100644 --- a/config/config_example.json +++ b/config/config_example.json @@ -18,6 +18,7 @@ "contact": "Your Contact ", "mailPath": "https://yourServer.com" }, + "edt": "https://adelb.univ-lyon1.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=XXXX&projectId=XXXX&calType=ical&firstDate=", "secret": "keyboard cat", "passwordPrivateKey": "ecc635295f200847b79299df48e15759" } diff --git a/models/UE.js b/models/UE.js new file mode 100644 index 0000000..acda65b --- /dev/null +++ b/models/UE.js @@ -0,0 +1,23 @@ +"use strict"; + +const { Model } = require("sequelize"); + +module.exports = (sequelize, DataTypes) => { + class UE extends Model { + static associate(models) { + UE.hasMany(models.Event); + UE.belongsToMany(models.User, {through: "UEUser"}); + } + } + UE.init({ + name : { + type: DataTypes.STRING, + allowNull: false, + unique: true + } + }, { + sequelize, + modelName: "UE", + }); + return UE; +}; diff --git a/models/evaluation.js b/models/evaluation.js new file mode 100644 index 0000000..216a075 --- /dev/null +++ b/models/evaluation.js @@ -0,0 +1,27 @@ +"use strict"; + +const { Model } = require("sequelize"); + +module.exports = (sequelize, DataTypes) => { + class Evaluation extends Model { + static associate(models) { + Evaluation.belongsToMany(models.User, {through: "EvaluationTeacher"}); + Evaluation.hasMany(models.Grade); + Evaluation.belongsTo(models.UE); + } + } + Evaluation.init({ + name: { + type: DataTypes.STRING, + allowNull: false + }, + date: { + type: DataTypes.DATE, + allowNull: false + } + }, { + sequelize, + modelName: "Evaluation", + }); + return Evaluation; +}; diff --git a/models/event.js b/models/event.js new file mode 100644 index 0000000..8e0da6e --- /dev/null +++ b/models/event.js @@ -0,0 +1,36 @@ +"use strict"; + +const { Model } = require("sequelize"); + +module.exports = (sequelize, DataTypes) => { + class Event extends Model { + static associate(models) { + Event.belongsToMany(models.Semester, {through: "EventSemester"}); + Event.belongsToMany(models.Group, {through: "EventGroup"}); + Event.belongsToMany(models.User, {through: "EventTeacher"}); + Event.belongsTo(models.UE); + } + } + Event.init({ + name: { + type: DataTypes.STRING, + allowNull: false + }, + locations: { + type: DataTypes.STRING, + allowNull: false + }, + startDate: { + type: DataTypes.DATE, + allowNull: false + }, + endDate: { + type: DataTypes.DATE, + allowNull: false + } + }, { + sequelize, + modelName: "Event", + }); + return Event; +}; diff --git a/models/grade.js b/models/grade.js new file mode 100644 index 0000000..1f75ee4 --- /dev/null +++ b/models/grade.js @@ -0,0 +1,30 @@ +"use strict"; + +const { Model } = require("sequelize"); + +module.exports = (sequelize, DataTypes) => { + class Grade extends Model { + static associate(models) { + Grade.belongsTo(models.Evaluation); + Grade.belongsTo(models.User, {as: "TeacherGrade"}); + Grade.belongsTo(models.User, {as: "StudentGrade"}); + } + } + Grade.init({ + score: { + type: DataTypes.FLOAT, + allowNull: false + }, + limit: { + type: DataTypes.FLOAT, + allowNull: false + }, + comment: { + type: DataTypes.TEXT + } + }, { + sequelize, + modelName: "Grade", + }); + return Grade; +}; diff --git a/models/group.js b/models/group.js new file mode 100644 index 0000000..d9a2cf7 --- /dev/null +++ b/models/group.js @@ -0,0 +1,23 @@ +"use strict"; + +const { Model } = require("sequelize"); + +module.exports = (sequelize, DataTypes) => { + class Group extends Model { + static associate(models) { + Group.belongsTo(models.Semester); + Group.belongsToMany(models.User, {through: "UserGroup"}); + Group.belongsToMany(models.Event, {through: "EventGroup"}); + } + } + Group.init({ + number: { + type: DataTypes.STRING, + allowNull: false + } + }, { + sequelize, + modelName: "Group", + }); + return Group; +}; diff --git a/models/semester.js b/models/semester.js new file mode 100644 index 0000000..fac1dad --- /dev/null +++ b/models/semester.js @@ -0,0 +1,28 @@ +"use strict"; + +const { Model } = require("sequelize"); + +module.exports = (sequelize, DataTypes) => { + class Semester extends Model { + static associate(models) { + Semester.hasMany(models.Group); + Semester.belongsToMany(models.Event, {through: "EventSemester"}); + } + } + Semester.init({ + year: { + type: DataTypes.INTEGER, + allowNull: false, + unique: "semester" + }, + name : { + type: DataTypes.STRING, + allowNull: false, + unique: "semester" + } + }, { + sequelize, + modelName: "Semester", + }); + return Semester; +}; diff --git a/models/user.js b/models/user.js index 82657b0..0eacb8f 100644 --- a/models/user.js +++ b/models/user.js @@ -8,7 +8,7 @@ const { } = require("sequelize"); module.exports = (sequelize, DataTypes) => { function hash(password, email) { - let cipher = crypto.createCipheriv( + const cipher = crypto.createCipheriv( "aes-256-cbc", privateKey, crypto.createHash("md5").update(email).digest("base64").slice(0, 16) @@ -18,6 +18,12 @@ module.exports = (sequelize, DataTypes) => { class User extends Model { static associate(models) { + User.belongsToMany(models.Group, {through: "UserGroup"}); + User.belongsToMany(models.Event, {through: "EventTeacher"}); + User.belongsToMany(models.Evaluation, {through: "EvaluationTeacher"}); + User.belongsToMany(models.UE, {through: "UEUser"}); + //User.hasMany(models.Grade, {as : "TeacherGrade"}); + //User.hasMany(models.Grade, {as: "StudentGrade"}); } checkPassword(password) { diff --git a/public/browserconfig.xml b/public/browserconfig.xml new file mode 100644 index 0000000..35157e2 --- /dev/null +++ b/public/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/public/images/android-chrome-192x192.png b/public/images/android-chrome-192x192.png new file mode 100644 index 0000000..dbd96ea Binary files /dev/null and b/public/images/android-chrome-192x192.png differ diff --git a/public/images/android-chrome-512x512.png b/public/images/android-chrome-512x512.png new file mode 100644 index 0000000..b3c8eb7 Binary files /dev/null and b/public/images/android-chrome-512x512.png differ diff --git a/public/images/apple-touch-icon.png b/public/images/apple-touch-icon.png new file mode 100644 index 0000000..1ec4672 Binary files /dev/null and b/public/images/apple-touch-icon.png differ diff --git a/public/images/favicon-16x16.png b/public/images/favicon-16x16.png new file mode 100644 index 0000000..f9a356e Binary files /dev/null and b/public/images/favicon-16x16.png differ diff --git a/public/images/favicon-32x32.png b/public/images/favicon-32x32.png new file mode 100644 index 0000000..766fc9c Binary files /dev/null and b/public/images/favicon-32x32.png differ diff --git a/public/images/favicon.ico b/public/images/favicon.ico new file mode 100644 index 0000000..780f472 Binary files /dev/null and b/public/images/favicon.ico differ diff --git a/public/images/mstile-150x150.png b/public/images/mstile-150x150.png new file mode 100644 index 0000000..d3d36af Binary files /dev/null and b/public/images/mstile-150x150.png differ diff --git a/public/images/safari-pinned-tab.svg b/public/images/safari-pinned-tab.svg new file mode 100644 index 0000000..8eafe32 --- /dev/null +++ b/public/images/safari-pinned-tab.svg @@ -0,0 +1,169 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + + + + + + diff --git a/public/javascripts/login.js b/public/javascripts/login.js index ece14e1..1f04c1b 100644 --- a/public/javascripts/login.js +++ b/public/javascripts/login.js @@ -11,7 +11,7 @@ document.getElementById("login").addEventListener("submit", e=>{ alert('Format d\'email incorrect.'); } return false; -}) +}); socket.on("login", data=>{ if(data.error){ @@ -19,4 +19,4 @@ socket.on("login", data=>{ }else{ window.location.href = "/"; } -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/public/javascripts/main.js b/public/javascripts/main.js index 20b1d66..74ddccb 100644 --- a/public/javascripts/main.js +++ b/public/javascripts/main.js @@ -18,7 +18,7 @@ socket.on("logout", data=>{ }else{ window.location.href = "/"; } -}) +}); function profilRedirect(){ diff --git a/public/javascripts/profil.js b/public/javascripts/profil.js index c2c044b..3d2b7f8 100644 --- a/public/javascripts/profil.js +++ b/public/javascripts/profil.js @@ -18,4 +18,4 @@ socket.on("profileEdit", data=>{ }else{ window.location.href = "/"; } -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/public/javascripts/register.js b/public/javascripts/register.js index 3b5833a..aad9acf 100644 --- a/public/javascripts/register.js +++ b/public/javascripts/register.js @@ -20,4 +20,4 @@ socket.on("register", data=>{ }else{ window.location.href = "/"; } -}) +}); diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 0000000..99062eb --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Lyon's Exceptional Tool for University", + "short_name": "L'ETU", + "icons": [ + { + "src": "/images/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/images/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/routes/edt.js b/routes/edt.js index 6e74d6b..8b9291c 100644 --- a/routes/edt.js +++ b/routes/edt.js @@ -1,8 +1,9 @@ -let express = require("express"); -let router = express.Router(); +const express = require("express"); +const sessionCheck = require("./utils/sessionCheck"); +const router = express.Router(); -router.get("/", (req, res) => { +router.get("/", sessionCheck(1), (req, res) => { res.render("pages/edt", { title: "L'ETU" }); }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/routes/email.js b/routes/email.js index 0597f15..0771a69 100644 --- a/routes/email.js +++ b/routes/email.js @@ -8,7 +8,7 @@ const sessionCheck = require("./utils/sessionCheck"); router.get("/check", async (req, res) => { if (!req.query.token) return error(req, res, "Missing argument", 400); - let user = await models.User.findOne({where: {emailToken: req.query.token}}); + const user = await models.User.findOne({where: {emailToken: req.query.token}}); if (user) { user.emailVerified = true; if (user.email.endsWith("@etu.univ-lyon1.fr")) @@ -25,7 +25,7 @@ router.get("/forget", sessionCheck(-1), async (req, res) => { if (!req.query.token) res.render("forget", {title: "L'ETU"}); else { - let user = await models.User.findOne({where: {passwordToken: data.token}}); + const user = await models.User.findOne({where: {passwordToken: data.token}}); if (!user) return error(req, res, "Invalid token", 400); else if (user.passwordTokenDate && ((new Date().getTime() - user.passwordTokenDate.getTime()) / 1000 > 3600)) diff --git a/routes/home.js b/routes/home.js deleted file mode 100644 index 10276d9..0000000 --- a/routes/home.js +++ /dev/null @@ -1,9 +0,0 @@ -let express = require("express"); -let router = express.Router(); - -/* GET home page. */ -router.get("/", (req, res) => { - res.render("pages/home", { title: "L'ETU" }); -}); - -module.exports = router; \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index ced825d..fee5fcb 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,5 +1,5 @@ -let express = require("express"); -let router = express.Router(); +const express = require("express"); +const router = express.Router(); const sessionCheck = require("./utils/sessionCheck"); router.get("/", sessionCheck(1), (req, res) => { diff --git a/routes/login.js b/routes/login.js index 0a899f0..1f1aef2 100644 --- a/routes/login.js +++ b/routes/login.js @@ -1,5 +1,5 @@ -let express = require("express"); -let router = express.Router(); +const express = require("express"); +const router = express.Router(); const sessionCheck = require("./utils/sessionCheck"); router.get("/",sessionCheck(-1), (req, res) => { diff --git a/routes/marks.js b/routes/marks.js index c4d5cb9..8b3338a 100644 --- a/routes/marks.js +++ b/routes/marks.js @@ -1,7 +1,8 @@ -let express = require("express"); -let router = express.Router(); +const express = require("express"); +const sessionCheck = require("./utils/sessionCheck"); +const router = express.Router(); -router.get("/", (req, res) => { +router.get("/", sessionCheck(1), (req, res) => { res.render("pages/marks", { title: "L'ETU" }); }); diff --git a/routes/profil.js b/routes/profil.js index 529ff2b..3b0066b 100644 --- a/routes/profil.js +++ b/routes/profil.js @@ -1,7 +1,8 @@ -let express = require("express"); -let router = express.Router(); +const express = require("express"); +const sessionCheck = require("./utils/sessionCheck"); +const router = express.Router(); -router.get("/", (req, res) => { +router.get("/", sessionCheck(1), (req, res) => { res.render("pages/profil", { title: "L'ETU" }); }); diff --git a/routes/register.js b/routes/register.js index 9e99f1c..a7410dd 100644 --- a/routes/register.js +++ b/routes/register.js @@ -1,5 +1,5 @@ -let express = require("express"); -let router = express.Router(); +const express = require("express"); +const router = express.Router(); const sessionCheck = require("./utils/sessionCheck"); router.get("/",sessionCheck(-1), (req, res) => { diff --git a/routes/utils/sessionCheck.js b/routes/utils/sessionCheck.js index 5a54bc5..5c66215 100644 --- a/routes/utils/sessionCheck.js +++ b/routes/utils/sessionCheck.js @@ -1,4 +1,4 @@ -let error = require("./error"); +const error = require("./error"); function sessionCheck(permission) { return (req, res, next) => { diff --git a/routes/viescol.js b/routes/viescol.js index bc0c341..4bd1b47 100644 --- a/routes/viescol.js +++ b/routes/viescol.js @@ -1,8 +1,9 @@ -let express = require("express"); -let router = express.Router(); +const express = require("express"); +const sessionCheck = require("./utils/sessionCheck"); +const router = express.Router(); -router.get("/", (req, res) => { +router.get("/", sessionCheck(1), (req, res) => { res.render("pages/viescol", { title: "L'ETU" }); }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/sass/style.sass b/sass/style.sass index 4af3869..eab44b6 100644 --- a/sass/style.sass +++ b/sass/style.sass @@ -71,16 +71,8 @@ h2 i color: $primary -#panel - background-color: $dark2 - height: 100vh - border-right: 2px solid $light3 - padding: 0 - position: fixed - #main background-color: $dark1 - margin-left: 16vw #navtop font-size: 22px @@ -130,11 +122,11 @@ i cursor: pointer .student - background: radial-gradient(circle, rgb(1, 79, 116) 20%, rgb(15, 15, 99) 100%) + background-image: linear-gradient(to top, #4481eb 0%, #04befe 100%) .teacher - background: radial-gradient(circle, rgb(5, 116, 1) 20%, rgb(11, 68, 10) 100%) + background-image: radial-gradient( circle farthest-corner at -1% 57.5%, rgba(19,170,82,1) 0%, rgba(0,102,43,1) 90% ) .admin - background: radial-gradient(circle, rgb(116, 74, 1) 20%, rgb(99, 42, 15) 100%) + background-image: radial-gradient( circle farthest-corner at 7.2% 19%, rgba(120,0,0,1) 0%, rgba(239,75,75,1) 100.2% ) #page margin: 0 @@ -219,6 +211,7 @@ h3 .midi td height: 10vh + cursor: auto th background-color: $secondary text-align: center @@ -234,6 +227,7 @@ h3 td border-bottom: 1px solid white td + cursor: pointer text-align: center font-size: 15px background-color: $medium @@ -489,6 +483,37 @@ div#visible + div margin: auto cursor: pointer +.detailsedt + z-index: 1 + display: none + position: fixed + top: 20vh + left: 30% + background-color: $dark2 + width: 40% + border-radius: 50px + border: 1px solid $light3 + p + font-size: 20px + padding: 5px + color: white + margin: 0 + text-align: center + p:first-child + font-size: 25px + background-color: $secondary + border-top-right-radius: 50px + border-top-left-radius: 50px + +#slide-out + background-color: $dark2 + a + color: white + font-size: 20px + +#hamburger + position: fixed + #notvisible display: none diff --git a/sockets/email/checkResend.js b/sockets/email/checkResend.js index 07d85c5..d0de62a 100644 --- a/sockets/email/checkResend.js +++ b/sockets/email/checkResend.js @@ -11,4 +11,4 @@ module.exports = socket => { else await emailCheck(socket, user, null); } -} +}; diff --git a/sockets/email/forgotPassword.js b/sockets/email/forgotPassword.js index d415c15..b13bccc 100644 --- a/sockets/email/forgotPassword.js +++ b/sockets/email/forgotPassword.js @@ -10,4 +10,4 @@ module.exports = socket => { else await emailPassword(socket, user, null); } -} +}; diff --git a/sockets/email/setPassword.js b/sockets/email/setPassword.js index 809de18..7c3399a 100644 --- a/sockets/email/setPassword.js +++ b/sockets/email/setPassword.js @@ -5,7 +5,7 @@ module.exports = socket => { return async (data) => { let user = await models.User.findOne({where: {passwordToken: data.token}}); if (!user) - socket.emit("setPassword", {error: {message: "invalid_token"}}) + socket.emit("setPassword", {error: {message: "invalid_token"}}); else if (user.passwordTokenDate && ((new Date().getTime() - user.passwordTokenDate.getTime()) / 1000 > 3600)) socket.emit("setPassword", {error: {message: "expired_token"}}); else { @@ -16,4 +16,4 @@ module.exports = socket => { socket.emit("setPassword", true); } } -} +}; diff --git a/sockets/get/agendaGet.js b/sockets/get/agendaGet.js new file mode 100644 index 0000000..8472920 --- /dev/null +++ b/sockets/get/agendaGet.js @@ -0,0 +1,61 @@ +const models = require("../../models"); + + +function addWhere(options, index, attr, value) { + if (!options.include[index].where) + options.include[index].where = {}; + if (!options.include[index].where[attr]) + options.include[index].where[attr] = {}; + if (!options.include[index].where[attr][models.Sequelize.Op.or]) + options.include[index].where[attr][models.Sequelize.Op.or] = []; + options.include[index].where[attr][models.Sequelize.Op.or].push(value); + options.include[index].require = true; +} + +module.exports = socket => { + return async (data) => { + let options = { + where: {}, + include: [ + {model: models.User, attributes: ["email", "firstName", "lastName"]}, + {model: models.Group, include: models.Semester}, + {model: models.Semester, include: models.Group} + ] + }; + if (data && data.startDate) + options.where.startDate = data.startDate; + if (data && data.endDate) + options.where.endDate = data.endDate; + if (data && data.semesters) { + for (let semester of data.semesters) { + let s = await models.Semester.findByPk(semester); + if (!s) { + socket.emit("agendaGet", {error: {message: "semester_not_found"}}); + return + } else + addWhere(options, 2, "id", s.id); + } + } + if (data && data.groups) { + for (let group of data.groups) { + let g = await models.Group.findByPk(group); + if (!g) { + socket.emit("agendaGet", {error: {message: "group_not_found"}}); + return + } else + addWhere(options, 1, "id", g.id); + } + } + if (data && data.teachers) { + for (let teacher of data.teachers) { + let t = await models.User.findByPk(teacher); + if (!t) { + socket.emit("agendaGet", {error: {message: "teacher_not_found"}}); + return + } else + addWhere(options, 0, "email", t.email); + } + } + socket.emit("agendaGet", await models.Event.findAll(options)); + } +}; diff --git a/sockets/get/evaluationGet.js b/sockets/get/evaluationGet.js new file mode 100644 index 0000000..55c3beb --- /dev/null +++ b/sockets/get/evaluationGet.js @@ -0,0 +1,22 @@ +const models = require("../../models"); + +module.exports = socket => { + return async (data) => { + const options = {where: {}, + include: [models.UE, {model: models.User}, + { + model: models.Grade, + include: [{model: models.User, as: "TeacherGrade"}, {model: models.User, as: "StudentGrade"}] + }] + }; + + if (data && data.id) + options.where.id = data.id; + + if (socket.request.session.user.permissions === 2) { + options.include[1].where = {email: socket.request.session.user.email}; + options.include[1].required = true; + } + socket.emit("evaluationGet", await models.Evaluation.findAll(options)); + } +}; diff --git a/sockets/get/gradeGet.js b/sockets/get/gradeGet.js new file mode 100644 index 0000000..f0a1174 --- /dev/null +++ b/sockets/get/gradeGet.js @@ -0,0 +1,22 @@ +const models = require("../../models"); + +module.exports = socket => { + return async (data) => { + const options = {where: {}, include: [models.Evaluation, + {model: models.User, as: "TeacherGrade"}, + {model: models.User, as: "StudentGrade"}]}; + + if (data && data.id) + options.where.id = data.id; + + switch (socket.request.session.user.permissions) { + case 1: + options.where.StudentGradeEmail = socket.request.session.user.email; + break; + case 2: + options.where.TeacherGradeEmail = socket.request.session.user.email; + break; + } + socket.emit("gradeGet", await models.Grade.findAll(options)); + } +}; diff --git a/sockets/get/groupGet.js b/sockets/get/groupGet.js new file mode 100644 index 0000000..48ae8ed --- /dev/null +++ b/sockets/get/groupGet.js @@ -0,0 +1,26 @@ +const models = require("../../models"); + +module.exports = socket => { + return async (data) => { + let options = {include: [{model: models.Semester}]}; + if (data && (data.id || data.number || data.semester)) + options.where = {}; + if (data && data.id) + options.where.id = data.id; + if (data && data.number) + options.where.number = data.number; + if (data && data.semester) { + let s = await models.Semester.findByPk(data.semester); + if (!s) { + socket.emit("groupGet", {error: {message: "semester_not_found"}}); + return + } + options.include[0].where = {id: s.id}; + options.include[0].require = true; + } + if (data && data.users) + options.include.push({model: models.User, where: {email: {[models.Sequelize.Op.in]: data.users}}, require: true}); + + socket.emit("groupGet", await models.Group.findAll(options)); + } +}; diff --git a/sockets/get/semesterGet.js b/sockets/get/semesterGet.js new file mode 100644 index 0000000..e7b0bee --- /dev/null +++ b/sockets/get/semesterGet.js @@ -0,0 +1,19 @@ +const models = require("../../models"); + +module.exports = socket => { + return async (data) => { + let options = {include: [{model: models.Group}]}; + if (data && (data.id || data.name || data.year)) + options.where = {}; + if (data && data.id) + options.where.id = data.id; + if (data && data.name) + options.where.name = data.name; + if (data && data.year) + options.where.year = data.year; + if (data && data.groups) + options.include[0].where = {id: {[models.Sequelize.Op.or]: data.groups}}; + + socket.emit("semesterGet", await models.Semester.findAll(options)); + } +}; diff --git a/sockets/get/userGet.js b/sockets/get/userGet.js new file mode 100644 index 0000000..0b905c5 --- /dev/null +++ b/sockets/get/userGet.js @@ -0,0 +1,17 @@ +const models = require("../../models"); + +module.exports = socket => { + return async (data) => { + let options = {where: {}}; + if (data.email) + options.where.email = data.email; + if (data.firstName && data.lastName) { + options.where.firstName = data.firstName; + options.where.lastName = data.lastName; + } + if (data.permissions) + options.where.permissions = data.permissions; + + socket.emit("userGet", await models.User.findAll(options)); + } +}; diff --git a/sockets/index.js b/sockets/index.js index 604338a..fb53df2 100644 --- a/sockets/index.js +++ b/sockets/index.js @@ -1,6 +1,5 @@ module.exports = socket => { console.log("New connection !"); - console.log(socket.request.session.user); if (!socket.request.session.user) { socket.on("login", require("./login")(socket)); socket.on("register", require("./register")(socket)); @@ -10,6 +9,15 @@ module.exports = socket => { } else { socket.on("profileEdit", require("./profile/edit")(socket)); socket.on("logout", require("./logout")(socket)); + socket.on("agendaGet", require("./get/agendaGet")(socket)); + socket.on("gradeGet", require("./get/gradeGet")(socket)); + if (socket.request.session.user.permissions > 1) + socket.on("evaluationGet", require("./get/evaluationGet")(socket)); + if (socket.request.session.user.permissions > 2) { + socket.on("groupGet", require("./get/groupGet")(socket)); + socket.on("semesterGet", require("./get/semesterGet")(socket)); + socket.on("userSet", require("./set/userSet")(socket)); + } } socket.emit("connected"); -} +}; diff --git a/sockets/login.js b/sockets/login.js index e1a7341..70b4707 100644 --- a/sockets/login.js +++ b/sockets/login.js @@ -2,7 +2,7 @@ const models = require("../models"); module.exports = socket => { return async (data) => { - let user = await models.User.findByPk(data.email); + let user = await models.User.findByPk(data.email, {include: {model: models.Group, include: models.Semester}}); if (!user) socket.emit("login", {error: {message: "not_found"}}); else if (!user.checkPassword(data.password)) @@ -15,4 +15,4 @@ module.exports = socket => { socket.emit("login", user) } } -} +}; diff --git a/sockets/logout.js b/sockets/logout.js index 2244722..6e7be2f 100644 --- a/sockets/logout.js +++ b/sockets/logout.js @@ -7,4 +7,4 @@ module.exports = socket => { socket.emit("logout", {error: { message: "not_logged_in"}}); } } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/sockets/profile/edit.js b/sockets/profile/edit.js index 99565a4..1b93aec 100644 --- a/sockets/profile/edit.js +++ b/sockets/profile/edit.js @@ -6,7 +6,7 @@ module.exports = socket => { if (!user) socket.emit("profileEdit", {error: {message: "not_found"}}); else if (!user.checkPassword(data.oldPassword)) - socket.emit("profileEdit", {error: {message: "invalid_password"}}) + socket.emit("profileEdit", {error: {message: "invalid_password"}}); else { if (data.firstName !== user.firstName) user.firstName = data.firstName; @@ -14,10 +14,10 @@ module.exports = socket => { user.lastName = data.lastName; user.newPassword = data.newPassword; if (data.password && !user.checkPassword(data.password)) - user.passwordHash = data.password + user.passwordHash = data.password; socket.request.session.user = user; socket.request.session.save(); socket.emit("profileEdit", user) } } -} +}; diff --git a/sockets/register.js b/sockets/register.js index 740b2c8..eaa8381 100644 --- a/sockets/register.js +++ b/sockets/register.js @@ -18,4 +18,4 @@ module.exports = socket => { await emailCheck(socket, user, null); } } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/sockets/set/userSet.js b/sockets/set/userSet.js new file mode 100644 index 0000000..ee3ee59 --- /dev/null +++ b/sockets/set/userSet.js @@ -0,0 +1,43 @@ +const models = require("../../models"); + +module.exports = socket => { + return async (data) => { + if (!data || !data.user) { + socket.emit("userSet", {error: {message: "missing_arguments"}}); + return; + } + let u = await models.User.findByPk(data.user); + if (!u) { + socket.emit("userSet", {error: {message: "user_not_found"}}); + return; + } + for (let i in data) + switch (i) { + case "firstName": + u.firstName = data.firstName; + break; + case "lastName": + u.lastName = data.lastName; + break; + case "password": + u.passwordHash = data.password; + break; + case "permissions": + u.permissions = data.permissions; + break; + case "groups": + let groups = []; + for (let g of data.groups) { + let gp = await models.Group.findByPk(g); + if (!gp) { + socket.emit("userGet", {error: {message: "group_not_found"}}); + return + } + groups.push(gp); + } + u.setGroups(groups); + break; + } + await u.save(); + } +}; diff --git a/sockets/utils/emailPassword.js b/sockets/utils/emailPassword.js index 3bc7132..281499d 100644 --- a/sockets/utils/emailPassword.js +++ b/sockets/utils/emailPassword.js @@ -16,7 +16,7 @@ module.exports = async (socket, user, callBack) => { subject: "forgot password" }), async (err, message) => { if (err) - socket.emit("forgotPassword", {error: {message: "fail_send_mail"}}) + socket.emit("forgotPassword", {error: {message: "fail_send_mail"}}); else { user.passwordToken = token; user.passwordTokenDate = new Date(); diff --git a/test/test-pages.js b/test/test-pages.js index f4afba0..c75dee1 100644 --- a/test/test-pages.js +++ b/test/test-pages.js @@ -20,7 +20,7 @@ async function clean() { before(async () => { [app, models] = await setup(); -}) +}); it("Main page content", async () => { await request(app) diff --git a/views/pages/edt.pug b/views/pages/edt.pug index ace08ac..028d26c 100644 --- a/views/pages/edt.pug +++ b/views/pages/edt.pug @@ -1,87 +1,100 @@ extends ../template/navbar block content - table(id="edttable") - tr - th Monday - th Tuesday - th Wednesday - th Thursday - th Friday - tr - td - p Maths - p S26 - td - p Maths - p S26 - td - p Maths - p S26 - td - p Maths - p S26 - td - p Maths - p S26 - tr - td - p Maths - p S26 - td - p Maths - p S26 - td - p Maths - p S26 - td - p Maths - p S26 - td - p Maths - p S26 - tr(class="midi") - td - td - td - td - td - tr - td - p Maths - p S26 - td - p Maths - p S26 - td - p Maths - p S26 - td - p Maths - p S26 - td - p Maths - p S26 - tr - td - p Maths - p S26 - td - p Maths - p S26 - td - p Maths - p S26 - td - p Maths - p S26 - td - p Maths - p S26 - div(class="row" id="edtweek") - div(class="col s3 offset-s1") - i(class="large material-icons") fast_rewind - div(class="col s4") - h3 Week of the 02/11/2020 - div(class="col s3") - i(class="large material-icons") fast_forward \ No newline at end of file + - let variable = [{name:"",ue:{id:0,name:""},teachers:[{id:0,firstName:"",lastName:""}],location:[""],dateStart:Date,dateEnd:Date,group:{id:0,number:0,promotion:{id:0,name:""}},promotion:{id:0,name:""}}]; + div.detailsedt#visibl + p Maths + p S26 + p Mr. Jaloux + p G4S3 + p 08h à 10h + p UE------------ + + div + table(id="edttable") + tr + th Monday + th Tuesday + th Wednesday + th Thursday + th Friday + tr + td + p Maths + p S26 + td + p Maths + p S26 + td + p Maths + p S26 + td + p Maths + p S26 + td + p Maths + p S26 + tr + td + p Maths + p S26 + td + p Maths + p S26 + td + p Maths + p S26 + td + p Maths + p S26 + td + p Maths + p S26 + tr(class="midi") + td + td + td + td + td + tr + td + p Maths + p S26 + td + p Maths + p S26 + td + p Maths + p S26 + td + p Maths + p S26 + td + p Maths + p S26 + tr + td + p Maths + p S26 + td + p Maths + p S26 + td + p Maths + p S26 + td + p Maths + p S26 + td + p Maths + p S26 + + div(class="row" id="edtweek") + div(class="col s3 offset-s1") + i(class="large material-icons") fast_rewind + div(class="col s4") + h3 Week of the 02/11/2020 + div(class="col s3") + i(class="large material-icons") fast_forward + + diff --git a/views/pages/viescol.pug b/views/pages/viescol.pug index e3efa17..ffc10ed 100644 --- a/views/pages/viescol.pug +++ b/views/pages/viescol.pug @@ -1,7 +1,7 @@ extends ../template/navbar block content - div.managesub#visible + div.managesub#visibl p.topicname Tick the topics you would like to see in your news feed form p diff --git a/views/template/layout.pug b/views/template/layout.pug index 165f7ee..1a421a8 100644 --- a/views/template/layout.pug +++ b/views/template/layout.pug @@ -5,15 +5,24 @@ html link(href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet") link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css") link(rel="stylesheet", href="/stylesheets/style.css") + link(rel="icon" type="image/png" href="/images/favicon.ico") + link(rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png") + link(rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png") + link(rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png") + link(rel="manifest" href="/site.webmanifest") + link(rel="mask-icon" href="/images/safari-pinned-tab.svg" color="#5bbad5") + meta(name="msapplication-TileColor" content="#da532c") + meta(name="theme-color" content="#ffffff") script(src="/socket.io/socket.io.js") body - var student = true - var teacher = false - var admin = false div(class="row" id="page") - div(class="col s2" id="panel") - block navbar - div(class="col s10" id="main") + block navbar + div(class="col s12" id="main") block content script(src="/javascripts/main.js") script(src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js") + script. + M.AutoInit(); diff --git a/views/template/navbar.pug b/views/template/navbar.pug index 2da3620..4b0baaa 100644 --- a/views/template/navbar.pug +++ b/views/template/navbar.pug @@ -1,54 +1,69 @@ extends layout block navbar - if student === true - div(id="navprofile" class="student" onclick="profilRedirect()") - p Kezel Benoit - p G4S3 - a(id="logout") Logout - if teacher === true - div(id="navprofile" class="teacher" onclick="profilRedirect()") - p Kezel Benoit - a(id="logout") Logout - if admin === true - div(id="navprofile" class="admin" onclick="profilRedirect()") - p Kezel Benoit - a(id="logout") Logout + ul#slide-out.sidenav + if session.user.permissions === 1 + li + .user-view.student + p + span.white-text.name=session.user.firstName + " " + session.user.lastName + p + if session.user.Groups[session.user.Groups.length-1] + if session.user.Groups[session.user.Groups.length-1].number.startsWith("G") + span.white-text.name=session.user.Groups[session.user.Groups.length-1].number + session.user.Groups[session.user.Groups.length-1].Semester.name + else + span.white-text.name=session.user.Groups[session.user.Groups.length-1].Semester.name + " " + session.user.Groups[session.user.Groups.length-1].number + p + span.white-text.email=session.user.email - div(id="navtop") - ul - if student === true - li(class="active") - a(href="/") Home - li - a(href="/edt") Planning - li - a(href="/marks") Marks - li - a(href="https://mail.univ-lyon1.fr/owa/") Mail - li - a(href="https://clarolineconnect.univ-lyon1.fr/") Claroline - li - a(href="/viescol") School and student life - if teacher === true - li(class="active") - a(href="/") Home - li - a(href="/edt") Planning - li - a(href="/marks") Marks - li - a(href="https://mail.univ-lyon1.fr/owa/") Mail - li - a(href="https://clarolineconnect.univ-lyon1.fr/") Claroline Connect - li - a(href="/viescol") School and student life - if admin === true - li(class="active") - a Accueil - li - a Notes - li - a Vie scolaire et étudiante - li - a Gestion des profils + li + a(href="/" class="waves-effect") Home + li + a(href="/edt" class="waves-effect") Planning + li(class="active") + a(href="/marks" class="waves-effect") Marks + li + a(href="https://mail.univ-lyon1.fr/owa/" class="waves-effect") Mail + li + a(href="https://clarolineconnect.univ-lyon1.fr/" class="waves-effect") Claroline + li + a(href="/viescol" class="waves-effect") School and student life + + if session.user.permissions === 2 + li + .user-view.teacher + p(href='#name') + span.white-text.name=session.user.firstName + " " + session.user.lastName + p(href='#email') + span.white-text.email=session.user.email + li + a(href="/" class="waves-effect") Home + li + a(href="/edt" class="waves-effect") Planning + li(class="active") + a(href="/marks" class="waves-effect") Marks + li + a(href="https://mail.univ-lyon1.fr/owa/" class="waves-effect") Mail + li + a(href="https://clarolineconnect.univ-lyon1.fr/" class="waves-effect") Claroline + li + a(href="/viescol" class="waves-effect") School and student life + + if session.user.permissions === 3 + li + .user-view.admin + p(href='#name') + span.white-text.name=session.user.firstName + " " + session.user.lastName + p(href='#email') + span.white-text.email=session.user.email + li + a(href="/" class="waves-effect") Home + li(class="active") + a(href="/marks" class="waves-effect") Marks + li + a(href="/viescol" class="waves-effect") School and student life + li + a(href="/viescol" class="waves-effect") Profil Edition + + a.sidenav-trigger(href='#' data-target='slide-out')#hamburger + i.material-icons.medium menu \ No newline at end of file