mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd9d015bbd | ||
|
|
7f83066034 | ||
|
|
432c2cefa9 | ||
|
|
79d1f61312 | ||
|
|
8e6400ea08 | ||
|
|
798a24f2ec | ||
|
|
0b9c3ef377 | ||
|
|
3b0005a374 | ||
|
|
fee6994d8b | ||
|
|
d8513141ef | ||
|
|
67ffd42cbe | ||
|
|
4604d6c544 | ||
|
|
438a0cceab | ||
|
|
a4dd87762c | ||
|
|
7e05c9ddb1 | ||
|
|
53593128e2 | ||
|
|
57221b6677 |
Generated
+791
-1159
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
+6
-51
@@ -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)
|
||||
|
||||
+14
-1
@@ -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 => ({
|
||||
|
||||
+5
-38
@@ -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)
|
||||
|
||||
-85
@@ -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)
|
||||
-103
@@ -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)
|
||||
-144
@@ -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)
|
||||
-184
@@ -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)
|
||||
+4
-39
@@ -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>
|
||||
|
||||
+2
-2
@@ -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))
|
||||
|
||||
+62
-150
@@ -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
|
||||
|
||||
-75
@@ -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
|
||||
+2
-2
@@ -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)
|
||||
-28
@@ -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)
|
||||
-108
@@ -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)
|
||||
-82
@@ -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)
|
||||
-95
@@ -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)
|
||||
-98
@@ -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)
|
||||
-101
@@ -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)
|
||||
-106
@@ -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 }
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user