Spaces:
Sleeping
Sleeping
Marwan Hassan Sobhy
feat(labs): add instructor labs CRUD, grading, and attendance improvements for Flutter Phase 7
fbaa73d | import { | |
| Controller, | |
| Get, | |
| Post, | |
| Put, | |
| Delete, | |
| Patch, | |
| Body, | |
| Param, | |
| Query, | |
| Req, | |
| UseGuards, | |
| ParseIntPipe, | |
| HttpCode, | |
| HttpStatus, | |
| UseInterceptors, | |
| UploadedFile, | |
| } from '@nestjs/common'; | |
| import { | |
| ApiTags, | |
| ApiOperation, | |
| ApiParam, | |
| ApiBody, | |
| ApiResponse, | |
| ApiBearerAuth, | |
| ApiConsumes, | |
| } from '@nestjs/swagger'; | |
| import { FileInterceptor } from '@nestjs/platform-express'; | |
| import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; | |
| import { RolesGuard } from '../../auth/guards/roles.guard'; | |
| import { Roles } from '../../auth/roles.decorator'; | |
| import { RoleName } from '../../auth/entities/role.entity'; | |
| import { LabsService } from '../services/labs.service'; | |
| import { | |
| CreateLabDto, | |
| UpdateLabDto, | |
| SubmitLabDto, | |
| GradeLabSubmissionDto, | |
| CreateInstructionDto, | |
| UpdateInstructionDto, | |
| MarkLabAttendanceDto, | |
| LabQueryDto, | |
| UploadLabInstructionDto, | |
| UploadLabTaMaterialDto, | |
| UploadLabSubmissionDto, | |
| } from '../dto'; | |
| import { LabStatus } from '../enums'; | |
| import { Lab } from '../entities/lab.entity'; | |
| import { LabInstruction } from '../entities/lab-instruction.entity'; | |
| ('Labs') | |
| ('JWT-auth') | |
| ('api/labs') | |
| (JwtAuthGuard, RolesGuard) | |
| export class LabsController { | |
| constructor(private readonly labsService: LabsService) {} | |
| // ============ LABS CRUD ============ | |
| () | |
| ({ | |
| summary: 'List labs', | |
| description: 'List all labs with optional filtering by course and status. Supports pagination.', | |
| }) | |
| ({ status: 200, description: 'Labs retrieved successfully' }) | |
| async findAll(() query: LabQueryDto) { | |
| return this.labsService.findAll(query); | |
| } | |
| () | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| (HttpStatus.CREATED) | |
| ({ | |
| summary: 'Create lab', | |
| description: 'Create a new lab assignment. Requires INSTRUCTOR, TA, ADMIN, or IT_ADMIN role.', | |
| }) | |
| ({ type: CreateLabDto }) | |
| ({ status: 201, description: 'Lab created successfully' }) | |
| ({ status: 403, description: 'Forbidden - Insufficient role' }) | |
| async create(() dto: CreateLabDto, () req: any) { | |
| const userId = req.user.userId || req.user.id; | |
| return this.labsService.create(dto, userId); | |
| } | |
| (':id') | |
| ({ | |
| summary: 'Get lab by ID', | |
| description: 'Get lab details including instructions.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ status: 200, description: 'Lab retrieved successfully' }) | |
| ({ status: 404, description: 'Lab not found' }) | |
| async findOne(('id', ParseIntPipe) id: number) { | |
| return this.labsService.findById(id); | |
| } | |
| (':id') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: 'Update lab', | |
| description: 'Update lab details. Requires INSTRUCTOR, TA, ADMIN, or IT_ADMIN role.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ type: UpdateLabDto }) | |
| ({ status: 200, description: 'Lab updated successfully' }) | |
| ({ status: 404, description: 'Lab not found' }) | |
| async update(('id', ParseIntPipe) id: number, () dto: UpdateLabDto) { | |
| return this.labsService.update(id, dto); | |
| } | |
| (':id') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| (HttpStatus.NO_CONTENT) | |
| ({ | |
| summary: 'Delete lab', | |
| description: 'Delete a lab. Requires INSTRUCTOR, TA, ADMIN, or IT_ADMIN role.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ status: 204, description: 'Lab deleted successfully' }) | |
| ({ status: 404, description: 'Lab not found' }) | |
| async remove(('id', ParseIntPipe) id: number) { | |
| return this.labsService.remove(id); | |
| } | |
| (':id/status') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: 'Change lab status', | |
| description: ` | |
| Change the status of a lab (publish, close, or archive). | |
| ### Status Values | |
| - \`draft\`: Not visible to students | |
| - \`published\`: Visible to students, accepting submissions | |
| - \`closed\`: No longer accepting submissions | |
| - \`archived\`: Hidden from all views | |
| `, | |
| }) | |
| ({ name: 'id', description: 'Lab ID', type: Number }) | |
| ({ | |
| schema: { | |
| properties: { | |
| status: { | |
| type: 'string', | |
| enum: ['draft', 'published', 'closed', 'archived'], | |
| example: 'published', | |
| }, | |
| }, | |
| }, | |
| }) | |
| ({ status: 200, description: 'Lab status updated', type: Lab }) | |
| ({ status: 404, description: 'Lab not found' }) | |
| async changeStatus( | |
| ('id', ParseIntPipe) id: number, | |
| ('status') status: LabStatus, | |
| ): Promise<Lab> { | |
| return this.labsService.changeStatus(id, status); | |
| } | |
| // ============ INSTRUCTIONS ============ | |
| (':id/instructions') | |
| ({ | |
| summary: 'Get lab instructions', | |
| description: 'Get all instructions for a lab, ordered by order_index.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ status: 200, description: 'Instructions retrieved' }) | |
| async getInstructions(('id', ParseIntPipe) id: number) { | |
| return this.labsService.getInstructions(id); | |
| } | |
| (':id/instructions') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| (HttpStatus.CREATED) | |
| ({ | |
| summary: 'Add instruction to lab', | |
| description: 'Add a step-by-step instruction to a lab. Supports markdown text and file attachments.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ type: CreateInstructionDto }) | |
| ({ status: 201, description: 'Instruction added' }) | |
| async addInstruction(('id', ParseIntPipe) id: number, () dto: CreateInstructionDto) { | |
| return this.labsService.addInstruction(id, dto); | |
| } | |
| (':id/instructions/:instructionId') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| (HttpStatus.OK) | |
| ({ | |
| summary: 'Update lab instruction', | |
| description: 'Update instruction text, order index, or file attachment. Requires INSTRUCTOR, TA, ADMIN, or IT_ADMIN role.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ name: 'instructionId', description: 'Instruction ID', example: 1 }) | |
| ({ type: UpdateInstructionDto }) | |
| ({ status: 200, description: 'Instruction updated' }) | |
| ({ status: 404, description: 'Instruction not found' }) | |
| async updateInstruction( | |
| ('id', ParseIntPipe) labId: number, | |
| ('instructionId', ParseIntPipe) instructionId: number, | |
| () dto: UpdateInstructionDto, | |
| ): Promise<LabInstruction> { | |
| return this.labsService.updateInstruction(labId, instructionId, dto); | |
| } | |
| (':id/instructions/:instructionId') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| (HttpStatus.OK) | |
| ({ | |
| summary: 'Delete lab instruction', | |
| description: 'Delete an instruction from a lab. Requires INSTRUCTOR, TA, ADMIN, or IT_ADMIN role.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ name: 'instructionId', description: 'Instruction ID', example: 1 }) | |
| ({ status: 200, description: 'Instruction deleted' }) | |
| ({ status: 404, description: 'Instruction not found' }) | |
| async deleteInstruction( | |
| ('id', ParseIntPipe) labId: number, | |
| ('instructionId', ParseIntPipe) instructionId: number, | |
| ): Promise<void> { | |
| return this.labsService.deleteInstruction(labId, instructionId); | |
| } | |
| // ============ SUBMISSIONS ============ | |
| (':id/submit') | |
| (RoleName.STUDENT) | |
| (HttpStatus.CREATED) | |
| ({ | |
| summary: 'Submit lab work', | |
| description: 'Submit lab work as a student. Can include text and/or file attachment. Auto-detects late submissions.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ type: SubmitLabDto }) | |
| ({ status: 201, description: 'Lab submitted successfully' }) | |
| async submit(('id', ParseIntPipe) id: number, () dto: SubmitLabDto, () req: any) { | |
| const userId = req.user.userId || req.user.id; | |
| return this.labsService.submit(id, userId, dto); | |
| } | |
| (':id/submissions') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: 'List lab submissions', | |
| description: 'List all submissions for a lab. Requires INSTRUCTOR, TA, ADMIN, or IT_ADMIN role.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ status: 200, description: 'Submissions retrieved' }) | |
| async getSubmissions(('id', ParseIntPipe) id: number) { | |
| return this.labsService.getSubmissions(id); | |
| } | |
| (':id/submissions/my') | |
| (RoleName.STUDENT) | |
| ({ | |
| summary: 'Get my lab submission', | |
| description: 'Get the current student\'s submission for a lab.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ status: 200, description: 'Student submission retrieved' }) | |
| async getMySubmission(('id', ParseIntPipe) id: number, () req: any) { | |
| const userId = req.user.userId || req.user.id; | |
| return this.labsService.getMySubmission(id, userId); | |
| } | |
| (':id/submissions/:subId/grade') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: 'Grade lab submission', | |
| description: 'Grade a lab submission with score, feedback, and status. Creates a grade record in the gradebook.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ name: 'subId', description: 'Submission ID', example: 1 }) | |
| ({ type: GradeLabSubmissionDto }) | |
| ({ status: 200, description: 'Submission graded' }) | |
| ({ status: 404, description: 'Submission not found' }) | |
| async gradeSubmission( | |
| ('id', ParseIntPipe) id: number, | |
| ('subId', ParseIntPipe) subId: number, | |
| () dto: GradeLabSubmissionDto, | |
| () req: any, | |
| ) { | |
| const graderId = req.user.userId || req.user.id; | |
| return this.labsService.gradeSubmission(id, subId, dto, graderId); | |
| } | |
| // ============ ATTENDANCE ============ | |
| (':id/attendance') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| (HttpStatus.CREATED) | |
| ({ | |
| summary: 'Mark lab attendance', | |
| description: 'Mark or update a student\'s attendance for a lab session.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ type: MarkLabAttendanceDto }) | |
| ({ status: 201, description: 'Attendance marked' }) | |
| async markAttendance( | |
| ('id', ParseIntPipe) id: number, | |
| () dto: MarkLabAttendanceDto, | |
| () req: any, | |
| ) { | |
| const markedBy = req.user.userId || req.user.id; | |
| return this.labsService.markAttendance(id, dto, markedBy); | |
| } | |
| (':id/attendance') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: 'Get lab attendance', | |
| description: 'Get attendance records for a lab session.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ({ status: 200, description: 'Attendance records retrieved' }) | |
| async getAttendance(('id', ParseIntPipe) id: number) { | |
| return this.labsService.getAttendance(id); | |
| } | |
| // ============ GOOGLE DRIVE UPLOADS ============ | |
| (':id/instructions/upload') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| (FileInterceptor('file')) | |
| (HttpStatus.CREATED) | |
| ({ | |
| summary: 'Upload lab instruction to Google Drive', | |
| description: 'Upload a lab instruction file (PDF, DOCX, etc.) directly to Google Drive. Creates folder structure automatically.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ('multipart/form-data') | |
| ({ | |
| schema: { | |
| type: 'object', | |
| required: ['file'], | |
| properties: { | |
| file: { | |
| type: 'string', | |
| format: 'binary', | |
| description: 'Instruction file (PDF, DOCX, etc.)', | |
| }, | |
| title: { | |
| type: 'string', | |
| description: 'Instruction title', | |
| example: 'Lab 1 - Getting Started Guide', | |
| }, | |
| orderIndex: { | |
| type: 'integer', | |
| description: 'Order index for instruction steps', | |
| example: 1, | |
| }, | |
| }, | |
| }, | |
| }) | |
| ({ status: 201, description: 'Instruction uploaded successfully' }) | |
| ({ status: 400, description: 'No file provided' }) | |
| ({ status: 404, description: 'Lab not found' }) | |
| async uploadInstruction( | |
| ('id', ParseIntPipe) id: number, | |
| () file: Express.Multer.File, | |
| () dto: UploadLabInstructionDto, | |
| () req: any, | |
| ) { | |
| if (!file) { | |
| throw new Error('No file provided'); | |
| } | |
| const userId = req.user.userId || req.user.id; | |
| return this.labsService.uploadInstructionToDrive( | |
| id, | |
| file, | |
| dto.title, | |
| dto.orderIndex || 0, | |
| userId, | |
| ); | |
| } | |
| (':id/ta-materials/upload') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| (FileInterceptor('file')) | |
| (HttpStatus.CREATED) | |
| ({ | |
| summary: 'Upload TA material to Google Drive', | |
| description: 'Upload TA-only material (answer keys, grading rubrics, etc.) to Google Drive.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ('multipart/form-data') | |
| ({ | |
| schema: { | |
| type: 'object', | |
| required: ['file'], | |
| properties: { | |
| file: { | |
| type: 'string', | |
| format: 'binary', | |
| description: 'TA material file', | |
| }, | |
| title: { | |
| type: 'string', | |
| description: 'Material title', | |
| example: 'Lab 1 Answer Key', | |
| }, | |
| materialType: { | |
| type: 'string', | |
| description: 'Type of TA material', | |
| example: 'answer_key', | |
| enum: ['answer_key', 'grading_rubric', 'solution', 'notes'], | |
| }, | |
| }, | |
| }, | |
| }) | |
| ({ status: 201, description: 'TA material uploaded successfully' }) | |
| ({ status: 400, description: 'No file provided' }) | |
| ({ status: 404, description: 'Lab not found' }) | |
| async uploadTaMaterial( | |
| ('id', ParseIntPipe) id: number, | |
| () file: Express.Multer.File, | |
| () dto: UploadLabTaMaterialDto, | |
| () req: any, | |
| ) { | |
| if (!file) { | |
| throw new Error('No file provided'); | |
| } | |
| const userId = req.user.userId || req.user.id; | |
| return this.labsService.uploadTaMaterialToDrive( | |
| id, | |
| file, | |
| dto.title, | |
| dto.materialType, | |
| userId, | |
| ); | |
| } | |
| (':id/submissions/upload') | |
| (RoleName.STUDENT) | |
| (FileInterceptor('file')) | |
| (HttpStatus.CREATED) | |
| ({ | |
| summary: 'Upload lab submission to Google Drive', | |
| description: 'Upload lab submission file directly to Google Drive. Creates student folder automatically. Auto-detects late submissions.', | |
| }) | |
| ({ name: 'id', description: 'Lab ID', example: 1 }) | |
| ('multipart/form-data') | |
| ({ | |
| 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.', | |
| }, | |
| }, | |
| }, | |
| }) | |
| ({ status: 201, description: 'Submission uploaded successfully' }) | |
| ({ status: 400, description: 'No file provided' }) | |
| ({ status: 404, description: 'Lab not found' }) | |
| async uploadSubmission( | |
| ('id', ParseIntPipe) id: number, | |
| () file: Express.Multer.File, | |
| () dto: UploadLabSubmissionDto, | |
| () req: any, | |
| ) { | |
| if (!file) { | |
| throw new Error('No file provided'); | |
| } | |
| const userId = req.user.userId || req.user.id; | |
| return this.labsService.uploadSubmissionToDrive( | |
| id, | |
| file, | |
| dto.submissionText, | |
| userId, | |
| ); | |
| } | |
| } | |