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