From f09744d0aad137be2fee2bfe9bfd8610ea346aeb Mon Sep 17 00:00:00 2001 From: Matti Luukkainen Date: Fri, 26 Jan 2024 13:35:04 +0200 Subject: [PATCH] extended device requests v0.01 --- client/components/AdminPage/AdminFilter.js | 3 + client/components/AdminPage/AllUsersTab.js | 11 +- .../components/AdminPage/UserTable/index.js | 14 ++ .../StudentPage/ExtendedRequestDeviceForm.js | 95 ++++++++++ .../StudentPage/ExtendedTaskStatus.js | 78 +++++++++ client/components/StudentPage/NotEligible.js | 8 +- client/components/StudentPage/index.js | 4 + client/util/fakeShibboleth.js | 2 + client/util/redux/deviceRequestReducer.js | 4 +- client/util/useTranslation.js | 3 +- docker-compose.yml | 4 +- ...25203210-extended-device-fields-to-user.js | 17 ++ server/controllers/userController.js | 10 +- server/middleware/authenticationMiddleware.js | 8 +- server/middleware/studentMiddleware.js | 1 + server/models/lib/apiInterface.js | 9 +- server/models/lib/mock.js | 162 +++++++++++++++++- server/models/user.js | 41 ++++- 18 files changed, 441 insertions(+), 33 deletions(-) create mode 100644 client/components/StudentPage/ExtendedRequestDeviceForm.js create mode 100644 client/components/StudentPage/ExtendedTaskStatus.js create mode 100644 migrations/20240125203210-extended-device-fields-to-user.js diff --git a/client/components/AdminPage/AdminFilter.js b/client/components/AdminPage/AdminFilter.js index 48d4c40..1f27738 100644 --- a/client/components/AdminPage/AdminFilter.js +++ b/client/components/AdminPage/AdminFilter.js @@ -20,6 +20,9 @@ export default function AdminFilter({ }, { key: 'currentYearEligible', name: 'Current years eligible students', + }, { + key: 'extendedRequester', + name: 'Extended requester', }, { key: 'allStaff', name: 'All staff (Admin/staff/distributor/reclaimer)', diff --git a/client/components/AdminPage/AllUsersTab.js b/client/components/AdminPage/AllUsersTab.js index 746f955..aee62a4 100644 --- a/client/components/AdminPage/AllUsersTab.js +++ b/client/components/AdminPage/AllUsersTab.js @@ -61,21 +61,26 @@ export default () => { switch (filter) { case 'all': filtered = users + hiddenColumns = ['ext_wants_device', 'ext_eligible'] break case 'deviceHolders': filtered = users.filter(u => !!u.deviceGivenAt && !u.deviceReturned) - hiddenColumns = ['admin', 'staff', 'distributor', 'reclaimer', 'eligible', 'digitaidot', 'enrolled', 'wants_device', 'mark_eligible', 'device_returned_at', 'device_returned_by'] + hiddenColumns = ['ext_wants_device', 'ext_eligible', 'admin', 'staff', 'distributor', 'reclaimer', 'eligible', 'digitaidot', 'enrolled', 'wants_device', 'mark_eligible', 'device_returned_at', 'device_returned_by'] break case 'returnedDevices': filtered = users.filter(u => u.deviceReturned) - hiddenColumns = ['admin', 'staff', 'distributor', 'reclaimer', 'eligible', 'digitaidot', 'enrolled', 'wants_device', 'mark_eligible'] + hiddenColumns = ['ext_wants_device', 'ext_eligible', 'admin', 'staff', 'distributor', 'reclaimer', 'eligible', 'digitaidot', 'enrolled', 'wants_device', 'mark_eligible'] break case 'currentYearEligible': filtered = users.filter(u => u.signupYear === settings.currentYear && u.eligible) - hiddenColumns = ['admin', 'staff', 'distributor', 'reclaimer', 'eligible', 'mark_eligible', 'device_returned_at', 'device_returned_by'] + hiddenColumns = ['ext_wants_device', 'ext_eligible', 'admin', 'staff', 'distributor', 'reclaimer', 'eligible', 'mark_eligible', 'device_returned_at', 'device_returned_by'] break case 'allStaff': filtered = users.filter(u => u.admin || u.staff || u.distributor || u.reclaimer) + hiddenColumns = ['ext_wants_device', 'ext_eligible', 'student_number', 'studyPrograms', 'eligible', 'digitaidot', 'enrolled', 'wants_device', 'device_given_at', 'device_id', 'device_distributed_by', 'mark_eligible', 'mark_returned', 'device_returned_at', 'device_returned_by'] + break + case 'extendedRequester': + filtered = users.filter(u => u.extendedEligible || u.extendedWantsDevice) hiddenColumns = ['student_number', 'studyPrograms', 'eligible', 'digitaidot', 'enrolled', 'wants_device', 'device_given_at', 'device_id', 'device_distributed_by', 'mark_eligible', 'mark_returned', 'device_returned_at', 'device_returned_by'] break default: diff --git a/client/components/AdminPage/UserTable/index.js b/client/components/AdminPage/UserTable/index.js index 2863e80..d9c2c6d 100644 --- a/client/components/AdminPage/UserTable/index.js +++ b/client/components/AdminPage/UserTable/index.js @@ -113,6 +113,13 @@ const UserTable = ({ getCellVal: ({ eligible }) => eligible, width: 80, }, + { + key: 'ext_eligible', + label: 'ExtEligible', + renderCell: ({ extendedEligible }) => boolToString(extendedEligible), + getCellVal: ({ extendedEligible }) => extendedEligible, + width: 80, + }, { key: 'digitaidot', label: 'Digi skills', @@ -134,6 +141,13 @@ const UserTable = ({ getCellVal: ({ wantsDevice }) => !!wantsDevice, width: 130, }, + { + key: 'ext_wants_device', + label: 'ExtWants', + renderCell: ({ extendedWantsDevice }) => boolToString(extendedWantsDevice), + getCellVal: ({ extendedWantsDevice }) => extendedWantsDevice, + width: 80, + }, { key: 'device_given_at', label: 'Device given at', diff --git a/client/components/StudentPage/ExtendedRequestDeviceForm.js b/client/components/StudentPage/ExtendedRequestDeviceForm.js new file mode 100644 index 0000000..bacfc90 --- /dev/null +++ b/client/components/StudentPage/ExtendedRequestDeviceForm.js @@ -0,0 +1,95 @@ +import React, { useState } from 'react' +import { useSelector, useDispatch } from 'react-redux' +import { + Button, Segment, Form, Checkbox, +} from 'semantic-ui-react' +import { deviceRequestAction } from 'Utilities/redux/deviceRequestReducer' +import { localeSelector } from 'Utilities/redux/localeReducer' +import InstructionModal from './InstructionModal' + +const translations = { + iWantDevice: { + en: 'I want a device', + fi: 'Haluan laitteen', + }, + hello: { + en: 'Hello', + fi: 'Hei', + }, + youAreEntitledToADevice: { + en: '...', + fi: 'Olet oikeutettu normaalin jakelun ulkopuolella annettavaan fuksilaitteeseen', + }, + email: { + en: 'Email', + fi: 'Sähköposti', + }, + termsAndConditions: { + en: 'Read the instructions', + fi: 'Lue ohjeet', + }, + areYouSure: { + en: 'Are you sure?', + fi: 'Oletko varma?', + }, + iHaveRead: { + en: 'I have read and understood the instructions', + fi: 'Olen lukenut ja ymmärtänyt ohjeet', + }, +} + +const ExtendedRequestDeviceForm = () => { + const [termsOpen, setTermsOpen] = useState(false) + const [termsHaveBeenOpened, setTermsHaveBeenOpened] = useState(false) + const [termsAccepted, setTermsAccepted] = useState(false) + const dispatch = useDispatch() + const user = useSelector(state => state.user.data) + const locale = useSelector(localeSelector) + + const handleRequestClick = () => { + dispatch(deviceRequestAction({ extended: true, email: null })) + } + + const handleTermsClose = () => setTermsOpen(false) + + const primaryButtonDisabled = !termsAccepted // Always disable if not valid + + const handleTermsOpen = () => { + setTermsOpen(true) + setTermsHaveBeenOpened(true) + } + + return ( +
+ + +

{`${translations.hello[locale]} ${user.name},`}

+

{translations.youAreEntitledToADevice[locale]}

+
+
+ + {translations.termsAndConditions[locale]} + + setTermsAccepted(!termsAccepted)} label={translations.iHaveRead[locale]} /> +
+
+ +
+
+
+
+ ) +} + +export default ExtendedRequestDeviceForm diff --git a/client/components/StudentPage/ExtendedTaskStatus.js b/client/components/StudentPage/ExtendedTaskStatus.js new file mode 100644 index 0000000..4ff6134 --- /dev/null +++ b/client/components/StudentPage/ExtendedTaskStatus.js @@ -0,0 +1,78 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { Segment, Icon, Header } from 'semantic-ui-react' +import { localeSelector } from 'Utilities/redux/localeReducer' +import TranslatedMarkdown from 'Components/TranslatedMarkdown' +import StudentInfo from './StudentInfo' +// import TaskInfo from './TaskInfo' + +const translations = { + taskStatus: { + en: 'Task status:', + fi: 'Tehtävien tila:', + }, + beFuksi: { + en: 'Be a fresher', + fi: 'Ole fuksi', + }, + registeredToRelevant: { + en: 'Registered to relevant course', + fi: 'Rekisteröitynyt relevantille kurssille', + }, + digiSkillsCompleted: { + en: 'DIGI-A completed', + fi: 'DIGI-A suoritettu', + }, + tasksFinished: { + en: 'You have completed the tasks required for fresher device. Info about device distribution will be sent to you by email.', + fi: 'Olet suorittanut fuksilaitteeseen vaadittavat tehtävät. Saat tiedot laitteiden jakelusta sähköpostitse.', + }, +} + +const Task = ({ task, completed }) => { + if (completed) { + return ( + + {task} + + + ) + } + return ( + + {task} + + + ) +} + +const fake = { + user: { + eligible: true, + courseRegistrationCompleted: true, + digiSkillsCompleted: true, + }, +} + +const StudentStatusPage = ({ faking }) => { + const user = faking ? fake.user : useSelector(state => state.user.data) + const locale = useSelector(localeSelector) + + return ( + + + {/* */} + + You will get an email when the device is ready! + + + + + + ) +} + +export default StudentStatusPage diff --git a/client/components/StudentPage/NotEligible.js b/client/components/StudentPage/NotEligible.js index e81e5ba..efadfea 100644 --- a/client/components/StudentPage/NotEligible.js +++ b/client/components/StudentPage/NotEligible.js @@ -44,6 +44,10 @@ const translations = { en: 'Name', fi: 'Nimi', }, + hasNotDeviceGiven: { + en: 'Has not a device given already', + fi: 'Et ole vielä saanut laitetta', + }, } const fake = { @@ -65,13 +69,15 @@ export default function NotEligible({ user, notCurrentYearsFuksi, faking }) { const EligibilityBreakdown = () => { if (!Object.entries(eligibilityReasons).length) return null // Only users starting from 2020 have eligibilityReasons (unless updated). + const reasons = Object.entries(eligibilityReasons).filter(r => r[0] !== 'hasNotDeviceGiven') + return ( <>
{translations.header[locale]}
- {Object.entries(eligibilityReasons).map(([key, status]) => { + {reasons.map(([key, status]) => { if (key === 'signedUpForFreshmanDeviceThisYear' && notCurrentYearsFuksi) { // eslint-disable-next-line no-param-reassign status = false diff --git a/client/components/StudentPage/index.js b/client/components/StudentPage/index.js index 89138de..5b18ca5 100644 --- a/client/components/StudentPage/index.js +++ b/client/components/StudentPage/index.js @@ -4,6 +4,8 @@ import RequestDeviceForm from './RequestDeviceForm' import TaskStatus from './TaskStatus' import DeviceInfo from './DeviceInfo' import NotEligible from './NotEligible' +import ExtendedRequestDeviceForm from './ExtendedRequestDeviceForm' +import ExtendedTaskStatus from './ExtendedTaskStatus' const StudentPage = () => { const user = useSelector(state => state.user.data) @@ -13,6 +15,8 @@ const StudentPage = () => { if (!user) return null if (user.deviceSerial) return + if (user.extendedEligible && !user.extendedWantsDevice) return + if (user.extendedEligible) return if (!user.eligible || notCurrentYearsFuksi) return if (!user.wantsDevice) return return diff --git a/client/util/fakeShibboleth.js b/client/util/fakeShibboleth.js index 228adb4..6022593 100644 --- a/client/util/fakeShibboleth.js +++ b/client/util/fakeShibboleth.js @@ -9,6 +9,7 @@ const possibleUsers = [ schacDateOfBirth: undefined, hyPersonStudentId: undefined, sn: 'admin', + hygroupcn: 'grp-toska', }, { uid: 'jakelija', @@ -45,6 +46,7 @@ const possibleUsers = [ schacDateOfBirth: 19850806, hyPersonStudentId: 'non-fuksi', sn: 'non-fuksi', + hygroupcn: 'grp-toska', }, { uid: 'fuksi_without_digiskills', diff --git a/client/util/redux/deviceRequestReducer.js b/client/util/redux/deviceRequestReducer.js index bf4bf26..514472e 100644 --- a/client/util/redux/deviceRequestReducer.js +++ b/client/util/redux/deviceRequestReducer.js @@ -3,10 +3,10 @@ import callBuilder from '../apiConnection' /** * Actions and reducers are in the same file for readability */ -export const deviceRequestAction = ({ email }) => { +export const deviceRequestAction = ({ email, extended }) => { const route = '/request_device' const prefix = 'NEW_DEVICE_REQUEST' - return callBuilder(route, prefix, 'post', { email }) + return callBuilder(route, prefix, 'post', { email, extended }) } // Reducer diff --git a/client/util/useTranslation.js b/client/util/useTranslation.js index 15f16f7..a3918af 100644 --- a/client/util/useTranslation.js +++ b/client/util/useTranslation.js @@ -5,7 +5,8 @@ import { customTextSelector } from 'Utilities/redux/serviceStatusReducer' export default function useTranslation(translationKey) { const customTexts = useSelector(customTextSelector) const locale = useSelector(localeSelector) - const translatedText = customTexts && customTexts[translationKey][locale] + + const translatedText = customTexts && customTexts[translationKey] && customTexts[translationKey][locale] return translatedText } diff --git a/docker-compose.yml b/docker-compose.yml index e4fd938..80fee23 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,8 +26,8 @@ services: - POSTGRES_PASSWORD=postgres volumes: - pg_data:/data - adminer: - container_name: adminer + adminer_fular: + container_name: adminer_fukrek environment: - ADMINER_DESIGN=pepa-linha - ADMINER_DEFAULT_SERVER=db diff --git a/migrations/20240125203210-extended-device-fields-to-user.js b/migrations/20240125203210-extended-device-fields-to-user.js new file mode 100644 index 0000000..34b2cb2 --- /dev/null +++ b/migrations/20240125203210-extended-device-fields-to-user.js @@ -0,0 +1,17 @@ + + +module.exports = { + up: (queryInterface, Sequelize) => queryInterface.sequelize.transaction(t => Promise.all([ + queryInterface.addColumn('users', 'extended_wants_device', { + type: Sequelize.BOOLEAN, + }, { transaction: t }), + queryInterface.addColumn('users', 'extended_eligible', { + type: Sequelize.BOOLEAN, + }, { transaction: t }), + ])), + + down: queryInterface => queryInterface.sequelize.transaction(t => Promise.all([ + queryInterface.removeColumn('users', 'extended_wants_device', { transaction: t }), + queryInterface.removeColumn('users', 'extended_eligible', { transaction: t }), + ])), +} diff --git a/server/controllers/userController.js b/server/controllers/userController.js index b643367..9fa8df8 100644 --- a/server/controllers/userController.js +++ b/server/controllers/userController.js @@ -14,8 +14,8 @@ const getUser = async (req, res) => { const userIsPotentiallyEligible = user.isStudent && !user.hasDeviceGiven && !userIsEligibleThisYear if (userIsPotentiallyEligible) { - await user.checkAndUpdateEligibility() - userIsEligibleThisYear = user.eligible && user.signupYear === settings.currentYear + await user.checkAndUpdateEligibility(req.toska) + userIsEligibleThisYear = user.extendedEligible || (user.eligible && user.signupYear === settings.currentYear) } if (userIsEligibleThisYear && !user.hasCompletedAllTasks) { @@ -49,16 +49,16 @@ const getLogoutUrl = async (req, res) => { } const requestDevice = async (req, res) => { - const { email } = req.body + const { email, extended } = req.body const { user } = req const settings = await ServiceStatus.getObject() - const userIsEligibleThisYear = user.eligible && user.signupYear === settings.currentYear + const userIsEligibleThisYear = extended || (user.eligible && user.signupYear === settings.currentYear) if (!userIsEligibleThisYear) throw new ForbiddenError('Not eligible') if (email !== null && !validateEmail(email)) throw new ParameterError('Invalid email') - await user.requestDevice(email) + await user.requestDevice(email, extended) await completionChecker(user) return res.json(user) } diff --git a/server/middleware/authenticationMiddleware.js b/server/middleware/authenticationMiddleware.js index b508ebe..7fc51a2 100644 --- a/server/middleware/authenticationMiddleware.js +++ b/server/middleware/authenticationMiddleware.js @@ -12,6 +12,7 @@ const authentication = async (req, res, next) => { hypersonstudentid: hyPersonStudentId = null, sn = null, uid = null, + hygroupcn, } = req.headers if (!uid) return res.status(403).json({ error: 'forbidden' }) @@ -27,6 +28,10 @@ const authentication = async (req, res, next) => { logger.warn(`Non superadmin ${uid} tried to use loginAs without permissions`) return res.sendStatus(403) } + + console.log('hygroupcn', hygroupcn) + req.toska = hygroupcn && hygroupcn.includes('grp-toska') + const foundUser = await User.findOne({ where: { userId: uid }, include: [ @@ -102,7 +107,7 @@ const authentication = async (req, res, next) => { // eligibilityReasons, }) - const { eligible, eligibilityReasons } = await newUser.checkEligibility() + const { eligible, eligibilityReasons, extendedEligible } = await newUser.checkEligibility() const { digiSkills, hasEnrollments } = await newUser.getStatus() @@ -113,6 +118,7 @@ const authentication = async (req, res, next) => { digiSkillsCompleted: digiSkills, courseRegistrationCompleted: hasEnrollments, eligibilityReasons, + extendedEligible: extendedEligible && req.toska, }) req.user = newUser diff --git a/server/middleware/studentMiddleware.js b/server/middleware/studentMiddleware.js index cf74da9..8c93041 100644 --- a/server/middleware/studentMiddleware.js +++ b/server/middleware/studentMiddleware.js @@ -6,6 +6,7 @@ const checkStudent = async (req, _res, next) => { if (!studentNumber) throw new ParameterError('Studentnumber required') const student = await User.findStudent(studentNumber) + if (!student) throw new NotFoundError('student not found') req.student = student diff --git a/server/models/lib/apiInterface.js b/server/models/lib/apiInterface.js index f05f242..bd52e97 100644 --- a/server/models/lib/apiInterface.js +++ b/server/models/lib/apiInterface.js @@ -5,6 +5,8 @@ const { } = require('../../util/common') const mock = require('./mock') +const useMock = true + class ApiInterface { constructor() { this.userApi = axios.create({ @@ -20,8 +22,9 @@ class ApiInterface { } async getStudyRights(studentNumber) { - if (!inProduction) return Promise.resolve(mock.findStudyrights(studentNumber)) + if (!inProduction && useMock) return Promise.resolve(mock.findStudyrights(studentNumber)) const { data } = await this.userApi.get(`/students/${studentNumber}/studyrights`) + return data } @@ -57,7 +60,7 @@ class ApiInterface { } async getSemesterEnrollments(studentNumber) { - if (!inProduction) return Promise.resolve(mock.findSemesterEnrollments(studentNumber)) + if (!inProduction && useMock) return Promise.resolve(mock.findSemesterEnrollments(studentNumber)) if (!SIS) { const res = await this.userApi.get(`/students/${studentNumber}/semesterEnrollments`) return res.data @@ -67,7 +70,7 @@ class ApiInterface { } async getYearsCredits(studentNumber, startingSemester, signUpYear) { - if (!inProduction) return Promise.resolve(mock.findFirstYearCredits(studentNumber)) + if (!inProduction && useMock) return Promise.resolve(mock.findFirstYearCredits(studentNumber)) if (!SIS) { const res = await this.userApi.get(`/students/${studentNumber}/fuksiYearCredits/${startingSemester}`) return res.data diff --git a/server/models/lib/mock.js b/server/models/lib/mock.js index 0f8f010..30a88cb 100644 --- a/server/models/lib/mock.js +++ b/server/models/lib/mock.js @@ -55,13 +55,14 @@ const mockData = { elements: [ { code: 'KH50_005', - start_date: '2019-07-31T21:00:00.000Z', + start_date: '2023-07-31T21:00:00.000Z', end_date: '2025-07-30T21:00:00.000Z', }, ], - admission_date: '2017-06-30T21:00:00.000Z', - end_date: '2024-07-30T21:00:00.000Z', - start_date: '2017-07-31T21:00:00.000Z', + admission_date: '2023-06-30T21:00:00.000Z', + end_date: '2025-07-30T21:00:00.000Z', + start_date: '2023-07-31T21:00:00.000Z', + id: 'hy-opinoik-131106475', }, ], }, @@ -69,16 +70,159 @@ const mockData = { md5: '12345', status: 200, elapsed: 0.002677027, + data: { + 'hy-opinoik-131106475': [ + { + full_time_student: 'true', + semester_enrollment_type_code: 1, + absence_reason_code: null, + semester_enrollment_date: '2016-06-30T21:00:00.000Z', + semester_code: 139, + }, + { + full_time_student: 'true', + semester_enrollment_type_code: 1, + absence_reason_code: null, + semester_enrollment_date: '2023-06-30T21:00:00.000Z', + semester_code: 147, + }, + ], + }, + }, + }, + 'non-fuksi': { + studyrights: { data: [ { - full_time_student: 'true', - semester_enrollment_type_code: 1, - absence_reason_code: null, - semester_enrollment_date: '2016-06-30T21:00:00.000Z', - semester_code: 139, + faculty_code: 'H50', + elements: [ + { + code: 'KH50_005', + start_date: '2019-07-31T21:00:00.000Z', + end_date: '2025-07-30T21:00:00.000Z', + }, + ], + admission_date: '2017-06-30T21:00:00.000Z', + end_date: '2024-07-30T21:00:00.000Z', + start_date: '2019-07-31T21:00:00.000Z', + id: 'hy-opinoik-131106475', }, ], }, + semesterEnrollments: { + md5: '12345', + status: 200, + elapsed: 0.002677027, + data: { + 'hy-avoin-ew-sr-C5960': [], + 'hy-avoin-ew-sr-C5DA9': [], + 'hy-avoin-ew-sr-C5DAA': [], + 'hy-avoin-ew-sr-C5F09': [], + 'hy-avoin-ew-sr-C7450': [], + 'hy-avoin-ew-sr-C7451': [], + 'hy-avoin-ew-sr-C7452': [], + 'hy-avoin-ew-sr-CD0E1': [], + 'hy-avoin-ew-sr-CD0E2': [], + 'hy-avoin-ew-sr-D392D': [], + 'hy-avoin-ew-sr-D3AA9': [], + 'hy-avoin-ew-sr-D4C67': [], + 'hy-avoin-ew-sr-D66F1': [], + 'hy-opinoik-128685407': [ + { + semester_enrollment_type_code: 1, + semester_code: 139, + }, + { + semester_enrollment_type_code: 1, + semester_code: 140, + }, + ], + 'hy-opinoik-129282918': [ + { + semester_enrollment_type_code: 1, + semester_code: 139, + }, + { + semester_enrollment_type_code: 1, + semester_code: 140, + }, + ], + 'hy-opinoik-131106475': [ + { + semester_enrollment_type_code: 1, + semester_code: 139, + }, + { + semester_enrollment_type_code: 1, + semester_code: 140, + }, + { + semester_enrollment_type_code: 1, + semester_code: 141, + }, + { + semester_enrollment_type_code: 1, + semester_code: 142, + }, + { + semester_enrollment_type_code: 1, + semester_code: 143, + }, + { + semester_enrollment_type_code: 1, + semester_code: 144, + }, + { + semester_enrollment_type_code: 1, + semester_code: 145, + }, + { + semester_enrollment_type_code: 1, + semester_code: 146, + }, + { + semester_enrollment_type_code: 1, + semester_code: 147, + }, + { + semester_enrollment_type_code: 1, + semester_code: 148, + }, + { + semester_enrollment_type_code: 2, + semester_code: 149, + }, + { + semester_enrollment_type_code: 2, + semester_code: 150, + }, + ], + 'hy-opinoik-135812691': [ + { + semester_enrollment_type_code: 1, + semester_code: 140, + }, + { + semester_enrollment_type_code: 1, + semester_code: 141, + }, + ], + 'hy-opinoik-136744535': [ + { + semester_enrollment_type_code: 1, + semester_code: 140, + }, + { + semester_enrollment_type_code: 1, + semester_code: 141, + }, + { + semester_enrollment_type_code: 1, + semester_code: 142, + }, + ], + }, + }, }, fuksi_without_digiskills: { studyrights: { diff --git a/server/models/user.js b/server/models/user.js index aea3d67..ed152f7 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -12,6 +12,7 @@ const api = new ApiInterface() const includesValidBachelorStudyright = async (studyrights) => { const acceptableStudyProgramCodes = (await StudyProgram.findAll({ attributes: ['code'] })).map(({ code }) => code) + return !!studyrights .reduce((pre, { elements }) => pre.concat(elements), []) .find(({ code, end_date }) => acceptableStudyProgramCodes.includes(code) && new Date(end_date) > new Date().getTime()) @@ -39,8 +40,10 @@ const getStudyrightValidities = async (studyrights, semesterEnrollments, current const previousStudyrightIsPossiblyNew = !hasPre2008Studyright && !hasNewStudyright && hasPreviousStudyright + const flattenEnrolments = Object.values(semesterEnrollments.data).reduce((set, obj) => set.concat(obj), []) + if (previousStudyrightIsPossiblyNew) { - const hasBeenPresentBefore = semesterEnrollments.data.some(({ semester_code, semester_enrollment_type_code }) => ( + const hasBeenPresentBefore = flattenEnrolments.some(({ semester_code, semester_enrollment_type_code }) => ( semester_code < currentSemester && semester_enrollment_type_code !== 2)) if (!hasBeenPresentBefore) { @@ -91,8 +94,10 @@ class User extends Model { }) } + async getStudyRights() { if (this.studyrights) return this.studyrights + const studyrights = await api.getStudyRights(this.studentNumber) this.studyrights = studyrights return studyrights @@ -117,7 +122,10 @@ class User extends Model { return api.getYearsCredits(this.studentNumber, startingSemester, this.signupYear) } + async checkEligibility() { + const flattenEnrolmentsFor = (rights, enrollments) => rights.reduce((set, right) => set.concat(enrollments[right]), []) + const settings = await ServiceStatus.getObject() const studyrights = await this.getStudyRights() const semesterEnrollments = await this.getSemesterEnrollments() @@ -128,7 +136,9 @@ class User extends Model { hasValidBachelorsStudyright, } = await getStudyrightValidities(studyrights, semesterEnrollments, settings.currentSemester) - const isPresent = semesterEnrollments.data.some(enrollment => ( + const flattenEnrolments = flattenEnrolmentsFor(studyrights.map(s => s.id), semesterEnrollments.data) + + const isPresent = flattenEnrolments.some(enrollment => ( enrollment.semester_code === settings.currentSemester && enrollment.semester_enrollment_type_code === 1)) return { @@ -137,7 +147,9 @@ class User extends Model { hasValidStudyright: hasValidBachelorsStudyright, hasNoPreviousStudyright: !hasPreviousStudyright, isPresent, + hasNotDeviceGiven: !this.hasDeviceGiven, }, + extendedEligible: (hasPreviousStudyright && isPresent && hasValidBachelorsStudyright && !this.hasDeviceGiven), } } @@ -181,11 +193,11 @@ class User extends Model { } } - async checkAndUpdateEligibility() { + async checkAndUpdateEligibility(inToska = false) { try { const settings = await ServiceStatus.getObject() - const { eligible, eligibilityReasons } = await this.checkEligibility() + const { eligible, eligibilityReasons, extendedEligible } = await this.checkEligibility() if (eligible) { await this.createUserStudyprograms() @@ -194,9 +206,18 @@ class User extends Model { logger.info(`${this.studentNumber} eligibility updated automatically`) } + if (extendedEligible && inToska) { + await this.createUserStudyprograms() + this.extendedEligible = extendedEligible + this.signupYear = settings.currentYear + logger.info(`${this.studentNumber} extendedEligible updated automatically`) + } + this.eligibilityReasons = eligibilityReasons await this.save() } catch (e) { + console.log(e) + logger.error(`Failed checking and updating ${this.studentNumber} eligibility`) } } @@ -324,8 +345,8 @@ class User extends Model { }) } - async requestDevice(email) { - await this.update({ wantsDevice: true, personalEmail: email }) + async requestDevice(email, extended = false) { + await this.update({ wantsDevice: !extended, extendedWantsDevice: extended, personalEmail: email }) } async claimDevice(deviceId, deviceDistributedBy) { @@ -466,6 +487,14 @@ User.init( type: DataTypes.BOOLEAN, field: 'third_year_or_later_student', }, + extendedEligible: { + type: DataTypes.BOOLEAN, + field: 'extended_eligible', + }, + extendedWantsDevice: { + type: DataTypes.BOOLEAN, + field: 'extended_wants_device', + }, isStudent: { type: DataTypes.VIRTUAL, get() {