mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# oodikone2-cli
|
||||
# oodikone
|
||||
|
||||
[](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone)
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
@@ -104,12 +104,19 @@ describe('Population Statistics tests', () => {
|
||||
cy.contains("DIGI-000A", { timeout: 10000 })
|
||||
cy.go("back")
|
||||
|
||||
|
||||
|
||||
cy.contains("Courses of Population").parentsUntil(".ui.segment").parent().within(() => {
|
||||
cy.contains("number at least").siblings().within(() => cy.get("input").type("0"))
|
||||
cy.contains("number at least").siblings().within(() => cy.get("input").clear().type("0"))
|
||||
cy.contains("Matematiikan didaktiikka").siblings().eq(2).should("have.text", '9')
|
||||
})
|
||||
|
||||
cy.contains("Courses of Population").parentsUntil(".ui.segment").parent().within(() => {
|
||||
cy.get(".refresh").click()
|
||||
cy.contains("number at least").siblings().within(() => cy.get("input").clear().type("0"))
|
||||
cy.contains("Matematiikan didaktiikka").siblings().eq(2).should("have.text", '1')
|
||||
})
|
||||
|
||||
cy.get("button").contains("show").click()
|
||||
cy.contains("Student names hidden").click()
|
||||
cy.contains("Oinonen").siblings().eq(2).click()
|
||||
|
||||
@@ -73,12 +73,12 @@ router.get('/v2/studyprogrammes/:id/throughput', async (req, res) => {
|
||||
const code = req.params.id
|
||||
if (code) {
|
||||
const throughputData = await getThroughput(code)
|
||||
const thesis = await findProgrammeTheses(req.params.id)
|
||||
if (throughputData[code]) {
|
||||
return res.json(throughputData)
|
||||
return res.json({ ...throughputData, thesis })
|
||||
}
|
||||
const since = req.params.since ? req.params.since : new Date().getFullYear() - 5
|
||||
const data = await throughputStatsForStudytrack(req.params.id, since)
|
||||
const thesis = await findProgrammeTheses(req.params.id)
|
||||
await setThroughput(data)
|
||||
return res.json({...data, thesis })
|
||||
} else {
|
||||
|
||||
+3753
-1974
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,6 @@
|
||||
"lint:js": "eslint server ./ --ext .jsx,.js",
|
||||
"lintfix": "eslint server ./ --ext .jsx,.js --fix",
|
||||
"stats": "webpack --json --config webpack.config.production.js > stats.json",
|
||||
"pretest": "npm run lint",
|
||||
"test": "npm run lint",
|
||||
"notest": "echo",
|
||||
"storybook": "start-storybook -h 0.0.0.0 -p 7777 --ci"
|
||||
@@ -23,7 +22,7 @@
|
||||
"licence": "",
|
||||
"main": "src/app.jsx",
|
||||
"devDependencies": {
|
||||
"@storybook/react": "^5.0.1",
|
||||
"@storybook/react": "^5.0.10",
|
||||
"babel-core": "6.26.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "7.1.4",
|
||||
@@ -101,6 +100,7 @@
|
||||
"rimraf": "^2.6.3",
|
||||
"semantic-ui-css": "^2.4.1",
|
||||
"semantic-ui-react": "^0.83.0",
|
||||
"uuid": "^3.3.2"
|
||||
"uuid": "^3.3.2",
|
||||
"xlsx": "^0.14.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,10 +99,14 @@ class PopulationCourseStats extends Component {
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (state && props.courses) {
|
||||
const studentAmountLimit = state.studentAmountLimit > props.selectedStudents.length
|
||||
? Math.round(props.selectedStudents.length / 2) : state.studentAmountLimit
|
||||
|
||||
return {
|
||||
...state,
|
||||
courseStatistics: PopulationCourseStats.updateCourseStatisticsCriteria(props, state),
|
||||
initialSortReady: true
|
||||
initialSortReady: true,
|
||||
studentAmountLimit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +144,7 @@ class PopulationCourseStats extends Component {
|
||||
state = {
|
||||
sortCriteria: tableColumnNames.STUDENTS,
|
||||
reversed: true,
|
||||
studentAmountLimit: parseInt(...this.props.populationSize * 0.15, 10),
|
||||
studentAmountLimit: Math.round(this.props.populationSize * 0.3),
|
||||
codeFilter: '',
|
||||
activeView: null
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { string, arrayOf, object, func, bool, shape } from 'prop-types'
|
||||
import { Header, Segment, Button, Icon, Popup, Tab } from 'semantic-ui-react'
|
||||
import { Header, Segment, Button, Icon, Popup, Tab, Grid } from 'semantic-ui-react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import _ from 'lodash'
|
||||
import XLSX from 'xlsx'
|
||||
import { getStudentTotalCredits, copyToClipboard, userRoles, reformatDate, getTextIn } from '../../common'
|
||||
import { PRIORITYCODE_TEXTS } from '../../constants'
|
||||
|
||||
@@ -191,6 +192,24 @@ class PopulationStudents extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
const verticalTitle = title => (
|
||||
// https://stackoverflow.com/a/41396815
|
||||
<div style={{ writingMode: 'vertical-rl', minWidth: '32px', textAlign: 'left' }}>
|
||||
{title}
|
||||
</div>
|
||||
)
|
||||
|
||||
const hasPassedMandatory = (studentNumber, code) => (
|
||||
this.props.mandatoryPassed[code] && this.props.mandatoryPassed[code].includes(studentNumber)
|
||||
)
|
||||
|
||||
const totalMandatoryPassed = studentNumber => (
|
||||
this.props.mandatoryCourses.reduce((acc, m) => (
|
||||
hasPassedMandatory(studentNumber, m.code) ?
|
||||
acc + 1 : acc
|
||||
), 0)
|
||||
)
|
||||
|
||||
const mandatoryCourseColumns = [
|
||||
...(this.props.showNames) ? [
|
||||
{ key: 'lastname', title: 'last name', getRowVal: s => s.lastname, cellProps: { title: 'last name' } },
|
||||
@@ -198,15 +217,22 @@ class PopulationStudents extends Component {
|
||||
] : [],
|
||||
{
|
||||
key: 'studentnumber',
|
||||
title: (
|
||||
// https://stackoverflow.com/a/41396815
|
||||
<div style={{ writingMode: 'vertical-rl', minWidth: '32px', textAlign: 'left' }}>
|
||||
student number
|
||||
</div>
|
||||
),
|
||||
title: verticalTitle('student number'),
|
||||
cellProps: { title: 'student number' },
|
||||
getRowVal: s => s.studentNumber
|
||||
},
|
||||
{
|
||||
key: 'icon',
|
||||
title: '',
|
||||
getRowVal: s => (<Icon name="level up alternate" onClick={() => pushToHistoryFn(s.studentNumber)} />),
|
||||
cellProps: { collapsing: true, className: styles.iconCell }
|
||||
},
|
||||
{
|
||||
key: 'totalpassed',
|
||||
title: verticalTitle('total passed'),
|
||||
getRowVal: s => totalMandatoryPassed(s.studentNumber),
|
||||
cellProps: { title: 'total passed' }
|
||||
},
|
||||
..._.sortBy(
|
||||
this.props.mandatoryCourses,
|
||||
[(m) => {
|
||||
@@ -215,18 +241,11 @@ class PopulationStudents extends Component {
|
||||
}]
|
||||
).map(m => ({
|
||||
key: m.code,
|
||||
title: (
|
||||
// https://stackoverflow.com/a/41396815
|
||||
<div style={{ writingMode: 'vertical-rl', minWidth: '32px', textAlign: 'left' }}>
|
||||
{getTextIn(m.name, this.props.language)}<br />{m.code}
|
||||
</div>
|
||||
),
|
||||
// https://stackoverflow.com/a/246451
|
||||
title: verticalTitle(<Fragment>{getTextIn(m.name, this.props.language)}<br />{m.code}</Fragment>),
|
||||
cellProps: { title: `${getTextIn(m.name, this.props.language)}\n${m.code}` },
|
||||
getRowVal: s => Boolean(this.props.mandatoryPassed[m.code].includes(s.studentNumber)),
|
||||
getRowVal: s => hasPassedMandatory(s.studentNumber, m.code),
|
||||
getRowContent: s => (
|
||||
this.props.mandatoryPassed[m.code] &&this.props.mandatoryPassed[m.code].includes(s.studentNumber) ?
|
||||
(<Icon fitted name="check" color="green" />) : (<Icon fitted name="" color="grey" />)
|
||||
hasPassedMandatory(s.studentNumber, m.code) ? (<Icon fitted name="check" color="green" />) : (null)
|
||||
)
|
||||
}))
|
||||
]
|
||||
@@ -249,31 +268,76 @@ class PopulationStudents extends Component {
|
||||
menuItem: 'Mandatory courses',
|
||||
render: () => (
|
||||
<Tab.Pane>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<SortableTable
|
||||
getRowKey={s => s.studentNumber}
|
||||
tableProps={{
|
||||
celled: true,
|
||||
compact: 'very',
|
||||
padded: false,
|
||||
collapsing: true,
|
||||
basic: true,
|
||||
striped: true,
|
||||
singleLine: true,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
columns={mandatoryCourseColumns}
|
||||
data={this.props.selectedStudents.map(sn => students[sn])}
|
||||
/>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<SortableTable
|
||||
getRowKey={s => s.studentNumber}
|
||||
tableProps={{
|
||||
celled: true,
|
||||
compact: 'very',
|
||||
padded: false,
|
||||
collapsing: true,
|
||||
basic: true,
|
||||
striped: true,
|
||||
singleLine: true,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
columns={mandatoryCourseColumns}
|
||||
data={this.props.selectedStudents.map(sn => students[sn])}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ paddingLeft: '2em' }}>
|
||||
{this.props.mandatoryCourses.length === 0 &&
|
||||
<h1>Please define mandatory courses at study program overview panel!</h1>}
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Pane>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
const generateWorkbook = () => {
|
||||
const data = this.props.selectedStudents.map(sn => students[sn])
|
||||
const sortedMandatory = _.sortBy(
|
||||
this.props.mandatoryCourses,
|
||||
[(m) => {
|
||||
const res = m.code.match(/\d+/)
|
||||
return res ? Number(res[0]) : Number.MAX_VALUE
|
||||
}]
|
||||
)
|
||||
const worksheet = XLSX.utils.json_to_sheet(data.map(s => ({
|
||||
'last name': s.firstnames,
|
||||
'first names': s.lastname,
|
||||
'student number': s.studentNumber,
|
||||
'credits since start': getStudentTotalCredits(s),
|
||||
'all credits': s.credits,
|
||||
email: s.email,
|
||||
'transferred from': (s.transferredStudyright ? transferFrom(s) : ''),
|
||||
priority: priorityText(s.studyrights),
|
||||
extent: extentCodes(s.studyrights),
|
||||
'updated at': reformatDate(s.updatedAt, 'YYYY-MM-DD hh:mm:ss'),
|
||||
'mandatory total passed': totalMandatoryPassed(s.studentNumber),
|
||||
...sortedMandatory.reduce((acc, m) => {
|
||||
acc[`${getTextIn(m.name, this.props.language)}\n${m.code}`] = hasPassedMandatory(s.studentNumber, m.code)
|
||||
return acc
|
||||
}, {})
|
||||
})))
|
||||
const workbook = XLSX.utils.book_new()
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet)
|
||||
return workbook
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<StudentNameVisibilityToggle />
|
||||
<Grid columns="two">
|
||||
<Grid.Column><StudentNameVisibilityToggle /></Grid.Column>
|
||||
<Grid.Column textAlign="right">
|
||||
<Button icon labelPosition="right" onClick={() => XLSX.writeFile(generateWorkbook(), 'students.xlsx')}>
|
||||
Download
|
||||
<Icon name="file excel" />
|
||||
</Button>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
<Tab panes={panes} />
|
||||
</Fragment>
|
||||
)
|
||||
@@ -328,7 +392,8 @@ const mapStateToProps = ({ settings, populations, populationCourses, populationM
|
||||
if (populationCourses.data.coursestatistics) {
|
||||
const courses = populationCourses.data.coursestatistics
|
||||
mandatoryPassed = mandatoryCodes.reduce((obj, code) => {
|
||||
obj[code] = Object.keys(courses.find(c => c.course.code === code).students.passed)
|
||||
const foundCourse = !!courses.find(c => c.course.code === code)
|
||||
obj[code] = foundCourse ? Object.keys(courses.find(c => c.course.code === code).students.passed) : null
|
||||
return obj
|
||||
}, {})
|
||||
}
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class StudyProgrammeMandatoryCourses extends Component {
|
||||
<React.Fragment>
|
||||
<Message
|
||||
content="The set of mandatory courses which can be used in population filtering
|
||||
in the population statistics page"
|
||||
in the 'Study programme' > 'Search by class' results page"
|
||||
/>
|
||||
<AddMandatoryCourses
|
||||
addMandatoryCourse={this.props.addMandatoryCourse}
|
||||
|
||||
@@ -15,13 +15,17 @@ class WelcomePage extends Component { //eslint-disable-line
|
||||
</h3>
|
||||
<Divider section />
|
||||
<h4>
|
||||
Population Statistics
|
||||
Study programme
|
||||
</h4>
|
||||
<p>
|
||||
Query a student population specified by starting year and studyright.
|
||||
Oodikone will give you study statistics and visualizations of the population,
|
||||
which you can interactively filter and explore.
|
||||
</p>
|
||||
<p>
|
||||
View overview of a studyprogramme with statistics of population progress
|
||||
and yearly productivity.
|
||||
</p>
|
||||
<Divider section />
|
||||
|
||||
<h4>
|
||||
|
||||
+7
-21
@@ -1017,14 +1017,12 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -1039,20 +1037,17 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -1169,8 +1164,7 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -1182,7 +1176,6 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -1197,7 +1190,6 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -1205,14 +1197,12 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -1231,7 +1221,6 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -1312,8 +1301,7 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -1325,7 +1313,6 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -1447,7 +1434,6 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
||||
Generated
+7
-21
@@ -807,13 +807,11 @@
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -826,18 +824,15 @@
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -940,8 +935,7 @@
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -951,7 +945,6 @@
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -964,20 +957,17 @@
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -994,7 +984,6 @@
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -1067,8 +1056,7 @@
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -1078,7 +1066,6 @@
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -1184,7 +1171,6 @@
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
||||
Reference in New Issue
Block a user