Compare commits

...
17 Commits
Author SHA1 Message Date
Joonas HäkkinenandGitHub bd9d015bbd Merge pull request #2291 from UniversityOfHelsinkiCS/trunk
Fixing charts and things
2020-08-11 16:04:49 +03:00
Joonas HäkkinenandGitHub 7f83066034 Merge branch 'master' into trunk 2020-08-11 15:34:36 +03:00
Joonas Häkkinen 432c2cefa9 Fix highcharts scaling, sort of 2020-08-11 15:32:48 +03:00
Joonas Häkkinen 79d1f61312 Change label color 2020-08-11 09:20:00 +03:00
Joonas HäkkinenandGitHub 8e6400ea08 Merge pull request #2290 from UniversityOfHelsinkiCS/trunk
Monday madness
2020-08-10 23:01:21 +03:00
Joonas HäkkinenandGitHub 798a24f2ec Merge branch 'master' into trunk 2020-08-10 20:42:46 +03:00
Joonas Häkkinen 0b9c3ef377 Remove deprecated tests 2020-08-10 20:42:08 +03:00
Joonas Häkkinen 3b0005a374 Does this fail in CI? 2020-08-10 16:34:07 +03:00
Joonas HäkkinenandGitHub fee6994d8b Merge pull request #2289 from UniversityOfHelsinkiCS/trunk
Mergeles
2020-08-10 15:17:29 +03:00
Joonas HäkkinenandGitHub d8513141ef Merge branch 'master' into trunk 2020-08-10 13:21:22 +03:00
Joonas Häkkinen 67ffd42cbe Update dependencies 2020-08-10 12:59:41 +03:00
Joonas HäkkinenandGitHub 4604d6c544 Merge pull request #2288 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/trunk/cypress-4.12.1
Bump cypress from 4.11.0 to 4.12.1
2020-08-10 12:00:26 +03:00
dependabot-preview[bot]andGitHub 438a0cceab Bump cypress from 4.11.0 to 4.12.1
Bumps [cypress](https://github.com/cypress-io/cypress) from 4.11.0 to 4.12.1.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Commits](https://github.com/cypress-io/cypress/compare/v4.11.0...v4.12.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-10 08:36:32 +00:00
Joonas HäkkinenandGitHub a4dd87762c Merge pull request #2287 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/trunk/concurrently-5.3.0
Bump concurrently from 5.2.0 to 5.3.0
2020-08-10 11:34:52 +03:00
dependabot-preview[bot]andGitHub 7e05c9ddb1 Bump concurrently from 5.2.0 to 5.3.0
Bumps [concurrently](https://github.com/kimmobrunfeldt/concurrently) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/kimmobrunfeldt/concurrently/releases)
- [Commits](https://github.com/kimmobrunfeldt/concurrently/compare/v5.2.0...v5.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-10 03:37:10 +00:00
Joonas HäkkinenandGitHub 53593128e2 Merge pull request #2286 from UniversityOfHelsinkiCS/trunk
Remove old filters and associated code
2020-08-07 18:15:24 +03:00
Joonas Häkkinen 57221b6677 Remove old filters and associated code 2020-08-07 17:53:54 +03:00
58 changed files with 932 additions and 6341 deletions
+791 -1159
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -51,8 +51,8 @@
},
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
"devDependencies": {
"concurrently": "^5.2.0",
"cypress": "^4.11.0",
"concurrently": "^5.3.0",
"cypress": "^4.12.1",
"cypress-smokify": "0.0.3",
"husky": "^4.2.5"
},
@@ -1,74 +0,0 @@
const schema = 'courses_schema'
process.env.DB_SCHEMA = schema
const test = require('ava')
const supertest = require('supertest')
const jwt = require('jsonwebtoken')
const { Course, CourseInstance, CourseTeacher, Credit, Teacher, Student, sequelize } = require('../../src/models')
const {
generateCourses,
generateCourseInstances,
generateCredits,
generateTeachers,
generateStudents,
generateCourseTeachers
} = require('../utils')
const app = require('../../src/app')
const conf = require('../../src/conf-backend')
const api = supertest(app)
const uid = 'tktl',
fullname = ''
const payload = { userId: uid, name: fullname }
const token = jwt.sign(payload, conf.TOKEN_SECRET, {
expiresIn: '24h'
})
const INSTANCE_COUNT = 4
let courses
let courseInstances
test.skip.before(async () => {
await sequelize.createSchema(schema)
await sequelize.sync()
courses = await generateCourses(1)
courseInstances = await generateCourseInstances(courses, INSTANCE_COUNT)
const students = await generateStudents()
const credits = await generateCredits(courseInstances, students)
const teachers = await generateTeachers()
const courseTeachers = generateCourseTeachers(courseInstances, teachers)
await Course.bulkCreate(courses)
await CourseInstance.bulkCreate(courseInstances)
await Student.bulkCreate(students)
await Credit.bulkCreate(credits)
await Teacher.bulkCreate(teachers)
await CourseTeacher.bulkCreate(courseTeachers)
})
test.skip.after.always(async () => {
await sequelize.dropSchema(schema)
})
test.skip('should pong when pinged', async t => {
const res = await api.get('/ping')
t.is(res.status, 200)
t.deepEqual(res.body, { data: 'pong' })
})
test.skip('courses can be searched by a searchterm', async t => {
const selectedCourse = courses[0]
const res = await api
.get('/api/courses')
.query({ name: selectedCourse.name })
.set('x-access-token', token)
.set('uid', uid)
t.is(res.status, 200)
t.truthy(res.body.length > 0, 'res body was empty')
const foundCourse = res.body.find(course => course.id === selectedCourse.id)
t.is(selectedCourse.code, foundCourse.code)
t.is(selectedCourse.name, foundCourse.name)
})
@@ -1,73 +0,0 @@
const schema = 'department_schema'
process.env.DB_SCHEMA = schema
const test = require('ava')
const supertest = require('supertest')
const jwt = require('jsonwebtoken')
const { Course, CourseInstance, CourseTeacher, Credit, Teacher, Student, sequelize } = require('../../src/models')
const {
generateCourses,
generateCourseInstances,
generateCredits,
generateTeachers,
generateStudents,
generateCourseTeachers
} = require('../utils')
const app = require('../../src/app')
const conf = require('../../src/conf-backend')
const api = supertest(app)
const uid = 'tktl',
fullname = ''
const payload = { userId: uid, name: fullname, enabled: true }
const token = jwt.sign(payload, conf.TOKEN_SECRET, {
expiresIn: '24h'
})
const INSTANCE_COUNT = 4
let courses
let courseInstances
test.skip.before(async () => {
await sequelize.createSchema(schema)
await sequelize.sync()
courses = await generateCourses(1)
courseInstances = await generateCourseInstances(courses, INSTANCE_COUNT)
const students = await generateStudents()
const credits = await generateCredits(courseInstances, students)
const teachers = await generateTeachers()
const courseTeachers = generateCourseTeachers(courseInstances, teachers)
await Course.bulkCreate(courses)
await CourseInstance.bulkCreate(courseInstances)
await Student.bulkCreate(students)
await Credit.bulkCreate(credits)
await Teacher.bulkCreate(teachers)
await CourseTeacher.bulkCreate(courseTeachers)
})
test.skip.after.always(async () => {
await sequelize.dropSchema(schema)
})
test.skip('should pong when pinged', async t => {
const res = await api.get('/ping')
t.is(res.status, 200)
t.deepEqual(res.body, { data: 'pong' })
})
test.skip('populations can be searched by a searchterm', async t => {
const res = await api
.get('/api/departmentsuccess')
.query({ date: '2005.08.01' })
.set('x-access-token', token)
.set('uid', uid)
.expect(200)
.expect('Content-Type', /application\/json/)
console.log(res.body)
t.deepEqual(Object.keys(res.body).sort(), ['Chemistry', 'Computer Science', 'Mathematics', 'Physics'].sort())
t.truthy(res.body['Computer Science'] > 20)
})
@@ -1,93 +0,0 @@
const schema = 'login_schema'
process.env.DB_SCHEMA = schema
const test = require('ava')
const supertest = require('supertest')
const jwt = require('jsonwebtoken')
const userService = require('../../src/services/userService')
const { sequelize } = require('../../src/models')
const { generateUsers } = require('../utils')
const app = require('../../src/app')
const api = supertest(app)
test.before(async () => {
await sequelize.createSchema(schema)
await sequelize.sync()
})
test.after.always(async () => {
await sequelize.dropSchema(schema)
})
test('should pong when pinged', async t => {
const res = await api.get('/ping')
t.is(res.status, 200)
t.deepEqual(res.body, { data: 'pong' })
})
test.skip('login does not allow without required headers', async t => {
const res = api.post('/api/login').set({ uid: 'uid' })
const res2 = api.post('/api/login').set({ 'shib-session-id': 'sessioniddiibadaaba' })
const responses = await Promise.all([res, res2])
responses.forEach(response => t.is(response.status, 401))
})
test.skip('login creates an user', async t => {
const user = generateUsers(1)[0]
const res = await api.post('/api/login').set({
uid: user.username,
'shib-session-id': 'sessioniddiibadaaba',
displayname: user.full_name
})
t.is(res.status, 200)
const foundUser = await userService.byUsername(user.username)
t.is(foundUser.username, user.username, 'Username did not match uid')
t.is(foundUser.full_name, user.full_name, 'Full name did not match fullname')
})
test.skip('login fetches an user and returns token to enabled', async t => {
const user = generateUsers(1)[0]
user.is_enabled = true
// await User.insertOrUpdate(user) replace with user service equivalent when test is no longer skipped
const res = await api.post('/api/login').set({
uid: user.username,
'shib-session-id': 'sessioniddiibadaaba',
displayname: user.full_name
})
t.is(res.status, 200)
t.truthy(res.body.token, `Token did not exist in body: ${res.body}`)
const decodedToken = jwt.decode(res.body.token)
t.is(decodedToken.userId, user.username, 'user id did not match username')
t.is(decodedToken.name, user.full_name, 'name did not match full name')
})
test.skip('logout removes token', async t => {
const user = generateUsers(1)[0]
await api
.post('/api/login')
.set({
uid: user.username,
'shib-session-id': 'sessioniddiibadaaba',
displayname: user.full_name
})
.expect(200)
const res2 = await api.delete('/api/logout').expect(200)
t.falsy(res2.body.token)
const res3 = await api.get('/api/departmentsuccess').expect(403)
t.is(res3.body.error, 'No token in headers')
})
@@ -1,64 +0,0 @@
const schema = 'student_schema'
process.env.DB_SCHEMA = schema
const test = require('ava')
const supertest = require('supertest')
const jwt = require('jsonwebtoken')
const { Course, CourseInstance, Credit, Student, sequelize } = require('../../src/models')
const { generateStudents, generateCredits, generateCourses, generateCourseInstances } = require('../utils')
const app = require('../../src/app')
const conf = require('../../src/conf-backend')
const api = supertest(app)
const userId = 'tktl'
const payload = { userId, name: 'Koko nimi', enabled: true }
const token = jwt.sign(payload, conf.TOKEN_SECRET, {
expiresIn: '24h'
})
let students
let courses
let courseInstances
let credits
test.skip.before(async () => {
await sequelize.createSchema(schema)
await sequelize.sync()
courses = await generateCourses(3)
courseInstances = await generateCourseInstances(courses, 1)
students = await generateStudents(4)
credits = await generateCredits(courseInstances, [students[0]], 1)
await Course.bulkCreate(courses)
await CourseInstance.bulkCreate(courseInstances)
await Student.bulkCreate(students)
await Credit.bulkCreate(credits)
})
test.skip.after.always(async () => {
await sequelize.dropSchema(schema)
})
test.skip('a students information can be fetched', async t => {
const res = await api
.get(`/api/students/${students[0].studentnumber}`)
.set('x-access-token', token)
.set('uid', userId)
.expect(200)
.expect('Content-Type', /application\/json/)
t.is(res.body.tags.length, 0)
t.is(res.body.courses.length, 3)
})
test.skip('students are returned when searching', async t => {
const res = await api
.get('/api/students')
.query({ searchTerm: '0' })
.set('x-access-token', token)
.set('uid', userId)
.expect(200)
.expect('Content-Type', /application\/json/)
t.is(res.body.length, 4)
})
@@ -1,33 +0,0 @@
const schema = 'teacher_schema'
process.env.DB_SCHEMA = schema
const test = require('ava')
const supertest = require('supertest')
const { sequelize } = require('../../src/models')
const app = require('../../src/app')
const api = supertest(app)
// const jwt = require('jsonwebtoken')
// const conf = require('../../src/conf-backend')
// const uid = 'tktl', fullname = ''
// const payload = { userId: uid, name: fullname, enabled: true }
// const token = jwt.sign(payload, conf.TOKEN_SECRET, {
// expiresIn: '24h'
// })
test.before(async () => {
await sequelize.createSchema(schema)
await sequelize.sync()
})
test.after.always(async () => {
await sequelize.dropSchema(schema)
})
test('should pong when pinged', async t => {
const res = await api.get('/ping')
t.is(res.status, 200)
t.deepEqual(res.body, { data: 'pong' })
})
@@ -1,255 +0,0 @@
const test = require('ava')
const supertest = require('supertest')
const app = require('../../src/app')
const api = supertest(app)
const { sequelizeKone } = require('../../src/models/models_kone')
const conf = require('../../src/conf-backend')
const jwt = require('jsonwebtoken')
/*
const auth = {
username: 'tktl',
password: 'CsZerc8TUtR1S95CYOtTaC69'
}
*/
let token
const uid = 'tktl',
fullname = ''
const payload = { userId: uid, name: fullname }
test.before(async () => {
token = jwt.sign(payload, conf.TOKEN_SECRET, {
expiresIn: '24h'
})
//console.log(token)
sequelizeKone.query(
`DELETE
FROM tag_student
WHERE tags_tagname = 'test3'`
)
})
test.after.always(async () => {
const tagToAdd = { text: 'mooc-2013' }
console.log('ennd bfefore')
await api
.delete('/api/students/014424850/tags')
.send(tagToAdd)
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
})
test('should pong when pinged', async t => {
const res = await api
.get('/ping')
.expect(200)
.expect('Content-Type', /application\/json/)
t.deepEqual(res.body, { data: 'pong' })
})
test('request with correct credentials succeed', async t => {
await api
.get('/api/enrollmentdates')
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
//.auth(auth.username, auth.password)
.expect(200)
.expect('Content-Type', /application\/json/)
t.pass()
})
test('students can be searched by searchterm', async t => {
const res = await api
.get('/api/students')
.query({ searchTerm: 'Luukkainen Matti' })
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(200)
.expect('Content-Type', /application\/json/)
const students = res.body
t.is(students.length, 1)
t.is(students[0].studentNumber, '011120775')
})
test('a student credit info is returned with student number', async t => {
const res = await api
.get('/api/students/011120775')
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(200)
.expect('Content-Type', /application\/json/)
const student = res.body
t.is(student.studentNumber, '011120775')
t.is(student.credits, 514)
t.is(student.courses.length, 86)
t.is(student.tags.length, 0)
})
test('a tagless student credit info is returned with student number', async t => {
const res = await api
.get('/api/students/014424850')
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(200)
.expect('Content-Type', /application\/json/)
const student = res.body
console.log(student.tags)
t.is(student.studentNumber, '014424850')
t.is(student.credits, 181)
t.is(student.tags.length, 3)
})
test('courses can be searched by a searchterm', async t => {
const res = await api
.get('/api/courses')
.query({ name: 'Ohjelmoinnin' })
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(200)
.expect('Content-Type', /application\/json/)
const courses = res.body
t.is(courses.length, 10)
const courseNames = courses.map(c => c.name.toUpperCase())
t.truthy(courseNames.every(n => n.includes('Ohjelmoinnin'.toUpperCase())))
})
test('populations can be searched by a searchterm', async t => {
const res = await api
.get('/api/studyrightkeywords')
.query({ search: 'computer Science' })
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(200)
.expect('Content-Type', /application\/json/)
t.is(res.body.length, 40)
t.truthy(res.body.every(r => r.toUpperCase().includes('COMPUTER SCIENCE')))
})
test('enrollment dates can be fetched', async t => {
const res = await api
.get('/api/enrollmentdates')
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(200)
.expect('Content-Type', /application\/json/)
t.is(res.body.length, 722)
t.truthy(res.body.includes('2014-09-02'))
})
test('tags can be searched by a searchterm', async t => {
const res = await api
.get('/api/tags')
.query({ query: 'mooc' })
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(200)
.expect('Content-Type', /application\/json/)
t.is(res.body.length, 2)
t.deepEqual(res.body, ['mooc-2012', 'mooc-2013'])
})
test('populations can be searched by a searchterm', async t => {
const res = await api
.get('/api/departmentsuccess')
.query({ date: '2005.08.01' })
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(200)
.expect('Content-Type', /application\/json/)
t.deepEqual(Object.keys(res.body).sort(), ['Chemistry', 'Computer Science', 'Mathematics', 'Physics'].sort())
t.truthy(res.body['Computer Science'] > 20)
})
test.only('new api populations can be fetched', async t => {
const res = await api
.get('/api/populationstatistics')
.query({
year: '2010',
semester: 'SPRING',
studyRights: '16'
})
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(200)
.expect('Content-Type', /application\/json/)
const stats = res.body
t.is(stats.length, 6)
})
test('multiple population studyrights can be fetched', async t => {
const res = await api
.get('/api/populationstatistics')
.query({
year: '2010',
semester: 'SPRING',
studyRights: ['6', '7']
})
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(200)
.expect('Content-Type', /application\/json/)
const stats = res.body
t.is(stats.length, 19)
})
test('population statics with wrong semester results in bad request', async t => {
const res = await api
.get('/api/populationstatistics')
.query({
year: '2010',
semester: 'NO SEASON',
studyRights: '500-K005'
})
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(400)
.expect('Content-Type', /application\/json/)
const stats = res.body
t.is(stats.error, 'Semester should be either SPRING OR FALL')
})
test('population statics with wrong semester results in bad request', async t => {
const res = await api
.get('/api/populationstatistics')
.query({
year: '2010',
semester: 'SPRING',
studyRights: '[Huolissaanolon maisteriohjelma, 500-K005]'
})
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(400)
.expect('Content-Type', /application\/json/)
const stats = res.body
t.is(stats.error, 'No such study rights: [Huolissaanolon maisteriohjelma, 500-K005]')
})
test('population statics without a proper queryresults in bad request', async t => {
const res = await api
.get('/api/populationstatistics')
.query({ myName: 'Jeff' })
.set('x-access-token', token)
.set('eduPersonPrincipalName', uid)
.expect(400)
.expect('Content-Type', /application\/json/)
const stats = res.body
t.is(stats.error, 'The query should have a year, semester and study rights defined')
})
@@ -1,38 +0,0 @@
const test = require('ava')
const Course = require('../../src/services/courses')
test.only('statistics of an instance are caclulated right', async t => {
const expected = {
all: {
'014013627': 39,
'014195503': 47,
'014248036': 10,
'014289817': 0,
'014420809': 8,
'014464603': 19,
'014471764': 0
},
pass: {
'014013627': 39,
'014195503': 47,
'014248036': 10,
'014289817': 0,
'014420809': 8,
'014471764': 0
},
fail: {
'014464603': 19
},
startYear: {
'2011': 1,
'2013': 2,
'2014': 3,
'2016': 1
}
}
const result = await Course.statisticsOf('581259', '2016-06-14', 13)
t.deepEqual(result, expected)
})
@@ -1,7 +0,0 @@
const test = require('ava')
//const Population = require('../src/services/populations')
test.only('population statistics are calculated right', async t => {
t.pass()
})
@@ -1,27 +0,0 @@
const test = require('ava')
const Teacher = require('../../src/services/teachers')
test('teacher statistics are calculated right', async t => {
const expected = [
{
teacherId: 'Lemström Kjell Michael Bernhard',
credits: 6,
studentsPassed: 1,
studentsTeached: 1,
instancesTeached: 1
},
{
teacherId: 'Luukkainen Matti Juhani',
credits: 593,
studentsPassed: 108,
studentsTeached: 118,
instancesTeached: 3
}
]
const sr = ['Bachelor of Science, Computer Science']
const result = await Teacher.statisticsOf(['581259'], '2016-05-01', '2016-10-1', 1, 1, sr)
t.deepEqual(result, expected)
})
+33 -39
View File
@@ -2386,8 +2386,7 @@
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"resolved": "",
"dev": true
}
}
@@ -3893,9 +3892,9 @@
}
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"ms": {
@@ -5929,15 +5928,6 @@
"domelementtype": "1"
}
},
"dot-prop": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
"integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==",
"dev": true,
"requires": {
"is-obj": "^1.0.0"
}
},
"dotenv": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz",
@@ -6007,9 +5997,9 @@
"integrity": "sha512-PUurrpyDA74MuAjJRD+79ss5BqJlU3mdArRbuu4wO/dt6jc3Ic/6BDmFJxkdwbfq39cHf/XKm2vW98XSvut9Dg=="
},
"elliptic": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
"integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",
@@ -9319,12 +9309,6 @@
"kind-of": "^3.0.2"
}
},
"is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true
},
"is-odd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz",
@@ -9784,9 +9768,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"lodash-es": {
"version": "4.17.14",
@@ -13451,8 +13435,7 @@
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"resolved": "",
"dev": true
},
"p-limit": {
@@ -13645,11 +13628,6 @@
}
}
},
"react-hookstore": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/react-hookstore/-/react-hookstore-1.4.4.tgz",
"integrity": "sha512-/shqvOsn/hoM/yN/je2phtCrK/Uzp6Dl/akXcF0NUEj1W5YCE/9GyIT+7n1EafTmq0EwYIZOqHZwoFlS6agxGQ=="
},
"react-hotkeys": {
"version": "2.0.0-pre4",
"resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0-pre4.tgz",
@@ -16237,6 +16215,12 @@
"integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=",
"dev": true
},
"is-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"dev": true
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -16346,14 +16330,25 @@
}
},
"postcss-selector-parser": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz",
"integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz",
"integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==",
"dev": true,
"requires": {
"dot-prop": "^4.1.1",
"dot-prop": "^5.2.0",
"indexes-of": "^1.0.1",
"uniq": "^1.0.1"
},
"dependencies": {
"dot-prop": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz",
"integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==",
"dev": true,
"requires": {
"is-obj": "^2.0.0"
}
}
}
},
"read-pkg": {
@@ -17950,8 +17945,7 @@
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"resolved": "",
"dev": true
},
"p-limit": {
+1 -1
View File
@@ -76,7 +76,7 @@
"jStat": "^1.8.5",
"jwt-decode": "^2.2.0",
"localforage": "^1.7.4",
"lodash": "^4.17.15",
"lodash": "^4.17.19",
"moment": "^2.24.0",
"path-to-regexp": "^2.4.0",
"piwik-react-router": "^0.12.1",
@@ -2,25 +2,11 @@ import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { Progress } from 'semantic-ui-react'
import { intersection, orderBy } from 'lodash'
import { shape, func, bool, arrayOf, string, number } from 'prop-types'
import { shape, bool, arrayOf, string, number } from 'prop-types'
import SearchResultTable from '../../SearchResultTable'
import { gradeFilter } from '../../../populationFilters'
import { setPopulationFilter, removePopulationFilter } from '../../../redux/populationFilters'
import { getHighestGradeOfCourseBetweenRange } from '../../../common'
const CoursePopulationCreditDist = ({
singleCourseStats,
pending,
selectedStudents,
setPopulationFilterDispatch,
removePopulationFilterDispatch,
samples,
filters,
codes,
from,
to
}) => {
const CoursePopulationCreditDist = ({ singleCourseStats, pending, selectedStudents, samples, codes, from, to }) => {
const [courseGrades, setGrades] = useState([])
useEffect(() => {
if (samples && singleCourseStats.alternatives) {
@@ -44,19 +30,6 @@ const CoursePopulationCreditDist = ({
setGrades(filteredGradeArray)
}
}, [pending, selectedStudents])
const setFilter = row => {
filters.map(filter => removePopulationFilterDispatch(filter.id))
setPopulationFilterDispatch(
gradeFilter({
grade: row[0],
coursecodes: codes,
coursename: singleCourseStats.name,
from,
to
})
)
}
const totalAmount = courseGrades.reduce((acc, curr) => acc + curr.amount, 0)
@@ -77,40 +50,22 @@ const CoursePopulationCreditDist = ({
])
const headers = ['Grades', `Students (all=${selectedStudents.length})`, 'Percentage of population']
return (
<SearchResultTable
headers={headers}
rows={rows}
selectable
rowClickFn={(e, row) => setFilter(row)}
noResultText="no data available"
/>
)
return <SearchResultTable headers={headers} rows={rows} selectable noResultText="no data available" />
}
CoursePopulationCreditDist.propTypes = {
singleCourseStats: shape({}).isRequired,
pending: bool.isRequired,
selectedStudents: arrayOf(string).isRequired,
setPopulationFilterDispatch: func.isRequired,
removePopulationFilterDispatch: func.isRequired,
samples: arrayOf(shape({})).isRequired,
filters: arrayOf(shape({})).isRequired,
codes: arrayOf(string).isRequired,
from: number.isRequired,
to: number.isRequired
}
const mapStateToProps = ({ singleCourseStats, populationFilters }) => ({
const mapStateToProps = ({ singleCourseStats }) => ({
singleCourseStats: singleCourseStats.stats,
pending: singleCourseStats.pending,
filters: populationFilters.filters.filter(f => f.type === 'GradeFilter')
pending: singleCourseStats.pending
})
export default connect(
mapStateToProps,
{
setPopulationFilterDispatch: setPopulationFilter,
removePopulationFilterDispatch: removePopulationFilter
}
)(CoursePopulationCreditDist)
export default connect(mapStateToProps)(CoursePopulationCreditDist)
@@ -66,6 +66,18 @@ class CreditAccumulationGraphHighCharts extends Component {
)
}
}
/**
* Trigger updating the chart after filter tray transition has finished.
* This hack remedies the problem that highcharts will not dynamically adapt to container width
* if a CSS transition is used. The resize must fire only after the transition has completed to
* ensure correct size.
*/
if (this.props.trayOpen !== nextProps.trayOpen) {
setTimeout(() => {
this.props.setChartHeight(this.props.currentGraphSize - 1)
}, 700)
}
}
componentDidUpdate() {
@@ -329,7 +341,8 @@ CreditAccumulationGraphHighCharts.propTypes = {
language: string.isRequired,
translate: func.isRequired,
absences: arrayOf(shape({})),
render: bool
render: bool,
trayOpen: bool.isRequired
}
const mapStateToProps = state => ({
@@ -1,35 +1,12 @@
import React, { useEffect } from 'react'
import React from 'react'
import { connect } from 'react-redux'
import { func, shape, arrayOf, string, bool } from 'prop-types'
import { Segment, Header, Popup } from 'semantic-ui-react'
import { getTranslate } from 'react-localize-redux'
import SegmentDimmer from '../../SegmentDimmer'
import PopulationCourseStats from '../../PopulationCourseStats'
import { refreshFilters } from '../../../redux/populationFilters'
import { getCustomPopulationCoursesByStudentnumbers } from '../../../redux/populationCourses'
const CustomPopulationCourses = ({
refreshNeeded,
dispatchRefreshFilters,
translate,
courses,
pending,
selectedStudents,
getCustomPopulationCoursesByStudentnumbersDispatch,
query
}) => {
const reloadCourses = () => {
dispatchRefreshFilters()
getCustomPopulationCoursesByStudentnumbersDispatch({ studentnumberlist: selectedStudents })
}
useEffect(() => {
if (refreshNeeded) {
reloadCourses()
}
}, [refreshNeeded])
const CustomPopulationCourses = ({ translate, courses, pending, selectedStudents, query }) => {
return (
<Segment>
<Header size="medium" dividing>
@@ -57,24 +34,14 @@ CustomPopulationCourses.propTypes = {
courses: shape([]).isRequired,
translate: func.isRequired,
selectedStudents: arrayOf(string).isRequired,
refreshNeeded: bool.isRequired,
getCustomPopulationCoursesByStudentnumbersDispatch: func.isRequired,
dispatchRefreshFilters: func.isRequired,
query: shape({}).isRequired
}
const mapStateToProps = ({ localize, populationFilters, populationCourses }) => ({
const mapStateToProps = ({ localize, populationCourses }) => ({
courses: populationCourses.data,
pending: populationCourses.pending,
translate: getTranslate(localize),
query: populationCourses.query,
refreshNeeded: populationFilters.refreshNeeded
query: populationCourses.query
})
export default connect(
mapStateToProps,
{
dispatchRefreshFilters: refreshFilters,
getCustomPopulationCoursesByStudentnumbersDispatch: getCustomPopulationCoursesByStudentnumbers
}
)(CustomPopulationCourses)
export default connect(mapStateToProps)(CustomPopulationCourses)
@@ -1,85 +0,0 @@
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Button, Form, Input, Popup } from 'semantic-ui-react'
import { func, shape, string, arrayOf } from 'prop-types'
import infoTooltips from '../../../common/InfoToolTips'
import { removePopulationFilter, setPopulationFilter } from '../../../redux/populationFilters'
import { courseCreditFilter } from '../../../populationFilters'
const CourseCreditFilter = ({
coursecodes,
courseData,
filter,
setPopulationFilterAction,
removePopulationFilterAction
}) => {
const [credits, setCredits] = useState(0)
const handleFilter = () => {
setPopulationFilterAction(courseCreditFilter({ credits, coursecodes, coursename: courseData.name }))
}
const handleChange = (e, { value }) => {
setCredits(value)
}
const clearFilter = () => {
removePopulationFilterAction(filter.id)
}
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.TagFilter}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Select students that have </label>
</Form.Field>
<Form.Field>
<Input type="number" onChange={handleChange} value={credits} />
</Form.Field>
<Form.Field>
<label>credits from course {courseData.name}</label>
</Form.Field>
<Form.Field>
<Button onClick={handleFilter} disabled={credits === 0}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Students that have {filter.params.credits} credits from course {filter.params.coursename}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={clearFilter} />
</span>
</Segment>
)
}
CourseCreditFilter.propTypes = {
setPopulationFilterAction: func.isRequired,
removePopulationFilterAction: func.isRequired,
filter: shape({}).isRequired,
courseData: shape({}).isRequired,
coursecodes: arrayOf(string).isRequired
}
const mapStateToProps = ({ singleCourseStats }) => ({
courseData: singleCourseStats.stats
})
export default connect(
mapStateToProps,
{
setPopulationFilterAction: setPopulationFilter,
removePopulationFilterAction: removePopulationFilter
}
)(CourseCreditFilter)
@@ -1,103 +0,0 @@
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
import { func, shape, string, arrayOf, number } from 'prop-types'
import infoTooltips from '../../../common/InfoToolTips'
import { removePopulationFilter, setPopulationFilter } from '../../../redux/populationFilters'
import { gradeFilter } from '../../../populationFilters'
const GradeFilter = ({
coursecodes,
courseData,
filter,
setPopulationFilterAction,
removePopulationFilterAction,
from,
to
}) => {
const [grade, setGrade] = useState(0)
const handleFilter = () => {
setPopulationFilterAction(gradeFilter({ grade, coursecodes, coursename: courseData.name, from, to }))
}
const handleChange = (e, { value }) => {
setGrade(value)
}
const clearFilter = () => {
removePopulationFilterAction(filter.id)
}
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.GradeFilter}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Select students that have grade </label>
</Form.Field>
<Form.Field>
<Dropdown
placeholder="select"
options={[
{ key: 1, text: '1', value: '1' },
{ key: 2, text: '2', value: '2' },
{ key: 3, text: '3', value: '3' },
{ key: 4, text: '4', value: '4' },
{ key: 5, text: '5', value: '5' },
{ key: 6, text: 'Hyv.', value: 'Hyv.' },
{ key: 7, text: 'Hyl.', value: 'Hyl.' }
]}
onChange={handleChange}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<label>from course {courseData.name}</label>
</Form.Field>
<Form.Field>
<Button onClick={handleFilter} disabled={grade === 0}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Students that have grade {filter.params.grade} from course {filter.params.coursename}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={clearFilter} />
</span>
</Segment>
)
}
GradeFilter.propTypes = {
setPopulationFilterAction: func.isRequired,
removePopulationFilterAction: func.isRequired,
filter: shape({}).isRequired,
courseData: shape({}).isRequired,
coursecodes: arrayOf(string).isRequired,
from: number.isRequired,
to: number.isRequired
}
const mapStateToProps = ({ singleCourseStats }) => ({
courseData: singleCourseStats.stats
})
export default connect(
mapStateToProps,
{
setPopulationFilterAction: setPopulationFilter,
removePopulationFilterAction: removePopulationFilter
}
)(GradeFilter)
@@ -1,144 +0,0 @@
import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
import { func, shape, string, arrayOf } from 'prop-types'
import { programmeFilter } from '../../../populationFilters'
import { textAndDescriptionSearch, getNewestProgramme, getTextIn } from '../../../common'
import infoTooltips from '../../../common/InfoToolTips'
import { removePopulationFilter, setPopulationFilter } from '../../../redux/populationFilters'
const ProgrammeFilter = ({
removePopulationFilterAction,
setPopulationFilterAction,
filter,
language,
samples,
studentToTargetCourseDateMap,
elementDetails
}) => {
const [programme, setProgramme] = useState('')
const [programmeName, setName] = useState('')
const [options, setOptions] = useState([])
const [programmes, setProgrammes] = useState(null)
useEffect(() => {
const allProgrammes = {}
samples.forEach(student => {
const programme = getNewestProgramme(
student.studyrights,
student.studentNumber,
studentToTargetCourseDateMap,
elementDetails
)
if (programme) {
if (allProgrammes[programme.code]) {
allProgrammes[programme.code].students.push({ studentnumber: student.studentNumber })
} else {
allProgrammes[programme.code] = { programme, students: [] }
allProgrammes[programme.code].students.push({ studentnumber: student.studentNumber })
}
} else {
if (!allProgrammes['00000']) {
allProgrammes['00000'] = { programme: { name: { en: 'No programme' } }, students: [] }
}
allProgrammes['00000'].students.push({ studentnumber: student.studentnumber })
}
})
const optionsToSet = Object.keys(allProgrammes).map(code => ({
key: code,
text: getTextIn(allProgrammes[code].programme.name, language),
value: code,
description: code
}))
setProgrammes(allProgrammes)
setOptions(optionsToSet)
}, [])
const handleFilter = () => {
setPopulationFilterAction(
programmeFilter({ programme, programmeName, studentToTargetCourseDateMap }, elementDetails)
)
}
const handleChange = (e, { value }) => {
setProgramme(value)
const chosenProgramme = programmes[value].programme
setName(chosenProgramme.name[language])
}
const clearFilter = () => {
removePopulationFilterAction(filter.id)
setProgramme('')
setName('')
}
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.TagFilter}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Select students that are in programme </label>
</Form.Field>
<Form.Field>
<Dropdown
placeholder="select"
options={options}
onChange={handleChange}
search={textAndDescriptionSearch}
noResultsMessage="No selectable study programmes"
closeOnChange
style={{ width: '500px' }}
selection
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<Button onClick={handleFilter} disabled={programme.length < 1}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Students that are in programme {filter.params.programmeName}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={clearFilter} />
</span>
</Segment>
)
}
ProgrammeFilter.defaultProps = {
studentToTargetCourseDateMap: null
}
ProgrammeFilter.propTypes = {
setPopulationFilterAction: func.isRequired,
removePopulationFilterAction: func.isRequired,
filter: shape({}).isRequired,
language: string.isRequired,
samples: arrayOf(shape({})).isRequired,
studentToTargetCourseDateMap: shape({}),
elementDetails: shape({}).isRequired
}
const mapStateToProps = ({ settings, populations }) => ({
language: settings.language,
elementDetails: populations.data.elementdetails.data
})
export default connect(
mapStateToProps,
{
setPopulationFilterAction: setPopulationFilter,
removePopulationFilterAction: removePopulationFilter
}
)(ProgrammeFilter)
@@ -1,184 +0,0 @@
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { Segment, Header, Button, Form, Radio } from 'semantic-ui-react'
import { func, arrayOf, bool, shape, string, number } from 'prop-types'
import { union, uniq, difference } from 'lodash'
import CreditsLessThan from '../../PopulationFilters/CreditsLessThan'
import CreditsAtLeast from '../../PopulationFilters/CreditsAtLeast'
import GradeMeanFilter from '../../PopulationFilters/GradeMeanFilter'
import CourseParticipation from '../../PopulationFilters/CourseParticipation'
import CourseParticipationNTimes from '../../PopulationFilters/CourseParticipationNTimes'
import SexFilter from '../../PopulationFilters/SexFilter'
import InfoBox from '../../InfoBox'
import infotooltips from '../../../common/InfoToolTips'
import GradeFilter from './GradeFilter'
import ProgrammeFilter from './ProgrammeFilter'
import CourseCreditFilter from './CourseCreditFilter'
import { clearPopulationFilters, setComplementFilter } from '../../../redux/populationFilters'
const componentFor = {
GradeFilter,
ProgrammeFilter,
CreditsAtLeast,
CreditsLessThan,
GradeMeanFilter,
SexFilter,
CourseParticipation,
CourseParticipationNTimes,
CourseCreditFilter
}
const advancedFilters = {
CourseCreditFilter
}
const CustomPopulationFilters = ({
samples,
filters,
clearPopulationFiltersDispatch,
complemented,
setComplementFilterDispatch,
allStudyrights,
coursecodes,
from,
to,
studentToTargetCourseDateMap,
coursePopulation
}) => {
const [visible, setVisible] = useState(false)
const [showAdvanced, setAdvanced] = useState(false)
const renderAddFilters = () => {
const { Add } = infotooltips.PopulationStatistics.Filters
const allFilters = union(Object.keys(componentFor))
const setFilters = union(filters.map(f => f.type), filters.filter(f => f.type === 'Preset').map(f => f.id))
const unsetFilters = uniq(difference(allFilters, setFilters))
const filteredUnsetFilters = unsetFilters.filter(
filter => !Object.keys(advancedFilters).includes(filter) || showAdvanced
)
if (filteredUnsetFilters.length === 0) {
return null
}
if (!visible) {
return (
<Segment>
<Header>
Add filters <InfoBox content={Add} />
</Header>
<Button onClick={() => setVisible(true)}>add</Button>
</Segment>
)
}
return (
<Segment>
<Header>
Add filters <InfoBox content={Add} />
</Header>
{coursePopulation ? (
<div>
<Radio toggle label="Advanced filters" checked={showAdvanced} onChange={() => setAdvanced(!showAdvanced)} />
</div>
) : null}
{filteredUnsetFilters.map(filterName => {
if (!coursePopulation && ['GradeFilter', 'CourseCreditFilter'].includes(filterName)) {
return null
}
return React.createElement(componentFor[filterName], {
filter: { notSet: true },
key: filterName,
samples,
studentToTargetCourseDateMap,
coursecodes,
from,
to
})
})}
<Button onClick={() => setVisible(false)}>cancel</Button>
</Segment>
)
}
const renderSetFilters = () => {
const setFilters = filters.map(f => f.type)
const { Filters } = infotooltips.PopulationStatistics.Filters
if (setFilters.length === 0) {
return null
}
return (
<Segment>
<Header>
Filters <InfoBox content={Filters} />
</Header>
{filters.map(filter =>
React.createElement(componentFor[filter.type], {
filter,
key: filter.id,
samples,
allStudyrights,
coursecodes,
from,
to
})
)}
<Form>
<Form.Group inline>
<Form.Field>
<label>Show excluded students only</label>
</Form.Field>
<Form.Field>
<Radio toggle checked={complemented} onClick={setComplementFilterDispatch} />
</Form.Field>
</Form.Group>
</Form>
<Button onClick={clearPopulationFiltersDispatch}>clear all filters</Button>
</Segment>
)
}
return (
<div>
{renderAddFilters()}
{renderSetFilters()}
</div>
)
}
CustomPopulationFilters.defaultProps = {
coursecodes: [],
from: 0,
to: 0,
studentToTargetCourseDateMap: null,
coursePopulation: false
}
CustomPopulationFilters.propTypes = {
filters: arrayOf(shape([])).isRequired,
complemented: bool.isRequired,
samples: arrayOf(shape([])).isRequired,
clearPopulationFiltersDispatch: func.isRequired,
setComplementFilterDispatch: func.isRequired,
coursecodes: arrayOf(string),
allStudyrights: shape({}).isRequired,
studentToTargetCourseDateMap: shape({}),
from: number,
to: number,
coursePopulation: bool
}
const mapStateToProps = ({ populationFilters, populations }) => ({
filters: populationFilters.filters,
complemented: populationFilters.complemented,
allStudyrights: populations.data.studyrights || {}
})
export default connect(
mapStateToProps,
{
clearPopulationFiltersDispatch: clearPopulationFilters,
setComplementFilterDispatch: setComplementFilter
}
)(CustomPopulationFilters)
@@ -1,20 +1,14 @@
import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { Progress } from 'semantic-ui-react'
import { func, arrayOf, string, shape } from 'prop-types'
import { arrayOf, string, shape } from 'prop-types'
import { getActiveLanguage } from 'react-localize-redux'
import SearchResultTable from '../../SearchResultTable'
import { programmeFilter } from '../../../populationFilters'
import { setPopulationFilter, removePopulationFilter } from '../../../redux/populationFilters'
import { getNewestProgramme, getTextIn } from '../../../common'
const CustomPopulationProgrammeDist = ({
samples,
selectedStudents,
setPopulationFilterDispatch,
removePopulationFilterDispatch,
filters,
studentToTargetCourseDateMap,
language,
populationStatistics
@@ -60,28 +54,9 @@ const CustomPopulationProgrammeDist = ({
}
}, [selectedStudents])
const setFilter = row => {
const splitRow = row[0].split(', ')
filters.map(filter => removePopulationFilterDispatch(filter.id))
setPopulationFilterDispatch(
programmeFilter(
{ programme: splitRow.slice(-1)[0], programmeName: splitRow[0], studentToTargetCourseDateMap },
populationStatistics.elementdetails.data
)
)
}
const headers = ['Programmes', `Students (all=${selectedStudents.length})`, 'Percentage of population']
return (
<SearchResultTable
headers={headers}
rows={tableRows}
selectable
rowClickFn={(e, row) => setFilter(row)}
noResultText="placeholder"
/>
)
return <SearchResultTable headers={headers} rows={tableRows} selectable noResultText="placeholder" />
}
CustomPopulationProgrammeDist.defaultProps = {
@@ -91,26 +66,16 @@ CustomPopulationProgrammeDist.defaultProps = {
CustomPopulationProgrammeDist.propTypes = {
samples: arrayOf(shape({})).isRequired,
selectedStudents: arrayOf(string).isRequired,
setPopulationFilterDispatch: func.isRequired,
removePopulationFilterDispatch: func.isRequired,
filters: arrayOf(shape({})).isRequired,
studentToTargetCourseDateMap: shape({}),
language: string.isRequired,
populationStatistics: shape({}).isRequired
}
const mapStateToProps = ({ populationFilters, localize, populations }) => {
const mapStateToProps = ({ localize, populations }) => {
return {
filters: populationFilters.filters.filter(f => f.type === 'ProgrammeFilter'),
language: getActiveLanguage(localize).code,
populationStatistics: populations.data
}
}
export default connect(
mapStateToProps,
{
setPopulationFilterDispatch: setPopulationFilter,
removePopulationFilterDispatch: removePopulationFilter
}
)(CustomPopulationProgrammeDist)
export default connect(mapStateToProps)(CustomPopulationProgrammeDist)
@@ -86,7 +86,7 @@ const FilterTray = ({ children, translate, filterSet, location }) => {
size="mini"
on="hover"
trigger={
<Label color="grey" size="small">
<Label color="blue" size="small">
<Icon name="filter" />
{noFilters}
</Label>
@@ -29,7 +29,7 @@ const CourseRow = ({ courseStatistics }) => {
const language = useLanguage()
const { courseIsSelected } = useCourseFilter()
const { isActiveCourse, onCourseNameCellClick, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
const { onCourseNameCellClick, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
const { course, grades } = courseStatistics
const { name, code } = course
@@ -49,7 +49,7 @@ const CourseRow = ({ courseStatistics }) => {
.reduce(countSumReducer, 0)
}
return (
<Table.Row active={isActiveCourse(course)}>
<Table.Row active={isActive}>
<Popup
trigger={
<Table.Cell className="filterCell clickableCell">
@@ -5,7 +5,6 @@ import { func, arrayOf, object, shape, string, bool } from 'prop-types'
import { getActiveLanguage, getTranslate } from 'react-localize-redux'
import { orderBy } from 'lodash'
import { withRouter } from 'react-router-dom'
import { setPopulationFilter, removePopulationFilterOfCourse } from '../../redux/populationFilters'
import { clearCourseStats } from '../../redux/coursestats'
import PassingSemesters from './PassingSemesters'
import './populationCourseStats.css'
@@ -280,11 +279,6 @@ function PopulationCourseStats(props) {
clearCourseStatsfn()
}
const isActiveCourse = course => {
const { selectedCourses } = props
return selectedCourses.length > 0 && selectedCourses.find(c => course.code === c.code) !== undefined
}
const onCourseNameCellClick = code => {
const courseStatistic = props.populationCourses.data.coursestatistics.find(cs => cs.course.code === code)
if (courseStatistic) {
@@ -316,7 +310,6 @@ function PopulationCourseStats(props) {
courseStatistics,
modules,
filterInput: renderFilterInputHeaderCell,
isActiveCourse,
onCourseNameCellClick,
onGoToCourseStatisticsClick,
onSortableColumnHeaderClick,
@@ -351,7 +344,6 @@ function PopulationCourseStats(props) {
filterInput={renderFilterInputHeaderCell}
courseStatistics={courseStatistics}
onCourseNameClickFn={onCourseNameCellClick}
isActiveCourseFn={isActiveCourse}
/>
</div>
)
@@ -407,7 +399,6 @@ PopulationCourseStats.propTypes = {
populationCourses: shape({
data: shape({ coursestatistics: arrayOf(shape({ course: shape({ code: string, name: shape({}) }) })) })
}).isRequired,
selectedCourses: arrayOf(object).isRequired,
clearCourseStats: func.isRequired,
pending: bool.isRequired,
selectedStudents: arrayOf(string).isRequired,
@@ -417,14 +408,12 @@ PopulationCourseStats.propTypes = {
const mapStateToProps = state => {
const { years } = state.semesters.data
const courseFilters = state.populationFilters.filters.filter(f => f.type === 'CourseParticipation')
const selectedCourses = courseFilters.map(f => f.params.course.course)
const isAdmin = getUserIsAdmin(state.auth.token.roles)
return {
language: getActiveLanguage(state.localize).code,
translate: getTranslate(state.localize),
years,
selectedCourses,
populationCourses: state.populationCourses,
isAdmin
}
@@ -433,8 +422,6 @@ const mapStateToProps = state => {
export default connect(
mapStateToProps,
{
setPopulationFilter,
removePopulationFilterOfCourse,
clearCourseStats
}
)(withRouter(PopulationCourseStats))
@@ -1,163 +1,75 @@
import React, { useCallback } from 'react'
import { connect } from 'react-redux'
import { func, arrayOf, object, number } from 'prop-types'
import { Progress, Icon, Popup } from 'semantic-ui-react'
import SearchResultTable from '../../SearchResultTable'
import React from 'react'
import PropTypes from 'prop-types'
import { useLocation } from 'react-router-dom'
import { Table, Progress } from 'semantic-ui-react'
import { getMonths } from '../../../common/query'
import { getStudentTotalCredits } from '../../../common'
import TSA from '../../../common/tsa'
import { creditsLessThan, creditsAtLeast } from '../../../populationFilters'
import { setPopulationFilter, removePopulationFilter } from '../../../redux/populationFilters'
import ExternalCreditFilterToggle from './ExternalCreditFilterToggle'
const getTotal = students => students.map(student => getStudentTotalCredits(student))
const CreditsGainedTab = ({ filteredStudents }) => {
const months = getMonths(useLocation())
const creditList = filteredStudents.map(student => getStudentTotalCredits(student))
const expectedAmountOfCredits = months => [
[Math.ceil(months * (55 / 12))],
[Math.ceil(months * (50 / 12)), Math.ceil(months * (55 / 12))],
[Math.ceil(months * (40 / 12)), Math.ceil(months * (50 / 12))],
[Math.ceil(months * (30 / 12)), Math.ceil(months * (40 / 12))],
[Math.ceil(months * (20 / 12)), Math.ceil(months * (30 / 12))],
[Math.ceil(months * (10 / 12)), Math.ceil(months * (20 / 12))],
[1, Math.ceil(months * (10 / 12))],
[0, 0]
]
const studentCount = (min, max = Infinity) =>
max === 0
? creditList.filter(credits => credits === 0).length
: creditList.filter(credits => credits < max && credits >= min).length
const filterStudents = (students, minCredits, maxCredits = Infinity) => {
if (minCredits === 0) {
return {
minCredits: '',
maxCredits,
amount: students.filter(s => s === minCredits).length
}
}
if (maxCredits === Infinity) {
return {
minCredits,
maxCredits: '',
amount: students.filter(s => s >= minCredits).length
}
}
return {
minCredits,
maxCredits,
amount: students.filter(s => s >= minCredits && s < maxCredits).length
}
}
const CreditsGainedTab = ({ translate, sample, months, filters, setPopulationFilter, removePopulationFilter }) => {
const handleCreditBracketRowClicked = useCallback(
(e, row) => {
// clear filters & set credit filter for selected range
TSA.Matomo.sendEvent('Population statistics', '"Credits gained during" row selected', row[0])
filters.forEach(filter => removePopulationFilter(filter.id))
const credits = row[0].split(/(\d+)/).map(count => Number(count))
if (credits[1]) {
setPopulationFilter(creditsAtLeast({ credit: credits[1] }))
if (credits[3]) {
setPopulationFilter(creditsLessThan({ credit: credits[3] }))
}
} else {
setPopulationFilter(creditsLessThan({ credit: 1 }))
}
},
[filters, removePopulationFilter, setPopulationFilter]
)
const stats = getTotal(sample)
const limits = expectedAmountOfCredits(months)
const arr = limits.map(l => filterStudents(stats, ...l))
const rows = arr.map(a => {
if (a.maxCredits === 0) {
return [
`${a.maxCredits}`,
a.amount,
<Progress
style={{ margin: '0px' }}
percent={stats.length === 0 ? 0 : Math.round((a.amount / stats.length) * 100)}
progress
/>
]
}
if (a.maxCredits) {
return [
`${a.minCredits} ≤ credits < ${a.maxCredits}`,
a.amount,
<Progress
style={{ margin: '0px' }}
percent={stats.length === 0 ? 0 : Math.round((a.amount / stats.length) * 100)}
progress
/>
]
}
return [
`${a.minCredits} ≤ credits`,
a.amount,
<Progress
style={{ margin: '0px' }}
percent={stats.length === 0 ? 0 : Math.round((a.amount / stats.length) * 100)}
progress
/>
]
})
const headers = [
`Credits gained during first ${months} months`,
`Students (all=${stats.length})`,
'Percentage of population'
const limits = [
[Math.ceil(months * (55 / 12)), null],
[Math.ceil(months * (50 / 12)), Math.ceil(months * (55 / 12))],
[Math.ceil(months * (40 / 12)), Math.ceil(months * (50 / 12))],
[Math.ceil(months * (30 / 12)), Math.ceil(months * (40 / 12))],
[Math.ceil(months * (20 / 12)), Math.ceil(months * (30 / 12))],
[Math.ceil(months * (10 / 12)), Math.ceil(months * (20 / 12))],
[1, Math.ceil(months * (10 / 12))],
[null, 0]
]
return (
<SearchResultTable
headers={headers}
rows={rows}
actionTrigger={row => {
return (
<Popup
content={
<span>
Rajaa opiskelijat ensimmäisen <b>{months}</b> kuukauden aikana saatujen opintopisteiden perusteella:{' '}
<b>({row[0]})</b>
</span>
}
trigger={
<>
<Icon
onClick={() => handleCreditBracketRowClicked(null, row)}
style={{ marginLeft: '10px', cursor: 'pointer' }}
name="filter"
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell collapsing></Table.HeaderCell>
<Table.HeaderCell>Credits Gained During First {months} Months</Table.HeaderCell>
<Table.HeaderCell>
Number of Students
<br />
<span style={{ fontWeight: 100 }}>(n={filteredStudents.length})</span>
</Table.HeaderCell>
<Table.HeaderCell>Percentage of Population</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{limits.map(([min, max]) => (
<Table.Row key={`table-row-${min}-${max}`}>
<Table.Cell collapsing>
<ExternalCreditFilterToggle min={min} max={max} />
</Table.Cell>
<Table.Cell>
{max === 0 ? 0 : `${min} ≤ credits`}
{max > 0 && ` < ${max}`}
</Table.Cell>
<Table.Cell>{studentCount(min, max)}</Table.Cell>
<Table.Cell>
{filteredStudents.length && (
<Progress
percent={Math.round((studentCount(min, max) / filteredStudents.length) * 100)}
progress
className="credit-stats-progress-bar"
/>
<Icon
onClick={() => handleCreditBracketRowClicked(null, row)}
name="add"
size="tiny"
style={{ marginLeft: '-9px', marginRight: '25px', cursor: 'pointer' }}
/>
</>
}
position="top left"
/>
)
}}
noResultText={translate('common.noResults')}
/>
)}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
)
}
const mapStateToProps = state => ({
months: state.populations.query.months,
filters: state.populationFilters.filters.filter(f => f.type === 'CreditsLessThan' || f.type === 'CreditsAtLeast')
})
CreditsGainedTab.propTypes = {
translate: func.isRequired,
sample: arrayOf(object).isRequired,
months: number.isRequired,
setPopulationFilter: func.isRequired,
removePopulationFilter: func.isRequired,
filters: arrayOf(object).isRequired
filteredStudents: PropTypes.arrayOf(PropTypes.object).isRequired
}
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(CreditsGainedTab)
export default CreditsGainedTab
@@ -1,75 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useLocation } from 'react-router-dom'
import { Table, Progress } from 'semantic-ui-react'
import { getMonths } from '../../../common/query'
import { getStudentTotalCredits } from '../../../common'
import ExternalCreditFilterToggle from './ExternalCreditFilterToggle'
const CreditsGainedTab = ({ filteredStudents }) => {
const months = getMonths(useLocation())
const creditList = filteredStudents.map(student => getStudentTotalCredits(student))
const studentCount = (min, max = Infinity) =>
max === 0
? creditList.filter(credits => credits === 0).length
: creditList.filter(credits => credits < max && credits >= min).length
const limits = [
[Math.ceil(months * (55 / 12)), null],
[Math.ceil(months * (50 / 12)), Math.ceil(months * (55 / 12))],
[Math.ceil(months * (40 / 12)), Math.ceil(months * (50 / 12))],
[Math.ceil(months * (30 / 12)), Math.ceil(months * (40 / 12))],
[Math.ceil(months * (20 / 12)), Math.ceil(months * (30 / 12))],
[Math.ceil(months * (10 / 12)), Math.ceil(months * (20 / 12))],
[1, Math.ceil(months * (10 / 12))],
[null, 0]
]
return (
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell collapsing></Table.HeaderCell>
<Table.HeaderCell>Credits Gained During First {months} Months</Table.HeaderCell>
<Table.HeaderCell>
Number of Students
<br />
<span style={{ fontWeight: 100 }}>(n={filteredStudents.length})</span>
</Table.HeaderCell>
<Table.HeaderCell>Percentage of Population</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{limits.map(([min, max]) => (
<Table.Row key={`table-row-${min}-${max}`}>
<Table.Cell collapsing>
<ExternalCreditFilterToggle min={min} max={max} />
</Table.Cell>
<Table.Cell>
{max === 0 ? 0 : `${min} ≤ credits`}
{max > 0 && ` < ${max}`}
</Table.Cell>
<Table.Cell>{studentCount(min, max)}</Table.Cell>
<Table.Cell>
{filteredStudents.length && (
<Progress
percent={Math.round((studentCount(min, max) / filteredStudents.length) * 100)}
progress
className="credit-stats-progress-bar"
/>
)}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
)
}
CreditsGainedTab.propTypes = {
filteredStudents: PropTypes.arrayOf(PropTypes.object).isRequired
}
export default CreditsGainedTab
@@ -5,7 +5,7 @@ import infotooltips from '../../../common/InfoToolTips'
import { useTabChangeAnalytics } from '../../../common/hooks'
import InfoBox from '../../InfoBox'
import StatisticsTab from './StatisticsTab'
import NewCreditsGainedTab from './NewCreditsGainedTab'
import CreditsGainedTab from './CreditsGainedTab'
import './creditGainStats.css'
const CreditGainStats = ({ filteredStudents, translate }) => {
@@ -14,7 +14,7 @@ const CreditGainStats = ({ filteredStudents, translate }) => {
const renderCreditsGainTab = useCallback(() => {
return (
<Tab.Pane attached={false}>
<NewCreditsGainedTab filteredStudents={filteredStudents} />
<CreditsGainedTab filteredStudents={filteredStudents} />
</Tab.Pane>
)
}, [filteredStudents, translate])
@@ -12,6 +12,7 @@ import InfoBox from '../InfoBox'
import infoTooltips from '../../common/InfoToolTips'
import CreditGainStats from './CreditGainStats'
import useFilters from '../FilterTray/useFilters'
import useFilterTray from '../FilterTray/useFilterTray'
const PopulationDetails = ({
samples,
@@ -23,6 +24,7 @@ const PopulationDetails = ({
mandatoryToggle
}) => {
const { allStudents, filteredStudents } = useFilters()
const [trayOpen] = useFilterTray('filterTray')
// TODO: Refactor this away from children:
const selectedStudents = filteredStudents.map(stu => stu.studentNumber)
@@ -60,6 +62,7 @@ const PopulationDetails = ({
label={samples.label}
maxCredits={samples.maxCredits}
selectedStudents={selectedStudents}
trayOpen={trayOpen}
/>
)
return (
@@ -1,102 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Dropdown, Icon, Form, Segment, Button } from 'semantic-ui-react'
import { shape, func, string } from 'prop-types'
import InfoBox from '../InfoBox'
import infoTooltips from '../../common/InfoToolTips'
import { canceledStudyright } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import Track from './tracking'
class CanceledStudyright extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
studyrights: shape({ programme: string, degree: string, studyTrack: string }).isRequired
}
state = {
cancel: 'true'
}
options = [{ value: 'false', text: 'have' }, { value: 'true', text: "haven't" }]
handleChange = (e, { value }) => {
this.setState({ cancel: value })
}
handleCancel = () => {
this.props.setPopulationFilter(
canceledStudyright({
studyrights: this.props.studyrights,
cancel: this.state.cancel
})
)
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
render() {
const { filter } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<InfoBox content={infoTooltips.PopulationStatistics.Filters.CanceledStudyright} />
<Form.Group inline>
<Form.Field>
<label>Filter students who </label>
</Form.Field>
<Form.Field>
<Dropdown
fluid
placeholder="have/haven't"
name="complemented"
onChange={this.handleChange}
options={this.options}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<label> enrolled present (n)or absent</label>
</Form.Field>
<Form.Field>
<Button onClick={this.handleCancel}>set filter</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
{filter.params.cancel === 'true' ? (
"Showing students who haven't enrolled present nor absent"
) : (
<span>
<b>Excluded</b> {"students who haven't enrolled present nor absent"}
</span>
)}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ populations }) => ({
studyrights: populations.query.studyRights
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(CanceledStudyright)
@@ -1,91 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { getActiveLanguage } from 'react-localize-redux'
import { Segment, Icon, Popup } from 'semantic-ui-react'
import { shape, func, string } from 'prop-types'
import { removePopulationFilter, alterPopulationCourseFilter } from '../../redux/populationFilters'
import { getTextIn } from '../../common'
import Track from './tracking'
import './populationFilters.css'
class CourseParticipation extends Component {
static propTypes = {
language: string.isRequired,
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
alterPopulationCourseFilter: func.isRequired
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
selectField = field => () => {
const selectedField = this.props.filter.params.field
if (field !== selectedField) {
this.props.alterPopulationCourseFilter(this.props.filter.id, field)
Track.altered(__filename)
}
}
renderFilterSegment = (type, text, active) => (
<Segment
inverted={active(type)}
secondary={active(type)}
onClick={this.selectField(type)}
className="filter-option"
>
{text}
</Segment>
)
render() {
const { filter, language } = this.props
if (filter.notSet) {
return null
}
const { course, field } = filter.params
const selectedField = field
const active = field2 => selectedField === field2
return (
<div className="filter-segment">
<Segment.Group horizontal size="small" className="filter-row">
<Popup
trigger={
<Segment className="filter-title">
<span className="filter-title-text">{getTextIn(course.course.name, language)}</span>
<span className="filter-title-icon">
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
}
content={getTextIn(course.course.name, language)}
/>
{this.renderFilterSegment('all', 'all', active)}
{this.renderFilterSegment('passed', 'passed', active)}
{this.renderFilterSegment('retryPassed', 'passed after fail', active)}
{this.renderFilterSegment('failed', 'failed', active)}
{this.renderFilterSegment('failedMany', 'failed many', active)}
{this.renderFilterSegment('notParticipated', 'not participated', active)}
{this.renderFilterSegment('notParticipatedOrFailed', 'not participated or failed', active)}
</Segment.Group>
</div>
)
}
}
const mapStateToProps = ({ localize }) => ({
language: getActiveLanguage(localize).code
})
export default connect(
mapStateToProps,
{
removePopulationFilter,
alterPopulationCourseFilter
}
)(CourseParticipation)
@@ -1,28 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import { arrayOf, object } from 'prop-types'
import CourseParticipation from './CourseParticipation'
const CourseParticipationFilters = ({ filters }) => {
if (filters.length === 0) {
return null
}
return (
<div>
{filters.map(f => (
<CourseParticipation key={f.id} filter={f} />
))}
</div>
)
}
CourseParticipationFilters.propTypes = {
filters: arrayOf(object).isRequired
}
const mapStateToProps = state => ({
filters: state.populationFilters.filters.filter(f => f.type === 'CourseParticipation')
})
export default connect(mapStateToProps)(CourseParticipationFilters)
@@ -1,108 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Form, Input, Button } from 'semantic-ui-react'
import { shape, func } from 'prop-types'
import { intersection } from 'lodash'
import { courseParticipationNTimes } from '../../populationFilters'
import { removePopulationFilter, alterPopulationCourseFilter, setPopulationFilter } from '../../redux/populationFilters'
import Track from './tracking'
class CourseParticipationNTimes extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
selectedPopulationCourses: shape({}).isRequired
}
state = {
participationAmount: 0
}
handleChange = e => {
this.setState({ participationAmount: e.target.value })
}
studyRights = () => {
const { selectedPopulationCourses } = this.props
return selectedPopulationCourses ? selectedPopulationCourses.query.studyRights : []
}
handleLimit = () => {
this.props.setPopulationFilter(
courseParticipationNTimes({
amount: this.state.participationAmount,
courses: ['375063', '339101']
})
)
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
courseInPopulation = () => intersection(this.studyRights(), ['MH30_001', '320001']).length > 0
render() {
const { filter } = this.props
const { participationAmount } = this.state
if (!this.courseInPopulation()) {
return null
}
const courseName = this.studyRights().includes('MH30_001') ? 'International progress test' : 'Progress test'
if (filter.notSet) {
return (
<Segment>
<Form>
<Form.Group inline>
<Form.Field>
<label>Show only students that have participated in {courseName} less than</label>
</Form.Field>
<Form.Field>
<Input type="number" onChange={this.handleChange} value={participationAmount} />
</Form.Field>
<Form.Field>
<label>times</label>
</Form.Field>
<Form.Field>
<Button onClick={this.handleLimit} disabled={participationAmount === 0}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Participated in
<i> {courseName} </i>
less than <b>{filter.params.amount}</b> times
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ populationSelectedStudentCourses, populationCourses }) => ({
selectedPopulationCourses: populationSelectedStudentCourses.data
? populationSelectedStudentCourses
: populationCourses
})
export default connect(
mapStateToProps,
{
removePopulationFilter,
alterPopulationCourseFilter,
setPopulationFilter
}
)(CourseParticipationNTimes)
@@ -1,82 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Input, Button, Form, Popup } from 'semantic-ui-react'
import { shape, func } from 'prop-types'
import { creditsAtLeast } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import infoTooltips from '../../common/InfoToolTips'
import Track from './tracking'
class CreditsAtLeast extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired
}
state = {
limit: ''
}
handleChange = e => {
if (Number(e.target.value) > -1) {
this.setState({ limit: e.target.value })
}
}
handleLimit = () => {
this.props.setPopulationFilter(creditsAtLeast({ credit: this.state.limit }))
this.setState({ limit: '' })
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
render() {
const { filter } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.CreditsAtLeast}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Show only students with credits at least</label>
</Form.Field>
<Form.Field>
<Input type="number" onChange={this.handleChange} value={this.state.limit} />
</Form.Field>
<Form.Field>
<Button onClick={this.handleLimit} disabled={this.state.limit.length === 0}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Credits at least {filter.params.credit}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
export default connect(
null,
{ setPopulationFilter, removePopulationFilter }
)(CreditsAtLeast)
@@ -1,82 +0,0 @@
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Input, Button, Form, Popup } from 'semantic-ui-react'
import { func, shape } from 'prop-types'
import infoTooltips from '../../common/InfoToolTips'
import { creditsBeforeStudyright } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import Track from './tracking'
const CreditsBeforeStudyright = ({ filter, removePopulationFilterAction, setPopulationFilterAction }) => {
const [limit, setLimit] = useState('')
const handleChange = e => {
if (Number(e.target.value) > -1) {
setLimit(e.target.value)
}
}
const handleLimit = () => {
setPopulationFilterAction(creditsBeforeStudyright({ credit: limit }))
setLimit('')
Track.set(__filename)
}
const clearFilter = () => {
removePopulationFilterAction(filter.id)
Track.cleared(__filename)
}
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.CreditsBeforeStudyright}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Show only students with credits more than</label>
</Form.Field>
<Form.Field>
<Input type="number" onChange={handleChange} value={limit} />
</Form.Field>
<Form.Field>
<label>before start of studyright</label>
</Form.Field>
<Form.Field>
<Button onClick={handleLimit} disabled={limit.length === 0}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Credits more than {filter.params.credit} before studyright
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={clearFilter} />
</span>
</Segment>
)
}
CreditsBeforeStudyright.propTypes = {
setPopulationFilterAction: func.isRequired,
removePopulationFilterAction: func.isRequired,
filter: shape({}).isRequired
}
export default connect(
null,
{
setPopulationFilterAction: setPopulationFilter,
removePopulationFilterAction: removePopulationFilter
}
)(CreditsBeforeStudyright)
@@ -1,87 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { getActiveLanguage } from 'react-localize-redux'
import { Segment, Icon, Input, Button, Form, Popup } from 'semantic-ui-react'
import { shape, func } from 'prop-types'
import infoTooltips from '../../common/InfoToolTips'
import { creditsLessThan } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import Track from './tracking'
class CreditsLessThan extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired
}
state = {
limit: ''
}
handleChange = e => {
if (Number(e.target.value) > -1) {
this.setState({ limit: e.target.value })
}
}
handleLimit = () => {
this.props.setPopulationFilter(creditsLessThan({ credit: this.state.limit }))
this.setState({ limit: '' })
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
render() {
const { filter } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.CreditsLessThan}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Show only students with credits less than</label>
</Form.Field>
<Form.Field>
<Input type="number" onChange={this.handleChange} value={this.state.limit} />
</Form.Field>
<Form.Field>
<Button onClick={this.handleLimit} disabled={this.state.limit.length === 0}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Credits less than {filter.params.credit}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ localize }) => ({
language: getActiveLanguage(localize).code
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(CreditsLessThan)
@@ -1,95 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Input, Button, Form, Popup } from 'semantic-ui-react'
import { shape, func, arrayOf } from 'prop-types'
import { creditsLessThanFromMandatory } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import infoTooltips from '../../common/InfoToolTips'
import Track from './tracking'
class CreditsLessThanFromMandatory extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
courses: arrayOf(shape({})).isRequired
}
state = {
limit: ''
}
handleChange = e => {
if (Number(e.target.value) > -1) {
this.setState({ limit: e.target.value })
}
}
handleLimit = () => {
this.props.setPopulationFilter(
creditsLessThanFromMandatory({
amount: this.state.limit,
courses: this.props.courses
})
)
this.setState({ limit: '' })
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
render() {
const { filter, courses } = this.props
if (courses.length === 0) return null
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.CreditsLessThanFromMandatory}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Show only students with credits less than </label>
</Form.Field>
<Form.Field>
<Input type="number" onChange={this.handleChange} value={this.state.limit} />
</Form.Field>
<Form.Field>
<label>from mandatory courses</label>
</Form.Field>
<Form.Field>
<Button onClick={this.handleLimit} disabled={this.state.limit.length === 0}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Credits less than {filter.params.amount} from mandatory courses
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ populationMandatoryCourses }) => ({
courses: populationMandatoryCourses.data
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(CreditsLessThanFromMandatory)
@@ -1,171 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Dropdown, Button, Form, Input, Popup } from 'semantic-ui-react'
import { shape, func, arrayOf, object } from 'prop-types'
import { sortBy } from 'lodash'
import infoTooltips from '../../common/InfoToolTips'
import { courseParticipation } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import Track from './tracking'
class DisciplineTypes extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
courseTypes: shape({}),
disciplines: shape({}),
courses: arrayOf(object)
}
state = {
discipline: '',
courseType: '',
atleast: 0
}
handleChange = (e, data) => {
this.setState({ [data.name]: data.value })
}
handleFilter = () => {
const courses = this.props.courses.filter(
({ course, stats }) =>
course.disciplines[this.state.discipline] &&
course.coursetypes[this.state.courseType] &&
stats.students >= this.state.atleast
)
courses.forEach(course => this.props.setPopulationFilter(courseParticipation({ field: 'all', course })))
this.setState({ discipline: '', courseType: '' })
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
render() {
const { filter, courseTypes, disciplines } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.DisciplineTypes}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Filter by </label>
</Form.Field>
<Form.Field style={{ width: 135 }}>
<Dropdown
search
fluid
icon={null}
name="courseType"
placeholder="course type"
onChange={this.handleChange}
value={this.state.courseType}
options={sortBy(
Object.entries(courseTypes).map(([value, text]) => ({
text: text.fi,
value
})),
entry => entry.text
)}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<label> of </label>
</Form.Field>
<Form.Field style={{ width: 200 }}>
<Dropdown
search
icon={null}
fluid
name="discipline"
placeholder="discipline"
onChange={this.handleChange}
value={this.state.discipline}
options={sortBy(
Object.entries(disciplines).map(([value, text]) => ({
text: text.fi,
value
})),
entry => entry.text
)}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<label> and at least </label>
</Form.Field>
<Form.Field>
<Input
transparent
maxLength="3"
style={{ width: 30 }}
name="atleast"
onChange={this.handleChange}
value={this.state.atleast}
/>
</Form.Field>
<Form.Field>
<label> participants</label>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleFilter}
disabled={
this.state.discipline.length === 0 ||
this.state.courseType.length === 0 ||
!String(this.state.atleast).match(/^[0-9]+$/)
}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Participants of {filter.params.discipline} {filter.params.courseType}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
DisciplineTypes.defaultProps = {
courses: [],
disciplines: {},
courseTypes: {}
}
const mapStateToProps = ({ populationSelectedStudentCourses, populationCourses }) => {
const selectedPopulationCourses = populationSelectedStudentCourses.data
? populationSelectedStudentCourses
: populationCourses
return {
courseTypes: selectedPopulationCourses.data.coursetypes,
disciplines: selectedPopulationCourses.data.disciplines,
courses: selectedPopulationCourses.data.coursestatistics
}
}
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(DisciplineTypes)
@@ -1,135 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
import { shape, func, object, arrayOf } from 'prop-types'
import moment from 'moment'
import { range } from 'lodash'
import infoTooltips from '../../common/InfoToolTips'
import { enrollmentStatus } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import Track from './tracking'
class EnrollmentStatus extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
samples: arrayOf(object).isRequired
}
state = {
semesters: [],
enrolled: true
}
componentDidMount() {
if (this.props.filter.notSet) {
const options = this.createPossibleOptions(this.props.samples)
this.setState({ options })
}
}
formatSemesterCodeToText = semester => {
const text = moment(moment('1950', 'YYYY').add(semester * 6, 'months')).format('YYYY.MM')
const split = text.split('.')
const formatted = split[1] > 6 ? `Fall ${split[0]}` : `Spring ${split[0]}`
return formatted
}
createPossibleOptions = ({ minDate, maxDate }) => {
const firstSemester = Math.floor(moment(minDate).diff(moment('1950', 'YYYY'), 'months') / 6)
const lastSemester = Math.floor(moment(maxDate).diff(moment('1950', 'YYYY'), 'months') / 6)
const allSemesters = range(firstSemester, lastSemester + 1)
const options = allSemesters.map(semester => ({
key: semester,
text: this.formatSemesterCodeToText(semester),
value: semester
}))
return options
}
handleTime = (e, data) => {
this.setState({ semesters: data.value })
}
handleFilter = () => {
this.props.setPopulationFilter(
enrollmentStatus({
semesters: this.state.semesters,
enrolled: this.state.enrolled
})
)
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
render() {
const { filter } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.EnrollmentStatus}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Select students that were </label>
</Form.Field>
<Form.Field>
<Dropdown
placeholder="select status"
onChange={(e, data) => this.setState({ enrolled: data.value })}
options={[{ key: 1, text: 'present', value: 1 }, { key: 2, text: 'absent', value: 2 }]}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<label> during </label>
</Form.Field>
<Form.Field>
<Dropdown
placeholder="select semesters"
fluid
multiple
options={this.state.options}
onChange={this.handleTime}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<Button onClick={this.handleFilter} disabled={this.state.semesters.length === 0}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Students that were {filter.params.enrolled === 1 ? 'present' : 'absent'} during{' '}
{filter.params.semesters.map(semester => this.formatSemesterCodeToText(semester)).join(', ')}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
export default connect(
null,
{ setPopulationFilter, removePopulationFilter }
)(EnrollmentStatus)
@@ -1,164 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { getActiveLanguage } from 'react-localize-redux'
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
import { shape, func, arrayOf, object, string } from 'prop-types'
import { sortBy } from 'lodash'
import infoTooltips from '../../common/InfoToolTips'
import { extentGraduated } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import { getTextIn } from '../../common'
import Track from './tracking'
class ExtentGraduated extends Component {
static propTypes = {
language: string.isRequired,
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
extents: arrayOf(object).isRequired,
allStudyRights: arrayOf(object).isRequired
}
state = {
code: undefined,
graduated: undefined,
complemented: 'false'
}
graduationOptions = [{ value: 'grad', text: 'graduated' }, { value: 'either', text: 'studying' }] // illegal to pass boolean values as Dropdown options value :(
complementedOptions = [{ value: 'false', text: 'are' }, { value: 'true', text: 'are not' }]
handleChange = (e, data) => {
this.setState({ [data.name]: data.value })
}
handleLimit = () => {
const { code, graduated, complemented } = this.state
this.props.setPopulationFilter(
extentGraduated({ code, graduated, complemented, isExtent: typeof code === 'number' })
)
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
renderSetText = (extents, filter, allStudyRights) => {
const { language } = this.props
const { code, graduated, complemented, isExtent } = filter.params
let returnText = ''
if (graduated === 'grad') {
returnText = 'students that graduated from'
} else if (graduated === 'either') {
returnText = 'students that are studying '
}
const postText = isExtent
? ` ${getTextIn(extents.find(extent => extent.extentcode === code).name, language)} `
: ` ${allStudyRights.find(sr => sr.value === code).text}`
return complemented === 'true' ? (
<span>
<b>Excluded</b> {returnText}
<b>{postText}</b>
</span>
) : (
<span>
<b>Included</b> {returnText}
<b>{postText}</b>
</span>
)
}
render() {
const { code, graduated, complemented } = this.state
const { filter, extents, language, allStudyRights } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.ExtentGraduated}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>Students that</Form.Field>
<Form.Field>
<Dropdown
fluid
placeholder="are/not"
name="complemented"
onChange={this.handleChange}
options={this.complementedOptions}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<Dropdown
search
fluid
name="graduated"
placeholder="select graduation"
onChange={this.handleChange}
options={this.graduationOptions}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<Dropdown
search
fluid
name="code"
placeholder="select extent"
onChange={this.handleChange}
options={sortBy(
Object.values(extents)
.map(({ extentcode: ecode, name }) => ({
value: ecode,
text: getTextIn(name, language)
}))
.concat(allStudyRights),
entry => entry.text
)}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<Button
disabled={!code || !graduated || !(complemented === 'true' || complemented === 'false')}
onClick={this.handleLimit}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
<label>{this.renderSetText(extents, filter, allStudyRights)}</label>
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ localize }) => ({ language: getActiveLanguage(localize).code })
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(ExtentGraduated)
@@ -1,110 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
import { shape, func } from 'prop-types'
import infoTooltips from '../../common/InfoToolTips'
import { gradeMeanFilter } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import Track from './tracking'
class GradeMeanFilter extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired
}
state = {
gradeMean: 0,
comparator: ''
}
handleFilter = () => {
this.props.setPopulationFilter(
gradeMeanFilter({
gradeMean: this.state.gradeMean,
comparator: this.state.comparator
})
)
this.setState({ gradeMean: 0, comparator: '' })
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
render() {
const { filter } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.GradeMeanFilter}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Select students that have grade mean </label>
</Form.Field>
<Form.Field>
<Dropdown
placeholder="select comparator"
onChange={(e, data) => this.setState({ comparator: data.value })}
options={[
{ key: 1, text: '≤', value: '≤' },
{ key: 2, text: '≥', value: '≥' },
{ key: 3, text: '<', value: '<' },
{ key: 4, text: '>', value: '>' }
]}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<Dropdown
placeholder="select grade mean"
options={[
{ key: 1, text: '1', value: 1 },
{ key: 2, text: '2', value: 2 },
{ key: 3, text: '3', value: 3 },
{ key: 4, text: '4', value: 4 },
{ key: 5, text: '5', value: 5 }
]}
onChange={(e, data) => this.setState({ gradeMean: data.value })}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleFilter}
disabled={this.state.gradeMean === 0 || this.state.comparator.length < 1}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Students that have grade mean {this.props.filter.params.comparator} {this.props.filter.params.gradeMean}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
export default connect(
null,
{ setPopulationFilter, removePopulationFilter }
)(GradeMeanFilter)
@@ -1,106 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Icon, Form, Segment, Button, Confirm, Grid } from 'semantic-ui-react'
import { shape, func } from 'prop-types'
import { removePopulationFilter, setPopulationFilter, deletePopulationFilter } from '../../redux/populationFilters'
import { presetFilter } from '../../populationFilters'
import Track from './tracking'
class Preset extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
deletePopulationFilter: func.isRequired,
destroy: func.isRequired
}
state = { open: false }
handleSetFilter = filter => {
this.props.filter.notSet = false
this.props.setPopulationFilter(presetFilter(filter))
Track.sendFilterEvent('Set preset filter', this.props.filter.name)
}
clearFilter = (destroy = false) => {
Track.sendFilterEvent('Cleared preset filter', this.props.filter.name)
this.props.filter.notSet = true
if (destroy) {
this.props.destroy(this.props.filter.id)
}
this.props.removePopulationFilter(this.props.filter.id)
}
render() {
const { filter } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Form.Group inline>
<Form.Field>
<label>{filter.name}</label>
<em>{filter.description}</em>
</Form.Field>
<Form.Field>
<Button onClick={() => this.handleSetFilter(filter)}>set filter</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
<Grid>
<Grid.Column floated="left" width={12}>
<Form>
<Form.Field inline>
<label>{filter.name}</label>
<em>{filter.description}</em>
</Form.Field>
</Form>
</Grid.Column>
<Grid.Column floated="right">
<Form style={{ float: 'right' }}>
<Form.Group inline>
<Form.Field>
<Icon name="trash" onClick={() => this.setState({ open: true })} />
<Confirm
open={this.state.open}
cancelButton="Just remove from use"
confirmButton="Delete for good"
content="Are you sure you want to delete this filter?"
onCancel={() => {
this.setState({ open: false })
this.clearFilter()
}}
onConfirm={() => {
this.clearFilter(true)
this.props.deletePopulationFilter({ id: filter.id })
this.setState({ open: false })
Track.sendFilterEvent('Delete Preset Filter', filter.name)
}}
size="small"
/>
</Form.Field>
<Form.Field>
<Icon name="remove" onClick={() => this.clearFilter(false)} />
</Form.Field>
</Form.Group>
</Form>
</Grid.Column>
</Grid>
</Segment>
)
}
}
export default connect(
null,
{ removePopulationFilter, setPopulationFilter, deletePopulationFilter }
)(Preset)
@@ -1,183 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { getActiveLanguage } from 'react-localize-redux'
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
import { shape, func, string } from 'prop-types'
import infoTooltips from '../../common/InfoToolTips'
import { priorityStudyright } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import { getTextIn } from '../../common'
import Track from './tracking'
class PriorityStudyright extends Component {
static propTypes = {
language: string.isRequired,
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
populationStatistics: shape({}).isRequired
}
state = {
prioritycode: undefined,
degree: undefined,
programme: undefined
}
graduationOptions = [{ value: 'grad', text: 'graduated' }, { value: 'either', text: 'studying' }] // illegal to pass boolean values as Dropdown options value :(
priorityOptions = [{ value: 1, text: 'primary studies' }, { value: 2, text: 'non-primary studies' }]
handleChange = (e, data) => {
this.setState({ [data.name]: data.value })
}
handleLimit = () => {
const { prioritycode, degree, programme } = this.state
this.props.setPopulationFilter(
priorityStudyright({
prioritycode,
degree,
programme
})
)
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
renderSetText = filter => {
const { language, populationStatistics } = this.props
const { prioritycode, degree, programme } = filter.params
let returnText = 'Showing students that has '
if (degree && programme) {
const degreeCode = populationStatistics.elementdetails.degrees.find(code => code === degree)
const programmeCode = populationStatistics.elementdetails.programmes.find(code => code === programme)
returnText = returnText.concat(`${
degreeCode ? getTextIn(populationStatistics.elementdetails.data[degreeCode].name, language) : ''
} and
${programmeCode ? getTextIn(populationStatistics.elementdetails.data[programmeCode].name, language) : ''}`)
} else if (degree && !programme) {
const degreeCode = populationStatistics.elementdetails.degrees.find(sr => sr.code === degree)
returnText = returnText.concat(
`${degreeCode ? getTextIn(populationStatistics.elementdetails.data[degreeCode].name, language) : ''}`
)
} else if (!degree && programme) {
const programmeCode = populationStatistics.elementdetails.programmes.find(sr => sr.code === programme)
returnText = returnText.concat(
`${programmeCode ? getTextIn(populationStatistics.elementdetails.data[programmeCode].name, language) : ''}`
)
}
returnText = returnText.concat(` as ${this.priorityOptions.find(option => option.value === prioritycode).text} `)
return returnText
}
render() {
const { filter, language, populationStatistics } = this.props
let degreeOptions = populationStatistics.elementdetails.degrees.map(code => ({
value: code,
text: getTextIn(populationStatistics.elementdetails.data[code].name, language)
}))
degreeOptions = [{ value: 'anyDegree', text: 'any degree' }, ...degreeOptions]
let programmeOptions = populationStatistics.elementdetails.programmes.map(code => ({
value: code,
text: getTextIn(populationStatistics.elementdetails.data[code].name, language)
}))
programmeOptions = [{ value: 'anyProgramme', text: 'any programme' }, ...programmeOptions]
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.PriorityStudyright}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Students that has</label>
</Form.Field>
<Form.Field>
<Dropdown
search
fluid
name="degree"
placeholder="degree"
onChange={this.handleChange}
options={degreeOptions}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<label>and</label>
</Form.Field>
<Form.Field>
<Dropdown
search
fluid
name="programme"
placeholder="programme"
onChange={this.handleChange}
options={programmeOptions}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<label>as</label>
</Form.Field>
<Form.Field>
<Dropdown
fluid
placeholder="priority"
name="prioritycode"
onChange={this.handleChange}
options={this.priorityOptions}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleLimit}
disabled={
this.state.prioritycode === undefined ||
(this.state.degree === undefined && this.state.programme === undefined)
}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
<label>{this.renderSetText(filter)}</label>
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ localize, populations }) => ({
language: getActiveLanguage(localize).code,
populationStatistics: populations.data
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(PriorityStudyright)
@@ -1,99 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { getActiveLanguage } from 'react-localize-redux'
import { Radio, Icon, Form, Segment, Button } from 'semantic-ui-react'
import { shape, func, string } from 'prop-types'
import { getTextIn } from '../../common'
import { sexFilter } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import Track from './tracking'
class SexFilter extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
language: string.isRequired
}
state = {
sex: ''
}
handleChange = (e, { value }) => {
this.setState({ sex: value })
}
handleSex = () => {
this.props.setPopulationFilter(sexFilter({ gender: this.state.sex }))
this.setState({ sex: '' })
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
render() {
const { filter, language } = this.props
const genderMale = { fi: 'Mies', en: 'Male', sv: 'Man' }
const genderFemale = { fi: 'Nainen', en: 'Female', sv: 'Kvinna' }
if (filter.notSet) {
return (
<Segment>
<Form>
<Form.Group inline>
<Form.Field>
<label>Filter by gender</label>
</Form.Field>
<Form.Field>
<Radio
name="sex"
onChange={this.handleChange}
value={1}
label={getTextIn(genderMale, language)}
checked={this.state.sex === 1}
/>
<Radio
name="sex"
onChange={this.handleChange}
value={2}
label={getTextIn(genderFemale, language)}
checked={this.state.sex === 2}
/>
</Form.Field>
<Form.Field>
<Button onClick={this.handleSex} disabled={this.state.sex === ''}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
{`Showing only students classified as: "${
filter.params.gender === 1 ? getTextIn(genderMale, language) : getTextIn(genderFemale, language)
}"`}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ localize }) => ({
language: getActiveLanguage(localize).code
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(SexFilter)
@@ -1,98 +0,0 @@
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { getActiveLanguage } from 'react-localize-redux'
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
import { shape, func, string } from 'prop-types'
import infoTooltips from '../../common/InfoToolTips'
import { extentGraduated } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import { getTextIn } from '../../common'
import Track from './tracking'
const SimpleExtentGraduated = props => {
SimpleExtentGraduated.propTypes = {
setPopulationFilter: func.isRequired,
programme: shape({}).isRequired,
removePopulationFilter: func.isRequired,
filter: shape({}).isRequired,
language: string.isRequired,
code: string.isRequired
}
const [complemented, setComplemented] = useState('false') // illegal to pass boolean values as Dropdown options value :(
const complementedOptions = [{ value: 'false', text: 'have' }, { value: 'true', text: 'have not' }]
const handleChange = (e, data) => {
setComplemented(data.value)
}
const handleLimit = () => {
props.setPopulationFilter(extentGraduated({ code: props.code, graduated: 'grad', complemented, simple: true }))
Track.set(__filename)
}
const clearFilter = () => {
props.removePopulationFilter(props.filter.id)
Track.cleared(__filename)
}
const { filter, programme, language } = props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.ExtentGraduated}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Students that</label>
</Form.Field>
<Form.Field>
<Dropdown
fluid
placeholder="have/not"
name="complemented"
onChange={handleChange}
options={complementedOptions}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<label>{`graduated from ${getTextIn(programme, language)}`}</label>
<Form.Field>
<Button onClick={handleLimit}>set filter</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
<label>
{`Showing students that ${
filter.params.complemented === 'true' ? 'have not' : 'have'
} graduated from ${getTextIn(programme, language)}`}
</label>
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={clearFilter} />
</span>
</Segment>
)
}
const mapStateToProps = ({ localize, populations, populationDegreesAndProgrammes }) => {
const code = populations.query.studyRights.programme
const studyrightName = populationDegreesAndProgrammes.data.programmes[code].name
return { language: getActiveLanguage(localize).code, programme: studyrightName, code }
}
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(SimpleExtentGraduated)
@@ -1,101 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Button, Form, Popup, Dropdown } from 'semantic-ui-react'
import { shape, func } from 'prop-types'
import infoTooltips from '../../common/InfoToolTips'
import { startingThisSemester } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import Track from './tracking'
const dropDownOptions = [
{
text: 'started during',
value: 'started'
},
{
text: 'started before ',
value: 'didnotstart'
}
]
class StartingThisSemester extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired
}
state = {
starting: true
}
handleChange = (e, data) => {
this.setState({ starting: data.value === 'started' })
}
handleRadio = () => {
this.props.setPopulationFilter(startingThisSemester({ starting: this.state.starting }))
this.setState({ starting: true })
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
render() {
const { filter } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.StartingThisSemester}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Show only students that</label>
</Form.Field>
<Form.Field>
<Dropdown
search
fluid
name="degree"
defaultValue="started"
onChange={this.handleChange}
options={dropDownOptions}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<label>chosen semester</label>
</Form.Field>
<Form.Field>
<Button onClick={this.handleRadio}>set filter</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
{filter.params.starting ? 'Started during chosen semester' : 'Had started before chosen semester'}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
export default connect(
null,
{ setPopulationFilter, removePopulationFilter }
)(StartingThisSemester)
@@ -1,122 +0,0 @@
import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
import { func, shape, arrayOf, object } from 'prop-types'
import { uniqBy } from 'lodash'
import infoTooltips from '../../common/InfoToolTips'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import { studytrackFilter } from '../../populationFilters'
import { flattenStudyrights } from '../../common'
import Track from './tracking'
const StudytrackFilter = ({
setPopulationFilterAction,
removePopulationFilterAction,
filter,
samples,
studyRights
}) => {
const [options, setOptions] = useState([])
const [selectedStudytracks, setSelectedStudytracks] = useState(null)
const createOptions = () => {
const studytracks = samples.reduce((acc, curr) => {
const newestStudytracks = flattenStudyrights(curr.studyrights, studyRights.programme)
const studentsStudyrightElements = curr.studyrights.flatMap(studyright => studyright.studyright_elements)
const studentsStudytracks = studentsStudyrightElements.filter(
studyrightElement =>
studyrightElement.element_detail.type === 30 && newestStudytracks.includes(studyrightElement.code)
)
if (studentsStudytracks.length > 0) acc.push(...studentsStudytracks)
return acc
}, [])
const uniqueStudytracks = uniqBy(studytracks, 'code')
const filterOptions = uniqueStudytracks.map(studytrack => ({
key: studytrack.code,
text: studytrack.element_detail.name.fi,
value: studytrack.code
}))
setOptions(filterOptions)
}
useEffect(() => {
createOptions()
}, [])
const handleFilter = () => {
setPopulationFilterAction(studytrackFilter({ studytracks: selectedStudytracks, programme: studyRights.programme }))
Track.set(__filename)
}
const handleChange = (e, { value }) => {
const selections = options.filter(option => value.includes(option.key))
setSelectedStudytracks({ codes: value, names: selections.map(selection => selection.text) })
}
const clearFilter = () => {
removePopulationFilterAction(filter.id)
Track.cleared(__filename)
}
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.StudytrackFilter}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Select students that are in </label>
</Form.Field>
<Form.Field style={{ width: '500px' }}>
<Dropdown
placeholder="select studytrack"
options={options}
onChange={handleChange}
selectOnBlur={false}
selectOnNavigation={false}
multiple
fluid
selection
search
/>
</Form.Field>
<Form.Field>
<label> studytrack(s) </label>
</Form.Field>
<Form.Field>
<Button onClick={handleFilter} disabled={!selectedStudytracks}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Students that are in studytrack(s) {filter.params.text.join(', ')}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={clearFilter} />
</span>
</Segment>
)
}
StudytrackFilter.propTypes = {
samples: arrayOf(object).isRequired,
setPopulationFilterAction: func.isRequired,
removePopulationFilterAction: func.isRequired,
filter: shape({}).isRequired,
studyRights: shape({}).isRequired
}
export default connect(
null,
{
setPopulationFilterAction: setPopulationFilter,
removePopulationFilterAction: removePopulationFilter
}
)(StudytrackFilter)
@@ -1,113 +0,0 @@
import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
import { func, shape, arrayOf } from 'prop-types'
import infoTooltips from '../../common/InfoToolTips'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import { tagFilter } from '../../populationFilters'
import Track from './tracking'
const TagFilter = ({ setPopulationFilterAction, removePopulationFilterAction, filter, tags }) => {
const [options, setOptions] = useState([])
const [selectedTag, setSelectedTag] = useState(null)
const [selectedComp, setSelectedComp] = useState(null)
const createOptions = () => {
const createdOptions = tags.map(tag => ({ key: tag.tag_id, text: tag.tagname, value: tag.tag_id }))
setOptions(createdOptions)
}
useEffect(() => {
createOptions()
}, [])
const handleFilter = () => {
setPopulationFilterAction(tagFilter({ tag: selectedTag, comp: selectedComp }))
Track.set(__filename)
}
const handleCompChange = (e, { value }) => {
setSelectedComp(value)
}
const handleChange = (e, { value }) => {
const selection = options.find(tag => tag.value === value)
setSelectedTag(selection)
}
const clearFilter = () => {
removePopulationFilterAction(filter.id)
Track.cleared(__filename)
}
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.TagFilter}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Select students that </label>
</Form.Field>
<Form.Field>
<Dropdown
placeholder="select"
options={[{ key: 1, text: 'have', value: 1 }, { key: 2, text: "don't have", value: 0 }]}
onChange={handleCompChange}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<label>a tag </label>
</Form.Field>
<Form.Field>
<Dropdown
placeholder="select tag"
options={options}
onChange={handleChange}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<Button onClick={handleFilter} disabled={!selectedTag || selectedComp === null}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Students that {filter.params.comp ? 'have' : "don't have"} a tag {filter.params.text}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={clearFilter} />
</span>
</Segment>
)
}
TagFilter.propTypes = {
setPopulationFilterAction: func.isRequired,
removePopulationFilterAction: func.isRequired,
filter: shape({}).isRequired,
tags: arrayOf(shape({})).isRequired
}
const mapStateToProps = ({ tags }) => ({
tags: tags.data
})
export default connect(
mapStateToProps,
{
setPopulationFilterAction: setPopulationFilter,
removePopulationFilterAction: removePopulationFilter
}
)(TagFilter)
@@ -1,170 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { getActiveLanguage } from 'react-localize-redux'
import { Segment, Icon, Dropdown, Button, Form, Popup } from 'semantic-ui-react'
import { shape, func, string } from 'prop-types'
import { sortBy } from 'lodash'
import infoTooltips from '../../common/InfoToolTips'
import { getTextIn } from '../../common'
import { transferFilter } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import Track from './tracking'
const ANYWHERE = { code: 'anywhere', name: { en: 'Anywhere', fi: 'Anywhere', sv: 'Anywhere' } }
class TransferFilter extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
transfers: shape({}).isRequired,
activeLanguage: string.isRequired,
elementDetails: shape({}).isRequired
}
state = {
selectedSource: '',
selectedTarget: ''
}
getName = code => {
const { activeLanguage, elementDetails } = this.props
return getTextIn(code === ANYWHERE.code ? ANYWHERE.name : elementDetails[code].name, activeLanguage)
}
handleChange = (e, data) => {
this.setState({ [data.name]: data.value })
}
handleFilter = () => {
const { selectedSource, selectedTarget } = this.state
this.props.setPopulationFilter(
transferFilter({
source: selectedSource,
target: selectedTarget
})
)
Track.set(__filename)
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
renderFilterText = filter => {
const sourceName = this.getName(filter.params.source)
const targetName = this.getName(filter.params.target)
let returnText = 'Showing students that transferred'
if (filter.params.source !== ANYWHERE.code) {
returnText = returnText.concat(` from ${sourceName}`)
}
if (filter.params.target !== ANYWHERE.code) {
returnText = returnText.concat(` to ${targetName}`)
}
return returnText
}
render() {
const { filter, transfers, activeLanguage, elementDetails } = this.props
const { sources, targets } = transfers
let filteredTargets = Object.keys(targets).map(code => elementDetails[code])
let filteredSources = Object.keys(sources).map(code => elementDetails[code])
if (this.state.selectedSource !== '' && this.state.selectedSource !== 'anywhere') {
filteredTargets = Object.keys(sources[this.state.selectedSource].targets).map(code => elementDetails[code])
}
if (this.state.selectedTarget !== '' && this.state.selectedTarget !== 'anywhere') {
filteredSources = Object.keys(targets[this.state.selectedTarget].sources).map(code => elementDetails[code])
}
filteredTargets.push(ANYWHERE)
filteredSources.push(ANYWHERE)
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.TransferFilter}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Students that transferred from </label>
</Form.Field>
<Form.Field style={{ width: 300 }}>
<Dropdown
search
fluid
icon={null}
name="selectedSource"
placeholder="select source"
onChange={this.handleChange}
value={this.state.courseType}
options={sortBy(
filteredSources.map(({ code, name }) => ({
value: code,
text: getTextIn(name, activeLanguage)
})),
'text'
)}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<label> to </label>
</Form.Field>
<Form.Field style={{ width: 300 }}>
<Dropdown
search
icon={null}
fluid
name="selectedTarget"
placeholder="select target"
onChange={this.handleChange}
value={this.state.discipline}
options={sortBy(
filteredTargets.map(({ code, name }) => ({
value: code,
text: getTextIn(name, activeLanguage)
})),
'text'
)}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleFilter}
disabled={this.state.selectedSource === '' || this.state.selectedTarget === ''}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
{this.renderFilterText(filter)}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ localize, populations }) => ({
activeLanguage: getActiveLanguage(localize).code,
elementDetails: populations.data.elementdetails.data
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(TransferFilter)
@@ -1,106 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { getActiveLanguage } from 'react-localize-redux'
import { Segment, Icon, Button, Form, Popup, Dropdown } from 'semantic-ui-react'
import { shape, func, string } from 'prop-types'
import infoTooltips from '../../common/InfoToolTips'
import { transferTo } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import { getTextIn } from '../../common'
import Track from './tracking'
class TransferToStudyrightFilter extends Component {
static propTypes = {
filter: shape({}).isRequired,
studyrightName: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
language: string.isRequired
}
state = {
negated: 'false'
}
options = [{ value: 'false', text: 'have not' }, { value: 'true', text: 'have' }]
handleRadio = () => {
this.props.setPopulationFilter(transferTo({ negated: this.state.negated === 'true' }))
Track.set(__filename)
}
handleChange = (e, { value }) => {
this.setState({ negated: value })
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
Track.cleared(__filename)
}
render() {
const { filter, studyrightName, language } = this.props
const toggleLabel = `transferred to ${getTextIn(studyrightName, language)}`
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup
content={infoTooltips.PopulationStatistics.Filters.TransferToStudyrightFilter}
trigger={<Icon style={{ float: 'right' }} name="info" />}
/>
<Form.Group inline>
<Form.Field>
<label>Show only students that</label>
</Form.Field>
<Form.Field>
<Dropdown
fluid
placeholder="have/have not"
name="complemented"
onChange={this.handleChange}
options={this.options}
selectOnBlur={false}
selectOnNavigation={false}
/>
</Form.Field>
<Form.Field>
<label>{toggleLabel}</label>
</Form.Field>
<Form.Field>
<Button onClick={this.handleRadio}>set filter</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
{filter.params.negated
? `Have transferred to ${getTextIn(studyrightName, language)}`
: `Have not transferred to ${getTextIn(studyrightName, language)}`}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = state => {
const code = state.populations.query.studyRights.programme
const studyrightName = state.populationDegreesAndProgrammes.data.programmes[code].name
return {
language: getActiveLanguage(state.localize).code,
studyrightName
}
}
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(TransferToStudyrightFilter)
@@ -1,497 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Header, Button, Form, Radio, Modal, Icon, TextArea, Input, Loader } from 'semantic-ui-react'
import { object, func, arrayOf, bool, shape, string } from 'prop-types'
import { union, uniq, difference } from 'lodash'
import uuidv4 from 'uuid/v4'
import { getTranslate, getActiveLanguage } from 'react-localize-redux'
import CreditsLessThan from './CreditsLessThan'
import CreditsLessThanFromMandatory from './CreditsLessThanFromMandatory'
import CreditsAtLeast from './CreditsAtLeast'
import GradeMeanFilter from './GradeMeanFilter'
import StartingThisSemester from './StartingThisSemester'
import CourseParticipation from './CourseParticipation'
import CourseParticipationNTimes from './CourseParticipationNTimes'
import ExtentGraduated from './ExtentGraduated'
import SimpleExtentGraduated from './SimpleExtentGraduated'
import Preset from './Preset'
import SexFilter from './SexFilter'
import TagFilter from './TagFilter'
import DisciplineTypes from './DisciplineTypes'
import EnrollmentStatus from './EnrollmentStatus'
import TransferFilter from './TransferFilter'
import TransferToStudyrightFilter from './TransferToStudyrightFilter'
import CanceledStudyright from './CanceledStudyright'
import PriorityStudyright from './PriorityStudyright'
import CreditsBeforeStudyright from './CreditsBeforeStudyright'
import StudytrackFilter from './StudytrackFilter'
import InfoBox from '../InfoBox'
import infotooltips from '../../common/InfoToolTips'
import {
clearPopulationFilters,
setComplementFilter,
savePopulationFilters,
setPopulationFilter
} from '../../redux/populationFilters'
import { presetFilter, getFilterFunction } from '../../populationFilters'
import { getTextIn, cancelablePromise } from '../../common'
import Track from './tracking'
const componentFor = {
CreditsAtLeast,
CreditsLessThan,
GradeMeanFilter,
CreditsLessThanFromMandatory,
StartingThisSemester,
CourseParticipationNTimes,
EnrollmentStatus,
CourseParticipation,
SexFilter,
TransferToStudyrightFilter,
SimpleExtentGraduated,
TagFilter,
CanceledStudyright,
PriorityStudyright,
DisciplineTypes,
TransferFilter,
ExtentGraduated,
CreditsBeforeStudyright,
StudytrackFilter
}
const advancedFilters = {
// Filters that are too hard to use for common folk
DisciplineTypes,
TransferFilter,
ExtentGraduated,
PriorityStudyright,
CreditsBeforeStudyright
}
const persistantFilters = {
// Filters that can be duplicated with different values
ExtentGraduated,
TransferFilter
}
class PopulationFilters extends Component {
static propTypes = {
filters: arrayOf(object).isRequired,
complemented: bool.isRequired,
clearPopulationFilters: func.isRequired,
setComplementFilter: func.isRequired,
savePopulationFilters: func.isRequired,
setPopulationFilter: func.isRequired,
studyRights: shape({ programme: string, degree: string, studyTrack: string }).isRequired,
populationFilters: shape({}).isRequired,
populationSelectedStudentCourses: shape({}).isRequired,
populationCourses: shape({}).isRequired
}
state = {
visible: false,
presetName: '',
presetDescription: '',
presetFilters: [],
advancedUser: false,
modalOpen: false
}
componentDidMount() {
this.initialFilterLoading()
}
componentWillUnmount() {
if (this.untilCoursesLoaded) this.untilCoursesLoaded.cancel()
if (this.timeout) clearTimeout(this.timeout)
}
initialFilterLoading = async () => {
const untilCoursesLoaded = () => {
const poll = resolve => {
const selectedPopulationCourses = this.props.populationSelectedStudentCourses.data
? this.props.populationSelectedStudentCourses
: this.props.populationCourses
const { data, pending } = selectedPopulationCourses
if (data && !pending) {
resolve()
} else {
this.timeout = setTimeout(() => poll(resolve), 400)
}
}
return new Promise(poll)
}
this.untilCoursesLoaded = cancelablePromise(untilCoursesLoaded())
const success = await this.untilCoursesLoaded.promise
if (success) this.updateFilterList(this.props.populationFilters.filtersFromBackend)
}
formatFilter = filter => {
let filterToSave = {}
if (filter.type === 'Preset') {
filterToSave = {
...filter,
filters: filter.filters.map(f => this.formatFilter(f))
}
} else {
filterToSave = {
...filter,
params:
filter.type === 'CourseParticipation'
? {
field: filter.params.field,
course: {
course: {
name: filter.params.course.course.name,
code: filter.params.course.course.code
}
}
}
: filter.params
}
}
return filterToSave
}
handleSaveFiltersModalSubmit = () => {
const preset = {
id: uuidv4(),
name: this.state.presetName,
description: this.state.presetDescription,
population: Object.values(this.props.studyRights),
filters: this.props.filters
}
this.setState({ presetName: '', presetDescription: '' })
const presetToSave = {
...preset,
filters: preset.filters.map(filter => this.formatFilter(filter))
}
this.props.savePopulationFilters(presetToSave)
this.updateFilterList([preset])
this.props.clearPopulationFilters()
this.props.setPopulationFilter(presetFilter(preset))
this.setState({ modalOpen: false })
Track.sendFilterEvent('Save Filters As Preset', this.state.presetName)
}
handlePresetName = value => {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.setState({ presetName: value })
}, 500)
}
handlePresetDescription = value => {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.setState({ presetDescription: value })
}, 500)
}
updateFilterList(filtersToCreate) {
const selectedPopulationCourses = this.props.populationSelectedStudentCourses.data
? this.props.populationSelectedStudentCourses
: this.props.populationCourses
// sorry for the uglyness but it kinda works (I think)
const regenerateFilterFunctions = (filters /* eslint-disable */) =>
filters.map(f =>
f.type === 'Preset'
? getFilterFunction(
f.type,
{ ...f, filters: regenerateFilterFunctions(f.filters) },
selectedPopulationCourses.data
)
: getFilterFunction(f.type, f.params, selectedPopulationCourses.data)
)
if (filtersToCreate) {
const newFilters = filtersToCreate.map(newFilter => ({
...newFilter,
filters: regenerateFilterFunctions(newFilter.filters)
}))
this.setState({ presetFilters: this.state.presetFilters.concat(newFilters) })
}
}
destroyFromAllFilters = id =>
this.setState({ presetFilters: this.state.presetFilters.filter(filter => filter.id !== id) })
handleClearAllFiltersClicked = () => {
this.props.clearPopulationFilters()
Track.sendFilterEvent('All filters cleared', 'All filters cleared')
}
handleSetComplementFilterClicked = () => {
this.props.setComplementFilter()
Track.sendFilterEvent(
'Toggle Show excluded students only',
this.props.complemented ? 'All students' : 'Excluded only'
)
}
handleAdvancedFiltersChanged = () => {
this.setState({ advancedUser: !this.state.advancedUser })
Track.sendFilterEvent('Toggle Advanced filters', !this.state.advancedUser ? 'Show' : 'Hide')
}
handleAddFilterCancelClicked = () => {
this.setState({ visible: false })
Track.sendFilterEvent('Toggle Add Filters', 'Hide')
}
handleAddFiltersClicked = () => {
this.setState({ visible: true })
Track.sendFilterEvent('Toggle Add Filters', 'Show')
}
handleSaveFiltersClicked = () => {
this.setState({ modalOpen: true })
Track.sendFilterEvent('Save Filters As Preset Modal', 'Shown')
}
handleSaveFiltersModalClosed = () => {
this.setState({ modalOpen: false })
Track.sendFilterEvent('Save Filters As Preset Modal', 'Closed')
}
handleSaveFiltersModalCanceled = () => {
this.setState({ modalOpen: false })
Track.sendFilterEvent('Save Filters As Preset Modal', 'Canceled')
}
renderAddFilters(allStudyRights) {
const {
extents,
transfers,
populationSelectedStudentCourses,
populationCourses,
tags,
exclude,
studyRights
} = this.props
const { Add } = infotooltips.PopulationStatistics.Filters
const selectedPopulationCourses = populationSelectedStudentCourses.data
? populationSelectedStudentCourses
: populationCourses
const allFilters = union(
Object.keys(componentFor)
.filter(f => !(Object.keys(advancedFilters).includes(f) && !this.state.advancedUser))
.map(f => String(f)),
this.state.presetFilters.map(f => f.id).filter(f => this.state.advancedUser)
).filter(f => !exclude.includes(f))
const setFilters = union(
this.props.filters.map(f => f.type),
this.props.filters.filter(f => f.type === 'Preset').map(f => f.id)
)
const unsetFilters = uniq(
difference(allFilters, setFilters.filter(setFilter => !Object.keys(persistantFilters).includes(setFilter)))
)
if (unsetFilters.length === 0) {
return null
}
if (!this.state.visible) {
return (
<>
<Header>
Add filters <InfoBox content={Add} />
</Header>
<Loader active={selectedPopulationCourses.pending} inline="centered" />
<Button onClick={this.handleAddFiltersClicked} disabled={selectedPopulationCourses.pending}>
add
</Button>
</>
)
}
return (
<>
<Header>
Add filters <InfoBox content={Add} />
</Header>
<div>
<Radio
toggle
label="Advanced filters"
checked={this.state.advancedUser}
onChange={this.handleAdvancedFiltersChanged}
/>
</div>
{unsetFilters.map(filterName => {
//eslint-disable-line
if (componentFor[filterName]) {
// THIS IS KINDA HACKED SOLUTION PLS FIX
// this is awful, shame on who ever wrote this, pls fix
// when is this going to be fixed?
// when the time is right, probably
// does this even need fixing though?
return React.createElement(componentFor[filterName], {
filter: { notSet: true },
key: filterName,
samples: this.props.samples,
transfers,
extents,
allStudyRights,
studyRights
})
} else if (!(filterName === 'TagFilter' && tags.length < 1)) {
return React.createElement(Preset, {
filter: {
...this.state.presetFilters.find(f => f.id === filterName),
notSet: true
},
key: filterName,
destroy: this.destroyFromAllFilters
})
}
})}
{exclude.length > 0 ? (
<Segment style={{ textAlign: 'center', backgroundColor: 'whitesmoke' }}>
Currently there are {exclude.length} hidden filter(s) (
{exclude.map(ex => ex.replace(/([a-z0-9])([A-Z])/g, '$1 $2')).join(', ')}) since there are no students that
could be filtered with them
</Segment>
) : null}
<Button onClick={this.handleAddFilterCancelClicked}>cancel</Button>
</>
)
}
renderSetFilters(allStudyRights) {
const setFilters = this.props.filters.map(f => f.type)
const { Filters } = infotooltips.PopulationStatistics.Filters
if (setFilters.length === 0) {
return null
}
return (
<div data-cy="active-filters-list">
<Header>
Filters <InfoBox content={Filters} />
</Header>
{this.props.filters.map(filter => {
if (filter.type !== 'Preset') {
return React.createElement(componentFor[filter.type], {
filter,
key: filter.id,
samples: this.props.samples,
transfers: this.props.transfers,
extents: this.props.extents,
allStudyRights,
studyRights: this.props.studyRights
})
}
return React.createElement(Preset, {
filter,
key: filter.id,
destroy: this.destroyFromAllFilters
})
})}
<Form>
<Form.Group inline>
<Form.Field>
<label>Show excluded students only</label>
</Form.Field>
<Form.Field>
<Radio toggle checked={this.props.complemented} onClick={this.handleSetComplementFilterClicked} />
</Form.Field>
</Form.Group>
</Form>
<Button onClick={this.handleClearAllFiltersClicked}>clear all filters</Button>
{this.state.advancedUser ? (
<Modal
trigger={<Button onClick={this.handleSaveFiltersClicked}>Save filters as preset</Button>}
open={this.state.modalOpen}
onClose={this.handleSaveFiltersModalClosed}
size="small"
>
<Header />
<Modal.Content>
<Form>
<Form.Field>
<h2> Save current filters as preset </h2>
<em> This filter is saved in this population for future use </em>
<Input placeholder="Name..." maxLength={40} onChange={e => this.handlePresetName(e.target.value)} />
</Form.Field>
<Form.Field>
<em> explain what your filter is doing here </em>
<TextArea
placeholder="Description..."
maxLength={160}
onChange={e => this.handlePresetDescription(e.target.value)}
/>
</Form.Field>
</Form>
</Modal.Content>
<Modal.Actions>
<Button negative onClick={this.handleSaveFiltersModalCanceled}>
Cancel
</Button>
<Button
disabled={this.state.presetName === ''}
color="green"
onClick={this.handleSaveFiltersModalSubmit}
inverted
>
<Icon name="checkmark" /> Save
</Button>
</Modal.Actions>
</Modal>
) : null}
</div>
)
}
render() {
const { allStudyRights, language } = this.props
let allStudyRightOptions = []
if (allStudyRights) {
allStudyRightOptions = Object.values(allStudyRights).reduce(
(options, level) => [...options, ...level.map(sr => ({ value: sr.code, text: getTextIn(sr.name, language) }))],
[]
)
}
return (
<div>
{this.renderAddFilters(allStudyRightOptions)}
{this.renderSetFilters(allStudyRightOptions)}
</div>
)
}
}
const mapStateToProps = ({
populationFilters,
localize,
graphSpinner,
populations,
populationSelectedStudentCourses,
populationCourses,
tags
}) => ({
populationSelectedStudentCourses,
populationCourses,
populationFilters,
filters: populationFilters.filters,
complemented: populationFilters.complemented,
translate: getTranslate(localize),
loading: graphSpinner,
language: getActiveLanguage(localize).code,
studyRights: populations.query.studyRights,
allStudyRights: populations.data.studyrights,
extents: populations.data.extents,
transfers: populations.data.transfers,
tags: tags.data
})
export default connect(
mapStateToProps,
{
clearPopulationFilters,
setComplementFilter,
savePopulationFilters,
setPopulationFilter
}
)(PopulationFilters)
@@ -1,35 +0,0 @@
.filter-row {
align-items: stretch;
}
.filter-title {
float: left;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-style: italic;
width: 30% !important;
display: flex;
align-items: center;
justify-content: space-between;
}
.filter-title-text {
overflow: hidden;
text-overflow: ellipsis;
}
.filter-title-icon {
padding-left: 10px;
cursor: pointer;
}
.filter-option {
font-size: 13px !important;
display: flex;
justify-content: center;
align-items: center;
padding: 20px !important;
cursor: pointer;
text-align: center;
}
@@ -1,22 +0,0 @@
import TSA from '../../common/tsa'
const rgx = /\/([^/]+)\.jsx$/
// 'src/components/PopulationFilters/CreditsAtLeast.jsx' -> 'CreditsAtLeast'
const filenameToComponentName = filename => rgx.exec(filename)[1]
const sendFilterEvent = (action, value) => {
TSA.Matomo.sendEvent('Population Filters', action, value)
}
// these are manual-labor-reducing functions for the individual filter components
// takes in __filename from webpack and gets the filter name from that
const set = filename => sendFilterEvent('Set filter', filenameToComponentName(filename))
const altered = filename => sendFilterEvent('Altered filter', filenameToComponentName(filename))
const cleared = filename => sendFilterEvent('Cleared filter', filenameToComponentName(filename))
export default {
set,
altered,
cleared,
sendFilterEvent
}
@@ -13,10 +13,8 @@ import moment from 'moment'
import { getPopulationStatistics, clearPopulations } from '../../redux/populations'
import { getPopulationCourses } from '../../redux/populationCourses'
import { getPopulationSelectedStudentCourses, clearSelected } from '../../redux/populationSelectedStudentCourses'
import { getPopulationFilters, setPopulationFilter, resetPopulationFilters } from '../../redux/populationFilters'
import { getMandatoryCourses } from '../../redux/populationMandatoryCourses'
import { getSemesters } from '../../redux/semesters'
import { transferTo } from '../../populationFilters'
import { getDegreesAndProgrammes } from '../../redux/populationDegreesAndProgrammes'
import { getTagsByStudytrackAction } from '../../redux/tags'
@@ -109,21 +107,17 @@ const PopulationSearchForm = props => {
setState({ isLoading: true })
props.setLoading()
props.clearSelected()
props.resetPopulationFilters()
fetchPopulationPromises.current = cancelablePromise(
Promise.all([
props.getPopulationStatistics({ ...formattedQueryParams, uuid, onProgress }),
props.getPopulationCourses(request),
props.getPopulationFilters(request),
[],
props.getMandatoryCourses(formattedQueryParams.studyRights.programme, props.mandatoryToggle),
props.getTagsByStudytrackAction(query.studyRights.programme)
])
)
const success = await fetchPopulationPromises.current.promise
if (success) {
if (query.studyRights.programme === 'KH50_001') {
props.setPopulationFilter(transferTo(false))
}
setState({
isLoading: false
})
@@ -164,7 +158,6 @@ const PopulationSearchForm = props => {
setDidMount(true)
return () => {
if (fetchPopulationPromises.current) fetchPopulationPromises.current.cancel()
props.resetPopulationFilters()
}
}, [location.search])
@@ -633,9 +626,6 @@ PopulationSearchForm.propTypes = {
getPopulationStatistics: func.isRequired,
getPopulationCourses: func.isRequired,
getMandatoryCourses: func.isRequired,
getPopulationFilters: func.isRequired,
setPopulationFilter: func.isRequired,
resetPopulationFilters: func.isRequired,
queries: shape({}).isRequired,
studyProgrammes: shape({}), //eslint-disable-line
degrees: arrayOf(dropdownType), //eslint-disable-line
@@ -675,10 +665,7 @@ export default withRouter(
getPopulationStatistics,
getPopulationCourses,
getPopulationSelectedStudentCourses,
getPopulationFilters,
getMandatoryCourses,
setPopulationFilter,
resetPopulationFilters,
getDegreesAndProgrammes,
setLoading,
getSemesters,
@@ -10,7 +10,6 @@ import { get as lodashGet } from 'lodash'
import PopulationQueryCard from '../PopulationQueryCard'
import { removePopulation, updatePopulationStudents } from '../../redux/populations'
import { clearPopulationFilters } from '../../redux/populationFilters'
import TSA from '../../common/tsa'
import './populationSearchHistory.css'
@@ -331,7 +330,6 @@ const mapStateToProps = ({ populations, populationDegreesAndProgrammes, localize
const mapDispatchToProps = dispatch => ({
removePopulation: uuid => {
dispatch(removePopulation(uuid))
dispatch(clearPopulationFilters())
},
updatePopulationStudents: students => dispatch(updatePopulationStudents(students))
})
@@ -1,422 +0,0 @@
import uuidv4 from 'uuid/v4'
import { intersection } from 'lodash'
import {
getStudentTotalCredits,
getStudentTotalCreditsFromMandatory,
getStudentGradeMean,
getNewestProgramme,
getHighestGradeOfCourseBetweenRange,
flattenStudyrights
} from '../common'
export const creditsLessThan = params => {
const { credit } = params
return {
id: uuidv4(),
type: 'CreditsLessThan',
params: {
credit
},
filter: student => {
const creditsOfStudent = getStudentTotalCredits(student)
return credit > creditsOfStudent
}
}
}
export const sexFilter = params => {
const { gender } = params
return {
id: uuidv4(),
type: 'SexFilter',
params: {
gender
},
filter: student => student.gender_code === gender
}
}
export const creditsAtLeast = params => {
const { credit } = params
return {
id: uuidv4(),
type: 'CreditsAtLeast',
params: {
credit
},
filter: student => {
const creditsOfStudent = getStudentTotalCredits(student)
return credit <= creditsOfStudent
}
}
}
export const enrollmentStatus = params => {
const { semesters, enrolled } = params
return {
id: uuidv4(),
type: 'EnrollmentStatus',
params: {
semesters,
enrolled
},
filter: student =>
student.semesterenrollments
.filter(({ semestercode }) => semesters.includes(semestercode))
.map(({ enrollmenttype }) => enrollmenttype === enrolled)
.every(b => b === true)
}
}
export const transferFilter = params => {
const { source, target } = params
return {
id: uuidv4(),
type: 'TransferFilter',
params: {
source,
target
},
filter: student =>
student.transfers.some(
transfer =>
(source === 'anywhere' || transfer.sourcecode === source) &&
(target === 'anywhere' || transfer.targetcode === target)
)
}
}
export const startingThisSemester = params => {
const { starting } = params
return {
id: uuidv4(),
type: 'StartingThisSemester',
params: {
starting
},
filter: student => student.starting === starting
}
}
export const transferTo = params => {
const { negated } = params
return {
id: uuidv4(),
type: 'TransferToStudyrightFilter',
params: { negated },
filter: student => (negated ? student.transferredStudyright : !student.transferredStudyright)
}
}
export const courseParticipation = ({ field, course = {} }) => {
let filterFunc = student => (course.students ? course.students[field][student.studentNumber] === true : false)
if (field === 'notParticipated') {
filterFunc = student => (course.students ? !course.students.all[student.studentNumber] : false)
}
if (field === 'notParticipatedOrFailed') {
filterFunc = student =>
course.students
? !course.students.all[student.studentNumber] || course.students.failed[student.studentNumber]
: false
}
return {
id: uuidv4(),
type: 'CourseParticipation',
params: {
field,
course
},
filter: filterFunc
}
}
export const extentGraduated = params => {
const { code, graduated, complemented, isExtent, studyright, simple } = params
return {
id: uuidv4(),
type: simple ? 'SimpleExtentGraduated' : 'ExtentGraduated',
params: {
code,
graduated,
complemented,
studyright,
isExtent,
simple
},
filter: student => {
if (isExtent) {
if (complemented === 'true' && graduated === 'either') {
// jos koodi 7 eli exchange student
const thisStudyright = student.studyrights.find(s =>
s.studyright_elements.map(e => e.code).includes(studyright)
)
return !thisStudyright || thisStudyright.extentcode !== code
}
if (complemented === 'true' && graduated === 'grad') {
return !student.studyrights
.filter(sr => sr.extentcode === code && sr.graduated)
.map(sr => sr.extentcode)
.includes(code)
}
if (complemented === 'false' && graduated === 'either') {
return student.studyrights.map(sr => sr.extentcode).includes(code)
}
return student.studyrights
.filter(sr => sr.extentcode === code && sr.graduated)
.map(sr => sr.extentcode)
.includes(code)
}
const foundStudyRight = student.studyrights.find(s => s.studyright_elements.map(e => e.code).includes(code))
const returnable = graduated !== 'grad' ? !!foundStudyRight : foundStudyRight && foundStudyRight.graduated
return complemented === 'true' ? !returnable : returnable
}
}
}
export const courseParticipationNTimes = params => {
const { amount, courses } = params
return {
id: uuidv4(),
type: 'CourseParticipationNTimes',
params: {
amount,
courses
},
filter: student => student.courses.filter(cr => courses.includes(cr.course.code)).length < amount
}
}
export const canceledStudyright = params => {
const { studyrights, cancel } = params
return {
id: uuidv4(),
type: 'CanceledStudyright',
params: {
studyrights,
cancel
},
filter: student => {
if (cancel === 'true') {
return student.studyrights
.filter(sr => sr.studyright_elements.some(e => Object.values(studyrights).includes(e.code)))
.every(sr => sr.canceldate)
}
return !student.studyrights
.filter(sr => sr.studyright_elements.some(e => Object.values(studyrights).includes(e.code)))
.every(sr => sr.canceldate)
}
}
}
export const creditsLessThanFromMandatory = params => {
const { amount, courses } = params
return {
id: uuidv4(),
type: 'CreditsLessThanFromMandatory',
params: {
amount,
courses
},
filter: student => amount > getStudentTotalCreditsFromMandatory(student, courses)
}
}
export const priorityStudyright = params => {
const { prioritycode, degree, programme } = params
return {
id: uuidv4(),
type: 'PriorityStudyright',
params: {
prioritycode,
degree,
programme
},
filter: student =>
student.studyrights.some(sr => {
if (sr.prioritycode === prioritycode) {
const elements = sr.studyright_elements.map(e => e.code)
let bools = []
if (degree) {
bools = bools.concat(elements.includes(degree) || degree === 'anyDegree')
}
if (programme) {
bools = bools.concat(elements.includes(programme) || programme === 'anyProgramme')
}
return bools.length > 0 ? bools.every(b => b === true) : false
}
return false
})
}
}
export const gradeMeanFilter = params => {
const { gradeMean, comparator } = params
return {
id: uuidv4(),
type: 'GradeMeanFilter',
params: {
gradeMean,
comparator
},
filter: student => {
const gradeMeanOfStudent = getStudentGradeMean(student)
if (comparator === '≤') {
return gradeMeanOfStudent <= gradeMean
}
if (comparator === '≥') {
return gradeMeanOfStudent >= gradeMean
}
if (comparator === '<') {
return gradeMeanOfStudent < gradeMean
}
return gradeMeanOfStudent > gradeMean
}
}
}
export const tagFilter = params => {
const { text, value } = params.tag
const { comp } = params
return {
id: uuidv4(),
type: 'TagFilter',
params: {
text,
comp
},
filter: student => {
const studentTagIds = student.tags.map(t => t.tag.tag_id)
if (comp) {
return studentTagIds.includes(value)
}
return !studentTagIds.includes(value)
}
}
}
export const gradeFilter = params => {
const { coursecodes, grade, coursename, from, to } = params
return {
id: uuidv4(),
type: 'GradeFilter',
params: {
coursecodes,
grade,
coursename
},
filter: student => {
const courses = student.courses.filter(c => coursecodes.includes(c.course_code))
const highestGrade = getHighestGradeOfCourseBetweenRange(courses, from, to)
return highestGrade && highestGrade.grade === grade
}
}
}
export const courseCreditFilter = params => {
const { coursecodes, credits, coursename } = params
return {
id: uuidv4(),
type: 'CourseCreditFilter',
params: {
coursecodes,
credits,
coursename
},
filter: student => {
const course = student.courses.find(c => coursecodes.includes(c.course_code))
return Number(course.credits) === Number(credits)
}
}
}
export const programmeFilter = (params, elementDetails) => {
const { programme, programmeName, studentToTargetCourseDateMap = null } = params
return {
id: uuidv4(),
type: 'ProgrammeFilter',
params: {
programme,
programmeName
},
filter: student => {
const studentStudyrightCode = getNewestProgramme(
student.studyrights,
student.studentNumber,
studentToTargetCourseDateMap,
elementDetails
)
return studentStudyrightCode.code === programme
}
}
}
export const creditsBeforeStudyright = params => {
const { credit } = params
return {
id: uuidv4(),
type: 'CreditsBeforeStudyright',
params: {
credit
},
filter: student => {
const credits = getStudentTotalCredits(student)
return student.credits - credits > credit
}
}
}
export const studytrackFilter = params => {
const { studytracks, programme } = params
return {
id: uuidv4(),
type: 'StudytrackFilter',
params: {
text: studytracks.names,
code: studytracks.codes
},
filter: student => {
const studyrights = flattenStudyrights(student.studyrights, programme)
return intersection(studyrights, studytracks.codes).length > 0
}
}
}
export const presetFilter = preset => ({
id: preset.id,
type: 'Preset',
name: preset.name,
description: preset.description,
filters: preset.filters,
filter: student => preset.filters.map(f => f.filter(student)).every(b => b === true)
})
const typeList = {
CreditsLessThanFromMandatory: creditsLessThanFromMandatory,
CreditsLessThan: creditsLessThan,
CreditsAtLeast: creditsAtLeast,
SexFilter: sexFilter,
GradeMeanFilter: gradeMeanFilter,
CourseParticipation: courseParticipation,
StartingThisSemester: startingThisSemester,
ExtentGraduated: extentGraduated,
SimpleExtentGraduated: extentGraduated,
EnrollmentStatus: enrollmentStatus,
TransferFilter: transferFilter,
CourseParticipationNTimes: courseParticipationNTimes,
CanceledStudyright: canceledStudyright,
Preset: presetFilter,
TransferToStudyrightFilter: transferTo,
PriorityStudyright: priorityStudyright,
TagFilter: tagFilter,
CreditsBeforeStudyright: creditsBeforeStudyright
}
export const getFilterFunction = (type, params, populationCourses) => {
switch (type) {
case 'CourseParticipation':
return courseParticipation({
field: params.field,
course: populationCourses.coursestatistics.find(c => c.course.code === params.course.course.code)
})
default:
return typeList[type](params)
}
}
@@ -5,7 +5,6 @@ import users from './users'
import populations from './populations'
import populationCourses from './populationCourses'
import populationSelectedStudentCourses from './populationSelectedStudentCourses'
import populationFilters from './populationFilters'
import populationDegreesAndProgrammes from './populationDegreesAndProgrammes'
import populationDegreesAndProgrammesUnfiltered from './populationDegreesAndProgrammesUnfiltered'
import populationMandatoryCourses from './populationMandatoryCourses'
@@ -54,7 +53,6 @@ export default combineReducers({
populations,
populationCourses,
populationSelectedStudentCourses,
populationFilters,
populationDegreesAndProgrammes,
populationDegreesAndProgrammesUnfiltered,
populationMandatoryCourses,
@@ -1,208 +0,0 @@
import { courseParticipation } from '../populationFilters'
import { callController } from '../apiConnection'
const getArrayParams = (paramName, entries) => entries.map(entry => `&${paramName}=${entry}`).join('')
export const clearPopulationFilters = () => ({
type: 'CLEAR_POPULATION_FILTERS'
})
export const resetPopulationFilters = () => ({
type: 'RESET_POPULATION_FILTERS'
})
export const setPopulationFilter = filter => ({
type: 'ADD_POPULATION_FILTER',
filter
})
export const alterPopulationCourseFilter = (id, field) => ({
type: 'ALTER_POPULATION_COURSE_FILTER',
id,
field
})
export const removePopulationFilter = id => ({
type: 'REMOVE_POPULATION_FILTER',
id
})
export const removePopulationFilterOfCourse = course => ({
type: 'REMOVE_POPULATION_FILTER_OF_COURSE',
course
})
export const setComplementFilter = () => ({
type: 'SET_COMPLEMENT_FILTER'
})
export const refreshFilters = () => ({
type: 'REFRESH_FILTERS'
})
export const savePopulationFilters = preset => {
const route = '/v2/populationstatistics/filters'
const prefix = 'SAVE_FILTER_'
const data = preset
const method = 'post'
return callController(route, prefix, data, method)
}
export const deletePopulationFilter = preset => {
const route = '/v2/populationstatistics/filters'
const prefix = 'DELETE_FILTER_'
const data = preset
const method = 'delete'
return callController(route, prefix, data, method)
}
export const getPopulationFilters = ({ studyRights }) => {
const route = `/v2/populationstatistics/filters?${getArrayParams('studyRights', studyRights)}`
const prefix = 'GET_FILTER_'
const query = {
studyRights
}
return callController(route, prefix, null, 'get', query)
}
const initial = {
filters: [],
filtersFromBackend: [],
complemented: true,
refreshNeeded: false // used to keep track whether the course table is refreshed or not
}
initial.complemented = false
const reducer = (state = initial, action) => {
switch (action.type) {
case 'ADD_POPULATION_FILTER':
return {
...state,
filters: state.filters.concat(action.filter),
refreshNeeded: true
}
case 'REMOVE_POPULATION_FILTER':
return {
...state,
filters: state.filters.filter(f => f.id !== action.id),
refreshNeeded: true
}
case 'REMOVE_POPULATION_FILTER_OF_COURSE': {
const notRemoved = filter => {
if (filter.type !== 'CourseParticipation') {
return true
}
const { course } = filter.params.course
return (
(course.name.fi !== action.course.name.fi &&
course.name.en !== action.course.name.en &&
course.name.sv !== action.course.name.sv) ||
course.code !== action.course.code
)
}
return {
...state,
filters: state.filters.filter(notRemoved),
refreshNeeded: true
}
}
case 'CLEAR_POPULATION_FILTERS':
return {
...state,
filters: [],
refreshNeeded: true
}
case 'RESET_POPULATION_FILTERS':
return {
...state,
filters: []
}
case 'ALTER_POPULATION_COURSE_FILTER': {
const toAlter = state.filters.find(f => f.id === action.id)
const { course } = toAlter.params
const params = { course, field: action.field }
const alteredFilter = courseParticipation(params)
alteredFilter.id = toAlter.id
return {
...state,
filters: state.filters.map(f => (f.id !== action.id ? f : alteredFilter)),
refreshNeeded: true
}
}
case 'SET_COMPLEMENT_FILTER': {
return {
...state,
complemented: !state.complemented,
refreshNeeded: true
}
}
case 'SAVE_FILTER_ATTEMPT':
return {
...state,
filterPending: true,
error: false
}
case 'SAVE_FILTER_FAILURE':
return {
...state,
filterPending: false,
error: true
}
case 'SAVE_FILTER_SUCCESS':
return {
...state,
filterPending: false,
error: false,
filtersFromBackend: state.filtersFromBackend.concat(action.response)
}
case 'GET_FILTER_ATTEMPT':
return {
...state,
filterPending: true,
error: false
}
case 'GET_FILTER_FAILURE':
return {
...state,
filterPending: false,
error: true
}
case 'GET_FILTER_SUCCESS':
return {
...state,
filterPending: false,
error: false,
filtersFromBackend: action.response
}
case 'DELETE_FILTER_ATTEMPT':
return {
...state,
filterPending: true,
error: false
}
case 'DELETE_FILTER_FAILURE':
return {
...state,
filterPending: false,
error: true
}
case 'DELETE_FILTER_SUCCESS':
return {
...state,
filterPending: false,
error: false,
filtersFromBackend: state.filtersFromBackend.filter(f => f.id !== action.response.id)
}
case 'REFRESH_FILTERS':
return {
...state,
courseTableFilters: state.filters.map(fil => fil.id),
refreshNeeded: false
}
default:
return state
}
}
export default reducer
@@ -1,19 +1,16 @@
import { createSelector } from 'reselect'
import { intersection, difference } from 'lodash'
const getPopulations = state => state.populations
const getFilters = state => state.populationFilters
const makePopulationsToData = createSelector(
[getPopulations, getFilters],
(populations, populationFilters) => {
[getPopulations],
populations => {
const { pending, data, query } = populations
const { programme } = query ? query.studyRights : ''
const samples = pending || !data.students ? [] : data.students
const { complemented } = populationFilters
let selectedStudents = samples.length > 0 ? samples.map(({ studentNumber }) => studentNumber) : []
const complemented = false
const selectedStudents = samples.length > 0 ? samples.map(({ studentNumber }) => studentNumber) : []
const years = []
const selectedStudentsByYear = {}
@@ -27,31 +24,6 @@ const makePopulationsToData = createSelector(
})
}
if (samples.length > 0 && populationFilters.filters.length > 0) {
const studentsForFilter = f => {
return samples.filter(f.filter).map(s => s.studentNumber)
}
const matchingStudents = populationFilters.filters.map(studentsForFilter)
selectedStudents = intersection(...matchingStudents)
if (complemented) {
selectedStudents = difference(samples.map(s => s.studentNumber), selectedStudents)
}
years.forEach(year => {
selectedStudentsByYear[year] = intersection(...matchingStudents, selectedStudentsByYear[year])
})
if (complemented) {
years.forEach(year => {
selectedStudentsByYear[year] = difference(
samples
.filter(student => new Date(student.studyrightStart).getFullYear() === year)
.map(student => student.studentNumber),
selectedStudentsByYear[year]
)
})
}
}
return { samples, selectedStudents, complemented, programme, selectedStudentsByYear }
}
)