diff --git a/services/backend/oodikone2-backend/src/services/course_stats_counter.js b/services/backend/oodikone2-backend/src/services/course_stats_counter.js index a29296c0ed..f85b43e5ca 100644 --- a/services/backend/oodikone2-backend/src/services/course_stats_counter.js +++ b/services/backend/oodikone2-backend/src/services/course_stats_counter.js @@ -76,8 +76,6 @@ class CourseStatsCounter { if (passed) { if (!improved && !this.students.passed[studentnumber]) { this.markPassedSemester(semester) - } else if (studentnumber === '012327001') { - this.markPassedSemester(semester) } this.markPassingGrade(studentnumber) } else if (improved) { diff --git a/services/oodikone2-frontend/src/common/index.js b/services/oodikone2-frontend/src/common/index.js index a802a18bf7..0ab36755f5 100644 --- a/services/oodikone2-frontend/src/common/index.js +++ b/services/oodikone2-frontend/src/common/index.js @@ -1,8 +1,10 @@ +import { useState, useEffect } from 'react' import moment from 'moment' import jwtDecode from 'jwt-decode' import Datetime from 'react-datetime' import { uniqBy, filter } from 'lodash' import pathToRegexp from 'path-to-regexp' +import qs from 'query-string' import { API_DATE_FORMAT, DISPLAY_DATE_FORMAT, TOKEN_NAME } from '../constants' import toskaLogo from '../assets/toska.png' import irtomikko from '../assets/irtomikko.png' @@ -213,3 +215,38 @@ export const getTextIn = (texts, language) => { } return null } + +export const useTabs = (id, initialTab, { location, replace }) => { + const [tab, setTab] = useState(null) + const [didMount, setDidMount] = useState(false) + + const pushToUrl = (newTab) => { + replace({ + pathname: location.pathname, + search: qs.stringify({ ...qs.parse(location.search), [id]: newTab }) + }) + } + + useEffect(() => { + const params = qs.parse(location.search) + let queryTab = params[id] + if (queryTab === undefined) { + setTab(initialTab) + } else { + queryTab = JSON.parse(queryTab) + if (tab !== queryTab) setTab(queryTab) + } + setDidMount(true) + }, []) + + useEffect(() => { + if (tab !== undefined && didMount) pushToUrl(tab) + }, [tab]) + + return [ + tab, + (e, { activeIndex }) => { + setTab(activeIndex) + } + ] +} diff --git a/services/oodikone2-frontend/src/components/CourseStatistics/ResultTabs/index.jsx b/services/oodikone2-frontend/src/components/CourseStatistics/ResultTabs/index.jsx index ab16318d6f..47af492b2d 100644 --- a/services/oodikone2-frontend/src/components/CourseStatistics/ResultTabs/index.jsx +++ b/services/oodikone2-frontend/src/components/CourseStatistics/ResultTabs/index.jsx @@ -1,9 +1,12 @@ -import React, { Component } from 'react' +import React, { useState } from 'react' import { Tab, Grid, Radio, Menu } from 'semantic-ui-react' +import { withRouter } from 'react-router-dom' +import { shape } from 'prop-types' import { dataSeriesType, viewModeNames } from './Panes/util' import PassRate from './Panes/passRate' import Distribution from './Panes/distribution' import Tables from './Panes/tables' +import { useTabs } from '../../../common' import './resultTabs.css' @@ -13,84 +16,29 @@ const paneViewIndex = { GRADE_DISTRIBUTION: 2 } -class ResultTabs extends Component { - state = { - activeIndex: paneViewIndex.TABLE, - viewMode: viewModeNames.CUMULATIVE, - isRelative: false - } - - getPanes = () => { - const { primary, comparison } = this.props - const { viewMode, isRelative } = this.state - - const paneMenuItems = [ - { - menuItem: { key: 'Table', icon: 'table', content: 'Table' }, - renderFn: () => - () - }, - { - menuItem: { key: 'pass', icon: 'balance', content: 'Pass rate chart' }, - renderFn: () => - () - }, - { - menuItem: { key: 'grade', icon: 'chart bar', content: 'Grade distribution chart' }, - renderFn: () => - () - } - ] - - return paneMenuItems.map((p) => { - const { menuItem, renderFn } = p - return { - menuItem, - render: () => ( - - - {this.renderViewModeSelector()} - - {renderFn()} - - ) - } - }) - } - - handleTabChange = (e, { activeIndex }) => { - const { viewMode, activeIndex: oldIndex } = this.state - const resetViewMode = oldIndex === paneViewIndex.TABLE +const ResultTabs = (props) => { + const [tab, setTab] = useTabs( + 'cs_tab', + 0, + props.history + ) + const [viewMode, setViewMode] = useState(viewModeNames.CUMULATIVE) + const [isRelative, setIsRelative] = useState(false) + + const handleTabChange = (...params) => { + const resetViewMode = params[1].activeIndex === paneViewIndex.TABLE && viewMode === viewModeNames.GRADES - this.setState({ - activeIndex, - viewMode: resetViewMode ? viewModeNames.CUMULATIVE : viewMode - }) + setTab(...params) + setViewMode(resetViewMode ? viewModeNames.CUMULATIVE : viewMode) } - handleModeChange = (viewMode) => { - this.setState({ viewMode }) + const handleModeChange = (newViewMode) => { + setViewMode(newViewMode) } - renderViewModeSelector = () => { - const { activeIndex, viewMode } = this.state - - const isTogglePane = activeIndex !== 0 + const renderViewModeSelector = () => { + const isTogglePane = tab !== 0 const getButtonMenu = () => ( @@ -99,7 +47,7 @@ class ResultTabs extends Component { key={name} name={name} active={viewMode === name} - onClick={() => this.handleModeChange(name)} + onClick={() => handleModeChange(name)} />))} ) @@ -116,17 +64,17 @@ class ResultTabs extends Component { id={toggleId} checked={isToggleChecked} toggle - onChange={() => this.handleModeChange(newMode)} + onChange={() => handleModeChange(newMode)} /> - {this.props.comparison && + {props.comparison &&
this.setState({ isRelative: !this.state.isRelative })} + checked={isRelative} + onChange={() => setIsRelative(!isRelative)} />
@@ -142,24 +90,76 @@ class ResultTabs extends Component { ) } - render() { - return ( -
- -
) + const getPanes = () => { + const { primary, comparison } = props + + const paneMenuItems = [ + { + menuItem: { key: 'Table', icon: 'table', content: 'Table' }, + renderFn: () => + () + }, + { + menuItem: { key: 'pass', icon: 'balance', content: 'Pass rate chart' }, + renderFn: () => + () + }, + { + menuItem: { key: 'grade', icon: 'chart bar', content: 'Grade distribution chart' }, + renderFn: () => + () + } + ] + + return paneMenuItems.map((p) => { + const { menuItem, renderFn } = p + return { + menuItem, + render: () => ( + + + {renderViewModeSelector()} + + {renderFn()} + + ) + } + }) } + + return ( +
+ +
+ ) } ResultTabs.propTypes = { primary: dataSeriesType.isRequired, - comparison: dataSeriesType + comparison: dataSeriesType, + history: shape({}).isRequired } ResultTabs.defaultProps = { comparison: undefined } -export default ResultTabs +export default withRouter(ResultTabs) diff --git a/services/oodikone2-frontend/src/components/PopulationCourseStats/index.jsx b/services/oodikone2-frontend/src/components/PopulationCourseStats/index.jsx index 140f7f887d..bb949c6461 100644 --- a/services/oodikone2-frontend/src/components/PopulationCourseStats/index.jsx +++ b/services/oodikone2-frontend/src/components/PopulationCourseStats/index.jsx @@ -275,7 +275,7 @@ class PopulationCourseStats extends Component { reversed={reversed} /> - {courseGradesTypes.map(g => )} + {courseGradesTypes.map(g => )} diff --git a/services/oodikone2-frontend/src/components/StudyProgramme/StudyProgrammeMandatoryCourses/index.jsx b/services/oodikone2-frontend/src/components/StudyProgramme/StudyProgrammeMandatoryCourses/index.jsx index 5fea8c9bb8..2510f7cfc1 100644 --- a/services/oodikone2-frontend/src/components/StudyProgramme/StudyProgrammeMandatoryCourses/index.jsx +++ b/services/oodikone2-frontend/src/components/StudyProgramme/StudyProgrammeMandatoryCourses/index.jsx @@ -1,89 +1,104 @@ -import React, { Component } from 'react' +import React, { useEffect } from 'react' import { connect } from 'react-redux' import { func, shape, string } from 'prop-types' import { Message, Tab } from 'semantic-ui-react' +import { withRouter } from 'react-router-dom' import { - getMandatoryCourses, - addMandatoryCourse, - deleteMandatoryCourse, - setMandatoryCourseLabel + getMandatoryCourses as getMandatoryCoursesAction, + addMandatoryCourse as addMandatoryCourseAction, + deleteMandatoryCourse as deleteMandatoryCourseAction, + setMandatoryCourseLabel as setMandatoryCourseLabelAction } from '../../../redux/populationMandatoryCourses' import MandatoryCourseTable from '../MandatoryCourseTable' import AddMandatoryCourses from '../AddMandatoryCourses' import MandatoryCourseLabels from '../MandatoryCourseLabels' +import { useTabs } from '../../../common' -class StudyProgrammeMandatoryCourses extends Component { - static propTypes = { - getMandatoryCourses: func.isRequired, - addMandatoryCourse: func.isRequired, - deleteMandatoryCourse: func.isRequired, - setMandatoryCourseLabel: func.isRequired, - studyProgramme: string.isRequired, - mandatoryCourses: shape({}).isRequired, - language: string.isRequired - } +const StudyProgrammeMandatoryCourses = (props) => { + const { + getMandatoryCourses, + addMandatoryCourse, + deleteMandatoryCourse, + setMandatoryCourseLabel, + studyProgramme, + mandatoryCourses, + language, + history + } = props + const [tab, setTab] = useTabs( + 'p_m_tab', + 0, + history + ) - componentDidMount() { - const { studyProgramme } = this.props + useEffect(() => { if (studyProgramme) { - this.props.getMandatoryCourses(studyProgramme) + getMandatoryCourses(studyProgramme) } - } + }, [studyProgramme]) - componentDidUpdate(prevProps) { - const { studyProgramme } = this.props - if (studyProgramme !== prevProps.studyProgramme) { - this.props.getMandatoryCourses(this.props.studyProgramme) + if (!studyProgramme) return null + + const panes = [ + { + menuItem: 'Mandatory courses', + render: () => ( + + + + + ) + }, + { + menuItem: 'Group labels', + render: () => ( + + + + ) } - } + ] - render() { - const { studyProgramme, mandatoryCourses, language } = this.props - if (!studyProgramme) return null + return ( + + + + + ) +} - const panes = [ - { - menuItem: 'Mandatory courses', - render: () => ( - - - - - ) - }, - { - menuItem: 'Group labels', - render: () => ( - - - - ) - } - ] +StudyProgrammeMandatoryCourses.propTypes = { + getMandatoryCourses: func.isRequired, + addMandatoryCourse: func.isRequired, + deleteMandatoryCourse: func.isRequired, + setMandatoryCourseLabel: func.isRequired, + studyProgramme: string.isRequired, + mandatoryCourses: shape({}).isRequired, + language: string.isRequired, + history: shape({}).isRequired +} - return ( - - - - - ) - } +const mapDispatchToProps = { + getMandatoryCourses: getMandatoryCoursesAction, + addMandatoryCourse: addMandatoryCourseAction, + deleteMandatoryCourse: deleteMandatoryCourseAction, + setMandatoryCourseLabel: setMandatoryCourseLabelAction } export default connect( @@ -91,5 +106,5 @@ export default connect( mandatoryCourses: populationMandatoryCourses, language: settings.language }), - { getMandatoryCourses, addMandatoryCourse, deleteMandatoryCourse, setMandatoryCourseLabel } -)(StudyProgrammeMandatoryCourses) + mapDispatchToProps +)(withRouter(StudyProgrammeMandatoryCourses)) diff --git a/services/oodikone2-frontend/src/components/StudyProgramme/index.jsx b/services/oodikone2-frontend/src/components/StudyProgramme/index.jsx index 9a2c401c90..c7107de56c 100644 --- a/services/oodikone2-frontend/src/components/StudyProgramme/index.jsx +++ b/services/oodikone2-frontend/src/components/StudyProgramme/index.jsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React from 'react' import { withRouter } from 'react-router-dom' import { connect } from 'react-redux' import { shape, string } from 'prop-types' @@ -10,35 +10,17 @@ import Overview from './Overview' import AggregateView from '../CourseGroups/AggregateView' import ThesisCourses from './ThesisCourses' import '../PopulationQueryCard/populationQueryCard.css' -import { getRolesWithoutRefreshToken, getRightsWithoutRefreshToken, getTextIn } from '../../common' +import { getRolesWithoutRefreshToken, getRightsWithoutRefreshToken, getTextIn, useTabs } from '../../common' import Tags from './Tags' -class StudyProgramme extends Component { - static propTypes = { - match: shape({ - params: shape({ - studyProgrammeId: string, - courseGroupId: string - }) - }), - programmes: shape({}), - language: string.isRequired, - history: shape({}).isRequired - } - - static defaultProps = { - match: { - params: { studyProgrammeId: undefined } - }, - programmes: {} - } - - state = { - selected: this.props.match.params.courseGroupId ? 2 : 0 - } - - getPanes() { - const { match } = this.props +const StudyProgramme = (props) => { + const [tab, setTab] = useTabs( + 'p_tab', + props.match.params.courseGroupId ? 2 : 0, + props.history + ) + const getPanes = () => { + const { match } = props const { studyProgrammeId, courseGroupId } = match.params const panes = [] panes.push( @@ -73,51 +55,63 @@ class StudyProgramme extends Component { return panes } - handleSelect = (programme) => { - this.props.history.push(`/study-programme/${programme}`, { selected: programme }) + const handleSelect = (programme) => { + props.history.push(`/study-programme/${programme}`, { selected: programme }) } - select = (e, { activeIndex }) => { - this.setState({ selected: activeIndex }) - } + const { match, programmes, language } = props + const { studyProgrammeId } = match.params + const programmeName = programmes[studyProgrammeId] && getTextIn(programmes[studyProgrammeId].name, language) + const panes = getPanes() + return ( +
+
+ Study Programme +
+ + + { + studyProgrammeId ? ( + + + + + {programmeName} + props.history.push('/study-programme')} + /> + + + + + + + ) : null + } + +
+ ) +} - render() { - const { selected } = this.state - const { match, programmes, language } = this.props - const { studyProgrammeId } = match.params - const programmeName = programmes[studyProgrammeId] && getTextIn(programmes[studyProgrammeId].name, language) - const panes = this.getPanes() - return ( -
-
- Study Programme -
- - - { - studyProgrammeId ? ( - - - - - {programmeName} - this.props.history.push('/study-programme')} - /> - - - - - - - ) : null - } - -
- ) - } +StudyProgramme.propTypes = { + match: shape({ + params: shape({ + studyProgrammeId: string, + courseGroupId: string + }) + }), + programmes: shape({}), + language: string.isRequired, + history: shape({}).isRequired +} + +StudyProgramme.defaultProps = { + match: { + params: { studyProgrammeId: undefined } + }, + programmes: {} } const mapStateToProps = ({ populationDegreesAndProgrammes, settings }) => { diff --git a/services/oodikone2-frontend/src/components/Teachers/index.jsx b/services/oodikone2-frontend/src/components/Teachers/index.jsx index 8a47db8673..65a3221627 100644 --- a/services/oodikone2-frontend/src/components/Teachers/index.jsx +++ b/services/oodikone2-frontend/src/components/Teachers/index.jsx @@ -6,7 +6,7 @@ import TeacherSearchTab from '../TeacherSearchTab' import TeacherPage from '../TeacherPage' import TeacherStatistics from '../TeacherStatistics' import TeacherLeaderBoard from '../TeacherLeaderBoard' -import { userRoles } from '../../common' +import { userRoles, useTabs } from '../../common' const pane = (title, Content, icon) => ({ menuItem: { key: title, content: title, icon }, @@ -17,7 +17,12 @@ const pane = (title, Content, icon) => ({ ) }) -const TeachersTabs = ({ admin }) => { +const TeachersTabs = withRouter(({ admin, history }) => { + const [tab, setTab] = useTabs( + 't_tab', + 0, + history + ) const panes = admin ? [ pane('Statistics', TeacherStatistics, 'table'), pane('Leaderboard', TeacherLeaderBoard, 'trophy'), @@ -28,9 +33,12 @@ const TeachersTabs = ({ admin }) => { ) -} +}) + class Teachers extends Component { state = {}