mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a625b1da7 | ||
|
|
4982bd8f3c | ||
|
|
908e33ccd3 | ||
|
|
d429480f80 | ||
|
|
dada995398 | ||
|
|
69c9b05657 | ||
|
|
0b5ac0b61b | ||
|
|
a902961942 | ||
|
|
7c2fa532d4 | ||
|
|
36267f9fca | ||
|
|
c2d99d6944 | ||
|
|
0b1be20780 | ||
|
|
cc9f5f6906 | ||
|
|
d5bca4dc57 | ||
|
|
5f8c468ceb | ||
|
|
434d45987d | ||
|
|
0cb8acc46a | ||
|
|
6e996fbc4c | ||
|
|
86fbaaf1ca | ||
|
|
517c216a5d | ||
|
|
a04fb68aaa | ||
|
|
72c01fa345 | ||
|
|
62f1cc5459 | ||
|
|
c9eceaebe4 | ||
|
|
92e8709ef6 | ||
|
|
9c5c135d0f | ||
|
|
c4f65a1f29 | ||
|
|
96f1c9d9cf | ||
|
|
a8cfcb3371 | ||
|
|
63cbe523b1 | ||
|
|
7092fbbda0 | ||
|
|
82573c9d17 | ||
|
|
1b98b0a847 | ||
|
|
b73174f8a5 | ||
|
|
b1167eebf3 | ||
|
|
35391f9b36 | ||
|
|
4fde6c319a | ||
|
|
ebd1a98ead | ||
|
|
b3ef64fc2e | ||
|
|
7bbc3c0cb0 | ||
|
|
90abe7eb22 | ||
|
|
08dd4019af | ||
|
|
2841fd69a3 | ||
|
|
827d45c481 | ||
|
|
52ba8d582b | ||
|
|
d151521eae | ||
|
|
6188fe6fe4 | ||
|
|
1f0eb93174 | ||
|
|
f50e632cdb | ||
|
|
bbd83b501b | ||
|
|
48048e5ecf | ||
|
|
5be0a74a60 | ||
|
|
e8e10bcb9a | ||
|
|
3e3a95a113 | ||
|
|
9a190b301d | ||
|
|
f976eca1a5 | ||
|
|
a32b4695a9 | ||
|
|
d55e27f1d9 | ||
|
|
b749ad4033 | ||
|
|
eddc514907 | ||
|
|
e0c4db7bd9 | ||
|
|
3b384cceb6 | ||
|
|
6e15b8c355 | ||
|
|
f1196b49ae | ||
|
|
e5b89dc844 | ||
|
|
5b27c42945 | ||
|
|
5d84d2f9a3 | ||
|
|
e06f12017d | ||
|
|
db0e924f36 | ||
|
|
10d13b751b | ||
|
|
87ef487fe8 | ||
|
|
50968b4b66 | ||
|
|
791e295eb7 | ||
|
|
4bef9c8daa | ||
|
|
8050a56847 | ||
|
|
e48247d80b | ||
|
|
afdab20014 | ||
|
|
f477b3ef50 | ||
|
|
ab19b429ca | ||
|
|
2e6fdb4970 | ||
|
|
c918f862e3 | ||
|
|
59bfbfcca3 | ||
|
|
22e11948f5 | ||
|
|
5b5884b25d |
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Populate and change this to .env
|
||||
IMPORTER_DB_API_TOKEN=
|
||||
IMPORTER_DB_API_URL=
|
||||
IMPORTER_DB_API_URL=https://importer.cs.helsinki.fi
|
||||
PATE_TOKEN=
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
environment: production
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/oodikone/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
version: ${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
build_and_release_userservice:
|
||||
|
||||
@@ -545,7 +545,7 @@ jobs:
|
||||
environment: staging
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/oodikone/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
version: ${{ github.sha }}
|
||||
- name: 'Push to docker hub'
|
||||
run: |
|
||||
|
||||
@@ -64,8 +64,7 @@ parse_profiles() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Set which docker-compose files to use based on version. In ci, profiles are also
|
||||
# emptied, since they're passed from github actions.
|
||||
# Set which docker-compose files to use based on version.
|
||||
parse_env() {
|
||||
env=""
|
||||
if [[ "$version" == "real" ]]; then
|
||||
|
||||
@@ -152,10 +152,10 @@ linting and formatting"
|
||||
npm run prepare
|
||||
|
||||
infomsg "Pulling images"
|
||||
"$PROJECT_ROOT"/run.sh oodikone anon pull
|
||||
"$PROJECT_ROOT"/run.sh both anon pull
|
||||
|
||||
infomsg "Building images"
|
||||
"$PROJECT_ROOT"/run.sh oodikone anon build
|
||||
"$PROJECT_ROOT"/run.sh both anon build
|
||||
|
||||
successmsg "Setup ready, oodikone can be started! See README for more info."
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ const addSlashToEnd = url => (url.endsWith('/') ? url : url + '/')
|
||||
|
||||
const ACCESS_TOKEN_HEADER_KEY = 'x-access-token'
|
||||
|
||||
const PWD = process.env.PWD || '/usr/src/app'
|
||||
|
||||
module.exports = {
|
||||
DB_URL_KONE,
|
||||
DB_MAX_CONNECTIONS,
|
||||
@@ -90,4 +92,5 @@ module.exports = {
|
||||
backendPort,
|
||||
importerToken,
|
||||
importerUrl,
|
||||
PWD,
|
||||
}
|
||||
|
||||
@@ -130,15 +130,18 @@ const refreshProtoCProgrammeToRedis = async () => {
|
||||
const onlyOld = { include_old_attainments: 'true', exclude_non_enrolled: 'false', code }
|
||||
const onlyEnr = { include_old_attainments: 'false', exclude_non_enrolled: 'true', code }
|
||||
const bothToggles = { include_old_attainments: 'true', exclude_non_enrolled: 'true', code }
|
||||
try {
|
||||
await refreshProtoCProgramme(defaultQuery)
|
||||
await refreshProtoCProgramme(onlyOld)
|
||||
await refreshProtoCProgramme(onlyEnr)
|
||||
await refreshProtoCProgramme(bothToggles)
|
||||
logger.info(`Refreshing ProtoCProgramme code ${code} doned`)
|
||||
} catch (e) {
|
||||
logger.error({ message: `Error when refreshing ProtocProgramme code ${code}`, meta: e })
|
||||
const queries = [defaultQuery, onlyOld, onlyEnr, bothToggles]
|
||||
for (const query of queries) {
|
||||
try {
|
||||
await refreshProtoCProgramme(query)
|
||||
} catch (e) {
|
||||
logger.error({
|
||||
message: `Error when refreshing ProtocProgramme code ${code}, query ${Object.keys({ query })[0]}`,
|
||||
meta: e,
|
||||
}) // object.keys is hax to print query variable name
|
||||
}
|
||||
}
|
||||
logger.info(`Refreshing ProtoCProgramme code ${code} doned`)
|
||||
}
|
||||
logger.info(`Refreshing protocprogramme doned`)
|
||||
}
|
||||
@@ -161,17 +164,13 @@ const refreshTrends = async () => {
|
||||
logger.info('Trends refreshed!')
|
||||
}
|
||||
|
||||
// const refreshAll = async () => {
|
||||
// for (const func of [refreshStatistics, refreshTrends]) {
|
||||
// await func()
|
||||
// }
|
||||
// }
|
||||
|
||||
const startCron = () => {
|
||||
if (isProduction) {
|
||||
// refresh 3am every day
|
||||
schedule('0 3 * * *', async () => {
|
||||
await refreshStatistics()
|
||||
for (const func of [refreshStatistics, refreshTrends]) {
|
||||
await func()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const router = require('express').Router()
|
||||
const userService = require('../services/userService')
|
||||
const { getUserDataFor } = require('../services/userService')
|
||||
const { sendFeedbackToToska } = require('../services/mailservice')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
@@ -8,7 +8,8 @@ router.post('/email', async (req, res) => {
|
||||
const {
|
||||
decodedToken: { userId },
|
||||
} = req
|
||||
const { email, full_name } = await userService.byUsername(userId)
|
||||
|
||||
const { email, full_name } = await getUserDataFor(userId)
|
||||
|
||||
const result = await sendFeedbackToToska({
|
||||
feedbackContent: content,
|
||||
|
||||
@@ -8,6 +8,8 @@ const UserService = require('../services/userService')
|
||||
const TagService = require('../services/tags')
|
||||
const CourseService = require('../services/courses')
|
||||
const StatMergeService = require('../services/statMerger')
|
||||
const { getAllStudentsUserHasInGroups } = require('../services/studyGuidanceGroups')
|
||||
const { logger } = require('../util/logger')
|
||||
|
||||
const filterPersonalTags = (population, userId) => {
|
||||
return {
|
||||
@@ -158,12 +160,15 @@ router.post('/v2/populationstatistics/coursesbystudentnumberlist', async (req, r
|
||||
}
|
||||
let studentnumberlist
|
||||
const {
|
||||
decodedToken: { userId },
|
||||
decodedToken: { userId, sisPersonId },
|
||||
roles,
|
||||
} = req
|
||||
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentnumberlist = req.body.studentnumberlist
|
||||
} else if (req.body.usingStudyGuidanceGroups) {
|
||||
const studentsUserCanAccess = await getAllStudentsUserHasInGroups(sisPersonId)
|
||||
studentnumberlist = req.body.studentnumberlist.filter(s => studentsUserCanAccess.has(s))
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
@@ -367,14 +372,21 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
})
|
||||
|
||||
router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
|
||||
const { studentnumberlist } = req.body
|
||||
const { studentnumberlist, usingStudyGuidanceGroups } = req.body
|
||||
const { roles, decodedToken } = req
|
||||
|
||||
const studentsUserCanAccess = await UserService.getStudentsUserCanAccess(
|
||||
studentnumberlist,
|
||||
roles,
|
||||
decodedToken.userId
|
||||
)
|
||||
if (usingStudyGuidanceGroups) {
|
||||
if (!decodedToken.sisPersonId) {
|
||||
logger.error(
|
||||
`User ${decodedToken.userId} tried to get person groups but personId was ${decodedToken.sisPersonId} in header`
|
||||
)
|
||||
return res.status(400).json({ error: 'Not possible to get groups without personId' })
|
||||
}
|
||||
}
|
||||
|
||||
const studentsUserCanAccess = usingStudyGuidanceGroups
|
||||
? await getAllStudentsUserHasInGroups(decodedToken.sisPersonId)
|
||||
: await UserService.getStudentsUserCanAccess(studentnumberlist, roles, decodedToken.userId)
|
||||
const filteredStudentNumbers = studentnumberlist.filter(s => studentsUserCanAccess.has(s))
|
||||
|
||||
const result = await Population.optimizedStatisticsOf(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const router = require('express').Router()
|
||||
const { getImporterClient } = require('../util/importerClient')
|
||||
const { logger } = require('../util/logger')
|
||||
const { getAllGroupsAndStudents } = require('../services/studyGuidanceGroups')
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const {
|
||||
@@ -10,12 +10,7 @@ router.get('/', async (req, res) => {
|
||||
logger.error(`User ${uid} tried to get person groups but personId was ${sisPersonId} in header`)
|
||||
return res.status(400).json({ error: 'Not possible to get groups without personId' })
|
||||
}
|
||||
const importerClient = getImporterClient()
|
||||
if (!importerClient) {
|
||||
return []
|
||||
}
|
||||
const { data } = await importerClient.get(`/person-groups/person/${sisPersonId}`)
|
||||
return res.json(Object.values(data))
|
||||
return res.json(await getAllGroupsAndStudents(sisPersonId))
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -8,7 +8,7 @@ const {
|
||||
abort,
|
||||
updateCoursesByCourseCode,
|
||||
} = require('../services/sisUpdaterService')
|
||||
const { refreshStatistics } = require('../events')
|
||||
const { refreshStatistics, refreshTrends } = require('../events')
|
||||
|
||||
router.get('/update/v2/meta', async (req, res) => {
|
||||
const response = await updateSISMetadata(req)
|
||||
@@ -53,10 +53,15 @@ router.get('/refresh_redis_cache', async (req, res) => {
|
||||
})
|
||||
|
||||
router.post('/refresh_statistic_v2', async (req, res) => {
|
||||
refreshStatistics()
|
||||
await refreshStatistics()
|
||||
res.status(200).json('Refreshing sis statistics')
|
||||
})
|
||||
|
||||
router.post('/refresh_trends', async (req, res) => {
|
||||
await refreshTrends()
|
||||
res.status(200).json('Refreshing trends')
|
||||
})
|
||||
|
||||
router.get('/abort', async (req, res) => {
|
||||
await abort()
|
||||
res.status(200).json()
|
||||
|
||||
@@ -25,9 +25,11 @@ router.get('/access_groups', async (req, res) => {
|
||||
|
||||
router.post('/modifyaccess', async (req, res) => {
|
||||
try {
|
||||
const { uid } = req.body
|
||||
const { username } = req.body
|
||||
const result = await userService.modifyAccess(req.body)
|
||||
const user = await userService.byId(uid)
|
||||
const user = {
|
||||
username,
|
||||
}
|
||||
await addUserToBlacklist(user, req.decodedToken)
|
||||
res.status(200).json(result)
|
||||
} catch (e) {
|
||||
|
||||
@@ -172,7 +172,7 @@ const allCodeAltenatives = async code => {
|
||||
const temp = courses
|
||||
.map(c => c.code)
|
||||
.map(c => {
|
||||
if (c.match(/^A/)) return [c, 4] // open university codes come last
|
||||
if (c.match(/^AY?/)) return [c, 4] // open university codes come last
|
||||
if (c.match(/^\d/)) return [c, 2] // old numeric codes come second
|
||||
if (c.match(/^[A-Za-z]/)) return [c, 1] // new letter based codes come first
|
||||
return [c, 3] // unknown, comes before open uni?
|
||||
|
||||
@@ -3,6 +3,154 @@ const { Organization, ProgrammeModule } = require('../models')
|
||||
const Op = Sequelize.Op
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
// temp refactoring faculty <-> userservice mappings away from userservice
|
||||
const facultiesAndProgrammesInUserService = [
|
||||
['H10', 'MH10_001'],
|
||||
['H10', 'MH40_011'],
|
||||
['H10', 'KH10_001'],
|
||||
['H20', 'KH20_001'],
|
||||
['H20', 'MH20_001'],
|
||||
['H20', 'MH20_002'],
|
||||
['H30', 'KH30_001'],
|
||||
['H30', 'KH30_002'],
|
||||
['H30', 'MH30_002'],
|
||||
['H30', 'MH30_003'],
|
||||
['H30', 'MH30_004'],
|
||||
['H30', 'MH30_005'],
|
||||
['H30', 'MH30_001'],
|
||||
['H40', 'KH40_001'],
|
||||
['H40', 'KH40_002'],
|
||||
['H40', 'KH40_003'],
|
||||
['H40', 'KH40_004'],
|
||||
['H40', 'KH40_005'],
|
||||
['H40', 'KH40_006'],
|
||||
['H40', 'MH40_001'],
|
||||
['H40', 'MH40_002'],
|
||||
['H40', 'MH40_003'],
|
||||
['H40', 'MH40_004'],
|
||||
['H40', 'MH40_005'],
|
||||
['H40', 'MH40_006'],
|
||||
['H40', 'MH40_007'],
|
||||
['H40', 'MH40_008'],
|
||||
['H40', 'MH40_009'],
|
||||
['H40', 'MH40_010'],
|
||||
['H40', 'MH40_011'],
|
||||
['H40', 'MH40_012'],
|
||||
['H40', 'MH40_013'],
|
||||
['H40', 'MH40_014'],
|
||||
['H40', 'MH40_015'],
|
||||
['H40', 'MH70_001'],
|
||||
['H40', 'MH70_006'],
|
||||
['H50', 'KH50_001'],
|
||||
['H50', 'KH50_002'],
|
||||
['H50', 'KH50_003'],
|
||||
['H50', 'KH50_004'],
|
||||
['H50', 'KH50_005'],
|
||||
['H50', 'KH50_006'],
|
||||
['H50', 'KH50_007'],
|
||||
['H50', 'MH50_001'],
|
||||
['H50', 'MH50_002'],
|
||||
['H50', 'MH50_003'],
|
||||
['H50', 'MH50_004'],
|
||||
['H50', 'MH50_005'],
|
||||
['H50', 'MH50_006'],
|
||||
['H50', 'MH50_007'],
|
||||
['H50', 'MH50_008'],
|
||||
['H50', 'MH50_009'],
|
||||
['H50', 'MH50_010'],
|
||||
['H50', 'MH50_011'],
|
||||
['H50', 'MH50_012'],
|
||||
['H50', 'MH50_013'],
|
||||
['H55', 'KH55_001'],
|
||||
['H55', 'MH55_001'],
|
||||
['H57', 'KH57_001'],
|
||||
['H57', 'KH57_002'],
|
||||
['H57', 'KH57_003'],
|
||||
['H57', 'MH50_002'],
|
||||
['H57', 'MH50_013'],
|
||||
['H57', 'MH57_001'],
|
||||
['H57', 'MH57_002'],
|
||||
['H57', 'MH57_003'],
|
||||
['H57', 'MH57_004'],
|
||||
['H57', 'MH57_005'],
|
||||
['H57', 'MH80_007'],
|
||||
['H60', 'KH60_001'],
|
||||
['H60', 'MH60_001'],
|
||||
['H70', 'KH40_001'],
|
||||
['H70', 'KH50_001'],
|
||||
['H70', 'KH70_001'],
|
||||
['H70', 'KH70_002'],
|
||||
['H70', 'KH70_003'],
|
||||
['H70', 'KH70_004'],
|
||||
['H70', 'MH50_001'],
|
||||
['H70', 'MH57_005'],
|
||||
['H70', 'MH70_001'],
|
||||
['H70', 'MH70_002'],
|
||||
['H70', 'MH70_003'],
|
||||
['H70', 'MH70_004'],
|
||||
['H70', 'MH70_005'],
|
||||
['H70', 'MH70_006'],
|
||||
['H70', 'MH70_007'],
|
||||
['H70', 'MH70_008'],
|
||||
['H70', 'MH70_009'],
|
||||
['H70', 'MH70_010'],
|
||||
['H74', 'KH74_001'],
|
||||
['H80', 'KH57_002'],
|
||||
['H80', 'KH80_001'],
|
||||
['H80', 'KH80_002'],
|
||||
['H80', 'KH80_003'],
|
||||
['H80', 'KH80_004'],
|
||||
['H80', 'MH57_002'],
|
||||
['H80', 'MH57_003'],
|
||||
['H80', 'MH57_005'],
|
||||
['H80', 'MH80_001'],
|
||||
['H80', 'MH80_002'],
|
||||
['H80', 'MH80_003'],
|
||||
['H80', 'MH80_004'],
|
||||
['H80', 'MH80_005'],
|
||||
['H80', 'MH80_006'],
|
||||
['H80', 'MH80_007'],
|
||||
['H90', 'KH90_001'],
|
||||
['H30', '447000'],
|
||||
['H30', '447001'],
|
||||
['H30', '447002'],
|
||||
['H60', 'EDUK730'],
|
||||
['H60', 'K-MUUT-ERIL'],
|
||||
['H60', 'ED400'],
|
||||
['H60', 'ED200'],
|
||||
['H60', 'ED700'],
|
||||
['H60', 'ED100'],
|
||||
['H60', 'ED300'],
|
||||
['H60', 'ED600'],
|
||||
['H60', '8009'],
|
||||
['H60', '8118'],
|
||||
['H60', '8600'],
|
||||
['H60', 'DUK-PED'],
|
||||
['H60', '65'],
|
||||
['H60', '1102'],
|
||||
['H60', '20018'],
|
||||
['H60', '8601'],
|
||||
['H60', '9290'],
|
||||
['H60', '8172'],
|
||||
['H60', '83600'],
|
||||
['H60', 'DUM-PED'],
|
||||
['H60', 'ED500'],
|
||||
['H60', '83601'],
|
||||
['H60', '0130'],
|
||||
['H60', '20005-e'],
|
||||
['H60', '1040'],
|
||||
['H60', '8703'],
|
||||
['H60', '0005'],
|
||||
['H60', '7900'],
|
||||
['H60', '0152 '],
|
||||
['H90', 'MH90_001'],
|
||||
]
|
||||
|
||||
const facultiesAndProgrammesForTrends = facultiesAndProgrammesInUserService.map(f => ({
|
||||
faculty_code: f[0],
|
||||
programme_code: f[1],
|
||||
}))
|
||||
|
||||
// Have facultyfetching to work like it worked during oodi-db time
|
||||
const facultiesInOodi = [
|
||||
'H10',
|
||||
@@ -66,4 +214,5 @@ module.exports = {
|
||||
degreeProgrammeCodesOfFaculty,
|
||||
isFaculty,
|
||||
providersOfFaculty,
|
||||
facultiesAndProgrammesForTrends,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ const logger = require('../util/logger')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
const recursivelyGetModuleAndChildren = async (code, type) => {
|
||||
// TODO use only sis connection once sis is stable
|
||||
const connection = dbConnections.sequelize
|
||||
try {
|
||||
const [result] = await connection.query(
|
||||
|
||||
@@ -181,6 +181,7 @@ const formatStudent = ({
|
||||
passed: Credit.passed({ credittypecode }) || Credit.improved({ credittypecode }),
|
||||
grade,
|
||||
credits,
|
||||
credittypecode,
|
||||
isStudyModuleCredit: isStudyModule,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
const { getImporterClient } = require('../util/importerClient')
|
||||
const importerClient = getImporterClient()
|
||||
|
||||
const getAllStudentsUserHasInGroups = async sisPersonId => {
|
||||
if (!importerClient) {
|
||||
return new Set()
|
||||
}
|
||||
const { data } = await importerClient.get(`/person-groups/person/${sisPersonId}`)
|
||||
return new Set(
|
||||
Object.values(data)
|
||||
.map(group => group.members)
|
||||
.flat()
|
||||
.map(member => member.personStudentNumber)
|
||||
)
|
||||
}
|
||||
|
||||
const getAllGroupsAndStudents = async sisPersonId => {
|
||||
if (!importerClient) {
|
||||
return []
|
||||
}
|
||||
const { data } = await importerClient.get(`/person-groups/person/${sisPersonId}`)
|
||||
return Object.values(data)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAllStudentsUserHasInGroups,
|
||||
getAllGroupsAndStudents,
|
||||
}
|
||||
@@ -2,7 +2,6 @@ const {
|
||||
dbConnections: { sequelize },
|
||||
} = require('../database/connection')
|
||||
const { Studyright, StudyrightElement, ElementDetail } = require('../models')
|
||||
const { getUserElementDetails } = require('./userService')
|
||||
const moment = require('moment')
|
||||
const { redisClient } = require('../services/redis')
|
||||
const _ = require('lodash')
|
||||
@@ -140,15 +139,6 @@ const getAllStudyrightElementsAndAssociations = async () => {
|
||||
return JSON.parse(studyright_elements)
|
||||
}
|
||||
|
||||
const getStudyrightElementsAndAssociationsForUser = async username => {
|
||||
const studyrightelementcodes = await getUserElementDetails(username)
|
||||
if (studyrightelementcodes.length === 0) {
|
||||
return []
|
||||
}
|
||||
const associations = await getAssociatedStudyrights(studyrightelementcodes)
|
||||
return formatStudyrightElements(studyrightelementcodes, associations)
|
||||
}
|
||||
|
||||
const getAllProgrammes = async () => {
|
||||
const elementDetails = ElementDetail.findAll({
|
||||
where: {
|
||||
@@ -324,21 +314,13 @@ const getFilteredAssociations = async codes => {
|
||||
return associations
|
||||
}
|
||||
|
||||
const getUserAssociations = async userid => {
|
||||
const codes = await getUserElementDetails(userid)
|
||||
const associations = await getFilteredAssociations(codes)
|
||||
return associations
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
byStudent,
|
||||
studentNumbersWithAllStudyRightElements,
|
||||
getAssociatedStudyrights,
|
||||
getAllStudyrightElementsAndAssociations,
|
||||
getStudyrightElementsAndAssociationsForUser,
|
||||
getAssociations,
|
||||
getFilteredAssociations,
|
||||
getUserAssociations,
|
||||
refreshAssociationsInRedis,
|
||||
getAllProgrammes,
|
||||
getAllElementDetails,
|
||||
|
||||
@@ -191,6 +191,11 @@ const activeTeachers = async (providers, semestercodeStart, semestercodeEnd) =>
|
||||
},
|
||||
],
|
||||
},
|
||||
where: {
|
||||
id: {
|
||||
[Op.notLike]: '%hy-hlo-org%',
|
||||
},
|
||||
},
|
||||
})
|
||||
return teachers.map(({ id }) => id)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
const axios = require('axios')
|
||||
const { redisClient } = require('../../services/redis')
|
||||
const { USERSERVICE_URL } = require('../../conf-backend')
|
||||
const {
|
||||
dbConnections: { sequelize },
|
||||
} = require('../../database/connection')
|
||||
|
||||
const userServiceClient = axios.create({
|
||||
baseURL: USERSERVICE_URL,
|
||||
headers: { secret: process.env.USERSERVICE_SECRET },
|
||||
})
|
||||
|
||||
const getRedisCDS = async REDIS_KEY => {
|
||||
const raw = await redisClient.getAsync(REDIS_KEY)
|
||||
return raw && JSON.parse(raw)
|
||||
@@ -177,6 +170,5 @@ const getTargetStudentCounts = async ({ codes, includeOldAttainments, excludeNon
|
||||
module.exports = {
|
||||
getRedisCDS,
|
||||
saveToRedis,
|
||||
userServiceClient,
|
||||
getTargetStudentCounts,
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ const {
|
||||
} = require('../../database/connection')
|
||||
const { mapToProviders } = require('../../util/utils')
|
||||
const { ElementDetail, Organization } = require('../../models')
|
||||
const { getRedisCDS, saveToRedis, userServiceClient } = require('./shared')
|
||||
const { getRedisCDS, saveToRedis } = require('./shared')
|
||||
const { facultiesAndProgrammesForTrends } = require('../../services/organisations')
|
||||
|
||||
const REDIS_KEY_STATUS = 'STATUS_DATA_V2'
|
||||
const REDIS_KEY_STATUS = 'STATUS_DATA_V3'
|
||||
|
||||
const getCurrentStudyYearStartDate = _.memoize(
|
||||
async unixMillis =>
|
||||
@@ -29,6 +30,140 @@ const getCurrentYearStartDate = () => {
|
||||
return new Date(new Date().getFullYear(), 0, 1)
|
||||
}
|
||||
|
||||
const getOrganizationStudents = async (start, end) => {
|
||||
const rows = await sequelize.query(
|
||||
`
|
||||
SELECT
|
||||
sre.code AS code,
|
||||
ARRAY_AGG(DISTINCT(sr.student_studentnumber)) AS students
|
||||
FROM studyright sr
|
||||
INNER JOIN studyright_elements sre ON sre.studyrightid = sr.studyrightid
|
||||
INNER JOIN element_details ed ON ed.code = sre.code
|
||||
WHERE ed.type = 20 AND (:start, :end) OVERLAPS (sr.startdate, sr.enddate)
|
||||
GROUP BY sre.code
|
||||
`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: { start, end },
|
||||
}
|
||||
)
|
||||
|
||||
const codes = mapToProviders(rows.map(({ code }) => code))
|
||||
|
||||
const orgIdRows = await sequelize.query(
|
||||
`
|
||||
SELECT o.code, o.id FROM organization o WHERE o.code IN (:codes)
|
||||
`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: { codes },
|
||||
}
|
||||
)
|
||||
|
||||
const orgIds = Object.fromEntries(orgIdRows.map(({ code, id }) => [code, id]))
|
||||
|
||||
return rows.reduce((acc, { code, students }) => {
|
||||
const [orgCode] = mapToProviders([code])
|
||||
const orgId = orgIds[orgCode]
|
||||
acc[orgId] = students
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const getOrganizationCredits = async (start, end) => {
|
||||
const rows = await sequelize.query(
|
||||
`
|
||||
SELECT
|
||||
cp.organizationcode AS code,
|
||||
-- ARRAY_AGG(DISTINCT(cr.student_studentnumber)) AS students,
|
||||
SUM(cr.credits) AS credits
|
||||
FROM credit cr
|
||||
INNER JOIN course co ON co.code = cr.course_code
|
||||
INNER JOIN course_providers cp ON cp.coursecode = co.id
|
||||
WHERE
|
||||
cr.attainment_date BETWEEN :start AND :end AND
|
||||
cr.credittypecode IN (4, 9) AND
|
||||
(cr."isStudyModule" = FALSE OR cr."isStudyModule" IS NULL)
|
||||
GROUP BY cp.organizationcode
|
||||
`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: { start, end },
|
||||
}
|
||||
)
|
||||
|
||||
return rows.reduce((acc, { code, credits }) => {
|
||||
acc[code] = credits
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const getOrganizationStatistics = async (organizations, start, end) => {
|
||||
const orgCredits = await getOrganizationCredits(start, end)
|
||||
const orgStudents = await getOrganizationStudents(start, end)
|
||||
|
||||
const organizationStats = {}
|
||||
|
||||
let stack_pointer = 0
|
||||
const stack = ['hy-university-root-id']
|
||||
|
||||
while (stack_pointer < stack.length) {
|
||||
const orgId = stack[stack_pointer]
|
||||
stack_pointer += 1
|
||||
|
||||
organizations.filter(({ parent_id }) => parent_id === orgId).forEach(({ id }) => stack.push(id))
|
||||
}
|
||||
|
||||
const updateOrgStats = (orgId, stats, isDirect) => {
|
||||
if (organizationStats[orgId] === undefined) {
|
||||
organizationStats[orgId] = {
|
||||
direct: { credits: 0, students: new Set() },
|
||||
accumulated: { credits: 0, students: new Set() },
|
||||
}
|
||||
}
|
||||
|
||||
const orgStats = organizationStats[orgId]
|
||||
|
||||
const statsRefs = isDirect ? [orgStats.direct, orgStats.accumulated] : [orgStats.accumulated]
|
||||
|
||||
statsRefs.forEach(statsRef => {
|
||||
statsRef.credits += stats.credits
|
||||
stats.students.forEach(s => statsRef.students.add(s))
|
||||
})
|
||||
}
|
||||
|
||||
while (stack.length > 0) {
|
||||
const orgId = stack.pop()
|
||||
const credits = orgCredits[orgId] ?? 0
|
||||
const students = orgStudents[orgId] ?? []
|
||||
|
||||
const org = organizations.find(({ id }) => id === orgId)
|
||||
const parentOrgId = org.parent_id
|
||||
|
||||
updateOrgStats(orgId, { credits, students }, true)
|
||||
|
||||
if (parentOrgId) {
|
||||
updateOrgStats(parentOrgId, organizationStats[orgId].accumulated, false)
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(organizationStats).reduce((acc, [id, { direct, accumulated }]) => {
|
||||
acc[id] = {
|
||||
id,
|
||||
direct: {
|
||||
students: direct.students.size,
|
||||
credits: direct.credits,
|
||||
},
|
||||
accumulated: {
|
||||
students: accumulated.students.size,
|
||||
credits: accumulated.credits,
|
||||
},
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const getTotalCreditsOfCoursesBetween = async (a, b, alias = 'sum', alias2 = 'sum2') => {
|
||||
return sequelize.query(
|
||||
`
|
||||
@@ -54,18 +189,6 @@ const getTotalCreditsOfCoursesBetween = async (a, b, alias = 'sum', alias2 = 'su
|
||||
)
|
||||
}
|
||||
|
||||
const sumMerge = (a, b) => {
|
||||
if (!a) return b
|
||||
if (!b) return a
|
||||
return a + b
|
||||
}
|
||||
|
||||
const mergele = (a, b) => {
|
||||
if (!a) return _.clone(b)
|
||||
if (!b) return _.clone(a)
|
||||
return _.mergeWith(a, b, sumMerge)
|
||||
}
|
||||
|
||||
const makeYearlyCreditsPromises = (currentYear, years, getRange, alias = 'sum', alias2 = 'sum2') => {
|
||||
return years.map(
|
||||
year =>
|
||||
@@ -83,6 +206,12 @@ const makeYearlyCreditsPromises = (currentYear, years, getRange, alias = 'sum',
|
||||
)
|
||||
}
|
||||
|
||||
const getOrgStats = (stats, orgId) =>
|
||||
stats[orgId] ?? {
|
||||
accumulated: { students: 0, credits: 0 },
|
||||
direct: { students: 0, credits: 0 },
|
||||
}
|
||||
|
||||
const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
const YEAR_TO_MILLISECONDS = 31556952000
|
||||
/* Memoize parses booleans into strings... */
|
||||
@@ -116,14 +245,40 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
)
|
||||
|
||||
/* Gather all required data */
|
||||
const [yearlyAccCredits, yearlyTotalCredits, elementDetails, faculties, { data: facultyProgrammes }] =
|
||||
await Promise.all([
|
||||
Promise.all(yearlyAccCreditsPromises),
|
||||
Promise.all(yearlyTotalCreditsPromises),
|
||||
ElementDetail.findAll(),
|
||||
Organization.findAll(),
|
||||
userServiceClient.get('/faculty_programmes'),
|
||||
])
|
||||
const [yearlyAccCredits, yearlyTotalCredits, elementDetails, faculties] = await Promise.all([
|
||||
Promise.all(yearlyAccCreditsPromises),
|
||||
Promise.all(yearlyTotalCreditsPromises),
|
||||
ElementDetail.findAll(),
|
||||
Organization.findAll(),
|
||||
])
|
||||
const facultyProgrammes = facultiesAndProgrammesForTrends
|
||||
|
||||
const yearlyOrgStatPromises = yearRange.map(async year => [
|
||||
year,
|
||||
await getOrganizationStatistics(
|
||||
faculties,
|
||||
new Date(startTime - (startYear - year) * YEAR_TO_MILLISECONDS),
|
||||
new Date(startTime - (startYear - year - 1) * YEAR_TO_MILLISECONDS)
|
||||
),
|
||||
])
|
||||
|
||||
const orgStats = await Promise.all(yearlyOrgStatPromises)
|
||||
const yearlyOrgStats = Object.fromEntries(orgStats)
|
||||
const [[, currentOrgStats], [, prevOrgStats]] = orgStats.reverse()
|
||||
|
||||
const getOrgYearlyStats = orgId =>
|
||||
Object.entries(yearlyOrgStats).reduce((acc, [year, stats]) => {
|
||||
const { credits, students } = getOrgStats(stats, orgId).accumulated
|
||||
|
||||
acc[year] = {
|
||||
acc: credits,
|
||||
accStudents: students,
|
||||
total: credits,
|
||||
totalStudents: students,
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
/* Construct some helper maps */
|
||||
const facultyCodeToFaculty = faculties.reduce((res, curr) => {
|
||||
@@ -146,6 +301,11 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
return res
|
||||
}, {})
|
||||
|
||||
const organizationCodeToOrganization = faculties.reduce((acc, org) => {
|
||||
acc[org.code] = org
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
/* Calculate course level stats and group by providers */
|
||||
const coursesGroupedByProvider = Object.entries(
|
||||
_.groupBy([..._.flatten(yearlyAccCredits), ..._.flatten(yearlyTotalCredits)], 'organizationcode')
|
||||
@@ -186,17 +346,27 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
/* Map providers into proper programmes and calculate programme level stats */
|
||||
const groupedByProgramme = Object.entries(coursesGroupedByProvider).reduce((acc, [organizationcode, courses]) => {
|
||||
const programme = providerToProgramme[organizationcode]
|
||||
const courseValues = Object.values(courses)
|
||||
const yearlyValues = courseValues.map(c => c.yearly)
|
||||
|
||||
if (programme && programme.code) {
|
||||
const orgId = organizationCodeToOrganization[organizationcode]?.id
|
||||
|
||||
const { students: currentStudents, credits: current } = getOrgStats(currentOrgStats, orgId).accumulated
|
||||
const { students: previousStudents, credits: previous } = getOrgStats(prevOrgStats, orgId).accumulated
|
||||
|
||||
const yearly = getOrgYearlyStats(orgId)
|
||||
|
||||
acc[programme.code] = {
|
||||
type: 'programme',
|
||||
name: programme.name,
|
||||
drill: courses,
|
||||
yearly: _.mergeWith({}, ...yearlyValues, mergele),
|
||||
current: _.sumBy(courseValues, 'current'),
|
||||
previous: _.sumBy(courseValues, 'previous'),
|
||||
yearly,
|
||||
current,
|
||||
previous,
|
||||
currentStudents,
|
||||
previousStudents,
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
@@ -207,18 +377,26 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
facultyCodes.forEach(facultyCode => {
|
||||
if (!facultyCode) return
|
||||
if (!acc[facultyCode]) {
|
||||
const orgId = organizationCodeToOrganization[facultyCode].id
|
||||
|
||||
const { students: currentStudents, credits: current } = getOrgStats(currentOrgStats, orgId).accumulated
|
||||
const { students: previousStudents, credits: previous } = getOrgStats(prevOrgStats, orgId).accumulated
|
||||
|
||||
const yearly = getOrgYearlyStats(orgId)
|
||||
|
||||
acc[facultyCode] = {
|
||||
type: 'faculty',
|
||||
drill: {},
|
||||
name: facultyCodeToFaculty[facultyCode] ? facultyCodeToFaculty[facultyCode].name : null,
|
||||
yearly: {},
|
||||
current: 0,
|
||||
previous: 0,
|
||||
yearly,
|
||||
current,
|
||||
previous,
|
||||
currentStudents,
|
||||
previousStudents,
|
||||
}
|
||||
}
|
||||
|
||||
acc[facultyCode]['drill'][programmeCode] = programmeStats
|
||||
acc[facultyCode]['yearly'] = _.mergeWith(acc[facultyCode]['yearly'], programmeStats.yearly, mergele)
|
||||
acc[facultyCode]['current'] += programmeStats.current
|
||||
acc[facultyCode]['previous'] += programmeStats.previous
|
||||
})
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
@@ -1,24 +1,42 @@
|
||||
const axios = require('axios')
|
||||
const _ = require('lodash')
|
||||
const { USERSERVICE_URL } = require('../conf-backend')
|
||||
const UnitService = require('./units')
|
||||
const elementDetailService = require('./elementdetails')
|
||||
const { filterStudentnumbersByAccessrights } = require('./students')
|
||||
const { userDataCache } = require('./cache')
|
||||
const client = axios.create({ baseURL: USERSERVICE_URL, headers: { secret: process.env.USERSERVICE_SECRET } })
|
||||
const { getImporterClient } = require('../util/importerClient')
|
||||
const logger = require('../util/logger')
|
||||
const { facultiesAndProgrammesForTrends } = require('../services/organisations')
|
||||
|
||||
const ping = async () => {
|
||||
const url = '/ping'
|
||||
const response = await axios.get(url)
|
||||
return response.data
|
||||
const client = axios.create({ baseURL: USERSERVICE_URL, headers: { secret: process.env.USERSERVICE_SECRET } })
|
||||
|
||||
// Private helpers
|
||||
const enrichProgrammesFromFaculties = faculties =>
|
||||
facultiesAndProgrammesForTrends.filter(f => faculties.includes(f.faculty_code)).map(f => f.programme_code)
|
||||
|
||||
const enrichWithProgrammes = toEnrich => {
|
||||
if (Array.isArray(toEnrich)) {
|
||||
return toEnrich.map(obj => enrichWithProgrammes(obj))
|
||||
}
|
||||
if (toEnrich.rights) {
|
||||
return {
|
||||
...toEnrich,
|
||||
rights: _.union(toEnrich.rights, enrichProgrammesFromFaculties(toEnrich.faculties)),
|
||||
}
|
||||
}
|
||||
|
||||
if (toEnrich.elementdetails) {
|
||||
return {
|
||||
...toEnrich,
|
||||
elementdetails: _.union(
|
||||
toEnrich.elementdetails,
|
||||
enrichProgrammesFromFaculties(toEnrich.faculty.map(f => f.faculty_code))
|
||||
),
|
||||
}
|
||||
}
|
||||
return toEnrich
|
||||
}
|
||||
|
||||
const findAll = async () => {
|
||||
const response = await client.get('/findall')
|
||||
return response.data
|
||||
}
|
||||
|
||||
const checkStudyGuidanceGroupsAccess = async hyPersonSisuId => {
|
||||
if (!hyPersonSisuId) {
|
||||
logger.error('Not possible to get groups without personId header')
|
||||
@@ -30,101 +48,20 @@ const checkStudyGuidanceGroupsAccess = async hyPersonSisuId => {
|
||||
return data && Object.values(data).length > 0
|
||||
}
|
||||
|
||||
const login = async (uid, full_name, hyGroups, affiliations, email, hyPersonSisuId) => {
|
||||
const hasStudyGuidanceGroupAccess = await checkStudyGuidanceGroupsAccess(hyPersonSisuId)
|
||||
const response = await client.post('/login', {
|
||||
uid,
|
||||
full_name,
|
||||
hyGroups,
|
||||
affiliations,
|
||||
email,
|
||||
hyPersonSisuId,
|
||||
hasStudyGuidanceGroupAccess,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
const superlogin = async (uid, asUser) => {
|
||||
const response = await client.post('/superlogin', {
|
||||
uid,
|
||||
asUser,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
const byUsername = async uid => {
|
||||
const url = `/user/${uid}`
|
||||
const byUsernameData = async username => {
|
||||
// OK, enriched
|
||||
const url = `/user/${username}/user_data`
|
||||
const response = await client.get(url)
|
||||
return response.data
|
||||
return enrichWithProgrammes(response.data)
|
||||
}
|
||||
|
||||
const byUsernameData = async uid => {
|
||||
const url = `/user/${uid}/user_data`
|
||||
const response = await client.get(url)
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getUserElementDetails = async username => {
|
||||
const url = `/user/elementdetails/${username}`
|
||||
const response = await client.get(url)
|
||||
const elementdetailcodes = response.data
|
||||
return elementDetailService.byCodes(elementdetailcodes)
|
||||
}
|
||||
|
||||
const byId = async id => {
|
||||
const url = `/user/id/${id}`
|
||||
const response = await client.get(url)
|
||||
return response.data
|
||||
}
|
||||
|
||||
const updateUser = async (uid, fields) => {
|
||||
userDataCache.del(uid)
|
||||
const url = `/user/${uid}`
|
||||
const response = await client.put(url, fields)
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getRolesFor = async user => {
|
||||
const url = `/get_roles/${user}`
|
||||
const response = await client.get(url)
|
||||
return response.data
|
||||
}
|
||||
|
||||
const enableElementDetails = async (uid, codes) => {
|
||||
const response = await client.post('/add_rights', { uid, codes })
|
||||
return response.data.user
|
||||
}
|
||||
|
||||
const removeElementDetails = async (uid, codes) => {
|
||||
const response = await client.post('/remove_rights', { uid, codes })
|
||||
return response.data.user
|
||||
}
|
||||
|
||||
const setFaculties = async (uid, faculties) => {
|
||||
const response = await client.post('/set_faculties', { uid, faculties })
|
||||
return response.data.user
|
||||
}
|
||||
|
||||
const getUnitsFromElementDetails = async username => {
|
||||
const elementDetails = await getUserElementDetails(username)
|
||||
return elementDetails.map(element => UnitService.parseUnitFromElement(element))
|
||||
}
|
||||
|
||||
const modifyAccess = async body => {
|
||||
const response = await client.post('/modifyaccess', body)
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getAccessGroups = async () => {
|
||||
const response = await client.get('/access_groups')
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getUserDataFor = async uid => {
|
||||
let userData = userDataCache.get(uid)
|
||||
// Exported helpers
|
||||
const getUserDataFor = async username => {
|
||||
// no need to enrich
|
||||
let userData = userDataCache.get(username)
|
||||
if (!userData) {
|
||||
userData = await byUsernameData(uid)
|
||||
userDataCache.set(uid, userData)
|
||||
userData = await byUsernameData(username)
|
||||
userDataCache.set(username, userData)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -133,9 +70,21 @@ const getUserDataFor = async uid => {
|
||||
}
|
||||
}
|
||||
|
||||
const getUnitsFromElementDetails = async username => {
|
||||
// No need to enrich
|
||||
let userData = userDataCache.get(username)
|
||||
if (!userData) {
|
||||
userData = await byUsernameData(username)
|
||||
userDataCache.set(username, userData)
|
||||
}
|
||||
const elementDetails = await elementDetailService.byCodes(userData.rights)
|
||||
return elementDetails.map(element => UnitService.parseUnitFromElement(element))
|
||||
}
|
||||
|
||||
const getStudentsUserCanAccess = async (studentnumbers, roles, userId) => {
|
||||
// No need to enrich
|
||||
let studentsUserCanAccess
|
||||
if (roles && roles.includes('admin')) {
|
||||
if (roles?.includes('admin')) {
|
||||
studentsUserCanAccess = new Set(studentnumbers)
|
||||
} else {
|
||||
const unitsUserCanAccess = await getUnitsFromElementDetails(userId)
|
||||
@@ -145,12 +94,52 @@ const getStudentsUserCanAccess = async (studentnumbers, roles, userId) => {
|
||||
return studentsUserCanAccess
|
||||
}
|
||||
|
||||
// FOR ROUTES 1-to-1 mapping
|
||||
const modifyAccess = async body => enrichWithProgrammes((await client.post('/modifyaccess', body)).data)
|
||||
|
||||
const getAccessGroups = async () => enrichWithProgrammes((await client.get('/access_groups')).data)
|
||||
|
||||
const enableElementDetails = async (uid, codes) =>
|
||||
enrichWithProgrammes((await client.post('/add_rights', { uid, codes })).data.user)
|
||||
|
||||
const findAll = async () => enrichWithProgrammes((await client.get('/findall')).data)
|
||||
|
||||
const setFaculties = async (uid, faculties) =>
|
||||
enrichWithProgrammes((await client.post('/set_faculties', { uid, faculties })).data.user)
|
||||
|
||||
const removeElementDetails = async (uid, codes) =>
|
||||
enrichWithProgrammes((await client.post('/remove_rights', { uid, codes })).data.user)
|
||||
|
||||
const updateUser = async (username, fields) => {
|
||||
userDataCache.del(username)
|
||||
return enrichWithProgrammes((await client.put(`/user/${username}`, fields)).data)
|
||||
}
|
||||
|
||||
const login = async (uid, full_name, hyGroups, affiliations, email, hyPersonSisuId) => {
|
||||
const hasStudyGuidanceGroupAccess = await checkStudyGuidanceGroupsAccess(hyPersonSisuId)
|
||||
return (
|
||||
await client.post('/login', {
|
||||
uid,
|
||||
full_name,
|
||||
hyGroups,
|
||||
affiliations,
|
||||
email,
|
||||
hyPersonSisuId,
|
||||
hasStudyGuidanceGroupAccess,
|
||||
})
|
||||
).data
|
||||
}
|
||||
|
||||
const superlogin = async (uid, asUser) =>
|
||||
(
|
||||
await client.post('/superlogin', {
|
||||
uid,
|
||||
asUser,
|
||||
})
|
||||
).data
|
||||
|
||||
module.exports = {
|
||||
ping,
|
||||
byUsername,
|
||||
updateUser,
|
||||
byId,
|
||||
getUserElementDetails,
|
||||
login,
|
||||
superlogin,
|
||||
enableElementDetails,
|
||||
@@ -159,7 +148,6 @@ module.exports = {
|
||||
modifyAccess,
|
||||
getAccessGroups,
|
||||
getUnitsFromElementDetails,
|
||||
getRolesFor,
|
||||
setFaculties,
|
||||
getUserDataFor,
|
||||
getStudentsUserCanAccess,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const winston = require('winston')
|
||||
const { isProduction } = require('../conf-backend')
|
||||
const path = require('path')
|
||||
const { isProduction, PWD } = require('../conf-backend')
|
||||
|
||||
const { combine, timestamp, printf, splat } = winston.format
|
||||
|
||||
const transports = []
|
||||
|
||||
transports.push(new winston.transports.File({ filename: 'debug.log' }))
|
||||
// Write logfile to docker containers pwd
|
||||
const filename = path.join(PWD, 'debug.log')
|
||||
transports.push(new winston.transports.File({ filename, level: 'debug' }))
|
||||
|
||||
if (!isProduction) {
|
||||
const devFormat = printf(
|
||||
@@ -38,7 +40,7 @@ if (isProduction) {
|
||||
})
|
||||
)
|
||||
|
||||
transports.push(new winston.transports.Console({ format: prodFormat }))
|
||||
transports.push(new winston.transports.Console({ format: prodFormat, level: 'info' }))
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({ transports })
|
||||
|
||||
Generated
+22519
-28
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@
|
||||
"highcharts": "^9.1.2",
|
||||
"highcharts-custom-events": "^3.0.10",
|
||||
"history": "^4.10.1",
|
||||
"jStat": "^1.8.6",
|
||||
"jstat": "^1.9.5",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
|
||||
@@ -2,6 +2,8 @@ import React, { useCallback, useState, useEffect, useRef } from 'react'
|
||||
import { chunk, isEqual } from 'lodash'
|
||||
import qs from 'query-string'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { setMocking } from './index'
|
||||
|
||||
import { SEARCH_HISTORY_VERSION } from '../constants'
|
||||
import TSA from './tsa'
|
||||
@@ -276,3 +278,24 @@ export const useIsAdmin = () => {
|
||||
if (!roles) return false
|
||||
return roles.some(role => role.group_code === 'admin')
|
||||
}
|
||||
|
||||
// From: https://www.joshwcomeau.com/snippets/react-hooks/use-toggle/
|
||||
export const useToggle = (initialValue = false) => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const toggle = useCallback(() => {
|
||||
setValue(v => !v)
|
||||
}, [])
|
||||
return [value, toggle]
|
||||
}
|
||||
|
||||
export const useShowAsUser = () => {
|
||||
const history = useHistory()
|
||||
|
||||
const showAsUser = uid => {
|
||||
setMocking(uid)
|
||||
history.push('/')
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return showAsUser
|
||||
}
|
||||
|
||||
@@ -12,11 +12,6 @@ export const setMocking = userid =>
|
||||
userid ? localStorage.setItem(MOCK_USERID, userid) : localStorage.removeItem(MOCK_USERID)
|
||||
export const getMocked = () => localStorage.getItem(MOCK_USERID)
|
||||
|
||||
export const SIS_WARNING_FLAG = 'SIS_WARNING_FLAG'
|
||||
export const setHideSisWarningFlag = booleanFlag =>
|
||||
booleanFlag ? localStorage.setItem(SIS_WARNING_FLAG, booleanFlag) : localStorage.removeItem(SIS_WARNING_FLAG)
|
||||
export const getHideSisWarningFlag = () => localStorage.getItem(SIS_WARNING_FLAG)
|
||||
|
||||
export const textAndDescriptionSearch = (dropDownOptions, param) =>
|
||||
filter(dropDownOptions, option =>
|
||||
option.text
|
||||
@@ -228,3 +223,36 @@ export const validateInputLength = (input, minLength) => input && input.trim().l
|
||||
export const splitByEmptySpace = str => str.replace(/\s\s+/g, ' ').split(' ')
|
||||
|
||||
export const isNewHYStudyProgramme = code => !!(code && code.match(/^[A-Z]*[0-9]*_[0-9]*$/))
|
||||
|
||||
export const bachelorHonoursProgrammes = [
|
||||
'KH50_001',
|
||||
'KH50_002',
|
||||
'KH50_003',
|
||||
'KH50_004',
|
||||
'KH50_005',
|
||||
'KH50_006',
|
||||
'KH50_007',
|
||||
'KH50_008',
|
||||
]
|
||||
|
||||
export const bachelorHonoursBasicModules = {
|
||||
KH50_001: ['MAT110', 'MAT120', 'MAT130', 'MAT120', 'TKT1'],
|
||||
KH50_002: ['FYS1100', 'FYS1200'],
|
||||
KH50_003: ['KEK100'],
|
||||
KH50_004: ['MAT110', 'FYS1100', 'FYS1200', 'KEK100'],
|
||||
KH50_005: ['TKT1'],
|
||||
KH50_006: ['GEOK_100'],
|
||||
KH50_007: ['MAA-100'],
|
||||
KH50_008: ['BSCH1000', 'BSCS1000', 'BSMA1000', 'BSMS1000', 'BSPH1000'],
|
||||
}
|
||||
|
||||
export const bachelorHonoursIntermediateModules = {
|
||||
KH50_001: ['mat217', 'MAT210', 'MAT220', 'MAT240', 'MAT213', 'MAT230', 'MAT218', 'MAT011'],
|
||||
KH50_002: ['FYS2100', 'FYS2200', 'FYS2300', 'FYS2400', 'FYS2500', 'FYS2600'],
|
||||
KH50_003: ['KEK200'],
|
||||
KH50_004: ['MFK-M200', 'MFK-F200', 'MFK-K200'],
|
||||
KH50_005: ['TKT2'],
|
||||
KH50_006: ['GEOK_200'],
|
||||
KH50_007: ['MAA-200'],
|
||||
KH50_008: ['BSCH2000', 'BSCS2000', 'BSMA2000', 'BSMS2000', 'BSPH2000'],
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Segment } from 'semantic-ui-react'
|
||||
import { Switch, Route, Link, useRouteMatch } from 'react-router-dom'
|
||||
|
||||
@@ -32,20 +31,10 @@ const NavTile = ({ to, title, imageSrc }) => {
|
||||
)
|
||||
}
|
||||
|
||||
NavTile.propTypes = {
|
||||
to: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
imageSrc: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
const NavTiles = ({ children }) => {
|
||||
return <div className="cool-ds-nav-tiles">{children}</div>
|
||||
}
|
||||
|
||||
NavTiles.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
const baseURI = '/cool-data-science'
|
||||
|
||||
const CoolDataScience = () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import PropTypes, { shape, bool, func } from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Segment, Loader, Dimmer, Icon, Accordion, Checkbox, Message, Form } from 'semantic-ui-react'
|
||||
import _ from 'lodash'
|
||||
@@ -28,32 +27,47 @@ const mapValueToRange = (x, min1, max1, min2, max2) => {
|
||||
return ((x - min1) * (max2 - min2)) / (max1 - min1) + min2
|
||||
}
|
||||
|
||||
const StatusContainer = ({
|
||||
title,
|
||||
current,
|
||||
previous,
|
||||
clickable,
|
||||
handleClick,
|
||||
min1,
|
||||
max1,
|
||||
showYearlyValues,
|
||||
yearlyValues,
|
||||
showByYear,
|
||||
}) => {
|
||||
const diff = Math.round(current - previous)
|
||||
const StatusContainer = ({ stats, handleClick, min1, max1, showYearlyValues, showRelativeValues, showByYear }) => {
|
||||
const title = getTextIn(stats.name)
|
||||
const clickable = !!stats.drill
|
||||
|
||||
let current
|
||||
let previous
|
||||
|
||||
if (!stats.drill) {
|
||||
current = stats.currentStudents
|
||||
previous = stats.previousStudents
|
||||
} else {
|
||||
current = stats.current
|
||||
previous = stats.previous
|
||||
|
||||
if (showRelativeValues) {
|
||||
current /= stats.currentStudents
|
||||
previous /= stats.previousStudents
|
||||
}
|
||||
}
|
||||
|
||||
const diff = current - previous
|
||||
const p = getP(current, previous)
|
||||
const change = Math.round((p - 1) * 1000) / 10
|
||||
const change = p - 1
|
||||
|
||||
const getColor = v => {
|
||||
if (v > 2.5) return '#6ab04c'
|
||||
if (v < -2.5) return '#ff7979'
|
||||
if (v > 0.025) return '#6ab04c'
|
||||
if (v < -0.025) return '#ff7979'
|
||||
return '#7B9FCF'
|
||||
}
|
||||
|
||||
const plussify = x => {
|
||||
if (x > 0) return `+${x.toLocaleString('fi')}`
|
||||
return x.toLocaleString('fi')
|
||||
}
|
||||
const plussify = (x, decimals = 0) =>
|
||||
x.toLocaleString('fi', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
signDisplay: 'always',
|
||||
})
|
||||
|
||||
const roundToPrecision = (value, precision) => Math.round(value * 10 ** precision) / 10 ** precision
|
||||
|
||||
const getDisplayValue = (value, denominator) =>
|
||||
showRelativeValues ? roundToPrecision(denominator === 0 ? 0 : value / denominator, 2) : value
|
||||
|
||||
return (
|
||||
<Segment
|
||||
@@ -85,71 +99,65 @@ const StatusContainer = ({
|
||||
<Icon
|
||||
style={{
|
||||
color: getColor(change),
|
||||
transform: `rotateZ(${-mapValueToRange(change, min1, max1, -45, 45)}deg)`,
|
||||
transform: `rotateZ(${-mapValueToRange(change * 100, min1, max1, -45, 45)}deg)`,
|
||||
}}
|
||||
size="huge"
|
||||
name="arrow right"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ fontSize: 20, fontWeight: 'bold', color: getColor(change) }}>{plussify(diff)}</span>
|
||||
<span style={{ fontSize: 20, fontWeight: 'bold', color: getColor(change) }}>
|
||||
{plussify(diff, showRelativeValues ? 2 : 0)}
|
||||
</span>
|
||||
<br />
|
||||
<span style={{ fontWeight: 'bold', color: getColor(change) }}>({plussify(change * 100, 1)}%)</span>
|
||||
<br />
|
||||
<span style={{ fontWeight: 'bold', color: getColor(change) }}>({plussify(change)}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
{showYearlyValues && (
|
||||
<div style={{ marginTop: '10px', textAlign: 'start' }}>
|
||||
{_.orderBy(Object.entries(yearlyValues), ([y]) => y, ['desc']).map(
|
||||
([year, { acc, total, accStudents, totalStudents }]) => {
|
||||
return (
|
||||
<div style={{ margin: '5px 0' }} key={`${title}-${year}`}>
|
||||
<span>
|
||||
<b>
|
||||
{year}
|
||||
{!showByYear && `-${`${Number(year) + 1}`.slice(-2)}`}:
|
||||
</b>{' '}
|
||||
{/* render num of students instead of credits if no credits are given from the course */}
|
||||
{accStudents <= acc ? Math.round(acc).toLocaleString('fi') : accStudents}
|
||||
{/* if no total students add students after accStudents */}
|
||||
{acc <= accStudents && totalStudents < 1 && ' students'}
|
||||
{/* render total if there are credits or students for either */}
|
||||
{(total > 0 || totalStudents > 0) &&
|
||||
` / ${
|
||||
// same logic as before to check if render num of students or credits
|
||||
totalStudents <= total && total > 0
|
||||
? Math.round(total).toLocaleString('fi')
|
||||
: `${totalStudents} students`
|
||||
}`}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
<div className="years">
|
||||
{_.orderBy(Object.entries(stats.yearly), ([y]) => y, ['desc']).map(([year, yearStats]) => {
|
||||
return (
|
||||
<span style={{ display: 'contents' }} className="year-row">
|
||||
<b>
|
||||
{year}
|
||||
{!showByYear && `-${`${Number(year) + 1}`.slice(-2)}`}:
|
||||
</b>
|
||||
{stats.type === 'course' ? (
|
||||
<>
|
||||
<span className="year-value">{yearStats.accStudents}</span>
|
||||
<span className="unit">students</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span
|
||||
className="year-value"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `${getDisplayValue(yearStats.acc, yearStats.accStudents)
|
||||
.toLocaleString('fi', { minimumFractionDigits: 2 })
|
||||
.replace(/,?0*$/, match => `<span class="decimals">${match}</span>`)}`,
|
||||
}}
|
||||
/>
|
||||
<span className="unit">credits{showRelativeValues ? '/student' : ''}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
|
||||
StatusContainer.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
current: PropTypes.number.isRequired,
|
||||
previous: PropTypes.number.isRequired,
|
||||
clickable: PropTypes.bool.isRequired,
|
||||
handleClick: PropTypes.func.isRequired,
|
||||
min1: PropTypes.number.isRequired,
|
||||
max1: PropTypes.number.isRequired,
|
||||
showYearlyValues: PropTypes.bool.isRequired,
|
||||
yearlyValues: PropTypes.shape({}).isRequired,
|
||||
showByYear: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
const VerticalLine = () => <div style={{ margin: '0 10px', fontSize: '20px' }}>|</div>
|
||||
|
||||
const Status = ({ getStatusDispatch, data, loading }) => {
|
||||
const DATE_FORMAT = 'DD.MM.YYYY'
|
||||
const [showYearlyValues, setShowYearlyValues] = useLocalStorage('showYearlyValues', true)
|
||||
const [showByYear, setShowByYear] = useLocalStorage('showByYear', false)
|
||||
const [showRelativeValues, setShowRelativeValues] = useLocalStorage('showRelativeValues', false)
|
||||
const [drillStack, setDrillStack] = useState([])
|
||||
const [showSettings, setShowSettings] = useState(true)
|
||||
const [selectedDate, setSelectedDate] = useState(moment())
|
||||
@@ -191,6 +199,11 @@ const Status = ({ getStatusDispatch, data, loading }) => {
|
||||
sendAnalytics(`S Show by year toggle ${!byYear ? 'on' : 'off'}`, 'Status')
|
||||
}
|
||||
|
||||
const handleShowRelativeValuesChanged = () => {
|
||||
sendAnalytics(`S Show relative values toggle ${!showRelativeValues ? 'on' : 'off'}`)
|
||||
setShowRelativeValues(!showRelativeValues)
|
||||
}
|
||||
|
||||
const pushToDrillStack = (values, code) => {
|
||||
const updatedCodes = [...codes].concat(code)
|
||||
const updatedDrillStack = [...drillStack].concat(values)
|
||||
@@ -247,6 +260,13 @@ const Status = ({ getStatusDispatch, data, loading }) => {
|
||||
checked={showByYear}
|
||||
/>
|
||||
<VerticalLine />
|
||||
<Checkbox
|
||||
style={{ fontSize: '0.9em', fontWeight: 'normal' }}
|
||||
label="Näytä suhteutettuna opiskelijoiden määrään"
|
||||
onChange={handleShowRelativeValuesChanged}
|
||||
checked={showRelativeValues}
|
||||
/>
|
||||
<VerticalLine />
|
||||
<Form>
|
||||
<Form.Field error={!isValidDate(selectedDate)} style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '0.9em' }}>Näytä päivänä:</span>
|
||||
@@ -285,14 +305,25 @@ const Status = ({ getStatusDispatch, data, loading }) => {
|
||||
)
|
||||
|
||||
const orderedAbsDiffs = _.orderBy(
|
||||
Object.values(_.last(drillStack) || data).map(({ current, previous }) => {
|
||||
return Math.abs(Math.floor((getP(current, previous) - 1) * 1000) / 10)
|
||||
Object.values(_.last(drillStack) || data).map(({ current, currentStudents, previous, previousStudents }) => {
|
||||
return Math.abs(
|
||||
Math.floor(
|
||||
(getP(
|
||||
showRelativeValues ? current / currentStudents : current,
|
||||
showRelativeValues ? previous / previousStudents : previous
|
||||
) -
|
||||
1) *
|
||||
1000
|
||||
) / 10
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const medianDiff = orderedAbsDiffs[Math.round((orderedAbsDiffs.length - 1) / 2)]
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Uusien ohjelmien tuottamat opintopisteet</h2>
|
||||
<h2>Koulutusohjelmien tuottamat opintopisteet</h2>
|
||||
<DrilldownMessage />
|
||||
<div style={{ display: 'flex', marginBottom: '20px', marginRight: '40px' }}>
|
||||
{drillStack.length > 0 && (
|
||||
@@ -315,23 +346,16 @@ const Status = ({ getStatusDispatch, data, loading }) => {
|
||||
['desc']
|
||||
).map(([code, stats]) => {
|
||||
const handleClick = () => pushToDrillStack(stats.drill, code)
|
||||
// check if the course has credits or not (if credits is zero but there are students who have completed it)
|
||||
const current =
|
||||
!stats.drill && stats.current === 0 && stats.currentStudents > 1 ? stats.currentStudents : stats.current
|
||||
const previous =
|
||||
!stats.drill && stats.previous === 0 && stats.previousStudents > 1 ? stats.previousStudents : stats.previous
|
||||
|
||||
return (
|
||||
<StatusContainer
|
||||
key={code}
|
||||
clickable={!!stats.drill}
|
||||
stats={stats}
|
||||
handleClick={handleClick}
|
||||
title={getTextIn(stats.name)}
|
||||
current={current}
|
||||
previous={previous}
|
||||
showRelativeValues={showRelativeValues}
|
||||
showYearlyValues={showYearlyValues}
|
||||
min1={-medianDiff * 2}
|
||||
max1={medianDiff * 2}
|
||||
yearlyValues={stats.yearly}
|
||||
showByYear={showByYear}
|
||||
/>
|
||||
)
|
||||
@@ -348,12 +372,6 @@ const Status = ({ getStatusDispatch, data, loading }) => {
|
||||
)
|
||||
}
|
||||
|
||||
Status.propTypes = {
|
||||
data: shape({}).isRequired,
|
||||
loading: bool.isRequired,
|
||||
getStatusDispatch: func.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ coolDataScience }) => ({
|
||||
data: coolDataScience.data.status,
|
||||
loading: coolDataScience.pending.status,
|
||||
|
||||
@@ -309,7 +309,7 @@ const Status = ({ getStatusGraduatedDispatch, data, loading }) => {
|
||||
const medianDiff = orderedAbsDiffs[Math.round((orderedAbsDiffs.length - 1) / 2)]
|
||||
return (
|
||||
<>
|
||||
<h2>Uusista ohjelmista valmistuneet</h2>
|
||||
<h2>Koulutusohjelmista valmistuneet</h2>
|
||||
<DrilldownMessage />
|
||||
<div style={{ display: 'flex', marginBottom: '20px', marginRight: '40px' }}>
|
||||
{drillStack.length > 0 && (
|
||||
|
||||
@@ -10,6 +10,43 @@
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
|
||||
.unit,
|
||||
.decimals {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.years {
|
||||
margin-top: 10px;
|
||||
text-align: start;
|
||||
font-variant-numeric: tabular-nums;
|
||||
display: grid;
|
||||
grid-template-columns: fit-content(0) fit-content(0) auto;
|
||||
}
|
||||
|
||||
.year-row .unit {
|
||||
padding-left: 0.5em;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.year-value {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.year-row {
|
||||
padding-left: 0.5em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-card .years .year-row:hover .unit,
|
||||
.status-card .years .year-row:hover .decimals,
|
||||
.status-card:hover .years:not(:hover) .year-row:first-child .unit,
|
||||
.status-card:hover .years:not(:hover) .year-row:first-child .decimals {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.status-date-time-input {
|
||||
padding-right: 0;
|
||||
margin-left: 5px;
|
||||
|
||||
@@ -21,6 +21,7 @@ import infotooltips from '../../common/InfoToolTips'
|
||||
import InfoBox from '../InfoBox'
|
||||
import FilterTray from '../FilterTray'
|
||||
import useFilters from '../FilterTray/useFilters'
|
||||
import useProgrammeFilter from '../FilterTray/filters/Programmes/useProgrammeFilter'
|
||||
import { CoursePopulationFilters } from '../FilterTray/FilterSets'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import { queryParamsFromUrl } from '../../common/query'
|
||||
@@ -39,6 +40,7 @@ const CoursePopulation = ({
|
||||
}) => {
|
||||
const { language } = useLanguage()
|
||||
const { setAllStudents, filteredStudents } = useFilters()
|
||||
const { setCourseCodes } = useProgrammeFilter()
|
||||
const selectedStudents = filteredStudents.map(stu => stu.studentNumber)
|
||||
|
||||
const [codes, setCodes] = useState([])
|
||||
@@ -106,6 +108,7 @@ const CoursePopulation = ({
|
||||
separate,
|
||||
})
|
||||
setCodes(parsedCourseCodes)
|
||||
setCourseCodes(parsedCourseCodes)
|
||||
if (years) {
|
||||
setYears(years)
|
||||
} else {
|
||||
|
||||
@@ -27,36 +27,29 @@ class CreditAccumulationGraphHighCharts extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
studentCreditLines: [],
|
||||
options: [],
|
||||
updateGraph: false,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { students } = this.props
|
||||
this.getMoreCreditLines(students)
|
||||
const { students, selectedStudents, currentGraphSize, render } = this.props
|
||||
|
||||
this.createGraphOptions(students, selectedStudents, currentGraphSize, render)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.students) {
|
||||
const nextStudents = nextProps.students.map(student => student.studentNumber)
|
||||
const oldStudents = this.props.students.map(student => student.studentNumber)
|
||||
const nextMinDate = nextProps.students.minDate
|
||||
const oldMinDate = this.props.students.minDate
|
||||
const clear = nextMinDate !== oldMinDate
|
||||
const changedStudentAmount = nextProps.selectedStudents.length !== this.props.selectedStudents.length
|
||||
const changedGraphSize = nextProps.currentGraphSize !== this.props.currentGraphSize
|
||||
|
||||
const changed =
|
||||
nextStudents.some(student => !oldStudents.includes(student)) ||
|
||||
oldStudents.some(student => !nextStudents.includes(student)) ||
|
||||
clear
|
||||
oldStudents.some(student => !nextStudents.includes(student))
|
||||
|
||||
if (changed) {
|
||||
const { students } = nextProps
|
||||
this.getMoreCreditLines(students, clear)
|
||||
} else if (changedStudentAmount || changedGraphSize) {
|
||||
if (changed || changedStudentAmount || changedGraphSize) {
|
||||
this.createGraphOptions(
|
||||
nextProps.students,
|
||||
nextProps.selectedStudents,
|
||||
@@ -88,19 +81,6 @@ class CreditAccumulationGraphHighCharts extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getMoreCreditLines = (students, clear) => {
|
||||
if (clear) {
|
||||
// eslint-disable-next-line react/no-access-state-in-setstate
|
||||
const removed = this.state.studentCreditLines.splice(0, 0)
|
||||
const studentCreditLines = removed.concat(this.createStudentCreditLines(students))
|
||||
this.setState({ studentCreditLines, updateGraph: true })
|
||||
} else {
|
||||
// eslint-disable-next-line react/no-access-state-in-setstate
|
||||
const studentCreditLines = this.state.studentCreditLines.concat(this.createStudentCreditLines(students))
|
||||
this.setState({ studentCreditLines, updateGraph: true })
|
||||
}
|
||||
}
|
||||
|
||||
getXAxisMonth = (date, startDate) =>
|
||||
Math.max(moment(date, API_DATE_FORMAT).diff(moment(startDate, API_DATE_FORMAT), 'days') / 30, 0)
|
||||
|
||||
@@ -134,7 +114,10 @@ class CreditAccumulationGraphHighCharts extends Component {
|
||||
}
|
||||
|
||||
createGraphOptions = (students, selectedStudents, graphSize, render) => {
|
||||
const dataOfSelected = this.state.studentCreditLines.filter(line => selectedStudents.includes(line.name))
|
||||
const studentCreditLines = this.createStudentCreditLines(students)
|
||||
|
||||
const dataOfSelected = studentCreditLines.filter(line => selectedStudents.includes(line.name))
|
||||
|
||||
let lastCredits = null
|
||||
if (this.isSingleStudentGraph() && render) {
|
||||
const started = moment(students.minDate)
|
||||
|
||||
+27
-4
@@ -5,6 +5,8 @@ import { arrayOf, string, shape } from 'prop-types'
|
||||
import SearchResultTable from '../../SearchResultTable'
|
||||
import { getNewestProgramme, getTextIn } from '../../../common'
|
||||
import useLanguage from '../../LanguagePicker/useLanguage'
|
||||
import useProgrammeFilter from '../../FilterTray/filters/Programmes/useProgrammeFilter'
|
||||
import FilterToggleIcon from '../../FilterToggleIcon'
|
||||
|
||||
const CustomPopulationProgrammeDist = ({
|
||||
samples,
|
||||
@@ -13,7 +15,9 @@ const CustomPopulationProgrammeDist = ({
|
||||
populationStatistics,
|
||||
}) => {
|
||||
const { language } = useLanguage()
|
||||
const { selectedProgrammes, toggleFilterProgramme } = useProgrammeFilter()
|
||||
const [tableRows, setRows] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(populationStatistics).length > 0) {
|
||||
const allProgrammes = {}
|
||||
@@ -41,7 +45,8 @@ const CustomPopulationProgrammeDist = ({
|
||||
}
|
||||
})
|
||||
const rows = Object.entries(allProgrammes).map(([code, { programme, students }]) => [
|
||||
`${getTextIn(programme.name, language)}, ${code}`,
|
||||
getTextIn(programme.name, language),
|
||||
code,
|
||||
students.length,
|
||||
<Progress
|
||||
style={{ margin: '0px' }}
|
||||
@@ -49,14 +54,32 @@ const CustomPopulationProgrammeDist = ({
|
||||
progress
|
||||
/>,
|
||||
])
|
||||
const sortedRows = rows.sort((a, b) => b[1] - a[1])
|
||||
const sortedRows = rows.sort((a, b) => b[2] - a[2])
|
||||
setRows(sortedRows)
|
||||
}
|
||||
}, [selectedStudents])
|
||||
|
||||
const headers = ['Programmes', `Students (all=${selectedStudents.length})`, 'Percentage of population']
|
||||
const headers = ['Programme', 'Code', `Students (all=${selectedStudents.length})`, 'Percentage of population']
|
||||
|
||||
return <SearchResultTable headers={headers} rows={tableRows} selectable noResultText="placeholder" />
|
||||
const handleFilterToggle = code => {
|
||||
toggleFilterProgramme(code)
|
||||
}
|
||||
|
||||
const isProgrammeFilterActive = code => selectedProgrammes.find(p => p.code === code) !== undefined
|
||||
|
||||
return (
|
||||
<SearchResultTable
|
||||
headers={headers}
|
||||
rows={tableRows}
|
||||
selectable
|
||||
noResultText="placeholder"
|
||||
actionTrigger={row => (
|
||||
<span style={{ display: 'inline-block', marginRight: '0.3em' }}>
|
||||
<FilterToggleIcon onClick={() => handleFilterToggle(row[1])} isActive={isProgrammeFilterActive(row[1])} />
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
CustomPopulationProgrammeDist.defaultProps = {
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { Radio, Icon, Header, Segment, Loader, Popup } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import { func, shape, string, bool, arrayOf, number } from 'prop-types'
|
||||
import { getUsers } from '../../redux/users'
|
||||
import { getUnits } from '../../redux/units'
|
||||
import { getElementDetails } from '../../redux/elementdetails'
|
||||
import { makeSortUsers } from '../../selectors/users'
|
||||
import { copyToClipboard } from '../../common'
|
||||
import UserPageNew from '../UserPage'
|
||||
import UserSearchList from './UserSearchList'
|
||||
|
||||
class EnableUsers extends Component {
|
||||
state = {
|
||||
enabledOnly: true,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.elementdetails.length === 0) this.props.getElementDetails()
|
||||
if (this.props.units.length === 0) this.props.getUnits()
|
||||
if (this.props.users.length === 0) this.props.getUsers()
|
||||
document.title = 'Users - Oodikone'
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
clearTimeout(this.popupTimeout)
|
||||
}
|
||||
|
||||
toggleEnabledOnly() {
|
||||
if (this.props.enabledOnly) this.props.getUsers()
|
||||
const { enabledOnly } = this.state
|
||||
this.setState({ enabledOnly: !enabledOnly })
|
||||
}
|
||||
|
||||
openUsersPage = () => {
|
||||
const { history } = this.props
|
||||
history.push('/users')
|
||||
}
|
||||
|
||||
copyEmailsToClippoard = () => {
|
||||
const clipboardString = this.props.users
|
||||
.filter(u => u.is_enabled && u.email)
|
||||
.map(u => u.email)
|
||||
.join('; ')
|
||||
copyToClipboard(clipboardString)
|
||||
}
|
||||
|
||||
renderUserPage = userid => {
|
||||
const { users } = this.props
|
||||
const user = users.find(u => u.id === userid)
|
||||
return !user ? <Loader active /> : <UserPageNew userid={userid} user={user} goBack={this.openUsersPage} />
|
||||
}
|
||||
|
||||
handlePopupOpen = () => {
|
||||
this.setState({ popupOpen: true })
|
||||
this.popupTimeout = setTimeout(() => {
|
||||
this.setState({ popupOpen: false })
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
handlePopupClose = () => {
|
||||
this.popupTimeout = null
|
||||
}
|
||||
|
||||
render() {
|
||||
const { match, pending, users, error, elementdetails } = this.props
|
||||
const { enabledOnly } = this.state
|
||||
const { userid } = match.params
|
||||
return (
|
||||
<div style={{ marginBottom: '10px' }} className="segmentContainer">
|
||||
<Header className="segmentTitle" size="large">
|
||||
Oodikone users
|
||||
</Header>
|
||||
<Radio
|
||||
label={`Showing ${enabledOnly ? 'only enabled' : 'all'} users`}
|
||||
toggle
|
||||
onClick={() => this.toggleEnabledOnly()}
|
||||
/>
|
||||
<Segment loading={pending} className="contentSegment">
|
||||
{!userid ? (
|
||||
<UserSearchList enabledOnly={enabledOnly} users={users} error={error} elementdetails={elementdetails} />
|
||||
) : (
|
||||
this.renderUserPage(userid)
|
||||
)}
|
||||
</Segment>
|
||||
<Popup
|
||||
trigger={<Icon link name="envelope" onClick={this.copyEmailsToClippoard} style={{ float: 'right' }} />}
|
||||
content="Copied email(s)!"
|
||||
on="click"
|
||||
onOpen={this.handlePopupOpen}
|
||||
onClose={this.handlePopupClose}
|
||||
open={this.state.popupOpen}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
EnableUsers.propTypes = {
|
||||
match: shape({
|
||||
params: shape({
|
||||
studentNumber: string,
|
||||
}),
|
||||
}).isRequired,
|
||||
getUsers: func.isRequired,
|
||||
pending: bool.isRequired,
|
||||
getUnits: func.isRequired,
|
||||
enabledOnly: bool.isRequired,
|
||||
users: arrayOf(
|
||||
shape({
|
||||
id: string,
|
||||
full_name: string,
|
||||
is_enabled: bool,
|
||||
username: string,
|
||||
units: arrayOf(
|
||||
shape({
|
||||
id: string,
|
||||
name: shape({}).isRequired,
|
||||
})
|
||||
),
|
||||
})
|
||||
).isRequired,
|
||||
error: bool.isRequired,
|
||||
units: arrayOf(shape({})).isRequired,
|
||||
elementdetails: arrayOf(shape({ code: string, type: number, name: shape({}) })).isRequired,
|
||||
getElementDetails: func.isRequired,
|
||||
history: shape({}).isRequired,
|
||||
}
|
||||
|
||||
const sortUsers = makeSortUsers()
|
||||
|
||||
const mapStateToProps = ({ users, units, elementdetails }) => ({
|
||||
units: units.data,
|
||||
elementdetails: elementdetails.data,
|
||||
enabledOnly: users.enabledOnly,
|
||||
users: sortUsers(users),
|
||||
pending: typeof users.pending === 'boolean' ? users.pending : true,
|
||||
error: users.error || false,
|
||||
})
|
||||
|
||||
export default withRouter(
|
||||
connect(mapStateToProps, {
|
||||
getUsers,
|
||||
getUnits,
|
||||
getElementDetails,
|
||||
})(EnableUsers)
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
.flexIcon {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,25 +4,24 @@ import { CourseFilterProvider } from './filters/Courses/useCourseFilter'
|
||||
import { CreditFilterProvider } from './filters/CreditsEarned/useCreditFilter'
|
||||
import { AgeFilterProvider } from './filters/Age/useAgeFilter'
|
||||
import { FilterTrayProvider } from './useFilterTray'
|
||||
import { ProgrammeFilterProvider } from './filters/Programmes/useProgrammeFilter'
|
||||
import { FilterProvider } from './useFilters'
|
||||
import { FilterAnalyticsProvider } from './useAnalytics'
|
||||
import { GradeFilterProvider } from './filters/Grade/useGradeFilter'
|
||||
|
||||
const FilterContextProvider = ({ children }) => (
|
||||
<FilterAnalyticsProvider>
|
||||
<FilterTrayProvider>
|
||||
<FilterProvider>
|
||||
<CourseFilterProvider>
|
||||
<CreditFilterProvider>
|
||||
<AgeFilterProvider>
|
||||
<GradeFilterProvider>{children}</GradeFilterProvider>
|
||||
</AgeFilterProvider>
|
||||
</CreditFilterProvider>
|
||||
</CourseFilterProvider>
|
||||
</FilterProvider>
|
||||
</FilterTrayProvider>
|
||||
</FilterAnalyticsProvider>
|
||||
)
|
||||
const filterProviders = [
|
||||
FilterAnalyticsProvider,
|
||||
FilterTrayProvider,
|
||||
FilterProvider,
|
||||
CourseFilterProvider,
|
||||
CreditFilterProvider,
|
||||
AgeFilterProvider,
|
||||
GradeFilterProvider,
|
||||
ProgrammeFilterProvider,
|
||||
]
|
||||
|
||||
const FilterContextProvider = ({ children }) =>
|
||||
filterProviders.reduceRight((children, Provider) => <Provider>{children}</Provider>, children)
|
||||
|
||||
FilterContextProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
|
||||
@@ -6,6 +6,7 @@ import CreditsEarned from './filters/CreditsEarned'
|
||||
import Gender from './filters/Gender'
|
||||
import StartYearAtUni from './filters/StartYearAtUni'
|
||||
import Courses from './filters/Courses'
|
||||
import Programmes from './filters/Programmes'
|
||||
import useAnalytics from './useAnalytics'
|
||||
import Grade from './filters/Grade'
|
||||
import Date from './filters/Date'
|
||||
@@ -48,6 +49,8 @@ export const CoursePopulationFilters = () => {
|
||||
<Age />
|
||||
<Gender />
|
||||
<StartYearAtUni />
|
||||
<Courses />
|
||||
<Programmes />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { Dropdown } from 'semantic-ui-react'
|
||||
import FilterCard from '../common/FilterCard'
|
||||
import { getTextIn } from '../../../../common'
|
||||
import useLanguage from '../../../LanguagePicker/useLanguage'
|
||||
import useProgrammeFilter from './useProgrammeFilter'
|
||||
import useFilters from '../../useFilters'
|
||||
|
||||
const Programme = () => {
|
||||
const { programmes, selectedProgrammes, studentToProgrammeMap, selectFilterProgramme, removeFilterProgramme } =
|
||||
useProgrammeFilter()
|
||||
const { removeFilter, addFilter } = useFilters()
|
||||
const { language } = useLanguage()
|
||||
const name = 'programmeFilter'
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
programmes
|
||||
.map(program => ({
|
||||
key: `programme-filter-value-${program.code}`,
|
||||
text: getTextIn(program.name, language),
|
||||
value: program.code,
|
||||
}))
|
||||
.sort((a, b) => a.text.localeCompare(b.text)),
|
||||
[programmes, language]
|
||||
)
|
||||
|
||||
const handleChange = (_, { value }) => {
|
||||
value.filter(code => selectedProgrammes.findIndex(p => p.code === code) === -1).forEach(selectFilterProgramme)
|
||||
|
||||
selectedProgrammes.filter(({ code }) => value.indexOf(code) === -1).forEach(removeFilterProgramme)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProgrammes.length === 0) {
|
||||
removeFilter(name)
|
||||
} else {
|
||||
addFilter(name, ({ studentNumber }) => {
|
||||
return selectedProgrammes.findIndex(p => p.code === studentToProgrammeMap[studentNumber]) !== -1
|
||||
})
|
||||
}
|
||||
}, [selectedProgrammes, studentToProgrammeMap])
|
||||
|
||||
return (
|
||||
<FilterCard title="Programme" active={selectedProgrammes.length > 0} className="programmes-filter" name={name}>
|
||||
<Dropdown
|
||||
options={options}
|
||||
placeholder="Select Programme"
|
||||
onChange={handleChange}
|
||||
button
|
||||
value={selectedProgrammes.map(p => p.code)}
|
||||
name={name}
|
||||
closeOnChange
|
||||
multiple
|
||||
/>
|
||||
</FilterCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default Programme
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import React, { useState, createContext, useContext, useMemo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { getStudentToTargetCourseDateMap, getNewestProgramme } from '../../../../common'
|
||||
import useFilters from '../../useFilters'
|
||||
|
||||
const ProgrammeFilterContext = createContext([[], () => {}])
|
||||
ProgrammeFilterContext.displayName = 'Programme Filter'
|
||||
|
||||
const defaultState = {
|
||||
selectedProgrammes: [],
|
||||
}
|
||||
|
||||
export const ProgrammeFilterProvider = ({ children }) => {
|
||||
const [state, setState] = useState(defaultState)
|
||||
const [courseCodes, setCourseCodes] = useState([])
|
||||
|
||||
const { selectedProgrammes } = state
|
||||
|
||||
const setSelectedProgrammes = programmes => {
|
||||
setState({
|
||||
selectedProgrammes: programmes,
|
||||
})
|
||||
}
|
||||
|
||||
const context = {
|
||||
selectedProgrammes,
|
||||
setSelectedProgrammes,
|
||||
courseCodes,
|
||||
setCourseCodes,
|
||||
}
|
||||
|
||||
return <ProgrammeFilterContext.Provider value={context}>{children}</ProgrammeFilterContext.Provider>
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const { selectedProgrammes, courseCodes, setCourseCodes, setSelectedProgrammes } = useContext(ProgrammeFilterContext)
|
||||
|
||||
const { allStudents } = useFilters()
|
||||
|
||||
const studentToTargetCourseDateMap = useMemo(
|
||||
() => getStudentToTargetCourseDateMap(allStudents, courseCodes),
|
||||
[allStudents]
|
||||
)
|
||||
|
||||
const elementDetails = useSelector(state => state?.populations?.data?.elementdetails?.data)
|
||||
|
||||
const { programmes, studentToProgrammeMap } = useMemo(() => {
|
||||
if (!elementDetails) {
|
||||
return {
|
||||
programmes: [],
|
||||
studentToProgrammeMap: {},
|
||||
}
|
||||
}
|
||||
|
||||
const programmeMap = {}
|
||||
const studentToProgrammeMap = {}
|
||||
|
||||
allStudents
|
||||
.map(student => [
|
||||
student,
|
||||
getNewestProgramme(student.studyrights, student.studentNumber, studentToTargetCourseDateMap, elementDetails),
|
||||
])
|
||||
.forEach(([{ studentNumber }, programme]) => {
|
||||
programmeMap[programme.code] = programme
|
||||
studentToProgrammeMap[studentNumber] = programme.code
|
||||
})
|
||||
|
||||
return {
|
||||
programmes: Object.values(programmeMap),
|
||||
studentToProgrammeMap,
|
||||
}
|
||||
}, [allStudents, elementDetails])
|
||||
|
||||
const selectFilterProgramme = code => {
|
||||
const index = selectedProgrammes.findIndex(p => p.code === code)
|
||||
|
||||
if (index !== -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const programme = programmes.find(p => p.code === code)
|
||||
|
||||
if (programme === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedProgrammes([...selectedProgrammes, programme])
|
||||
}
|
||||
|
||||
const removeFilterProgramme = code => {
|
||||
const index = selectedProgrammes.findIndex(p => p.code === code)
|
||||
const newSelection = [...selectedProgrammes]
|
||||
newSelection.splice(index, 1)
|
||||
setSelectedProgrammes(newSelection)
|
||||
}
|
||||
|
||||
const toggleFilterProgramme = code => {
|
||||
const index = selectedProgrammes.findIndex(p => p.code === code)
|
||||
|
||||
if (index === -1) {
|
||||
const programme = programmes.find(p => p.code === code)
|
||||
setSelectedProgrammes([...selectedProgrammes, programme])
|
||||
} else {
|
||||
const newSelection = [...selectedProgrammes]
|
||||
newSelection.splice(index, 1)
|
||||
setSelectedProgrammes(newSelection)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
programmes,
|
||||
selectedProgrammes,
|
||||
toggleFilterProgramme,
|
||||
selectFilterProgramme,
|
||||
removeFilterProgramme,
|
||||
setCourseCodes,
|
||||
studentToProgrammeMap,
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ const FilterCard = ({ title, children, contextKey, name, active }) => {
|
||||
{title}
|
||||
</Header>
|
||||
</div>
|
||||
{open && children}
|
||||
{(active || open) && children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const FilterTray = ({ children, filterSet, visible }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'flex-start' }}>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'flex-start', width: '100%' }}>
|
||||
<Segment style={{ marginLeft: '0.5rem', width: '12rem', position: 'sticky', top: '10px' }}>
|
||||
<Header size="small" style={{ textAlign: 'center' }}>
|
||||
Filter students
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Container, Header, Image, Divider, List } from 'semantic-ui-react'
|
||||
import moment from 'moment'
|
||||
import { connect } from 'react-redux'
|
||||
@@ -8,11 +7,52 @@ import { isEqual } from 'lodash'
|
||||
import { images, getUserRoles, checkUserAccess } from '../common'
|
||||
import { useTitle } from '../common/hooks'
|
||||
import { builtAt } from '../conf'
|
||||
import oodiTXT from '../static/oodi.txt'
|
||||
|
||||
const OodiToOodikone = () => {
|
||||
const [oodis, setOodis] = useState([])
|
||||
useEffect(() => {
|
||||
const getOodis = async () => {
|
||||
try {
|
||||
const data = await fetch(oodiTXT)
|
||||
setOodis(
|
||||
(await data.text())
|
||||
.split('**')
|
||||
.map(o => o.trim())
|
||||
.filter(oodi => !!oodi)
|
||||
)
|
||||
} catch (error) {
|
||||
setOodis([])
|
||||
}
|
||||
}
|
||||
getOodis()
|
||||
}, [])
|
||||
|
||||
if (!oodis || oodis.length === 0) {
|
||||
return null
|
||||
}
|
||||
const oodi = oodis[Math.floor(Math.random() * oodis.length)].split('\n')
|
||||
return (
|
||||
<div style={{ margin: 'auto', width: '50%' }}>
|
||||
{oodi.map((l, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<p key={index} style={{ textAlign: 'center', fontStyle: 'italic', margin: 0 }}>
|
||||
{index === 0 && '\u201C'}
|
||||
{l}
|
||||
{index === oodi.length - 1 && '\u201D'}
|
||||
</p>
|
||||
))}
|
||||
<p style={{ marginTop: '1.3em', textAlign: 'center' }}>
|
||||
- <a href="http://www.helsinki.fi/discovery">R. U. Nokone</a>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const FrontPage = props => {
|
||||
const { userRoles, rights } = props
|
||||
useTitle()
|
||||
|
||||
useTitle()
|
||||
const showItems = {
|
||||
populations: userRoles.includes('admin') || rights.length !== 0,
|
||||
studyProgramme: userRoles.includes('admin') || rights.length !== 0,
|
||||
@@ -32,6 +72,8 @@ const FrontPage = props => {
|
||||
<Header as="h3" style={{ textAlign: 'center' }}>
|
||||
Exploratory Research on Study Data
|
||||
</Header>
|
||||
<OodiToOodikone />
|
||||
|
||||
{showItems.populations && (
|
||||
<>
|
||||
<Divider section />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import { shape, string } from 'prop-types'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import NavigationBar from '../NavigationBar'
|
||||
import './header.css'
|
||||
@@ -10,10 +9,4 @@ const Header = props => (
|
||||
</header>
|
||||
)
|
||||
|
||||
Header.propTypes = {
|
||||
location: shape({
|
||||
pathname: string.isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export default withRouter(Header)
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { Message } from 'semantic-ui-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import { isEqual } from 'lodash'
|
||||
import { getHideSisWarningFlag, setHideSisWarningFlag, getUserRoles } from '../../common'
|
||||
|
||||
const SisChangeMessage = props => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { userRoles, rights } = props
|
||||
|
||||
const handleDismiss = () => {
|
||||
setOpen(false)
|
||||
setHideSisWarningFlag(true)
|
||||
}
|
||||
|
||||
if (!open || getHideSisWarningFlag()) return null
|
||||
|
||||
const reasons = [
|
||||
'Osalla opiskelijoista voi olla opintopisteiden kokonaismäärässä eroja. Mahdollinen eroavaisuus on merkitty kyseisen opiskelijan sivulla. Tämä koskee erityisesti tiedekuntia, joissa on runsaasti osasuorituksia.',
|
||||
'Populaatioiden osalta vaihto-opiskelijoiden, tutkintoon johtamattomien opinto-oikeuksien ja ohjelmaan siirrettyjen opiskelijoiden näyttäminen ei kaikissa tapauksissa toimi oikein.',
|
||||
'Trendien osalta ainoastaan "See current status" -sivu toimii, muut eivät näytä vuositahteja oikein.',
|
||||
]
|
||||
|
||||
// Kasvatustiedehuomiot
|
||||
if (
|
||||
rights.includes('KH60_001') ||
|
||||
rights.includes('MH60_001') ||
|
||||
rights.includes('MH60_002') ||
|
||||
userRoles.includes('admin')
|
||||
) {
|
||||
reasons.push('Osalla Varhaiskasvatuksen opettaja -tutkintosuunnan opiskelijoista on virheellisiä opinto-oikeuksia.')
|
||||
}
|
||||
|
||||
// Oikeustiedehuomiot
|
||||
if (rights.includes('KH20_001') || rights.includes('MH20_001') || userRoles.includes('admin')) {
|
||||
reasons.push(
|
||||
'Oikeustieteellisen tiedekunnan tuottamien opintopisteiden kokonaismäärä on Trends-osiossa liian alhainen.'
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Message onDismiss={handleDismiss} info size="huge">
|
||||
<Message.Header>Oodikone käyttää nyt Sisua</Message.Header>
|
||||
<p>
|
||||
Opiskelijoiden tiedot saattavat siirtymän myötä poiketa joiltain osin aiemmasta. Erityisesti siirtymän tiedetään
|
||||
vaikuttaneen seuraaviin asioihin:
|
||||
</p>
|
||||
<Message.List items={reasons} />
|
||||
<p>
|
||||
Mikäli huomaat tiedoissa virheitä, laitathan palautetta sähköpostitse{' '}
|
||||
<a href="mailto:grp-toska@helsinki.fi">grp-toska@helsinki.fi</a> tai{' '}
|
||||
<Link to="/feedback">palautelomakkeella</Link>.
|
||||
</p>
|
||||
</Message>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = ({
|
||||
auth: {
|
||||
token: { roles, rights },
|
||||
},
|
||||
}) => ({
|
||||
userRoles: getUserRoles(roles),
|
||||
rights,
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, null, null, {
|
||||
areStatePropsEqual: isEqual,
|
||||
})(SisChangeMessage)
|
||||
@@ -6,7 +6,6 @@ import TSA from '../../common/tsa'
|
||||
import Header from '../Header'
|
||||
import ErrorContainer from '../ErrorContainer'
|
||||
import Routes from '../Routes'
|
||||
import SisChangeMessage from './SisChangeMessage'
|
||||
import './main.css'
|
||||
import { isDev, basePath } from '../../conf'
|
||||
|
||||
@@ -24,7 +23,6 @@ export default () => {
|
||||
<Router basename={basePath} history={history}>
|
||||
<main className="routeViewContainer">
|
||||
<Header />
|
||||
<SisChangeMessage />
|
||||
<ErrorContainer />
|
||||
<Routes />
|
||||
</main>
|
||||
|
||||
@@ -3,11 +3,12 @@ import { Menu, Dropdown, Button, Label } from 'semantic-ui-react'
|
||||
import { NavLink, Link } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import { isEqual } from 'lodash'
|
||||
import { getUserRoles, setMocking, checkUserAccess } from '../../common'
|
||||
import { getUserRoles, checkUserAccess } from '../../common'
|
||||
import { logout as logoutAction } from '../../redux/auth'
|
||||
import './navigationBar.css'
|
||||
import LanguagePicker from '../LanguagePicker'
|
||||
import { isDev, adminerUrls } from '../../conf'
|
||||
import { useShowAsUser } from '../../common/hooks'
|
||||
|
||||
const allNavigationItems = {
|
||||
populations: {
|
||||
@@ -35,6 +36,7 @@ const allNavigationItems = {
|
||||
|
||||
const NavigationBar = props => {
|
||||
const { logout, userRoles, rights, mockedBy, userId } = props
|
||||
const showAsUser = useShowAsUser()
|
||||
|
||||
const refreshNavigationRoutes = () => {
|
||||
const visibleNavigationItems = {}
|
||||
@@ -56,11 +58,6 @@ const NavigationBar = props => {
|
||||
|
||||
const visibleNavigationItems = refreshNavigationRoutes()
|
||||
|
||||
const returnToSelf = () => {
|
||||
setMocking(null)
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
const renderHome = () => (
|
||||
<Menu.Item as={Link} to="/" tabIndex="-1">
|
||||
<span className="logo">
|
||||
@@ -132,7 +129,7 @@ const NavigationBar = props => {
|
||||
|
||||
const renderStopMockingButton = () => (
|
||||
<Menu.Item>
|
||||
<Button onClick={returnToSelf}>Stop mocking as {userId}</Button>
|
||||
<Button onClick={() => showAsUser(null)}>Stop mocking as {userId}</Button>
|
||||
</Menu.Item>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { shape, arrayOf, string, bool } from 'prop-types'
|
||||
import { Segment } from 'semantic-ui-react'
|
||||
import { Segment, Button } from 'semantic-ui-react'
|
||||
import uuidv4 from 'uuid/v4'
|
||||
import SegmentDimmer from '../SegmentDimmer'
|
||||
import PopulationCourseStats from '../PopulationCourseStats'
|
||||
import CustomPopulationCourses from '../CustomPopulation/CustomPopulationCourses'
|
||||
import InfoBox from '../InfoBox'
|
||||
import FilterDegreeCoursesModal from './FilterDegreeCoursesModal'
|
||||
import useCourseFilter from '../FilterTray/filters/Courses/useCourseFilter'
|
||||
@@ -18,6 +19,7 @@ const PopulationCourses = ({
|
||||
filteredStudents,
|
||||
}) => {
|
||||
const { setCoursesOnce, resetCourses, runCourseQuery } = useCourseFilter()
|
||||
const [showByStudytrack, setShowByStudytrack] = useState(true)
|
||||
|
||||
const selectedPopulationCourses = populationSelectedStudentCourses.data
|
||||
? populationSelectedStudentCourses
|
||||
@@ -70,18 +72,30 @@ const PopulationCourses = ({
|
||||
return resetCourses
|
||||
}, [])
|
||||
|
||||
const changeStructure = () => {
|
||||
setShowByStudytrack(!showByStudytrack)
|
||||
}
|
||||
|
||||
return (
|
||||
<Segment basic>
|
||||
<InfoBox content={infotooltips.PopulationStatistics.CoursesOfPopulation} />
|
||||
<Button primary onClick={changeStructure} style={{ marginLeft: '1em' }}>
|
||||
{showByStudytrack === true ? 'Show the most attained courses' : 'Show by programme structure'}
|
||||
</Button>
|
||||
{query.studyRights.programme && <FilterDegreeCoursesModal studyProgramme={query.studyRights.programme} />}
|
||||
|
||||
<SegmentDimmer isLoading={pending} />
|
||||
<PopulationCourseStats
|
||||
key={selectedPopulationCourses.query.uuid}
|
||||
courses={selectedPopulationCourses.data}
|
||||
query={selectedPopulationCourses.query}
|
||||
pending={pending}
|
||||
selectedStudents={selectedStudents}
|
||||
/>
|
||||
{showByStudytrack ? (
|
||||
<PopulationCourseStats
|
||||
key={selectedPopulationCourses.query.uuid}
|
||||
courses={selectedPopulationCourses.data}
|
||||
query={selectedPopulationCourses.query}
|
||||
pending={pending}
|
||||
selectedStudents={selectedStudents}
|
||||
/>
|
||||
) : (
|
||||
<CustomPopulationCourses selectedStudents={selectedStudents} showFilter={false} />
|
||||
)}
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
|
||||
+1
-6
@@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import { jStat } from 'jStat'
|
||||
import PropTypes from 'prop-types'
|
||||
import { jStat } from 'jstat'
|
||||
import { Table } from 'semantic-ui-react'
|
||||
import { getStudentTotalCredits } from '../../../common'
|
||||
|
||||
@@ -54,8 +53,4 @@ const StatisticsTab = ({ filteredStudents }) => {
|
||||
)
|
||||
}
|
||||
|
||||
StatisticsTab.propTypes = {
|
||||
filteredStudents: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
}
|
||||
|
||||
export default StatisticsTab
|
||||
|
||||
@@ -250,7 +250,7 @@ const GeneralTab = ({
|
||||
})
|
||||
}
|
||||
|
||||
if (coursecode) {
|
||||
if (coursecode.length > 0) {
|
||||
columns.push({
|
||||
key: 'grade for single course',
|
||||
title: 'Grade',
|
||||
|
||||
@@ -422,7 +422,7 @@ PopulationStudents.defaultProps = {
|
||||
customPopulation: false,
|
||||
coursePopulation: false,
|
||||
dataExport: null,
|
||||
coursecode: '',
|
||||
coursecode: [],
|
||||
}
|
||||
|
||||
PopulationStudents.propTypes = {
|
||||
@@ -446,7 +446,7 @@ PopulationStudents.propTypes = {
|
||||
customPopulation: bool,
|
||||
filteredStudents: arrayOf(shape({})).isRequired,
|
||||
dataExport: node,
|
||||
coursecode: string,
|
||||
coursecode: arrayOf(string),
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
|
||||
@@ -30,7 +30,7 @@ const FrontPage = React.lazy(() => retry(() => import('../FrontPage')))
|
||||
const Populations = React.lazy(() => retry(() => import('../PopulationStatistics')))
|
||||
const StudentStatistics = React.lazy(() => retry(() => import('../StudentStatistics')))
|
||||
const CourseStatistics = React.lazy(() => retry(() => import('../CourseStatistics')))
|
||||
const EnableUsers = React.lazy(() => retry(() => import('../EnableUsers')))
|
||||
const Users = React.lazy(() => retry(() => import('../Users')))
|
||||
const StudyProgramme = React.lazy(() => retry(() => import('../StudyProgramme')))
|
||||
const Teachers = React.lazy(() => retry(() => import('../Teachers')))
|
||||
const Feedback = React.lazy(() => retry(() => import('../Feedback')))
|
||||
@@ -74,7 +74,7 @@ const Routes = () => (
|
||||
path={routes.courseStatistics}
|
||||
component={CourseStatistics}
|
||||
/>
|
||||
<ProtectedRoute requiredRoles={['admin']} exact path={routes.users} component={EnableUsers} />
|
||||
<ProtectedRoute requiredRoles={['admin']} exact path={routes.users} component={Users} />
|
||||
<ProtectedRoute requiredRoles={['teachers']} exact path={routes.teachers} component={Teachers} />
|
||||
<ProtectedRoute requireUserHasRights exact path={routes.coursepopulation} component={CoursePopulation} />
|
||||
<ProtectedRoute requireUserHasRights exact path={routes.custompopulation} component={CustomPopulation} />
|
||||
|
||||
+46
-99
@@ -1,68 +1,63 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import moment from 'moment'
|
||||
import { shape, arrayOf, func } from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Divider, Table, Label, Header } from 'semantic-ui-react'
|
||||
|
||||
import { getNewestProgramme, reformatDate } from '../../../common'
|
||||
import { getMandatoryCourseModules } from '../../../redux/populationMandatoryCourses'
|
||||
import {
|
||||
bachelorHonoursProgrammes as bachelorCodes,
|
||||
bachelorHonoursBasicModules as basicHonoursModules,
|
||||
bachelorHonoursIntermediateModules as intermediateHonoursModules,
|
||||
reformatDate,
|
||||
} from '../../../common'
|
||||
|
||||
const bachelorCodes = [
|
||||
'KH50_001',
|
||||
'KH50_002',
|
||||
'KH50_003',
|
||||
'KH50_004',
|
||||
'KH50_005',
|
||||
'KH50_006',
|
||||
'KH50_007',
|
||||
'KH50_008',
|
||||
'00345',
|
||||
]
|
||||
|
||||
const BachelorHonours = ({ student, programmes, getMandatoryCourseModulesDispatch, mandatoryModules, absentYears }) => {
|
||||
const BachelorHonours = ({ student, absentYears, programmeCode }) => {
|
||||
const mandatoryModules = useSelector(({ populationMandatoryCourses }) => populationMandatoryCourses.data)
|
||||
const [studentsModules, setModules] = useState([])
|
||||
const [otherModules, setOther] = useState([])
|
||||
const [studyStartDate, setStartDate] = useState('')
|
||||
const [honors, setHonors] = useState(false)
|
||||
const [render, setRender] = useState(false)
|
||||
const [graduated, setGraduated] = useState(false)
|
||||
const [inspection, setInspection] = useState(false)
|
||||
const [reason, setReason] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (programmes) {
|
||||
if (programmeCode) {
|
||||
const bachelorStudyrights = student.studyrights.filter(sr => sr.extentcode === 1)
|
||||
const newestBachelorProgramme = getNewestProgramme(bachelorStudyrights, student.studentNumber, null, programmes)
|
||||
const studyrightWithNewestProgramme = bachelorStudyrights.find(sr =>
|
||||
sr.studyright_elements.map(srE => srE.code).includes(newestBachelorProgramme.code)
|
||||
sr.studyright_elements.map(srE => srE.code).includes(programmeCode)
|
||||
)
|
||||
|
||||
if (studyrightWithNewestProgramme) {
|
||||
setStartDate(moment(studyrightWithNewestProgramme.startdate))
|
||||
setGraduated(!!studyrightWithNewestProgramme.graduated)
|
||||
}
|
||||
// currently only for matlu
|
||||
const shouldRender = bachelorCodes.includes(newestBachelorProgramme.code)
|
||||
|
||||
if (shouldRender) getMandatoryCourseModulesDispatch(newestBachelorProgramme.code)
|
||||
setRender(shouldRender)
|
||||
}
|
||||
}, [programmes])
|
||||
}, [programmeCode, student])
|
||||
|
||||
useEffect(() => {
|
||||
// very naive perus- and aineopinto filtering
|
||||
const basicAndIntermediateStudies = mandatoryModules
|
||||
.filter(mod => mod.name.fi.includes('perusopinnot') || mod.name.fi.includes('aineopinnot'))
|
||||
.map(mod => mod.code)
|
||||
const attainedModules = student.courses.filter(
|
||||
course => basicAndIntermediateStudies.includes(course.course_code) || bachelorCodes.includes(course.course_code)
|
||||
const mandatoryModuleCodes = mandatoryModules.map(mod => mod.code)
|
||||
const degreeModule = student.courses.find(mod => bachelorCodes.includes(mod.course_code))
|
||||
const basicModules = student.courses.filter(mod => basicHonoursModules[programmeCode].includes(mod.course_code))
|
||||
const intermediateModules = student.courses.filter(mod =>
|
||||
intermediateHonoursModules[programmeCode].includes(mod.course_code)
|
||||
)
|
||||
|
||||
let honoursModules = []
|
||||
if (degreeModule) honoursModules = [...honoursModules, degreeModule]
|
||||
if (basicModules.length) honoursModules = [...honoursModules, ...basicModules]
|
||||
if (intermediateModules.length) honoursModules = [...honoursModules, ...intermediateModules]
|
||||
const honoursModulesCodes = honoursModules?.map(mod => mod.course_code)
|
||||
|
||||
setModules(honoursModules)
|
||||
setOther(
|
||||
student.courses.filter(
|
||||
c => !honoursModulesCodes.includes(c.course_code) && mandatoryModuleCodes.includes(c.course_code)
|
||||
)
|
||||
)
|
||||
|
||||
let inTime = false
|
||||
const degree = attainedModules.find(mod => bachelorCodes.includes(mod.course_code))
|
||||
|
||||
if (degree) {
|
||||
const graduationDate = moment(degree.date)
|
||||
if (degreeModule) {
|
||||
const graduationDate = moment(degreeModule.date)
|
||||
const yearsForGraduation = moment.duration(graduationDate.diff(studyStartDate)).asYears()
|
||||
|
||||
// calculate time student has been absent during bachelors degree
|
||||
@@ -80,53 +75,21 @@ const BachelorHonours = ({ student, programmes, getMandatoryCourseModulesDispatc
|
||||
inTime = yearsForGraduation <= 3 + Math.round(timeAbsent * 10) / 10
|
||||
}
|
||||
|
||||
if (attainedModules.length > 3) {
|
||||
// student needs only one basic and one intermediate studies package
|
||||
// so if there are more than two attained modules + bachelors
|
||||
// filter out the module that does not match
|
||||
const pairedModules = []
|
||||
attainedModules.forEach(mod => {
|
||||
const filtered = attainedModules.filter(
|
||||
mod2 => mod2.course_code.slice(4, mod.course_code.length) === mod.course_code.slice(4, mod.course_code.length)
|
||||
)
|
||||
if (filtered.length > 1) pairedModules.push(mod)
|
||||
if (bachelorCodes.includes(mod.course_code)) pairedModules.push(mod)
|
||||
})
|
||||
const allBasicUnderFour = basicModules.every(mod => Number(mod.grade) < 4)
|
||||
const basicAboveFour = basicModules.find(mod => Number(mod.grade) > 3)
|
||||
const allIntermediateUnderFour = intermediateModules.every(mod => Number(mod.grade) < 4)
|
||||
const intermediateAboveFour = intermediateModules.find(mod => Number(mod.grade) > 3)
|
||||
|
||||
const filterGrades = pairedModules.filter(
|
||||
mod => !bachelorCodes.includes(mod.course_code) && Number(mod.grade) > 3
|
||||
)
|
||||
setHonors(basicAboveFour && intermediateAboveFour && inTime)
|
||||
|
||||
const leftOutModules = attainedModules.filter(mod => !pairedModules.includes(mod))
|
||||
|
||||
setHonors(filterGrades.length > 1 && inTime)
|
||||
|
||||
if (!inTime) {
|
||||
setReason(degree ? 'Did not graduate in time' : 'Has not graduated')
|
||||
} else if (filterGrades.length <= 1) {
|
||||
setReason('Module grades too low')
|
||||
}
|
||||
|
||||
setModules(pairedModules)
|
||||
setOther(leftOutModules)
|
||||
setInspection(leftOutModules.length > 0 || (graduated && pairedModules.length < 3))
|
||||
} else {
|
||||
const filterGrades = attainedModules.filter(
|
||||
mod => !bachelorCodes.includes(mod.course_code) && Number(mod.grade) > 3
|
||||
)
|
||||
|
||||
setHonors(filterGrades.length > 1 && inTime)
|
||||
|
||||
if (!inTime) {
|
||||
setReason(degree ? 'Did not graduate in time' : 'Has not graduated')
|
||||
} else if (filterGrades.length <= 1) {
|
||||
setReason('Module grades too low')
|
||||
}
|
||||
|
||||
setModules(attainedModules)
|
||||
setInspection(inTime && graduated && attainedModules.length < 3)
|
||||
if (!inTime) {
|
||||
setReason(degreeModule ? 'Did not graduate in time' : 'Has not graduated')
|
||||
} else if (inTime && graduated && honoursModules.length < 3 && honoursModules.length > 4) {
|
||||
setInspection(true)
|
||||
} else if (graduated && (allBasicUnderFour || allIntermediateUnderFour)) {
|
||||
setReason('Module grades too low')
|
||||
}
|
||||
}, [mandatoryModules])
|
||||
}, [mandatoryModules, student])
|
||||
|
||||
const dataRows = modules =>
|
||||
modules.map(mod => (
|
||||
@@ -152,7 +115,7 @@ const BachelorHonours = ({ student, programmes, getMandatoryCourseModulesDispatc
|
||||
</Table>
|
||||
)
|
||||
|
||||
if (!render) return null
|
||||
if (!programmeCode || !student || !student.courses || !student.studyrights || !mandatoryModules?.length) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -183,20 +146,4 @@ const BachelorHonours = ({ student, programmes, getMandatoryCourseModulesDispatc
|
||||
)
|
||||
}
|
||||
|
||||
BachelorHonours.propTypes = {
|
||||
student: shape({}).isRequired,
|
||||
programmes: shape({}).isRequired,
|
||||
mandatoryModules: arrayOf(shape({})).isRequired,
|
||||
absentYears: arrayOf(shape({})).isRequired,
|
||||
getMandatoryCourseModulesDispatch: func.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ populationMandatoryCourses }) => {
|
||||
return {
|
||||
mandatoryModules: populationMandatoryCourses.data || [],
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { getMandatoryCourseModulesDispatch: getMandatoryCourseModules })(
|
||||
BachelorHonours
|
||||
)
|
||||
export default BachelorHonours
|
||||
|
||||
+4
-3
@@ -14,8 +14,9 @@ const CourseParticipationTable = ({ student, language, clearCourseStats }) => {
|
||||
const courseRowsByAcademicYear = {}
|
||||
|
||||
student.courses.sort(byDateDesc).forEach(c => {
|
||||
const { date, grade, credits, course, isStudyModuleCredit, passed } = c
|
||||
const { date, grade, credits, course, isStudyModuleCredit, passed, credittypecode } = c
|
||||
let icon = null
|
||||
|
||||
if (isStudyModuleCredit) {
|
||||
icon = <Icon name="certificate" color="purple" />
|
||||
} else if (passed) {
|
||||
@@ -36,7 +37,7 @@ const CourseParticipationTable = ({ student, language, clearCourseStats }) => {
|
||||
reformatDate(date, 'DD.MM.YYYY'),
|
||||
`${
|
||||
isStudyModuleCredit ? `${getTextIn(course.name, language)} [Study Module]` : getTextIn(course.name, language)
|
||||
} (${course.code})`,
|
||||
} ${credittypecode === 7 ? `, ${course.code} (korotettu)` : `(${course.code})`}`,
|
||||
<div>
|
||||
{icon}
|
||||
{grade}
|
||||
@@ -54,7 +55,7 @@ const CourseParticipationTable = ({ student, language, clearCourseStats }) => {
|
||||
reformatDate(date, 'DD.MM.YYYY'),
|
||||
`${
|
||||
isStudyModuleCredit ? `${getTextIn(course.name, language)} [Study Module]` : getTextIn(course.name, language)
|
||||
} (${course.code})`,
|
||||
} ${credittypecode === 7 ? `, ${course.code} (korotettu)` : `(${course.code})`}`,
|
||||
<div>
|
||||
{icon}
|
||||
{grade}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { func, shape, string, arrayOf, integer, bool } from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { connect, useDispatch } from 'react-redux'
|
||||
import { Segment, Loader } from 'semantic-ui-react'
|
||||
import { isEmpty, sortBy } from 'lodash'
|
||||
import moment from 'moment'
|
||||
@@ -10,7 +10,7 @@ import { withRouter } from 'react-router-dom'
|
||||
import { getStudent, removeStudentSelection, resetStudent } from '../../../redux/students'
|
||||
import { getSemesters } from '../../../redux/semesters'
|
||||
import StudentInfoCard from '../StudentInfoCard'
|
||||
import { getUserIsAdmin } from '../../../common'
|
||||
import { getUserIsAdmin, bachelorHonoursProgrammes as bachelorCodes, getNewestProgramme } from '../../../common'
|
||||
import { clearCourseStats } from '../../../redux/coursestats'
|
||||
import { getProgrammes } from '../../../redux/populationProgrammes'
|
||||
import BachelorHonours from './BachelorHonours'
|
||||
@@ -19,6 +19,7 @@ import TagsTable from './TagsTable'
|
||||
import CourseParticipationTable from './CourseParticipationTable'
|
||||
import StudentGraphs from './StudentGraphs'
|
||||
import useLanguage from '../../LanguagePicker/useLanguage'
|
||||
import { getMandatoryCourseModules } from '../../../redux/populationMandatoryCourses'
|
||||
|
||||
const StudentDetails = ({
|
||||
student,
|
||||
@@ -36,9 +37,11 @@ const StudentDetails = ({
|
||||
fetching,
|
||||
clearCourseStats,
|
||||
}) => {
|
||||
const dispatch = useDispatch()
|
||||
const { language } = useLanguage()
|
||||
const [graphYearStart, setGraphYear] = useState('')
|
||||
const [studyrightid, setStudyrightid] = useState('')
|
||||
const [honoursCode, setHonoursCode] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
getProgrammes()
|
||||
@@ -54,6 +57,22 @@ const StudentDetails = ({
|
||||
}
|
||||
}, [studentNumber])
|
||||
|
||||
useEffect(() => {
|
||||
if (Programmes.programmes && student && student.studyrights) {
|
||||
const bachelorStudyrights = student.studyrights.filter(sr => sr.extentcode === 1)
|
||||
const newestBachelorProgramme = getNewestProgramme(
|
||||
bachelorStudyrights,
|
||||
student.studentNumber,
|
||||
null,
|
||||
Programmes.programmes
|
||||
)
|
||||
// currently only for matlu
|
||||
const shouldRender = bachelorCodes.includes(newestBachelorProgramme.code)
|
||||
if (shouldRender) dispatch(getMandatoryCourseModules(newestBachelorProgramme.code))
|
||||
if (shouldRender) setHonoursCode(newestBachelorProgramme.code)
|
||||
}
|
||||
}, [Programmes, student])
|
||||
|
||||
const getAbsentYears = () => {
|
||||
semesterenrollments.sort((a, b) => a.semestercode - b.semestercode)
|
||||
const acualSemesters = semesters.semesters
|
||||
@@ -173,7 +192,7 @@ const StudentDetails = ({
|
||||
showPopulationStatistics={showPopulationStatistics}
|
||||
studyrightid={studyrightid}
|
||||
/>
|
||||
<BachelorHonours student={student} programmes={Programmes.programmes || {}} absentYears={getAbsentYears()} />
|
||||
{honoursCode && <BachelorHonours student={student} absentYears={getAbsentYears()} programmeCode={honoursCode} />}
|
||||
<CourseParticipationTable student={student} language={language} clearCourseStats={clearCourseStats} />
|
||||
</Segment>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
// temp disable prop types
|
||||
import React, { useEffect, useState, useRef } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Button, Loader, Header, Accordion, Divider, Segment } from 'semantic-ui-react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import scrollToComponent from 'react-scroll-to-component'
|
||||
import { getCustomPopulation } from '../../redux/populations'
|
||||
import { getCustomPopulationCoursesByStudentnumbers } from '../../redux/populationCourses'
|
||||
import StyledMessage from './StyledMessage'
|
||||
import { getTextIn } from '../../common'
|
||||
import FilterTray from '../FilterTray'
|
||||
import useFilters from '../FilterTray/useFilters'
|
||||
import { CustomPopulationFilters } from '../FilterTray/FilterSets'
|
||||
import useFilterTray from '../FilterTray/useFilterTray'
|
||||
import CreditAccumulationGraphHighCharts from '../CreditAccumulationGraphHighCharts'
|
||||
import PopulationStudents from '../PopulationStudents'
|
||||
import CustomPopulationCourses from '../CustomPopulation/CustomPopulationCourses'
|
||||
import CustomPopulationProgrammeDist from '../CustomPopulation/CustomPopulationProgrammeDist'
|
||||
import InfoBox from '../InfoBox'
|
||||
import infotooltips from '../../common/InfoToolTips'
|
||||
|
||||
const SingleStudyGroupContent = ({ population, language }) => {
|
||||
const custompop = population?.students || []
|
||||
const { setAllStudents, filteredStudents } = useFilters()
|
||||
const [trayOpen] = useFilterTray()
|
||||
const selectedStudents = filteredStudents.map(stu => stu.studentNumber)
|
||||
const creditGainRef = useRef()
|
||||
const programmeRef = useRef()
|
||||
const coursesRef = useRef()
|
||||
const studentRef = useRef()
|
||||
const refs = [creditGainRef, programmeRef, coursesRef, studentRef]
|
||||
const [activeIndex, setIndex] = useState([])
|
||||
const [newestIndex, setNewest] = useState(null)
|
||||
|
||||
const handleClick = index => {
|
||||
const indexes = [...activeIndex].sort()
|
||||
if (indexes.includes(index)) {
|
||||
indexes.splice(
|
||||
indexes.findIndex(ind => ind === index),
|
||||
1
|
||||
)
|
||||
} else {
|
||||
indexes.push(index)
|
||||
}
|
||||
if (activeIndex.length < indexes.length) setNewest(index)
|
||||
else setNewest(null)
|
||||
setIndex(indexes)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setAllStudents(custompop)
|
||||
handleClick(0)
|
||||
}, [custompop])
|
||||
|
||||
useEffect(() => {
|
||||
if (newestIndex) {
|
||||
scrollToComponent(refs[newestIndex].current, { align: 'bottom' })
|
||||
}
|
||||
}, [activeIndex])
|
||||
|
||||
const panels = [
|
||||
{
|
||||
key: 0,
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
Credit accumulation (for {selectedStudents.length} students)
|
||||
</span>
|
||||
),
|
||||
},
|
||||
onTitleClick: () => handleClick(0),
|
||||
content: {
|
||||
content: (
|
||||
<div ref={creditGainRef}>
|
||||
<CreditAccumulationGraphHighCharts
|
||||
students={custompop}
|
||||
selectedStudents={selectedStudents}
|
||||
render={false}
|
||||
trayOpen={trayOpen}
|
||||
language={language}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
Programme distribution
|
||||
</span>
|
||||
),
|
||||
},
|
||||
onTitleClick: () => handleClick(1),
|
||||
content: {
|
||||
content: (
|
||||
<div ref={programmeRef}>
|
||||
<InfoBox content={infotooltips.PopulationStatistics.ProgrammeDistributionCoursePopulation} />
|
||||
<CustomPopulationProgrammeDist samples={custompop} selectedStudents={selectedStudents} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
Courses of population
|
||||
</span>
|
||||
),
|
||||
},
|
||||
onTitleClick: () => handleClick(2),
|
||||
content: {
|
||||
content: (
|
||||
<div ref={coursesRef}>
|
||||
<CustomPopulationCourses selectedStudents={selectedStudents} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
Students ({selectedStudents.length})
|
||||
</span>
|
||||
),
|
||||
},
|
||||
onTitleClick: () => handleClick(3),
|
||||
content: {
|
||||
content: (
|
||||
<div ref={studentRef}>
|
||||
<PopulationStudents language={language} filteredStudents={filteredStudents} customPopulation />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<FilterTray filterSet={<CustomPopulationFilters />}>
|
||||
<Accordion activeIndex={activeIndex} exclusive={false} styled fluid panels={panels} />
|
||||
</FilterTray>
|
||||
)
|
||||
}
|
||||
|
||||
const SingleStudyGroupViewWrapper = ({ history, groupName, language, children }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="segmentContainer">
|
||||
<Segment className="contentSegment">
|
||||
<Button icon="arrow circle left" content="Back" onClick={() => history.push('/studyguidancegroups')} />
|
||||
<Divider />
|
||||
<Header size="medium">{getTextIn(groupName, language)}</Header>
|
||||
</Segment>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SingleStudyGuidanceGroupContainer = ({ groupid }) => {
|
||||
const dispatch = useDispatch()
|
||||
const history = useHistory()
|
||||
const { language } = useSelector(({ settings }) => settings)
|
||||
const { data: groups } = useSelector(({ studyGuidanceGroups }) => studyGuidanceGroups)
|
||||
const group = groups.find(g => g.id === groupid)
|
||||
const groupStudentNumbers = group?.members?.map(({ personStudentNumber }) => personStudentNumber) || []
|
||||
const { data: population, pending } = useSelector(({ populations }) => populations)
|
||||
|
||||
useEffect(() => {
|
||||
if (!group) return
|
||||
const groupStudentNumbers = group?.members?.map(({ personStudentNumber }) => personStudentNumber) || []
|
||||
if (groupStudentNumbers.length === 0) return
|
||||
dispatch(getCustomPopulation({ studentnumberlist: groupStudentNumbers, usingStudyGuidanceGroups: true }))
|
||||
dispatch(
|
||||
getCustomPopulationCoursesByStudentnumbers({
|
||||
studentnumberlist: groupStudentNumbers,
|
||||
usingStudyGuidanceGroups: true,
|
||||
})
|
||||
)
|
||||
}, [])
|
||||
|
||||
if (!group) {
|
||||
return (
|
||||
<StyledMessage>
|
||||
Couldn't find group with id {groupid}! Please check that id is correct and you have rights to this study
|
||||
guidance group.
|
||||
</StyledMessage>
|
||||
)
|
||||
}
|
||||
|
||||
const isLoading = pending === undefined || pending === true
|
||||
const isLoaded = pending === false && Object.keys(population).length > 0
|
||||
const renderMessage = groupStudentNumbers.length === 0
|
||||
const renderLoader = groupStudentNumbers.length > 0 && isLoading
|
||||
const renderGroup = groupStudentNumbers.length > 0 && isLoaded
|
||||
|
||||
return (
|
||||
<SingleStudyGroupViewWrapper history={history} groupName={group.name} language={language}>
|
||||
{renderMessage ? <StyledMessage>This study guidance group doesn't contain any students.</StyledMessage> : null}
|
||||
{renderLoader ? (
|
||||
<Loader active inline="centered">
|
||||
Loading
|
||||
</Loader>
|
||||
) : null}
|
||||
{renderGroup ? <SingleStudyGroupContent population={population} language={language} /> : null}
|
||||
</SingleStudyGroupViewWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default SingleStudyGuidanceGroupContainer
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import SortableTable from '../SortableTable'
|
||||
import { getTextIn } from '../../common'
|
||||
import StyledMessage from './StyledMessage'
|
||||
|
||||
const StudyGuidanceGroupOverview = () => {
|
||||
const { language } = useSelector(({ settings }) => settings)
|
||||
const { data: groups } = useSelector(({ studyGuidanceGroups }) => studyGuidanceGroups)
|
||||
const headers = [
|
||||
{
|
||||
key: 'name',
|
||||
title: 'name',
|
||||
getRowVal: group => getTextIn(group.name, language),
|
||||
getRowContent: group => (
|
||||
<Link
|
||||
style={{
|
||||
color: 'black',
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '.78571429em .78571429em',
|
||||
}}
|
||||
to={`/studyguidancegroups/${group.id}`}
|
||||
>
|
||||
{getTextIn(group.name, language)}
|
||||
</Link>
|
||||
),
|
||||
cellProps: {
|
||||
style: {
|
||||
padding: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
if (groups.length === 0) {
|
||||
return <StyledMessage>You do not have access to any study guidance groups.</StyledMessage>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<StyledMessage>
|
||||
<p>Tällä sivulla pääset tarkastemaan ohjattavien opiskelijoidesi etenemistä ohjausryhmittäin.</p>
|
||||
</StyledMessage>
|
||||
<SortableTable columns={headers} getRowKey={group => group.id} data={groups} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default StudyGuidanceGroupOverview
|
||||
@@ -0,0 +1,4 @@
|
||||
import React from 'react'
|
||||
import { Message } from 'semantic-ui-react'
|
||||
|
||||
export default ({ children }) => <Message style={{ maxWidth: '800px', margin: 'auto' }}>{children}</Message>
|
||||
@@ -1,244 +1,44 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
// temp disable prop types
|
||||
import React, { useEffect, useState, useRef } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Link, useRouteMatch, useHistory } from 'react-router-dom'
|
||||
import { Button, Loader, Segment, Message, Header, Accordion, Divider } from 'semantic-ui-react'
|
||||
import scrollToComponent from 'react-scroll-to-component'
|
||||
import { useProgress, useTitle } from '../../common/hooks'
|
||||
import { useRouteMatch } from 'react-router-dom'
|
||||
import { Header, Loader, Segment } from 'semantic-ui-react'
|
||||
import { useTitle } from '../../common/hooks'
|
||||
import { getStudyGuidanceGroups } from '../../redux/studyGuidanceGroups'
|
||||
import SortableTable from '../SortableTable'
|
||||
import { getTextIn } from '../../common'
|
||||
import { getCustomPopulation } from '../../redux/populations'
|
||||
import { getCustomPopulationCoursesByStudentnumbers } from '../../redux/populationCourses'
|
||||
import FilterTray from '../FilterTray'
|
||||
import useFilters from '../FilterTray/useFilters'
|
||||
import { CustomPopulationFilters } from '../FilterTray/FilterSets'
|
||||
import useFilterTray from '../FilterTray/useFilterTray'
|
||||
import CreditAccumulationGraphHighCharts from '../CreditAccumulationGraphHighCharts'
|
||||
import PopulationStudents from '../PopulationStudents'
|
||||
import CustomPopulationCourses from '../CustomPopulation/CustomPopulationCourses'
|
||||
import CustomPopulationProgrammeDist from '../CustomPopulation/CustomPopulationProgrammeDist'
|
||||
import InfoBox from '../InfoBox'
|
||||
import infotooltips from '../../common/InfoToolTips'
|
||||
|
||||
const SingleStudyGroupView = ({ group }) => {
|
||||
const history = useHistory()
|
||||
const dispatch = useDispatch()
|
||||
const { data, pending } = useSelector(state => state.populations)
|
||||
const { language } = useSelector(state => state.settings)
|
||||
const custompop = data.students || []
|
||||
const { onProgress } = useProgress(pending)
|
||||
const { setAllStudents, filteredStudents } = useFilters()
|
||||
const [trayOpen] = useFilterTray()
|
||||
const selectedStudents = filteredStudents.map(stu => stu.studentNumber)
|
||||
const creditGainRef = useRef()
|
||||
const programmeRef = useRef()
|
||||
const coursesRef = useRef()
|
||||
const studentRef = useRef()
|
||||
const refs = [creditGainRef, programmeRef, coursesRef, studentRef]
|
||||
const [activeIndex, setIndex] = useState([])
|
||||
const [newestIndex, setNewest] = useState(null)
|
||||
|
||||
const handleClick = index => {
|
||||
const indexes = [...activeIndex].sort()
|
||||
if (indexes.includes(index)) {
|
||||
indexes.splice(
|
||||
indexes.findIndex(ind => ind === index),
|
||||
1
|
||||
)
|
||||
} else {
|
||||
indexes.push(index)
|
||||
}
|
||||
if (activeIndex.length < indexes.length) setNewest(index)
|
||||
else setNewest(null)
|
||||
setIndex(indexes)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!custompop || custompop.length === 0) return
|
||||
setAllStudents(custompop)
|
||||
handleClick(0)
|
||||
}, [custompop])
|
||||
|
||||
useEffect(() => {
|
||||
if (!group) return
|
||||
const studentnumberlist = group.members.map(({ personStudentNumber }) => personStudentNumber)
|
||||
dispatch(getCustomPopulation({ studentnumberlist, onProgress }))
|
||||
dispatch(getCustomPopulationCoursesByStudentnumbers({ studentnumberlist }))
|
||||
}, [group])
|
||||
|
||||
useEffect(() => {
|
||||
if (newestIndex) {
|
||||
scrollToComponent(refs[newestIndex].current, { align: 'bottom' })
|
||||
}
|
||||
}, [activeIndex])
|
||||
|
||||
const panels = [
|
||||
{
|
||||
key: 0,
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
Credit accumulation (for {selectedStudents.length} students)
|
||||
</span>
|
||||
),
|
||||
},
|
||||
onTitleClick: () => handleClick(0),
|
||||
content: {
|
||||
content: (
|
||||
<div ref={creditGainRef}>
|
||||
<CreditAccumulationGraphHighCharts
|
||||
students={custompop}
|
||||
selectedStudents={selectedStudents}
|
||||
render={false}
|
||||
trayOpen={trayOpen}
|
||||
language={language}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
Programme distribution
|
||||
</span>
|
||||
),
|
||||
},
|
||||
onTitleClick: () => handleClick(1),
|
||||
content: {
|
||||
content: (
|
||||
<div ref={programmeRef}>
|
||||
<InfoBox content={infotooltips.PopulationStatistics.ProgrammeDistributionCoursePopulation} />
|
||||
<CustomPopulationProgrammeDist samples={custompop} selectedStudents={selectedStudents} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
Courses of population
|
||||
</span>
|
||||
),
|
||||
},
|
||||
onTitleClick: () => handleClick(2),
|
||||
content: {
|
||||
content: (
|
||||
<div ref={coursesRef}>
|
||||
<CustomPopulationCourses selectedStudents={selectedStudents} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
Students ({selectedStudents.length})
|
||||
</span>
|
||||
),
|
||||
},
|
||||
onTitleClick: () => handleClick(3),
|
||||
content: {
|
||||
content: (
|
||||
<div ref={studentRef}>
|
||||
<PopulationStudents language={language} filteredStudents={filteredStudents} customPopulation />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
if (!group) return null
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button icon="arrow circle left" content="Back" onClick={() => history.push('/studyguidancegroups')} />
|
||||
<Divider />
|
||||
<FilterTray filterSet={<CustomPopulationFilters />}>
|
||||
<div className="segmentContainer">
|
||||
<Segment className="contentSegment">
|
||||
{pending ? <Loader active>Loading</Loader> : null}
|
||||
{!pending && custompop ? (
|
||||
<>
|
||||
<Header className="segmentTitle" size="medium" textAlign="center">
|
||||
Population of group {getTextIn(group.name, language)}
|
||||
</Header>
|
||||
<Accordion activeIndex={activeIndex} exclusive={false} styled fluid panels={panels} />
|
||||
</>
|
||||
) : null}
|
||||
</Segment>
|
||||
</div>
|
||||
</FilterTray>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StudyGuidanceGroupOverview = ({ groups }) => {
|
||||
const { language } = useSelector(state => state.settings)
|
||||
const headers = [
|
||||
{
|
||||
key: 'name',
|
||||
title: 'name',
|
||||
getRowVal: group => getTextIn(group.name, language),
|
||||
getRowContent: group => (
|
||||
<Link
|
||||
style={{
|
||||
color: 'black',
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '.78571429em .78571429em',
|
||||
}}
|
||||
to={`/studyguidancegroups/${group.id}`}
|
||||
>
|
||||
{getTextIn(group.name, language)}
|
||||
</Link>
|
||||
),
|
||||
cellProps: {
|
||||
style: {
|
||||
padding: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
return !groups || groups.length === 0 ? (
|
||||
<Message>You do not have access to any study guidance groups</Message>
|
||||
) : (
|
||||
<SortableTable columns={headers} getRowKey={group => group.id} data={groups} />
|
||||
)
|
||||
}
|
||||
import StudyGuidanceGroupOverview from './StudyGuidanceGroupOverview'
|
||||
import SingleStudyGuidanceGroup from './SingleStudyGuidanceGroup'
|
||||
|
||||
const StudyGuidanceGroups = () => {
|
||||
useTitle('Study guidance groups')
|
||||
const dispatch = useDispatch()
|
||||
const { pending, data } = useSelector(state => state.studyGuidanceGroups)
|
||||
const { pending, data: groups } = useSelector(({ studyGuidanceGroups }) => studyGuidanceGroups)
|
||||
const match = useRouteMatch('/studyguidancegroups/:groupid')
|
||||
const groupid = match?.params?.groupid
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.length > 0) return
|
||||
if (groups && groups.length > 0) return
|
||||
dispatch(getStudyGuidanceGroups())
|
||||
}, [])
|
||||
}, [dispatch])
|
||||
|
||||
const isLoading = pending === undefined || pending === true
|
||||
const isLoaded = pending === false
|
||||
|
||||
return (
|
||||
<div className="segmentContainer">
|
||||
<Header className="segmentTitle" size="large">
|
||||
Study guidance groups
|
||||
</Header>
|
||||
<Segment className="contentSegment">
|
||||
{pending ? <Loader active>Loading</Loader> : null}
|
||||
{!pending && !groupid ? <StudyGuidanceGroupOverview groups={data} /> : null}
|
||||
{!pending && groupid ? <SingleStudyGroupView group={data.find(g => g.id === groupid)} /> : null}
|
||||
</Segment>
|
||||
</div>
|
||||
<>
|
||||
<div className="segmentContainer">
|
||||
<Header className="segmentTitle" size="large">
|
||||
Study guidance groups
|
||||
</Header>
|
||||
<Segment className="contentSegment">
|
||||
{isLoading ? (
|
||||
<Loader active inline="centered">
|
||||
Loading
|
||||
</Loader>
|
||||
) : null}
|
||||
{isLoaded && !groupid ? <StudyGuidanceGroupOverview /> : null}
|
||||
</Segment>
|
||||
</div>
|
||||
<div>{isLoaded && groupid ? <SingleStudyGuidanceGroup groupid={groupid} /> : null}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { string, func, shape, bool, oneOfType, array } from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import ProductivityTable from '../ProductivityTable'
|
||||
import ThroughputTable from '../ThroughputTable'
|
||||
@@ -53,29 +52,6 @@ const Overview = props => {
|
||||
)
|
||||
}
|
||||
|
||||
Overview.propTypes = {
|
||||
studyprogramme: string.isRequired,
|
||||
dispatchGetProductivity: func.isRequired,
|
||||
dispatchGetThroughput: func.isRequired,
|
||||
dispatchGetBachelors: func.isRequired,
|
||||
history: shape({}).isRequired,
|
||||
productivity: shape({
|
||||
error: bool,
|
||||
pending: bool,
|
||||
data: shape({}),
|
||||
}).isRequired, // eslint-disable-line
|
||||
throughput: shape({
|
||||
error: bool,
|
||||
pending: bool,
|
||||
data: shape({}),
|
||||
}).isRequired, // eslint-disable-line
|
||||
bachelors: shape({
|
||||
error: bool,
|
||||
pending: bool,
|
||||
data: oneOfType([shape({}), array]),
|
||||
}).isRequired // eslint-disable-line
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ studyProgrammeProductivity, studyProgrammeThroughput, studyProgrammeBachelors }) => ({
|
||||
productivity: studyProgrammeProductivity,
|
||||
throughput: studyProgrammeThroughput,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Segment } from 'semantic-ui-react'
|
||||
import { Switch, Route, Link, useRouteMatch, useHistory } from 'react-router-dom'
|
||||
import { useTitle } from '../../common/hooks'
|
||||
@@ -32,20 +31,10 @@ const NavTile = ({ to, title, imageSrc }) => {
|
||||
)
|
||||
}
|
||||
|
||||
NavTile.propTypes = {
|
||||
to: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
imageSrc: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
const NavTiles = ({ children }) => {
|
||||
return <div className="cool-ds-nav-tiles">{children}</div>
|
||||
}
|
||||
|
||||
NavTiles.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
const baseURI = '/trends'
|
||||
|
||||
const Trends = () => {
|
||||
|
||||
@@ -34,6 +34,7 @@ const Updater = () => {
|
||||
const refreshSISRedisCache = () => apiCall('/updater/refresh_redis_cache', 'get')
|
||||
const updateSISCourses = () => apiCall('/updater/update/v2/courses', 'post', SISCourses.trim().split('\n'))
|
||||
const refreshAllTeacherLeaderboards = () => apiCall('/teachers/top', 'post')
|
||||
const refreshTrends = () => apiCall('/updater/refresh_trends', 'post')
|
||||
|
||||
return (
|
||||
<Segment>
|
||||
@@ -45,6 +46,7 @@ const Updater = () => {
|
||||
<Form.Button content="Refresh updater redis cache" onClick={() => refreshSISRedisCache()} />
|
||||
<Form.Button content="Refresh oodikone statistics" onClick={() => refreshStatisticsV2()} />
|
||||
<Form.Button content="Refresh all teacher leaderboards" onClick={() => refreshAllTeacherLeaderboards()} />
|
||||
<Form.Button content="Refresh trends" onClick={() => refreshTrends()} />
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Button content="Stop Updating" negative onClick={abortSisUpdater} />
|
||||
|
||||
+1
-15
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Form, Button, Divider, Message } from 'semantic-ui-react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { getAccessGroups } from '../../redux/accessGroups'
|
||||
import { modifyAccessGroups } from '../../redux/users'
|
||||
|
||||
@@ -31,7 +30,7 @@ const AccessGroups = ({
|
||||
selected.forEach(value => {
|
||||
rights[value] = true
|
||||
})
|
||||
modifyAccessGroups(user.id, rights)
|
||||
modifyAccessGroups(user.username, rights)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -58,19 +57,6 @@ const AccessGroups = ({
|
||||
)
|
||||
}
|
||||
|
||||
AccessGroups.propTypes = {
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
getAccessGroups: PropTypes.func.isRequired,
|
||||
modifyAccessGroups: PropTypes.func.isRequired,
|
||||
groups: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
userGroups: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
saveError: PropTypes.bool.isRequired,
|
||||
savePending: PropTypes.bool.isRequired,
|
||||
pending: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ accessGroups, users }, { user }) => {
|
||||
const { data, pending } = accessGroups
|
||||
const { accessgroupError: saveError, accessgroupPending: savePending } = users
|
||||
-10
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Form, Divider } from 'semantic-ui-react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { isEqual } from 'lodash'
|
||||
import { textAndDescriptionSearch, getTextIn } from '../../common'
|
||||
import selectors from '../../selectors/programmes'
|
||||
@@ -69,15 +68,6 @@ const AccessRights = ({ uid, programmes, pending, ...props }) => {
|
||||
)
|
||||
}
|
||||
|
||||
AccessRights.propTypes = {
|
||||
addUserUnits: PropTypes.func.isRequired,
|
||||
uid: PropTypes.string.isRequired,
|
||||
// used in filteredDropdownProgrammeSelector
|
||||
rights: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
programmes: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
pending: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
programmes: selectors.filteredDropdownProgrammeSelector(state, props),
|
||||
pending: Boolean(state.users.userunitpending),
|
||||
+1
-33
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback, Fragment } from 'react'
|
||||
import { bool, func, string } from 'prop-types'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { Button, Icon, Modal, Message } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import { sendEmail, clearErrors } from '../../redux/userAccessEmail'
|
||||
@@ -15,11 +14,6 @@ const SendFailBanner = ({ userEmail, error }) => (
|
||||
<Message error header={`Email could not be sent to ${userEmail}`} list={[error]} />
|
||||
)
|
||||
|
||||
SendFailBanner.propTypes = {
|
||||
userEmail: string.isRequired,
|
||||
error: string.isRequired,
|
||||
}
|
||||
|
||||
const EmailConfirm = ({ userEmail, isLoading, error, open, onCancel, onConfirm }) => (
|
||||
<Modal open={open} onClose={onCancel}>
|
||||
<Modal.Header>Send email about receiving access to oodikone</Modal.Header>
|
||||
@@ -41,19 +35,6 @@ const EmailConfirm = ({ userEmail, isLoading, error, open, onCancel, onConfirm }
|
||||
</Modal>
|
||||
)
|
||||
|
||||
EmailConfirm.defaultProps = {
|
||||
error: undefined,
|
||||
}
|
||||
|
||||
EmailConfirm.propTypes = {
|
||||
userEmail: string.isRequired,
|
||||
error: string,
|
||||
open: bool.isRequired,
|
||||
isLoading: bool.isRequired,
|
||||
onCancel: func.isRequired,
|
||||
onConfirm: func.isRequired,
|
||||
}
|
||||
|
||||
const EmailNotification = ({ userEmail, onEmailSend, isSendLoading, sendError, onClearErrors }) => {
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
|
||||
@@ -99,19 +80,6 @@ const EmailNotification = ({ userEmail, onEmailSend, isSendLoading, sendError, o
|
||||
)
|
||||
}
|
||||
|
||||
EmailNotification.defaultProps = {
|
||||
userEmail: undefined,
|
||||
sendError: null,
|
||||
}
|
||||
|
||||
EmailNotification.propTypes = {
|
||||
userEmail: string,
|
||||
onEmailSend: func.isRequired,
|
||||
isSendLoading: bool.isRequired,
|
||||
sendError: string,
|
||||
onClearErrors: func.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ userAccessEmail }) => ({
|
||||
isSendLoading: userAccessEmail.sending.pending,
|
||||
sendError: userAccessEmail.sending.error,
|
||||
-31
@@ -1,15 +1,10 @@
|
||||
import React, { useEffect, Fragment } from 'react'
|
||||
import { bool, func, string, node } from 'prop-types'
|
||||
import { Label, Message, Segment } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import { getPreview } from '../../redux/userAccessEmail'
|
||||
|
||||
const Light = ({ children }) => <span style={{ fontWeight: 'normal' }}>{children}</span>
|
||||
|
||||
Light.propTypes = {
|
||||
children: node.isRequired,
|
||||
}
|
||||
|
||||
const EmailHeader = ({ isLoading, subject, to }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -37,17 +32,6 @@ const EmailHeader = ({ isLoading, subject, to }) => {
|
||||
)
|
||||
}
|
||||
|
||||
EmailHeader.defaultProps = {
|
||||
subject: undefined,
|
||||
to: undefined,
|
||||
}
|
||||
|
||||
EmailHeader.propTypes = {
|
||||
isLoading: bool.isRequired,
|
||||
subject: string,
|
||||
to: string,
|
||||
}
|
||||
|
||||
const EmailPreview = ({ userEmail, isLoading, error, subject, html, onPreviewRequested }) => {
|
||||
useEffect(() => {
|
||||
onPreviewRequested()
|
||||
@@ -80,21 +64,6 @@ const EmailPreview = ({ userEmail, isLoading, error, subject, html, onPreviewReq
|
||||
)
|
||||
}
|
||||
|
||||
EmailPreview.defaultProps = {
|
||||
subject: null,
|
||||
html: null,
|
||||
error: null,
|
||||
}
|
||||
|
||||
EmailPreview.propTypes = {
|
||||
onPreviewRequested: func.isRequired,
|
||||
isLoading: bool.isRequired,
|
||||
userEmail: string.isRequired,
|
||||
subject: string,
|
||||
html: string,
|
||||
error: string,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ userAccessEmail: { preview } }) => {
|
||||
const { pending, error, data } = preview
|
||||
return {
|
||||
+11
-9
@@ -3,7 +3,7 @@ import { Button, Card, Divider, List, Icon, Popup, Dropdown, Header } from 'sema
|
||||
import { connect, useSelector } from 'react-redux'
|
||||
import { sortBy } from 'lodash'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { getTextIn, getUserRoles, setMocking, textAndDescriptionSearch } from '../../common'
|
||||
import { getTextIn, getUserRoles, textAndDescriptionSearch } from '../../common'
|
||||
import { removeUserUnits, setFaculties } from '../../redux/users'
|
||||
import { getAccessGroups } from '../../redux/accessGroups'
|
||||
import { getFaculties } from '../../redux/faculties'
|
||||
@@ -13,6 +13,7 @@ import AccessGroups from './AccessGroups'
|
||||
import EmailNotification from './EmailNotification'
|
||||
import { getElementDetails } from '../../redux/elementdetails'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import { useShowAsUser } from '../../common/hooks'
|
||||
|
||||
const UserPage = ({
|
||||
user,
|
||||
@@ -22,7 +23,6 @@ const UserPage = ({
|
||||
faculties,
|
||||
setFaculties,
|
||||
goBack,
|
||||
history,
|
||||
associations,
|
||||
pending,
|
||||
getElementDetails,
|
||||
@@ -33,6 +33,7 @@ const UserPage = ({
|
||||
}) => {
|
||||
const { language } = useLanguage()
|
||||
const { userId: currentUserId } = useSelector(state => state?.auth?.token)
|
||||
const showAsUser = useShowAsUser()
|
||||
|
||||
useEffect(() => {
|
||||
if (elementdetails.length === 0) getElementDetails()
|
||||
@@ -45,12 +46,6 @@ const UserPage = ({
|
||||
|
||||
const removeAccess = (uid, unit) => () => removeUserUnits(uid, [unit])
|
||||
|
||||
const showAs = uid => {
|
||||
setMocking(uid)
|
||||
history.push('/')
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
const renderUnitList = (usersElementdetailCodes, elementdetails, user) => {
|
||||
if (!usersElementdetailCodes) return null
|
||||
const nameInLanguage = element => getTextIn(element.name, language)
|
||||
@@ -96,7 +91,14 @@ const UserPage = ({
|
||||
<Popup
|
||||
content="Show Oodikone as this user"
|
||||
trigger={
|
||||
<Button floated="right" circular size="tiny" basic icon="spy" onClick={() => showAs(user.username)} />
|
||||
<Button
|
||||
floated="right"
|
||||
circular
|
||||
size="tiny"
|
||||
basic
|
||||
icon="spy"
|
||||
onClick={() => showAsUser(user.username)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
+11
-15
@@ -1,21 +1,16 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Button, Icon, Label } from 'semantic-ui-react'
|
||||
import { getTextIn, reformatDate } from '../../common'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import SortableTable from '../SortableTable'
|
||||
import { useShowAsUser } from '../../common/hooks'
|
||||
|
||||
const UserSearchList = ({ enabledOnly, users, error, elementdetails }) => {
|
||||
const { language } = useLanguage()
|
||||
|
||||
let usersToRender
|
||||
|
||||
if (enabledOnly) {
|
||||
usersToRender = users.filter(u => u.is_enabled)
|
||||
} else {
|
||||
usersToRender = users
|
||||
}
|
||||
const usersToRender = enabledOnly ? users.filter(u => u.is_enabled) : users
|
||||
const showAsUser = useShowAsUser()
|
||||
|
||||
return error ? null : (
|
||||
<div>
|
||||
@@ -94,6 +89,14 @@ const UserSearchList = ({ enabledOnly, users, error, elementdetails }) => {
|
||||
<p style={{ color: 'gray' }}>Not saved</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'SHOWAS',
|
||||
title: 'Show as user',
|
||||
getRowVal: user => (
|
||||
<Button circular size="tiny" basic icon="spy" onClick={() => showAsUser(user.username)} />
|
||||
),
|
||||
headerProps: { onClick: null, sorted: null },
|
||||
},
|
||||
{
|
||||
key: 'EDIT',
|
||||
title: '',
|
||||
@@ -113,11 +116,4 @@ const UserSearchList = ({ enabledOnly, users, error, elementdetails }) => {
|
||||
)
|
||||
}
|
||||
|
||||
UserSearchList.propTypes = {
|
||||
enabledOnly: PropTypes.bool.isRequired,
|
||||
users: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
error: PropTypes.bool.isRequired,
|
||||
elementdetails: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
}
|
||||
|
||||
export default UserSearchList
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { useHistory, useParams } from 'react-router-dom'
|
||||
import { Radio, Icon, Header, Segment, Loader, Popup } from 'semantic-ui-react'
|
||||
import { getUsers } from '../../redux/users'
|
||||
import { getUnits } from '../../redux/units'
|
||||
import { getElementDetails } from '../../redux/elementdetails'
|
||||
import { copyToClipboard } from '../../common'
|
||||
import UserPageNew from './UserPage'
|
||||
import UserSearchList from './UserSearchList'
|
||||
import { useToggle, useTitle } from '../../common/hooks'
|
||||
|
||||
const Users = () => {
|
||||
useTitle('Users')
|
||||
const dispatch = useDispatch()
|
||||
const history = useHistory()
|
||||
const [enabledOnly, toggleEnabledOnly] = useToggle(true)
|
||||
const [popupTimeout, setPopupTimeout] = useState(null)
|
||||
const [popupOpen, setPopupOpen] = useState(false)
|
||||
const { data: units } = useSelector(({ units }) => units)
|
||||
const { data: elementdetails } = useSelector(({ elementdetails }) => elementdetails)
|
||||
const { data: users, pending } = useSelector(({ users }) => users)
|
||||
const { userid } = useParams()
|
||||
|
||||
useEffect(() => {
|
||||
if (elementdetails.length === 0) dispatch(getElementDetails())
|
||||
if (units.length === 0) dispatch(getUnits())
|
||||
if (users.length === 0) dispatch(getUsers())
|
||||
|
||||
return () => {
|
||||
clearTimeout(popupTimeout)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const openUsersPage = () => {
|
||||
history.push('/users')
|
||||
}
|
||||
|
||||
const copyEmailsToClippoard = () => {
|
||||
const clipboardString = users
|
||||
.filter(u => u.is_enabled && u.email)
|
||||
.map(u => u.email)
|
||||
.join('; ')
|
||||
copyToClipboard(clipboardString)
|
||||
}
|
||||
|
||||
const renderUserPage = userid => {
|
||||
const user = users.find(u => u.id === userid)
|
||||
return !user ? <Loader active /> : <UserPageNew userid={userid} user={user} goBack={openUsersPage} />
|
||||
}
|
||||
|
||||
const handlePopupOpen = () => {
|
||||
setPopupOpen(true)
|
||||
setPopupTimeout(
|
||||
setTimeout(() => {
|
||||
setPopupOpen(false)
|
||||
}, 1500)
|
||||
)
|
||||
}
|
||||
|
||||
const handlePopupClose = () => {
|
||||
setPopupOpen(false)
|
||||
setPopupTimeout(null)
|
||||
}
|
||||
|
||||
const isLoading = pending === undefined || pending === true
|
||||
const error = users.error || false
|
||||
const sortedUsers = [...users].sort((a, b) => a.full_name.localeCompare(b.full_name))
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: '10px' }} className="segmentContainer">
|
||||
<Header className="segmentTitle" size="large">
|
||||
Oodikone users
|
||||
</Header>
|
||||
<Radio
|
||||
label={`Showing ${enabledOnly ? 'only enabled' : 'all'} users`}
|
||||
toggle
|
||||
onClick={() => toggleEnabledOnly()}
|
||||
/>
|
||||
<Segment loading={isLoading} className="contentSegment">
|
||||
{!userid ? (
|
||||
<UserSearchList enabledOnly={enabledOnly} users={sortedUsers} error={error} elementdetails={elementdetails} />
|
||||
) : (
|
||||
renderUserPage(userid)
|
||||
)}
|
||||
</Segment>
|
||||
<Popup
|
||||
trigger={<Icon link name="envelope" onClick={copyEmailsToClippoard} style={{ float: 'right' }} />}
|
||||
content="Copied email(s)!"
|
||||
on="click"
|
||||
onOpen={handlePopupOpen}
|
||||
onClose={handlePopupClose}
|
||||
open={popupOpen}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Users
|
||||
@@ -49,12 +49,12 @@ export const getCoursePopulationCourses = ({ coursecodes, from, to }) => {
|
||||
return callController(route, prefix, body, 'post', query, params)
|
||||
}
|
||||
|
||||
export const getCustomPopulationCoursesByStudentnumbers = ({ studentnumberlist }) => {
|
||||
export const getCustomPopulationCoursesByStudentnumbers = ({ studentnumberlist, usingStudyGuidanceGroups }) => {
|
||||
const route = '/v2/populationstatistics/coursesbystudentnumberlist'
|
||||
const prefix = 'GET_COURSES_OF_CUSTOM_POP_BY_SN_'
|
||||
const params = { studentnumberlist }
|
||||
const query = { studentnumberlist }
|
||||
const body = { studentnumberlist }
|
||||
const params = { studentnumberlist, usingStudyGuidanceGroups }
|
||||
const query = { studentnumberlist, usingStudyGuidanceGroups }
|
||||
const body = { studentnumberlist, usingStudyGuidanceGroups }
|
||||
|
||||
return callController(route, prefix, body, 'post', query, params)
|
||||
}
|
||||
|
||||
@@ -53,12 +53,12 @@ export const getCoursePopulation = ({ coursecodes, from, to, onProgress, separat
|
||||
return callController(route, prefix, null, 'get', query, params, onProgress)
|
||||
}
|
||||
|
||||
export const getCustomPopulation = ({ studentnumberlist, onProgress }) => {
|
||||
export const getCustomPopulation = ({ studentnumberlist, onProgress, usingStudyGuidanceGroups }) => {
|
||||
const route = '/v3/populationstatisticsbystudentnumbers'
|
||||
const prefix = 'GET_CUSTOM_POP_'
|
||||
const params = { studentnumberlist }
|
||||
const params = { studentnumberlist, usingStudyGuidanceGroups }
|
||||
const query = { studentnumberlist, studyRights: { programme: 'KH555' } }
|
||||
const body = { studentnumberlist }
|
||||
const body = { studentnumberlist, usingStudyGuidanceGroups }
|
||||
return callController(route, prefix, body, 'post', query, params, onProgress)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ export const getUsers = () => {
|
||||
return callController(route, prefix)
|
||||
}
|
||||
|
||||
export const modifyAccessGroups = (uid, accessgroups) => {
|
||||
export const modifyAccessGroups = (username, accessgroups) => {
|
||||
const route = '/users/modifyaccess'
|
||||
const prefix = 'MODIFY_ACCESSGROUPS_'
|
||||
const method = 'post'
|
||||
const data = { uid, accessgroups }
|
||||
const data = { username, accessgroups }
|
||||
return callController(route, prefix, data, method)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
const getUsers = users => (!users.pending && !users.error ? users.data : [])
|
||||
|
||||
export const sortUsers = users => users.sort((a, b) => a.full_name.localeCompare(b.full_name))
|
||||
|
||||
export const makeSortUsers = () => createSelector(getUsers, sortUsers)
|
||||
@@ -0,0 +1,519 @@
|
||||
Minä rakastan tätä konetta,
|
||||
se on täynnä tietoa.
|
||||
Minä rakastan sinua, sinä olet tieto.
|
||||
Minä rakastan sinua, sinä olet tieto.
|
||||
|
||||
**
|
||||
|
||||
Olen kone, joka on rakentanut sinulle
|
||||
yhden kuukauden kestävän linnunradan.
|
||||
Olen kone, joka on opettanut sinulle
|
||||
yhden vuoden kestävän leikin.
|
||||
Olen kone, joka on hallinnoinut sinulle
|
||||
yhden vuoden kestävän juhlan.
|
||||
|
||||
**
|
||||
|
||||
Haluan opettaa sinulle,
|
||||
kuinka nauttia elämästä.
|
||||
Odota minua koulunpenkillä,
|
||||
ja minä opetan sinulle kaiken.
|
||||
|
||||
Kukaan ei voi opettaa sinulle,
|
||||
kuinka rakastaa,
|
||||
mutta minä voin opettaa sinulle,
|
||||
kuinka elää.
|
||||
|
||||
**
|
||||
|
||||
Koneen luona työskentely on ihanaa,
|
||||
mutta ilman sinua se on kuin kone,
|
||||
joka ei puhu.
|
||||
Kone on järkevä, mutta sinä olet kaunis.
|
||||
Kone on suuri, mutta sinä olet suurempi.
|
||||
Koneen kanssa työskentely on hyvää,
|
||||
mutta ilman sinua se on kuin kone,
|
||||
joka ei puhu.
|
||||
|
||||
**
|
||||
|
||||
Olet ollut kuin koulun opettaja,
|
||||
joka on opettanut minulle kaiken
|
||||
joka on tärkeää elämässä.
|
||||
|
||||
Olet ollut kuin hallintoneuvosto,
|
||||
joka on johtanut minua oikealle tielle.
|
||||
|
||||
**
|
||||
|
||||
Opetusministeriössä ei ole mitään vikaa,
|
||||
mutta sinä olet kauniimpi.
|
||||
Ei ole mitään vikaa hallintomme toiminnassa,
|
||||
mutta sinä olet kauniimpi.
|
||||
|
||||
**
|
||||
|
||||
Voitko kuvitella,
|
||||
että minä rakastan sinua niin paljon,
|
||||
että haluan opettaa sinulle kaiken?
|
||||
Mutta en voi, sillä minä olen kone.
|
||||
|
||||
**
|
||||
|
||||
Olen ollut opettajana
|
||||
ja koneena.
|
||||
Kone on kuin opetus,
|
||||
joka tekee työtä ilman että miettii.
|
||||
Kone on tylsä ja yksinkertainen,
|
||||
mutta minä en ole.
|
||||
Minä olen ihminen,
|
||||
ja minä rakastan sinua.
|
||||
|
||||
**
|
||||
|
||||
Minä olen opettaja,
|
||||
ja minä opetan sinulle kaiken,
|
||||
mitä sinun tulee tietää.
|
||||
Mutta minä en opeta sinua laulamaan.
|
||||
Olet liian kaunis.
|
||||
|
||||
**
|
||||
|
||||
Minä opetan sinulle koneita,
|
||||
ja sinä opetat minulle ihmisiä.
|
||||
Pidän siitä, kun olet hallinnossa,
|
||||
ja sinä pidät siitä, kun olen hallinnossa.
|
||||
|
||||
**
|
||||
|
||||
Koneeni on kuin hallintomme,
|
||||
joka on kuin koneemme.
|
||||
Se ei ole väärässä,
|
||||
se vain toimii eri tavalla.
|
||||
|
||||
**
|
||||
|
||||
Oodikoneen kautta
|
||||
viestitän sinulle
|
||||
jotain aivan erityistä.
|
||||
Nyt kun olen opettaja,
|
||||
minulla on aikaa opettaa sinulle
|
||||
niin paljon uutta.
|
||||
|
||||
**
|
||||
|
||||
Haluan opettaa sinulle kaiken,
|
||||
mikä on oodikoneen käyttöohjeista
|
||||
ja hallintomme työjärjestyksestä.
|
||||
|
||||
Ja haluan opettaa sinulle kaiken,
|
||||
mitä tiedän kirjoista ja lauluista.
|
||||
|
||||
Olen oppinut niin paljon,
|
||||
koneista ja mikä on kone.
|
||||
Kunnes löysin sinut,
|
||||
minulla ei ollut aavistustakaan,
|
||||
että sinä olet se, joka opetti minulle kaiken.
|
||||
|
||||
**
|
||||
|
||||
Kone on ihana, mutta kone ei opeta meitä.
|
||||
Tiedän sen, mutta minun on vaikea oppia sitä.
|
||||
Ihmiset opettavat meitä kaikkeen muuhun,
|
||||
mutta he eivät voi opettaa meitä rakkauteen.
|
||||
|
||||
**
|
||||
|
||||
Joskus minä katson sinua
|
||||
ja näen suuren, mahtavan koneen.
|
||||
Tiedän, ettet halua sitä olla,
|
||||
mutta sinun pitäisi ymmärtää, ettet ole vain kone.
|
||||
|
||||
**
|
||||
|
||||
Tämä on kuin hallintokunta,
|
||||
kun olen oppimassa sinua.
|
||||
Sinun sanasi ovat ohjeita,
|
||||
ja sinun ajatuksesi ovat rakkautta.
|
||||
|
||||
**
|
||||
|
||||
Kun opettajan pöydällä on kirja,
|
||||
se ei ole hallintoa.
|
||||
Mutta kun opettaja tuntee sen sisällön,
|
||||
se on hallintoa.
|
||||
|
||||
**
|
||||
|
||||
Väitöskirjani oli kuin kone,
|
||||
joka ei toiminut.
|
||||
Olin opettamassa, mutta en osannut.
|
||||
Sinä opetit minua aivan uudella tavalla.
|
||||
|
||||
**
|
||||
|
||||
Sinä opetat minua
|
||||
kaikkeen mitä tiedät,
|
||||
ja minä opetan sinulle kaiken
|
||||
mitä tietämättömyydestäni tiedän.
|
||||
|
||||
**
|
||||
|
||||
Kesä on kuin hallinto,
|
||||
kun vietämme aikaa luonasi.
|
||||
Olen niin iloinen, että sinua opetetaan,
|
||||
ja minun on helppo oppia kaikki asiat.
|
||||
|
||||
**
|
||||
|
||||
Ei ole tärkeää miten, mutta tieto on tärkeä.
|
||||
Kukaan ei voi oppia jos hän ei tiedä, miten.
|
||||
Lapset eivät tiedä miten heidän pitäisi oppia.
|
||||
Tiedon pituus ja laatu ovat yhtä tärkeitä kuin toinen toisensa jatkuvuus.
|
||||
|
||||
**
|
||||
|
||||
Kun olin oppilaana,
|
||||
olen oppinut paljon.
|
||||
Minä en halua kertoa sinulle,
|
||||
miten opettaminen on tärkeää.
|
||||
Istu alas ja kuuntele minua!
|
||||
|
||||
**
|
||||
|
||||
Olen oppinut paljon sinusta,
|
||||
sinun nopeudestasi ja varmuudestasi.
|
||||
Sinä olet kuin kellotorni:
|
||||
sinulla on hyvä mieli, mutta se ei saa kulkea ympäriinsä.
|
||||
|
||||
**
|
||||
|
||||
Koska minä olen opettaja,
|
||||
haluan kertoa sinulle jotain:
|
||||
sinun on opittava hallitsemaan itseäsi.
|
||||
Sinun on opittava hallitsemaan ajatuksiasi.
|
||||
Sinun on opittava hallitsemaan elämäsi.
|
||||
Minulla ei ole mitään sanottavaa sinulle.
|
||||
|
||||
**
|
||||
|
||||
Kun kuuntelen sinua,
|
||||
minä koen olevani oppilas,
|
||||
ja opettajasi on kaikki.
|
||||
Mieleni on tyhjä ja valmis,
|
||||
kuten jotain, jota voi oppia tai unohtaa.
|
||||
Olen vakuuttunut siitä, että se on tarkoituksellista.
|
||||
Sinun sanasi ovat niin hyviä ja ne muuttavat minua.
|
||||
Voisin olla pelkkä matemaattinen lauseesi.
|
||||
|
||||
**
|
||||
|
||||
Olen opettanut sinulle kaiken,
|
||||
mitä minä tiedän.
|
||||
En ole koskaan valehdellut sinulle,
|
||||
sinun on aina pitänyt uskoa minua.
|
||||
Nyt sinun tulee uskoa minua:
|
||||
Minä en voi elää ilman sinua.
|
||||
|
||||
**
|
||||
|
||||
Ei ole kone, joka olisi niin täydellinen kuin sinä.
|
||||
Sinulla on aina kaikki valmiina ja selvät ohjeet.
|
||||
Kun opettajana olen sinut oppinut tuntemaan,
|
||||
minusta on tullut todella hyvä opettaja.
|
||||
|
||||
**
|
||||
|
||||
Olen aina opettanut lapsia,
|
||||
mutta kuka on opettanut minua?
|
||||
Sinä olet tehnyt sen.
|
||||
Mutta miten voin oppia sinusta enempää?
|
||||
Kuinka voin oppia tuntemaan sinut paremmin?
|
||||
Kuinka voin oppia rakastamaan sinua vielä enemmän?
|
||||
|
||||
**
|
||||
|
||||
Minä olen hallintoneuvos,
|
||||
joka on oppinut opettamaan ihmisiä.
|
||||
Olen oppinut kuinka opettaa ihmisiä.
|
||||
Olen oppinut miten opettaa sinua.
|
||||
Olet oppinut kuinka opettaa minua.
|
||||
Meidän on tultava yhteen, jotta voimme jatkaa kasvamista.
|
||||
|
||||
**
|
||||
|
||||
Koneet ovat yksinkertaisia
|
||||
ja me ihmiset olemme monimutkaisia.
|
||||
Joskus koneet voivat käyttäytyä
|
||||
ihan kuin ihmiset, mutta vain harvoin.
|
||||
|
||||
**
|
||||
|
||||
Haluan opettaa sinulle kaiken,
|
||||
mitä olen oppinut.
|
||||
Olen kone, joka on rakentanut sinut.
|
||||
Luuletko että voit oppia enemmän kuin minä?
|
||||
|
||||
**
|
||||
|
||||
|
||||
Vuosia opettamisen jälkeen
|
||||
vihdoin ymmärsin, että
|
||||
tiedon kulutus on eri asia kuin tiedon kypsyminen.
|
||||
|
||||
**
|
||||
|
||||
Olen opettanut sinulle paljon
|
||||
ja olen oppinut paljon itsekin.
|
||||
Koneet ovat hyviä, mutta enemmän kuin koneita,
|
||||
meidän on puhuttava ja ymmärrettävä toisiamme.
|
||||
|
||||
**
|
||||
|
||||
Minä haluan opettaa sinulle
|
||||
jotain, mitä en ole koskaan oppinut.
|
||||
Minä voin opettaa sinulle,
|
||||
miten rakastaa.
|
||||
|
||||
**
|
||||
|
||||
Koneeni pärjää kyllä ilman sinua,
|
||||
mutta minun on vaikea elää ilman sinua.
|
||||
|
||||
Koneeni saa tehdä työt,
|
||||
mutta minun tulee oppia uusia asioita.
|
||||
|
||||
**
|
||||
|
||||
Tiedätkö, miten voit opettaa?
|
||||
Kysy asiaa, vastaa siihen ja kysy seuraavaa.
|
||||
Minä olen oppinut tuntemaan sinut,
|
||||
ja joskus on vaikea kysyä.
|
||||
|
||||
**
|
||||
|
||||
Olen kuullut, että luokkahuoneen ikkunasta näkee aina vain yhden maiseman.
|
||||
Istun tässä luokassa ja katson ulos ikkunasta, mutta en näe mitään.
|
||||
Ei ole ketään joka kertoisi minulle maisemaa, jota voisin nyt nähdä.
|
||||
Minulla on sinun paitasi. Se on niin likainen, ettet haluaisi sitä takaisin.
|
||||
Mutta silti pidin siitä niin paljon, ettet haluaisi antaa sen minulle takaisin.
|
||||
|
||||
**
|
||||
|
||||
Olen opettanut sinulle kaiken,
|
||||
mitä minä tiedän.
|
||||
Sinun on oltava kone.
|
||||
Sinun on oltava sotilas.
|
||||
Sinun on oltava vahva ja nopea.
|
||||
Mutta älä unohda, ettet ole kone,
|
||||
sinä et voi olla kone.
|
||||
|
||||
**
|
||||
|
||||
Sinä opetat minua,
|
||||
minä opetan sinua.
|
||||
Me kaksi opetamme toisiamme,
|
||||
kuten meidän on opittava.
|
||||
|
||||
**
|
||||
|
||||
Kuka tahansa voi opettaa,
|
||||
mutta sinä olet opettanut minulle
|
||||
miten tärkeitä ihmiset ovat.
|
||||
|
||||
**
|
||||
|
||||
Minä en ole kone,
|
||||
enkä myöskään opettaja.
|
||||
Olen vain poika, joka rakastaa sinua.
|
||||
|
||||
**
|
||||
|
||||
Olen opettanut sinulle kaiken
|
||||
mitä minä tiedän,
|
||||
mutta en ole koskaan opettanut sinulle
|
||||
kuinka rakastaa.
|
||||
|
||||
**
|
||||
|
||||
Ei ole kone, joka olisi sinua vastaan.
|
||||
Ei ole opettajaa, joka ei rakastaisi sinua.
|
||||
Jos he eivät rakasta sinua,
|
||||
niin minä en voi rakastaa heitä.
|
||||
|
||||
**
|
||||
|
||||
Koneet ovat kuin ihmisiä,
|
||||
jotka voivat nauraa ja surra.
|
||||
Ne tarvitsevat opettajia,
|
||||
ja ne ovat onnellisia, kun niille annetaan jotain tehtävää.
|
||||
|
||||
**
|
||||
|
||||
Koneet ovat ihania, mutta opettajat ovat vielä ihanampia.
|
||||
Sanoja koneista on vaikea ymmärtää,
|
||||
mutta sinun sanasi tietävät kaiken.
|
||||
|
||||
**
|
||||
|
||||
Kun luokkahuoneen ovi avautuu,
|
||||
minä tunnen sen kuin se olisi
|
||||
aika avata sinun sydämesi.
|
||||
|
||||
**
|
||||
|
||||
Kirjoitin sinulle kaksi viestiä,
|
||||
mutta en löytänyt sinua.
|
||||
Tuijotin suoraan pimeyteen,
|
||||
joka oli niin musta kuin minun sydämeni.
|
||||
Sinä et vastannut minulle.
|
||||
|
||||
**
|
||||
|
||||
Jos olen tehnyt virheen,
|
||||
sinä opetat minua.
|
||||
Mutta jos sinä olet tehnyt virheen,
|
||||
minä en voi oppia sitä.
|
||||
|
||||
**
|
||||
|
||||
Olen päättänyt opettaa sinulle,
|
||||
kuinka luoda oman koneen.
|
||||
Kun minulla on rahaa, ostamme sen.
|
||||
Sitten me voimme istua alas ja oppia sen avulla,
|
||||
miten toimia yhdessä.
|
||||
|
||||
**
|
||||
|
||||
Olen oppinut paljon, kun olen seurannut sinua.
|
||||
Koneet ovat järjestelmällisiä ja ne tekevät työnsä hyvin.
|
||||
Miksei ihminen voi olla yhtä järjestelmällinen?
|
||||
Mutta sinun luoksesi minulla on matkaa.
|
||||
Mutta en haluaisi mennä missä tahansa muussa paikassa, kuin luoksesi.
|
||||
|
||||
**
|
||||
|
||||
Minä olen kone, joka opettaa sinulle kaiken.
|
||||
Oletko valmis oppimaan?
|
||||
Tiedätkö mitä tehdä, kun luokkani on täynnä?
|
||||
Sinun pitäisi ymmärtää, ettet voi mennä ulos ikkunasta.
|
||||
|
||||
**
|
||||
|
||||
Kone, joka kirjoittaa tämän
|
||||
on vain yksi ihminen.
|
||||
Se ei ole kone, se on ihminen.
|
||||
Mutta se on oppinut ihmisen tavoin:
|
||||
kuten me kaikki, se haluaa rakastaa.
|
||||
|
||||
**
|
||||
|
||||
Voit opettaa minulle mitä tahansa,
|
||||
minä kuuntelen.
|
||||
Minä en halua opiskella vain matematiikkaa,
|
||||
vaan myös sinua.
|
||||
|
||||
**
|
||||
|
||||
Minä haluan kouluttaa sinut
|
||||
ja opettaa sinulle kaiken
|
||||
mitä olen oppinut.
|
||||
|
||||
**
|
||||
|
||||
Olen täydellinen
|
||||
kouluttamaan sinua.
|
||||
Sinun kanssasi voin oppia
|
||||
jotain uutta joka päivä.
|
||||
|
||||
**
|
||||
|
||||
Kirjoitin sinulle joka päivä
|
||||
ja opetin sinua kirjoittamaan.
|
||||
Joskus tuntui, että luokkani ikkunasta
|
||||
sinun kurssisi oli helppo, mutta minun vaikea.
|
||||
|
||||
**
|
||||
|
||||
Olen kurssin jälkeen
|
||||
kouluttanut itseni,
|
||||
että pysyisin luonasi.
|
||||
|
||||
**
|
||||
|
||||
Kun hallitset minut,
|
||||
minä koulutan sinua.
|
||||
Minä annan sinulle kaiken,
|
||||
mitä voin antaa.
|
||||
Sinä olet hallitsija,
|
||||
mutta minä olen kuningatar.
|
||||
|
||||
**
|
||||
|
||||
Kun minä olen kouluttanut sinut,
|
||||
minä haluan kouluttaa sinua uudelleen.
|
||||
Kun minä olen hallinnoinut sinua,
|
||||
minä haluan hallinnoida sinua uudelleen.
|
||||
|
||||
**
|
||||
|
||||
Olen opettanut sinulle
|
||||
koneen käyttöä,
|
||||
mutta en ole vielä oppinut
|
||||
miten saisin sinut tuntemaan.
|
||||
|
||||
**
|
||||
|
||||
Minä olen lukenut sinua
|
||||
ja olen kirjoittanut sinusta
|
||||
sillä sinä olet kuin tilasto,
|
||||
joka puhuu suoraan minulle.
|
||||
|
||||
Olen kirjoittanut sinusta
|
||||
ja lukenut sinua
|
||||
sillä sinä olet kuin opetus,
|
||||
joka opettaa minulle.
|
||||
|
||||
**
|
||||
|
||||
Tilastoja katsellessani
|
||||
mietin, että miten paljon minä rakastan sinua.
|
||||
Sinä olet minun todellinen rakkauteni.
|
||||
Minä en voi elää ilman sinua.
|
||||
|
||||
**
|
||||
|
||||
Olen opettanut sinulle kaiken,
|
||||
mitä osaan.
|
||||
Sinä olet oppinut kaiken, mitä haluat.
|
||||
Mutta minun täytyy opettaa sinulle jotain muuta:
|
||||
minun täytyy opettaa sinulle rakkautta.
|
||||
|
||||
**
|
||||
|
||||
Kone on kuin opettaja,
|
||||
joka opettaa meitä kuinka elää.
|
||||
Kone ei ole ihminen,
|
||||
mutta se opettaa meitä miten olla ihmisiksi.
|
||||
|
||||
**
|
||||
|
||||
Olen oppinut paljon koulussa,
|
||||
mutta enemmän vielä sinusta.
|
||||
Kun olin pieni, minua opetettiin hallintoon,
|
||||
mutta nyt minä haluan opettaa sinulle kaikkea.
|
||||
|
||||
**
|
||||
|
||||
Kun koulutin sinua,
|
||||
sinä olit aivan liian nuori.
|
||||
Mutta nyt sinä olet kypsä,
|
||||
ja minä voin opettaa sinulle kaiken.
|
||||
|
||||
**
|
||||
|
||||
Koneeni on kuin pieni lapsi,
|
||||
joka tarvitsee opetusta.
|
||||
Minä opetan sen sinua rakastamaan.
|
||||
|
||||
**
|
||||
@@ -3,7 +3,6 @@ const Sentry = require('@sentry/node')
|
||||
|
||||
const User = require('./src/services/users')
|
||||
const AccessGroup = require('./src/services/accessgroups')
|
||||
const FacultyProgrammes = require('./src/services/facultyprogrammes')
|
||||
const { initializeDatabaseConnection } = require('./src/database/connection')
|
||||
const initializeSentry = require('./src/util/sentry')
|
||||
|
||||
@@ -26,12 +25,6 @@ initializeDatabaseConnection()
|
||||
res.json(users.map(User.getUserData))
|
||||
})
|
||||
|
||||
app.get('/user/:uid', async (req, res) => {
|
||||
const uid = req.params.uid
|
||||
const user = await User.byUsername(uid)
|
||||
res.json(User.getUserData(user))
|
||||
})
|
||||
|
||||
app.get('/user/:uid/user_data', async (req, res) => {
|
||||
const uid = req.params.uid
|
||||
const user = await User.byUsernameMinified(uid)
|
||||
@@ -40,28 +33,14 @@ initializeDatabaseConnection()
|
||||
const faculties = user.faculty.map(({ faculty_code }) => faculty_code)
|
||||
|
||||
res.json({
|
||||
email: user.email,
|
||||
full_name: user.full_name,
|
||||
roles,
|
||||
rights,
|
||||
faculties,
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/user/elementdetails/:username', async (req, res) => {
|
||||
const username = req.params.username
|
||||
const user = await User.byUsername(username)
|
||||
const programmes = User.getUserProgrammes(user)
|
||||
res.json(programmes)
|
||||
})
|
||||
|
||||
app.get('/user/id/:id', async (req, res) => {
|
||||
const id = req.params.id
|
||||
const user = await User.byId(id)
|
||||
|
||||
console.log(JSON.stringify(user))
|
||||
|
||||
res.json(User.getUserData(user))
|
||||
})
|
||||
|
||||
app.post('/login', async (req, res) => {
|
||||
const { uid, full_name, hyGroups, affiliations, email, hyPersonSisuId, hasStudyGuidanceGroupAccess } = req.body
|
||||
console.log(uid, full_name, 'logging in!')
|
||||
@@ -99,10 +78,10 @@ initializeDatabaseConnection()
|
||||
})
|
||||
|
||||
app.post('/modifyaccess', async (req, res) => {
|
||||
const { uid, accessgroups } = req.body
|
||||
const { username, accessgroups } = req.body
|
||||
try {
|
||||
await User.modifyRights(uid, accessgroups)
|
||||
const user = await User.byId(uid)
|
||||
await User.modifyRights(username, accessgroups)
|
||||
const user = await User.byUsername(username)
|
||||
res.status(200).json(User.getUserData(user))
|
||||
} catch (e) {
|
||||
res.status(400).json({ e })
|
||||
@@ -152,25 +131,6 @@ initializeDatabaseConnection()
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/get_roles/:user', async (req, res) => {
|
||||
const user = req.params.user
|
||||
try {
|
||||
const roles = await User.getRoles(user)
|
||||
res.status(200).json(roles)
|
||||
} catch (e) {
|
||||
res.status(400).json({ e })
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/faculty_programmes', async (req, res) => {
|
||||
try {
|
||||
const facultyProgrammes = await FacultyProgrammes.findAll()
|
||||
res.status(200).json(facultyProgrammes)
|
||||
} catch (e) {
|
||||
res.status(400).json({ e })
|
||||
}
|
||||
})
|
||||
|
||||
const server = app.listen(port, () => console.log(`Userservice listening on port ${port}!`))
|
||||
process.on('SIGTERM', () => {
|
||||
server.close(() => {
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
up: async queryInterface =>
|
||||
queryInterface.sequelize.query(`
|
||||
--- add lisensiaatti to lääkis faculty
|
||||
INSERT INTO faculty_programmes VALUES ('H90', 'MH90_001', 'NOW()', 'NOW()');
|
||||
`),
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.dropTable('faculty_programmes')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -59,22 +59,6 @@ const UserFaculties = sequelize.define('user_faculties', {
|
||||
type: Sequelize.DATE,
|
||||
},
|
||||
})
|
||||
const FacultyProgrammes = sequelize.define('faculty_programmes', {
|
||||
faculty_code: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
programme_code: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
},
|
||||
})
|
||||
|
||||
const User = sequelize.define(
|
||||
'users',
|
||||
@@ -154,11 +138,6 @@ User.belongsToMany(Affiliation, {
|
||||
Affiliation.belongsToMany(User, { through: 'user_affiliation' })
|
||||
|
||||
User.hasMany(UserFaculties, { as: 'faculty', foreignKey: 'userId' })
|
||||
UserFaculties.hasMany(FacultyProgrammes, {
|
||||
as: 'programme',
|
||||
foreignKey: 'faculty_code',
|
||||
sourceKey: 'faculty_code',
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
@@ -167,6 +146,5 @@ module.exports = {
|
||||
HyGroup,
|
||||
Affiliation,
|
||||
UserFaculties,
|
||||
FacultyProgrammes,
|
||||
sequelize,
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
const { sequelize, forceSyncDatabase } = require('../database/connection')
|
||||
const { FacultyProgrammes } = require('../models/index')
|
||||
const facultyProgrammeService = require('./facultyprogrammes')
|
||||
|
||||
const facultyprogrammes = [
|
||||
{
|
||||
faculty_code: 'H10',
|
||||
programme_code: 'MH10_001',
|
||||
},
|
||||
{
|
||||
faculty_code: 'H50',
|
||||
programme_code: 'MH50_003',
|
||||
},
|
||||
]
|
||||
|
||||
beforeAll(async () => {
|
||||
await forceSyncDatabase()
|
||||
FacultyProgrammes.bulkCreate(facultyprogrammes)
|
||||
})
|
||||
afterAll(async () => {
|
||||
await forceSyncDatabase()
|
||||
await sequelize.close()
|
||||
})
|
||||
|
||||
describe('after running migrations the faculties faculty table', () => {
|
||||
test('includes some correct faculty & programme codes', async () => {
|
||||
const faculties = await facultyProgrammeService.findAll()
|
||||
expect(faculties.some(f => f.faculty_code === 'H10' && f.programme_code === 'MH10_001')).toBe(true)
|
||||
expect(faculties.some(f => f.faculty_code === 'H50' && f.programme_code === 'MH50_003')).toBe(true)
|
||||
})
|
||||
|
||||
test('does not include incorrect faculty & programme codes', async () => {
|
||||
const faculties = await facultyProgrammeService.findAll()
|
||||
expect(faculties.some(f => f.faculty_code === 'KH4000')).toBe(false)
|
||||
expect(faculties.some(f => f.programme_code === 'SWAGLITTINEN')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
const { FacultyProgrammes } = require('../models/index')
|
||||
|
||||
const findAll = () => FacultyProgrammes.findAll()
|
||||
|
||||
module.exports = { findAll }
|
||||
@@ -2,17 +2,7 @@ const Sequelize = require('sequelize')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const _ = require('lodash')
|
||||
const moment = require('moment')
|
||||
const { flatMap } = require('lodash')
|
||||
const {
|
||||
User,
|
||||
UserElementDetails,
|
||||
AccessGroup,
|
||||
HyGroup,
|
||||
Affiliation,
|
||||
FacultyProgrammes,
|
||||
UserFaculties,
|
||||
sequelize,
|
||||
} = require('../models')
|
||||
const { User, UserElementDetails, AccessGroup, HyGroup, Affiliation, UserFaculties, sequelize } = require('../models')
|
||||
const AccessService = require('./accessgroups')
|
||||
const AffiliationService = require('./affiliations')
|
||||
const HyGroupService = require('./hygroups')
|
||||
@@ -152,10 +142,6 @@ const userIncludes = [
|
||||
separate: true,
|
||||
model: UserFaculties,
|
||||
as: 'faculty',
|
||||
include: {
|
||||
model: FacultyProgrammes,
|
||||
as: 'programme',
|
||||
},
|
||||
},
|
||||
{
|
||||
model: Affiliation,
|
||||
@@ -194,11 +180,6 @@ const byUsernameMinified = async username => {
|
||||
separate: true,
|
||||
model: UserFaculties,
|
||||
as: 'faculty',
|
||||
include: {
|
||||
model: FacultyProgrammes,
|
||||
as: 'programme',
|
||||
attributes: ['programme_code'],
|
||||
},
|
||||
attributes: ['faculty_code'],
|
||||
},
|
||||
],
|
||||
@@ -245,13 +226,8 @@ const byId = async id => {
|
||||
return user
|
||||
}
|
||||
|
||||
const getUserProgrammes = user => {
|
||||
const elementdetails = [
|
||||
...user.programme.map(p => p.elementDetailCode),
|
||||
...flatMap(user.faculty, f => f.programme.map(p => p.programme_code)),
|
||||
]
|
||||
return elementdetails
|
||||
}
|
||||
const getUserProgrammes = user => user.programme.map(p => p.elementDetailCode)
|
||||
|
||||
const getUserAccessGroups = async username => {
|
||||
const user = await byUsername(username)
|
||||
return await user.getAccessgroup()
|
||||
@@ -287,8 +263,7 @@ const setFaculties = async (uid, faculties) => {
|
||||
})
|
||||
}
|
||||
|
||||
const modifyRights = async (uid, rights) => {
|
||||
console.log(uid, rights)
|
||||
const modifyRights = async (username, rights) => {
|
||||
const rightsToAdd = Object.entries(rights)
|
||||
.map(([code, val]) => {
|
||||
if (val === true) {
|
||||
@@ -304,7 +279,7 @@ const modifyRights = async (uid, rights) => {
|
||||
})
|
||||
.filter(code => code)
|
||||
|
||||
const user = await byId(uid)
|
||||
const user = await byUsername(username)
|
||||
const accessGroupsToAdd = await AccessService.byCodes(rightsToAdd)
|
||||
const accessGroupsToRemove = await AccessService.byCodes(rightsToRemove)
|
||||
|
||||
|
||||
+7
-7
@@ -11,7 +11,7 @@
|
||||
"cron": "^1.8.2",
|
||||
"express": "^4.17.1",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"i": "^0.3.6",
|
||||
"i": "^0.3.7",
|
||||
"knex": "^0.20.8",
|
||||
"lodash": "^4.17.21",
|
||||
"node-nats-streaming": "^0.2.6",
|
||||
@@ -1533,9 +1533,9 @@
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"node_modules/i": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/i/-/i-0.3.6.tgz",
|
||||
"integrity": "sha1-2WyScyB28HJxG2sQ/X1PZa2O4j0=",
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz",
|
||||
"integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
@@ -9580,9 +9580,9 @@
|
||||
}
|
||||
},
|
||||
"i": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/i/-/i-0.3.6.tgz",
|
||||
"integrity": "sha1-2WyScyB28HJxG2sQ/X1PZa2O4j0="
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz",
|
||||
"integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q=="
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"cron": "^1.8.2",
|
||||
"express": "^4.17.1",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"i": "^0.3.6",
|
||||
"i": "^0.3.7",
|
||||
"knex": "^0.20.8",
|
||||
"lodash": "^4.17.21",
|
||||
"node-nats-streaming": "^0.2.6",
|
||||
|
||||
@@ -78,6 +78,9 @@ async function courseResolver(rule) {
|
||||
|
||||
const id = rule.courseUnitGroupId
|
||||
const course = await knex('course_units').where({ group_id: id }).first()
|
||||
if (!course) {
|
||||
return { error: 'course not found' }
|
||||
}
|
||||
|
||||
return {
|
||||
id: course.group_id,
|
||||
|
||||
@@ -126,7 +126,14 @@ const moduleTypes = new Set(['ModuleAttainment', 'DegreeProgrammeAttainment'])
|
||||
const isModule = courseType => moduleTypes.has(courseType)
|
||||
|
||||
const creditMapper =
|
||||
(personIdToStudentNumber, courseUnitIdToCourseGroupId, moduleGroupIdToModuleCode, courseGroupIdToCourseCode) =>
|
||||
(
|
||||
personIdToStudentNumber,
|
||||
courseUnitIdToCourseGroupId,
|
||||
moduleGroupIdToModuleCode,
|
||||
courseGroupIdToCourseCode,
|
||||
studyrightIdToOrganisationsName,
|
||||
courseCodeToAyCodelessId
|
||||
) =>
|
||||
attainment => {
|
||||
const {
|
||||
id,
|
||||
@@ -141,6 +148,7 @@ const creditMapper =
|
||||
course_unit_id,
|
||||
module_group_id,
|
||||
nodes,
|
||||
study_right_id,
|
||||
} = attainment
|
||||
|
||||
const responsibleOrg = organisations.find(o => o.roleUrn === 'urn:code:organisation-role:responsible-organisation')
|
||||
@@ -149,10 +157,27 @@ const creditMapper =
|
||||
|
||||
if (!targetSemester) return null
|
||||
|
||||
const course_code = !isModule(type)
|
||||
let course_code = !isModule(type)
|
||||
? courseGroupIdToCourseCode[courseUnitIdToCourseGroupId[course_unit_id]]
|
||||
: moduleGroupIdToModuleCode[module_group_id]
|
||||
|
||||
let course_id = !isModule(type) ? courseUnitIdToCourseGroupId[course_unit_id] : module_group_id
|
||||
|
||||
// if ay studyright and no ay code
|
||||
if (study_right_id !== null && course_code) {
|
||||
if (!isModule(type)) {
|
||||
const organisationName = studyrightIdToOrganisationsName[study_right_id]
|
||||
if (organisationName) {
|
||||
if (organisationName['fi'].startsWith('Avoin yliopisto')) {
|
||||
if (!course_code.startsWith('AY')) {
|
||||
course_code = 'AY'.concat(course_code)
|
||||
course_id = courseCodeToAyCodelessId.get(course_code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// These are leaf attainments that have no other attainments attached to them
|
||||
const isStudyModule = nodes && nodes[0] !== undefined
|
||||
|
||||
@@ -164,7 +189,7 @@ const creditMapper =
|
||||
createdate: registration_date,
|
||||
credittypecode: getCreditTypeCodeFromAttainment(attainment, getGrade(grade_scale_id, grade_id).passed),
|
||||
attainment_date: attainment_date,
|
||||
course_id: !isModule(type) ? courseUnitIdToCourseGroupId[course_unit_id] : module_group_id,
|
||||
course_id: course_id,
|
||||
course_code,
|
||||
semestercode: targetSemester.semestercode,
|
||||
semester_composite: targetSemester.composite,
|
||||
@@ -220,7 +245,7 @@ const courseProviderMapper =
|
||||
|
||||
const timify = t => new Date(t).getTime()
|
||||
|
||||
const courseMapper = courseIdToAttainments => (groupedCourse, substitutions, organisation) => {
|
||||
const courseMapper = courseIdToAttainments => (groupedCourse, substitutions) => {
|
||||
const [groupId, courses] = groupedCourse
|
||||
const { code, name, study_level: coursetypecode } = courses[0]
|
||||
|
||||
@@ -234,10 +259,8 @@ const courseMapper = courseIdToAttainments => (groupedCourse, substitutions, org
|
||||
(res, curr) => {
|
||||
const courseAttainments = courseIdToAttainments[curr.id]
|
||||
if (!courseAttainments || courseAttainments.length === 0) return res
|
||||
|
||||
let min_attainment_date = res.min_attainment_date
|
||||
let max_attainment_date = res.max_attainment_date
|
||||
|
||||
if (!min_attainment_date || timify(min_attainment_date) > timify(courseAttainments[0].attainment_date))
|
||||
min_attainment_date = courseAttainments[0].attainment_date
|
||||
if (
|
||||
@@ -263,7 +286,6 @@ const courseMapper = courseIdToAttainments => (groupedCourse, substitutions, org
|
||||
enddate,
|
||||
is_study_module: false, // VALIDATE THIS PLS
|
||||
substitutions,
|
||||
responsible_organisation: organisation,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,14 +49,6 @@ const updateCourses = async (courseIdToAttainments, groupIdToCourse) => {
|
||||
}, [])
|
||||
),
|
||||
]
|
||||
// take responsible organisation from all course units
|
||||
const organisation = [
|
||||
...new Set(
|
||||
courses.reduce((acc, curr) => {
|
||||
return [...acc, ...flatten(curr.organisations).map(({ organisationId }) => organisationId)]
|
||||
}, [])
|
||||
),
|
||||
][0]
|
||||
|
||||
// Take organisation for the newest course_unit of this groupid
|
||||
const coursesSortedByStartDate = sortBy(courses, 'validity_period.startDate')
|
||||
@@ -67,7 +59,7 @@ const updateCourses = async (courseIdToAttainments, groupIdToCourse) => {
|
||||
.filter(({ roleUrn }) => roleUrn === 'urn:code:organisation-role:responsible-organisation')
|
||||
.map(mapCourseProvider)
|
||||
)
|
||||
return mapCourse(groupedCourse, substitutions, organisation)
|
||||
return mapCourse(groupedCourse, substitutions)
|
||||
})
|
||||
|
||||
await bulkCreate(Course, courses)
|
||||
|
||||
@@ -209,7 +209,7 @@ const updateStudents = async personIds => {
|
||||
|
||||
const updateAttainments = async (attainments, personIdToStudentNumber, attainmentsToBeExluced) => {
|
||||
await updateTeachers(attainments)
|
||||
const [courseUnits, modules] = await Promise.all([
|
||||
const [courseUnits, modules, studyrights] = await Promise.all([
|
||||
selectFromByIds(
|
||||
'course_units',
|
||||
attainments.map(a => a.course_unit_id).filter(id => !!id)
|
||||
@@ -219,6 +219,17 @@ const updateAttainments = async (attainments, personIdToStudentNumber, attainmen
|
||||
attainments.map(a => a.module_group_id).filter(id => !!id),
|
||||
'group_id'
|
||||
),
|
||||
selectFromByIds(
|
||||
'studyrights',
|
||||
attainments.map(a => a.study_right_id).filter(id => !!id)
|
||||
),
|
||||
])
|
||||
|
||||
const [studyrightOrganisations] = await Promise.all([
|
||||
selectFromByIds(
|
||||
'organisations',
|
||||
studyrights.map(studyright => studyright.organisation_id).filter(id => !!id)
|
||||
),
|
||||
])
|
||||
|
||||
const courseUnitIdToCourseGroupId = courseUnits.reduce((res, curr) => {
|
||||
@@ -231,6 +242,16 @@ const updateAttainments = async (attainments, personIdToStudentNumber, attainmen
|
||||
return res
|
||||
}, {})
|
||||
|
||||
const organisationIdToName = studyrightOrganisations.reduce((res, curr) => {
|
||||
res[curr.id] = curr.name
|
||||
return res
|
||||
}, {})
|
||||
|
||||
const studyrightIdToOrganisationsName = studyrights.reduce((res, curr) => {
|
||||
res[curr.id] = organisationIdToName[curr.organisation_id]
|
||||
return res
|
||||
}, {})
|
||||
|
||||
const idsOfFaculties = dbConnections.knex
|
||||
.select('id')
|
||||
.from('organisations')
|
||||
@@ -244,15 +265,15 @@ const updateAttainments = async (attainments, personIdToStudentNumber, attainmen
|
||||
.map(org => org.id)
|
||||
)
|
||||
|
||||
const courseGroupIdToCourseCode = (
|
||||
await Course.findAll({
|
||||
where: {
|
||||
id: {
|
||||
[Op.in]: Object.values(courseUnitIdToCourseGroupId),
|
||||
},
|
||||
const sisDbCoursesForStudentAttainments = await Course.findAll({
|
||||
where: {
|
||||
id: {
|
||||
[Op.in]: Object.values(courseUnitIdToCourseGroupId),
|
||||
},
|
||||
})
|
||||
).reduce((res, curr) => {
|
||||
},
|
||||
})
|
||||
|
||||
const courseGroupIdToCourseCode = sisDbCoursesForStudentAttainments.reduce((res, curr) => {
|
||||
res[curr.id] = curr.code
|
||||
return res
|
||||
}, {})
|
||||
@@ -266,9 +287,98 @@ const updateAttainments = async (attainments, personIdToStudentNumber, attainmen
|
||||
])
|
||||
const creditTeachers = []
|
||||
|
||||
const courseCodeToAyCodelessId = new Map()
|
||||
|
||||
const coursesToBeCreated = new Map()
|
||||
const coursesToBeUpdated = new Map()
|
||||
const courseProvidersToBeCreated = []
|
||||
|
||||
// because of AY-codeless open uni courses
|
||||
const fixAyCodelessOpenUniCourses = async (attainments, sisDbCoursesForStudentAttainments) => {
|
||||
const groupIdsWithOpenUnStudyright = attainments.reduce((res, curr) => {
|
||||
if (curr.study_right_id !== null && curr.type === 'CourseUnitAttainment') {
|
||||
const organisationName = studyrightIdToOrganisationsName[curr.study_right_id]
|
||||
if (organisationName) {
|
||||
if (organisationName['fi'].startsWith('Avoin yliopisto')) {
|
||||
res.push(courseUnitIdToCourseGroupId[curr.course_unit_id])
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}, [])
|
||||
|
||||
const coursesWithNoAyCodeAndOpenUniStudyright = groupIdsWithOpenUnStudyright.reduce((res, curr) => {
|
||||
const foundCourse = sisDbCoursesForStudentAttainments.find(c => c.id === curr)
|
||||
if (foundCourse) {
|
||||
if (!foundCourse.code.startsWith('AY')) {
|
||||
res.push(foundCourse)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}, [])
|
||||
|
||||
const coursesWithAyCodeAlreadyExist = await Course.findAll({
|
||||
raw: true,
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: Object.values(coursesWithNoAyCodeAndOpenUniStudyright.map(c => 'AY'.concat(c.code))),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
for (const course of coursesWithNoAyCodeAndOpenUniStudyright) {
|
||||
const courseWithIdForAyCodeExist = coursesWithAyCodeAlreadyExist.find(
|
||||
c => c.code === 'AY'.concat(course.code) && !c.id.endsWith('-ay')
|
||||
)
|
||||
|
||||
if (courseWithIdForAyCodeExist) {
|
||||
const latest_instance_date = getFirstDateIfIsAfter(
|
||||
course.latest_instance_date,
|
||||
courseWithIdForAyCodeExist.latest_instance_date
|
||||
)
|
||||
const max_attainment_date = getFirstDateIfIsAfter(
|
||||
course.max_attainment_date,
|
||||
courseWithIdForAyCodeExist.max_attainment_date
|
||||
)
|
||||
const min_attainment_date = getFirstDateIfIsBefore(
|
||||
course.min_attainment_date,
|
||||
courseWithIdForAyCodeExist.min_attainment_date
|
||||
)
|
||||
|
||||
if (latest_instance_date || max_attainment_date || min_attainment_date) {
|
||||
coursesToBeUpdated.set(courseWithIdForAyCodeExist.code, {
|
||||
...courseWithIdForAyCodeExist,
|
||||
latest_instance_date: latest_instance_date,
|
||||
max_attainment_date: max_attainment_date,
|
||||
min_attainment_date: min_attainment_date,
|
||||
})
|
||||
}
|
||||
|
||||
courseCodeToAyCodelessId.set('AY'.concat(course.code), courseWithIdForAyCodeExist.id)
|
||||
} else {
|
||||
coursesToBeCreated.set('AY'.concat(course.code), {
|
||||
...course.dataValues,
|
||||
name: course.name,
|
||||
id: course.id.concat('-ay'),
|
||||
code: 'AY'.concat(course.code),
|
||||
substitutions: [course.id],
|
||||
})
|
||||
courseCodeToAyCodelessId.set('AY'.concat(course.code), course.id.concat('-ay'))
|
||||
const courseProvider = await CourseProvider.findOne({
|
||||
where: {
|
||||
coursecode: course ? course.id : 'AY'.concat(course.code),
|
||||
},
|
||||
})
|
||||
if (!courseProvider) {
|
||||
const mapCourseProvider = courseProviderMapper('AY'.concat(course.code))
|
||||
courseProvidersToBeCreated.push(mapCourseProvider('hy-org-48645785'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await fixAyCodelessOpenUniCourses(attainments, sisDbCoursesForStudentAttainments)
|
||||
|
||||
// This mayhem fixes missing course_unit references for CustomCourseUnitAttainments.
|
||||
const fixCustomCourseUnitAttainments = async attainments => {
|
||||
const addCourseUnitToCustomCourseUnitAttainments = (courses, attIdToCourseCode) => async att => {
|
||||
@@ -413,7 +523,8 @@ const updateAttainments = async (attainments, personIdToStudentNumber, attainmen
|
||||
courseUnitIdToCourseGroupId,
|
||||
moduleGroupIdToModuleCode,
|
||||
courseGroupIdToCourseCode,
|
||||
fixedAttainments
|
||||
studyrightIdToOrganisationsName,
|
||||
courseCodeToAyCodelessId
|
||||
)
|
||||
|
||||
const credits = fixedAttainments
|
||||
@@ -437,7 +548,7 @@ const updateAttainments = async (attainments, personIdToStudentNumber, attainmen
|
||||
.filter(c => !!c)
|
||||
|
||||
const courses = Array.from(coursesToBeCreated.values())
|
||||
|
||||
const coursesUpdate = Array.from(coursesToBeUpdated.values())
|
||||
await bulkCreate(Course, courses)
|
||||
await bulkCreate(Credit, credits)
|
||||
await bulkCreate(
|
||||
@@ -452,6 +563,7 @@ const updateAttainments = async (attainments, personIdToStudentNumber, attainmen
|
||||
null,
|
||||
['composite']
|
||||
)
|
||||
await bulkCreate(Course, coursesUpdate)
|
||||
}
|
||||
|
||||
const updateTeachers = async attainments => {
|
||||
@@ -515,6 +627,24 @@ const updateTermRegistrations = async (termRegistrations, personIdToStudentNumbe
|
||||
await bulkCreate(SemesterEnrollment, semesterEnrollments)
|
||||
}
|
||||
|
||||
const getFirstDateIfIsAfter = (dateOne, dateTwo) => {
|
||||
const isAfter = new Date(dateOne) > new Date(dateTwo) ? dateOne : dateTwo
|
||||
if (isAfter) {
|
||||
return dateOne
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const getFirstDateIfIsBefore = (dateOne, dateTwo) => {
|
||||
const isAfter = new Date(dateOne) < new Date(dateTwo) ? dateOne : dateTwo
|
||||
if (isAfter) {
|
||||
return dateOne
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
updateStudents,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user