Merge pull request #31 from internship-humic/feat/multiple-file-submission
feat(task): submission dengan multiple files sebanyak 20 files
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `task_submission_file` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`id_submission` INTEGER NOT NULL,
|
||||
`file_path` TEXT NOT NULL,
|
||||
`original_name` VARCHAR(255) NULL,
|
||||
`mime_type` VARCHAR(255) NULL,
|
||||
`size_bytes` INTEGER NULL,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
INDEX `task_submission_file_id_submission_foreign`(`id_submission`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `task_submission_file` ADD CONSTRAINT `task_submission_file_id_submission_foreign` FOREIGN KEY (`id_submission`) REFERENCES `task_submission`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -279,6 +279,7 @@ model TaskSubmission {
|
||||
|
||||
task Task @relation(fields: [id_task], references: [id], map: "task_submission_id_task_foreign")
|
||||
user User @relation(fields: [id_user], references: [id], map: "task_submission_id_user_foreign")
|
||||
files TaskSubmissionFile[]
|
||||
|
||||
@@unique([id_task, id_user])
|
||||
@@index([id_task], map: "task_submission_id_task_foreign")
|
||||
@@ -286,6 +287,21 @@ model TaskSubmission {
|
||||
@@map("task_submission")
|
||||
}
|
||||
|
||||
model TaskSubmissionFile {
|
||||
id Int @id @default(autoincrement())
|
||||
id_submission Int
|
||||
file_path String @db.Text
|
||||
original_name String? @db.VarChar(255)
|
||||
mime_type String? @db.VarChar(255)
|
||||
size_bytes Int?
|
||||
created_at DateTime @default(now())
|
||||
|
||||
submission TaskSubmission @relation(fields: [id_submission], references: [id], onDelete: Cascade, map: "task_submission_file_id_submission_foreign")
|
||||
|
||||
@@index([id_submission], map: "task_submission_file_id_submission_foreign")
|
||||
@@map("task_submission_file")
|
||||
}
|
||||
|
||||
enum Role {
|
||||
admin
|
||||
mentor
|
||||
|
||||
+49
-6
@@ -5,6 +5,7 @@ const { getProjectBackgroundColor } = require('../src/helpers/utils/projectTheme
|
||||
|
||||
// Diperlukan ketika isi databasenya masih kosong dan ingin menambahkan dummy data sebagai testing development [Rafi 08/06/2026 16:34]
|
||||
async function main() {
|
||||
await prisma.taskSubmissionFile.deleteMany({});
|
||||
await prisma.taskSubmission.deleteMany({});
|
||||
await prisma.projectMember.deleteMany({});
|
||||
await prisma.user.deleteMany({});
|
||||
@@ -288,12 +289,54 @@ async function main() {
|
||||
});
|
||||
|
||||
// 15. TaskSubmission
|
||||
await prisma.taskSubmission.createMany({
|
||||
data: [
|
||||
{ id_task: task1.id, id_user: user1.id, url_link: 'https://github.com/rafiathallah3' },
|
||||
{ id_task: task2.id, id_user: user2.id, file_path: 'https://example.com/submission-alice.zip' },
|
||||
{ id_task: task3.id, id_user: user3.id, url_link: 'https://github.com/bob/auth-gateway' }
|
||||
]
|
||||
await prisma.taskSubmission.create({
|
||||
data: {
|
||||
id_task: task1.id,
|
||||
id_user: user1.id,
|
||||
url_link: 'https://github.com/rafiathallah3',
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.taskSubmission.create({
|
||||
data: {
|
||||
id_task: task2.id,
|
||||
id_user: user2.id,
|
||||
|
||||
// Backward compatibility untuk frontend lama
|
||||
file_path: '/uploads/task-submissions/alice-api-specification-part-1.pdf',
|
||||
|
||||
// Multi-file submission data
|
||||
files: {
|
||||
create: [
|
||||
{
|
||||
file_path: '/uploads/task-submissions/alice-api-specification-part-1.pdf',
|
||||
original_name: 'alice-api-specification-part-1.pdf',
|
||||
mime_type: 'application/pdf',
|
||||
size_bytes: 204800,
|
||||
},
|
||||
{
|
||||
file_path: '/uploads/task-submissions/alice-api-specification-part-2.pdf',
|
||||
original_name: 'alice-api-specification-part-2.pdf',
|
||||
mime_type: 'application/pdf',
|
||||
size_bytes: 256000,
|
||||
},
|
||||
{
|
||||
file_path: '/uploads/task-submissions/alice-api-specification-appendix.pdf',
|
||||
original_name: 'alice-api-specification-appendix.pdf',
|
||||
mime_type: 'application/pdf',
|
||||
size_bytes: 128000,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.taskSubmission.create({
|
||||
data: {
|
||||
id_task: task3.id,
|
||||
id_user: user3.id,
|
||||
url_link: 'https://github.com/bob/auth-gateway',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -331,6 +331,19 @@ class ProjectRepository {
|
||||
url_link: true,
|
||||
submitted_at: true,
|
||||
updated_at: true,
|
||||
files: {
|
||||
select: {
|
||||
id: true,
|
||||
file_path: true,
|
||||
original_name: true,
|
||||
mime_type: true,
|
||||
size_bytes: true,
|
||||
created_at: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -523,6 +523,8 @@ class ProjectService {
|
||||
submission_details: submission ? {
|
||||
id: submission.id,
|
||||
file_path: submission.file_path,
|
||||
files: submission.files || [],
|
||||
file_paths: (submission.files || []).map((file) => file.file_path),
|
||||
url_link: submission.url_link,
|
||||
submitted_at: submission.submitted_at,
|
||||
updated_at: submission.updated_at,
|
||||
|
||||
@@ -17,10 +17,26 @@ const getActor = (req) => ({
|
||||
email: req.email,
|
||||
});
|
||||
|
||||
const cleanupUploadedFile = (file) => {
|
||||
const getUploadedSubmissionFiles = (req) => {
|
||||
const multipleFiles = Array.isArray(req.files?.submission_files)
|
||||
? req.files.submission_files
|
||||
: [];
|
||||
|
||||
const legacyFiles = Array.isArray(req.files?.submission_file)
|
||||
? req.files.submission_file
|
||||
: [];
|
||||
|
||||
const singleFile = req.file ? [req.file] : [];
|
||||
|
||||
return [...multipleFiles, ...legacyFiles, ...singleFile];
|
||||
};
|
||||
|
||||
const cleanupUploadedFiles = (files = []) => {
|
||||
for (const file of files) {
|
||||
if (file?.path && fs.existsSync(file.path)) {
|
||||
fs.unlinkSync(file.path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class TaskController {
|
||||
@@ -171,11 +187,13 @@ class TaskController {
|
||||
}
|
||||
|
||||
async submitTask(req, res) {
|
||||
const uploadedFiles = getUploadedSubmissionFiles(req);
|
||||
|
||||
try {
|
||||
const validatePayload = isValidPayload(req.body, submitTaskModel);
|
||||
|
||||
if (validatePayload.err) {
|
||||
cleanupUploadedFile(req.file);
|
||||
cleanupUploadedFiles(uploadedFiles);
|
||||
|
||||
return response(
|
||||
res,
|
||||
@@ -190,19 +208,19 @@ class TaskController {
|
||||
const result = await taskService.submitTask(
|
||||
req.params.id,
|
||||
validatePayload.data,
|
||||
req.file,
|
||||
uploadedFiles,
|
||||
actor,
|
||||
req.query.project || req.query.id_project
|
||||
);
|
||||
|
||||
if (result.err) {
|
||||
cleanupUploadedFile(req.file);
|
||||
cleanupUploadedFiles(uploadedFiles);
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Task submitted successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
cleanupUploadedFile(req.file);
|
||||
cleanupUploadedFiles(uploadedFiles);
|
||||
|
||||
return response(
|
||||
res,
|
||||
@@ -215,11 +233,13 @@ class TaskController {
|
||||
}
|
||||
|
||||
async updateSubmission(req, res) {
|
||||
const uploadedFiles = getUploadedSubmissionFiles(req);
|
||||
|
||||
try {
|
||||
const validatePayload = isValidPayload(req.body, updateSubmissionModel);
|
||||
|
||||
if (validatePayload.err) {
|
||||
cleanupUploadedFile(req.file);
|
||||
cleanupUploadedFiles(uploadedFiles);
|
||||
|
||||
return response(
|
||||
res,
|
||||
@@ -234,18 +254,18 @@ class TaskController {
|
||||
const result = await taskService.updateSubmission(
|
||||
req.params.id,
|
||||
validatePayload.data,
|
||||
req.file,
|
||||
uploadedFiles,
|
||||
actor
|
||||
);
|
||||
|
||||
if (result.err) {
|
||||
cleanupUploadedFile(req.file);
|
||||
cleanupUploadedFiles(uploadedFiles);
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Submission updated successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
cleanupUploadedFile(req.file);
|
||||
cleanupUploadedFiles(uploadedFiles);
|
||||
|
||||
return response(
|
||||
res,
|
||||
|
||||
@@ -63,7 +63,7 @@ const taskUploadErrorHandler = (err, _req, res, next) => {
|
||||
status: false,
|
||||
data: null,
|
||||
message: {
|
||||
[err.field]: `Field '${err.field}' cannot have more than 1 file.`,
|
||||
[err.field]: `Field '${err.field}' exceeds the allowed file count or is not allowed.`,
|
||||
},
|
||||
code: 400,
|
||||
});
|
||||
|
||||
@@ -44,6 +44,12 @@ class TaskRepository {
|
||||
url_link: true,
|
||||
submitted_at: true,
|
||||
updated_at: true,
|
||||
files: {
|
||||
select: this.submissionFileSelect(),
|
||||
orderBy: {
|
||||
id: 'asc'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -91,6 +97,12 @@ class TaskRepository {
|
||||
professional_bio: true,
|
||||
},
|
||||
},
|
||||
files: {
|
||||
select: this.submissionFileSelect(),
|
||||
orderBy: {
|
||||
id: 'asc'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -140,6 +152,14 @@ class TaskRepository {
|
||||
id_user: parseInt(idUser),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
files: {
|
||||
select: this.submissionFileSelect(),
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,23 +192,86 @@ class TaskRepository {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
files: {
|
||||
select: this.submissionFileSelect(),
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createSubmission(submissionData) {
|
||||
async createSubmission(submissionData, filesData = []) {
|
||||
return prisma.taskSubmission.create({
|
||||
data: submissionData,
|
||||
data: {
|
||||
...submissionData,
|
||||
files: filesData.length > 0
|
||||
? {
|
||||
create: filesData,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
include: {
|
||||
files: {
|
||||
select: this.submissionFileSelect(),
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateSubmission(idSubmission, submissionData) {
|
||||
return prisma.taskSubmission.update({
|
||||
async updateSubmission(idSubmission, submissionData, filesData = null) {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const submissionId = parseInt(idSubmission);
|
||||
|
||||
await tx.taskSubmission.update({
|
||||
where: {
|
||||
id: parseInt(idSubmission),
|
||||
id: submissionId,
|
||||
},
|
||||
data: submissionData,
|
||||
});
|
||||
|
||||
if (filesData !== null) {
|
||||
await tx.taskSubmissionFile.deleteMany({
|
||||
where: {
|
||||
id_submission: submissionId,
|
||||
},
|
||||
});
|
||||
|
||||
if (filesData.length > 0) {
|
||||
await tx.taskSubmissionFile.createMany({
|
||||
data: filesData.map((file) => ({
|
||||
...file,
|
||||
id_submission: submissionId,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tx.taskSubmission.findUnique({
|
||||
where: {
|
||||
id: submissionId,
|
||||
},
|
||||
include: {
|
||||
files: {
|
||||
select: {
|
||||
id: true,
|
||||
file_path: true,
|
||||
original_name: true,
|
||||
mime_type: true,
|
||||
size_bytes: true,
|
||||
created_at: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSubmission(idSubmission) {
|
||||
@@ -266,6 +349,12 @@ class TaskRepository {
|
||||
professional_bio: true,
|
||||
},
|
||||
},
|
||||
files: {
|
||||
select: this.submissionFileSelect(),
|
||||
orderBy: {
|
||||
id: 'asc'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -293,6 +382,17 @@ class TaskRepository {
|
||||
|
||||
return Boolean(task);
|
||||
}
|
||||
|
||||
submissionFileSelect() {
|
||||
return {
|
||||
id: true,
|
||||
file_path: true,
|
||||
original_name: true,
|
||||
mime_type: true,
|
||||
size_bytes: true,
|
||||
created_at: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new TaskRepository();
|
||||
|
||||
@@ -257,7 +257,7 @@ class TaskService {
|
||||
}
|
||||
}
|
||||
|
||||
async submitTask(idTask, payload, file, actor = {}, projectIdentifier = null) {
|
||||
async submitTask(idTask, payload, files = [], actor = {}, projectIdentifier = null) {
|
||||
try {
|
||||
if (!idTask) {
|
||||
return error(new BadRequestError('Task ID or slug is required'));
|
||||
@@ -288,21 +288,29 @@ class TaskService {
|
||||
id_user: parseInt(actor.id),
|
||||
};
|
||||
|
||||
let filesData = [];
|
||||
|
||||
if (task.submission_type === 'file_upload') {
|
||||
if (!file) {
|
||||
if (!files || files.length === 0) {
|
||||
return error(new BadRequestError('Submission file is required for this task'));
|
||||
}
|
||||
|
||||
if (files.length > 20) {
|
||||
return error(new BadRequestError('Maximum 20 submission files are allowed'));
|
||||
}
|
||||
|
||||
if (payload.url_link) {
|
||||
return error(new BadRequestError('URL link is not allowed for file upload task'));
|
||||
}
|
||||
|
||||
submissionData.file_path = `/uploads/task-submissions/${file.filename}`;
|
||||
filesData = this.mapUploadedSubmissionFiles(files);
|
||||
|
||||
submissionData.file_path = filesData[0]?.file_path || null;
|
||||
submissionData.url_link = null;
|
||||
}
|
||||
|
||||
if (task.submission_type === 'url_link') {
|
||||
if (file) {
|
||||
if (files && files.length > 0) {
|
||||
return error(new BadRequestError('File upload is not allowed for URL link task'));
|
||||
}
|
||||
|
||||
@@ -319,10 +327,17 @@ class TaskService {
|
||||
let submission;
|
||||
|
||||
if (existingSubmission) {
|
||||
this.deleteLocalSubmissionFile(existingSubmission.file_path);
|
||||
submission = await taskRepository.updateSubmission(existingSubmission.id, submissionData);
|
||||
const oldFilePaths = this.getSubmissionFilePaths(existingSubmission);
|
||||
|
||||
submission = await taskRepository.updateSubmission(
|
||||
existingSubmission.id,
|
||||
submissionData,
|
||||
task.submission_type === 'file_upload' ? filesData : []
|
||||
);
|
||||
|
||||
this.deleteLocalSubmissionFiles(oldFilePaths);
|
||||
} else {
|
||||
submission = await taskRepository.createSubmission(submissionData);
|
||||
submission = await taskRepository.createSubmission(submissionData, filesData);
|
||||
}
|
||||
|
||||
const projectDetails = await taskRepository.findProjectById(task.id_project);
|
||||
@@ -346,7 +361,7 @@ class TaskService {
|
||||
}
|
||||
}
|
||||
|
||||
async updateSubmission(idSubmission, payload, file, actor = {}) {
|
||||
async updateSubmission(idSubmission, payload, files = [], actor = {}) {
|
||||
try {
|
||||
if (!this.isPositiveInteger(idSubmission)) {
|
||||
return error(new BadRequestError('Submission ID must be a valid number'));
|
||||
@@ -380,22 +395,29 @@ class TaskService {
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
let filesData = null;
|
||||
|
||||
if (submission.task.submission_type === 'file_upload') {
|
||||
if (!file) {
|
||||
if (!files || files.length === 0) {
|
||||
return error(new BadRequestError('Submission file is required for this task'));
|
||||
}
|
||||
|
||||
if (files.length > 20) {
|
||||
return error(new BadRequestError('Maximum 20 submission files are allowed'));
|
||||
}
|
||||
|
||||
if (payload.url_link) {
|
||||
return error(new BadRequestError('URL link is not allowed for file upload task'));
|
||||
}
|
||||
|
||||
updateData.file_path = `/uploads/task-submissions/${file.filename}`;
|
||||
filesData = this.mapUploadedSubmissionFiles(files);
|
||||
|
||||
updateData.file_path = filesData[0]?.file_path || null;
|
||||
updateData.url_link = null;
|
||||
}
|
||||
|
||||
if (submission.task.submission_type === 'url_link') {
|
||||
if (file) {
|
||||
if (files && files.length > 0) {
|
||||
return error(new BadRequestError('File upload is not allowed for URL link task'));
|
||||
}
|
||||
|
||||
@@ -403,17 +425,21 @@ class TaskService {
|
||||
return error(new BadRequestError('URL link is required for this task'));
|
||||
}
|
||||
|
||||
filesData = [];
|
||||
updateData.file_path = null;
|
||||
updateData.url_link = payload.url_link;
|
||||
}
|
||||
|
||||
this.deleteLocalSubmissionFile(submission.file_path);
|
||||
const oldFilePaths = this.getSubmissionFilePaths(submission);
|
||||
|
||||
const updatedSubmission = await taskRepository.updateSubmission(
|
||||
idSubmission,
|
||||
updateData
|
||||
updateData,
|
||||
filesData
|
||||
);
|
||||
|
||||
this.deleteLocalSubmissionFiles(oldFilePaths);
|
||||
|
||||
return data(this.mapSubmission(updatedSubmission));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
@@ -455,7 +481,7 @@ class TaskService {
|
||||
|
||||
await taskRepository.deleteSubmission(idSubmission);
|
||||
|
||||
this.deleteLocalSubmissionFile(submission.file_path);
|
||||
this.deleteLocalSubmissionFiles(this.getSubmissionFilePaths(submission));
|
||||
|
||||
return data(null);
|
||||
} catch (err) {
|
||||
@@ -582,11 +608,28 @@ class TaskService {
|
||||
mapSubmission(submission) {
|
||||
if (!submission) return null;
|
||||
|
||||
const files = Array.isArray(submission.files)
|
||||
? submission.files.map((file) => ({
|
||||
id: file.id,
|
||||
file_path: file.file_path,
|
||||
original_name: file.original_name || null,
|
||||
mime_type: file.mime_type || null,
|
||||
size_bytes: file.size_bytes || null,
|
||||
created_at: file.created_at || null,
|
||||
}))
|
||||
: [];
|
||||
|
||||
return {
|
||||
id: submission.id,
|
||||
id_task: submission.id_task,
|
||||
id_user: submission.id_user,
|
||||
|
||||
// Backward compatibility when only file_path is available (1 file per submission)
|
||||
file_path: submission.file_path,
|
||||
// Multi-file fields
|
||||
files,
|
||||
file_paths: files.map((file) => file.file_path),
|
||||
|
||||
url_link: submission.url_link,
|
||||
submitted_at: submission.submitted_at,
|
||||
updated_at: submission.updated_at,
|
||||
@@ -654,6 +697,39 @@ class TaskService {
|
||||
};
|
||||
}
|
||||
|
||||
mapUploadedSubmissionFiles(files = []) {
|
||||
return files.map((file) => ({
|
||||
file_path: `/uploads/task-submissions/${file.filename}`,
|
||||
original_name: file.originalname || null,
|
||||
mime_type: file.mimetype || null,
|
||||
size_bytes: file.size || null,
|
||||
}));
|
||||
}
|
||||
|
||||
getSubmissionFilePaths(submission) {
|
||||
const filePaths = [];
|
||||
|
||||
if (Array.isArray(submission?.files)) {
|
||||
for (const file of submission.files) {
|
||||
if (file?.file_path) {
|
||||
filePaths.push(file.file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filePaths.length === 0 && submission?.file_path) {
|
||||
filePaths.push(submission.file_path);
|
||||
}
|
||||
|
||||
return [...new Set(filePaths)];
|
||||
}
|
||||
|
||||
deleteLocalSubmissionFiles(filePaths = []) {
|
||||
for (const filePath of filePaths) {
|
||||
this.deleteLocalSubmissionFile(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
deleteLocalSubmissionFile(filePath) {
|
||||
if (!filePath || typeof filePath !== 'string') {
|
||||
return;
|
||||
|
||||
@@ -426,6 +426,40 @@ const router = express.Router();
|
||||
* type: string
|
||||
* format: date-time
|
||||
* example: 2026-07-20T10:00:00.000Z
|
||||
* files:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* example: 1
|
||||
* file_path:
|
||||
* type: string
|
||||
* example: /uploads/task-submissions/report-part-1.pdf
|
||||
* original_name:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* example: report-part-1.pdf
|
||||
* mime_type:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* example: application/pdf
|
||||
* size_bytes:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* example: 204800
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* example: 2026-07-20T10:00:00.000Z
|
||||
* file_paths:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* example:
|
||||
* - /uploads/task-submissions/report-part-1.pdf
|
||||
* - /uploads/task-submissions/report-part-2.pdf
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
|
||||
@@ -113,6 +113,40 @@ const router = express.Router();
|
||||
* type: string
|
||||
* nullable: true
|
||||
* example: /uploads/task-submissions/report.pdf
|
||||
* files:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* example: 1
|
||||
* file_path:
|
||||
* type: string
|
||||
* example: /uploads/task-submissions/report-part-1.pdf
|
||||
* original_name:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* example: report-part-1.pdf
|
||||
* mime_type:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* example: application/pdf
|
||||
* size_bytes:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* example: 204800
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* example: 2026-06-09T10:00:00.000Z
|
||||
* file_paths:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* example:
|
||||
* - /uploads/task-submissions/report-part-1.pdf
|
||||
* - /uploads/task-submissions/report-part-2.pdf
|
||||
* url_link:
|
||||
* type: string
|
||||
* nullable: true
|
||||
@@ -673,6 +707,11 @@ router.patch('/tasks/:id', verifyJWT, isMentorOrAdmin, taskController.updateTask
|
||||
*/
|
||||
router.delete('/tasks/:id', verifyJWT, isMentorOrAdmin, taskController.deleteTask);
|
||||
|
||||
const submissionUpload = taskUpload.fields([
|
||||
{ name: 'submission_files', maxCount: 20 },
|
||||
{ name: 'submission_file', maxCount: 1 },
|
||||
]);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /task-api/tasks/{id}/submissions:
|
||||
@@ -712,10 +751,17 @@ router.delete('/tasks/:id', verifyJWT, isMentorOrAdmin, taskController.deleteTas
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* submission_files:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: Required when task submission_type is file_upload. Send this field multiple times. Max 20 PDF files, 5MB each.
|
||||
* submission_file:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: Required when task submission_type is file_upload. Allowed file types are PDF. Max size is 5MB.
|
||||
* deprecated: true
|
||||
* description: Deprecated. Use submission_files instead.
|
||||
* url_link:
|
||||
* type: string
|
||||
* format: uri
|
||||
@@ -751,7 +797,7 @@ router.delete('/tasks/:id', verifyJWT, isMentorOrAdmin, taskController.deleteTas
|
||||
router.post(
|
||||
'/tasks/:id/submissions',
|
||||
verifyJWT,
|
||||
taskUpload.single('submission_file'),
|
||||
submissionUpload,
|
||||
taskUploadErrorHandler,
|
||||
taskController.submitTask
|
||||
);
|
||||
@@ -780,10 +826,17 @@ router.post(
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* submission_files:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: Required when task submission_type is file_upload. Send this field multiple times. Max 20 PDF files, 5MB each.
|
||||
* submission_file:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: Required when related task submission_type is file_upload. Allowed file types are PDF, DOCX, and ZIP. Max size is 5MB.
|
||||
* deprecated: true
|
||||
* description: Deprecated. Use submission_files instead.
|
||||
* url_link:
|
||||
* type: string
|
||||
* format: uri
|
||||
@@ -819,7 +872,7 @@ router.post(
|
||||
router.patch(
|
||||
'/submissions/:id',
|
||||
verifyJWT,
|
||||
taskUpload.single('submission_file'),
|
||||
submissionUpload,
|
||||
taskUploadErrorHandler,
|
||||
taskController.updateSubmission
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user