From 2ed74256414debb0d5227cbd40e4f497a275dc87 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Sat, 19 Dec 2020 20:33:32 +0530 Subject: [PATCH] Create submission API MySQL Tables modified to separate MCQ and Subjective Submissions --- server/database.sql | 51 +++++-- server/models/submissions/createSubmission.js | 140 ++++++++++++++++++ server/models/submissions/index.js | 5 + server/routes/submissions/createSubmission.js | 54 +++++++ server/routes/submissions/index.js | 5 + server/schema/submissions/createSubmission.js | 3 - 6 files changed, 240 insertions(+), 18 deletions(-) create mode 100644 server/models/submissions/createSubmission.js create mode 100644 server/models/submissions/index.js create mode 100644 server/routes/submissions/createSubmission.js create mode 100644 server/routes/submissions/index.js diff --git a/server/database.sql b/server/database.sql index 242bf8c..b93c22d 100644 --- a/server/database.sql +++ b/server/database.sql @@ -153,7 +153,6 @@ DROP TABLE IF EXISTS `leaderboard`; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `leaderboard` ( `id` int NOT NULL AUTO_INCREMENT, - `rank` int NOT NULL, `username` varchar(45) NOT NULL, `contest_id` int NOT NULL, `score` int NOT NULL, @@ -254,31 +253,53 @@ CREATE TABLE `questions_tags` ( /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `submissions` +-- Table structure for table 'mcq_submissions' -- -DROP TABLE IF EXISTS `submissions`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `submissions` ( +CREATE TABLE `mcq_submissions` ( + `id` int NOT NULL AUTO_INCREMENT, + `question_id` int NOT NULL, + `contest_id` int NOT NULL, + `username` varchar(45) NOT NULL, + `response` int NOT NULL, + `submission_time` timestamp NOT NULL, + `score` int DEFAULT '0', + `judged` tinyint DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`), + UNIQUE KEY `unique_user_mcq_submission` (`username`,`contest_id`,`question_id`), + KEY `fk_mcq_submissions_questions_idx` (`question_id`), + KEY `fk_mcq_submissions_contests_idx` (`contest_id`), + KEY `fk_mcq_submissions_users_idx` (`username`), + CONSTRAINT `fk_mcq_submissions_contests` FOREIGN KEY (`contest_id`) REFERENCES `contests` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_mcq_submissions_questions` FOREIGN KEY (`question_id`) REFERENCES `questions` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_mcq_submissions_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table 'subjective_submissions' +-- + +CREATE TABLE `subjective_submissions` ( `id` int NOT NULL AUTO_INCREMENT, `question_id` int NOT NULL, `contest_id` int NOT NULL, `username` varchar(45) NOT NULL, - `mcq_submission` int DEFAULT NULL, - `subjective_submission` text, - `output` text, + `response` int NOT NULL, `submission_time` timestamp NOT NULL, `score` int DEFAULT '0', `judged` tinyint DEFAULT '0', + `feedback` varchar(110) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`), - KEY `fk_submissions_questions_idx` (`question_id`), - KEY `fk_submissions_contests_idx` (`contest_id`), - KEY `fk_submissions_users_idx` (`username`), - CONSTRAINT `fk_submissions_contests` FOREIGN KEY (`contest_id`) REFERENCES `contests` (`id`) ON UPDATE CASCADE, - CONSTRAINT `fk_submissions_questions` FOREIGN KEY (`question_id`) REFERENCES `questions` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `fk_submissions_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`) ON DELETE CASCADE ON UPDATE CASCADE + UNIQUE KEY `unique_user_subjective_submission` (`username`,`contest_id`,`question_id`), + KEY `fk_subjective_submissions_questions_idx` (`question_id`), + KEY `fk_subjective_submissions_contests_idx` (`contest_id`), + KEY `fk_subjective_submissions_users_idx` (`username`), + CONSTRAINT `fk_subjective_submissions_contests` FOREIGN KEY (`contest_id`) REFERENCES `contests` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_subjective_submissions_questions` FOREIGN KEY (`question_id`) REFERENCES `questions` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_subjective_submissions_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/server/models/submissions/createSubmission.js b/server/models/submissions/createSubmission.js new file mode 100644 index 0000000..67bc470 --- /dev/null +++ b/server/models/submissions/createSubmission.js @@ -0,0 +1,140 @@ +const { pool } = require('../database') + +/** + * + * @param {*} param0 + * @param {String} param0.username + * @param {Number} param0.contestId + * @param {Number} param0.questionId + * @param {String} param0.subjective_submission + * @param {Number} param0.mcq_submission + * @return {Promise} + */ + +// For practise, keep only coding questions +// User should have already registered for the contest for active contest + +function createSubmission({ + username, + contestId, + questionId, + subjective_submission: subjectiveSubmission, + mcq_submission: mcqSubmission, +}) { + return new Promise((resolve, reject) => { + pool.getConnection((err, connection) => { + if (err) { + return reject(err) + } + connection.beginTransaction((err) => { + if (err) { + connection.release() + return reject(err) + } + let insertionQuery + let queryArr = [questionId, contestId, username] + if (mcqSubmission) { + insertionQuery = `INSERT INTO mcq_submissions(question_id,contest_id,username,submission_time,response,judged,score) + SELECT ?,?,?,NOW(),?,1,(?=q.correct)*cq.max_score FROM questions q INNER JOIN contests_questions cq ON q.id=cq.question_id + WHERE q.id=? AND cq.contest_id=? AND q.type=? + AND EXISTS(SELECT 1 FROM contests WHERE id=? AND NOW()>=start_time AND NOW()<=end_time) + AND EXISTS(SELECT 1 FROM contests_participants WHERE contests_id=? AND participant=?)` + queryArr.push( + mcqSubmission, + mcqSubmission, + questionId, + contestId, + 'mcq', + contestId, + contestId, + username + ) + } else if (subjectiveSubmission) { + insertionQuery = `INSERT INTO subjective_submissions(question_id,contest_id,username,submission_time,response) + VALUES(?,?,?,NOW(),?) WHERE EXISTS(SELECT 1 FROM questions WHERE id=? AND type=?) + AND EXISTS(SELECT 1 FROM contests WHERE id=? AND NOW()>=start_time AND NOW()<=end_time) + AND EXISTS(SELECT 1 FROM contests_participants WHERE contests_id=? AND participant=?) + ON DUPLICATE KEY UPDATE response=?` + queryArr.push( + subjectiveSubmission, + questionId, + 'subjective', + contestId, + contestId, + username, + subjectiveSubmission + ) + } + // Logic for coding questions to be added later + + connection.query(insertionQuery, queryArr, (error, results) => { + if (error) { + connection.release() + if (mcqSubmission && error.code === 'ER_DUP_ENTRY') + return reject( + 'You had already submitted response for the question' + ) + else { + return reject( + 'Either you have not registered for the contest or the contest has ended' + ) + } + } + + const submissionId = results.insertId + + if (subjectiveSubmission) { + return connection.commit((error) => { + if (error) { + return connection.rollback(() => { + connection.release() + return reject(error) + }) + } + connection.release() + return resolve({ + message: 'Response submitted successfully', + submissionId, + }) + }) + } else if (mcqSubmission) { + connection.query( + `INSERT INTO leaderboard(username,contest_id,score,total_time,attempted_count) + SELECT ?,?,sub.score,sub.submission_time,1 FROM mcq_submissions sub + INNER JOIN contests c ON c.id=sub.contest_id + WHERE sub.id=? + ON DUPLICATE KEY UPDATE + score=score+sub.score, + total_time=(sub.score>0)*(sub.submission_time-c.start_time)+(sub.score=0)*(total_time), + attempted_count=attempted_count+1`, + [username, contestId, submissionId], + (error, results) => { + if (error) { + return connection.rollback(() => { + connection.release() + return reject(error) + }) + } + return connection.commit((error) => { + if (error) { + return connection.rollback(() => { + connection.release() + return reject(error) + }) + } + connection.release() + return resolve({ + message: 'Response submitted successfully', + submissionId, + }) + }) + } + ) + } + }) + }) + }) + }) +} + +module.exports = createSubmission diff --git a/server/models/submissions/index.js b/server/models/submissions/index.js new file mode 100644 index 0000000..2b79029 --- /dev/null +++ b/server/models/submissions/index.js @@ -0,0 +1,5 @@ +const createSubmission = require('./createSubmission') + +module.exports = { + createSubmission, +} diff --git a/server/routes/submissions/createSubmission.js b/server/routes/submissions/createSubmission.js new file mode 100644 index 0000000..991d975 --- /dev/null +++ b/server/routes/submissions/createSubmission.js @@ -0,0 +1,54 @@ +const express = require('express') +const { verifyUserAccessToken } = require('../middlewares') +const router = express.Router() +const ajv = require('../../schema') +const { createSubmissionSchema } = require('../../schema/submissions') +const { createSubmission } = require('../../models/submissions') + +/** + * + * @param {Array} errArray + * @return {String} + */ +function sumErrors(errArray) { + const cb = (a, b) => a + b.message + ', ' + return errArray.reduce(cb, '') +} + +router.post( + '/contests/:contestId/questions/:questionId/submit', + verifyUserAccessToken, + (req, res) => { + const validate = ajv.compile(createSubmissionSchema) + const isValid = validate(req.body) + if (!isValid) { + return res.status(400).json({ + success: false, + error: sumErrors(validate.errors), + results: null, + }) + } + createSubmission({ + ...req.body, + username: req.username, + questionId: req.params.questionId, + contestId: req.params.contestId, + }) + .then((results) => { + return res.status(200).json({ + success: true, + results, + error: null, + }) + }) + .catch((error) => { + return res.status(400).json({ + success: false, + results: null, + error, + }) + }) + } +) + +module.exports = router diff --git a/server/routes/submissions/index.js b/server/routes/submissions/index.js new file mode 100644 index 0000000..2b79029 --- /dev/null +++ b/server/routes/submissions/index.js @@ -0,0 +1,5 @@ +const createSubmission = require('./createSubmission') + +module.exports = { + createSubmission, +} diff --git a/server/schema/submissions/createSubmission.js b/server/schema/submissions/createSubmission.js index 4670a28..8b24273 100644 --- a/server/schema/submissions/createSubmission.js +++ b/server/schema/submissions/createSubmission.js @@ -12,9 +12,6 @@ const schema = { }, }, errorMessage: { - required: { - mcq_submission: 'One of the mcq or subjective submissions is required', - }, properties: { subjective_submission: 'Invalid subjective submission', mcq_submission: 'Invalid MCQ submission',