diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..8ab3485 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,3 @@ +{ + "esversion": 6 +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index cf92760..fbc37a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,8 +13,10 @@ before_script: - cp -a .env.example .env before_install: -- openssl aes-256-cbc -K $encrypted_d810adeb7664_key -iv $encrypted_d810adeb7664_iv - -in .travis/deploy_key.enc -out .travis/deploy_key -d + - if [ "$TRAVIS_BRANCH" == "master" ]; then + openssl aes-256-cbc -K $encrypted_d810adeb7664_key -iv $encrypted_d810adeb7664_iv + -in .travis/deploy_key.enc -out .travis/deploy_key -d; + fi install: npm install diff --git a/README.md b/README.md index 7b86d05..e6b5604 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ L'API de calendz est développée avec les frameworks et outils suivants* : | Librairie | Version | Description | | ---------------- | ---------- | ------------------------------------------------------------------------------------------------ | -| [Node.js] | 12.16.0 | Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. | +| [Node.js] | 12.18.3 | Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. | | [Adonis.js] | 4.1 | The Node.js Framework highly focused on developer ergonomics, stability and confidence. | | [Cheerio] | 1.0.0-rc.3 | Fast, flexible, and lean implementation of core jQuery designed specifically for the server. | @@ -36,7 +36,7 @@ L'API de calendz est développée avec les frameworks et outils suivants* : ### Pré-requis -* Installer Node 12.16.0 +* Installer Node 12.18.3 * Créer un fichier `.env` à la source de ce repository contenant les valeurs suivantes (modifiables selon vos besoins) NODE_ENV=development diff --git a/app/Controllers/Http/MonthController.js b/app/Controllers/Http/MonthController.js new file mode 100644 index 0000000..ca22438 --- /dev/null +++ b/app/Controllers/Http/MonthController.js @@ -0,0 +1,57 @@ +'use strict' + +const moment = require('moment') +const Cache = use('Cache') +const Scrapper = use('Scrapper') +const ScrappingException = use('App/Exceptions/ScrappingException') + +const numberOfWeekByMonth = 4 + +class MonthController { + /** + * Get courses of the current month + */ + async getCurrent ({ request }) { + const { firstname, lastname, ignoreCache } = request.only(['firstname', 'lastname', 'ignoreCache']) + + const result = [] + + for (let i = 0; i < numberOfWeekByMonth; i++) { + let cacheUsed = false + // get current date (+ i * 7 days) and format with moment + const date = moment(new Date()).add(i * 7, 'd').format('MM/DD/YY') + + // --------------------------------------------------- + // Use cache (or at least, try to) + + if (!ignoreCache) { + // Get data from cache + const data = await Cache.getWeek(firstname, lastname, date) + if (data) { + cacheUsed = true + result.push(data) + } + } + + // --------------------------------------------------- + // Data not in cache: scrapping + + if (ignoreCache || !cacheUsed) { + // if not cached, scrap it + const data = await Scrapper.fetchWeek(firstname, lastname, date) + .catch(() => { /* istanbul ignore next */ throw new ScrappingException() }) + + result.push(data) + + // set scrap result in cache + await Cache.setWeek(firstname, lastname, date, data) + // indicate to not restart scrapping today + await Cache.setIsDailyScrapped(firstname, lastname, date) + } + } + + return result + } +} + +module.exports = MonthController diff --git a/app/Controllers/Http/TeamsController.js b/app/Controllers/Http/TeamsController.js new file mode 100644 index 0000000..2d9ee91 --- /dev/null +++ b/app/Controllers/Http/TeamsController.js @@ -0,0 +1,20 @@ +'use strict' + +const Scrapper = use('Scrapper') +const ScrappingException = use('App/Exceptions/ScrappingException') + +class TeamsController { + /** + * Get Microsoft Teams current links + */ + async get ({ request }) { + const { firstname, lastname } = request.only(['firstname', 'lastname']) + + const result = await Scrapper.fetchTeamsLinks(firstname, lastname) + .catch(() => { /* istanbul ignore next */ throw new ScrappingException() }) + + return result + } +} + +module.exports = TeamsController diff --git a/app/Controllers/Http/WeekController.js b/app/Controllers/Http/WeekController.js index 513cfca..72939fd 100644 --- a/app/Controllers/Http/WeekController.js +++ b/app/Controllers/Http/WeekController.js @@ -9,7 +9,9 @@ const InvalidDateException = use('App/Exceptions/InvalidDateException') class WeekController { /** + * ============================================================== * Get courses of the current week + * ============================================================== */ async getCurrent ({ request }) { const { firstname, lastname, ignoreCache } = request.only(['firstname', 'lastname', 'ignoreCache']) @@ -37,7 +39,9 @@ class WeekController { } /** + * ============================================================== * Get courses of a given week + * ============================================================== */ async getByDate ({ request, params }) { const { firstname, lastname, ignoreCache } = request.only(['firstname', 'lastname', 'ignoreCache']) @@ -45,14 +49,24 @@ class WeekController { // check if date is valid, if yes format it if (!DateUtils.isValid(params.date)) throw new InvalidDateException() const date = moment(params.date, 'MM-DD-YYYY').format('MM/DD/YY') + const translatedDate = DateUtils.getWeekNumber(date) + + // --------------------------------------------------- + // Use cache (or at least, try to) - // try to retrieve and return data from cache/redis if (!ignoreCache) { + // Get data from cache const data = await Cache.getWeek(firstname, lastname, date) - if (data) return data + if (data) { + const scrappedToday = await Cache.getIsDailyScrapped(firstname, lastname, date) + return { ...data, scrappedToday } + } } - // if not cached, scrap it + // --------------------------------------------------- + // Data not in cache: scrapping + + // start scrapping const result = await Scrapper.fetchWeek(firstname, lastname, date) .catch(() => { /* istanbul ignore next */ @@ -61,8 +75,44 @@ class WeekController { // set scrap result in cache await Cache.setWeek(firstname, lastname, date, result) + // indicate to not restart scrapping today + await Cache.setIsDailyScrapped(firstname, lastname, date) - return result + return { ...result, scrappedToday: true, weekNumber: translatedDate.number } + } + + /** + * ============================================================== + * Route used by Calendz for background actualization + * ============================================================== + */ + async updateByDate ({ request, params }) { + const { firstname, lastname } = request.only(['firstname', 'lastname']) + + // check if date is valid, if yes format it + if (!DateUtils.isValid(params.date)) throw new InvalidDateException() + const date = moment(params.date, 'MM-DD-YYYY').format('MM/DD/YY') + const translatedDate = DateUtils.getWeekNumber(date) + + // --------------------------------------------------- + // Get both CACHED and SCRAPPED data + + const cachedData = await Cache.getWeek(firstname, lastname, date) + + let scrappedData = await Scrapper.fetchWeek(firstname, lastname, date) + .catch(() => { + /* istanbul ignore next */ + throw new ScrappingException() + }) + scrappedData = { ...scrappedData, weekNumber: translatedDate.number } + await Cache.setIsDailyScrapped(firstname, lastname, date) + + // --------------------------------------------------- + // Compare data + + return JSON.stringify(scrappedData) === JSON.stringify(cachedData) + ? { update: false } + : { update: true, ...scrappedData } } } diff --git a/app/Middleware/ConvertIcal.js b/app/Middleware/ConvertIcal.js new file mode 100644 index 0000000..55dc9e7 --- /dev/null +++ b/app/Middleware/ConvertIcal.js @@ -0,0 +1,55 @@ +/* istanbul ignore file */ +'use strict' + +const ical = require('ical-generator') +const moment = require('moment') + +class ConvertIcal { + async handle ({ response, request }, next) { + await next() + + const format = request.input('format') + + if (format === 'icalendar') { + const cal = ical() + + const events = findValuesHelper(response._lazyBody, 'date', []) + + events.forEach(event => { + cal.createEvent({ + start: moment(event.date + ' ' + event.start, 'DD/MM/YYYY hh:mm').toDate(), + end: moment(event.date + ' ' + event.end, 'DD/MM/YYYY hh:mm').toDate(), + summary: event.subject, + description: event.professor, + location: event.room + }) + }) + + response._lazyBody.content = cal.toString() + response.safeHeader('Content-type', 'text/calendar') + } + } +} + +function findValuesHelper (obj, key, list) { + if (!obj) return list + if (obj instanceof Array) { + for (var i in obj) { + list = list.concat(findValuesHelper(obj[i], key, [])) + } + return list + } + if (obj[key]) list.push(obj) + + if ((typeof obj === 'object') && (obj !== null)) { + var children = Object.keys(obj) + if (children.length > 0) { + for (i = 0; i < children.length; i++) { + list = list.concat(findValuesHelper(obj[children[i]], key, [])) + } + } + } + return list +} + +module.exports = ConvertIcal diff --git a/app/Middleware/ForceJson.js b/app/Middleware/ForceJson.js deleted file mode 100644 index 77a4c36..0000000 --- a/app/Middleware/ForceJson.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict' - -class ForceJson { - async handle ({ request }, next) { - request.request.headers.accept = 'application/json' - await next() - } -} - -module.exports = ForceJson diff --git a/package-lock.json b/package-lock.json index 192e9fc..59619ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "calendz-api-calendar", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -146,9 +146,9 @@ } }, "@adonisjs/lucid": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@adonisjs/lucid/-/lucid-6.2.0.tgz", - "integrity": "sha512-1MUOYIELJ5J6uFeZFry+mYxAN5cW3c4TCv+7okpzt75I5RxmEXp08DG66ZuLPBy7MNeq0mDEWedOl4KD5xK1rA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@adonisjs/lucid/-/lucid-6.2.1.tgz", + "integrity": "sha512-2HEID7fqZ91GCNCb9/YqcL5Ac6PVAB6eEEz+YTOzd2mg9BUHdwVdAkQwYXiVUtACyYT4YhEX0jzKblWZrqm4Pw==", "requires": { "@adonisjs/generic-exceptions": "^2.0.1", "chance": "^1.0.18", @@ -425,19 +425,12 @@ "dev": true }, "@babel/polyfill": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.10.4.tgz", - "integrity": "sha512-8BYcnVqQ5kMD2HXoHInBH7H1b/uP3KdnwCYXOqFnXqguOyuu443WXusbIUbWEfY3Z0Txk0M1uG/8YuAMhNl6zg==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.11.5.tgz", + "integrity": "sha512-FunXnE0Sgpd61pKSj2OSOs1D44rKTD3pGOfGilZ6LGrrIH0QEtJlTjqOqdF8Bs98JmjfGhni2BBkTfv9KcKJ9g==", "requires": { "core-js": "^2.6.5", "regenerator-runtime": "^0.13.4" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" - } } }, "@babel/template": { @@ -640,6 +633,59 @@ "dev": true, "optional": true }, + "@eslint/eslintrc": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz", + "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", + "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -778,9 +824,9 @@ "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==" }, "acorn-jsx": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", - "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", "dev": true }, "acorn-node": { @@ -1469,9 +1515,9 @@ } }, "chance": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.6.tgz", - "integrity": "sha512-DXLzaGjasDWbvlFAJyQBIwlzdQZuPdz4of9TTTxmHTjja88ZU/vBwUwxxjalSt43zWTPrhiJT0z0N4bZqfZS9w==" + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.7.tgz", + "integrity": "sha512-bua/2cZEfzS6qPm0vi3JEvGNbriDLcMj9lKxCQOjUcCJRcyjA7umP0zZm6bKWWlBN04vA0L99QGH/CZQawr0eg==" }, "chardet": { "version": "0.7.0", @@ -1799,9 +1845,9 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "commitizen": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.2.0.tgz", - "integrity": "sha512-dMTgCkmxDOaH32HViVyvoGqVzCAyvBsgMViWnIDQxVHlEwHS1d6d8rweCGE7fjCROryBZmHRQrWVWN9GTw2F/g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.2.1.tgz", + "integrity": "sha512-nZsp8IThkDu7C+93BFD/mLShb9Gd6Wsaf90tpKE3x/6u5y/Q52kzanIJpGr0qvIsJ5bCMpgKtr3Lbu3miEJfaA==", "dev": true, "requires": { "cachedir": "2.2.0", @@ -2022,9 +2068,9 @@ "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" }, "cz-conventional-changelog": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.2.1.tgz", - "integrity": "sha512-MroXCfO8sahklT0KSwZ1oaNwKxZaZ2A8ONc+MH+is8QDZ2zBhGOFFiAXuWpcWFJLpxlGPvnYlhvOlYiSOAGc7w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz", + "integrity": "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==", "dev": true, "requires": { "@commitlint/load": ">6.1.1", @@ -2367,9 +2413,9 @@ }, "dependencies": { "is-callable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", - "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", "dev": true } } @@ -2402,22 +2448,23 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.7.0.tgz", - "integrity": "sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.11.0.tgz", + "integrity": "sha512-G9+qtYVCHaDi1ZuWzBsOWo2wSwd70TXnU6UHA3cTYHp7gCTXZcpggWFoUVAMRarg68qtPoNfFbzPh+VdOgmwmw==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.1.3", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.0.1", "doctrine": "^3.0.0", "enquirer": "^2.3.5", - "eslint-scope": "^5.1.0", + "eslint-scope": "^5.1.1", "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^1.3.0", - "espree": "^7.2.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.0", "esquery": "^1.2.0", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", @@ -2458,12 +2505,11 @@ "dev": true }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -2513,9 +2559,9 @@ } }, "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", "dev": true }, "glob-parent": { @@ -2600,9 +2646,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -2707,9 +2753,9 @@ } }, "eslint-plugin-import": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz", - "integrity": "sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==", + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", + "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", "dev": true, "requires": { "array-includes": "^3.1.1", @@ -2717,7 +2763,7 @@ "contains-path": "^0.1.0", "debug": "^2.6.9", "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.3", + "eslint-import-resolver-node": "^0.3.4", "eslint-module-utils": "^2.6.0", "has": "^1.0.3", "minimatch": "^3.0.4", @@ -2813,12 +2859,12 @@ "dev": true }, "eslint-scope": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", - "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "requires": { - "esrecurse": "^4.1.0", + "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, @@ -2838,20 +2884,20 @@ "dev": true }, "espree": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.2.0.tgz", - "integrity": "sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", + "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", "dev": true, "requires": { - "acorn": "^7.3.1", + "acorn": "^7.4.0", "acorn-jsx": "^5.2.0", "eslint-visitor-keys": "^1.3.0" }, "dependencies": { "acorn": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", - "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true }, "eslint-visitor-keys": { @@ -2885,12 +2931,20 @@ } }, "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "requires": { - "estraverse": "^4.1.0" + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } }, "estraverse": { @@ -3786,15 +3840,15 @@ } }, "husky": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.5.tgz", - "integrity": "sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.0.tgz", + "integrity": "sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==", "dev": true, "requires": { "chalk": "^4.0.0", "ci-info": "^2.0.0", "compare-versions": "^3.6.0", - "cosmiconfig": "^6.0.0", + "cosmiconfig": "^7.0.0", "find-versions": "^3.2.0", "opencollective-postinstall": "^2.0.2", "pkg-dir": "^4.2.0", @@ -3814,9 +3868,9 @@ } }, "chalk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", - "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -3839,16 +3893,16 @@ "dev": true }, "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", "dev": true, "requires": { "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", + "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", - "yaml": "^1.7.2" + "yaml": "^1.10.0" } }, "find-up": { @@ -3900,18 +3954,6 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, - "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", - "lines-and-columns": "^1.1.6" - } - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3940,9 +3982,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -3950,6 +3992,14 @@ } } }, + "ical-generator": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/ical-generator/-/ical-generator-1.15.1.tgz", + "integrity": "sha512-ZfrZYywBigtET2SPXtf+1qLupXx9KCWfWoHWjGs1k/hXWma8PtrhthWI3EdsLmWFyVvx2YzP/0Jh4sLAx7JVpw==", + "requires": { + "moment-timezone": "^0.5.31" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4227,6 +4277,12 @@ "is-extglob": "^2.1.0" } }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true + }, "is-number": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz", @@ -4241,9 +4297,9 @@ } }, "is-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", - "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "dev": true, "requires": { "has-symbols": "^1.0.1" @@ -4514,12 +4570,6 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, "json-parse-even-better-errors": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.0.tgz", @@ -5166,9 +5216,17 @@ } }, "moment": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", - "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.0.tgz", + "integrity": "sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA==" + }, + "moment-timezone": { + "version": "0.5.31", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz", + "integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==", + "requires": { + "moment": ">= 2.9.0" + } }, "ms": { "version": "2.1.2", @@ -5519,15 +5577,43 @@ } }, "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz", + "integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + } } }, "object.defaults": { @@ -5601,9 +5687,9 @@ } }, "opencollective-postinstall": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz", - "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", "dev": true }, "optionator": { @@ -5721,7 +5807,6 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", "dev": true, - "optional": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -6556,6 +6641,11 @@ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -7222,9 +7312,9 @@ } }, "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", + "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==", "dev": true }, "split-string": { diff --git a/package.json b/package.json index b21788c..b9cee06 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "author": "Calendz", "license": "ISC", "private": true, - "version": "2.0.0", + "version": "2.1.0", "adonis-version": "4.1.0", "main": "index.js", "scripts": { @@ -37,23 +37,24 @@ "@adonisjs/framework": "^5.0.9", "@adonisjs/http-logger": "^1.0.0", "@adonisjs/ignitor": "^2.0.8", - "@adonisjs/lucid": "^6.2.0", + "@adonisjs/lucid": "^6.2.1", "@adonisjs/redis": "^2.0.7", "@adonisjs/validator": "^5.0.6", "@adonisjs/vow": "^1.0.17", - "cheerio": "^1.0.0-rc.3" + "cheerio": "^1.0.0-rc.3", + "ical-generator": "^1.15.1" }, "devDependencies": { - "commitizen": "^4.2.0", + "commitizen": "^4.2.1", "coveralls": "^3.1.0", - "cz-conventional-changelog": "^3.2.1", - "eslint": "^7.7.0", + "cz-conventional-changelog": "^3.3.0", + "eslint": "^7.11.0", "eslint-config-standard": "^14.1.1", - "eslint-plugin-import": "^2.22.0", + "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", - "husky": "^4.2.5", + "husky": "^4.3.0", "nyc": "^15.1.0" }, "autoload": { diff --git a/providers/Cache/index.js b/providers/Cache/index.js index 8462024..6586c07 100644 --- a/providers/Cache/index.js +++ b/providers/Cache/index.js @@ -18,8 +18,11 @@ class Cache { */ async setWeek (firstname, lastname, date, data) { const translatedDate = DateUtils.getWeekNumber(date) - const expireIn = DateUtils.computeExpireMidnight() - await Redis.hmset(`u:${firstname}.${lastname}`, `y:${translatedDate.year}|w:${translatedDate.number}`, JSON.stringify(data)) + const expireIn = DateUtils.computeExpireFriday() + await Redis.hmset( + `u:${firstname}.${lastname}`, `y:${translatedDate.year}|w:${translatedDate.number}`, + JSON.stringify({ ...data, weekNumber: translatedDate.number }) + ) await Redis.expire(`u:${firstname}.${lastname}`, expireIn) } @@ -39,6 +42,31 @@ class Cache { await Redis.hmset(`u:${firstname}.${lastname}`, `d:${date}`, JSON.stringify(data)) await Redis.expire(`u:${firstname}.${lastname}`, expireIn) } + + /* + |-------------------------------------------------------------------------- + | Background actualization + |-------------------------------------------------------------------------- + */ + + /** + * Indicate that requested date has already been scrapped today + */ + async setIsDailyScrapped (firstname, lastname, date) { + const translatedDate = DateUtils.getWeekNumber(date) + const expireIn = DateUtils.computeExpireMidnight() + await Redis.hmset(`u:${firstname}.${lastname}|daily`, `y:${translatedDate.year}|w:${translatedDate.number}`, 'true') + await Redis.expire(`u:${firstname}.${lastname}|daily`, expireIn) + } + + /** + * Check if date has already been scrapped today + */ + async getIsDailyScrapped (firstname, lastname, date) { + const translatedDate = DateUtils.getWeekNumber(date) + const result = await Redis.hget(`u:${firstname}.${lastname}|daily`, [`y:${translatedDate.year}|w:${translatedDate.number}`]) + return result || false + } } module.exports = Cache diff --git a/providers/DateUtils/index.js b/providers/DateUtils/index.js index d75bd52..6a10f0a 100644 --- a/providers/DateUtils/index.js +++ b/providers/DateUtils/index.js @@ -115,6 +115,15 @@ class DateUtils { const expireIn = Math.abs((new Date(expireDate).getTime() - today.getTime()) / 1000) return expireIn } + + computeExpireFriday () { + const temp = new Date() + const today = new Date() + const friday = temp.setDate(today.getDate() + (7 + 5 - today.getDay()) % 7) + const expireDate = new Date(friday).setHours(4, 0, 0) + const expireIn = Math.abs((new Date(expireDate).getTime() - today.getTime()) / 1000) + return expireIn + } } module.exports = DateUtils diff --git a/providers/Scrapper/index.js b/providers/Scrapper/index.js index 3da7b17..4ec65f6 100644 --- a/providers/Scrapper/index.js +++ b/providers/Scrapper/index.js @@ -8,17 +8,33 @@ const dateUtils = use('DateUtils') class Scrapper { /** - * Get week courses of a given date for a given student - */ + | -------------------------------------------------------------- + | Get week courses of a given date for a given student + | -------------------------------------------------------------- + */ + async fetchWeek (firstname, lastname, queriedDate) { + let result = null + let success = false + + while (!success) { + result = await this.scrapWeek(firstname, lastname, queriedDate) + .then((res) => { success = true; return res }) + .catch((err) => { if (err.message !== 'E_SCRAPPING_PARAMETERS') success = true }) + } + + return result + } + + async scrapWeek (firstname, lastname, queriedDate) { return new Promise((resolve, reject) => { const CALENDAR_URL_TO_SCRAP = 'https://edtmobiliteng.wigorservices.net//WebPsDyn.aspx?action=posEDTBEECOME&serverid=i' request(`${CALENDAR_URL_TO_SCRAP}&Tel=${firstname}.${lastname}&date=${queriedDate}`, (err, resp, html) => { /* istanbul ignore if */ - if (err || !html || resp.statusCode !== 200) { - return reject(new Error('An error has occurred whilst trying to scrape the agenda')) - } + if (err || !html || resp.statusCode !== 200) return reject(new Error('An error has occurred whilst trying to scrape the agenda')) + /* istanbul ignore if */ + if (html.includes('Erreur de parametres')) return reject(new Error('E_SCRAPPING_PARAMETERS')) // init the response object let result = {} @@ -72,17 +88,7 @@ class Scrapper { presence = false } - const data = { - date, - subject, - start, - end, - professor, - room, - weekday, - bts, - presence - } + const data = { date, subject, start, end, professor, room, weekday, bts, presence } if (result[key][weekday]) { result[key][weekday].push(data) @@ -92,6 +98,7 @@ class Scrapper { } }) }) + // return response object result = regroupCourses(result) resolve(result) @@ -100,9 +107,25 @@ class Scrapper { } /** - * Get day courses of a given date for a given student - */ - async fetchDay (firstname, lastname, date) { + | -------------------------------------------------------------- + | Get day courses of a given date for a given student + | -------------------------------------------------------------- + */ + + async fetchDay (firstname, lastname, queriedDate) { + let result = null + let success = false + + while (!success) { + result = await this.scrapDay(firstname, lastname, queriedDate) + .then((res) => { success = true; return res }) + .catch((err) => { if (err.message !== 'E_SCRAPPING_PARAMETERS') success = true }) + } + + return result + } + + async scrapDay (firstname, lastname, date) { return new Promise((resolve, reject) => { const CALENDAR_URL_TO_SCRAP = 'http://edtmobilite.wigorservices.net/WebPsDyn.aspx?Action=posETUD&serverid=i' @@ -131,15 +154,7 @@ class Scrapper { const room = $(el).children('div.Salle').text() const weekday = dateUtils.getDayFromInt(moment(date, 'MM/DD/YY').day()) - const data = { - date, - subject, - start, - end, - professor, - room, - weekday - } + const data = { date, subject, start, end, professor, room, weekday } // push the data in the response object result[key].push(data) @@ -149,6 +164,54 @@ class Scrapper { }) }) } + + /** + | -------------------------------------------------------------- + | Get Microsoft Teams current links + | -------------------------------------------------------------- + */ + + async fetchTeamsLinks (firstname, lastname) { + // get current date and format with moment + const date = moment(new Date()).format('MM/DD/YYYY') + + return new Promise((resolve, reject) => { + const CALENDAR_URL_TO_SCRAP = 'https://edtmobiliteng.wigorservices.net//WebPsDyn.aspx?action=posEDTBEECOME&serverid=i' + + request(`${CALENDAR_URL_TO_SCRAP}&Tel=${firstname}.${lastname}&date=${date}`, (err, resp, html) => { + /* istanbul ignore if */ + if (err || !html || resp.statusCode !== 200) return reject(new Error('An error has occurred whilst trying to scrape the agenda')) + /* istanbul ignore if */ + if (html.includes('Erreur de parametres')) return reject(new Error('E_SCRAPPING_PARAMETERS')) + + // init the response object + const result = [] + + // load the html of the page in the $ variable + const $ = cheerio.load(html, { decodeEntities: false }) + + $('div.Teams').each((i, el) => { + $(el).children('a').each((i, el) => { + const subject = $(el).parent().parent().text() + const link = $(el).attr('href') + const start = $(el).parent().parent().parent().parent().children('tr').children('td.TChdeb').html().substr(0, 5) + const end = $(el).parent().parent().parent().parent().children('tr').children('td.TChdeb').html().substr(8, 5) + + // add subject if it doesn't exist + const index = result.findIndex(el => el.subject === subject && el.start === start && el.end === end) + if (index === -1) { + result.push({ subject, start, end, links: [link] }) + } else { + result[index].links.push(link) + } + }) + }) + + // return response object + resolve(result) + }) + }) + } } function regroupCourses (result) { diff --git a/start/kernel.js b/start/kernel.js index 8dc297a..370df02 100644 --- a/start/kernel.js +++ b/start/kernel.js @@ -13,8 +13,8 @@ const Server = use('Server') */ const globalMiddleware = [ 'Adonis/Middleware/BodyParser', - 'App/Middleware/ForceJson', - 'App/Middleware/ConvertEmptyStringsToNull' + 'App/Middleware/ConvertEmptyStringsToNull', + 'App/Middleware/ConvertIcal' ] /* diff --git a/start/routes.js b/start/routes.js index a1c7c68..04f1c1d 100644 --- a/start/routes.js +++ b/start/routes.js @@ -24,8 +24,15 @@ Route.group(() => { // Get with current date Route.get('/day', 'DayController.getCurrent').validator('RequireName') Route.get('/week', 'WeekController.getCurrent').validator('RequireName') + Route.get('/month', 'MonthController.getCurrent').validator('RequireName') // Get with specific date Route.get('/day/:date', 'DayController.getByDate').validator('RequireName') Route.get('/week/:date', 'WeekController.getByDate').validator('RequireName') + + // Get Microsoft Teams current links + Route.get('/teams', 'TeamsController.get').validator('RequireName') + + // Route used by Calendz for background actualization + Route.get('/week/:date/update', 'WeekController.updateByDate').validator('RequireName') }).prefix('/v1') diff --git a/test/functional/teams-get.spec.js b/test/functional/teams-get.spec.js new file mode 100644 index 0000000..6385282 --- /dev/null +++ b/test/functional/teams-get.spec.js @@ -0,0 +1,30 @@ +const { test, trait } = use('Test/Suite')('Teams Get') +const { testRequireField } = require('../helpers') + +trait('Test/ApiClient') + +const data = { + firstname: 'Arthur', + lastname: 'Dufour' +} + +// =============================================================== +// == GET /v1/teams +// =============================================================== + +test('should test that firstname is required', async ({ client }) => { + await testRequireField('firstname', data, '/v1/teams', client) +}) + +test('should test that lastname is required', async ({ client }) => { + await testRequireField('lastname', data, '/v1/teams', client) +}) + +test('should not throw an error', async ({ client, assert }) => { + const response = await client + .get('/v1/teams') + .send(data) + .end() + + response.assertStatus(200) +}).timeout(0) diff --git a/test/functional/week-get.spec.js b/test/functional/week-get.spec.js index a2b6746..60966a4 100644 --- a/test/functional/week-get.spec.js +++ b/test/functional/week-get.spec.js @@ -99,3 +99,33 @@ test('should return the week\'s courses without cache (ignoreCache)', async ({ c assert.property(response.body, 'week') response.assertStatus(200) }).timeout(0) + +// =============================================================== +// == GET /v1/week/:date/update +// =============================================================== + +test('should test that firstname is required', async ({ client }) => { + await testRequireField('firstname', data, '/v1/week/05-25-2020/update', client) +}) + +test('should test that lastname is required', async ({ client }) => { + await testRequireField('lastname', data, '/v1/week/05-25-2020/update', client) +}) + +test('should test that date is invalid', async ({ client }) => { + await testInvalidDate(data, '/v1/week/invalid-date/update', client) +}) + +test('should test that date is invalid', async ({ client }) => { + await testInvalidDate(data, '/v1/week/13-05-2020/update', client) +}) + +test('should return the week\'s courses', async ({ client, assert }) => { + const response = await client + .get('/v1/week/04-25-2020/update') + .send(data) + .end() + + assert.property(response.body, 'update') + response.assertStatus(200) +}).timeout(0) diff --git a/test/helpers/requireField.js b/test/helpers/requireField.js index e969419..beeb62d 100644 --- a/test/helpers/requireField.js +++ b/test/helpers/requireField.js @@ -4,6 +4,7 @@ module.exports = async function testRequireField (field, data, endpoint, client) const response = await client .get(endpoint) .send(omit(data, field)) + .header('accept', 'application/json') .end() response.assertStatus(400)