diff --git a/services/backend/oodikone2-backend/src/routes/population.js b/services/backend/oodikone2-backend/src/routes/population.js index 0364145e5d..aa79af32b0 100644 --- a/services/backend/oodikone2-backend/src/routes/population.js +++ b/services/backend/oodikone2-backend/src/routes/population.js @@ -48,7 +48,7 @@ router.get('/v3/populationstatistics', async (req, res) => { return } } - } catch(e) { + } catch (e) { console.error(e) res.status(400).json({ error: 'The query had invalid studyRights' }) return @@ -57,8 +57,7 @@ router.get('/v3/populationstatistics', async (req, res) => { if (req.query.months == null) { req.query.months = 12 } - - const result = await Population.optimizedStatisticsOf({ ...req.query, studyRights}) + const result = await Population.optimizedStatisticsOf({ ...req.query, studyRights }) if (result.error) { console.log(result.error) diff --git a/services/backend/oodikone2-backend/src/services/populations.js b/services/backend/oodikone2-backend/src/services/populations.js index d10416ffac..7a4e33fbe5 100644 --- a/services/backend/oodikone2-backend/src/services/populations.js +++ b/services/backend/oodikone2-backend/src/services/populations.js @@ -107,7 +107,13 @@ const formatStudentForPopulationStatistics = ({ const dateMonthsFromNow = (date, months) => moment(date).add(months, 'months').format('YYYY-MM-DD') -const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDate, studyright) => { +const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDate, studyright, tag) => { + const tagQuery = tag ? { + tag_id: { + [Op.eq]: tag + } + } : null + const creditsOfStudentOther = { student_studentnumber: { [Op.in]: studentnumbers @@ -138,7 +144,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa const creditsOfStudent = ['320001', 'MH30_001'].includes(studyright[0]) ? creditsOfStudentLaakis : creditsOfStudentOther - + const students = await Student.findAll({ attributes: ['firstnames', 'lastname', 'studentnumber', 'dateofuniversityenrollment', 'creditcount', 'matriculationexamination', @@ -216,10 +222,11 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa { model: TagStudent, attributes: ['id'], + where: tagQuery, include: [ { model: Tag, - attributes: ['tag_id','tagname'] + attributes: ['tag_id', 'tagname'], } ], } @@ -301,10 +308,10 @@ const studentnumbersWithAllStudyrightElements = async (studyRights, startDate, e } const parseQueryParams = query => { - const { semesters, studentStatuses, year, studyRights, months } = query + const { semesters, studentStatuses, studyRights, months, year, tagYear } = query const startDate = semesters.includes('FALL') ? - `${year}-${semesterStart[semesters.find(s => s === 'FALL')]}` : - `${moment(year, 'YYYY').add(1, 'years').format('YYYY')}-${semesterStart[semesters.find(s => s === 'SPRING')]}` + `${tagYear}-${semesterStart[semesters.find(s => s === 'FALL')]}` : + `${moment(tagYear, 'YYYY').add(1, 'years').format('YYYY')}-${semesterStart[semesters.find(s => s === 'SPRING')]}` const endDate = semesters.includes('SPRING') ? `${moment(year, 'YYYY').add(1, 'years').format('YYYY')}-${semesterEnd[semesters.find(s => s === 'SPRING')]}` : `${year}-${semesterEnd[semesters.find(s => s === 'FALL')]}` @@ -417,7 +424,7 @@ const optimizedStatisticsOf = async (query) => { } const { - studyRights, startDate, endDate, months, exchangeStudents, cancelledStudents, nondegreeStudents + studyRights, startDate, months, endDate, exchangeStudents, cancelledStudents, nondegreeStudents } = parseQueryParams(query) const studentnumbers = @@ -425,7 +432,7 @@ const optimizedStatisticsOf = async (query) => { studyRights, startDate, endDate, exchangeStudents, cancelledStudents, nondegreeStudents ) const students = - await getStudentsIncludeCoursesBetween(studentnumbers, startDate, dateMonthsFromNow(startDate, months), studyRights) + await getStudentsIncludeCoursesBetween(studentnumbers, startDate, dateMonthsFromNow(startDate, months), studyRights, query.tag) const formattedStudents = await formatStudentsForApi(students, startDate, endDate, query) return formattedStudents diff --git a/services/backend/oodikone2-backend/test/jest/services/population.test.js b/services/backend/oodikone2-backend/test/jest/services/population.test.js index 73bc6c33c2..06accc06df 100644 --- a/services/backend/oodikone2-backend/test/jest/services/population.test.js +++ b/services/backend/oodikone2-backend/test/jest/services/population.test.js @@ -59,6 +59,7 @@ const semesters = { const createQueryObject = (year, semester, codes, months) => ({ studyRights: codes, year, + tagYear: year, semesters: [semester], months }) @@ -137,90 +138,90 @@ describe('optimizedStatisticsOf tests', () => { - Two credits in 2011-09-31 and 2012-02-31. `, () => { - beforeAll(async () => { - await Semester.bulkCreate([semesters.fall, semesters.spring]) - await Student.create(student) - await Course.create(courses.elements_of_ai) - await Credit.bulkCreate([creditFall, creditSpring]) - await ElementDetails.bulkCreate([elementdetails.bsc, elementdetails.maths, elementdetails.cs]) - await StudyrightExtent.create(studyrightextents.bachelors) - await Studyright.create(studyright) - await StudyrightElement.bulkCreate([studyrightelements.bsc, studyrightelements.maths]) - }) - - test('Query result for BSc, Fall 2011 for 12 months should contain the student.', async () => { - const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.bsc.code], 12) - const { students } = await optimizedStatisticsOf(query) - expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(true) - }) - - test('Query result for BSc, Fall 2012 for 12 months should contain the student.', async () => { - const query = createQueryObject('2012', SEMESTER.FALL, [elementdetails.bsc.code], 12) - const { students } = await optimizedStatisticsOf(query) - expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(true) - }) - - test('Query result for Mathematics, Fall 2010 for 12 months should not contain the student.', async () => { - const query = createQueryObject('2010', SEMESTER.FALL, [elementdetails.maths.code], 12) - const { students } = await optimizedStatisticsOf(query) - expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(false) - }) - - test('Query result for Mathematics, Fall 2011 for 12 months should contain the student.', async () => { - const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.maths.code], 12) - const { students } = await optimizedStatisticsOf(query) - expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(true) - }) - - test('Query result for Mathematics, Fall 2012 for 12 months should not contain the student.', async () => { - const query = createQueryObject('2012', SEMESTER.FALL, [elementdetails.maths.code], 12) - const { students } = await optimizedStatisticsOf(query) - expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(false) - }) + beforeAll(async () => { + await Semester.bulkCreate([semesters.fall, semesters.spring]) + await Student.create(student) + await Course.create(courses.elements_of_ai) + await Credit.bulkCreate([creditFall, creditSpring]) + await ElementDetails.bulkCreate([elementdetails.bsc, elementdetails.maths, elementdetails.cs]) + await StudyrightExtent.create(studyrightextents.bachelors) + await Studyright.create(studyright) + await StudyrightElement.bulkCreate([studyrightelements.bsc, studyrightelements.maths]) + }) + + test('Query result for BSc, Fall 2011 for 12 months should contain the student.', async () => { + const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.bsc.code], 12) + const { students } = await optimizedStatisticsOf(query) + expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(true) + }) + + test('Query result for BSc, Fall 2012 for 12 months should contain the student.', async () => { + const query = createQueryObject('2012', SEMESTER.FALL, [elementdetails.bsc.code], 12) + const { students } = await optimizedStatisticsOf(query) + expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(true) + }) + + test('Query result for Mathematics, Fall 2010 for 12 months should not contain the student.', async () => { + const query = createQueryObject('2010', SEMESTER.FALL, [elementdetails.maths.code], 12) + const { students } = await optimizedStatisticsOf(query) + expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(false) + }) + + test('Query result for Mathematics, Fall 2011 for 12 months should contain the student.', async () => { + const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.maths.code], 12) + const { students } = await optimizedStatisticsOf(query) + expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(true) + }) + + test('Query result for Mathematics, Fall 2012 for 12 months should not contain the student.', async () => { + const query = createQueryObject('2012', SEMESTER.FALL, [elementdetails.maths.code], 12) + const { students } = await optimizedStatisticsOf(query) + expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(false) + }) + + test('Query result for BSc and Computer Science, Fall 2011 for 12 months should not contain the student.', async () => { + const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.bsc.code, elementdetails.cs.code], 12) + const { students } = await optimizedStatisticsOf(query) + expect(students.some(s => s.studentNumber === student.student_studentnumber)).toBe(false) + }) + + test('Query result for BSc and Mathematics, Fall 2011 for 12 months should contain the student.', async () => { + const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.bsc.code, elementdetails.maths.code], 12) + const { students } = await optimizedStatisticsOf(query) + expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(true) + }) + + test('Query result for BSc, Spring 2012 for 12 months should contain the student', async () => { + const query = createQueryObject('2012', SEMESTER.SPRING, [elementdetails.bsc.code], 12) + const { students } = await optimizedStatisticsOf(query) + expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(true) + }) + + test('Query result for Mathematics, Spring 2012 for 12 months should not contain the student', async () => { + const query = createQueryObject('2012', SEMESTER.SPRING, [elementdetails.maths.code], 12) + const { students } = await optimizedStatisticsOf(query) + expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(false) + }) + + test('Query result for BSc, Fall 2011 for 4 months should only return the FALL course instance for student. ', async () => { + const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.bsc.code], 4) + const { students } = await optimizedStatisticsOf(query) + const result = students.find(s => s.studentNumber === student.studentnumber) + const courseinstances = result.courses + expect(courseinstances.length).toBe(1) + expect( + courseinstances.some(instance => + (instance.date.getTime() === courseinstanceFall.coursedate.getTime()) && + (instance.course.code === courseinstanceFall.course_code)) + ).toBe(true) + }) + + test('Query result for BSc, Fall 2011 for 1 month should return student even though do not have any credits yet. ', async () => { + const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.bsc.code], 1) + const { students } = await optimizedStatisticsOf(query) + expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(true) + }) - test('Query result for BSc and Computer Science, Fall 2011 for 12 months should not contain the student.', async () => { - const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.bsc.code, elementdetails.cs.code], 12) - const { students } = await optimizedStatisticsOf(query) - expect(students.some(s => s.studentNumber === student.student_studentnumber)).toBe(false) }) - test('Query result for BSc and Mathematics, Fall 2011 for 12 months should contain the student.', async () => { - const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.bsc.code, elementdetails.maths.code], 12) - const { students } = await optimizedStatisticsOf(query) - expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(true) - }) - - test('Query result for BSc, Spring 2012 for 12 months should contain the student', async () => { - const query = createQueryObject('2012', SEMESTER.SPRING, [elementdetails.bsc.code], 12) - const { students } = await optimizedStatisticsOf(query) - expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(true) - }) - - test('Query result for Mathematics, Spring 2012 for 12 months should not contain the student', async () => { - const query = createQueryObject('2012', SEMESTER.SPRING, [elementdetails.maths.code], 12) - const { students } = await optimizedStatisticsOf(query) - expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(false) - }) - - test('Query result for BSc, Fall 2011 for 4 months should only return the FALL course instance for student. ', async () => { - const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.bsc.code], 4) - const { students } = await optimizedStatisticsOf(query) - const result = students.find(s => s.studentNumber === student.studentnumber) - const courseinstances = result.courses - expect(courseinstances.length).toBe(1) - expect( - courseinstances.some(instance => - (instance.date.getTime() === courseinstanceFall.coursedate.getTime()) && - (instance.course.code === courseinstanceFall.course_code)) - ).toBe(true) - }) - - test('Query result for BSc, Fall 2011 for 1 month should return student even though do not have any credits yet. ', async () => { - const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.bsc.code], 1) - const { students } = await optimizedStatisticsOf(query) - expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(true) - }) - - }) - }) diff --git a/services/oodikone2-frontend/src/components/PopulationSearchForm/index.jsx b/services/oodikone2-frontend/src/components/PopulationSearchForm/index.jsx index 37cbb8d0b9..23a81e87d5 100644 --- a/services/oodikone2-frontend/src/components/PopulationSearchForm/index.jsx +++ b/services/oodikone2-frontend/src/components/PopulationSearchForm/index.jsx @@ -18,7 +18,8 @@ import { getSemesters } from '../../redux/semesters' import { transferTo } from '../../populationFilters' import { getDegreesAndProgrammes } from '../../redux/populationDegreesAndProgrammes' -import { momentFromFormat, reformatDate, textAndDescriptionSearch, getTextIn } from '../../common' +import { getTagsByStudytrackAction } from '../../redux/tags' +import { momentFromFormat, reformatDate, textAndDescriptionSearch, getTextIn, userIsAdmin } from '../../common' import { setLoading } from '../../redux/graphSpinner' import './populationSearchForm.css' import { dropdownType } from '../../constants/types' @@ -48,7 +49,9 @@ class PopulationSearchForm extends Component { getSemesters: func.isRequired, semesters: shape({}).isRequired, history: shape({}).isRequired, - location: shape({}).isRequired + location: shape({}).isRequired, + getTagsByStudytrackAction: func.isRequired, + tags: arrayOf(shape({ tag_id: string, tagname: string })).isRequired } constructor() { @@ -60,11 +63,13 @@ class PopulationSearchForm extends Component { isLoading: false, showAdvancedSettings: false, momentYear: Datetime.moment('2017-01-01'), - floatMonths: this.months('2017', 'FALL') + floatMonths: this.months('2017', 'FALL'), + selectedTag: '', + isAdmin: false } } - componentDidMount() { + async componentDidMount() { const { studyProgrammes, semesters, location } = this.props if (!studyProgrammes || Object.values(studyProgrammes).length === 0) { this.setState({ query: this.initialQuery() }) // eslint-disable-line @@ -76,6 +81,8 @@ class PopulationSearchForm extends Component { if (location.search) { this.fetchPopulationFromUrlParams() } + const admin = await userIsAdmin() + this.setState({ isAdmin: admin }) } componentDidUpdate(prevProps) { @@ -113,6 +120,7 @@ class PopulationSearchForm extends Component { const query = { ...initial, ...rest, + tagYear: rest.year, studyRights: JSON.parse(studyRights), months: JSON.parse(months) } @@ -132,15 +140,14 @@ class PopulationSearchForm extends Component { this.pushQueryToUrl(query) } - fetchPopulation = (query) => { + fetchPopulation = (query, tag) => { const queryCodes = Object.values(query.studyRights).filter(e => e != null) - const uuid = uuidv4() const request = { ...query, studyRights: queryCodes, uuid } this.setState({ isLoading: true }) this.props.setLoading() Promise.all([ - this.props.getPopulationStatistics({ ...query, uuid }), + this.props.getPopulationStatistics({ ...query, uuid, tag }), this.props.getPopulationCourses(request), this.props.getPopulationFilters(request), this.props.getMandatoryCourses(query.studyRights.programme) @@ -199,6 +206,7 @@ class PopulationSearchForm extends Component { query: { ...query, year: reformatDate(momentYear, YEAR_DATE_FORMAT), + tagYear: reformatDate(momentYear, YEAR_DATE_FORMAT), months: this.months( reformatDate(momentYear, YEAR_DATE_FORMAT), this.state.query.semesters.includes('FALL') ? 'FALL' : 'SPRING' @@ -212,6 +220,20 @@ class PopulationSearchForm extends Component { }) } + handleTagSearch = (event, { value }) => { + const { query } = this.state + const months = this.getMonths('2015', new Date(), 'FALL') + this.setState({ + query: { + ...query, + year: '2018', + tagYear: '2015', + months + }, + selectedTag: value + }) + } + addYear = () => { const { year } = this.state.query const nextYear = momentFromFormat(year, YEAR_DATE_FORMAT).add(1, 'year') @@ -269,6 +291,7 @@ class PopulationSearchForm extends Component { handleProgrammeChange = (e, { value }) => { const programme = value + this.props.getTagsByStudytrackAction(value) if (programme === '') { this.handleClear('programme') return @@ -360,6 +383,7 @@ class PopulationSearchForm extends Component { initialQuery = () => ({ year: Datetime.moment('2017-01-01').year(), + tagYear: Datetime.moment('2017-01-01').year(), semesters: ['FALL', 'SPRING'], studentStatuses: [], studyRights: [], @@ -471,10 +495,10 @@ class PopulationSearchForm extends Component { return (