diff --git a/backend/docs/Student.md b/backend/docs/Student.md index a42a3aa69e89e4fccf659a809f0ee61bef74ced3..cf5be4bbdcbc8b824b6871333cd6d253fa779801 100644 --- a/backend/docs/Student.md +++ b/backend/docs/Student.md @@ -1,7 +1,7 @@ # Student API ## Get Student Data by User Id -- Endpoint: `/student/{id}` +- Endpoint: `/student/{id}[?includeTranscript="true"|"false"&includeScore="true"|"false"&includeCredits="true"|"false"]` - HTTP Method: `GET` - Response Body (Success): @@ -16,11 +16,137 @@ "nik": "12345677777", "phone": "0812347890", "year": 2020, - "createdAt": "2021-02-17T03:35:10.494Z", - "updatedAt": "2021-02-17T03:35:10.494Z", - "advisorId": "57e19842-d712-45fd-9068-04cde6bf41f3", - "majorId": "d7123456-45fd-9068-0123-04cde6bf41f3", - "userId": "626a872a-7dbf-403a-bca3-83cb9946a9fe" + "createdAt": "2021-03-22T11:10:35.287Z", + "updatedAt": "2021-03-22T11:10:35.287Z", + "advisorId": "a6d2b6b1-7c03-4d9a-a12f-f32809d01639", + "majorId": "476cfe20-e7b2-4537-bdbc-3b76aa226ae3", + "userId": "052b3a5a-d51f-4cb5-ad8e-41ad94803cc5", + "Advisor": { + "id": "a6d2b6b1-7c03-4d9a-a12f-f32809d01639", + "userName": null, + "fullName": null, + "gender": null, + "nip": null, + "nik": null, + "phone": null, + "academicRole": "KAPRODI", + "createdAt": "2021-03-22T11:10:35.263Z", + "updatedAt": "2021-03-22T11:10:35.263Z", + "skillGroupId": "2d0e4681-d984-4ecb-b57f-b9b92fe6d996", + "userId": "052b3a5a-d51f-4cb5-ad8e-41ad94803cc5" + }, + "Major": { + "id": "476cfe20-e7b2-4537-bdbc-3b76aa226ae3", + "numberCode": 235, + "name": "Magister Teknik Informatika", + "createdAt": "2021-03-22T11:10:34.922Z", + "updatedAt": "2021-03-22T11:10:34.922Z", + "facultyId": "f183c826-1b97-4b68-b448-53a0087c8c38", + "Faculty": { + "id": "f183c826-1b97-4b68-b448-53a0087c8c38", + "shortName": "STEI", + "fullName": "Sekolah Teknik Elektro dan Informatika", + "createdAt": "2021-03-22T11:10:34.807Z", + "updatedAt": "2021-03-22T11:10:34.807Z" + } + }, + "studyPlans": [ + { + "id": "94bb4598-d900-4f39-9e63-fd13bbb4c2aa", + "startYear": 2022, + "creditsTotal": 0, + "semester": "2", + "status": "DRAFT", + "notes": null, + "createdAt": "2021-03-22T11:30:01.305Z", + "updatedAt": "2021-03-22T11:30:01.305Z", + "studentId": "070d0c07-f741-4358-b90a-8bfe134e0198", + "studyPlanCourses": [] + }, + { + "id": "54dbfaa8-1af3-448f-b395-f6f873fdbb5d", + "startYear": 2022, + "creditsTotal": 0, + "semester": "1", + "status": "FINAL", + "notes": "LGTM", + "createdAt": "2021-03-22T11:13:28.237Z", + "updatedAt": "2021-03-22T11:18:29.776Z", + "studentId": "070d0c07-f741-4358-b90a-8bfe134e0198", + "studyPlanCourses": [] + }, + { + "id": "b5f12832-3ea8-4312-a077-ad26c1d1dc40", + "startYear": 2021, + "creditsTotal": 2, + "semester": "2", + "status": "FINAL", + "notes": "LGTM", + "createdAt": "2021-03-22T11:10:35.457Z", + "updatedAt": "2021-03-26T06:07:10.417Z", + "studentId": "070d0c07-f741-4358-b90a-8bfe134e0198", + "studyPlanCourses": [ + { + "id": "de473be6-92bd-4f66-a51a-949d69546ef5", + "attendancePercentage": 0, + "score": "T", + "status": "APPROVED", + "createdAt": "2021-03-26T06:07:10.390Z", + "updatedAt": "2021-03-26T06:07:10.390Z", + "courseClassId": "f52290f5-da7d-4d2b-b230-dd335d6a2247", + "studyPlanId": "b5f12832-3ea8-4312-a077-ad26c1d1dc40", + "CourseClass": { + "courseId": "0e3ceed1-d054-4cb4-8396-05ddd58bade8" + } + } + ] + }, + { + "id": "a9e9a9e8-904b-4af5-8fa2-47ca26bb7ea8", + "startYear": 2021, + "creditsTotal": 5, + "semester": "1", + "status": "FINAL", + "notes": "LGTM", + "createdAt": "2021-03-22T11:10:35.444Z", + "updatedAt": "2021-03-22T11:10:35.444Z", + "studentId": "070d0c07-f741-4358-b90a-8bfe134e0198", + "studyPlanCourses": [ + { + "id": "b5f4b392-3a76-4a87-b733-d3730b65f7c9", + "attendancePercentage": 0, + "score": "A", + "status": "APPROVED", + "createdAt": "2021-04-06T12:57:16.089Z", + "updatedAt": "2021-04-06T12:57:16.089Z", + "courseClassId": "5ed2ff48-21ab-4152-a857-70faa5d8cfc4", + "studyPlanId": "a9e9a9e8-904b-4af5-8fa2-47ca26bb7ea8", + "CourseClass": { + "courseId": "937a2f5c-0367-4090-8bc2-c9d9d73e7fed" + } + }, + { + "id": "130b3fba-b1d9-4363-88ea-ba68f91914b0", + "attendancePercentage": 0, + "score": "B", + "status": "APPROVED", + "createdAt": "2021-04-06T12:57:16.108Z", + "updatedAt": "2021-04-06T12:57:16.108Z", + "courseClassId": "b4a2d11c-a0fa-4752-8d38-51e484737e65", + "studyPlanId": "a9e9a9e8-904b-4af5-8fa2-47ca26bb7ea8", + "CourseClass": { + "courseId": "6ecf74c5-e667-4ea4-bc95-f535a7b11a00" + } + } + ] + } + ], + "score": { + "nr": 0, + "ip": 3.5, + "ipk": 3.5 + }, + "creditsTotal": 7 } ``` diff --git a/backend/src/controllers/student.js b/backend/src/controllers/student.js index c241bb08cd967afaab597bdb5e4d1068546e7770..0a8f36102783380f9505db44e803c64d2892921d 100644 --- a/backend/src/controllers/student.js +++ b/backend/src/controllers/student.js @@ -1,41 +1,58 @@ +'use strict'; const { Student, - Lecturer, - Major, - Faculty, } = require('../models/index'); +const { + NotExistError +} = require('../util/error'); + +const { + handleRequestWithInternalServerError, + handleRequestWithResourceItemNotFound +} = require('../util/common'); + +const { + getStudent, + getCreditsTotal, + getHistoricalTranscript, + getStudentIP, + getStudentIPK, + getStudentNR +} = require('../util/db/student'); + exports.getStudentData = async(req, res) => { - const foundStudent = await Student.findOne({ - where: { - userId: req.params.id, - }, - include: [ - { - model: Lecturer, - as: 'Advisor', - }, - { - model: Major, - include: Faculty, - }, - ], - }).catch(err => { - res.status(400) - .json({ - status: 400, - message: 'Invalid uuid input.' - }); - }); + const { opts } = req; - if (foundStudent){ - res.json(foundStudent); - } else { - res.status(400) - .json({ - status: 400, - message: 'Student doesn\'t exist.', - }); + try { + const student = await getStudent(opts); + + if (req.includeTranscript.toLowerCase() === "true"){ + student.dataValues.studyPlans = await getHistoricalTranscript(student.id); + } + + if (req.includeScore.toLowerCase() === "true"){ + const nr = await getStudentNR(student); + const ip = await getStudentIP(student); + const ipk = await getStudentIPK(student); + student.dataValues.score = { + nr, + ip, + ipk + }; + } + + if (req.includeCredits.toLowerCase() === "true"){ + student.dataValues.creditsTotal = await getCreditsTotal(student); + } + + res.json(student); + } catch (error) { + if (error instanceof NotExistError) { + handleRequestWithResourceItemNotFound(res, error); + } else { + handleRequestWithInternalServerError(res, error); + } } }; diff --git a/backend/src/middleware/student.js b/backend/src/middleware/student.js new file mode 100644 index 0000000000000000000000000000000000000000..5beaf21500da99caa517cc075acedb022f69681e --- /dev/null +++ b/backend/src/middleware/student.js @@ -0,0 +1,45 @@ + +const { + Lecturer, + Major, + Faculty +} = require('../models/index'); + +const getStudentMiddleware = async(req, res, next) => { + const { + includeTranscript = "false", + includeScore = "false", + includeCredits = "false" + } = req.query; + + const { + id + } = req.params; + + req.opts = { + where: { + userId: id + } + }; + + req.opts.include = [ + { + model: Lecturer, + as: 'Advisor', + }, + { + model: Major, + include: Faculty, + }, + ]; + + req.includeTranscript = includeTranscript; + req.includeScore = includeScore; + req.includeCredits = includeCredits; + + next(); +}; + +module.exports = { + getStudentMiddleware +}; \ No newline at end of file diff --git a/backend/src/routes/student.js b/backend/src/routes/student.js index bab11558bab7aaf80a70afd34dbddc09f2d398e5..1852a2ce2679f0a46dc19e4fd07097b8c1077e7b 100644 --- a/backend/src/routes/student.js +++ b/backend/src/routes/student.js @@ -2,8 +2,12 @@ var express = require('express'); var router = express.Router(); var studentController = require('../controllers/student'); +const { + getStudentMiddleware +} = require('../middleware/student'); // GET request to get student data +router.get('/:id', getStudentMiddleware); router.get('/:id', studentController.getStudentData); // PUT request to update student data diff --git a/backend/src/test/student.controller.test.js b/backend/src/test/student.controller.test.js index fe3b5ad47a82a893e730fbc02f41088f2ce8efb8..96cb7b5fd6c0f63ac8ea3092228554e9a45bd143 100644 --- a/backend/src/test/student.controller.test.js +++ b/backend/src/test/student.controller.test.js @@ -4,27 +4,36 @@ const Student = require('../models/index')['Student']; const Lecturer = require('../models/index')['Lecturer']; const Major = require('../models/index')['Major']; -let chai = require('chai'); -let chaiHttp = require('chai-http'); -let server = require('../index'); -let should = chai.should(); +const chai = require('chai'); +const chaiHttp = require('chai-http'); +const server = require('../index'); +const should = chai.should(); + +const { + EndpointEnum +} = require('../enums/index'); chai.use(chaiHttp); -let EMAIL = 'ihsan@gmail.com'; -let PASSWORD = 'abc123'; -let SALT = 'salt'; -let DUMMY_ID = '57e19842-d712-45fd-9068-04cde6bf41f3'; -let INVALID_ID = '57e19842-d712-45fd-9068-04cde6bf41f'; -let LECTURER_USERNAME = 'di.aditia'; -let MAJOR_NAME = 'Informatika'; -let USERNAME = 'ihsan.ma'; -let FULLNAME = 'Ihsan Muhammad Asnadi'; -let GENDER = 'M'; -let NIM = '23520019'; -let NIK = '1234567890'; -let PHONE = '0823456789'; -let YEAR = 2020; +const EMAIL = 'ihsan@gmail.com'; +const PASSWORD = 'abc123'; +const SALT = 'salt'; +const DUMMY_ID = '57e19842-d712-45fd-9068-04cde6bf41f3'; +const INVALID_ID = '57e19842-d712-45fd-9068-04cde6bf41f'; +const LECTURER_USERNAME = 'di.aditia'; +const MAJOR_NAME = 'Informatika'; +const USERNAME = 'ihsan.ma'; +const FULLNAME = 'Ihsan Muhammad Asnadi'; +const GENDER = 'M'; +const NIM = '23520019'; +const NIK = '1234567890'; +const PHONE = '0823456789'; +const YEAR = 2020; + +const SUCCESS_CODE = 200; +const SERVER_ERROR_CODE = 500; +const NOT_EXIST_ERROR_CODE = 404; +const ERROR = "Error"; describe('Student Test', () => { @@ -47,16 +56,16 @@ describe('Student Test', () => { salt: SALT, }); - const student = await Student.create({ + await Student.create({ userId: user.id, }); }); it('student get by user id success', (done) => { chai.request(server) - .get('/student/' + user.id) + .get(`${EndpointEnum.STUDENT}/${user.id}`) .end((err, res) => { - res.should.have.status(200); + res.should.have.status(SUCCESS_CODE); res.body.should.have.property('id'); res.body.should.have.property('userName'); res.body.should.have.property('fullName'); @@ -75,25 +84,94 @@ describe('Student Test', () => { it('student get by user id not exist', (done) => { chai.request(server) - .get('/student/' + DUMMY_ID) + .get(`${EndpointEnum.STUDENT}/${DUMMY_ID}`) .end((err, res) => { - res.should.have.status(400); - res.body.should.have.property('status').eql(400); - res.body.should.have.property('message').eql('Student doesn\'t exist.'); + res.should.have.status(NOT_EXIST_ERROR_CODE); + res.body.should.have.property('name').eql(ERROR); + res.body.should.have.property('message').eql('Object doesn\'t exist.'); done(); }); }); - it('student get by user id invalid uuid', (done) => { - chai.request(server) - .get('/student/' + INVALID_ID) - .end((err, res) => { - res.should.have.status(400); - res.body.should.have.property('status').eql(400); - res.body.should.have.property('message').eql('Invalid uuid input.'); - done(); - }); - }); + it('student get by user id invalid uuid', (done) => { + chai.request(server) + .get(`${EndpointEnum.STUDENT}/${INVALID_ID}`) + .end((err, res) => { + res.should.have.status(SERVER_ERROR_CODE); + res.body.should.have.property('name').to.not.eql(null); + res.body.should.have.property('message').to.not.eql(null); + done(); + }); + }); + + it('should get by user id success include historical transcript', (done) => { + chai.request(server) + .get(`${EndpointEnum.STUDENT}/${user.id}?includeTranscript=true`) + .end((err, res) => { + res.should.have.status(SUCCESS_CODE); + res.body.should.have.property('id'); + res.body.should.have.property('userName'); + res.body.should.have.property('fullName'); + res.body.should.have.property('gender'); + res.body.should.have.property('nim'); + res.body.should.have.property('nik'); + res.body.should.have.property('phone'); + res.body.should.have.property('studyPlans'); + res.body.should.have.property('advisorId'); + res.body.should.have.property('createdAt'); + res.body.should.have.property('updatedAt'); + res.body.should.have.property('majorId'); + res.body.should.have.property('userId'); + done(); + }); + }); + + it('should get by user id success include cumulative score', (done) => { + chai.request(server) + .get(`${EndpointEnum.STUDENT}/${user.id}?includeScore=true`) + .end((err, res) => { + res.should.have.status(SUCCESS_CODE); + res.body.should.have.property('id'); + res.body.should.have.property('userName'); + res.body.should.have.property('fullName'); + res.body.should.have.property('gender'); + res.body.should.have.property('nim'); + res.body.should.have.property('nik'); + res.body.should.have.property('phone'); + res.body.should.have.property('score').to.not.eql(null); + res.body.score.should.have.property('nr'); + res.body.score.should.have.property('ip'); + res.body.score.should.have.property('ipk'); + res.body.should.have.property('advisorId'); + res.body.should.have.property('createdAt'); + res.body.should.have.property('updatedAt'); + res.body.should.have.property('majorId'); + res.body.should.have.property('userId'); + done(); + }); + }); + + it('should get by user id success include credits total', (done) => { + chai.request(server) + .get(`${EndpointEnum.STUDENT}/${user.id}?includeCredits=true`) + .end((err, res) => { + res.should.have.status(SUCCESS_CODE); + res.body.should.have.property('id'); + res.body.should.have.property('userName'); + res.body.should.have.property('fullName'); + res.body.should.have.property('gender'); + res.body.should.have.property('nim'); + res.body.should.have.property('nik'); + res.body.should.have.property('phone'); + res.body.should.have.property('creditsTotal').to.not.eql(null); + res.body.should.have.property('advisorId'); + res.body.should.have.property('createdAt'); + res.body.should.have.property('updatedAt'); + res.body.should.have.property('majorId'); + res.body.should.have.property('userId'); + done(); + }); + }); }); describe('/PUT/:id student', () => { @@ -133,7 +211,7 @@ describe('Student Test', () => { }; chai.request(server) - .put('/student/' + user.id) + .put(`${EndpointEnum.STUDENT}/${user.id}`) .send(student) .end((err, res) => { res.should.have.status(200); @@ -145,11 +223,11 @@ describe('Student Test', () => { it('student update by user id not exist', (done) => { chai.request(server) - .get('/student/' + DUMMY_ID) + .get(`${EndpointEnum.STUDENT}/${DUMMY_ID}`) .end((err, res) => { - res.should.have.status(400); - res.body.should.have.property('status').eql(400); - res.body.should.have.property('message').eql('Student doesn\'t exist.'); + res.should.have.status(NOT_EXIST_ERROR_CODE); + res.body.should.have.property('name').eql(ERROR); + res.body.should.have.property('message').eql('Object doesn\'t exist.'); done(); }); }); @@ -168,7 +246,7 @@ describe('Student Test', () => { }; chai.request(server) - .put('/student/' + user.id) + .put(`${EndpointEnum.STUDENT}/${user.id}`) .send(student) .end((err, res) => { res.should.have.status(400); diff --git a/backend/src/util/db/student.js b/backend/src/util/db/student.js new file mode 100644 index 0000000000000000000000000000000000000000..8a87b83076eac0b611d7905db9b9f74d860c70d7 --- /dev/null +++ b/backend/src/util/db/student.js @@ -0,0 +1,147 @@ +const { + StudyPlan, + StudyPlanCourse, + Student, + CourseClass +} = require('../../models/index'); + +const { + ScoreEnum +} = require('../../enums/index'); + +const { + NotExistError, +} = require('../error'); + + +const getStudentIPK = async (student) => { + let cumulativeTotal = 0; + let cumulativeCount = 0; + + let studyPlans = student.studyPlans; + + if (!studyPlans){ + studyPlans = await getHistoricalTranscript(student.id); + } + + for (const studyPlan of studyPlans){ + const {count, total} = getScoreOneStudyPlan(studyPlan); + cumulativeCount += count; + cumulativeTotal += total; + } + + if (cumulativeCount !== 0) return Number((cumulativeTotal/cumulativeCount).toFixed(2)); + + return 0; +} + +const getScoreOneStudyPlan = (studyPlan) => { + let count = 0; + let total = 0; + + for (const studyPlanCourse of studyPlan.studyPlanCourses){ + if (studyPlanCourse.score !== ScoreEnum.T.name ){ + count += 1; + total += ScoreEnum[studyPlanCourse.score].value; + } + } + + return {count, total}; +} + +const getStudentIP = async (student) => { + let total = 0; + let count = 0; + + let studyPlans = student.studyPlans; + + if (!studyPlans){ + studyPlans = await getHistoricalTranscript(student.id); + } + + let courses = [] + + for (const studyPlan of studyPlans){ + for (const studyPlanCourse of studyPlan.studyPlanCourses){ + if (studyPlanCourse.score !== ScoreEnum.T.name && !courses.includes(studyPlanCourse['CourseClass'].courseId)){ + count += 1; + total += ScoreEnum[studyPlanCourse.score].value; + courses.push(studyPlanCourse['CourseClass'].courseId); + } + } + } + + if (count !== 0) return Number((total/count).toFixed(2)); + + return 0; +} + +const getStudentNR = async (student) => { + let studyPlans = student.studyPlans; + + if (!studyPlans){ + studyPlans = await getHistoricalTranscript(student.id); + } + + if (studyPlans.length !== 0){ + const currentStudyPlan = studyPlans[0]; + + const {count, total} = getScoreOneStudyPlan(currentStudyPlan); + + if (count !== 0) return Number((total/count).toFixed(2)); + } + + return 0; +} + +const getCreditsTotal = async (student) => { + let total = 0; + let studyPlans = student.studyPlans; + + if (!studyPlans){ + studyPlans = await getHistoricalTranscript(student.id); + } + + for (const studyPlan of studyPlans){ + total += studyPlan.creditsTotal; + } + + return total; +} + +const getStudent = async (opts) =>{ + const student = await Student.findOne(opts); + + if (!student) throw new NotExistError(); + + return student; +} + +const getHistoricalTranscript = async (studentId) => { + return await StudyPlan.findAll({ + where: { + studentId: studentId + }, + include: [{ + model: StudyPlanCourse, + as: 'studyPlanCourses', + include: [{ + model: CourseClass, + attributes: ['courseId'] + }] + }], + order: [ + ['startYear', 'DESC'], + ['semester', 'DESC'] + ] + }); +} + +module.exports = { + getStudentIPK, + getStudentIP, + getStudentNR, + getCreditsTotal, + getStudent, + getHistoricalTranscript +} \ No newline at end of file