Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk

This commit is contained in:
totutotu
2019-04-26 10:41:37 +03:00
12 changed files with 3898 additions and 2062 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# oodikone2-cli
# oodikone
[![Build Status](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone.svg?branch=master)](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone)
+5
View File
@@ -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"
}
+8 -1
View File
@@ -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 {
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -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
}, {})
}
@@ -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
View File
@@ -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",
+7 -21
View File
@@ -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",