From 1ec9562ebffd5725f1d60a96e68d46f29307fea7 Mon Sep 17 00:00:00 2001 From: Ranindya Paramitha <23520019@std.stei.itb.ac.id> Date: Tue, 20 Apr 2021 14:39:28 +0000 Subject: [PATCH] #31 Create Kurikulum API for admin --- backend/docs/Course.md | 97 +++++++++ backend/src/controllers/course.js | 19 ++ backend/src/middleware/course.js | 82 ++++++++ backend/src/models/course.js | 4 + backend/src/routes/course.js | 7 + backend/src/test/course.controller.test.js | 220 ++++++++++++++++++++- backend/src/util/db/course.js | 29 +++ 7 files changed, 450 insertions(+), 8 deletions(-) create mode 100644 backend/src/middleware/course.js create mode 100644 backend/src/util/db/course.js diff --git a/backend/docs/Course.md b/backend/docs/Course.md index 354e5a25..0123b3e6 100644 --- a/backend/docs/Course.md +++ b/backend/docs/Course.md @@ -152,3 +152,100 @@ "message": "invalid input syntax for type uuid: \"517c210a-f35e-4978-ba9f-e38a6addccb\"" } ``` + + +## Create Course Data +- Endpoint: `/course` +- HTTP Method: `POST` +- Request Body (for S1 Course): +```json +{ + "code": "IF5124", + "name": "Kualitas Perangkat Lunak", + "type": "WAJIB", + "credits": 2, + "defaultSemester": 2, + "shortSyllabus": "Review SDLC dan software methodology, agile methodology, OOAD, Design principles, Component design, Configuration management, Continous integration, Service oriented Design, Code Inspection and Code Review", + "completeSyllabus": "Pada kuliah ini mahasiswa dibekali dengan prinsip design perangkat lunak yang “baikâ€, dan menerapkan proses pembangunan (konstruksi) perangkat lunak dalam team, sesuai dengan praktik yang diterapkan di industri dan tools yang banyak digunakan.", + "outcomes": "Mahasiswa mampu untuk menerapkan prinsip design dalam membangun perangkat lunak, dan membangun perangkat lunak sesuai praktik yang baik (studi kasus).", + "curriculumYear": 2013, + "majorId": "10ee3421-21da-46ca-bb2d-c65c9d76b2db" +} +``` + +- Response Body (for S1 Course): +```json +{ + "id": "72a0fbf7-0782-4f9f-9295-1716ff466577", + "code": "IF5124", + "name": "Kualitas Perangkat Lunak", + "type": "WAJIB", + "credits": 2, + "defaultSemester": 2, + "shortSyllabus": "Review SDLC dan software methodology, agile methodology, OOAD, Design principles, Component design, Configuration management, Continous integration, Service oriented Design, Code Inspection and Code Review", + "completeSyllabus": "Pada kuliah ini mahasiswa dibekali dengan prinsip design perangkat lunak yang “baikâ€, dan menerapkan proses pembangunan (konstruksi) perangkat lunak dalam team, sesuai dengan praktik yang diterapkan di industri dan tools yang banyak digunakan.", + "outcomes": "Mahasiswa mampu untuk menerapkan prinsip design dalam membangun perangkat lunak, dan membangun perangkat lunak sesuai praktik yang baik (studi kasus).", + "curriculumYear": 2013, + "majorId": "10ee3421-21da-46ca-bb2d-c65c9d76b2db", + "updatedAt": "2021-04-13T18:19:08.273Z", + "createdAt": "2021-04-13T18:19:08.273Z" +} +``` + +- Request Body (for S2 Course): +```json +{ + "code": "IF5124", + "name": "Kualitas Perangkat Lunak", + "type": "WAJIB", + "credits": 2, + "defaultSemester": 2, + "shortSyllabus": "Review SDLC dan software methodology, agile methodology, OOAD, Design principles, Component design, Configuration management, Continous integration, Service oriented Design, Code Inspection and Code Review", + "completeSyllabus": "Pada kuliah ini mahasiswa dibekali dengan prinsip design perangkat lunak yang “baikâ€, dan menerapkan proses pembangunan (konstruksi) perangkat lunak dalam team, sesuai dengan praktik yang diterapkan di industri dan tools yang banyak digunakan.", + "outcomes": "Mahasiswa mampu untuk menerapkan prinsip design dalam membangun perangkat lunak, dan membangun perangkat lunak sesuai praktik yang baik (studi kasus).", + "curriculumYear": 2013, + "optionId": "cd23bb93-81e6-4870-8365-73dbeb8fcf09" +} +``` + +- Response Body (for S2 Course): +```json +{ + "id": "94867701-bff1-466b-b5a1-fe9b5d3cef97", + "code": "IF5124", + "name": "Kualitas Perangkat Lunak", + "type": "WAJIB", + "credits": 2, + "defaultSemester": 2, + "shortSyllabus": "Review SDLC dan software methodology, agile methodology, OOAD, Design principles, Component design, Configuration management, Continous integration, Service oriented Design, Code Inspection and Code Review", + "completeSyllabus": "Pada kuliah ini mahasiswa dibekali dengan prinsip design perangkat lunak yang “baikâ€, dan menerapkan proses pembangunan (konstruksi) perangkat lunak dalam team, sesuai dengan praktik yang diterapkan di industri dan tools yang banyak digunakan.", + "outcomes": "Mahasiswa mampu untuk menerapkan prinsip design dalam membangun perangkat lunak, dan membangun perangkat lunak sesuai praktik yang baik (studi kasus).", + "curriculumYear": 2013, + "majorId": "10ee3421-21da-46ca-bb2d-c65c9d76b2db", + "courseS2": [ + { + "id": "eb919101-beba-4c50-a84e-459eb9de8dd9", + "optionId": "cd23bb93-81e6-4870-8365-73dbeb8fcf09", + "courseId": "94867701-bff1-466b-b5a1-fe9b5d3cef97", + "updatedAt": "2021-04-13T18:16:59.269Z", + "createdAt": "2021-04-13T18:16:59.269Z" + } + ], + "updatedAt": "2021-04-13T18:16:59.242Z", + "createdAt": "2021-04-13T18:16:59.242Z" +} +``` + +- Response Body (Failure): +```json +{ + "name": "SequelizeDatabaseError", + "message": "invalid input value for enum \"enum_Courses_type\": \"W\"" +} +``` +```json +{ + "name": "Error", + "message": "Undefined code,name,type" +} +``` diff --git a/backend/src/controllers/course.js b/backend/src/controllers/course.js index 21f85a85..9b6c21c4 100644 --- a/backend/src/controllers/course.js +++ b/backend/src/controllers/course.js @@ -3,10 +3,15 @@ const { Course, Major, Faculty } = require('../models/index'); const { handleRequestWithInvalidRequestBody, handleRequestWithResourceItemNotFound, + handleRequestWithInternalServerError } = require('../util/common'); const { NotExistError } = require('../util/error'); +const { + createCourse +} = require('../util/db/course'); + exports.getAllCourseData = async (req, res) => { let courses = await Course.findAll(); res.json(courses); @@ -33,3 +38,17 @@ exports.getCourseData = async (req, res) => { handleRequestWithResourceItemNotFound(res, new NotExistError()); } }; + +exports.createCourseData = async (req, res) => { + try { + const { newCourse } = req.body; + const course = await createCourse(newCourse); + res.json(course); + } catch (error) { + if (error instanceof NotExistError) { + handleRequestWithResourceItemNotFound(res, error); + } else { + handleRequestWithInternalServerError(res, error); + } + } +}; diff --git a/backend/src/middleware/course.js b/backend/src/middleware/course.js new file mode 100644 index 00000000..300274f7 --- /dev/null +++ b/backend/src/middleware/course.js @@ -0,0 +1,82 @@ +const { + handleRequestWithInvalidRequestBody, + handleRequestWithInternalServerError, + checkRequiredParameter +} = require('../util/common'); + +const { + RequiredParameterUndefinedError, +} = require('../util/error'); + +const { + getOption +} = require('../util/db/option'); + +const createCourseMiddleware = async (req, res, next) => { + let newCourse; + + let { + code, + name, + type, + credits, + defaultSemester, + shortSyllabus, + completeSyllabus, + outcomes, + curriculumYear, + majorId, + optionId + } = req.body; + + + try { + checkRequiredParameter({ code, name, type, credits, curriculumYear }); + checkRequiredParameter({ + oneOf: { + majorId, + optionId + } + }); + + if (optionId){ + const option = await getOption({ + where: { + id: optionId + } + }); + majorId = option.majorId; + } + + newCourse = { + code, + name, + type, + credits, + defaultSemester, + shortSyllabus, + completeSyllabus, + outcomes, + curriculumYear, + majorId, + }; + + if (optionId) { + newCourse.courseS2 = { + optionId + }; + } + + req.body.newCourse = newCourse; + + next(); + } catch (error) { + if (error instanceof RequiredParameterUndefinedError) + handleRequestWithInvalidRequestBody(res, error); + else handleRequestWithInternalServerError(res, error); + } +}; + +module.exports = { + createCourseMiddleware +} \ No newline at end of file diff --git a/backend/src/models/course.js b/backend/src/models/course.js index b075c966..dda9d7a6 100644 --- a/backend/src/models/course.js +++ b/backend/src/models/course.js @@ -14,6 +14,10 @@ module.exports = (sequelize, DataTypes) => { foreignKey: 'courseId', as: 'classes', }); + this.hasMany(models['CourseS2'], { + foreignKey: 'courseId', + as: 'courseS2', + }); } } Course.init( diff --git a/backend/src/routes/course.js b/backend/src/routes/course.js index c19f48fd..f78a0933 100644 --- a/backend/src/routes/course.js +++ b/backend/src/routes/course.js @@ -3,10 +3,17 @@ var router = express.Router(); var courseController = require('../controllers/course'); +const { + createCourseMiddleware +} = require('../middleware/course'); + // GET request to get all faculty data router.get('/', courseController.getAllCourseData); // GET request to get course data by id router.get('/:id', courseController.getCourseData); +router.post('/', createCourseMiddleware); +router.post('/', courseController.createCourseData); + module.exports = router; diff --git a/backend/src/test/course.controller.test.js b/backend/src/test/course.controller.test.js index f54a9128..8449eeeb 100644 --- a/backend/src/test/course.controller.test.js +++ b/backend/src/test/course.controller.test.js @@ -1,4 +1,10 @@ -const Course = require('../models/index')['Course']; +const { + Course, + CourseS2, + Faculty, + Major, + Option +}= require('../models/index'); let chai = require('chai'); let chaiHttp = require('chai-http'); @@ -7,25 +13,56 @@ let should = chai.should(); chai.use(chaiHttp); -const { EndpointEnum } = require('../enums/index'); +const { + EndpointEnum, + CourseTypeEnum +} = require('../enums/index'); -let CODE = 'IF5122'; -let NAME = 'Pembangunan Perangkat Lunak'; +const CODE = 'IF5122'; +const NAME = 'Pembangunan Perangkat Lunak'; +const DEFAULT_SEMESTER = 1; +const TYPE = CourseTypeEnum.MANDATORY; +const CURRICULUM_YEAR = 2013; +const OUTCOMES = 'Outcome'; +const SHORT_SYLLABUS = 'Short syllabus'; +const COMPLETE_SYLLABUS = 'Complete syllabus'; +const CREDITS = 2; +const FACULTY_NAME = 'STEI'; let DUMMY_ID = '57e19842-d712-45fd-9068-04cde6bf41f3'; let INVALID_ID = '57e19842-d712-45fd-9068-04cde6bf41f'; const REQUEST_ERROR_CODE = 400; const OBJECT_NOT_EXIST_ERROR_CODE = 404; const SUCCESS_CODE = 200; +const SERVER_ERROR_CODE = 500; const COURSE_DOESNT_EXIST_ERROR = "Object doesn't exist."; -const COURSE_DOESNT_EXIST_ERROR_NAME = 'Error'; +const ERROR = 'Error'; const INVALID_UUID_ERROR = 'invalid input syntax for type uuid: "' + INVALID_ID + '"'; -const INVALID_UUID_ERROR_NAME = 'SequelizeDatabaseError'; +const SEQUELIZE_DATABASE_ERROR = 'SequelizeDatabaseError'; +const TYPE_UNDEFINED_ERROR = 'Undefined type'; describe('Course Test', () => { + let major, option; + beforeEach(async () => { await Course.destroy({ where: {} }); + await CourseS2.destroy({ where: {} }); + await Faculty.destroy({ where: {} }); + await Major.destroy({ where: {} }); + await Option.destroy({ where: {} }); + + const faculty = await Faculty.create({ + shortName: FACULTY_NAME + }); + + major = await Major.create({ + facultyId: faculty.id + }); + + option = await Option.create({ + majorId: major.id + }); }); afterEach(() => { @@ -94,7 +131,7 @@ describe('Course Test', () => { res.should.have.status(OBJECT_NOT_EXIST_ERROR_CODE); res.body.should.have .property('name') - .eql(COURSE_DOESNT_EXIST_ERROR_NAME); + .eql(ERROR); res.body.should.have .property('message') .eql(COURSE_DOESNT_EXIST_ERROR); @@ -108,10 +145,177 @@ describe('Course Test', () => { .get(EndpointEnum.COURSE + '/' + INVALID_ID) .end((err, res) => { res.should.have.status(REQUEST_ERROR_CODE); - res.body.should.have.property('name').eql(INVALID_UUID_ERROR_NAME); + res.body.should.have.property('name').eql(SEQUELIZE_DATABASE_ERROR); res.body.should.have.property('message').eql(INVALID_UUID_ERROR); done(); }); }); }); + + describe('Creating course data', () => { + it('should return course data when request body S2 valid', (done) => { + const requestBody = { + code: CODE, + name: NAME, + type: TYPE, + credits: CREDITS, + defaultSemester: DEFAULT_SEMESTER, + shortSyllabus: SHORT_SYLLABUS, + completeSyllabus: COMPLETE_SYLLABUS, + outcomes: OUTCOMES, + curriculumYear: CURRICULUM_YEAR, + optionId: option.id + }; + + chai + .request(server) + .post(EndpointEnum.COURSE) + .send(requestBody) + .end((_, res) => { + res.should.have.status(SUCCESS_CODE); + res.body.should.have.property('id').to.not.equal(null); + res.body.should.have.property('code').deep.equal(requestBody.code); + res.body.should.have.property('name').deep.equal(requestBody.name); + res.body.should.have + .property('type') + .deep.equal(requestBody.type); + res.body.should.have + .property('credits') + .deep.equal(requestBody.credits); + res.body.should.have + .property('defaultSemester') + .deep.equal(requestBody.defaultSemester); + res.body.should.have + .property('curriculumYear') + .deep.equal(requestBody.curriculumYear); + res.body.should.have + .property('shortSyllabus') + .deep.equal(requestBody.shortSyllabus); + res.body.should.have + .property('completeSyllabus') + .deep.equal(requestBody.completeSyllabus); + res.body.should.have + .property('outcomes') + .deep.equal(requestBody.outcomes); + res.body.should.have.property('courseS2').to.not.equal(null); + res.body.should.have.property('updatedAt').to.not.equal(null); + res.body.should.have.property('createdAt').to.not.equal(null); + done(); + }); + }); + + it('should return course data when request body S1 valid', (done) => { + const requestBody = { + code: CODE, + name: NAME, + type: TYPE, + credits: CREDITS, + defaultSemester: DEFAULT_SEMESTER, + shortSyllabus: SHORT_SYLLABUS, + completeSyllabus: COMPLETE_SYLLABUS, + outcomes: OUTCOMES, + curriculumYear: CURRICULUM_YEAR, + majorId: major.id + }; + + chai + .request(server) + .post(EndpointEnum.COURSE) + .send(requestBody) + .end((_, res) => { + res.should.have.status(SUCCESS_CODE); + res.body.should.have.property('id').to.not.equal(null); + res.body.should.have.property('code').deep.equal(requestBody.code); + res.body.should.have.property('name').deep.equal(requestBody.name); + res.body.should.have + .property('type') + .deep.equal(requestBody.type); + res.body.should.have + .property('credits') + .deep.equal(requestBody.credits); + res.body.should.have + .property('defaultSemester') + .deep.equal(requestBody.defaultSemester); + res.body.should.have + .property('curriculumYear') + .deep.equal(requestBody.curriculumYear); + res.body.should.have + .property('shortSyllabus') + .deep.equal(requestBody.shortSyllabus); + res.body.should.have + .property('completeSyllabus') + .deep.equal(requestBody.completeSyllabus); + res.body.should.have + .property('outcomes') + .deep.equal(requestBody.outcomes); + res.body.should.have + .property('majorId') + .deep.equal(requestBody.majorId); + res.body.should.have.property('updatedAt').to.not.equal(null); + res.body.should.have.property('createdAt').to.not.equal(null); + done(); + }); + }); + + it('should return error when request body invalid (wrong course type enum)', (done) => { + const requestBody = { + code: CODE, + name: NAME, + type: `${TYPE}A`, + credits: CREDITS, + defaultSemester: DEFAULT_SEMESTER, + shortSyllabus: SHORT_SYLLABUS, + completeSyllabus: COMPLETE_SYLLABUS, + outcomes: OUTCOMES, + curriculumYear: CURRICULUM_YEAR, + majorId: major.id + }; + + chai + .request(server) + .post(EndpointEnum.COURSE) + .send(requestBody) + .end((_, res) => { + res.should.have.status(SERVER_ERROR_CODE); + res.body.should.have + .property('name') + .to.equal(SEQUELIZE_DATABASE_ERROR); + res.body.should.have + .property('message') + .to.equal( + 'invalid input value for enum "enum_Courses_type": "WAJIBA"', + ); + done(); + }); + }); + + it('should return error when request body invalid (required parameter type not given)', (done) => { + const requestBody = { + code: CODE, + name: NAME, + credits: CREDITS, + defaultSemester: DEFAULT_SEMESTER, + shortSyllabus: SHORT_SYLLABUS, + completeSyllabus: COMPLETE_SYLLABUS, + outcomes: OUTCOMES, + curriculumYear: CURRICULUM_YEAR, + majorId: major.id + }; + + chai + .request(server) + .post(EndpointEnum.COURSE) + .send(requestBody) + .end((_, res) => { + res.should.have.status(REQUEST_ERROR_CODE); + res.body.should.have + .property('name') + .to.equal(ERROR); + res.body.should.have + .property('message') + .to.equal(TYPE_UNDEFINED_ERROR); + done(); + }); + }); + }); }); diff --git a/backend/src/util/db/course.js b/backend/src/util/db/course.js new file mode 100644 index 00000000..8d7987fa --- /dev/null +++ b/backend/src/util/db/course.js @@ -0,0 +1,29 @@ +'use strict'; +const { + Course, + CourseS2 +} = require('../../models/index'); + +const { NotExistError } = require('../error'); + +const createCourse = async (data) => { + try { + const course = await Course.create(data, { + include: [ + { + model: CourseS2, + as: 'courseS2', + }, + ], + }); + + if (course) return course; + else throw new NotExistError(); + } catch (error) { + throw error; + } +}; + +module.exports = { + createCourse +} \ No newline at end of file -- GitLab