mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9242b3063 | ||
|
|
6ccf320bb2 | ||
|
|
4e9b579843 | ||
|
|
985084864c | ||
|
|
71c61c3393 | ||
|
|
f3b1f639b1 | ||
|
|
e599e620c0 | ||
|
|
f5b081656f | ||
|
|
7af8e1a4df | ||
|
|
8c092c8d9f | ||
|
|
e0789186ba | ||
|
|
c798a4747f | ||
|
|
8f396a9913 | ||
|
|
148a5acc25 | ||
|
|
9cf1b87b3f | ||
|
|
82ed55e1f9 | ||
|
|
82041e673d | ||
|
|
1e4405aac9 |
@@ -193,7 +193,7 @@ const getTotalOpenUniCreditsOfCoursesBetween = async (a, b, alias = 'sum', alias
|
||||
return sequelize.query(
|
||||
`
|
||||
SELECT SUM(cr.credits) AS ${alias}, COUNT(DISTINCT(cr.student_studentnumber)) AS ${alias2},
|
||||
o.code AS organizationcode, co.code, co.id
|
||||
o.code AS organizationcode, co.code, co.id, co.name
|
||||
FROM credit cr
|
||||
INNER JOIN course co ON cr.course_code = co.code
|
||||
INNER JOIN course_providers cp ON cp.coursecode = co.id
|
||||
@@ -245,6 +245,27 @@ const getOrgStats = (stats, orgId) =>
|
||||
},
|
||||
}
|
||||
|
||||
const getOrgsOfOpenUniReqularCounterparts = async codes => {
|
||||
const rows = await sequelize.query(
|
||||
`
|
||||
SELECT co.id, co.code, cp.organizationcode, org.code AS organization
|
||||
FROM course co, course_providers cp, organization org
|
||||
WHERE co.code IN(:codes)
|
||||
AND co.id = cp.coursecode
|
||||
AND org.id = cp.organizationcode;
|
||||
`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: { codes },
|
||||
}
|
||||
)
|
||||
|
||||
return rows.reduce((acc, { code, organization }) => {
|
||||
acc[code] = organization
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
const YEAR_TO_MILLISECONDS = 31556952000
|
||||
/* Memoize parses booleans into strings... */
|
||||
@@ -314,6 +335,21 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
Organization.findAll(),
|
||||
])
|
||||
|
||||
const isOpenUniCourseCode = code => code.match(/^AY?(.+?)(?:en|fi|sv)?$/)
|
||||
|
||||
const unifyOpenUniversity = code => {
|
||||
const regexresult = isOpenUniCourseCode(code)
|
||||
if (!regexresult) return code
|
||||
return regexresult[1]
|
||||
}
|
||||
|
||||
const fromAyToNormalCourses = [
|
||||
...new Set(_.flatten(yearlyOpenUniAccCredits).map(c => unifyOpenUniversity(c.code))),
|
||||
...new Set(_.flatten(yearlyOpenUniTotalCredits).map(c => unifyOpenUniversity(c.code))),
|
||||
]
|
||||
|
||||
const orgsOfOpenUniReqularCounterparts = await getOrgsOfOpenUniReqularCounterparts(fromAyToNormalCourses)
|
||||
|
||||
const facultyProgrammes = facultiesAndProgrammesForTrends
|
||||
|
||||
const yearlyOrgStatPromises = yearRange.map(async year => {
|
||||
@@ -380,7 +416,6 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
}, {})
|
||||
|
||||
programmeToFaculties['H906'] = ['H906']
|
||||
programmeToFaculties['H930'] = ['H930']
|
||||
|
||||
const providerToProgramme = elementDetails.reduce((res, curr) => {
|
||||
const [p] = mapToProviders([curr.code])
|
||||
@@ -418,17 +453,64 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
return _.mergeWith(a, b, sumMerge)
|
||||
}
|
||||
|
||||
const refactoredOpenUniCredits = [..._.flatten(yearlyOpenUniAccCredits), ..._.flatten(yearlyOpenUniTotalCredits)].map(
|
||||
ele => {
|
||||
const org = orgsOfOpenUniReqularCounterparts[unifyOpenUniversity(ele.code)]
|
||||
return {
|
||||
...ele,
|
||||
organizationcode: org ? org : 'H930',
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/* Calculate course level stats and group by providers */
|
||||
const coursesGroupedByProvider = Object.entries(
|
||||
_.groupBy(
|
||||
[
|
||||
..._.flatten(yearlyAccCredits),
|
||||
..._.flatten(yearlyTotalCredits),
|
||||
..._.flatten(yearlyOpenUniAccCredits),
|
||||
..._.flatten(yearlyOpenUniTotalCredits),
|
||||
],
|
||||
'organizationcode'
|
||||
_.groupBy([..._.flatten(yearlyAccCredits), ..._.flatten(yearlyTotalCredits)], 'organizationcode')
|
||||
).reduce((acc, [organizationcode, courseCredits]) => {
|
||||
acc[organizationcode] = Object.entries(_.groupBy(courseCredits, 'code')).reduce(
|
||||
(acc, [courseCode, yearlyInstances]) => {
|
||||
acc[courseCode] = {
|
||||
yearly: {},
|
||||
name: yearlyInstances[0].name,
|
||||
type: 'course',
|
||||
code: courseCode,
|
||||
}
|
||||
|
||||
const array = _.range(2017, startYear + 1)
|
||||
|
||||
array.forEach(year => {
|
||||
acc[courseCode]['yearly'][year] = {}
|
||||
acc[courseCode]['yearly'][year]['acc'] = 0
|
||||
acc[courseCode]['yearly'][year]['accStudents'] = 0
|
||||
acc[courseCode]['yearly'][year]['total'] = undefined
|
||||
acc[courseCode]['yearly'][year]['totalStudents'] = undefined
|
||||
})
|
||||
|
||||
yearlyInstances.forEach(instance => {
|
||||
if (!acc[courseCode]['yearly'][instance.year]) acc[courseCode]['yearly'][instance.year] = {}
|
||||
|
||||
if (instance.acc !== undefined) {
|
||||
acc[courseCode]['yearly'][instance.year]['acc'] = instance.acc
|
||||
acc[courseCode]['yearly'][instance.year]['accStudents'] = Number(instance.students)
|
||||
} else {
|
||||
acc[courseCode]['yearly'][instance.year]['total'] = instance.total
|
||||
acc[courseCode]['yearly'][instance.year]['totalStudents'] = Number(instance.students)
|
||||
}
|
||||
})
|
||||
|
||||
acc[courseCode]['current'] = _.get(acc, [courseCode, 'yearly', startYear, 'acc']) || 0
|
||||
acc[courseCode]['previous'] = _.get(acc, [courseCode, 'yearly', startYear - 1, 'acc']) || 0
|
||||
acc[courseCode]['currentStudents'] = _.get(acc, [courseCode, 'yearly', startYear, 'accStudents']) || 0
|
||||
acc[courseCode]['previousStudents'] = _.get(acc, [courseCode, 'yearly', startYear - 1, 'accStudents']) || 0
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const openUniCoursesGroupedByProvider = Object.entries(
|
||||
_.groupBy(refactoredOpenUniCredits, 'organizationcode')
|
||||
).reduce((acc, [organizationcode, courseCredits]) => {
|
||||
acc[organizationcode] = Object.entries(_.groupBy(courseCredits, 'code')).reduce(
|
||||
(acc, [courseCode, yearlyInstances]) => {
|
||||
@@ -477,7 +559,7 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
const programme = providerToProgramme[organizationcode]
|
||||
|
||||
if (programme && programme.code) {
|
||||
if (programme.code === 'H906' || programme.code === 'H930') {
|
||||
if (programme.code === 'H906') {
|
||||
const courseValues = Object.values(courses)
|
||||
const yearlyValues = courseValues.map(c => c.yearly)
|
||||
|
||||
@@ -519,6 +601,34 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const openUniGroupedByProgramme = Object.entries(openUniCoursesGroupedByProvider).reduce(
|
||||
(acc, [organizationcode, courses]) => {
|
||||
const programme = providerToProgramme[organizationcode]
|
||||
|
||||
if (programme && programme.code) {
|
||||
const courseValues = Object.values(courses)
|
||||
const yearlyValues = courseValues.map(c => c.yearly)
|
||||
|
||||
acc[programme.code] = {
|
||||
type: 'programme',
|
||||
code: programme.code,
|
||||
name: programme.name,
|
||||
drill: courses,
|
||||
yearly: _.mergeWith({}, ...yearlyValues, mergele),
|
||||
current: _.sumBy(courseValues, 'current') | undefined,
|
||||
previous: _.sumBy(courseValues, 'previous') | undefined,
|
||||
currentStudents: _.sumBy(courseValues, 'currentStudents') || 'undefined',
|
||||
previousStudents: _.sumBy(courseValues, 'previousStudents') | 'undefined',
|
||||
}
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
/* Group programmes into faculties and calculate faculty level stats */
|
||||
const groupedByFaculty = Object.entries(groupedByProgramme).reduce((acc, [programmeCode, programmeStats]) => {
|
||||
const facultyCodes = programmeToFaculties[programmeCode]
|
||||
@@ -569,7 +679,31 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return groupedByFaculty
|
||||
const openUniGroupedByFaculty = Object.entries(openUniGroupedByProgramme).reduce(
|
||||
(acc, [programmeCode, programmeStats]) => {
|
||||
const facultyCode = 'H930'
|
||||
|
||||
if (!acc[facultyCode]) {
|
||||
acc[facultyCode] = {
|
||||
type: 'faculty',
|
||||
code: facultyCode,
|
||||
drill: {},
|
||||
name: facultyCodeToFaculty[facultyCode] ? facultyCodeToFaculty[facultyCode].name : null,
|
||||
yearly: {},
|
||||
current: 0,
|
||||
previous: 0,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
{}
|
||||
)
|
||||
return { ...groupedByFaculty, ...openUniGroupedByFaculty }
|
||||
}
|
||||
|
||||
const getStatus = async (unixMillis, showByYear, doRefresh = false) => {
|
||||
|
||||
Generated
+72
-22583
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@
|
||||
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.6.2",
|
||||
"@sentry/browser": "^6.9.0",
|
||||
"@sentry/tracing": "^6.11.0",
|
||||
"axios": "^0.21.1",
|
||||
@@ -22,18 +23,14 @@
|
||||
"qs": "^6.10.1",
|
||||
"query-string": "^6.14.1",
|
||||
"react": "^16.14.0",
|
||||
"react-context-devtool": "^2.0.3",
|
||||
"react-datetime": "^3.0.4",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-highcharts": "^16.1.0",
|
||||
"react-markdown": "^6.0.2",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-scroll-to-component": "^1.0.2",
|
||||
"redux": "^4.1.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"semantic-ui-css": "^2.4.1",
|
||||
"semantic-ui-react": "^2.0.3",
|
||||
"unfuck-spa-shibboleth-session": "^3.1.3",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import FilterContextProvider from './components/FilterTray/FilterContextProvider'
|
||||
import { LanguageProvider } from './components/LanguagePicker/useLanguage'
|
||||
|
||||
@@ -9,8 +8,4 @@ const CommonContext = ({ children }) => (
|
||||
</LanguageProvider>
|
||||
)
|
||||
|
||||
CommonContext.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
export default CommonContext
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios from 'axios'
|
||||
import * as Sentry from '@sentry/browser'
|
||||
import { getMocked, setMocking } from '../common'
|
||||
import { apiBasePath, isDev } from '../conf'
|
||||
import { loginPrefix, logoutPrefix } from '../redux/auth'
|
||||
|
||||
const getHeaders = () => {
|
||||
// Set up dev user for development environment, mimicking production admin user
|
||||
@@ -19,16 +20,16 @@ const getHeaders = () => {
|
||||
|
||||
export const api = axios.create({ baseURL: apiBasePath, headers: getHeaders() })
|
||||
|
||||
const types = {
|
||||
attempt: prefix => `${prefix}ATTEMPT`,
|
||||
failure: prefix => `${prefix}FAILURE`,
|
||||
success: prefix => `${prefix}SUCCESS`,
|
||||
export const actionSuffixes = {
|
||||
attempt: `ATTEMPT`,
|
||||
failure: `FAILURE`,
|
||||
success: `SUCCESS`,
|
||||
}
|
||||
|
||||
export const actionTypes = prefix => ({
|
||||
attempt: types.attempt(prefix),
|
||||
failure: types.failure(prefix),
|
||||
success: types.success(prefix),
|
||||
attempt: `${prefix}${actionSuffixes.attempt}`,
|
||||
failure: `${prefix}${actionSuffixes.failure}`,
|
||||
success: `${prefix}${actionSuffixes.success}`,
|
||||
})
|
||||
|
||||
export const logout = async () => {
|
||||
@@ -69,7 +70,8 @@ export const callController = (route, prefix, data, method = 'get', query, param
|
||||
params,
|
||||
onProgress,
|
||||
}
|
||||
return { type: `${prefix}ATTEMPT`, requestSettings }
|
||||
const { attempt } = actionTypes(prefix)
|
||||
return { type: attempt, requestSettings }
|
||||
}
|
||||
|
||||
const handleError = (err, actionHistory = []) => {
|
||||
@@ -90,20 +92,24 @@ export const handleRequest = store => next => async action => {
|
||||
const { requestSettings } = action
|
||||
if (requestSettings) {
|
||||
const { route, method, data, prefix, query, params, onProgress } = requestSettings
|
||||
const { success, failure } = actionTypes(prefix)
|
||||
try {
|
||||
const res = await callApi(route, method, data, params, 0, onProgress)
|
||||
store.dispatch({ type: `${prefix}SUCCESS`, response: res.data, query })
|
||||
store.dispatch({ type: success, response: res.data, query })
|
||||
} catch (e) {
|
||||
store.dispatch({ type: `${prefix}FAILURE`, response: e, query })
|
||||
store.dispatch({ type: failure, response: e, query })
|
||||
handleError(e, store.getState().actionHistory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const handleAuth = store => next => async action => {
|
||||
const loginTypes = actionTypes(loginPrefix)
|
||||
const logoutTypes = actionTypes(logoutPrefix)
|
||||
next(action)
|
||||
const { type } = action
|
||||
if (type === 'LOGIN_ATTEMPT') {
|
||||
if (type === loginTypes.attempt) {
|
||||
const { success, failure } = loginTypes
|
||||
try {
|
||||
let token
|
||||
const mocked = getMocked()
|
||||
@@ -118,17 +124,18 @@ export const handleAuth = store => next => async action => {
|
||||
...api.defaults.headers.common,
|
||||
'x-access-token': token,
|
||||
}
|
||||
store.dispatch({ type: 'LOGIN_SUCCESS', token })
|
||||
store.dispatch({ type: success, token })
|
||||
} catch (err) {
|
||||
store.dispatch({ type: 'LOGIN_FAILURE' })
|
||||
store.dispatch({ type: failure })
|
||||
handleError(err, store.getState().actionHistory)
|
||||
}
|
||||
} else if (type === 'LOGOUT_ATTEMPT') {
|
||||
} else if (type === loginTypes.attempt) {
|
||||
const { success, failure } = logoutTypes
|
||||
try {
|
||||
await logout()
|
||||
store.dispatch({ type: 'LOGOUT_SUCCESSFUL' })
|
||||
store.dispatch({ type: success })
|
||||
} catch (err) {
|
||||
store.dispatch({ type: 'LOGOUT_FAILURE' })
|
||||
store.dispatch({ type: failure })
|
||||
handleError(err, store.getState().actionHistory)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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'
|
||||
import propTypes from 'prop-types'
|
||||
import { isEqual } from 'lodash'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { images, getUserRoles, checkUserAccess } from '../common'
|
||||
import { useTitle } from '../common/hooks'
|
||||
import { builtAt } from '../conf'
|
||||
@@ -49,10 +47,11 @@ const OodiToOodikone = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const FrontPage = props => {
|
||||
const { userRoles, rights } = props
|
||||
|
||||
const FrontPage = () => {
|
||||
const { roles, rights } = useSelector(state => state.auth.token)
|
||||
const userRoles = getUserRoles(roles)
|
||||
useTitle()
|
||||
|
||||
const showItems = {
|
||||
populations: userRoles.includes('admin') || rights.length !== 0,
|
||||
studyProgramme: userRoles.includes('admin') || rights.length !== 0,
|
||||
@@ -122,25 +121,4 @@ const FrontPage = props => {
|
||||
)
|
||||
}
|
||||
|
||||
FrontPage.propTypes = {
|
||||
userRoles: propTypes.arrayOf(propTypes.string),
|
||||
rights: propTypes.arrayOf(propTypes.string),
|
||||
}
|
||||
|
||||
FrontPage.defaultProps = {
|
||||
userRoles: [],
|
||||
rights: [],
|
||||
}
|
||||
|
||||
const mapStateToProps = ({
|
||||
auth: {
|
||||
token: { roles, rights },
|
||||
},
|
||||
}) => ({
|
||||
userRoles: getUserRoles(roles),
|
||||
rights,
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, null, null, {
|
||||
areStatePropsEqual: isEqual,
|
||||
})(FrontPage)
|
||||
export default FrontPage
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 { getCustomPopulation, clearPopulations } from '../../redux/populations'
|
||||
import { getCustomPopulationCoursesByStudentnumbers } from '../../redux/populationCourses'
|
||||
import StyledMessage from './StyledMessage'
|
||||
import { getTextIn } from '../../common'
|
||||
@@ -150,11 +150,17 @@ const SingleStudyGroupContent = ({ population, group, language }) => {
|
||||
}
|
||||
|
||||
const SingleStudyGroupViewWrapper = ({ history, groupName, language, children }) => {
|
||||
const dispatch = useDispatch()
|
||||
const handleBack = () => {
|
||||
dispatch(clearPopulations())
|
||||
history.push('/studyguidancegroups')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="segmentContainer">
|
||||
<Segment className="contentSegment">
|
||||
<Button icon="arrow circle left" content="Back" onClick={() => history.push('/studyguidancegroups')} />
|
||||
<Button icon="arrow circle left" content="Back" onClick={handleBack} />
|
||||
<Divider />
|
||||
<Header size="medium">{getTextIn(groupName, language)}</Header>
|
||||
</Segment>
|
||||
@@ -174,9 +180,8 @@ const SingleStudyGuidanceGroupContainer = ({ groupid }) => {
|
||||
const { data: population, pending } = useSelector(({ populations }) => populations)
|
||||
|
||||
useEffect(() => {
|
||||
if (!group) return
|
||||
const groupStudentNumbers = group?.members?.map(({ personStudentNumber }) => personStudentNumber) || []
|
||||
if (groupStudentNumbers.length === 0) return
|
||||
if (groupStudentNumbers.length === 0 || population?.students?.length > 0) return
|
||||
dispatch(getCustomPopulation({ studentnumberlist: groupStudentNumbers, usingStudyGuidanceGroups: true }))
|
||||
dispatch(
|
||||
getCustomPopulationCoursesByStudentnumbers({
|
||||
|
||||
+38
-6
@@ -31,6 +31,10 @@ const prettifyCamelCase = str => {
|
||||
return splitted.map(w => w.charAt(0).toLowerCase() + w.substring(1)).join(' ')
|
||||
}
|
||||
|
||||
const startYearToAcademicYear = year => {
|
||||
return year === '' || Number.isNaN(year) ? '' : `${year} - ${parseInt(year, 10) + 1}`
|
||||
}
|
||||
|
||||
const AssociateTagForm = ({ group, tagName, toggleEdit, selectFieldItems }) => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
@@ -65,7 +69,14 @@ const AssociateTagForm = ({ group, tagName, toggleEdit, selectFieldItems }) => {
|
||||
dateFormat="YYYY"
|
||||
timeFormat={false}
|
||||
initialvalue={group.tags?.[tagName]}
|
||||
renderYear={(props, selectableYear) => <td {...props}>{selectableYear}</td>}
|
||||
renderYear={(props, selectableYear) => <td {...props}>{startYearToAcademicYear(selectableYear)}</td>}
|
||||
renderInput={({ value, ...rest }) => {
|
||||
return (
|
||||
<div>
|
||||
<input value={startYearToAcademicYear(value)} placeholder="Select year" {...rest} />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
closeOnSelect
|
||||
value={formik.values[tagName]}
|
||||
onChange={value => formik.setFieldValue(tagName, value?.format('YYYY'))}
|
||||
@@ -90,8 +101,17 @@ const AssociateTagForm = ({ group, tagName, toggleEdit, selectFieldItems }) => {
|
||||
|
||||
const TagCell = ({ tagName, group, studyProgrammes }) => {
|
||||
const [showEdit, toggleEdit] = useToggle()
|
||||
const value = group.tags?.[tagName]
|
||||
const text = tagName === 'studyProgramme' ? studyProgrammes.find(p => p.value === value)?.text : value
|
||||
const getText = () => {
|
||||
switch (tagName) {
|
||||
case 'studyProgramme':
|
||||
return studyProgrammes.find(p => p.value === group.tags?.[tagName])?.text
|
||||
case 'year':
|
||||
return startYearToAcademicYear(group.tags?.[tagName])
|
||||
default:
|
||||
throw Error(`Wrong tagname: ${tagName}`)
|
||||
}
|
||||
}
|
||||
const text = getText()
|
||||
|
||||
return group.tags?.[tagName] && !showEdit ? (
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'baseline' }}>
|
||||
@@ -138,10 +158,22 @@ const StudyGuidanceGroupOverview = () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'students',
|
||||
title: 'students',
|
||||
getRowVal: group => group.members?.length || 0,
|
||||
getRowContent: group => group.members?.length || 0,
|
||||
cellProps: {
|
||||
style: {
|
||||
padding: '0',
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'studyProgramme',
|
||||
title: 'Study programme',
|
||||
getRowVal: () => 'studyProgramme',
|
||||
getRowVal: group => group.tags?.studyProgramme,
|
||||
headerProps: { onClick: null, sorted: null },
|
||||
getRowContent: group => (
|
||||
<TagCell tagName="studyProgramme" studyProgrammes={studyProgrammesFilteredForDropdown} group={group} />
|
||||
@@ -149,8 +181,8 @@ const StudyGuidanceGroupOverview = () => {
|
||||
},
|
||||
{
|
||||
key: 'associatedyear',
|
||||
title: 'Associated year',
|
||||
getRowVal: () => 'associatedYear',
|
||||
title: 'Associated starting academic year',
|
||||
getRowVal: group => group.tags?.year,
|
||||
headerProps: { onClick: null, sorted: null },
|
||||
getRowContent: group => <TagCell tagName="year" group={group} />,
|
||||
},
|
||||
|
||||
@@ -4,18 +4,14 @@ import { Provider } from 'react-redux'
|
||||
import 'semantic-ui-css/semantic.min.css'
|
||||
import 'react-datetime/css/react-datetime.css'
|
||||
import './styles/custom.css'
|
||||
import { debugContextDevtool } from 'react-context-devtool'
|
||||
import store from './store'
|
||||
import store from './redux'
|
||||
import ErrorBoundary from './components/ErrorBoundary'
|
||||
import Main from './components/Main'
|
||||
import CommonContext from './CommonContext'
|
||||
import initializeSentry from './util/sentry'
|
||||
import { isProduction } from './conf'
|
||||
|
||||
initializeSentry()
|
||||
|
||||
const container = document.getElementById('root')
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<CommonContext>
|
||||
@@ -24,8 +20,5 @@ ReactDOM.render(
|
||||
</ErrorBoundary>
|
||||
</CommonContext>
|
||||
</Provider>,
|
||||
container
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
||||
// Best not to use this in production as the performance impact appears to be quite high.
|
||||
debugContextDevtool(container, { disable: isProduction })
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { decodeToken } from '../common'
|
||||
|
||||
export const loginPrefix = 'LOGIN_'
|
||||
export const logoutPrefix = 'LOGOUT_'
|
||||
|
||||
export const login = () => ({
|
||||
type: 'LOGIN_ATTEMPT',
|
||||
})
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { actionTypes } from '../../apiConnection'
|
||||
|
||||
const additionalTypes = prefix => ({
|
||||
reset: `${prefix}_CLEAR`,
|
||||
})
|
||||
|
||||
const createReducer = (
|
||||
prefix,
|
||||
{
|
||||
reduceGET = (_, action) => action.response,
|
||||
reducePOST = (data, action) => data.concat(action.response),
|
||||
reduceDELETE = (data, action) => data.filter(d => d.id === action.query.id),
|
||||
defaults = undefined,
|
||||
}
|
||||
) => {
|
||||
const prefixGET = `${prefix}GET_`
|
||||
const prefixPOST = `${prefix}POST_`
|
||||
const prefixDELETE = `${prefix}DELETE_`
|
||||
const read = actionTypes(prefixGET)
|
||||
const create = actionTypes(prefixPOST)
|
||||
const destroy = actionTypes(prefixDELETE)
|
||||
const other = additionalTypes(prefix)
|
||||
const initialState = {
|
||||
pending: false,
|
||||
error: false,
|
||||
data: [],
|
||||
...defaults,
|
||||
}
|
||||
const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case destroy.attempt:
|
||||
case create.attempt:
|
||||
case read.attempt:
|
||||
return {
|
||||
...state,
|
||||
error: false,
|
||||
pending: true,
|
||||
}
|
||||
case destroy.failure:
|
||||
case create.failure:
|
||||
case read.failure:
|
||||
return {
|
||||
...state,
|
||||
error: true,
|
||||
pending: false,
|
||||
}
|
||||
case read.success:
|
||||
return {
|
||||
...state,
|
||||
data: reduceGET(state.data, action),
|
||||
error: false,
|
||||
pending: false,
|
||||
}
|
||||
case create.success:
|
||||
return {
|
||||
...state,
|
||||
data: reducePOST(state.data, action),
|
||||
error: false,
|
||||
pending: false,
|
||||
}
|
||||
case destroy.success:
|
||||
return {
|
||||
...state,
|
||||
data: reduceDELETE(state.data, action),
|
||||
error: false,
|
||||
pending: false,
|
||||
}
|
||||
case other.reset: {
|
||||
return {
|
||||
...initialState,
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
return {
|
||||
reducer,
|
||||
prefixGET,
|
||||
prefixPOST,
|
||||
prefixDELETE,
|
||||
}
|
||||
}
|
||||
|
||||
export default createReducer
|
||||
@@ -0,0 +1,33 @@
|
||||
import { actionSuffixes } from '../../apiConnection'
|
||||
|
||||
export const matcherReducers = [
|
||||
{
|
||||
matcher: action => action.type.endsWith(actionSuffixes.attempt),
|
||||
reducer(state) {
|
||||
state.error = false
|
||||
state.pending = true
|
||||
},
|
||||
},
|
||||
{
|
||||
matcher: action => action.type.endsWith(actionSuffixes.failure),
|
||||
reducer(state) {
|
||||
state.error = true
|
||||
state.pending = false
|
||||
},
|
||||
},
|
||||
{
|
||||
matcher: action => action.type.endsWith(actionSuffixes.success),
|
||||
reducer(state) {
|
||||
state.error = false
|
||||
state.pending = false
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const defaultReducer = () => {}
|
||||
|
||||
export const initialState = {
|
||||
pending: false,
|
||||
error: false,
|
||||
data: [],
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { combineReducers } from 'redux'
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import { handleRequest, handleAuth } from '../apiConnection'
|
||||
import actionHistory from './actionHistory'
|
||||
import users from './users'
|
||||
import populations from './populations'
|
||||
@@ -41,46 +42,55 @@ import customPopulationSearch from './customPopulationSearch'
|
||||
import coolDataScience from './coolDataScience'
|
||||
import studyGuidanceGroups from './studyGuidanceGroups'
|
||||
|
||||
export default combineReducers({
|
||||
actionHistory,
|
||||
users,
|
||||
mandatoryCourseLabels,
|
||||
populations,
|
||||
populationCourses,
|
||||
populationSelectedStudentCourses,
|
||||
populationProgrammes,
|
||||
populationProgrammesUnfiltered,
|
||||
populationMandatoryCourses,
|
||||
studyProgrammeProductivity,
|
||||
studyProgrammeThroughput,
|
||||
studyProgrammeBachelors,
|
||||
faculties,
|
||||
units,
|
||||
students,
|
||||
errors,
|
||||
courses,
|
||||
courseInstances,
|
||||
graphSpinner,
|
||||
settings,
|
||||
teachers,
|
||||
providers,
|
||||
semesters,
|
||||
teacherStatistics,
|
||||
teachersTop,
|
||||
teachersTopCategories,
|
||||
courseSearch,
|
||||
courseStats,
|
||||
courseSummaryForm,
|
||||
thesisCourses,
|
||||
accessGroups,
|
||||
elementdetails,
|
||||
feedback,
|
||||
tags,
|
||||
tagstudent,
|
||||
auth,
|
||||
singleCourseStats,
|
||||
userAccessEmail,
|
||||
customPopulationSearch,
|
||||
coolDataScience,
|
||||
studyGuidanceGroups,
|
||||
export default configureStore({
|
||||
reducer: {
|
||||
actionHistory,
|
||||
users,
|
||||
mandatoryCourseLabels,
|
||||
populations,
|
||||
populationCourses,
|
||||
populationSelectedStudentCourses,
|
||||
populationProgrammes,
|
||||
populationProgrammesUnfiltered,
|
||||
populationMandatoryCourses,
|
||||
studyProgrammeProductivity,
|
||||
studyProgrammeThroughput,
|
||||
studyProgrammeBachelors,
|
||||
faculties,
|
||||
units,
|
||||
students,
|
||||
errors,
|
||||
courses,
|
||||
courseInstances,
|
||||
graphSpinner,
|
||||
settings,
|
||||
teachers,
|
||||
providers,
|
||||
semesters,
|
||||
teacherStatistics,
|
||||
teachersTop,
|
||||
teachersTopCategories,
|
||||
courseSearch,
|
||||
courseStats,
|
||||
courseSummaryForm,
|
||||
thesisCourses,
|
||||
accessGroups,
|
||||
elementdetails,
|
||||
feedback,
|
||||
tags,
|
||||
tagstudent,
|
||||
auth,
|
||||
singleCourseStats,
|
||||
userAccessEmail,
|
||||
customPopulationSearch,
|
||||
coolDataScience,
|
||||
studyGuidanceGroups,
|
||||
},
|
||||
// oodikone is currently too heavy for other middlewares than thunk, but
|
||||
// feel free to take use them at some point if possible
|
||||
middleware: getDefaultMiddleware => [
|
||||
...getDefaultMiddleware({ immutableCheck: false, serializableCheck: false }),
|
||||
handleRequest,
|
||||
handleAuth,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -1,80 +1,37 @@
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { callController, actionTypes } from '../apiConnection'
|
||||
import { initialState, matcherReducers, defaultReducer } from './common'
|
||||
|
||||
const baseUrl = '/studyguidancegroups'
|
||||
const getPrefix = 'GET_STUDY_GUIDANCE_GROUPS_'
|
||||
const getTypes = {
|
||||
...actionTypes(getPrefix),
|
||||
}
|
||||
|
||||
export const getStudyGuidanceGroups = () => {
|
||||
const route = '/studyguidancegroups'
|
||||
return callController(route, getPrefix)
|
||||
}
|
||||
|
||||
const changeTagsPrefix = 'CHANGE_STUDY_GUIDANCE_GROUP_TAGS_'
|
||||
const changeTagsTypes = {
|
||||
...actionTypes(changeTagsPrefix),
|
||||
}
|
||||
|
||||
export const changeStudyGuidanceGroupTags = (groupId, tags) => {
|
||||
const route = `/studyguidancegroups/${groupId}/tags`
|
||||
return callController(route, changeTagsPrefix, tags, 'put')
|
||||
}
|
||||
const initialState = {
|
||||
pending: false,
|
||||
error: false,
|
||||
data: [],
|
||||
}
|
||||
export const getStudyGuidanceGroups = () => callController(baseUrl, getPrefix)
|
||||
|
||||
const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case getTypes.attempt:
|
||||
return {
|
||||
...state,
|
||||
error: false,
|
||||
pending: true,
|
||||
}
|
||||
case getTypes.failure:
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: true,
|
||||
}
|
||||
case getTypes.success:
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: false,
|
||||
data: action.response,
|
||||
}
|
||||
case changeTagsTypes.attempt:
|
||||
return {
|
||||
...state,
|
||||
error: false,
|
||||
pending: false,
|
||||
}
|
||||
case changeTagsTypes.failure:
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: true,
|
||||
}
|
||||
case changeTagsTypes.success:
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: false,
|
||||
data: state.data.map(group =>
|
||||
group.id === action.response.studyGuidanceGroupId
|
||||
? {
|
||||
...state.data.find(group => group.id === action.response.studyGuidanceGroupId),
|
||||
tags: (({ studyGuidanceGroupId, ...rest }) => rest)(action.response), // omit id
|
||||
}
|
||||
: group
|
||||
),
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
export const changeStudyGuidanceGroupTags = (groupId, tags) =>
|
||||
callController(`${baseUrl}/${groupId}/tags`, changeTagsPrefix, tags, 'put')
|
||||
|
||||
const reducer = createReducer(
|
||||
initialState,
|
||||
{
|
||||
[getTypes.success]: (state, action) => {
|
||||
state.data = action.response
|
||||
},
|
||||
[changeTagsTypes.success]: (state, action) => {
|
||||
state.data.find(group => group.id === action.response.studyGuidanceGroupId).tags = (({
|
||||
studyGuidanceGroupId,
|
||||
...rest
|
||||
}) => rest)(action.response)
|
||||
},
|
||||
},
|
||||
matcherReducers,
|
||||
defaultReducer
|
||||
)
|
||||
|
||||
export default reducer
|
||||
|
||||
@@ -1,5 +1,86 @@
|
||||
import createReducer from './common/crudreducer'
|
||||
import { callController } from '../apiConnection/index'
|
||||
import { callController, actionTypes } from '../apiConnection/index'
|
||||
|
||||
const additionalTypes = prefix => ({
|
||||
reset: `${prefix}_CLEAR`,
|
||||
})
|
||||
|
||||
const createReducer = (
|
||||
prefix,
|
||||
{
|
||||
reduceGET = (_, action) => action.response,
|
||||
reducePOST = (data, action) => data.concat(action.response),
|
||||
reduceDELETE = (data, action) => data.filter(d => d.id === action.query.id),
|
||||
defaults = undefined,
|
||||
}
|
||||
) => {
|
||||
const prefixGET = `${prefix}GET_`
|
||||
const prefixPOST = `${prefix}POST_`
|
||||
const prefixDELETE = `${prefix}DELETE_`
|
||||
const read = actionTypes(prefixGET)
|
||||
const create = actionTypes(prefixPOST)
|
||||
const destroy = actionTypes(prefixDELETE)
|
||||
const other = additionalTypes(prefix)
|
||||
const initialState = {
|
||||
pending: false,
|
||||
error: false,
|
||||
data: [],
|
||||
...defaults,
|
||||
}
|
||||
const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case destroy.attempt:
|
||||
case create.attempt:
|
||||
case read.attempt:
|
||||
return {
|
||||
...state,
|
||||
error: false,
|
||||
pending: true,
|
||||
}
|
||||
case destroy.failure:
|
||||
case create.failure:
|
||||
case read.failure:
|
||||
return {
|
||||
...state,
|
||||
error: true,
|
||||
pending: false,
|
||||
}
|
||||
case read.success:
|
||||
return {
|
||||
...state,
|
||||
data: reduceGET(state.data, action),
|
||||
error: false,
|
||||
pending: false,
|
||||
}
|
||||
case create.success:
|
||||
return {
|
||||
...state,
|
||||
data: reducePOST(state.data, action),
|
||||
error: false,
|
||||
pending: false,
|
||||
}
|
||||
case destroy.success:
|
||||
return {
|
||||
...state,
|
||||
data: reduceDELETE(state.data, action),
|
||||
error: false,
|
||||
pending: false,
|
||||
}
|
||||
case other.reset: {
|
||||
return {
|
||||
...initialState,
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
return {
|
||||
reducer,
|
||||
prefixGET,
|
||||
prefixPOST,
|
||||
prefixDELETE,
|
||||
}
|
||||
}
|
||||
|
||||
const prefix = 'PROGRAMME_THESIS_'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { combineReducers } from 'redux'
|
||||
import { combineReducers } from '@reduxjs/toolkit'
|
||||
import { callController, actionTypes } from '../apiConnection'
|
||||
|
||||
const CLEAR_USER_ACCESS_ERRORS = 'CLEAR_USER_ACCESS_ERRORS'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSelector } from 'reselect'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import { sortBy, flatten } from 'lodash'
|
||||
|
||||
const courseStatsSelector = state => state.courseStats.data
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSelector } from 'reselect'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import { byDateDesc, reformatDate } from '../common'
|
||||
|
||||
const getData = obj => obj
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSelector } from 'reselect'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
|
||||
const getPopulations = state => state.populations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSelector } from 'reselect'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
|
||||
const associationsSelector = state => state.populationProgrammesUnfiltered.data
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSelector } from 'reselect'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import { reformatDate } from '../common'
|
||||
|
||||
const getStudents = students => students.data
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { createStore, applyMiddleware, compose } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import reducers from './redux'
|
||||
import { handleRequest, handleAuth } from './apiConnection'
|
||||
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
|
||||
|
||||
const store = createStore(reducers, composeEnhancers(applyMiddleware(thunk, handleRequest, handleAuth)))
|
||||
|
||||
export default store
|
||||
Reference in New Issue
Block a user