Skip to content

Notas (sem a definição do calculo da média)#32

Open
SapoSopa wants to merge 101 commits intopauloborba:mainfrom
Zed201:main
Open

Notas (sem a definição do calculo da média)#32
SapoSopa wants to merge 101 commits intopauloborba:mainfrom
Zed201:main

Conversation

@SapoSopa
Copy link

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:

  1. Sistema de Importação de Notas - Importação de notas através de planilhas CSV/XLSX com mapeamento inteligente de colunas
  2. Cálculo Automático de Média - Sistema que calcula médias antes e depois da final, com lógica de aprovação automática
  3. Visualização de Status Acadêmico com Cores - Indicadores visuais (verde/amarelo/vermelho) que mostram o status do aluno baseado no desempenho
  4. Importação de Notas de Roteiros - Funcionalidade específica para importar notas de atividades/roteiros práticos
  5. Gerenciamento Completo de Avaliações - Interface aprimorada com colunas de Média, Final e Média Final
  6. Prevenção de Edição para Aprovados - Sistema impede edição da nota final para alunos já aprovados

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:

  • 🟢 Baixo - Mudança isolada, sem efeitos colaterais
  • 🟡 Médio - Afeta algumas funcionalidades
  • 🔴 Alto - Mudanças críticas ou estruturais

Riscos identificados:

  • Dependência de formato específico de planilhas CSV/XLSX (mitigado com mapeamento flexível)
  • Mudanças na estrutura de persistência podem exigir migração de dados existentes
  • Lógica de aprovação/reprovação precisa estar alinhada com regras da instituição

📦 Área afetada

Frontend (Client - React):

  • 🖥️ Componentes React
  • 🔌 Services/API
  • 📝 Types/Interfaces TypeScript
  • 🎨 Estilos e UI

Backend (Server - Express/Node.js):

  • 🔌 API Routes/Endpoints
  • 📊 Models e lógica de negócio
  • 💾 Persistência de dados

Funcionalidades:

  • 🎓 Gerenciamento de Estudantes
  • 📋 Gerenciamento de Turmas
  • 📝 Gerenciamento de Matrículas
  • 📈 Gerenciamento de Avaliações
  • 📄 Importação de dados
  • Provas
  • Flashcards
  • Gerenciamento de Notas
  • 📊 Cálculo e visualização de médias
  • 🎨 Sistema de cores para status acadêmico

Infraestrutura:

  • 🔧 Configurações (tsconfig, jest.config, cucumber.js)
  • 📦 Dependências (package.json)
  • 🚀 Scripts de build/deploy
  • 📝 Documentação (README, guias)

🔧 Tipo de mudança

  • 🐛 Correção de bug (fix)
  • ✨ Nova funcionalidade (feature)
  • ♻️ Refatoração de código
  • 📚 Atualização de documentação
  • ✅ Adição/atualização de testes
  • 🎨 Melhorias de UI/UX
  • 🔒 Correção de segurança
  • ⚡ Melhoria de performance

🧪 Testes

Testes automatizados

Por nível de teste:

  • 🔬 Testes Unitários (Jest) - Classes e funções isoladas
    • Dependências: versões antigas, stubs e drivers
  • 🔗 Testes de Integração - Interação entre componentes/serviços
    • Dependências: novas versões evoluídas por outros desenvolvedores
  • 🎯 Testes End-to-End (Cucumber) - Fluxos completos da aplicação
    • Dependências: todas as dependências envolvidas
  • 🌐 Testes de Sistema - Ambiente completo de produção
    • Dependências: ambiente de produção com dependências externas reais

Por área testada:

  • 🖥️ Testes de GUI (interface do usuário)
  • 📊 Testes de Classes/Models (lógica de negócio)
  • 🔌 Testes de Services/API (comunicação e endpoints)

Cobertura:

  • Cobertura de testes mantida/aumentada
  • Casos de erro e edge cases cobertos

Como revisar e testar

Testes Unitários (Server):

cd server
npm install
npm test                    # Executar testes
npm run test:watch          # Modo watch
npm run test:coverage       # Com cobertura

Testes E2E (Client):

cd client
npm install
npm start                   # Terminal 1: iniciar app
npm run test:cucumber       # Terminal 2: executar testes

Testar manualmente:

  • Servidor: http://localhost:3000
  • Cliente: http://localhost:3004

📸 Screenshots/GIFs

✅ Checklist

  • Código segue os padrões do projeto
  • Realizei self-review do código
  • Comentei código em áreas complexas
  • Atualizei a documentação (se necessário)
  • Mudanças não geram warnings
  • Adicionei testes que provam que a correção/feature funciona
  • Testes novos e existentes passam localmente
  • Mudanças dependentes foram merged e publicadas

🔗 Issues relacionadas

  • Closes #
  • Relacionado a #

Zed201 and others added 30 commits November 10, 2025 15:30
Importacao notas planilha e implementações de cenário na branch dev
Add input_file componente and fix front-end
Copilot AI review requested due to automatic review settings December 10, 2025 17:52
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +263
<<<<<<< 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
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
}
}
}
// persintence
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "persintence" should be "persistence".

Copilot uses AI. Check for mistakes.
// 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 });
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in response field: "mapping_colums" should be "mapping_columns" (missing 'n'). This inconsistency should be fixed for API clarity.

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +54
const evaluationGoals = [...generalGoals, ...roteiroGoals];

Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable evaluationGoals.

Suggested change
const evaluationGoals = [...generalGoals, ...roteiroGoals];

Copilot uses AI. Check for mistakes.

// pasta usada para salvar os upload's feitos
const upload_dir = multer({dest: 'tmp_data/'})
const upload_dir = multer({ dest: 'tmp_data/' })
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid automated semicolon insertion (97% of all statements in the enclosing script have an explicit semicolon).

Copilot uses AI. Check for mistakes.
return 'approved';
}
// Está na final se média pré-final < 7
if (average < 7) {
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition 'average < 7' is always true.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +81
if (preExamAverage < 7)
{
if (typeof finalAverage === 'number')
{
if (finalAverage >= 5) return 'approved-in-final';
else return 'failed';
}

return 'in-final';
}

return 'failed';
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition 'preExamAverage < 7' is always true.

Suggested change
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';

Copilot uses AI. Check for mistakes.
Comment on lines +731 to +741
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();
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +303 to +354
// 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();
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants