eduverse-backend / src /modules /assignments /controllers /assignments.controller.ts
amirhamdi's picture
feat: enhance assignments module with user role handling and upload file type definition
cb527e9
Raw
History Blame Contribute Delete
11 kB
import {
Controller,
Post,
Get,
Delete,
Param,
Body,
Query,
UseGuards,
Request,
Patch,
UseInterceptors,
UploadedFile,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiParam,
ApiBody,
ApiConsumes,
} from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { Roles } from '../../auth/roles.decorator';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../auth/guards/roles.guard';
import { RoleName } from '../../auth/entities/role.entity';
import { AssignmentsService } from '../services';
import {
CreateAssignmentDto,
UpdateAssignmentDto,
SubmitAssignmentDto,
GradeSubmissionDto,
AssignmentQueryDto,
UploadAssignmentInstructionDto,
UploadAssignmentSubmissionDto,
} from '../dto';
import { AssignmentStatus } from '../enums';
type UploadedAssignmentFile = {
originalname: string;
mimetype: string;
buffer: Buffer;
};
@ApiTags('📝 Assignments')
@Controller('api/assignments')
@UseGuards(JwtAuthGuard, RolesGuard)
@ApiBearerAuth('JWT-auth')
export class AssignmentsController {
constructor(private readonly assignmentsService: AssignmentsService) {}
@Get()
@ApiOperation({
summary: 'List assignments',
description:
'Filter by course, status, due dates. Supports pagination and sorting.',
})
@ApiResponse({ status: 200, description: 'Paginated list of assignments' })
async findAll(@Query() query: AssignmentQueryDto, @Request() req) {
const userId = req.user.userId || req.user.id;
const roles = req.user.roles?.map((role: any) => role.roleName || role.name || role) || [];
return this.assignmentsService.findAll(query, userId, roles);
}
@Post()
@Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN)
@ApiOperation({
summary: 'Create assignment',
description:
'Create a new assignment for a course. Only instructors, TAs, and admins.',
})
@ApiResponse({ status: 201, description: 'Assignment created' })
async create(@Body() dto: CreateAssignmentDto, @Request() req) {
const userId = req.user.userId || req.user.id;
return this.assignmentsService.create(dto, userId);
}
@Get(':id')
@ApiOperation({ summary: 'Get assignment details', description: 'Returns assignment with course info and submissions.' })
@ApiParam({ name: 'id', type: Number, description: 'Assignment ID', example: 3 })
@ApiResponse({
status: 200,
description: 'Assignment details with submissions',
})
async findOne(@Param('id') id: number) {
return this.assignmentsService.findOne(+id);
}
@Patch(':id')
@Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN)
@ApiOperation({ summary: 'Update assignment' })
@ApiParam({ name: 'id', type: Number, description: 'Assignment ID', example: 3 })
@ApiResponse({ status: 200, description: 'Assignment updated' })
async update(
@Param('id') id: number,
@Body() dto: UpdateAssignmentDto,
@Request() req,
) {
const userId = req.user.userId || req.user.id;
return this.assignmentsService.update(+id, dto, userId);
}
@Delete(':id')
@Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN)
@ApiOperation({ summary: 'Delete assignment (soft delete)' })
@ApiParam({ name: 'id', type: Number, description: 'Assignment ID', example: 3 })
@ApiResponse({ status: 200, description: 'Assignment deleted' })
async remove(@Param('id') id: number) {
return this.assignmentsService.remove(+id);
}
@Patch(':id/status')
@Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN)
@ApiOperation({
summary: 'Change assignment status (publish/close/archive)',
})
@ApiParam({ name: 'id', type: Number, description: 'Assignment ID', example: 3 })
@ApiBody({
schema: {
properties: {
status: {
type: 'string',
enum: ['draft', 'published', 'closed', 'archived'],
example: 'published',
},
},
},
})
async changeStatus(
@Param('id') id: number,
@Body('status') status: AssignmentStatus,
@Request() req,
) {
const userId = req.user.userId || req.user.id;
return this.assignmentsService.changeStatus(+id, status, userId);
}
@Post(':id/submit')
@Roles(RoleName.STUDENT)
@ApiOperation({
summary: 'Submit assignment',
description: 'Student submits their work',
})
@ApiParam({ name: 'id', type: Number, description: 'Assignment ID', example: 3 })
async submit(
@Param('id') id: number,
@Body() dto: SubmitAssignmentDto,
@Request() req,
) {
const userId = req.user.userId || req.user.id;
return this.assignmentsService.submit(+id, dto, userId);
}
// /submissions/my must be defined BEFORE /:subId to avoid route conflicts
@Get(':id/submissions/my')
@Roles(RoleName.STUDENT)
@ApiOperation({ summary: "Get student's own submission" })
@ApiParam({ name: 'id', type: Number, description: 'Assignment ID', example: 3 })
async getMySubmission(@Param('id') id: number, @Request() req) {
const userId = req.user.userId || req.user.id;
return this.assignmentsService.getMySubmission(+id, userId);
}
@Get(':id/submissions')
@Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN)
@ApiOperation({ summary: 'List all submissions for an assignment' })
@ApiParam({ name: 'id', type: Number, description: 'Assignment ID', example: 3 })
async getSubmissions(@Param('id') id: number) {
return this.assignmentsService.getSubmissions(+id);
}
@Patch(':id/submissions/:subId/grade')
@Roles(RoleName.INSTRUCTOR, RoleName.TA)
@ApiOperation({ summary: 'Grade a submission (creates grade record)' })
@ApiParam({ name: 'id', type: Number, description: 'Assignment ID', example: 3 })
@ApiParam({ name: 'subId', type: Number, description: 'Submission ID', example: 1 })
async gradeSubmission(
@Param('id') id: number,
@Param('subId') subId: number,
@Body() dto: GradeSubmissionDto,
@Request() req,
) {
const userId = req.user.userId || req.user.id;
return this.assignmentsService.gradeSubmission(+id, +subId, dto, userId);
}
// ============ GOOGLE DRIVE UPLOADS ============
@Post(':id/instructions/upload')
@Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN)
@UseInterceptors(FileInterceptor('file'))
@HttpCode(HttpStatus.CREATED)
@ApiOperation({
summary: 'Upload assignment instruction to Google Drive',
description: 'Upload an assignment instruction file (PDF, DOCX, etc.) directly to Google Drive. Creates folder structure automatically.',
})
@ApiParam({ name: 'id', type: Number, description: 'Assignment ID', example: 3 })
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
required: ['file'],
properties: {
file: {
type: 'string',
format: 'binary',
description: 'Instruction file (PDF, DOCX, etc.)',
},
title: {
type: 'string',
description: 'Instruction title',
example: 'Assignment 1 - Database Design Guide',
},
orderIndex: {
type: 'integer',
description: 'Order index for sorting',
example: 1,
},
},
},
})
@ApiResponse({ status: 201, description: 'Instruction uploaded successfully' })
@ApiResponse({ status: 400, description: 'No file provided' })
@ApiResponse({ status: 404, description: 'Assignment not found' })
async uploadInstruction(
@Param('id') id: number,
@UploadedFile() file: UploadedAssignmentFile,
@Body() dto: UploadAssignmentInstructionDto,
@Request() req,
) {
if (!file) {
throw new Error('No file provided');
}
const userId = req.user.userId || req.user.id;
return this.assignmentsService.uploadInstructionToDrive(
+id,
file,
dto.title,
dto.orderIndex || 0,
userId,
);
}
@Delete(':assignmentId/instructions/:driveId')
@Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN)
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Delete assignment instruction file',
description: 'Delete an uploaded instruction file from an assignment. Removes the file from Google Drive and the database record.',
})
@ApiParam({ name: 'assignmentId', type: Number, description: 'Assignment ID', example: 3 })
@ApiParam({ name: 'driveId', type: String, description: 'Google Drive file ID (driveId from instructionFiles array)', example: '1IOjnr6y7JRO7hcoMXkpoRIw8txm7WZlV' })
@ApiResponse({ status: 200, description: 'Instruction file deleted successfully' })
@ApiResponse({ status: 400, description: 'Instruction file not found or invalid driveId' })
@ApiResponse({ status: 403, description: 'Forbidden — user not authorized' })
@ApiResponse({ status: 404, description: 'Assignment not found' })
async deleteInstruction(
@Param('assignmentId') assignmentId: number,
@Param('driveId') driveId: string,
@Request() req,
) {
const userId = req.user.userId || req.user.id;
return this.assignmentsService.deleteInstructionFile(
+assignmentId,
driveId,
userId,
);
}
@Post(':id/submissions/upload')
@Roles(RoleName.STUDENT)
@UseInterceptors(FileInterceptor('file'))
@HttpCode(HttpStatus.CREATED)
@ApiOperation({
summary: 'Upload assignment submission to Google Drive',
description: 'Upload assignment submission file directly to Google Drive. Creates student folder automatically. Auto-detects late submissions.',
})
@ApiParam({ name: 'id', type: Number, description: 'Assignment ID', example: 3 })
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
required: ['file'],
properties: {
file: {
type: 'string',
format: 'binary',
description: 'Submission file',
},
submissionText: {
type: 'string',
description: 'Optional submission notes/comments',
example: 'Completed all tasks as instructed.',
},
submissionLink: {
type: 'string',
description: 'Optional link to external submission (e.g., GitHub)',
example: 'https://github.com/student/project',
},
},
},
})
@ApiResponse({ status: 201, description: 'Submission uploaded successfully' })
@ApiResponse({ status: 400, description: 'No file provided' })
@ApiResponse({ status: 404, description: 'Assignment not found' })
async uploadSubmission(
@Param('id') id: number,
@UploadedFile() file: UploadedAssignmentFile,
@Body() dto: UploadAssignmentSubmissionDto,
@Request() req,
) {
if (!file) {
throw new Error('No file provided');
}
const userId = req.user.userId || req.user.id;
return this.assignmentsService.uploadSubmissionToDrive(
+id,
file,
dto.submissionText,
dto.submissionLink,
userId,
);
}
}