diff --git a/services/backend/oodikone2-backend/scripts/force_sync_database.js b/services/backend/oodikone2-backend/scripts/force_sync_database.js index ed123972b3..b7da12cd4a 100644 --- a/services/backend/oodikone2-backend/scripts/force_sync_database.js +++ b/services/backend/oodikone2-backend/scripts/force_sync_database.js @@ -1,13 +1,10 @@ -const { sequelize, sequelizeKone } = require('../src/models/index') -const { seedMigrations } = require('../src/database/seed_migrations') - -const options = { force: true } +const { seedAllMigrations } = require('../src/database/seed_migrations') +const { forceSyncDatabase } = require('../src/database/connection') const sync = async () => { try { - await sequelize.sync(options) - await sequelizeKone.sync(options) - await seedMigrations() + await forceSyncDatabase() + await seedAllMigrations() console.log('Force sync succeeded. ') process.exit(0) } catch (e) { diff --git a/services/backend/oodikone2-backend/scripts/seed_migrations.js b/services/backend/oodikone2-backend/scripts/seed_migrations.js index 6f5f33684d..b85ae8cf88 100644 --- a/services/backend/oodikone2-backend/scripts/seed_migrations.js +++ b/services/backend/oodikone2-backend/scripts/seed_migrations.js @@ -1,8 +1,8 @@ -const { seedMigrations } = require('../src/database/seed_migrations') +const { seedAllMigrations } = require('../src/database/seed_migrations') const run = async () => { try { - await seedMigrations() + await seedAllMigrations() process.exit(0) } catch (e) { console.log(`Seeding migrations failed: ${e}`) diff --git a/services/backend/oodikone2-backend/src/database/connection.js b/services/backend/oodikone2-backend/src/database/connection.js index 5a661c2917..55128ac956 100644 --- a/services/backend/oodikone2-backend/src/database/connection.js +++ b/services/backend/oodikone2-backend/src/database/connection.js @@ -76,8 +76,7 @@ const runMigrationsKone = async () => { } } -const migrationPromise = !conf.isTest && conf.DB_SCHEMA === 'public' ? runMigrations().then(() => runMigrationsKone()) - : Promise.resolve() +const migrationPromise = !conf.isTest ? runMigrations().then(() => runMigrationsKone()) : Promise.resolve() const forceSyncDatabase = async () => { try { @@ -90,8 +89,8 @@ const forceSyncDatabase = async () => { } catch (e) { // console.log(e) } - await sequelize.sync({ force: true, schema: conf.DB_SCHEMA, searchPath: conf.DB_SCHEMA }) - await sequelizeKone.sync({ force: true, schema: conf.DB_SCHEMA_KONE, searchPath: conf.DB_SCHEMA_KONE }) + await sequelize.sync({ force: true }) + await sequelizeKone.sync({ force: true }) } module.exports = { diff --git a/services/backend/oodikone2-backend/src/database/seed_migrations.js b/services/backend/oodikone2-backend/src/database/seed_migrations.js index 0fcafeceec..73a9dbd0df 100644 --- a/services/backend/oodikone2-backend/src/database/seed_migrations.js +++ b/services/backend/oodikone2-backend/src/database/seed_migrations.js @@ -17,12 +17,11 @@ const seedMigrationsKone = async (migrationfilepath=DEFAULT_PATH_KONE) => { await saveMigrationsToDatabase(MigrationKone)(filenames) } -const run = async () => { +const seedAllMigrations = async () => { await seedMigrations() await seedMigrationsKone() } module.exports = { - run, - seedMigrations + seedAllMigrations } \ No newline at end of file diff --git a/services/backend/oodikone2-backend/src/routes/teachers.js b/services/backend/oodikone2-backend/src/routes/teachers.js index 2fee103be9..b711f95bbb 100644 --- a/services/backend/oodikone2-backend/src/routes/teachers.js +++ b/services/backend/oodikone2-backend/src/routes/teachers.js @@ -20,6 +20,12 @@ router.get('/top', async (req, res) => { res.json(result) }) +router.post('/top', async (req, res) => { + const { startyearcode, endyearcode } = req.body + res.status(200).end() + await topteachers.findAndSaveTeachers(startyearcode, endyearcode) +}) + router.get('/top/categories', async (req, res) => { const result = await topteachers.getCategoriesAndYears() res.json(result) diff --git a/services/backend/oodikone2-backend/src/services/semesters.js b/services/backend/oodikone2-backend/src/services/semesters.js index e8dcc315c6..1fc21c9c7e 100644 --- a/services/backend/oodikone2-backend/src/services/semesters.js +++ b/services/backend/oodikone2-backend/src/services/semesters.js @@ -1,5 +1,6 @@ -const { Op } = require('sequelize') +const sequelize = require('sequelize') const { Semester } = require('../models/index') +const { Op } = sequelize const getSemestersAndYears = async before => { const semesters = await Semester.findAll({ @@ -24,6 +25,15 @@ const getSemestersAndYears = async before => { return result } +const getMaxYearcode = async () => { + const aa = await Semester.findAll({ + attributes: [[sequelize.fn('max', sequelize.col('yearcode')), 'maxYearCode']], + raw: true + }) + return aa[0].maxYearCode +} + module.exports = { - getSemestersAndYears + getSemestersAndYears, + getMaxYearcode } \ No newline at end of file diff --git a/services/backend/oodikone2-backend/src/services/studytrack.js b/services/backend/oodikone2-backend/src/services/studytrack.js index 08f581e5c5..af9de7bf16 100644 --- a/services/backend/oodikone2-backend/src/services/studytrack.js +++ b/services/backend/oodikone2-backend/src/services/studytrack.js @@ -8,8 +8,6 @@ const { studentnumbersWithAllStudyrightElements } = require('./populations') const { semesterStart, semesterEnd } = require('../util/semester') const isNumber = str => !Number.isNaN(Number(str)) -const FIVE_YEARS_IN_MONTHS = 60 - const studytrackToProviderCode = code => { const [left, right] = code.split('_') const prefix = [...left].filter(isNumber).join('') @@ -66,7 +64,7 @@ const productivityStatsForProvider = async (providercode, since) => { const formatGraduatedStudyright = ({ studyrightid, enddate, studystartdate }) => { const year = enddate && enddate.getFullYear() - const timeToGraduation = moment(enddate).diff(moment(studystartdate), 'days') + const timeToGraduation = moment(enddate).diff(moment(studystartdate), 'months') return { studyrightid, year, timeToGraduation } } @@ -94,10 +92,32 @@ const findGraduated = (studytrack, since) => Studyright.findAll({ const graduatedStatsFromStudyrights = studyrights => { const stats = {} - studyrights.forEach(({ year }) => { - const graduated = stats[year] || 0 - stats[year] = graduated + 1 + let graduationTimes = [] + studyrights.forEach(({ year, timeToGraduation }) => { + const graduated = stats[year] ? stats[year].graduated : 0 + stats[year] = { + graduated: graduated + 1, + timesToGraduation: stats[year] ? + [...stats[year].timesToGraduation, timeToGraduation || 0] : [ timeToGraduation || 0] + } + graduationTimes = [...graduationTimes, timeToGraduation || 0] + }) + const median = (values) => { + if (values.length === 0) return 0 + + values.sort((a, b) => a - b) + + var half = Math.floor(values.length / 2) + + if (values.length % 2) + return values[half] + + return (values[half - 1] + values[half]) / 2.0 + } + Object.keys(stats).forEach(year => { + stats[year].medianGraduationTime = median(stats[year].timesToGraduation) }) + stats['medianGraduationTime'] = median(graduationTimes) return stats } @@ -153,10 +173,10 @@ const thesisProductivityForStudytrack = async code => { const combineStatistics = (creditStats, studyrightStats, thesisStats) => { const stats = { ...creditStats } - console.log(studyrightStats) Object.keys(stats).forEach(year => { const thesis = thesisStats[year] || {} - stats[year].graduated = studyrightStats[year] || 0 + stats[year].graduated = studyrightStats[year] ? studyrightStats[year].graduated : 0 + stats[year].medianGraduationTime = studyrightStats[year] ? studyrightStats[year].medianGraduationTime : 0 stats[year].bThesis = thesis.bThesis || 0 stats[year].mThesis = thesis.mThesis || 0 }) @@ -327,7 +347,8 @@ const productivityStats = async (studentnumbers, startDate, studytrack, endDate) thesesFromClass(studentnumbers, startDate, studytrack), gendersFromClass(studentnumbers), countriesFromClass(studentnumbers), - tranferredToStudyprogram(studentnumbers, startDate, studytrack, endDate)]) + tranferredToStudyprogram(studentnumbers, startDate, studytrack, endDate) + ]) } const getYears = (since) => { @@ -357,7 +378,8 @@ const throughputStatsForStudytrack = async (studytrack, since) => { transferred: 0 } const years = getYears(since) - const graduationTimeLimit = studytrack[0] === 'K' ? 36 : 24 // studyprogramme starts with K if bachelors and M if masters + // studyprogramme starts with K if bachelors and M if masters + const graduationTimeLimit = studytrack[0] === 'K' ? 36 : 24 const arr = await Promise.all(years.map(async year => { const startDate = `${year}-${semesterStart['FALL']}` @@ -367,7 +389,7 @@ const throughputStatsForStudytrack = async (studytrack, since) => { await productivityStats(studentnumbers, startDate, studytrack, endDate) delete genders[null] delete countries[null] - + const creditValues = credits.reduce((acc, curr) => { acc.mte30 = curr >= 30 ? acc.mte30 + 1 : acc.mte30 acc.mte60 = curr >= 60 ? acc.mte60 + 1 : acc.mte60 @@ -402,7 +424,7 @@ const throughputStatsForStudytrack = async (studytrack, since) => { year: `${year}-${year + 1}`, credits: credits.map(cr => cr === null ? 0 : cr), graduated: graduated.length, - inTargetTime, + inTargetTime, thesisM: theses.MASTER || 0, thesisB: theses.BACHELOR || 0, genders, diff --git a/services/backend/oodikone2-backend/src/services/studytrack.test.js b/services/backend/oodikone2-backend/src/services/studytrack.test.js index 92c514e3e2..0650f0196b 100644 --- a/services/backend/oodikone2-backend/src/services/studytrack.test.js +++ b/services/backend/oodikone2-backend/src/services/studytrack.test.js @@ -125,8 +125,8 @@ test('graduatedStatsFromStudyrights calculates stats correctly', () => { ] const stats = graduatedStatsFromStudyrights(studyrights) expect(stats).toMatchObject({ - 2015: 2, - 2014: 1 + 2015: { graduated: 2., medianGraduationTime: 0, timesToGraduation: [ 0, 0 ]}, + 2014: { graduated: 1., medianGraduationTime: 0, timesToGraduation: [ 0 ]} }) }) @@ -137,8 +137,8 @@ test('combineStatistics returns correctly formatted array', () => { 2014: { year: 2014, credits: 20 } } const studyrightStats = { - 2015: 2, - 2016: 1 + 2015: { graduated: 2, medianGraduationTime: 1.5, timesToGraduation: [1, 2] }, + 2016: { graduated: 1, medianGraduationTime: 0, timesToGraduation: [ 0 ] } } const thesisStats = { 2014: { mThesis: 1 }, @@ -150,6 +150,7 @@ test('combineStatistics returns correctly formatted array', () => { mThesis: 2, bThesis: 1, credits: 40, + medianGraduationTime: 1.5, graduated: 2 }) expect(stats).toContainEqual({ @@ -157,14 +158,16 @@ test('combineStatistics returns correctly formatted array', () => { mThesis: 1, bThesis: 0, credits: 20, - graduated: 0 + graduated: 0, + medianGraduationTime: 0 }) expect(stats).toContainEqual({ year: 2016, mThesis: 0, bThesis: 0, credits: 5, - graduated: 1 + graduated: 1, + medianGraduationTime: 0 }) }) @@ -175,14 +178,16 @@ test('productivityStatsForStudytrack integrates', async () => { graduated: 0, bThesis: 0, mThesis: 1, - credits: 40 + credits: 40, + medianGraduationTime: 0 }) expect(stats.data).toContainEqual({ year: 2016, graduated: 1, mThesis: 0, bThesis: 0, - credits: 5 + credits: 5, + medianGraduationTime: 0 }) }) diff --git a/services/backend/oodikone2-backend/src/services/topteachers.js b/services/backend/oodikone2-backend/src/services/topteachers.js index 54820b0543..121bc91f50 100644 --- a/services/backend/oodikone2-backend/src/services/topteachers.js +++ b/services/backend/oodikone2-backend/src/services/topteachers.js @@ -1,5 +1,5 @@ const { redisClient } = require('./redis') -const { getSemestersAndYears } = require('./semesters') +const { getSemestersAndYears, getMaxYearcode } = require('./semesters') const { Teacher, Semester, Credit, Course } = require('../models/index') const { Op } = require('sequelize') @@ -126,6 +126,13 @@ const findTopTeachers = async (yearcode) => { } } +const findAndSaveTeachers = async (startcode = 50, to) => { + const endcode = to ? to : await getMaxYearcode() + for (let code = startcode; code <= endcode; code++) { + await findAndSaveTopTeachers(code) + } +} + const findAndSaveTopTeachers = async yearcode => { const { all, openuni } = await findTopTeachers(yearcode) await setTeacherStats(ID.OPENUNI, yearcode, openuni) @@ -138,6 +145,7 @@ module.exports = { setTeacherStats, getCategoriesAndYears, findTopTeachers, + findAndSaveTeachers, findAndSaveTopTeachers, ID } \ No newline at end of file diff --git a/services/backend/oodikone2-backend/test/jest/globals.js b/services/backend/oodikone2-backend/test/jest/globals.js index 0dfeaf87b3..8048e88337 100644 --- a/services/backend/oodikone2-backend/test/jest/globals.js +++ b/services/backend/oodikone2-backend/test/jest/globals.js @@ -1,11 +1,11 @@ const { sequelize } = require('../../src/models/index') const { redisClient } = require('../../src/services/redis') const { forceSyncDatabase } = require('../../src/database/connection') -const { seedMigrations } = require('../../src/database/seed_migrations') +const { seedAllMigrations } = require('../../src/database/seed_migrations') beforeAll(async () => { await forceSyncDatabase() - await seedMigrations() + await seedAllMigrations() }) afterAll(async () => { diff --git a/services/backend/shared/models/index.js b/services/backend/shared/models/index.js index d46cbba956..012dc67a18 100644 --- a/services/backend/shared/models/index.js +++ b/services/backend/shared/models/index.js @@ -1,5 +1,5 @@ const Sequelize = require('sequelize') -const { sequelize, migrationPromise } = require('../database/connection') +const { sequelize, sequelizeKone, migrationPromise } = require('../database/connection') const conf = require('../conf-backend') const ThesisTypeEnums = { @@ -135,13 +135,13 @@ const Tag = sequelize.define('tag', } ) -const MigrationKone = sequelize.define('migrations', { +const MigrationKone = sequelize.define('migrationsKone', { name: { type: Sequelize.STRING, primaryKey: true } }, { - tablename: 'migrations', + tableName: 'migrations', timestamps: false, schema: conf.DB_SCHEMA_KONE }) @@ -303,7 +303,7 @@ const StudyrightElement = sequelize.define('studyright_elements', fields: ['startdate'] } ], - tablename: 'studyright_elements' + tableName: 'studyright_elements' } ) @@ -317,7 +317,7 @@ const ElementDetails = sequelize.define('element_details', type: { type: Sequelize.INTEGER } }, { - tablename: 'element_details' + tableName: 'element_details' } ) @@ -549,7 +549,7 @@ const Migration = sequelize.define('migrations', { primaryKey: true } }, { - tablename: 'migrations', + tableName: 'migrations', timestamps: false }) @@ -695,6 +695,7 @@ module.exports = { Tag, Teacher, sequelize, + sequelizeKone, migrationPromise, Organisation, StudentList, diff --git a/services/oodikone2-frontend/src/components/SortableTable/index.jsx b/services/oodikone2-frontend/src/components/SortableTable/index.jsx index 131433269b..315f2552cc 100644 --- a/services/oodikone2-frontend/src/components/SortableTable/index.jsx +++ b/services/oodikone2-frontend/src/components/SortableTable/index.jsx @@ -10,8 +10,8 @@ const DIRECTIONS = { class SortableTable extends Component { state={ - direction: DIRECTIONS.ASC, - selected: this.props.columns[0].key + direction: this.props.defaultdescending ? DIRECTIONS.DESC : DIRECTIONS.ASC, + selected: this.props.defaultsortkey == null ? this.props.columns[0].key : this.props.defaultsortkey } handleSort = column => () => { @@ -107,12 +107,16 @@ SortableTable.propTypes = { group: bool, children: arrayOf() })).isRequired, - data: arrayOf(shape({})).isRequired + data: arrayOf(shape({})).isRequired, + defaultdescending: bool, + defaultsortkey: string } SortableTable.defaultProps = { tableProps: undefined, - getRowProps: undefined + getRowProps: undefined, + defaultdescending: false, + defaultsortkey: null } export default SortableTable diff --git a/services/oodikone2-frontend/src/components/StudentSearch/index.jsx b/services/oodikone2-frontend/src/components/StudentSearch/index.jsx index 32f53f56cf..e6d61b7087 100644 --- a/services/oodikone2-frontend/src/components/StudentSearch/index.jsx +++ b/services/oodikone2-frontend/src/components/StudentSearch/index.jsx @@ -1,8 +1,8 @@ -import React, { Component } from 'react' +import React, { Component, Fragment } from 'react' import { withRouter } from 'react-router-dom' import { func, string, arrayOf, object, bool, shape } from 'prop-types' import { connect } from 'react-redux' -import { Search, Segment, Icon } from 'semantic-ui-react' +import { Search, Segment, Container } from 'semantic-ui-react' import { findStudents, getStudent, selectStudent } from '../../redux/students' import SegmentDimmer from '../SegmentDimmer' @@ -10,7 +10,6 @@ import SortableTable from '../SortableTable' import Timeout from '../Timeout' import { makeFormatStudentRows } from '../../selectors/students' -import './studentSearch.css' import { containsOnlyNumbers } from '../../common' const DEFAULT_STATE = { @@ -89,25 +88,24 @@ class StudentSearch extends Component { } const columns = [ - { key: 'studentnumber', title: translate('common.studentNumber'), getRowVal: s => s.studentNumber, headerProps: { onClick: null, sorted: null } }, - { key: 'started', title: translate('common.started'), getRowVal: s => s.started, headerProps: { onClick: null, sorted: null } }, - { key: 'credits', title: translate('common.credits'), getRowVal: s => s.credits, headerProps: { onClick: null, sorted: null, colSpan: showNames ? 1 : 2 } } + { key: 'studentnumber', title: translate('common.studentNumber'), getRowVal: s => s.studentNumber }, + { key: 'started', title: translate('common.started'), getRowVal: s => s.started }, + { key: 'credits', title: translate('common.credits'), getRowVal: s => s.credits } ] if (showNames) { - columns.push({ key: 'lastnames', title: 'last names', getRowVal: s => s.lastname, headerProps: { onClick: null, sorted: null } }) - columns.push({ key: 'firstnames', title: 'first names', getRowVal: s => s.firstnames, headerProps: { onClick: null, sorted: null, colSpan: showNames ? 2 : 1 } }) + columns.push({ key: 'lastnames', title: 'last names', getRowVal: s => s.lastname }) + columns.push({ key: 'firstnames', title: 'first names', getRowVal: s => s.firstnames }) } - columns.push({ key: 'icon', getRowVal: () => (), cellProps: { collapsing: true }, headerProps: { onClick: null, sorted: null } }) return ( s.studentNumber} getRowProps={s => ({ - className: 'clickable', + style: { cursor: 'pointer' }, onClick: () => this.handleSearchSelect(s) })} - tableProps={{ celled: false, sortable: false }} + tableProps={{ celled: false }} columns={columns} data={students} /> @@ -124,21 +122,22 @@ class StudentSearch extends Component { const { isLoading, searchStr } = this.state return ( -
- - + + + + + {this.renderSearchResults()} -
+ ) } } diff --git a/services/oodikone2-frontend/src/components/StudentSearch/studentSearch.css b/services/oodikone2-frontend/src/components/StudentSearch/studentSearch.css deleted file mode 100644 index 62fba9b51e..0000000000 --- a/services/oodikone2-frontend/src/components/StudentSearch/studentSearch.css +++ /dev/null @@ -1,19 +0,0 @@ -.searchContainer { - align-items: center; - display: flex; - flex-direction: column; - justify-content: center; -} - -.studentSearch { - width: 100%; - max-width: 750px; -} - -.searchResultTitle { - border-bottom: 1px solid var(--darkSilver); -} - -.clickable { - cursor: pointer; -} diff --git a/services/oodikone2-frontend/src/components/StudyProgramme/ProductivityTable/index.jsx b/services/oodikone2-frontend/src/components/StudyProgramme/ProductivityTable/index.jsx index aa7ddac30f..60d396ea54 100644 --- a/services/oodikone2-frontend/src/components/StudyProgramme/ProductivityTable/index.jsx +++ b/services/oodikone2-frontend/src/components/StudyProgramme/ProductivityTable/index.jsx @@ -12,12 +12,13 @@ const ProductivityTable = ({ productivity, thesis, loading, error, studyprogramm if (thesis) { thesisTypes = thesis.map(t => t.thesisType) } - const headerList = ['Year', 'Credits', thesisTypes.includes('MASTER') && 'Masters Thesis', thesisTypes.includes('BACHELOR') && 'Bachelors Thesis', 'Graduated'].filter(_ => _) + const headerList = ['Year', 'Credits', thesisTypes.includes('MASTER') && 'Masters Thesis', thesisTypes.includes('BACHELOR') && 'Bachelors Thesis', 'Graduated', 'Graduation median time'].filter(_ => _) const refresh = () => { callApi('/v2/studyprogrammes/productivity/recalculate', 'get', null, { code: studyprogramme }) .then(() => { dispatchGetProductivity(studyprogramme) }) } + console.log(productivity) return (
@@ -69,6 +70,7 @@ const ProductivityTable = ({ productivity, thesis, loading, error, studyprogramm {year.mThesis} )} {year.graduated} + {year.medianGraduationTime} months )) : null} diff --git a/services/oodikone2-frontend/src/components/StudyProgramme/StudyProgrammeSelector/index.jsx b/services/oodikone2-frontend/src/components/StudyProgramme/StudyProgrammeSelector/index.jsx index 0c41b89b9c..919150de19 100644 --- a/services/oodikone2-frontend/src/components/StudyProgramme/StudyProgrammeSelector/index.jsx +++ b/services/oodikone2-frontend/src/components/StudyProgramme/StudyProgrammeSelector/index.jsx @@ -1,23 +1,22 @@ import React, { Component } from 'react' import { connect } from 'react-redux' -import { arrayOf, func, string, bool } from 'prop-types' -import { sortBy } from 'lodash' -import { Loader } from 'semantic-ui-react' +import { arrayOf, func, string, bool, shape } from 'prop-types' +import { Loader, Message } from 'semantic-ui-react' import { getDegreesAndProgrammes } from '../../../redux/populationDegreesAndProgrammes' -import Table from '../../SearchResultTable' import { getTextIn } from '../../../common' - -const headers = [ - 'name', - 'code' -] +import SortableTable from '../../SortableTable' class StudyProgrammeSelector extends Component { static propTypes = { getDegreesAndProgrammes: func.isRequired, handleSelect: func.isRequired, - studyprogrammes: arrayOf(arrayOf(string)), // eslint-disable-line - selected: bool.isRequired + studyprogrammes: arrayOf(shape({ name: shape({}), code: string })), + selected: bool.isRequired, + language: string.isRequired + } + + static defaultProps = { + studyprogrammes: null } componentDidMount() { @@ -27,16 +26,32 @@ class StudyProgrammeSelector extends Component { } render() { - const { studyprogrammes, selected } = this.props + const { studyprogrammes, selected, language } = this.props if (selected) return null if (!studyprogrammes) return Loading + + const headers = [ + { + key: 'name', + title: 'name', + getRowContent: prog => getTextIn(prog.name, language) + }, + { + key: 'code', + title: 'code', + getRowContent: prog => prog.code + } + ] + if (studyprogrammes == null) { + return You do not have access to any programmes + } return ( - this.props.handleSelect(row)} - selectable - noResultText="No study programmes" + programme.code} + getRowProps={programme => ({ onClick: () => this.props.handleSelect(programme.code), style: { cursor: 'pointer' } })} + data={studyprogrammes} + defaultdescending /> ) } @@ -47,9 +62,9 @@ const mapStateToProps = ({ populationDegreesAndProgrammes, settings }) => { const { language } = settings return { - studyprogrammes: programmes ? sortBy(Object.values(programmes).filter(programme => - programme.code.includes('_')) - .map(programme => [getTextIn(programme.name, language), programme.code]), '0') : null + studyprogrammes: programmes ? Object.values(programmes).filter(programme => + programme.code.includes('_')) : null, + language } } diff --git a/services/oodikone2-frontend/src/components/StudyProgramme/index.jsx b/services/oodikone2-frontend/src/components/StudyProgramme/index.jsx index a5f433d98d..621de665e5 100644 --- a/services/oodikone2-frontend/src/components/StudyProgramme/index.jsx +++ b/services/oodikone2-frontend/src/components/StudyProgramme/index.jsx @@ -66,8 +66,8 @@ class StudyProgramme extends Component { return panes } - handleSelect = (target) => { - this.props.history.push(`/study-programme/${target[1]}`, { selected: target[1] }) + handleSelect = (programme) => { + this.props.history.push(`/study-programme/${programme}`, { selected: programme }) } select = (e, { activeIndex }) => { diff --git a/services/oodikone2-frontend/src/components/TeacherLeaderBoard/LeaderForm.jsx b/services/oodikone2-frontend/src/components/TeacherLeaderBoard/LeaderForm.jsx index 33d0738d04..9bd87fbb45 100644 --- a/services/oodikone2-frontend/src/components/TeacherLeaderBoard/LeaderForm.jsx +++ b/services/oodikone2-frontend/src/components/TeacherLeaderBoard/LeaderForm.jsx @@ -1,31 +1,20 @@ import React, { Component } from 'react' import { Segment, Form } from 'semantic-ui-react' import { connect } from 'react-redux' -import { func, arrayOf, shape, string, any } from 'prop-types' +import { func, arrayOf, shape, string, any, number } from 'prop-types' import { getTopTeachers } from '../../redux/teachersTop' class LeaderForm extends Component { - state={ - selectedyear: null, - selectedcategory: null - } - componentDidMount() { const { year, category } = this.defaultValues() if (year && category) { - this.updateAndSubmitForm({ + this.props.updateAndSubmitForm({ selectedyear: year, selectedcategory: category }) } } - updateAndSubmitForm = (args) => { - this.setState(args) - const { selectedyear, selectedcategory } = { ...this.state, ...args } - this.props.getTopTeachers(selectedyear, selectedcategory) - } - defaultValues = () => { const { yearoptions, categoryoptions } = this.props const [defaultyear = {}] = yearoptions @@ -36,8 +25,6 @@ class LeaderForm extends Component { } } - handleChange = (e, { value, name }) => this.updateAndSubmitForm({ [name]: value }) - render() { const { yearoptions, categoryoptions } = this.props return ( @@ -51,8 +38,8 @@ class LeaderForm extends Component { options={yearoptions} selection search - value={this.state.selectedyear} - onChange={this.handleChange} + value={this.props.selectedyear} + onChange={this.props.handleChange} /> @@ -74,7 +61,10 @@ class LeaderForm extends Component { LeaderForm.propTypes = { yearoptions: arrayOf(shape({})).isRequired, categoryoptions: arrayOf(shape({ key: any, value: any, text: string })).isRequired, - getTopTeachers: func.isRequired + updateAndSubmitForm: func.isRequired, + handleChange: func.isRequired, + selectedcategory: string, // eslint-disable-line + selectedyear: number // eslint-disable-line } export default connect(null, { getTopTeachers })(LeaderForm) diff --git a/services/oodikone2-frontend/src/components/TeacherLeaderBoard/index.jsx b/services/oodikone2-frontend/src/components/TeacherLeaderBoard/index.jsx index f1a96376ba..a912db46cc 100644 --- a/services/oodikone2-frontend/src/components/TeacherLeaderBoard/index.jsx +++ b/services/oodikone2-frontend/src/components/TeacherLeaderBoard/index.jsx @@ -1,45 +1,102 @@ import React, { Component } from 'react' -import { Segment, Message } from 'semantic-ui-react' +import { Segment, Message, Button, Popup } from 'semantic-ui-react' import { withRouter } from 'react-router-dom' import { connect } from 'react-redux' import { func, arrayOf, bool, shape, any, string } from 'prop-types' import { getTopTeachersCategories } from '../../redux/teachersTopCategories' +import { getTopTeachers } from '../../redux/teachersTop' import TeacherStatisticsTable from '../TeacherStatisticsTable' import LeaderForm from './LeaderForm' +import { callApi } from '../../apiConnection' class TeacherLeaderBoard extends Component { - state={} + state = { + selectedyear: null, + selectedcategory: null, + recalculating: false, + isOpen: false + } + + componentDidMount() { + this.props.getTopTeachersCategories() + } + + handleOpen = () => { + this.setState({ isOpen: true, recalculating: true }) + + this.timeout = setTimeout(() => { + this.setState({ isOpen: false }) + }, 5000) + } + + handleClose = () => { + this.setState({ isOpen: false }) + clearTimeout(this.timeout) + } + + refresh = () => { + const { selectedyear } = this.state + callApi('/teachers/top', 'post', { startyearcode: selectedyear, endyearcode: selectedyear + 1 }, null) + } - componentDidMount() { - this.props.getTopTeachersCategories() - } + updateAndSubmitForm = (args) => { + this.setState({ recalculating: false, ...args }) + const { selectedyear, selectedcategory } = { ...this.state, ...args } + this.props.getTopTeachers(selectedyear, selectedcategory) + } - render() { - const { statistics, updated, isLoading, yearoptions, categoryoptions } = this.props - return ( -
- { isLoading - ? - : ( -
- - - - - this.props.history.push(`/teachers/${e.target.innerText}`)} - /> - -
- ) - } -
- ) - } + handleChange = (e, { value, name }) => this.updateAndSubmitForm({ [name]: value }) + + render() { + const { statistics, updated, isLoading, yearoptions, categoryoptions } = this.props + const { selectedcategory, selectedyear, recalculating } = this.state + return ( +
+ {isLoading + ? + : ( +
+ + Teacher leaderboard + Teachers who have produced the most credits from all departments. + + + + + {`Last updated: ${updated}`} + + { this.refresh() }} + />} + content="Recalculation started. Recalculation might take multiple minutes. Refresh page to see the results" + on="click" + open={this.state.isOpen} + onClose={this.handleClose} + onOpen={this.handleOpen} + /> + + this.props.history.push(`/teachers/${e.target.innerText}`)} + /> + +
+ ) + } +
+ ) + } } TeacherLeaderBoard.propTypes = { @@ -48,6 +105,7 @@ TeacherLeaderBoard.propTypes = { yearoptions: arrayOf(shape({})).isRequired, history: shape({}).isRequired, updated: string.isRequired, + getTopTeachers: func.isRequired, getTopTeachersCategories: func.isRequired, categoryoptions: arrayOf(shape({ key: any, text: string, value: any })).isRequired } @@ -76,5 +134,6 @@ const mapStateToProps = ({ teachersTop, teachersTopCategories }) => { } export default connect(mapStateToProps, { - getTopTeachersCategories + getTopTeachersCategories, + getTopTeachers })(withRouter(TeacherLeaderBoard))