Notas (sem a definição do calculo da média)#32
Notas (sem a definição do calculo da média)#32SapoSopa wants to merge 101 commits intopauloborba:mainfrom
Conversation
…d install projects
… e organizacao da rota
Importacao notas planilha e implementações de cenário na branch dev
Add input_file componente and fix front-end
… into pauloborba-main
… into pauloborba-main
… server integration
Pull Request - Teaching Assistant
There was a problem hiding this comment.
Pull request overview
This PR implements a comprehensive grade management system for a teaching assistant application, including CSV/XLSX grade import, automatic average calculation, visual status indicators, and roteiros (practical activities) grading support.
Key changes:
- New spreadsheet reading service (CSV and XLSX support)
- Grade import functionality with flexible column mapping
- Student status color indicators based on performance
- Average calculation logic (pre-final and post-final)
- Extended evaluation goals to include roteiros (Roteiro 1-6)
Reviewed changes
Copilot reviewed 41 out of 48 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| server/src/services/SpreeadsheetReader.ts | New service for reading CSV/XLSX files with abstract base class pattern |
| server/src/server.ts.orig | Merge conflict file requiring resolution |
| server/src/server.ts | Grade import endpoints, student status endpoint, refactored persistence |
| server/src/models/StudentStatusColor.ts | Color determination logic based on student performance |
| server/src/models/Enrollment.ts | Average calculation implementation |
| server/src/models/Evaluation.ts | Extended goals to include roteiros |
| server/package.json | Added csv-parser and xlsx dependencies |
| client/src/types/* | TypeScript types for student status |
| client/src/features/*.feature | Cucumber E2E test scenarios |
| Multiple test files | Comprehensive unit, integration, and E2E tests |
Files not reviewed (1)
- server/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -0,0 +1,81 @@ | |||
| import readline from "readline"; | |||
There was a problem hiding this comment.
The file is named SpreeadsheetReader.ts with three 'e's instead of SpreadsheetReader.ts with two 'e's. This is a spelling error that should be corrected to maintain consistency and avoid confusion. The correct spelling is "Spreadsheet".
| <<<<<<< HEAD | ||
| try { | ||
| const data = { | ||
| students: studentSet.getAllStudents().map(student => ({ | ||
| name: student.name, | ||
| cpf: student.getCPF(), | ||
| email: student.email | ||
| })), | ||
| classes: classes.getAllClasses().map(classObj => ({ | ||
| topic: classObj.getTopic(), | ||
| semester: classObj.getSemester(), | ||
| year: classObj.getYear(), | ||
| especificacaoDoCalculoDaMedia: classObj.getEspecificacaoDoCalculoDaMedia().toJSON(), | ||
| enrollments: classObj.getEnrollments().map(enrollment => ({ | ||
| studentCPF: enrollment.getStudent().getCPF(), | ||
| evaluations: enrollment.getEvaluations().map(evaluation => evaluation.toJSON()), | ||
| notaFinal: (typeof (enrollment as any).getNotaFinal === 'function') ? (enrollment as any).getNotaFinal() : null, | ||
| mediaPreFinal: enrollment.getMediaPreFinal(), | ||
| mediaPosFinal: enrollment.getMediaPosFinal(), | ||
| reprovadoPorFalta: enrollment.getReprovadoPorFalta() | ||
| })) | ||
| })) | ||
| }; | ||
|
|
||
| ensureDataDirectory(); | ||
| fs.writeFileSync(dataFile, JSON.stringify(data, null, 2), 'utf8'); | ||
| } catch (error) { | ||
| console.error('Error saving students to file:', error); | ||
| } | ||
| ||||||| a2a337c | ||
| try { | ||
| const data = { | ||
| students: studentSet.getAllStudents().map(student => ({ | ||
| name: student.name, | ||
| cpf: student.getCPF(), | ||
| email: student.email | ||
| })), | ||
| classes: classes.getAllClasses().map(classObj => ({ | ||
| topic: classObj.getTopic(), | ||
| semester: classObj.getSemester(), | ||
| year: classObj.getYear(), | ||
| especificacaoDoCalculoDaMedia: classObj.getEspecificacaoDoCalculoDaMedia().toJSON(), | ||
| enrollments: classObj.getEnrollments().map(enrollment => ({ | ||
| studentCPF: enrollment.getStudent().getCPF(), | ||
| evaluations: enrollment.getEvaluations().map(evaluation => evaluation.toJSON()), | ||
| mediaPreFinal: enrollment.getMediaPreFinal(), | ||
| mediaPosFinal: enrollment.getMediaPosFinal(), | ||
| reprovadoPorFalta: enrollment.getReprovadoPorFalta() | ||
| })) | ||
| })) | ||
| }; | ||
|
|
||
| ensureDataDirectory(); | ||
| fs.writeFileSync(dataFile, JSON.stringify(data, null, 2), 'utf8'); | ||
| } catch (error) { | ||
| console.error('Error saving students to file:', error); | ||
| } | ||
| ======= | ||
| try { | ||
| const data = { | ||
| students: studentSet.getAllStudents().map(student => ({ | ||
| name: student.name, | ||
| cpf: student.getCPF(), | ||
| email: student.email | ||
| })), | ||
| classes: classes.getAllClasses().map(classObj => ({ | ||
| topic: classObj.getTopic(), | ||
| semester: classObj.getSemester(), | ||
| year: classObj.getYear(), | ||
| especificacaoDoCalculoDaMedia: classObj.getEspecificacaoDoCalculoDaMedia().toJSON(), | ||
| enrollments: classObj.getEnrollments().map(enrollment => ({ | ||
| studentCPF: enrollment.getStudent().getCPF(), | ||
| evaluations: enrollment.getEvaluations().map(evaluation => evaluation.toJSON()), | ||
| mediaPreFinal: enrollment.getMediaPreFinal(), | ||
| mediaPosFinal: enrollment.getMediaPosFinal(), | ||
| reprovadoPorFalta: enrollment.getReprovadoPorFalta() | ||
| })) | ||
| })) | ||
| }; | ||
|
|
||
| ensureDataDirectory(); | ||
| fs.writeFileSync(dataFile, JSON.stringify(data, null, 2), 'utf8'); | ||
| } catch (error) { | ||
| console.error('Error saving students to file:', error); | ||
| } | ||
| >>>>>>> origin/main | ||
| }; | ||
|
|
||
| // Load data from file | ||
| export const loadDataFromFile = (): void => { | ||
| try { | ||
| if (fs.existsSync(dataFile)) { | ||
| const fileContent = fs.readFileSync(dataFile, 'utf-8'); | ||
| const data = JSON.parse(fileContent); | ||
|
|
||
| // Load students | ||
| if (data.students && Array.isArray(data.students)) { | ||
| data.students.forEach((studentData: any) => { | ||
| // Create student with basic info only - evaluations handled through enrollments | ||
| const student = new Student( | ||
| studentData.name, | ||
| studentData.cpf, | ||
| studentData.email | ||
| ); | ||
|
|
||
| <<<<<<< HEAD | ||
| // Load enrollments for this class | ||
| if (classData.enrollments && Array.isArray(classData.enrollments)) { | ||
| classData.enrollments.forEach((enrollmentData: any) => { | ||
| const student = studentSet.findStudentByCPF(enrollmentData.studentCPF); | ||
| if (student) { | ||
| const enrollment = classObj.addEnrollment(student); | ||
|
|
||
| // Load evaluations for this enrollment | ||
| if (enrollmentData.evaluations && Array.isArray(enrollmentData.evaluations)) { | ||
| enrollmentData.evaluations.forEach((evalData: any) => { | ||
| const evaluation = Evaluation.fromJSON(evalData); | ||
| enrollment.addOrUpdateEvaluation(evaluation.getGoal(), evaluation.getGrade()); | ||
| }); | ||
| } | ||
| // Load notaFinal if provided (keeps evaluations in sync) | ||
| if (typeof enrollmentData.notaFinal !== 'undefined') { | ||
| // setNotaFinal will add/update/remove the 'Final' evaluation as appropriate | ||
| if (typeof (enrollment as any).setNotaFinal === 'function') { | ||
| (enrollment as any).setNotaFinal(enrollmentData.notaFinal); | ||
| } | ||
| } | ||
| // Load medias and attendance status if provided in the data file | ||
| if (typeof enrollmentData.mediaPreFinal !== 'undefined') { | ||
| enrollment.setMediaPreFinal(enrollmentData.mediaPreFinal); | ||
| } | ||
| if (typeof enrollmentData.mediaPosFinal !== 'undefined') { | ||
| enrollment.setMediaPosFinal(enrollmentData.mediaPosFinal); | ||
| } | ||
| if (typeof enrollmentData.reprovadoPorFalta !== 'undefined') { | ||
| enrollment.setReprovadoPorFalta(Boolean(enrollmentData.reprovadoPorFalta)); | ||
| } | ||
| } else { | ||
| console.error(`Student with CPF ${enrollmentData.studentCPF} not found for enrollment`); | ||
| ||||||| a2a337c | ||
| // Load enrollments for this class | ||
| if (classData.enrollments && Array.isArray(classData.enrollments)) { | ||
| classData.enrollments.forEach((enrollmentData: any) => { | ||
| const student = studentSet.findStudentByCPF(enrollmentData.studentCPF); | ||
| if (student) { | ||
| const enrollment = classObj.addEnrollment(student); | ||
|
|
||
| // Load evaluations for this enrollment | ||
| if (enrollmentData.evaluations && Array.isArray(enrollmentData.evaluations)) { | ||
| enrollmentData.evaluations.forEach((evalData: any) => { | ||
| const evaluation = Evaluation.fromJSON(evalData); | ||
| enrollment.addOrUpdateEvaluation(evaluation.getGoal(), evaluation.getGrade()); | ||
| }); | ||
| } | ||
|
|
||
| // Load medias and attendance status if provided in the data file | ||
| if (typeof enrollmentData.mediaPreFinal !== 'undefined') { | ||
| enrollment.setMediaPreFinal(enrollmentData.mediaPreFinal); | ||
| } | ||
| if (typeof enrollmentData.mediaPosFinal !== 'undefined') { | ||
| enrollment.setMediaPosFinal(enrollmentData.mediaPosFinal); | ||
| } | ||
| if (typeof enrollmentData.reprovadoPorFalta !== 'undefined') { | ||
| enrollment.setReprovadoPorFalta(Boolean(enrollmentData.reprovadoPorFalta)); | ||
| } | ||
| } else { | ||
| console.error(`Student with CPF ${enrollmentData.studentCPF} not found for enrollment`); | ||
| ======= | ||
| try { | ||
| studentSet.addStudent(student); | ||
| } catch (error) { | ||
| console.error(`Error adding student ${studentData.name}:`, error); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // Load classes with enrollments | ||
| if (data.classes && Array.isArray(data.classes)) { | ||
| data.classes.forEach((classData: any) => { | ||
| try { | ||
| const classObj = new Class(classData.topic, classData.semester, classData.year, EspecificacaoDoCalculoDaMedia.fromJSON(classData.especificacaoDoCalculoDaMedia)); | ||
| classes.addClass(classObj); | ||
|
|
||
| // Load enrollments for this class | ||
| if (classData.enrollments && Array.isArray(classData.enrollments)) { | ||
| classData.enrollments.forEach((enrollmentData: any) => { | ||
| const student = studentSet.findStudentByCPF(enrollmentData.studentCPF); | ||
| if (student) { | ||
| const enrollment = classObj.addEnrollment(student); | ||
|
|
||
| // Load evaluations for this enrollment | ||
| if (enrollmentData.evaluations && Array.isArray(enrollmentData.evaluations)) { | ||
| enrollmentData.evaluations.forEach((evalData: any) => { | ||
| const evaluation = Evaluation.fromJSON(evalData); | ||
| enrollment.addOrUpdateEvaluation(evaluation.getGoal(), evaluation.getGrade()); | ||
| }); | ||
| } | ||
|
|
||
| // Load medias and attendance status if provided in the data file | ||
| if (typeof enrollmentData.mediaPreFinal !== 'undefined') { | ||
| enrollment.setMediaPreFinal(enrollmentData.mediaPreFinal); | ||
| } | ||
| if (typeof enrollmentData.mediaPosFinal !== 'undefined') { | ||
| enrollment.setMediaPosFinal(enrollmentData.mediaPosFinal); | ||
| } | ||
| if (typeof enrollmentData.reprovadoPorFalta !== 'undefined') { | ||
| enrollment.setReprovadoPorFalta(Boolean(enrollmentData.reprovadoPorFalta)); | ||
| } | ||
| } else { | ||
| console.error(`Student with CPF ${enrollmentData.studentCPF} not found for enrollment`); | ||
| } | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| console.error(`Error adding class ${classData.topic}:`, error); | ||
| } | ||
| }); | ||
| } | ||
| >>>>>>> origin/main | ||
| } |
There was a problem hiding this comment.
This file contains unresolved merge conflict markers (<<<<<<<, =======, >>>>>>>). The merge conflict must be resolved before merging this PR. Lines 44-263 show conflicting versions of the saveDataToFile and loadDataFromFile functions that need to be properly merged.
| } | ||
| } | ||
| } | ||
| // persintence |
There was a problem hiding this comment.
Typo in comment: "persintence" should be "persistence".
| // store session metadata so later requests can detect original extension | ||
| uploadSessions.set(uploaded.path, { ext, original }); | ||
| // return the temp path (session_string) + header columns and expected mapping fields | ||
| return res.status(200).json({ session_string: uploaded.path, file_columns, mapping_colums: goals_field }); |
There was a problem hiding this comment.
Typo in response field: "mapping_colums" should be "mapping_columns" (missing 'n'). This inconsistency should be fixed for API clarity.
| const evaluationGoals = [...generalGoals, ...roteiroGoals]; | ||
|
|
There was a problem hiding this comment.
Unused variable evaluationGoals.
| const evaluationGoals = [...generalGoals, ...roteiroGoals]; | |
|
|
||
| // pasta usada para salvar os upload's feitos | ||
| const upload_dir = multer({dest: 'tmp_data/'}) | ||
| const upload_dir = multer({ dest: 'tmp_data/' }) |
There was a problem hiding this comment.
Avoid automated semicolon insertion (97% of all statements in the enclosing script have an explicit semicolon).
| return 'approved'; | ||
| } | ||
| // Está na final se média pré-final < 7 | ||
| if (average < 7) { |
There was a problem hiding this comment.
The condition 'average < 7' is always true.
| if (preExamAverage < 7) | ||
| { | ||
| if (typeof finalAverage === 'number') | ||
| { | ||
| if (finalAverage >= 5) return 'approved-in-final'; | ||
| else return 'failed'; | ||
| } | ||
|
|
||
| return 'in-final'; | ||
| } | ||
|
|
||
| return 'failed'; |
There was a problem hiding this comment.
The condition 'preExamAverage < 7' is always true.
| if (preExamAverage < 7) | |
| { | |
| if (typeof finalAverage === 'number') | |
| { | |
| if (finalAverage >= 5) return 'approved-in-final'; | |
| else return 'failed'; | |
| } | |
| return 'in-final'; | |
| } | |
| return 'failed'; | |
| // preExamAverage < 7 is always true here | |
| if (typeof finalAverage === 'number') | |
| { | |
| if (finalAverage >= 5) return 'approved-in-final'; | |
| else return 'failed'; | |
| } | |
| return 'in-final'; |
| const { session_string, mapping } = req.body ?? {}; | ||
| if (!session_string || !mapping) return res.status(400).json({ error: 'session_string and mapping are required' }); | ||
|
|
||
| // invert mapping so we can lookup file column by goal: { goal -> fileColumn } | ||
| const invertedMapping = Object.fromEntries(Object.entries(mapping).map(([k, v]) => [v, k])); | ||
| // try to recover original extension from uploadSessions (multer's uploaded.path usually has no extension) | ||
| const meta = uploadSessions.get(session_string); | ||
| const sessionExt = (meta?.ext) ? meta.ext : path.extname(session_string).toLowerCase(); | ||
| const ReaderForSession = sessionExt === '.xlsx' || sessionExt === '.xls' ? XLSXReader : CSVReader; | ||
| const sheet = new ReaderForSession(session_string); | ||
| const data = await sheet.process(); |
There was a problem hiding this comment.
In the grade import endpoint, the session_string value from the request body is used directly as a filesystem path in new ReaderForSession(session_string), without verifying that it corresponds to a previously uploaded file or that it resides under the expected upload directory. This allows an attacker to call STEP 2 of /api/classes/gradeImport/:classId with an arbitrary session_string and force CSVReader/XLSXReader to read from any readable path on the server, potentially exposing local files through the parsed JSON response. To fix this, ensure session_string is looked up in uploadSessions, reject requests when there is no matching entry, and additionally validate that the resolved path is inside the tmp_data/ directory (e.g. via path.resolve and a prefix check) before opening the file.
| // POST /api/students - Add a new student | ||
| app.post('/api/students', (req: Request, res: Response) => { | ||
| try { | ||
| const { name, cpf, email } = req.body; | ||
|
|
||
| if (!name || !cpf || !email) { | ||
| return res.status(400).json({ error: 'Name, CPF, and email are required' }); | ||
| } | ||
|
|
||
| // Create student with basic information only - evaluations handled through enrollments | ||
| const student = new Student(name, cpf, email); | ||
| const addedStudent = studentSet.addStudent(student); | ||
| triggerSave(); // Save to file after adding | ||
| res.status(201).json(addedStudent.toJSON()); | ||
| } catch (error) { | ||
| res.status(400).json({ error: (error as Error).message }); | ||
| } | ||
| }); | ||
|
|
||
| // PUT /api/students/:cpf - Update a student | ||
| app.put('/api/students/:cpf', (req: Request, res: Response) => { | ||
| try { | ||
| const { cpf } = req.params; | ||
| const { name, email } = req.body; | ||
|
|
||
| if (!name || !email) { | ||
| return res.status(400).json({ error: 'Name and email are required for update' }); | ||
| } | ||
|
|
||
| // Create a Student object for update - evaluations handled through enrollments | ||
| const updatedStudent = new Student(name, cpf, email); | ||
| const result = studentSet.updateStudent(updatedStudent); | ||
| triggerSave(); // Save to file after updating | ||
| res.json(result.toJSON()); | ||
| } catch (error) { | ||
| res.status(400).json({ error: (error as Error).message }); | ||
| } | ||
| }); | ||
|
|
||
| // DELETE /api/students/:cpf - Delete a student | ||
| app.delete('/api/students/:cpf', (req: Request, res: Response) => { | ||
| try { | ||
| const { cpf } = req.params; | ||
| const cleanedCPF = cleanCPF(cpf); | ||
| const success = studentSet.removeStudent(cleanedCPF); | ||
|
|
||
| if (!success) { | ||
| return res.status(404).json({ error: 'Student not found' }); | ||
| } | ||
|
|
||
| triggerSave(); // Save to file after deleting | ||
| res.status(204).send(); |
There was a problem hiding this comment.
All of the state-changing API routes (e.g. POST /api/students, PUT /api/students/:cpf, DELETE /api/students/:cpf) are exposed without any authentication or authorization checks, so any network client that can reach this service can create, modify, or delete student records and grades. This effectively allows unauthorized users to tamper with academic data, which is a high-impact integrity and privacy issue. To address this, introduce proper authentication (e.g. session/JWT middleware) and enforce role-based authorization on these endpoints so that only authorized professors/admins can perform these operations.
Pull Request - Teaching Assistant
📝 Descrição
O que foi feito:
Este PR implementa um conjunto completo de funcionalidades para o sistema de assistente de ensino, incluindo:
Por que foi feito:
Para automatizar e melhorar o processo de gestão acadêmica, reduzindo trabalho manual do professor ao importar notas, calcular médias e identificar visualmente o status dos alunos. O sistema agora oferece uma solução completa desde a importação até a visualização do desempenho dos estudantes.
📌 Observações
Impacto:
Riscos identificados:
📦 Área afetada
Frontend (Client - React):
Backend (Server - Express/Node.js):
Funcionalidades:
Infraestrutura:
🔧 Tipo de mudança
🧪 Testes
Testes automatizados
Por nível de teste:
Por área testada:
Cobertura:
Como revisar e testar
Testes Unitários (Server):
Testes E2E (Client):
Testar manualmente:
http://localhost:3000http://localhost:3004📸 Screenshots/GIFs
✅ Checklist
🔗 Issues relacionadas