mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
most of the api endpoints completed
This commit is contained in:
+2
-1
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
.vscode
|
||||
.vscode
|
||||
src/tester.js
|
||||
@@ -1 +1,2 @@
|
||||
require('./src/app')
|
||||
require('./src/tester')
|
||||
//require('./src/app')
|
||||
Generated
+2255
-2
File diff suppressed because it is too large
Load Diff
+12
-5
@@ -1,15 +1,19 @@
|
||||
{
|
||||
"name": "nodetest",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"name": "oodikone-backend",
|
||||
"version": "0.0.1",
|
||||
"description": "Backend for Oodikone",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "nodemon index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"author": "Matti Luukkainen",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.16.2",
|
||||
"body-parser": "^1.18.2",
|
||||
"cors": "^2.8.4",
|
||||
"express": "^4.16.2",
|
||||
"install": "^0.10.1",
|
||||
"knex": "^0.13.0",
|
||||
"moment": "^2.19.1",
|
||||
@@ -17,5 +21,8 @@
|
||||
"pg": "^7.3.0",
|
||||
"pg-hstore": "^2.3.2",
|
||||
"sequelize": "^4.13.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^1.12.1"
|
||||
}
|
||||
}
|
||||
|
||||
+85
-50
@@ -1,61 +1,96 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const moment = require('moment')
|
||||
const { Student, Credit, Studyright, sequelize } = require('./models')
|
||||
const Op = Sequelize.Op;
|
||||
const express = require('express')
|
||||
const cors = require('cors')
|
||||
const app = express()
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
const studyrightsStartedAt = (date) => {
|
||||
return Studyright.findAll({
|
||||
limit : 200,
|
||||
include: [ {
|
||||
model: Student,
|
||||
include: [ Credit ]
|
||||
} ],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.eq]: new Date(date)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
const Department = require('./services/departments')
|
||||
const Student = require('./services/students')
|
||||
const Course = require('./services/courses')
|
||||
const Teacher = require('./services/teachers')
|
||||
const Population = require('./services/populations')
|
||||
|
||||
const studentsStartedAtDepartment = (studyrights, dep, startDate) => {
|
||||
const hasNoPreviousStudies = (s) => {
|
||||
app.use(cors({credentials: true, origin: 'http://localhost:8000'}))
|
||||
app.use(bodyParser.json())
|
||||
|
||||
return true
|
||||
app.get('/api/departmentsuccess', async function (req, res) {
|
||||
const startDate = req.query.date? req.query.date.split('.').join('-'): '2005-08-01'
|
||||
const months = 13
|
||||
const results = await Department.averagesInMonths(startDate, months)
|
||||
|
||||
res.json(results)
|
||||
})
|
||||
|
||||
app.get('/api/students', async function (req, res) {
|
||||
let results = []
|
||||
if (req.query.searchTerm) {
|
||||
results = await Student.bySeachTerm(req.query.searchTerm)
|
||||
}
|
||||
|
||||
const hasStarted = (s) => {
|
||||
return s.credits.length>0
|
||||
}
|
||||
res.json(results)
|
||||
})
|
||||
|
||||
return studyrights
|
||||
.filter( s => s.highlevelname === dep )
|
||||
.map( s => s.student )
|
||||
.filter( hasStarted )
|
||||
.filter( hasNoPreviousStudies )
|
||||
}
|
||||
app.get('/api/students/:id', async function (req, res) {
|
||||
const results = await Student.withId(req.params.id)
|
||||
res.json(results)
|
||||
})
|
||||
|
||||
app.get('/api/courses', async function (req, res) {
|
||||
let results = []
|
||||
if (req.query.name) {
|
||||
results = await Course.bySeachTerm(req.query.name)
|
||||
}
|
||||
|
||||
res.json(results)
|
||||
})
|
||||
|
||||
app.post('/api/courselist', async function(req, res) {
|
||||
const results = await Course.instancesOf(req.body.code)
|
||||
|
||||
res.json(results)
|
||||
})
|
||||
|
||||
app.post('/api/coursestatistics', async function(req, res) {
|
||||
const code = req.body.code;
|
||||
const date = req.body.date.split('.').join('-')
|
||||
const months = req.body.subsequentMonthsToInvestigate
|
||||
|
||||
const results = await Course.statisticsOf(code, date, months)
|
||||
res.json(results)
|
||||
})
|
||||
|
||||
app.post('/api/teacherstatistics', async function(req, res) {
|
||||
const courses = req.body.courses.map(c => c.code)
|
||||
const fromDate = req.body.fromDate.split('.').join('-')
|
||||
const toDate = req.body.toDate.split('.').join('-')
|
||||
const minCourses = req.body.minCourses || 1
|
||||
const minStudents = req.body.minStudents || 1
|
||||
const studyRights = req.body.studyRights || 1
|
||||
|
||||
const results = await Teacher.statisticsOf(courses, fromDate, toDate, minCourses, minStudents, studyRights)
|
||||
res.json(results)
|
||||
})
|
||||
|
||||
app.get('/api/studyrightkeywords', async function(req, res) {
|
||||
let results = []
|
||||
if (req.query.search) {
|
||||
results = await Population.studyrightsByKeyword(req.query.search)
|
||||
}
|
||||
|
||||
res.json(results)
|
||||
})
|
||||
|
||||
|
||||
app.get('/api/enrollmentdates', async function(req, res) {
|
||||
const results = await Population.universityEnrolmentDates()
|
||||
res.json(results)
|
||||
})
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const BSC_CS = 'Bachelor of Science, Computer Science'
|
||||
const BSC_MATH = 'Bachelor of Science, Mathematics'
|
||||
|
||||
const startDate = '2005-08-01'
|
||||
|
||||
const studyrights = await studyrightsStartedAt(startDate)
|
||||
|
||||
const cs = studentsStartedAtDepartment(studyrights, BSC_CS, moment(startDate))
|
||||
app.get('*', async function (req, res) {
|
||||
const results = { error: "unknown endpoint" }
|
||||
res.status(404).json(results)
|
||||
})
|
||||
|
||||
cs.forEach( s => console.log(s.studentnumber))
|
||||
|
||||
//console.log(studyrights.map(s=>s.student.studentnumber))
|
||||
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
sequelize.close()
|
||||
}
|
||||
|
||||
main()
|
||||
app.listen(8080, function () {
|
||||
console.log('Example app listening on port 8080!')
|
||||
})
|
||||
+141
-2
@@ -1,11 +1,11 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const moment = require('moment')
|
||||
|
||||
const sequelize = new Sequelize('tkt_oodi', 'tkt_oodi', process.ENV.PW, {
|
||||
host: 'localhost',
|
||||
dialect:'postgres',
|
||||
});
|
||||
|
||||
|
||||
const Student = sequelize.define('student',
|
||||
{
|
||||
studentnumber: {
|
||||
@@ -35,6 +35,44 @@ const Student = sequelize.define('student',
|
||||
}
|
||||
);
|
||||
|
||||
Student.hasNoPreviousStudies = (startDate) => (student) => {
|
||||
const by = (a, b) => moment(a).isSameOrBefore(b) ? -1 : 1
|
||||
const dates = student.credits.map(c=>c.courseinstance.coursedate).sort(by)
|
||||
const earliestCreditDate = dates[0]
|
||||
|
||||
return moment(startDate).isSameOrBefore(earliestCreditDate)
|
||||
}
|
||||
|
||||
Student.hasStarted = (student) => {
|
||||
return student.credits.length > 0
|
||||
}
|
||||
|
||||
const TagStudent = sequelize.define('tag_student',
|
||||
{
|
||||
taggedstudents_studentnumber: { type: Sequelize.STRING },
|
||||
tags_tagname: { type: Sequelize.STRING },
|
||||
},
|
||||
{
|
||||
tableName: 'tag_student',
|
||||
timestamps: false,
|
||||
}
|
||||
);
|
||||
|
||||
TagStudent.removeAttribute('id')
|
||||
|
||||
const Tag = sequelize.define('tag',
|
||||
{
|
||||
tagname: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'tag',
|
||||
timestamps: false,
|
||||
}
|
||||
);
|
||||
|
||||
const Credit = sequelize.define('credit',
|
||||
{
|
||||
id: {
|
||||
@@ -55,6 +93,23 @@ const Credit = sequelize.define('credit',
|
||||
}
|
||||
);
|
||||
|
||||
Credit.inTimeRange = (date, months) => (credit) => {
|
||||
const creditDate = credit.courseinstance.coursedate
|
||||
const monthsFromDate = moment(date).add(months, 'months')
|
||||
|
||||
return moment(creditDate).isBetween(date, monthsFromDate, null, '[]')
|
||||
}
|
||||
|
||||
Credit.notUnnecessary = (credit) => {
|
||||
return credit.credits > 0 && credit.credits <= 12
|
||||
}
|
||||
|
||||
Credit.failed = (credit) =>
|
||||
['Luop', 'Hyl.', 'Eisa', '0'].includes(credit.grade)
|
||||
|
||||
Credit.passed = (credit) =>
|
||||
!['Luop', 'Hyl.', 'Eisa', '0'].includes(credit.grade)
|
||||
|
||||
const Studyright = sequelize.define('studyright',
|
||||
{
|
||||
studyrightid: {
|
||||
@@ -80,10 +135,94 @@ const Studyright = sequelize.define('studyright',
|
||||
}
|
||||
);
|
||||
|
||||
const CourseInstance = sequelize.define('courseinstance',
|
||||
{
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT
|
||||
},
|
||||
coursedate: { type: Sequelize.DATE },
|
||||
course_code: { type: Sequelize.STRING },
|
||||
},
|
||||
{
|
||||
tableName: 'courseinstance',
|
||||
timestamps: false,
|
||||
}
|
||||
);
|
||||
|
||||
const Course = sequelize.define('course',
|
||||
{
|
||||
code: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
name: { type: Sequelize.STRING },
|
||||
},
|
||||
{
|
||||
tableName: 'course',
|
||||
timestamps: false,
|
||||
}
|
||||
);
|
||||
|
||||
const Teacher = sequelize.define('teacher',
|
||||
{
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT
|
||||
},
|
||||
code: { type: Sequelize.STRING },
|
||||
name: { type: Sequelize.STRING },
|
||||
},
|
||||
{
|
||||
tableName: 'teacher',
|
||||
timestamps: false,
|
||||
}
|
||||
);
|
||||
|
||||
const CourseTeacher = sequelize.define('courseteacher',
|
||||
{
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT
|
||||
},
|
||||
teacherrole: { type: Sequelize.STRING },
|
||||
courseinstance_id: { type: Sequelize.BIGINT },
|
||||
teacher_id: { type: Sequelize.BIGINT },
|
||||
},
|
||||
{
|
||||
tableName: 'courseteacher',
|
||||
timestamps: false,
|
||||
}
|
||||
);
|
||||
|
||||
CourseInstance.belongsTo(Course, {foreignKey: 'course_code', targetKey: 'code'});
|
||||
Course.hasMany(CourseInstance, {foreignKey: 'course_code', targetKey: 'code'});
|
||||
|
||||
CourseInstance.hasMany(Credit, {foreignKey: 'courseinstance_id', targetKey: 'id'});
|
||||
Credit.belongsTo(CourseInstance, {foreignKey: 'courseinstance_id', targetKey: 'id'});
|
||||
|
||||
CourseInstance.hasMany(CourseTeacher, {foreignKey: 'courseinstance_id', targetKey: 'id'});
|
||||
|
||||
Credit.belongsTo(Student, {foreignKey: 'student_studentnumber', targetKey: 'studentnumber'});
|
||||
Student.hasMany(Credit, {foreignKey: 'student_studentnumber', sourceKey: 'studentnumber'});
|
||||
|
||||
Student.hasMany(TagStudent, {foreignKey: 'taggedstudents_studentnumber', sourceKey: 'studentnumber'});
|
||||
Tag.hasMany(TagStudent, {foreignKey: 'tags_tagname', sourceKey: 'tagname'});
|
||||
|
||||
// TagStudent belongsTo
|
||||
|
||||
Studyright.belongsTo(Student, {foreignKey: 'student_studentnumber', targetKey: 'studentnumber'});
|
||||
Student.hasMany(Studyright, {foreignKey: 'student_studentnumber', sourceKey: 'studentnumber'});
|
||||
|
||||
module.exports = {Student, Credit, Studyright, sequelize}
|
||||
module.exports = {
|
||||
Student,
|
||||
Credit,
|
||||
Studyright,
|
||||
CourseInstance,
|
||||
Course,
|
||||
TagStudent,
|
||||
Tag,
|
||||
Teacher,
|
||||
CourseTeacher,
|
||||
sequelize
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const moment = require('moment')
|
||||
const { Student, Credit, CourseInstance, Course, TagStudent, Tag, CourseTeacher, sequelize } = require('../models')
|
||||
const { arrayUnique } = require('../util')
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
const byNameOrCode = (searchTerm) => {
|
||||
return Course.findAll({
|
||||
limit: 10,
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{
|
||||
name: {
|
||||
[Op.iLike]: searchTerm
|
||||
}
|
||||
},
|
||||
{
|
||||
code: {
|
||||
[Op.like]: searchTerm
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const instanceStatistics = (code, date) => {
|
||||
return CourseInstance.findOne({
|
||||
include: [
|
||||
{
|
||||
model: Credit,
|
||||
include: [ Student ]
|
||||
}
|
||||
],
|
||||
where: {
|
||||
[Op.and]: [
|
||||
{
|
||||
course_code: {
|
||||
[Op.eq]: code
|
||||
}
|
||||
},
|
||||
{
|
||||
coursedate: {
|
||||
[Op.eq]: new Date(date)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const instancesByCode = (code) => {
|
||||
return CourseInstance.findAll({
|
||||
include: [ Credit, CourseTeacher ],
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.eq]: code
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const byIds = (ids) => {
|
||||
return Student.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Credit,
|
||||
include: [
|
||||
{
|
||||
model: CourseInstance,
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: ids
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function bySeachTerm(term) {
|
||||
const formatCourse = ({name, code}) => ({name, code})
|
||||
|
||||
try {
|
||||
const result = await byNameOrCode(`%${term}%`)
|
||||
return result.map(formatCourse)
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function statisticsOf(code, date, months) {
|
||||
|
||||
const getStudents = ({credits}) => {
|
||||
const all = credits.map(c=>c.student_studentnumber)
|
||||
const pass = credits.filter(Credit.passed).map(c=>c.student_studentnumber)
|
||||
const fail = credits.filter(Credit.failed).map(c=>c.student_studentnumber)
|
||||
return {all, pass, fail}
|
||||
}
|
||||
|
||||
const starYearsOf = (students) => {
|
||||
const years = students.map(s=> moment(s.dateofuniversityenrollment).year()).sort()
|
||||
return years.reduce((map, y) => { map[y] = map[y] ? map[y]+1 : 1; return map}, {})
|
||||
}
|
||||
|
||||
const studentStatsAfter = (studentsStats, date) => {
|
||||
|
||||
const creditsAfter = (student) => {
|
||||
return student.credits
|
||||
.filter(Credit.inTimeRange(date, months))
|
||||
.filter(Credit.passed)
|
||||
.filter(Credit.notUnnecessary)
|
||||
.reduce((set, c) => set+c.credits, 0.0)
|
||||
}
|
||||
|
||||
const toStudent = (set, student) => {
|
||||
set[student.studentnumber] = creditsAfter(student, date)
|
||||
return set
|
||||
}
|
||||
|
||||
return studentsStats.reduce(toStudent, {})
|
||||
}
|
||||
|
||||
try {
|
||||
const instanceStats = await instanceStatistics(code, date)
|
||||
const students = getStudents(instanceStats)
|
||||
const studentStats = await byIds(students.all)
|
||||
|
||||
const all = studentStatsAfter(studentStats.filter(s=>students.all.includes(s.studentnumber)), date)
|
||||
const pass = studentStatsAfter(studentStats.filter(s=>students.pass.includes(s.studentnumber)), date)
|
||||
const fail = studentStatsAfter(studentStats.filter(s=>students.fail.includes(s.studentnumber)), date)
|
||||
return {
|
||||
all, pass, fail,
|
||||
startYear: starYearsOf(instanceStats.credits.map(c=>c.student))
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return {
|
||||
error: e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function instancesOf(code) {
|
||||
const byDate = (a, b) => {
|
||||
return moment(a.coursedate).isSameOrBefore(b.coursedate) ? -1 : 1
|
||||
}
|
||||
|
||||
const formatInstance = (instance) => {
|
||||
return {
|
||||
id: instance.id,
|
||||
date: instance.coursedate,
|
||||
fail: instance.credits.filter(Credit.failed).length,
|
||||
pass: instance.credits.filter(Credit.passed).length,
|
||||
students: instance.credits.length,
|
||||
teachers: instance.courseteachers.map(t=>t.teacher_id).filter(arrayUnique).length
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await instancesByCode(code)
|
||||
return result.sort(byDate).map(formatInstance)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return {
|
||||
error: e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
bySeachTerm, instancesOf, statisticsOf
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const moment = require('moment')
|
||||
const { Student, Credit, Studyright, CourseInstance, sequelize } = require('../models')
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
const studyrightsStartedAt = (date) => {
|
||||
return Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
include: [
|
||||
{
|
||||
model: Credit,
|
||||
include: [ CourseInstance ]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.eq]: new Date(date)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const studentsStartedAtDepartment = (studyrights, department, startDate) => {
|
||||
const departmentMatches = (s) => {
|
||||
const name = s.highlevelname.toUpperCase()
|
||||
return department.toUpperCase().split(' ').every( part => name.indexOf(part)!==-1 )
|
||||
}
|
||||
|
||||
return studyrights
|
||||
.filter( departmentMatches )
|
||||
.map( s => s.student )
|
||||
.filter( Student.hasStarted )
|
||||
.filter( Student.hasNoPreviousStudies(startDate) )
|
||||
}
|
||||
|
||||
const averageGradesInMonths = (students, start, months) => {
|
||||
|
||||
const studentsAverageCreditsInMonths = (sum, student) => {
|
||||
|
||||
const notRepeatEntry = (set, credit) => {
|
||||
const completedCourses = set.map(c => c.courseinstance.course_code)
|
||||
if ( completedCourses.includes(credit.courseinstance.course_code) ) {
|
||||
return set
|
||||
}
|
||||
|
||||
return set.concat(credit)
|
||||
}
|
||||
|
||||
return sum + student.credits
|
||||
.filter(Credit.passed)
|
||||
.filter(Credit.notUnnecessary)
|
||||
.filter(Credit.inTimeRange(start, months))
|
||||
.reduce(notRepeatEntry, [])
|
||||
.reduce((sum, c) => sum+c.credits, 0)
|
||||
}
|
||||
|
||||
return students.reduce(studentsAverageCreditsInMonths, 0.0) / students.length
|
||||
}
|
||||
|
||||
const departments = [
|
||||
{
|
||||
name: 'Chemistry Bachelor',
|
||||
api_name: 'Chemistry'
|
||||
},
|
||||
{
|
||||
name: 'Computer Science Bachelor',
|
||||
api_name: 'Computer Science'
|
||||
},
|
||||
{
|
||||
name: 'Mathematics Bachelor',
|
||||
api_name: 'Mathematics'
|
||||
},
|
||||
{
|
||||
name: 'Physics Bachelor',
|
||||
api_name: 'Physics'
|
||||
},
|
||||
]
|
||||
|
||||
async function averagesInMonths(startDate, months) {
|
||||
try {
|
||||
const studyrights = await studyrightsStartedAt(startDate)
|
||||
|
||||
result = {}
|
||||
departments.forEach( p => {
|
||||
const students = studentsStartedAtDepartment(studyrights, p.name, startDate)
|
||||
|
||||
result[p.api_name] = averageGradesInMonths(students, startDate, months)
|
||||
})
|
||||
|
||||
return result
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return {
|
||||
error: e
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
averagesInMonths, departments
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const moment = require('moment')
|
||||
const { Studyright, Student, Credit, CourseInstance, Course, TagStudent, Tag, CourseTeacher, Teacher, sequelize } = require('../models')
|
||||
const { formatStudent } = require('../services/students')
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
const enrolmentDates = () => {
|
||||
const query = 'SELECT DISTINCT s.dateOfUniversityEnrollment as date FROM Student s'
|
||||
return sequelize.query(query)
|
||||
}
|
||||
|
||||
const studyRightLike = (searchTerm) => {
|
||||
const query = `
|
||||
SELECT DISTINCT highLevelName
|
||||
FROM StudyRight
|
||||
WHERE LOWER(highLevelName)
|
||||
LIKE LOWER('%${searchTerm}%')`
|
||||
return sequelize.query(query, { model: Studyright })
|
||||
}
|
||||
|
||||
const byCriteria = (conf) => {
|
||||
const terms = []
|
||||
|
||||
const enrollmentDateCriterias =
|
||||
conf.enrollmentDates.map( enrollmentDate => (
|
||||
{ // for some reason Op.eq does not work...
|
||||
dateofuniversityenrollment: {
|
||||
[Op.between]: [enrollmentDate, enrollmentDate]
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
terms.push({
|
||||
[Op.or]: enrollmentDateCriterias
|
||||
})
|
||||
|
||||
if ( conf.minBirthDate || conf.maxBirthDate ) {
|
||||
const minBirthDate = conf.minBirthDate || '1900-01-01'
|
||||
const maxBirthDate = conf.maxBirthDate || `${new Date().getFullYear()}-01-01`
|
||||
terms.push({
|
||||
birthdate: {
|
||||
[Op.between]: [minBirthDate, maxBirthDate]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if ( conf.sex && ['male', 'female'].includes(conf.sex)) {
|
||||
terms.push({
|
||||
sex: {
|
||||
[Op.eq]: conf.sex
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if ( conf.matriculationExamination && ['true', 'false'].includes(conf.matriculationExamination) ) {
|
||||
terms.push({
|
||||
matriculationexamination: {
|
||||
[Op.eq]: conf.matriculationExamination === 'true' ? '1' : '0'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if ( conf.studentNumbers && conf.studentNumbers.length>0 ) {
|
||||
terms.push({
|
||||
studentnumber: {
|
||||
[Op.in]: conf.studentNumbers
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let tagWithConstraint = {
|
||||
model: TagStudent
|
||||
}
|
||||
|
||||
if ( conf.tags && conf.tags.length > 0 ) {
|
||||
const tagRules = conf.tags.map(tag => ({ [Op.eq]: tag }) )
|
||||
|
||||
tagWithConstraint.where = {
|
||||
tags_tagname: {
|
||||
[Op.or]: tagRules
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let studyrightWithConstraint = {
|
||||
model: Studyright
|
||||
}
|
||||
|
||||
if ( conf.studyRights && conf.studyRights.length > 0 ) {
|
||||
const studyrightRules = conf.studyRights.map(name => ({ [Op.eq]: name }) )
|
||||
|
||||
studyrightWithConstraint.where = {
|
||||
highlevelname: {
|
||||
[Op.or]: studyrightRules
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Student.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Credit,
|
||||
include: [
|
||||
{
|
||||
model: CourseInstance,
|
||||
include: [ Course ]
|
||||
}
|
||||
]
|
||||
},
|
||||
tagWithConstraint,
|
||||
studyrightWithConstraint
|
||||
],
|
||||
where: {
|
||||
[Op.and] : terms
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const bySelectedCourses = (courses) => (student) => {
|
||||
if (courses.length===0) {
|
||||
return true
|
||||
} else {
|
||||
const passedCourses = student.credits.filter(Credit.passed).map(c=>c.courseinstance.course_code)
|
||||
return courses.every(c => passedCourses.includes(c))
|
||||
}
|
||||
}
|
||||
|
||||
const notAmongExcludes = (conf) => (student) => {
|
||||
if ( conf.excludeStudentsThatHaveNotStartedStudies &&
|
||||
student.credits.length === 0 ) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ( conf.excludeStudentsWithZeroCredits &&
|
||||
student.creditcount === 0 ) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ( conf.excludeStudentsWithPreviousStudies &&
|
||||
Student.hasNoPreviousStudies(student.dateofuniversityenrollment)(student) ) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ( conf.excludedTags && conf.excludedTags.length>0 ) {
|
||||
const noExcludedTags = student.tag_students.every( tag =>
|
||||
conf.excludedTags.includes(tag.tags_tagname)===false
|
||||
)
|
||||
if (!noExcludedTags) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (conf.excludedStudentNumbers &&
|
||||
conf.excludedStudentNumbers.includes(student.studentnumber) ) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const restrictToMonths = (months) => (student) => {
|
||||
const withinTimerange = Credit.inTimeRange(student.dateofuniversityenrollment, months)
|
||||
const creditsWithinTimelimit = student.credits.filter(withinTimerange)
|
||||
|
||||
return {...student, credits: creditsWithinTimelimit }
|
||||
}
|
||||
|
||||
async function studyrightsByKeyword(searchTerm) {
|
||||
const result = await studyRightLike(searchTerm)
|
||||
|
||||
return result.map( s => s.highlevelname )
|
||||
}
|
||||
|
||||
async function universityEnrolmentDates() {
|
||||
const [result, metadata] = await enrolmentDates()
|
||||
|
||||
return result.map( r => r.date ).filter( d => d ).sort()
|
||||
}
|
||||
|
||||
async function statisticsOf(conf) {
|
||||
const students = (await byCriteria(conf))
|
||||
.filter(bySelectedCourses(conf.courses))
|
||||
.filter(notAmongExcludes(conf))
|
||||
.map(restrictToMonths(conf.monthsToStudy))
|
||||
|
||||
return students.map(formatStudent)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
studyrightsByKeyword, universityEnrolmentDates, statisticsOf
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const moment = require('moment')
|
||||
const { Student, Credit, CourseInstance, Course, TagStudent, Tag, sequelize } = require('../models')
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
const byId = (id) => {
|
||||
return Student.findOne({
|
||||
include: [
|
||||
{
|
||||
model: Credit,
|
||||
include: [
|
||||
{
|
||||
model: CourseInstance,
|
||||
include: [ Course ]
|
||||
}
|
||||
]
|
||||
},
|
||||
TagStudent
|
||||
],
|
||||
where: {
|
||||
studentnumber:{
|
||||
[Op.eq]: id
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const byAbreviatedNameOrStudentNumber = (searchTerm) => {
|
||||
return Student.findAll({
|
||||
limit: 10,
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{
|
||||
studentnumber: {
|
||||
[Op.like]: searchTerm
|
||||
}
|
||||
},
|
||||
{
|
||||
abbreviatedname: {
|
||||
[Op.iLike]: searchTerm
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const withCreditsAfter = (ids, date) => {
|
||||
return Student.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Credit,
|
||||
include: [
|
||||
{
|
||||
model: CourseInstance,
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.gte] :date
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: ids
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formatStudent = ({studentnumber, dateofuniversityenrollment, creditcount, credits, tag_students}) => {
|
||||
|
||||
const toCourse = ({grade, credits, status, statuscode, courseinstance}) => {
|
||||
return {
|
||||
course: {
|
||||
code: courseinstance.course_code,
|
||||
name: courseinstance.course.name
|
||||
},
|
||||
date: courseinstance.coursedate,
|
||||
passed: Credit.passed(grade),
|
||||
grade,
|
||||
credits
|
||||
}
|
||||
}
|
||||
|
||||
const tagnames = (tag_students) => {
|
||||
if ( tag_students === undefined || tag_students[0].tags_tagname === null ) {
|
||||
return []
|
||||
}
|
||||
|
||||
return tag_students.map(t=>t.tags_tagname)
|
||||
}
|
||||
|
||||
const byDate = (a, b) => {
|
||||
return moment(a.courseinstance.coursedate).isSameOrBefore(b.courseinstance.coursedate) ? -1 : 1
|
||||
}
|
||||
|
||||
if ( credits === undefined) {
|
||||
credits = []
|
||||
}
|
||||
|
||||
return {
|
||||
studentNumber: studentnumber,
|
||||
started: dateofuniversityenrollment,
|
||||
credits: creditcount,
|
||||
courses: credits.sort(byDate).map(toCourse),
|
||||
tags: tagnames(tag_students)
|
||||
}
|
||||
}
|
||||
|
||||
const addTagToStudent = (id, tag) => {
|
||||
return TagStudent.create({
|
||||
taggedstudents_studentnumber: id,
|
||||
tags_tagname: tag
|
||||
})
|
||||
}
|
||||
|
||||
const findTag = (tag) => {
|
||||
return Tag.findOne({
|
||||
where: {
|
||||
tagname: {
|
||||
[Op.eq]: tag
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const findTagOf = (id, tag) => {
|
||||
return TagStudent.findOne({
|
||||
where: {
|
||||
[Op.and]: [
|
||||
{
|
||||
tags_tagname: {
|
||||
[Op.eq]: tag
|
||||
}
|
||||
},
|
||||
{
|
||||
taggedstudents_studentnumber:{
|
||||
[Op.eq]: id
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteTagStudent = (id, tagname) => {
|
||||
return sequelize.query(
|
||||
`DELETE
|
||||
FROM tag_student
|
||||
WHERE taggedstudents_studentnumber = '${id}'
|
||||
AND tags_tagname = '${tagname}'`
|
||||
)
|
||||
}
|
||||
|
||||
async function bySeachTerm(term) {
|
||||
try{
|
||||
const result = await byAbreviatedNameOrStudentNumber(`%${term}%`)
|
||||
return result.map(formatStudent)
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function withId(id) {
|
||||
try {
|
||||
const result = await byId(id)
|
||||
return formatStudent(result)
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function addTag(id, tagname) {
|
||||
try {
|
||||
const student = await byId(id)
|
||||
const tag = await findTag(tagname);
|
||||
|
||||
if (tag === null) {
|
||||
sequelize.close()
|
||||
return {
|
||||
error: `tag '${tagname}' does not exist`
|
||||
}
|
||||
}
|
||||
|
||||
if (student.tag_students.map(t=>t.tags_tagname).includes(tagname)) {
|
||||
sequelize.close()
|
||||
return {
|
||||
error: `tag '${tagname}' already assosiated with student '${id}'`
|
||||
}
|
||||
}
|
||||
|
||||
return await addTagToStudent(id, tagname)
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTag(id, tagname) {
|
||||
try {
|
||||
const tag = await findTagOf(id, tagname)
|
||||
if (tag === null) {
|
||||
sequelize.close()
|
||||
return {
|
||||
error: `tag '${tagname}' is not assosiated with student '${id}'`
|
||||
}
|
||||
}
|
||||
|
||||
const result = await deleteTagStudent(id, tagname)
|
||||
return {}
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
withId, bySeachTerm, addTag, deleteTag, formatStudent
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const moment = require('moment')
|
||||
const { Student, Studyright, Credit, CourseInstance, Course, TagStudent, Tag, CourseTeacher, Teacher, sequelize } = require('../models')
|
||||
const { arrayUnique } = require('../util')
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
const courseInstancesInBetween = (courses, fromDate, toDate) => {
|
||||
return CourseInstance.findAll({
|
||||
where: {
|
||||
[Op.and]: [
|
||||
{
|
||||
course_code: {
|
||||
[Op.in]: courses
|
||||
}
|
||||
},
|
||||
{
|
||||
coursedate: {
|
||||
[Op.between]: [fromDate, toDate]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: CourseTeacher
|
||||
},
|
||||
{
|
||||
model: Credit,
|
||||
include: [ { model: Student, include: [Studyright] } ]
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const teachersByIds = (ids) => {
|
||||
return Teacher.findAll({
|
||||
where: {
|
||||
id: {
|
||||
[Op.in]: ids
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function statisticsOf(courses, fromDate, toDate, minCourses, minStudents, studyRights) {
|
||||
const extractStatsPerTeacherId = (courses) => {
|
||||
const hasAskedStudyright = (credit) => {
|
||||
if ( studyRights.length==0 ) {
|
||||
return true
|
||||
}
|
||||
const studentsRights = credit.student.studyrights.map(s => s.highlevelname)
|
||||
return studyRights.map( s => studentsRights.includes(s) ).find( s => s===true )
|
||||
}
|
||||
|
||||
const teachers = {}
|
||||
const accountedCredits = {}
|
||||
const attendances = {}
|
||||
|
||||
courses.forEach(course => {
|
||||
// horrible code to prevent duplicate entries
|
||||
const noRepeatedEntry = (credit) => {
|
||||
if ( accountedCredits[credit.student_studentnumber]===undefined ) {
|
||||
accountedCredits[credit.student_studentnumber] = {}
|
||||
}
|
||||
if ( accountedCredits[credit.student_studentnumber][course.course_code] ) {
|
||||
return false
|
||||
}
|
||||
|
||||
accountedCredits[credit.student_studentnumber][course.course_code] = credit
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// even worse code to prevent counting students with twice passed as attendance twice
|
||||
const noRepeatedAttendance = (credit) => {
|
||||
if ( attendances[credit.student_studentnumber]===undefined ) {
|
||||
attendances[credit.student_studentnumber] = {}
|
||||
}
|
||||
|
||||
const attendance = attendances[credit.student_studentnumber][course.course_code]
|
||||
|
||||
if ( attendance===undefined ) {
|
||||
attendances[credit.student_studentnumber][course.course_code] = ''+Credit.passed(credit)
|
||||
return true
|
||||
}
|
||||
|
||||
if ( attendance === 'true' && Credit.passed(credit) ) {
|
||||
return false
|
||||
} else if ( attendance === 'false' && Credit.passed(credit) ) {
|
||||
attendances[credit.student_studentnumber][course.course_code] = ''+Credit.passed(credit)
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const teacher_ids = course.courseteachers.map( t => t.teacher_id ).filter(arrayUnique)
|
||||
|
||||
const creditsWithAskedStudyrights = course.credits.filter(hasAskedStudyright)
|
||||
|
||||
teacher_ids.forEach( teacher_id =>{
|
||||
if ( teachers[teacher_id]===undefined ) {
|
||||
teachers[teacher_id] = {
|
||||
students: 0,
|
||||
passed: 0,
|
||||
courses: 0,
|
||||
points: 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const totalCredits = creditsWithAskedStudyrights
|
||||
.filter(Credit.passed)
|
||||
.filter(noRepeatedEntry)
|
||||
|
||||
const students = creditsWithAskedStudyrights
|
||||
.filter(noRepeatedAttendance).length
|
||||
|
||||
teachers[teacher_id].students += students
|
||||
teachers[teacher_id].courses += 1
|
||||
teachers[teacher_id].passed += totalCredits.length
|
||||
teachers[teacher_id].points += totalCredits.reduce((s,c) => s+c.credits, 0)
|
||||
} )
|
||||
|
||||
})
|
||||
|
||||
Object.keys(teachers).forEach( teacher => {
|
||||
const data = teachers[teacher]
|
||||
if ( data.students<minStudents || data.courses<minCourses ) {
|
||||
delete teachers[teacher]
|
||||
}
|
||||
})
|
||||
|
||||
return teachers
|
||||
}
|
||||
|
||||
const format = (stats, teachers) => {
|
||||
return Object.keys(stats).map( id => {
|
||||
const teacher_stat = stats[id]
|
||||
const teacher = teachers.find( t => t.id === id )
|
||||
return {
|
||||
teacherId: teacher.name,
|
||||
credits: teacher_stat.points,
|
||||
studentsPassed: teacher_stat.passed,
|
||||
studentsTeached: teacher_stat.students,
|
||||
instancesTeached: teacher_stat.courses
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const courseStats = await courseInstancesInBetween(courses, fromDate, toDate)
|
||||
const teacherStats = extractStatsPerTeacherId(courseStats)
|
||||
|
||||
// take into account studyRights
|
||||
|
||||
const teachers = await teachersByIds(Object.keys(teacherStats))
|
||||
return format(teacherStats, teachers)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return {
|
||||
error: e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
statisticsOf
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
function arrayUnique(value, index, self) {
|
||||
return self.indexOf(value) === index;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
arrayUnique
|
||||
}
|
||||
Reference in New Issue
Block a user