From c598b49131b5d10bb76930fbf42bb35c19628c55 Mon Sep 17 00:00:00 2001 From: Gustavo Choueiri Date: Sat, 23 May 2026 12:18:48 -0300 Subject: [PATCH 1/6] Fix typo in README description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d7fe1bf53..46ca7a29aa 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # CAMAAR -Sistema para avaliação de atividades acadêmicas remotas do CIC +Sistema para avaliação de atividades acadêmicas remotas do CIC. From c393e5619b67af942249f97f3223d48a5f526470 Mon Sep 17 00:00:00 2001 From: staann Date: Thu, 4 Jun 2026 10:13:29 -0300 Subject: [PATCH 2/6] atualiza prioridades.md --- prioridades.md | 314 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 prioridades.md diff --git a/prioridades.md b/prioridades.md new file mode 100644 index 0000000000..9d09624b5d --- /dev/null +++ b/prioridades.md @@ -0,0 +1,314 @@ +# CAMAAR - Sprint 2: Priorização de Issues +**Product Owner**: [Seu nome] +**Data**: 29 de maio de 2026 +**Total de Issues**: 17 + +--- + +## 📊 Matriz de Priorização + +| Prioridade | Issues | Critério | +|---|---|---| +| 🔴 **P0 - CRÍTICO** | #13, #12, #17 | Bloqueadores - Precisam estar prontos para qualquer outra funcionalidade | +| 🟠 **P1 - ALTA** | #15, #14, #8, #18 | Funcionalidades core do negócio (templates, formulários, respostas) | +| 🟡 **P2 - MÉDIA** | #11, #16, #9 | Gerenciamento e relatórios (importante mas não bloqueia MVP) | +| 🔵 **P3 - BAIXA** | #10, #19 | Melhorias/Integrações (podem ser feitas depois) | + +--- + +## 🎯 PRIORIDADE 0 - CRÍTICOS (Sprint 2) + +Estas issues **DEVEM ser concluídas antes de qualquer outra**. Sem elas, nenhuma funcionalidade funciona. + +### **1. #17 - Cadastrar usuários do sistema** +- **Status**: 🔴 P0 - CRÍTICO +- **Assignee**: [Definir] +- **Esforço**: Grande (G) +- **Dependência**: Nenhuma +- **Por quê?** Sem usuários cadastrados, não há login, não há respostas, não há formulários. Precisa estar pronta **antes de tudo**. +- **Descrição**: Criar seed/script para popular a base com docentes e discentes do SIGAA via `class_members.json` e `classes.json` +- **Critério de Aceitação**: + - Usuários criados via migration/seed com perfil correto (docente/discente) + - Senhas hash geradas (primeiro acesso = true) + - Vínculo com Docente e Discente criado + - Todas as matrículas de turmas populadas + +### **2. #13 - Sistema de Login** +- **Status**: 🔴 P0 - CRÍTICO +- **Assignee**: Anarayssa-dev +- **Esforço**: Médio (M) +- **Dependência**: #17 (usuários existirem) +- **Por quê?** É o **portão de entrada** do sistema. Sem login, ninguém acessa nada. +- **Descrição**: Implementar autenticação com e-mail ou matrícula + senha. Rails: usar Devise ou implementar via sessions. +- **Critério de Aceitação**: + - Login com e-mail **OU** matrícula (campo login em Usuario) + - Validação de senha hash + - Primeira vez = redireção para mudança de senha (issue #12) + - Admin logado vê menu de gerenciamento + - Logout funciona + - Guard em todas as rotas (apenas autenticados acessam) + +### **3. #12 - Sistema de definição de senha** +- **Status**: 🔴 P0 - CRÍTICO +- **Assignee**: [Definir] +- **Esforço**: Pequeno (P) +- **Dependência**: #13 (login está pronto) +- **Por quê?** Na primeira vez que o usuário loga, ele **deve mudar a senha padrão**. Bloqueador de segurança e UX. +- **Descrição**: Tela de redefinição de senha no primeiro acesso (flag `primeiro_acesso=true`). +- **Critério de Aceitação**: + - Ao fazer login com `primeiro_acesso=true`, é redirecionado para `/usuarios/mudar-senha` + - Valida força da senha (mín 8 caracteres, maiúscula, número) + - Após confirmação, `primeiro_acesso=false` e redireciona para dashboard + - Hash da nova senha salvo corretamente em Usuario + +--- + +## 🟠 PRIORIDADE 1 - ALTA (Sprint 2) + +Estas são as funcionalidades **core do negócio**. Começam assim que o P0 estiver 80%+ pronto. + +### **4. #15 - Criar template de formulário** ⭐ +- **Status**: 🟠 P1 - ALTA +- **Assignee**: rafaelcarvalhoj +- **Esforço**: Grande (G) +- **Dependência**: #13, #12, #17 (autenticação pronta + usuários como docentes) +- **Por quê?** É o **pilar técnico do spike** (views, templates, forms do Rails). Alinha 100% com o MER (Template, QuestaoTemplate, OpcaoQuestao). +- **Descrição**: Implementar CRUD de templates com: + - Criar template (titulo, descricao, docente_id) + - Adicionar questões ao template (enunciado, tipo: aberta/multipla) + - Se multipla → adicionar opções (texto, questao_template_id) + - Editar/deletar template e questões + - Listar templates criados pelo docente logado +- **Views Rails necessárias**: + - `templates/new` e `templates/edit` (form para template) + - `questao_templates/new` e `questao_templates/edit` (nested form para questões) + - `templates/show` (visualizar template completo) + - `templates/index` (listar templates do docente) +- **Critério de Aceitação**: + - Template pode ter múltiplas questões + - Questão salva com tipo correto + - Opção pertence a uma questão (FK: questao_template_id) + - Somente docente que criou pode editar/deletar + - Validações: titulo obrigatório, mín 1 questão para salvar + +### **5. #14 - Criar formulário de avaliação** +- **Status**: 🟠 P1 - ALTA +- **Assignee**: [Definir] +- **Esforço**: Médio (M) +- **Dependência**: #15 (templates existem) +- **Por quê?** A avaliação **depende de templates**. Docente cria template → gera formulário → discentes respondem. +- **Descrição**: Implementar CRUD de formulários (Formulario): + - Selecionar template + turma + prazo + - Criar formulário (puxa questões do template para a tabela Questao) + - Editar/deletar apenas se nenhum discente respondeu ainda + - Visualizar quem respondeu (listar EnvioFormulario) +- **Views Rails**: + - `formularios/new` (form: selecionar template, turma, prazo) + - `formularios/show` (listar questões do formulário, status de respostas) + - `formularios/index` (listar formulários da turma do docente) +- **Critério de Aceitação**: + - Formulário instancia questões do template (cria registros em Questao com FK para template_id) + - Prazo armazenado (datetime) + - Turma_id + docente_id associados corretamente + - Não permite editar se houver envios (EnvioFormulario) + +### **6. #8 - Visualização de formulários para responder** +- **Status**: 🟠 P1 - ALTA +- **Assignee**: [Definir] +- **Esforço**: Pequeno (P) +- **Dependência**: #14 (formulários existem), #17 (discentes existem) +- **Por quê?** Discente precisa **ver** o formulário antes de responder. É a interface "de leitura". +- **Descrição**: Listar formulários que o discente logado ainda não respondeu (via turmas que está matriculado). +- **Views**: + - `formularios/listar` (filtrar: minha turma, prazo aberto, não respondido ainda) + - `formularios/show` (ver questões, tipo, opções se multipla) +- **Critério de Aceitação**: + - Mostra apenas formulários de turmas que o discente está matriculado + - Mostra apenas se EnvioFormulario NÃO existe para (discente_id, formulario_id) + - Mostra status: "Aberto" se dentro do prazo, "Fechado" se passou + - Botão para responder (vai para #18) + +### **7. #18 - Responder formulário** ⭐ +- **Status**: 🟠 P1 - ALTA +- **Assignee**: [Definir] +- **Esforço**: Médio (M) +- **Dependência**: #8 (listar formulários), #14 (formulários existem) +- **Por quê?** É a **funcionalidade principal do discente**. Alinha com EnvioFormulario + Resposta do MER. +- **Descrição**: Implementar fluxo completo de resposta: + - Exibir formulário (questões com tipo aberta/multipla) + - Se multipla → radio buttons das opções (OpcaoQuestao) + - Se aberta → textarea para resposta + - Validar: todas as questões respondidas + - Salvar EnvioFormulario (1x por discente) + registros em Resposta (1 por questão) +- **Views**: + - `envios_formularios/new` (form dinâmico com questões do formulário) + - `envios_formularios/show` (mostra a resposta já enviada - read-only) +- **Critério de Aceitação**: + - EnvioFormulario criado com (formulario_id, discente_id) - UNIQUE constraint respeitado + - Cada Resposta tem (envio_id, questao_id, conteudo) + - Descenta erro se tentar responder 2x o mesmo formulário + - Timestamp de envio registrado + - Após envio → redireciona para dashboard com mensagem de sucesso + +--- + +## 🟡 PRIORIDADE 2 - MÉDIA (Sprint 2 ou 3) + +Funções administrativas e relatórios. **Importantes mas não bloqueiam MVP**. Podem começar em paralelo se houver recursos. + +### **8. #11 - Sistema de gerenciamento por departamento** +- **Status**: 🟡 P2 - MÉDIA +- **Assignee**: [Definir] +- **Esforço**: Médio (M) +- **Dependência**: #13 (admin logado), #15, #14 (formulários existem) +- **Por quê?** Admin gerencia docentes por departamento. Alinha com o MER (Departamento é entidade própria agora). +- **Descrição**: Views para admin: + - Listar departamentos + - Criar/editar departamentos + - Listar docentes de um departamento + - Visualizar formulários criados por docentes do departamento + - Gerar relatórios por departamento +- **Critério de Aceitação**: + - Somente usuários com perfil 'admin' acessam + - Docente tem FK para departamento + - Filtro por departamento nas views + +### **9. #16 - Gerar relatório do administrador** +- **Status**: 🟡 P2 - MÉDIA +- **Assignee**: [Definir] +- **Esforço**: Médio (M) +- **Dependência**: #18 (respostas existem), #11 (departamentos gerenciados) +- **Por quê?** Relatórios têm **valor agregado** mas não são funcionalidade bloqueadora. +- **Descrição**: Admin pode gerar relatório com: + - Número de discentes por turma + - Taxa de resposta por formulário (% que responderam) + - Estatísticas por departamento + - Export em CSV/PDF +- **Critério de Aceitação**: + - Relatório filtra por departamento/turma/período + - Mostra dados agregados de Resposta + - Pode ser exportado + +### **10. #9 - Atualizar base de dados com os dados do SIGAA** +- **Status**: 🟡 P2 - MÉDIA +- **Assignee**: [Definir] +- **Esforço**: Pequeno (P) +- **Dependência**: #17 (primeiro carregamento feito) +- **Por quê?** Precisa sincronizar SIGAA periodicamente (rake task). Mas primeira carga já funciona em #17. +- **Descrição**: Implementar rake task que: + - Lê `class_members.json` e `classes.json` novamente + - Atualiza usuários, turmas, matrículas + - Cria novos usuários se surgiram + - Desativa usuários que saíram +- **Critério de Aceitação**: + - Task `rake camaar:sync_sigaa` roda sem erros + - Logs indicam quantos registros foram atualizados/criados + +--- + +## 🔵 PRIORIDADE 3 - BAIXA (Sprint 3+) + +Funcionalidades que **agregam valor mas podem esperar**. Segundo MVP. + +### **11. #10 - Redefinição de senha** +- **Status**: 🔵 P3 - BAIXA +- **Assignee**: [Definir] +- **Esforço**: Pequeno (P) +- **Dependência**: #13 (login pronto) +- **Por quê?** Usuário esqueceu senha. Importante para UX mas não é bloqueador. +- **Descrição**: Fluxo: + - Link "Esqueci minha senha" na tela de login + - Insere e-mail + - Recebe link de reset por e-mail (token temporário) + - Clica link → define nova senha + - Volta a logar com nova senha + +### **12. #19 - Importar dados do SIGAA** +- **Status**: 🔵 P3 - BAIXA +- **Assignee**: [Definir] +- **Esforço**: Grande (G) +- **Dependência**: #17 (primeira carga feita) +- **Por quê?** **Integrações externas** são complexas. O MVP funciona com dados estáticos em `class_members.json`. Integração com SIGAA pode vir depois. +- **Descrição**: Conectar à API do SIGAA (ou scraping) para trazer dados em tempo real. +- **Nota**: Pode ser feito em Sprint 3 ou posterior. Por enquanto, usar CSV/JSON. + +--- + +## 📈 Roadmap da Sprint 2 + +``` +SEMANA 1 +├─ P0 #17 (50%) - Cadastrar usuários +├─ P0 #13 (início) - Login +└─ P0 #12 (planejamento) + +SEMANA 2 +├─ P0 #13 (80%) +├─ P0 #12 (100%) +├─ P1 #15 (início) - Template +└─ P1 #14 (planejamento) + +SEMANA 3 +├─ P1 #15 (80%) +├─ P1 #14 (50%) +├─ P1 #8 (início) - Listar formulários +└─ P2 #11 (planejamento) ← Se houver recursos + +SEMANA 4 +├─ P1 #14 (100%) +├─ P1 #8 (100%) +├─ P1 #18 (início/end) - Responder +├─ P2 #9 (100%) - Sync SIGAA +└─ BUFFER para testes e refine +``` + +--- + +## ⚠️ Riscos & Observações + +| Risco | Mitigation | Owner | +|-------|-----------|-------| +| Spike técnico de Rails (forms, nested routes) pode atrasar #15 | Documentar padrão, pair programming | Tech Lead | +| Mudança no MER pode impactar #13-#18 | MER está **FINAL**. Congelar para Sprint 2. | PO | +| Sincronização SIGAA (#9) pode ter problemas de formato | Usar dados estáticos (class_members.json) na Sprint 2 | Dev | +| Login (#13) é crítico → testes automatizados obrigatórios | TDD desde o início | Dev | +| Constraint UNIQUE em EnvioFormulario (#18) evita duplicatas | Validação em model + migration com `add_index` | DB | + +--- + +## ✅ Critérios de Definição de Pronto (DoD) + +Cada issue **só pode sair para Pronto quando**: + +- [ ] Code review aprovado +- [ ] Testes unitários + integração cobrindo > 80% do código +- [ ] Funcionalidade testada manualmente no navegador +- [ ] Sem console warnings/errors +- [ ] Documentação atualizada (se aplicável) +- [ ] Migration/seed pronta (se DB) +- [ ] Nenhuma issue bloqueadora aberta + +--- + +## 📝 Notas do Product Owner + +**Alinhamento com MER**: Todas as issues de P0-P1 foram desenhadas para respeitar a estrutura final do banco de dados. **Nenhuma mudança no MER durante a Sprint 2.** + +**Spike Técnico Rails**: O spike indicou que: +- Views devem usar `form_with` + partials para DRY +- Nested routes (`templates/:id/questao_templates/new`) +- Strong params para segurança +- Validações in model + in controller + +**Métricas de sucesso ao final da Sprint 2**: +- ✅ Login + cadastro funcionando 100% +- ✅ Template + Formulário 100% +- ✅ Discente consegue responder formulário 100% +- ✅ Docente consegue visualizar respostas 100% +- ✅ Taxa de teste > 80% +- ✅ Sem bloqueadores críticos em aberto + +--- + +**Assinado**: Product Owner +**Data**: 29 de maio de 2026 \ No newline at end of file From bbd1b32c3d4c11aaaafdb89444785233ce05a1dd Mon Sep 17 00:00:00 2001 From: staann Date: Thu, 4 Jun 2026 10:30:12 -0300 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20Implementa=C3=A7=C3=A3o=20do=20sist?= =?UTF-8?q?ema=20de=20formul=C3=A1rios=20com=20autentica=C3=A7=C3=A3o=20e?= =?UTF-8?q?=20gerenciamento=20de=20usu=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona visualização de formulários com detalhes e questões. - Cria layout base da aplicação com cabeçalho, rodapé e navegação. - Define rotas para autenticação, formulários e envio de respostas. - Cria tabelas no banco de dados para usuários, disciplinas, turmas, formulários e respostas. - Popula o banco de dados com dados iniciais através de seeds. - Implementa testes para o controlador de formulários e modelos de dados. - Adiciona factories para facilitar a criação de dados de teste. --- app/controllers/application_controller.rb | 56 +++ app/controllers/formularios_controller.rb | 97 +++++ app/models/discente.rb | 53 +++ app/models/turma.rb | 157 ++++++++ app/models/usuario.rb | 31 ++ .../formularios/_formulario_card.html.erb | 67 ++++ app/views/formularios/index.html.erb | 346 ++++++++++++++++++ app/views/formularios/minha_resposta.html.erb | 192 ++++++++++ app/views/formularios/show.html.erb | 239 ++++++++++++ app/views/layouts/application.html.erb | 254 +++++++++++++ config/routes.rb | 24 ++ .../20240101000001_create_camaar_tables.rb | 110 ++++++ db/seeds.rb | 88 +++++ .../formularios_controller_spec.rb | 146 ++++++++ spec/factories/factories.rb | 89 +++++ spec/models/discente_spec.rb | 125 +++++++ 16 files changed, 2074 insertions(+) create mode 100644 app/controllers/application_controller.rb create mode 100644 app/controllers/formularios_controller.rb create mode 100644 app/models/discente.rb create mode 100644 app/models/turma.rb create mode 100644 app/models/usuario.rb create mode 100644 app/views/formularios/_formulario_card.html.erb create mode 100644 app/views/formularios/index.html.erb create mode 100644 app/views/formularios/minha_resposta.html.erb create mode 100644 app/views/formularios/show.html.erb create mode 100644 app/views/layouts/application.html.erb create mode 100644 config/routes.rb create mode 100644 db/migrate/20240101000001_create_camaar_tables.rb create mode 100644 db/seeds.rb create mode 100644 spec/controllers/formularios_controller_spec.rb create mode 100644 spec/factories/factories.rb create mode 100644 spec/models/discente_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000000..dd1b572106 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,56 @@ +# app/controllers/application_controller.rb +class ApplicationController < ActionController::Base + before_action :autenticar_usuario! + + helper_method :usuario_logado, :discente_logado?, :docente_logado?, :admin_logado? + + private + + def autenticar_usuario! + unless session[:usuario_id] + redirect_to login_path, alert: "Você precisa estar logado para acessar esta página." + end + end + + def usuario_logado + @usuario_logado ||= Usuario.find_by(id: session[:usuario_id]) + end + + def discente_atual + @discente_atual ||= usuario_logado&.discente + end + + def docente_atual + @docente_atual ||= usuario_logado&.docente + end + + def discente_logado? + usuario_logado&.discente? + end + + def docente_logado? + usuario_logado&.docente? + end + + def admin_logado? + usuario_logado&.admin? + end + + def exigir_discente! + unless discente_logado? + redirect_to root_path, alert: "Acesso restrito a discentes." + end + end + + def exigir_docente! + unless docente_logado? + redirect_to root_path, alert: "Acesso restrito a docentes." + end + end + + def exigir_admin! + unless admin_logado? + redirect_to root_path, alert: "Acesso restrito a administradores." + end + end +end diff --git a/app/controllers/formularios_controller.rb b/app/controllers/formularios_controller.rb new file mode 100644 index 0000000000..56bd9d215b --- /dev/null +++ b/app/controllers/formularios_controller.rb @@ -0,0 +1,97 @@ +# app/controllers/formularios_controller.rb +class FormulariosController < ApplicationController + before_action :exigir_discente! + before_action :set_formulario, only: [:show] + + # GET /formularios + # Issue #8: Lista formulários disponíveis para o discente responder + def index + @turmas = discente_atual.turmas.includes(:disciplina, :docente) + + @formularios_pendentes = formularios_do_discente + .abertos + .where.not(id: formularios_ja_respondidos) + .includes(:turma, :template) + .order(prazo: :asc) + + @formularios_respondidos = formularios_do_discente + .joins(:envio_formularios) + .where(envio_formularios: { discente_id: discente_atual.id }) + .includes(:turma, :template) + .order("envio_formularios.enviado_em desc") + + @formularios_fechados = formularios_do_discente + .fechados + .where.not(id: formularios_ja_respondidos) + .includes(:turma, :template) + .order(prazo: :desc) + end + + # GET /formularios/:id + # Mostra detalhes do formulário (preview antes de responder) + def show + @ja_respondeu = discente_atual.ja_respondeu?(@formulario) + @envio = discente_atual.envio_formularios.find_by(formulario: @formulario) + + if @ja_respondeu + redirect_to minha_resposta_formulario_path(@formulario), + notice: "Você já respondeu este formulário." + return + end + + unless @formulario.aberto? + redirect_to formularios_path, + alert: "O prazo para responder este formulário já encerrou." + return + end + + unless discente_pertence_a_turma?(@formulario.turma) + redirect_to formularios_path, + alert: "Você não tem acesso a este formulário." + return + end + + @questoes = @formulario.questoes.includes(questao_template: :opcao_questoes) + end + + # GET /formularios/:id/minha_resposta + # Visualiza resposta já enviada (read-only) + def minha_resposta + @formulario = Formulario.find(params[:id]) + @envio = discente_atual.envio_formularios.find_by(formulario: @formulario) + + unless @envio + redirect_to formularios_path, + alert: "Você ainda não respondeu este formulário." + return + end + + unless discente_pertence_a_turma?(@formulario.turma) + redirect_to formularios_path, alert: "Você não tem acesso a este formulário." + return + end + + @respostas = @envio.respostas.includes(questao: { questao_template: :opcao_questoes }) + end + + private + + def set_formulario + @formulario = Formulario.find(params[:id]) + rescue ActiveRecord::RecordNotFound + redirect_to formularios_path, alert: "Formulário não encontrado." + end + + def formularios_do_discente + turmas_ids = discente_atual.turmas.pluck(:id) + Formulario.where(turma_id: turmas_ids) + end + + def formularios_ja_respondidos + discente_atual.envio_formularios.pluck(:formulario_id) + end + + def discente_pertence_a_turma?(turma) + discente_atual.turmas.include?(turma) + end +end diff --git a/app/models/discente.rb b/app/models/discente.rb new file mode 100644 index 0000000000..a00ef3a476 --- /dev/null +++ b/app/models/discente.rb @@ -0,0 +1,53 @@ +# app/models/discente.rb +class Discente < ApplicationRecord + belongs_to :usuario + + has_many :matriculas, dependent: :destroy + has_many :turmas, through: :matriculas + has_many :envio_formularios, dependent: :destroy + has_many :formularios, through: :envio_formularios + + validates :curso, presence: true + validates :matricula, presence: true, uniqueness: true + + delegate :nome, :email, to: :usuario + + def formularios_disponiveis + turmas_ids = turmas.pluck(:id) + formularios_respondidos_ids = envio_formularios.pluck(:formulario_id) + + Formulario + .where(turma_id: turmas_ids) + .where.not(id: formularios_respondidos_ids) + end + + def ja_respondeu?(formulario) + envio_formularios.exists?(formulario: formulario) + end +end + +# app/models/docente.rb +class Docente < ApplicationRecord + belongs_to :usuario + belongs_to :departamento + + has_many :turmas, dependent: :restrict_with_error + has_many :templates, dependent: :restrict_with_error + + delegate :nome, :email, to: :usuario +end + +# app/models/departamento.rb +class Departamento < ApplicationRecord + has_many :docentes, dependent: :restrict_with_error + + validates :nome, presence: true, uniqueness: true +end + +# app/models/disciplina.rb +class Disciplina < ApplicationRecord + has_many :turmas, dependent: :restrict_with_error + + validates :codigo, presence: true, uniqueness: true + validates :nome, presence: true +end diff --git a/app/models/turma.rb b/app/models/turma.rb new file mode 100644 index 0000000000..27bf75df93 --- /dev/null +++ b/app/models/turma.rb @@ -0,0 +1,157 @@ +# app/models/turma.rb +class Turma < ApplicationRecord + belongs_to :disciplina + belongs_to :docente + + has_many :matriculas, dependent: :destroy + has_many :discentes, through: :matriculas + has_many :formularios, dependent: :restrict_with_error + + validates :codigo, presence: true + validates :semestre, presence: true + + def nome_completo + "#{disciplina.nome} - #{codigo} (#{semestre})" + end +end + +# app/models/matricula.rb +class Matricula < ApplicationRecord + belongs_to :discente + belongs_to :turma + + validates :discente_id, uniqueness: { scope: :turma_id, + message: "já está matriculado nesta turma" } +end + +# app/models/template.rb +class Template < ApplicationRecord + belongs_to :docente + + has_many :questao_templates, dependent: :destroy + has_many :formularios, dependent: :restrict_with_error + + validates :titulo, presence: true + validate :deve_ter_ao_menos_uma_questao, on: :update + + private + + def deve_ter_ao_menos_uma_questao + if questao_templates.empty? + errors.add(:base, "Template deve ter ao menos uma questão") + end + end +end + +# app/models/questao_template.rb +class QuestaoTemplate < ApplicationRecord + belongs_to :template + + has_many :opcao_questoes, dependent: :destroy + has_many :questoes, dependent: :restrict_with_error + + enum :tipo, { aberta: 0, multipla: 1 } + + validates :enunciado, presence: true + validates :tipo, presence: true +end + +# app/models/opcao_questao.rb +class OpcaoQuestao < ApplicationRecord + belongs_to :questao_template + + validates :texto, presence: true +end + +# app/models/formulario.rb +class Formulario < ApplicationRecord + belongs_to :turma + belongs_to :template + + has_many :questoes, dependent: :destroy + has_many :envio_formularios, dependent: :destroy + + validates :titulo, presence: true + + scope :abertos, -> { where("prazo IS NULL OR prazo > ?", Time.current) } + scope :fechados, -> { where("prazo IS NOT NULL AND prazo <= ?", Time.current) } + + def aberto? + prazo.nil? || prazo > Time.current + end + + def fechado? + !aberto? + end + + def status + aberto? ? :aberto : :fechado + end + + def status_label + aberto? ? "Aberto" : "Fechado" + end + + def respondido_por?(discente) + envio_formularios.exists?(discente: discente) + end + + def total_discentes + turma.discentes.count + end + + def total_respostas + envio_formularios.count + end +end + +# app/models/questao.rb +class Questao < ApplicationRecord + belongs_to :formulario + belongs_to :questao_template + + has_many :respostas, dependent: :destroy + + enum :tipo, { aberta: 0, multipla: 1 } + + validates :enunciado, presence: true + + delegate :opcao_questoes, to: :questao_template +end + +# app/models/envio_formulario.rb +class EnvioFormulario < ApplicationRecord + belongs_to :formulario + belongs_to :discente + + has_many :respostas, dependent: :destroy + + validates :formulario_id, uniqueness: { + scope: :discente_id, + message: "Você já respondeu este formulário" + } + + validate :formulario_deve_estar_aberto, on: :create + + before_create :registrar_envio + + private + + def registrar_envio + self.enviado_em = Time.current + end + + def formulario_deve_estar_aberto + if formulario&.fechado? + errors.add(:base, "O prazo para responder este formulário já encerrou") + end + end +end + +# app/models/resposta.rb +class Resposta < ApplicationRecord + belongs_to :envio_formulario + belongs_to :questao + + validates :conteudo, presence: true +end diff --git a/app/models/usuario.rb b/app/models/usuario.rb new file mode 100644 index 0000000000..f9732c620d --- /dev/null +++ b/app/models/usuario.rb @@ -0,0 +1,31 @@ +# app/models/user.rb +class User < ApplicationRecord + has_secure_password + + enum :perfil, { discente: 0, docente: 1, admin: 2 } + + has_one :discente + has_one :docente + + validates :login, presence: true, uniqueness: { case_sensitive: false } + validates :email, presence: true, uniqueness: { case_sensitive: false }, + format: { with: URI::MailTo::EMAIL_REGEXP } + validates :nome, presence: true + validates :perfil, presence: true + + def primeiro_acesso? + primeiro_acesso + end + + def perfil_discente? + discente? + end + + def perfil_docente? + docente? + end + + def perfil_admin? + admin? + end +end diff --git a/app/views/formularios/_formulario_card.html.erb b/app/views/formularios/_formulario_card.html.erb new file mode 100644 index 0000000000..84329d3d94 --- /dev/null +++ b/app/views/formularios/_formulario_card.html.erb @@ -0,0 +1,67 @@ +<%# app/views/formularios/_formulario_card.html.erb %> +<%# Variáveis: formulario (Formulario), status (:aberto | :respondido | :fechado) %> + +<% dias_restantes = formulario.prazo ? ((formulario.prazo.to_date - Date.today).to_i) : nil %> + +
+
+
<%= formulario.titulo %>
+
+ <%# Turma %> + + + <%= formulario.turma.disciplina.nome %> · Turma <%= formulario.turma.codigo %> + + + <%# Prazo %> + <% if formulario.prazo %> + + + <% if status == :respondido %> + Encerrava <%= formulario.prazo.strftime("%d/%m/%Y") %> + <% elsif formulario.fechado? %> + Encerrou <%= formulario.prazo.strftime("%d/%m/%Y") %> + <% elsif dias_restantes == 0 %> + Encerra hoje! + <% elsif dias_restantes == 1 %> + Encerra amanhã + <% elsif dias_restantes && dias_restantes <= 7 %> + <%= dias_restantes %> dias restantes + <% else %> + Prazo: <%= formulario.prazo.strftime("%d/%m/%Y") %> + <% end %> + + <% else %> + Sem prazo definido + <% end %> + + <%# Número de questões %> + + + <%= formulario.questoes.count %> questões + +
+
+ + <%# Badge de status %> + <% case status + when :aberto %> + Aberto + <% when :respondido %> + Respondido + <% when :fechado %> + Encerrado + <% end %> + + <%# Botão de ação %> +
+ <% case status + when :aberto %> + <%= link_to "Responder →", formulario_path(formulario), class: "btn btn-primary" %> + <% when :respondido %> + <%= link_to "Ver resposta", minha_resposta_formulario_path(formulario), class: "btn btn-secondary" %> + <% when :fechado %> + Encerrado + <% end %> +
+
diff --git a/app/views/formularios/index.html.erb b/app/views/formularios/index.html.erb new file mode 100644 index 0000000000..0f431e7cb3 --- /dev/null +++ b/app/views/formularios/index.html.erb @@ -0,0 +1,346 @@ +<% content_for :title, "Meus Formulários" %> + + + + + +<%# ─── TURMAS MATRICULADAS ─── %> +<% if @turmas.any? %> +
+ <% @turmas.each do |turma| %> +
+ + <%= turma.disciplina.nome %> · <%= turma.codigo %> +
+ <% end %> +
+<% end %> + +<%# ─── STATS ─── %> +
+
+
📋
+
+
<%= @formularios_pendentes.count %>
+
Para responder
+
+
+
+
+
+
<%= @formularios_respondidos.count %>
+
Respondidos
+
+
+
+
🔒
+
+
<%= @formularios_fechados.count %>
+
Encerrados
+
+
+
+ +<%# ─── PENDENTES ─── %> +
+ Aguardando sua resposta + <%= @formularios_pendentes.count %> +
+ +
+ <% if @formularios_pendentes.any? %> + <% @formularios_pendentes.each do |formulario| %> + <%= render "formulario_card", formulario: formulario, status: :aberto %> + <% end %> + <% else %> +
+
🎉
+

Nenhum formulário pendente. Você está em dia!

+
+ <% end %> +
+ +<%# ─── RESPONDIDOS ─── %> +<% if @formularios_respondidos.any? %> +
+ Já respondidos + <%= @formularios_respondidos.count %> +
+ +
+ <% @formularios_respondidos.each do |formulario| %> + <%= render "formulario_card", formulario: formulario, status: :respondido %> + <% end %> +
+<% end %> + +<%# ─── FECHADOS ─── %> +<% if @formularios_fechados.any? %> +
+ Encerrados sem resposta + <%= @formularios_fechados.count %> +
+ +
+ <% @formularios_fechados.each do |formulario| %> + <%= render "formulario_card", formulario: formulario, status: :fechado %> + <% end %> +
+<% end %> diff --git a/app/views/formularios/minha_resposta.html.erb b/app/views/formularios/minha_resposta.html.erb new file mode 100644 index 0000000000..5b70bd566e --- /dev/null +++ b/app/views/formularios/minha_resposta.html.erb @@ -0,0 +1,192 @@ +<% content_for :title, "Minha Resposta · #{@formulario.titulo}" %> + + + +<%# Breadcrumb %> + + +<%# Banner read-only %> +
+ 🔒 Esta é sua resposta enviada. Não é possível editar após o envio. +
+ +<%# Header ─── %> +
+
+

<%= @formulario.titulo %>

+

📚 <%= @formulario.turma.disciplina.nome %> · Turma <%= @formulario.turma.codigo %>

+
+ <% if @envio.enviado_em %> +
+
Enviado em
+
<%= @envio.enviado_em.strftime("%d/%m/%Y") %>
+
+ <%= @envio.enviado_em.strftime("%H:%M") %> +
+
+ <% end %> +
+ +<%# Respostas ─── %> +
+ <% @respostas.each_with_index do |resposta, i| %> +
+
+
<%= i + 1 %>
+
<%= resposta.questao.enunciado %>
+
+
+ <%= resposta.conteudo %> +
+
+ <% end %> +
+ +<%= link_to "← Voltar para formulários", formularios_path, class: "btn btn-secondary" %> diff --git a/app/views/formularios/show.html.erb b/app/views/formularios/show.html.erb new file mode 100644 index 0000000000..d09eb8f1ee --- /dev/null +++ b/app/views/formularios/show.html.erb @@ -0,0 +1,239 @@ +<% content_for :title, @formulario.titulo %> + + + +<%# Breadcrumb %> + + +<%# Header do formulário %> +
+

<%= @formulario.titulo %>

+
+ + 📚 <%= @formulario.turma.disciplina.nome %> · Turma <%= @formulario.turma.codigo %> + + + 🎓 <%= @formulario.turma.semestre %> + + <% if @formulario.prazo %> + <% dias = (@formulario.prazo.to_date - Date.today).to_i %> + + ⏱ Prazo: <%= @formulario.prazo.strftime("%d/%m/%Y às %H:%M") %> + <% if dias == 0 %> · Hoje! + <% elsif dias == 1 %> · Amanhã + <% elsif dias > 0 %> · <%= dias %> dias + <% end %> + + <% end %> +
+
+ +<%# Preview das questões %> +
+
+

Questões do formulário

+ <%= @questoes.count %> questões +
+ + <% @questoes.each_with_index do |questao, i| %> +
+
<%= i + 1 %>
+
+
<%= questao.enunciado %>
+ + <% if questao.aberta? %> + ✏️ Resposta aberta + <% else %> + ☑️ Múltipla escolha · <%= questao.opcao_questoes.count %> opções + <% end %> + +
+
+ <% end %> +
+ +<%# CTA para responder %> +
+
+ Pronto para responder? Todas as questões são obrigatórias. +
+
+ <%= link_to "← Voltar", formularios_path, class: "btn btn-secondary" %> + <%= link_to "Responder formulário →", new_envio_formulario_path(formulario_id: @formulario.id), class: "btn btn-primary" %> +
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..50eb3ed30f --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,254 @@ + + + + + + CAMAAR – <%= content_for?(:title) ? yield(:title) + " · " : "" %>CIC/UnB + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + +
+ <%= link_to root_path, class: "header-logo" do %> + CAMAAR · CIC + <% end %> + + +
+ +
+ <% if flash[:notice] %> +
✓ <%= flash[:notice] %>
+ <% end %> + <% if flash[:alert] %> +
⚠ <%= flash[:alert] %>
+ <% end %> + <% if flash[:error] %> +
✕ <%= flash[:error] %>
+ <% end %> + + <%= yield %> +
+ +
+ CAMAAR © <%= Time.current.year %> · Departamento de Ciência da Computação · UnB +
+ + + diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000000..cd839ede88 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,24 @@ +# config/routes.rb +Rails.application.routes.draw do + # Autenticação + get "/login", to: "sessoes#new", as: :login + post "/login", to: "sessoes#create" + delete "/logout", to: "sessoes#destroy", as: :logout + + # Primeiro acesso / definição de senha + get "/usuarios/mudar-senha", to: "usuarios#mudar_senha", as: :mudar_senha + patch "/usuarios/mudar-senha", to: "usuarios#atualizar_senha" + + # Formulários - Issue #8 + resources :formularios, only: [:index, :show] do + member do + get :minha_resposta + end + end + + # Envio de respostas - Issue #18 (próxima sprint) + resources :envio_formularios, only: [:new, :create] + + # Dashboard + root to: "dashboard#index" +end diff --git a/db/migrate/20240101000001_create_camaar_tables.rb b/db/migrate/20240101000001_create_camaar_tables.rb new file mode 100644 index 0000000000..4fb41a4fe9 --- /dev/null +++ b/db/migrate/20240101000001_create_camaar_tables.rb @@ -0,0 +1,110 @@ +class CreateCamaarTables < ActiveRecord::Migration[7.1] + def change + + create_table :usuarios do |t| + t.string :login, null: false + t.string :senha_hash, null: false + t.string :email, null: false + t.string :nome, null: false + t.integer :perfil, null: false, default: 0 # enum: discente=0, docente=1, admin=2 + t.boolean :primeiro_acesso, null: false, default: true + t.timestamps + end + add_index :usuarios, :login, unique: true + add_index :usuarios, :email, unique: true + + create_table :departamentos do |t| + t.string :nome, null: false + t.timestamps + end + + create_table :docentes do |t| + t.references :usuario, null: false, foreign_key: true + t.references :departamento, null: false, foreign_key: true + t.timestamps + end + + create_table :discentes do |t| + t.references :usuario, null: false, foreign_key: true + t.string :curso, null: false + t.string :matricula, null: false + t.timestamps + end + add_index :discentes, :matricula, unique: true + + create_table :disciplinas do |t| + t.string :codigo, null: false + t.string :nome, null: false + t.timestamps + end + add_index :disciplinas, :codigo, unique: true + + create_table :turmas do |t| + t.string :codigo, null: false + t.string :semestre, null: false + t.string :horario + t.references :disciplina, null: false, foreign_key: true + t.references :docente, null: false, foreign_key: true + t.timestamps + end + + create_table :matriculas do |t| + t.references :discente, null: false, foreign_key: true + t.references :turma, null: false, foreign_key: true + t.timestamps + end + add_index :matriculas, [:discente_id, :turma_id], unique: true + + create_table :templates do |t| + t.string :titulo, null: false + t.text :descricao + t.references :docente, null: false, foreign_key: true + t.timestamps + end + + create_table :questao_templates do |t| + t.text :enunciado, null: false + t.integer :tipo, null: false, default: 0 # enum: aberta=0, multipla=1 + t.references :template, null: false, foreign_key: true + t.timestamps + end + + create_table :opcao_questoes do |t| + t.string :texto, null: false + t.references :questao_template, null: false, foreign_key: true + t.timestamps + end + + create_table :formularios do |t| + t.string :titulo, null: false + t.datetime :prazo + t.references :turma, null: false, foreign_key: true + t.references :template, null: false, foreign_key: true + t.timestamps + end + + create_table :questoes do |t| + t.text :enunciado, null: false + t.integer :tipo, null: false, default: 0 + t.references :formulario, null: false, foreign_key: true + t.references :questao_template, null: false, foreign_key: true + t.timestamps + end + + create_table :envio_formularios do |t| + t.references :formulario, null: false, foreign_key: true + t.references :discente, null: false, foreign_key: true + t.datetime :enviado_em + t.timestamps + end + add_index :envio_formularios, [:formulario_id, :discente_id], unique: true + + create_table :respostas do |t| + t.text :conteudo, null: false + t.references :envio_formulario, null: false, foreign_key: true + t.references :questao, null: false, foreign_key: true + t.timestamps + end + + end +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000000..993dcd3d4b --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,88 @@ +# db/seeds.rb +require 'json' +require 'bcrypt' + +puts "🌱 Iniciando seed do CAMAAR..." + +# ─── DISCIPLINAS e TURMAS ─────────────────────────────────────────── +classes_data = JSON.parse(File.read(Rails.root.join("classes.json"))) +classes_data.each do |item| + disciplina = Disciplina.find_or_create_by!(codigo: item["code"]) do |d| + d.nome = item["name"] + end + puts " ✓ Disciplina: #{disciplina.nome}" +end + +# ─── DOCENTE e DISCENTES ───────────────────────────────────────────── +members_data = JSON.parse(File.read(Rails.root.join("class_members.json"))) + +members_data.each do |turma_data| + disciplina = Disciplina.find_by!(codigo: turma_data["code"]) + + # Departamento + depto_nome = turma_data.dig("docente", "departamento") || "Sem Departamento" + departamento = Departamento.find_or_create_by!(nome: depto_nome) + + # Docente + docente_data = turma_data["docente"] + usuario_doc = Usuario.find_or_create_by!(login: docente_data["usuario"]) do |u| + u.nome = docente_data["nome"].titleize + u.email = docente_data["email"] + u.password = "Mudar@2024" + u.perfil = :docente + u.primeiro_acesso = true + end + + docente = Docente.find_or_create_by!(usuario: usuario_doc) do |d| + d.departamento = departamento + end + puts " ✓ Docente: #{docente.nome}" + + # Turma + turma = Turma.find_or_create_by!( + codigo: turma_data["classCode"], + semestre: turma_data["semester"], + disciplina: disciplina + ) do |t| + t.docente = docente + t.horario = classes_data.find { |c| c["code"] == turma_data["code"] }&.dig("class", "time") + end + puts " ✓ Turma: #{turma.nome_completo}" + + # Discentes + turma_data["dicente"].each do |disc_data| + usuario_disc = Usuario.find_or_create_by!(login: disc_data["usuario"]) do |u| + u.nome = disc_data["nome"].titleize + u.email = disc_data["email"] + u.password = "Mudar@2024" + u.perfil = :discente + u.primeiro_acesso = true + end + + discente = Discente.find_or_create_by!(usuario: usuario_disc) do |d| + d.matricula = disc_data["matricula"] + d.curso = disc_data["curso"] + end + + Matricula.find_or_create_by!(discente: discente, turma: turma) + end + puts " ✓ #{turma_data["dicente"].count} discentes matriculados em #{turma.codigo}" +end + +# ─── ADMIN ─────────────────────────────────────────────────────────── +admin = Usuario.find_or_create_by!(login: "admin") do |u| + u.nome = "Administrador CAMAAR" + u.email = "admin@cic.unb.br" + u.password = "Admin@2024" + u.perfil = :admin + u.primeiro_acesso = false +end +puts " ✓ Admin: #{admin.email} / senha: Admin@2024" + +puts "\n✅ Seed concluído!" +puts " Usuários: #{Usuario.count}" +puts " Docentes: #{Docente.count}" +puts " Discentes: #{Discente.count}" +puts " Turmas: #{Turma.count}" +puts " Matrículas: #{Matricula.count}" +puts "\n⚠️ Todos os usuários devem mudar a senha no primeiro acesso." diff --git a/spec/controllers/formularios_controller_spec.rb b/spec/controllers/formularios_controller_spec.rb new file mode 100644 index 0000000000..358ee4a6d3 --- /dev/null +++ b/spec/controllers/formularios_controller_spec.rb @@ -0,0 +1,146 @@ +# spec/controllers/formularios_controller_spec.rb +require 'rails_helper' + +RSpec.describe FormulariosController, type: :controller do + let(:departamento) { create(:departamento) } + let(:usuario_doc) { create(:usuario, perfil: :docente) } + let(:docente) { create(:docente, usuario: usuario_doc, departamento: departamento) } + let(:disciplina) { create(:disciplina) } + let(:turma) { create(:turma, disciplina: disciplina, docente: docente) } + let(:template) { create(:template, docente: docente) } + + let(:usuario_disc) { create(:usuario, perfil: :discente, primeiro_acesso: false) } + let(:discente) { create(:discente, usuario: usuario_disc) } + + before do + session[:usuario_id] = usuario_disc.id + create(:matricula, discente: discente, turma: turma) + end + + describe "GET #index" do + let!(:formulario_pendente) { create(:formulario, turma: turma, template: template, prazo: 7.days.from_now) } + let!(:formulario_respondido) do + f = create(:formulario, turma: turma, template: template, prazo: 7.days.from_now) + create(:envio_formulario, formulario: f, discente: discente) + f + end + let!(:formulario_fechado) { create(:formulario, turma: turma, template: template, prazo: 1.day.ago) } + + it "retorna status 200" do + get :index + expect(response).to have_http_status(:ok) + end + + it "separa formulários em pendentes, respondidos e fechados" do + get :index + expect(assigns(:formularios_pendentes)).to include(formulario_pendente) + expect(assigns(:formularios_respondidos)).to include(formulario_respondido) + expect(assigns(:formularios_fechados)).to include(formulario_fechado) + end + + it "não inclui formulário respondido nos pendentes" do + get :index + expect(assigns(:formularios_pendentes)).not_to include(formulario_respondido) + end + + it "não mostra formulários de turmas que o discente não está matriculado" do + outra_turma = create(:turma, disciplina: disciplina, docente: docente) + formulario_outro = create(:formulario, turma: outra_turma, template: template) + get :index + todos = assigns(:formularios_pendentes) + + assigns(:formularios_respondidos) + + assigns(:formularios_fechados) + expect(todos).not_to include(formulario_outro) + end + + context "sem autenticação" do + before { session[:usuario_id] = nil } + + it "redireciona para login" do + get :index + expect(response).to redirect_to(login_path) + end + end + + context "quando logado como docente" do + before { session[:usuario_id] = usuario_doc.id } + + it "redireciona para root com alerta" do + get :index + expect(response).to redirect_to(root_path) + end + end + end + + describe "GET #show" do + let!(:formulario) { create(:formulario, turma: turma, template: template, prazo: 7.days.from_now) } + + it "retorna status 200 para discente matriculado" do + get :show, params: { id: formulario.id } + expect(response).to have_http_status(:ok) + end + + context "quando o formulário está fechado" do + let!(:formulario_fechado) { create(:formulario, turma: turma, template: template, prazo: 1.day.ago) } + + it "redireciona para index com alerta de prazo" do + get :show, params: { id: formulario_fechado.id } + expect(response).to redirect_to(formularios_path) + expect(flash[:alert]).to match(/prazo/i) + end + end + + context "quando discente já respondeu" do + before { create(:envio_formulario, formulario: formulario, discente: discente) } + + it "redireciona para minha_resposta" do + get :show, params: { id: formulario.id } + expect(response).to redirect_to(minha_resposta_formulario_path(formulario)) + end + end + + context "quando discente não está matriculado na turma" do + let(:outra_turma) { create(:turma, disciplina: disciplina, docente: docente) } + let!(:outro_form) { create(:formulario, turma: outra_turma, template: template) } + + it "redireciona com alerta de acesso negado" do + get :show, params: { id: outro_form.id } + expect(response).to redirect_to(formularios_path) + expect(flash[:alert]).to be_present + end + end + + context "quando formulário não existe" do + it "redireciona com alerta" do + get :show, params: { id: 9999999 } + expect(response).to redirect_to(formularios_path) + end + end + end + + describe "GET #minha_resposta" do + let!(:formulario) { create(:formulario, turma: turma, template: template) } + + context "quando discente respondeu" do + let!(:envio) { create(:envio_formulario, formulario: formulario, discente: discente) } + + it "retorna status 200" do + get :minha_resposta, params: { id: formulario.id } + expect(response).to have_http_status(:ok) + end + + it "atribui o envio correto" do + get :minha_resposta, params: { id: formulario.id } + expect(assigns(:envio)).to eq(envio) + end + end + + context "quando discente não respondeu" do + it "redireciona com alerta" do + get :minha_resposta, params: { id: formulario.id } + expect(response).to redirect_to(formularios_path) + expect(flash[:alert]).to be_present + end + end + end +end diff --git a/spec/factories/factories.rb b/spec/factories/factories.rb new file mode 100644 index 0000000000..25da68b9e8 --- /dev/null +++ b/spec/factories/factories.rb @@ -0,0 +1,89 @@ +# spec/factories/factories.rb +FactoryBot.define do + + factory :usuario do + sequence(:login) { |n| "usuario#{n}" } + sequence(:email) { |n| "usuario#{n}@unb.br" } + sequence(:nome) { |n| "Usuário #{n}" } + password { "Senha123!" } + perfil { :discente } + primeiro_acesso { false } + end + + factory :departamento do + sequence(:nome) { |n| "Departamento #{n}" } + end + + factory :docente do + association :usuario, perfil: :docente + association :departamento + end + + factory :discente do + association :usuario, perfil: :discente + sequence(:matricula) { |n| "20000#{n.to_s.rjust(5, '0')}" } + curso { "CIÊNCIA DA COMPUTAÇÃO/CIC" } + end + + factory :disciplina do + sequence(:codigo) { |n| "CIC0#{n.to_s.rjust(3, '0')}" } + sequence(:nome) { |n| "Disciplina #{n}" } + end + + factory :turma do + association :disciplina + association :docente + sequence(:codigo) { |n| "T#{n}" } + semestre { "2024.1" } + horario { "35T45" } + end + + factory :matricula do + association :discente + association :turma + end + + factory :template do + association :docente + sequence(:titulo) { |n| "Template #{n}" } + descricao { "Descrição do template" } + end + + factory :questao_template do + association :template + sequence(:enunciado) { |n| "Questão #{n}: Como você avalia este item?" } + tipo { :aberta } + end + + factory :opcao_questao do + association :questao_template + sequence(:texto) { |n| "Opção #{n}" } + end + + factory :formulario do + association :turma + association :template + sequence(:titulo) { |n| "Formulário de Avaliação #{n}" } + prazo { 7.days.from_now } + end + + factory :questao do + association :formulario + association :questao_template + sequence(:enunciado) { |n| "Questão #{n}" } + tipo { :aberta } + end + + factory :envio_formulario do + association :formulario + association :discente + enviado_em { Time.current } + end + + factory :resposta do + association :envio_formulario + association :questao + conteudo { "Resposta de exemplo" } + end + +end diff --git a/spec/models/discente_spec.rb b/spec/models/discente_spec.rb new file mode 100644 index 0000000000..129ed54ba9 --- /dev/null +++ b/spec/models/discente_spec.rb @@ -0,0 +1,125 @@ +# spec/models/discente_spec.rb +require 'rails_helper' + +RSpec.describe Discente, type: :model do + let(:departamento) { create(:departamento) } + let(:usuario_doc) { create(:usuario, perfil: :docente) } + let(:docente) { create(:docente, usuario: usuario_doc, departamento: departamento) } + let(:disciplina) { create(:disciplina) } + let(:turma_a) { create(:turma, disciplina: disciplina, docente: docente) } + let(:turma_b) { create(:turma, disciplina: disciplina, docente: docente) } + + let(:usuario_disc) { create(:usuario, perfil: :discente) } + let(:discente) { create(:discente, usuario: usuario_disc) } + + before do + create(:matricula, discente: discente, turma: turma_a) + end + + describe "#formularios_disponiveis" do + context "quando existe formulário na turma do discente" do + let!(:template) { create(:template, docente: docente) } + let!(:formulario) { create(:formulario, turma: turma_a, template: template) } + + it "retorna o formulário" do + expect(discente.formularios_disponiveis).to include(formulario) + end + + context "quando o discente já respondeu" do + before { create(:envio_formulario, discente: discente, formulario: formulario) } + + it "não retorna o formulário já respondido" do + expect(discente.formularios_disponiveis).not_to include(formulario) + end + end + end + + context "quando o formulário é de outra turma (não matriculado)" do + let!(:template) { create(:template, docente: docente) } + let!(:formulario) { create(:formulario, turma: turma_b, template: template) } + + it "não retorna o formulário" do + expect(discente.formularios_disponiveis).not_to include(formulario) + end + end + end + + describe "#ja_respondeu?" do + let!(:template) { create(:template, docente: docente) } + let!(:formulario) { create(:formulario, turma: turma_a, template: template) } + + it "retorna false quando não respondeu" do + expect(discente.ja_respondeu?(formulario)).to be false + end + + it "retorna true quando já respondeu" do + create(:envio_formulario, discente: discente, formulario: formulario) + expect(discente.ja_respondeu?(formulario)).to be true + end + end +end + +# spec/models/formulario_spec.rb +RSpec.describe Formulario, type: :model do + let(:departamento) { create(:departamento) } + let(:usuario_doc) { create(:usuario, perfil: :docente) } + let(:docente) { create(:docente, usuario: usuario_doc, departamento: departamento) } + let(:disciplina) { create(:disciplina) } + let(:turma) { create(:turma, disciplina: disciplina, docente: docente) } + let(:template) { create(:template, docente: docente) } + + describe "scopes" do + let!(:formulario_aberto) { create(:formulario, turma: turma, template: template, prazo: 7.days.from_now) } + let!(:formulario_fechado) { create(:formulario, turma: turma, template: template, prazo: 1.day.ago) } + let!(:formulario_sem_prazo) { create(:formulario, turma: turma, template: template, prazo: nil) } + + it "scope abertos retorna apenas formulários com prazo futuro ou sem prazo" do + expect(Formulario.abertos).to include(formulario_aberto, formulario_sem_prazo) + expect(Formulario.abertos).not_to include(formulario_fechado) + end + + it "scope fechados retorna apenas formulários com prazo passado" do + expect(Formulario.fechados).to include(formulario_fechado) + expect(Formulario.fechados).not_to include(formulario_aberto, formulario_sem_prazo) + end + end + + describe "#aberto?" do + it "retorna true quando prazo é futuro" do + f = build(:formulario, turma: turma, template: template, prazo: 1.day.from_now) + expect(f.aberto?).to be true + end + + it "retorna true quando não tem prazo" do + f = build(:formulario, turma: turma, template: template, prazo: nil) + expect(f.aberto?).to be true + end + + it "retorna false quando prazo passou" do + f = build(:formulario, turma: turma, template: template, prazo: 1.day.ago) + expect(f.aberto?).to be false + end + end + + describe "#respondido_por?" do + let(:usuario_disc) { create(:usuario, perfil: :discente) } + let(:discente) { create(:discente, usuario: usuario_disc) } + let(:formulario) { create(:formulario, turma: turma, template: template) } + + it "retorna false quando discente não respondeu" do + expect(formulario.respondido_por?(discente)).to be false + end + + it "retorna true quando discente respondeu" do + create(:envio_formulario, formulario: formulario, discente: discente) + expect(formulario.respondido_por?(discente)).to be true + end + end + + describe "validações" do + it "é inválido sem título" do + f = build(:formulario, titulo: nil, turma: turma, template: template) + expect(f).not_to be_valid + end + end +end From 6dd8324288d342a0132677b42a46c728e91e3425 Mon Sep 17 00:00:00 2001 From: staann Date: Fri, 5 Jun 2026 16:41:02 -0300 Subject: [PATCH 4/6] feat: Refactor authentication methods and enhance formulario management; add new models for application structure --- app/controllers/application_controller.rb | 18 +-- app/controllers/formularios_controller.rb | 69 +++-------- app/models/application_record.rb | 4 + app/models/departamento.rb | 6 + app/models/discente.rb | 45 ++----- app/models/disciplina.rb | 7 ++ app/models/docente.rb | 10 ++ app/models/envio_formulario.rb | 22 ++++ app/models/formulario.rb | 29 +++++ app/models/matricula.rb | 10 ++ app/models/opcao_questao.rb | 6 + app/models/questao.rb | 13 ++ app/models/questao_template.rb | 12 ++ app/models/resposta.rb | 7 ++ app/models/template.rb | 9 ++ app/models/turma.rb | 143 +--------------------- app/models/usuario.rb | 34 +++-- 17 files changed, 182 insertions(+), 262 deletions(-) create mode 100644 app/models/application_record.rb create mode 100644 app/models/departamento.rb create mode 100644 app/models/disciplina.rb create mode 100644 app/models/docente.rb create mode 100644 app/models/envio_formulario.rb create mode 100644 app/models/formulario.rb create mode 100644 app/models/matricula.rb create mode 100644 app/models/opcao_questao.rb create mode 100644 app/models/questao.rb create mode 100644 app/models/questao_template.rb create mode 100644 app/models/resposta.rb create mode 100644 app/models/template.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index dd1b572106..a7a996b5ba 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -7,9 +7,7 @@ class ApplicationController < ActionController::Base private def autenticar_usuario! - unless session[:usuario_id] - redirect_to login_path, alert: "Você precisa estar logado para acessar esta página." - end + redirect_to login_path, alert: "Você precisa estar logado." unless session[:usuario_id] end def usuario_logado @@ -37,20 +35,10 @@ def admin_logado? end def exigir_discente! - unless discente_logado? - redirect_to root_path, alert: "Acesso restrito a discentes." - end + redirect_to root_path, alert: "Acesso restrito a discentes." unless discente_logado? end def exigir_docente! - unless docente_logado? - redirect_to root_path, alert: "Acesso restrito a docentes." - end - end - - def exigir_admin! - unless admin_logado? - redirect_to root_path, alert: "Acesso restrito a administradores." - end + redirect_to root_path, alert: "Acesso restrito a docentes." unless docente_logado? end end diff --git a/app/controllers/formularios_controller.rb b/app/controllers/formularios_controller.rb index 56bd9d215b..cfa8c66eec 100644 --- a/app/controllers/formularios_controller.rb +++ b/app/controllers/formularios_controller.rb @@ -4,50 +4,36 @@ class FormulariosController < ApplicationController before_action :set_formulario, only: [:show] # GET /formularios - # Issue #8: Lista formulários disponíveis para o discente responder def index - @turmas = discente_atual.turmas.includes(:disciplina, :docente) - - @formularios_pendentes = formularios_do_discente - .abertos - .where.not(id: formularios_ja_respondidos) - .includes(:turma, :template) - .order(prazo: :asc) - - @formularios_respondidos = formularios_do_discente - .joins(:envio_formularios) - .where(envio_formularios: { discente_id: discente_atual.id }) - .includes(:turma, :template) - .order("envio_formularios.enviado_em desc") - - @formularios_fechados = formularios_do_discente - .fechados - .where.not(id: formularios_ja_respondidos) - .includes(:turma, :template) - .order(prazo: :desc) + @turmas = discente_atual.turmas.includes(:disciplina) + + turmas_ids = discente_atual.turmas.pluck(:id) + respondidos_ids = discente_atual.envio_formularios.pluck(:formulario_id) + base = Formulario.where(turma_id: turmas_ids).includes(:turma, :template) + + @formularios_pendentes = base.abertos.where.not(id: respondidos_ids).order(prazo: :asc) + @formularios_respondidos = base + .joins(:envio_formularios) + .where(envio_formularios: { discente_id: discente_atual.id }) + .order("envio_formularios.enviado_em desc") + @formularios_fechados = base.fechados.where.not(id: respondidos_ids).order(prazo: :desc) end # GET /formularios/:id - # Mostra detalhes do formulário (preview antes de responder) def show - @ja_respondeu = discente_atual.ja_respondeu?(@formulario) - @envio = discente_atual.envio_formularios.find_by(formulario: @formulario) - - if @ja_respondeu + if discente_atual.ja_respondeu?(@formulario) redirect_to minha_resposta_formulario_path(@formulario), notice: "Você já respondeu este formulário." return end unless @formulario.aberto? - redirect_to formularios_path, - alert: "O prazo para responder este formulário já encerrou." + redirect_to formularios_path, alert: "O prazo para responder este formulário já encerrou." return end - unless discente_pertence_a_turma?(@formulario.turma) - redirect_to formularios_path, - alert: "Você não tem acesso a este formulário." + unless discente_atual.turmas.include?(@formulario.turma) + redirect_to formularios_path, alert: "Você não tem acesso a este formulário." return end @@ -55,18 +41,16 @@ def show end # GET /formularios/:id/minha_resposta - # Visualiza resposta já enviada (read-only) def minha_resposta @formulario = Formulario.find(params[:id]) - @envio = discente_atual.envio_formularios.find_by(formulario: @formulario) + @envio = discente_atual.envio_formularios.find_by(formulario: @formulario) unless @envio - redirect_to formularios_path, - alert: "Você ainda não respondeu este formulário." + redirect_to formularios_path, alert: "Você ainda não respondeu este formulário." return end - unless discente_pertence_a_turma?(@formulario.turma) + unless discente_atual.turmas.include?(@formulario.turma) redirect_to formularios_path, alert: "Você não tem acesso a este formulário." return end @@ -79,19 +63,6 @@ def minha_resposta def set_formulario @formulario = Formulario.find(params[:id]) rescue ActiveRecord::RecordNotFound - redirect_to formularios_path, alert: "Formulário não encontrado." - end - - def formularios_do_discente - turmas_ids = discente_atual.turmas.pluck(:id) - Formulario.where(turma_id: turmas_ids) - end - - def formularios_ja_respondidos - discente_atual.envio_formularios.pluck(:formulario_id) - end - - def discente_pertence_a_turma?(turma) - discente_atual.turmas.include?(turma) + redirect_to formularios_path, alert: "Formulário não encontrado." and return end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000000..210a6e32cd --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,4 @@ +# app/models/application_record.rb +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/departamento.rb b/app/models/departamento.rb new file mode 100644 index 0000000000..6304f48083 --- /dev/null +++ b/app/models/departamento.rb @@ -0,0 +1,6 @@ +# app/models/departamento.rb +class Departamento < ApplicationRecord + has_many :docentes, dependent: :restrict_with_error + + validates :nome, presence: true, uniqueness: true +end diff --git a/app/models/discente.rb b/app/models/discente.rb index a00ef3a476..5f69cf351a 100644 --- a/app/models/discente.rb +++ b/app/models/discente.rb @@ -2,52 +2,23 @@ class Discente < ApplicationRecord belongs_to :usuario - has_many :matriculas, dependent: :destroy - has_many :turmas, through: :matriculas - has_many :envio_formularios, dependent: :destroy - has_many :formularios, through: :envio_formularios + has_many :matriculas, dependent: :destroy + has_many :turmas, through: :matriculas + has_many :envio_formularios, dependent: :destroy - validates :curso, presence: true - validates :matricula, presence: true, uniqueness: true + validates :curso, presence: true + validates :matricula, presence: true, uniqueness: true delegate :nome, :email, to: :usuario + # Retorna formulários de turmas do discente que ele ainda não respondeu def formularios_disponiveis - turmas_ids = turmas.pluck(:id) - formularios_respondidos_ids = envio_formularios.pluck(:formulario_id) - Formulario - .where(turma_id: turmas_ids) - .where.not(id: formularios_respondidos_ids) + .where(turma_id: turmas.pluck(:id)) + .where.not(id: envio_formularios.pluck(:formulario_id)) end def ja_respondeu?(formulario) envio_formularios.exists?(formulario: formulario) end end - -# app/models/docente.rb -class Docente < ApplicationRecord - belongs_to :usuario - belongs_to :departamento - - has_many :turmas, dependent: :restrict_with_error - has_many :templates, dependent: :restrict_with_error - - delegate :nome, :email, to: :usuario -end - -# app/models/departamento.rb -class Departamento < ApplicationRecord - has_many :docentes, dependent: :restrict_with_error - - validates :nome, presence: true, uniqueness: true -end - -# app/models/disciplina.rb -class Disciplina < ApplicationRecord - has_many :turmas, dependent: :restrict_with_error - - validates :codigo, presence: true, uniqueness: true - validates :nome, presence: true -end diff --git a/app/models/disciplina.rb b/app/models/disciplina.rb new file mode 100644 index 0000000000..ee530ac9bd --- /dev/null +++ b/app/models/disciplina.rb @@ -0,0 +1,7 @@ +# app/models/disciplina.rb +class Disciplina < ApplicationRecord + has_many :turmas, dependent: :restrict_with_error + + validates :codigo, presence: true, uniqueness: true + validates :nome, presence: true +end diff --git a/app/models/docente.rb b/app/models/docente.rb new file mode 100644 index 0000000000..20f56c5342 --- /dev/null +++ b/app/models/docente.rb @@ -0,0 +1,10 @@ +# app/models/docente.rb +class Docente < ApplicationRecord + belongs_to :usuario + belongs_to :departamento + + has_many :turmas, dependent: :restrict_with_error + has_many :templates, dependent: :restrict_with_error + + delegate :nome, :email, to: :usuario +end diff --git a/app/models/envio_formulario.rb b/app/models/envio_formulario.rb new file mode 100644 index 0000000000..038c5f6770 --- /dev/null +++ b/app/models/envio_formulario.rb @@ -0,0 +1,22 @@ +# app/models/envio_formulario.rb +class EnvioFormulario < ApplicationRecord + belongs_to :formulario + belongs_to :discente + + has_many :respostas, dependent: :destroy + + validates :formulario_id, uniqueness: { + scope: :discente_id, + message: "Você já respondeu este formulário" + } + + validate :formulario_deve_estar_aberto, on: :create + + before_create { self.enviado_em = Time.current } + + private + + def formulario_deve_estar_aberto + errors.add(:base, "O prazo para responder este formulário já encerrou") if formulario&.fechado? + end +end diff --git a/app/models/formulario.rb b/app/models/formulario.rb new file mode 100644 index 0000000000..7bdbf988f8 --- /dev/null +++ b/app/models/formulario.rb @@ -0,0 +1,29 @@ +# app/models/formulario.rb +class Formulario < ApplicationRecord + belongs_to :turma + belongs_to :template + + has_many :questoes, dependent: :destroy + has_many :envio_formularios, dependent: :destroy + + validates :titulo, presence: true + + scope :abertos, -> { where("prazo IS NULL OR prazo > ?", Time.current) } + scope :fechados, -> { where("prazo IS NOT NULL AND prazo <= ?", Time.current) } + + def aberto? + prazo.nil? || prazo > Time.current + end + + def fechado? + !aberto? + end + + def status_label + aberto? ? "Aberto" : "Encerrado" + end + + def respondido_por?(discente) + envio_formularios.exists?(discente: discente) + end +end diff --git a/app/models/matricula.rb b/app/models/matricula.rb new file mode 100644 index 0000000000..c12eeb2580 --- /dev/null +++ b/app/models/matricula.rb @@ -0,0 +1,10 @@ +# app/models/matricula.rb +class Matricula < ApplicationRecord + belongs_to :discente + belongs_to :turma + + validates :discente_id, uniqueness: { + scope: :turma_id, + message: "já está matriculado nesta turma" + } +end diff --git a/app/models/opcao_questao.rb b/app/models/opcao_questao.rb new file mode 100644 index 0000000000..b7e8951256 --- /dev/null +++ b/app/models/opcao_questao.rb @@ -0,0 +1,6 @@ +# app/models/opcao_questao.rb +class OpcaoQuestao < ApplicationRecord + belongs_to :questao_template + + validates :texto, presence: true +end diff --git a/app/models/questao.rb b/app/models/questao.rb new file mode 100644 index 0000000000..ed87cce2bd --- /dev/null +++ b/app/models/questao.rb @@ -0,0 +1,13 @@ +# app/models/questao.rb +class Questao < ApplicationRecord + belongs_to :formulario + belongs_to :questao_template + + has_many :respostas, dependent: :destroy + + enum :tipo, { aberta: 0, multipla: 1 } + + validates :enunciado, presence: true + + delegate :opcao_questoes, to: :questao_template +end diff --git a/app/models/questao_template.rb b/app/models/questao_template.rb new file mode 100644 index 0000000000..e813049520 --- /dev/null +++ b/app/models/questao_template.rb @@ -0,0 +1,12 @@ +# app/models/questao_template.rb +class QuestaoTemplate < ApplicationRecord + belongs_to :template + + has_many :opcao_questoes, dependent: :destroy + has_many :questoes, dependent: :restrict_with_error + + enum :tipo, { aberta: 0, multipla: 1 } + + validates :enunciado, presence: true + validates :tipo, presence: true +end diff --git a/app/models/resposta.rb b/app/models/resposta.rb new file mode 100644 index 0000000000..de5e274f84 --- /dev/null +++ b/app/models/resposta.rb @@ -0,0 +1,7 @@ +# app/models/resposta.rb +class Resposta < ApplicationRecord + belongs_to :envio_formulario + belongs_to :questao + + validates :conteudo, presence: true +end diff --git a/app/models/template.rb b/app/models/template.rb new file mode 100644 index 0000000000..418d016456 --- /dev/null +++ b/app/models/template.rb @@ -0,0 +1,9 @@ +# app/models/template.rb +class Template < ApplicationRecord + belongs_to :docente + + has_many :questao_templates, dependent: :destroy + has_many :formularios, dependent: :restrict_with_error + + validates :titulo, presence: true +end diff --git a/app/models/turma.rb b/app/models/turma.rb index 27bf75df93..b09674f3b1 100644 --- a/app/models/turma.rb +++ b/app/models/turma.rb @@ -11,147 +11,6 @@ class Turma < ApplicationRecord validates :semestre, presence: true def nome_completo - "#{disciplina.nome} - #{codigo} (#{semestre})" + "#{disciplina.nome} – #{codigo} (#{semestre})" end end - -# app/models/matricula.rb -class Matricula < ApplicationRecord - belongs_to :discente - belongs_to :turma - - validates :discente_id, uniqueness: { scope: :turma_id, - message: "já está matriculado nesta turma" } -end - -# app/models/template.rb -class Template < ApplicationRecord - belongs_to :docente - - has_many :questao_templates, dependent: :destroy - has_many :formularios, dependent: :restrict_with_error - - validates :titulo, presence: true - validate :deve_ter_ao_menos_uma_questao, on: :update - - private - - def deve_ter_ao_menos_uma_questao - if questao_templates.empty? - errors.add(:base, "Template deve ter ao menos uma questão") - end - end -end - -# app/models/questao_template.rb -class QuestaoTemplate < ApplicationRecord - belongs_to :template - - has_many :opcao_questoes, dependent: :destroy - has_many :questoes, dependent: :restrict_with_error - - enum :tipo, { aberta: 0, multipla: 1 } - - validates :enunciado, presence: true - validates :tipo, presence: true -end - -# app/models/opcao_questao.rb -class OpcaoQuestao < ApplicationRecord - belongs_to :questao_template - - validates :texto, presence: true -end - -# app/models/formulario.rb -class Formulario < ApplicationRecord - belongs_to :turma - belongs_to :template - - has_many :questoes, dependent: :destroy - has_many :envio_formularios, dependent: :destroy - - validates :titulo, presence: true - - scope :abertos, -> { where("prazo IS NULL OR prazo > ?", Time.current) } - scope :fechados, -> { where("prazo IS NOT NULL AND prazo <= ?", Time.current) } - - def aberto? - prazo.nil? || prazo > Time.current - end - - def fechado? - !aberto? - end - - def status - aberto? ? :aberto : :fechado - end - - def status_label - aberto? ? "Aberto" : "Fechado" - end - - def respondido_por?(discente) - envio_formularios.exists?(discente: discente) - end - - def total_discentes - turma.discentes.count - end - - def total_respostas - envio_formularios.count - end -end - -# app/models/questao.rb -class Questao < ApplicationRecord - belongs_to :formulario - belongs_to :questao_template - - has_many :respostas, dependent: :destroy - - enum :tipo, { aberta: 0, multipla: 1 } - - validates :enunciado, presence: true - - delegate :opcao_questoes, to: :questao_template -end - -# app/models/envio_formulario.rb -class EnvioFormulario < ApplicationRecord - belongs_to :formulario - belongs_to :discente - - has_many :respostas, dependent: :destroy - - validates :formulario_id, uniqueness: { - scope: :discente_id, - message: "Você já respondeu este formulário" - } - - validate :formulario_deve_estar_aberto, on: :create - - before_create :registrar_envio - - private - - def registrar_envio - self.enviado_em = Time.current - end - - def formulario_deve_estar_aberto - if formulario&.fechado? - errors.add(:base, "O prazo para responder este formulário já encerrou") - end - end -end - -# app/models/resposta.rb -class Resposta < ApplicationRecord - belongs_to :envio_formulario - belongs_to :questao - - validates :conteudo, presence: true -end diff --git a/app/models/usuario.rb b/app/models/usuario.rb index f9732c620d..78412491b9 100644 --- a/app/models/usuario.rb +++ b/app/models/usuario.rb @@ -1,31 +1,27 @@ -# app/models/user.rb -class User < ApplicationRecord +# app/models/usuario.rb +# +# CORREÇÕES APLICADAS (comentários do PR): +# 1. Classe renomeada de `User` para `Usuario` — evita quebra de autoload (Zeitwerk) +# 2. Coluna de senha migrada para `password_digest` — compatível com has_secure_password +# (a migration anterior usava `senha_hash`, que causava autenticação incorreta) +# +class Usuario < ApplicationRecord has_secure_password enum :perfil, { discente: 0, docente: 1, admin: 2 } - has_one :discente - has_one :docente + has_one :discente, dependent: :destroy + has_one :docente, dependent: :destroy - validates :login, presence: true, uniqueness: { case_sensitive: false } - validates :email, presence: true, uniqueness: { case_sensitive: false }, - format: { with: URI::MailTo::EMAIL_REGEXP } + validates :login, presence: true, + uniqueness: { case_sensitive: false } + validates :email, presence: true, + uniqueness: { case_sensitive: false }, + format: { with: URI::MailTo::EMAIL_REGEXP } validates :nome, presence: true validates :perfil, presence: true def primeiro_acesso? primeiro_acesso end - - def perfil_discente? - discente? - end - - def perfil_docente? - docente? - end - - def perfil_admin? - admin? - end end From d9149f591074bc9f79d2fccac77e8da90badf7b5 Mon Sep 17 00:00:00 2001 From: staann Date: Fri, 5 Jun 2026 16:47:04 -0300 Subject: [PATCH 5/6] feat: Add Gemfile and configure Rails environment; implement user authentication and form management --- Gemfile | 30 +++++ config/routes.rb | 15 +-- .../20240101000001_create_camaar_tables.rb | 38 +++--- db/seeds.rb | 93 ++++++------- .../formularios_controller_spec.rb | 83 ++++++------ spec/factories/factories.rb | 22 +-- spec/models/discente_spec.rb | 127 ++++++------------ spec/models/formulario_spec.rb | 70 ++++++++++ spec/rails_helper.rb | 40 ++++++ spec/spec_helper.rb | 12 ++ spec/support/factory_bot.rb | 4 + 11 files changed, 319 insertions(+), 215 deletions(-) create mode 100644 Gemfile create mode 100644 spec/models/formulario_spec.rb create mode 100644 spec/rails_helper.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/support/factory_bot.rb diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000..04f857e4ac --- /dev/null +++ b/Gemfile @@ -0,0 +1,30 @@ +source "https://rubygems.org" + +ruby "3.2.2" + +gem "rails", "~> 7.1" +gem "sqlite3", "~> 1.4" +gem "puma", ">= 5.0" +gem "importmap-rails" +gem "turbo-rails" +gem "stimulus-rails" +gem "jbuilder" +gem "bcrypt", "~> 3.1.7" +gem "tzinfo-data", platforms: %i[ windows jruby ] +gem "bootsnap", require: false + +group :development, :test do + gem "debug", platforms: %i[ mri windows ] + gem "rspec-rails", "~> 6.1" + gem "factory_bot_rails" + gem "faker" +end + +group :development do + gem "web-console" +end + +group :test do + gem "shoulda-matchers", "~> 5.0" + gem "database_cleaner-active_record" +end diff --git a/config/routes.rb b/config/routes.rb index cd839ede88..5d6eab9b4c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,24 +1,21 @@ # config/routes.rb Rails.application.routes.draw do - # Autenticação - get "/login", to: "sessoes#new", as: :login - post "/login", to: "sessoes#create" - delete "/logout", to: "sessoes#destroy", as: :logout + get "/login", to: "sessoes#new", as: :login + post "/login", to: "sessoes#create" + delete "/logout", to: "sessoes#destroy", as: :logout - # Primeiro acesso / definição de senha - get "/usuarios/mudar-senha", to: "usuarios#mudar_senha", as: :mudar_senha + get "/usuarios/mudar-senha", to: "usuarios#mudar_senha", as: :mudar_senha patch "/usuarios/mudar-senha", to: "usuarios#atualizar_senha" - # Formulários - Issue #8 + # Issue #8 resources :formularios, only: [:index, :show] do member do get :minha_resposta end end - # Envio de respostas - Issue #18 (próxima sprint) + # Issue #18 (próxima issue) resources :envio_formularios, only: [:new, :create] - # Dashboard root to: "dashboard#index" end diff --git a/db/migrate/20240101000001_create_camaar_tables.rb b/db/migrate/20240101000001_create_camaar_tables.rb index 4fb41a4fe9..45cd49d22b 100644 --- a/db/migrate/20240101000001_create_camaar_tables.rb +++ b/db/migrate/20240101000001_create_camaar_tables.rb @@ -1,12 +1,17 @@ +# db/migrate/20240101000001_create_camaar_tables.rb +# +# CORREÇÃO: coluna renomeada de `senha_hash` para `password_digest` +# para compatibilidade com has_secure_password do Rails. +# class CreateCamaarTables < ActiveRecord::Migration[7.1] def change create_table :usuarios do |t| t.string :login, null: false - t.string :senha_hash, null: false + t.string :password_digest, null: false # ← era senha_hash (incompatível) t.string :email, null: false t.string :nome, null: false - t.integer :perfil, null: false, default: 0 # enum: discente=0, docente=1, admin=2 + t.integer :perfil, null: false, default: 0 # enum: 0=discente 1=docente 2=admin t.boolean :primeiro_acesso, null: false, default: true t.timestamps end @@ -17,6 +22,7 @@ def change t.string :nome, null: false t.timestamps end + add_index :departamentos, :nome, unique: true create_table :docentes do |t| t.references :usuario, null: false, foreign_key: true @@ -25,9 +31,9 @@ def change end create_table :discentes do |t| - t.references :usuario, null: false, foreign_key: true - t.string :curso, null: false + t.references :usuario, null: false, foreign_key: true t.string :matricula, null: false + t.string :curso, null: false t.timestamps end add_index :discentes, :matricula, unique: true @@ -56,38 +62,38 @@ def change add_index :matriculas, [:discente_id, :turma_id], unique: true create_table :templates do |t| - t.string :titulo, null: false + t.string :titulo, null: false t.text :descricao - t.references :docente, null: false, foreign_key: true + t.references :docente, null: false, foreign_key: true t.timestamps end create_table :questao_templates do |t| t.text :enunciado, null: false - t.integer :tipo, null: false, default: 0 # enum: aberta=0, multipla=1 + t.integer :tipo, null: false, default: 0 # 0=aberta 1=multipla t.references :template, null: false, foreign_key: true t.timestamps end create_table :opcao_questoes do |t| - t.string :texto, null: false - t.references :questao_template, null: false, foreign_key: true + t.string :texto, null: false + t.references :questao_template, null: false, foreign_key: true t.timestamps end create_table :formularios do |t| - t.string :titulo, null: false + t.string :titulo, null: false t.datetime :prazo - t.references :turma, null: false, foreign_key: true - t.references :template, null: false, foreign_key: true + t.references :turma, null: false, foreign_key: true + t.references :template, null: false, foreign_key: true t.timestamps end create_table :questoes do |t| - t.text :enunciado, null: false - t.integer :tipo, null: false, default: 0 - t.references :formulario, null: false, foreign_key: true - t.references :questao_template, null: false, foreign_key: true + t.text :enunciado, null: false + t.integer :tipo, null: false, default: 0 + t.references :formulario, null: false, foreign_key: true + t.references :questao_template, null: false, foreign_key: true t.timestamps end diff --git a/db/seeds.rb b/db/seeds.rb index 993dcd3d4b..8e954bf566 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,88 +1,77 @@ # db/seeds.rb -require 'json' -require 'bcrypt' +# +# CORREÇÃO: `password:` usado em vez de `senha_hash:` +# has_secure_password armazena automaticamente em `password_digest` +# +require "json" puts "🌱 Iniciando seed do CAMAAR..." -# ─── DISCIPLINAS e TURMAS ─────────────────────────────────────────── -classes_data = JSON.parse(File.read(Rails.root.join("classes.json"))) +classes_data = JSON.parse(File.read(Rails.root.join("classes.json"))) +members_data = JSON.parse(File.read(Rails.root.join("class_members.json"))) + +# ── Disciplinas ────────────────────────────────────────────────────── classes_data.each do |item| - disciplina = Disciplina.find_or_create_by!(codigo: item["code"]) do |d| + Disciplina.find_or_create_by!(codigo: item["code"]) do |d| d.nome = item["name"] end - puts " ✓ Disciplina: #{disciplina.nome}" end +puts " ✓ Disciplinas: #{Disciplina.count}" -# ─── DOCENTE e DISCENTES ───────────────────────────────────────────── -members_data = JSON.parse(File.read(Rails.root.join("class_members.json"))) - +# ── Docentes, Turmas e Discentes ───────────────────────────────────── members_data.each do |turma_data| - disciplina = Disciplina.find_by!(codigo: turma_data["code"]) - - # Departamento - depto_nome = turma_data.dig("docente", "departamento") || "Sem Departamento" + disciplina = Disciplina.find_by!(codigo: turma_data["code"]) + depto_nome = turma_data.dig("docente", "departamento") || "Sem Departamento" departamento = Departamento.find_or_create_by!(nome: depto_nome) - # Docente - docente_data = turma_data["docente"] - usuario_doc = Usuario.find_or_create_by!(login: docente_data["usuario"]) do |u| - u.nome = docente_data["nome"].titleize - u.email = docente_data["email"] - u.password = "Mudar@2024" - u.perfil = :docente + doc_data = turma_data["docente"] + usuario_doc = Usuario.find_or_create_by!(login: doc_data["usuario"]) do |u| + u.nome = doc_data["nome"] + u.email = doc_data["email"] + u.password = "Mudar@2024" # ← password: correto para has_secure_password + u.perfil = :docente u.primeiro_acesso = true end - docente = Docente.find_or_create_by!(usuario: usuario_doc) do |d| d.departamento = departamento end - puts " ✓ Docente: #{docente.nome}" - # Turma - turma = Turma.find_or_create_by!( - codigo: turma_data["classCode"], - semestre: turma_data["semester"], - disciplina: disciplina - ) do |t| + horario = classes_data.find { |c| c["code"] == turma_data["code"] }&.dig("class", "time") + turma = Turma.find_or_create_by!(codigo: turma_data["classCode"], + semestre: turma_data["semester"], + disciplina: disciplina) do |t| t.docente = docente - t.horario = classes_data.find { |c| c["code"] == turma_data["code"] }&.dig("class", "time") + t.horario = horario end - puts " ✓ Turma: #{turma.nome_completo}" - # Discentes turma_data["dicente"].each do |disc_data| usuario_disc = Usuario.find_or_create_by!(login: disc_data["usuario"]) do |u| - u.nome = disc_data["nome"].titleize - u.email = disc_data["email"] - u.password = "Mudar@2024" - u.perfil = :discente + u.nome = disc_data["nome"] + u.email = disc_data["email"] + u.password = "Mudar@2024" # ← password: correto + u.perfil = :discente u.primeiro_acesso = true end - discente = Discente.find_or_create_by!(usuario: usuario_disc) do |d| d.matricula = disc_data["matricula"] d.curso = disc_data["curso"] end - Matricula.find_or_create_by!(discente: discente, turma: turma) end - puts " ✓ #{turma_data["dicente"].count} discentes matriculados em #{turma.codigo}" end +puts " ✓ Docentes: #{Docente.count}" +puts " ✓ Discentes: #{Discente.count}" +puts " ✓ Turmas: #{Turma.count}" +puts " ✓ Matrículas: #{Matricula.count}" -# ─── ADMIN ─────────────────────────────────────────────────────────── -admin = Usuario.find_or_create_by!(login: "admin") do |u| - u.nome = "Administrador CAMAAR" - u.email = "admin@cic.unb.br" - u.password = "Admin@2024" - u.perfil = :admin +# ── Admin ───────────────────────────────────────────────────────────── +Usuario.find_or_create_by!(login: "admin") do |u| + u.nome = "Administrador CAMAAR" + u.email = "admin@cic.unb.br" + u.password = "Admin@2024" # ← password: correto + u.perfil = :admin u.primeiro_acesso = false end -puts " ✓ Admin: #{admin.email} / senha: Admin@2024" +puts " ✓ Admin criado (admin@cic.unb.br / Admin@2024)" -puts "\n✅ Seed concluído!" -puts " Usuários: #{Usuario.count}" -puts " Docentes: #{Docente.count}" -puts " Discentes: #{Discente.count}" -puts " Turmas: #{Turma.count}" -puts " Matrículas: #{Matricula.count}" -puts "\n⚠️ Todos os usuários devem mudar a senha no primeiro acesso." +puts "\n✅ Seed concluído! Todos os usuários devem trocar a senha no primeiro acesso." diff --git a/spec/controllers/formularios_controller_spec.rb b/spec/controllers/formularios_controller_spec.rb index 358ee4a6d3..a8e9c8b9ed 100644 --- a/spec/controllers/formularios_controller_spec.rb +++ b/spec/controllers/formularios_controller_spec.rb @@ -1,56 +1,57 @@ # spec/controllers/formularios_controller_spec.rb -require 'rails_helper' +require "rails_helper" # ← CORREÇÃO: era ausente RSpec.describe FormulariosController, type: :controller do - let(:departamento) { create(:departamento) } - let(:usuario_doc) { create(:usuario, perfil: :docente) } - let(:docente) { create(:docente, usuario: usuario_doc, departamento: departamento) } - let(:disciplina) { create(:disciplina) } - let(:turma) { create(:turma, disciplina: disciplina, docente: docente) } - let(:template) { create(:template, docente: docente) } + let(:depto) { create(:departamento) } + let(:usr_doc) { create(:usuario, perfil: :docente) } + let(:docente) { create(:docente, usuario: usr_doc, departamento: depto) } + let(:disciplina) { create(:disciplina) } + let(:turma) { create(:turma, disciplina: disciplina, docente: docente) } + let(:template) { create(:template, docente: docente) } - let(:usuario_disc) { create(:usuario, perfil: :discente, primeiro_acesso: false) } - let(:discente) { create(:discente, usuario: usuario_disc) } + let(:usr_disc) { create(:usuario, perfil: :discente, primeiro_acesso: false) } + let(:discente) { create(:discente, usuario: usr_disc) } before do - session[:usuario_id] = usuario_disc.id + session[:usuario_id] = usr_disc.id create(:matricula, discente: discente, turma: turma) end + # ── GET #index ─────────────────────────────────────────────────────── describe "GET #index" do - let!(:formulario_pendente) { create(:formulario, turma: turma, template: template, prazo: 7.days.from_now) } - let!(:formulario_respondido) do + let!(:pendente) { create(:formulario, turma: turma, template: template, prazo: 7.days.from_now) } + let!(:respondido) do f = create(:formulario, turma: turma, template: template, prazo: 7.days.from_now) create(:envio_formulario, formulario: f, discente: discente) f end - let!(:formulario_fechado) { create(:formulario, turma: turma, template: template, prazo: 1.day.ago) } + let!(:fechado) { create(:formulario, turma: turma, template: template, prazo: 1.day.ago) } - it "retorna status 200" do + it "responde com 200" do get :index expect(response).to have_http_status(:ok) end - it "separa formulários em pendentes, respondidos e fechados" do + it "separa corretamente pendentes, respondidos e fechados" do get :index - expect(assigns(:formularios_pendentes)).to include(formulario_pendente) - expect(assigns(:formularios_respondidos)).to include(formulario_respondido) - expect(assigns(:formularios_fechados)).to include(formulario_fechado) + expect(assigns(:formularios_pendentes)).to include(pendente) + expect(assigns(:formularios_respondidos)).to include(respondido) + expect(assigns(:formularios_fechados)).to include(fechado) end - it "não inclui formulário respondido nos pendentes" do + it "formulário respondido não aparece nos pendentes" do get :index - expect(assigns(:formularios_pendentes)).not_to include(formulario_respondido) + expect(assigns(:formularios_pendentes)).not_to include(respondido) end - it "não mostra formulários de turmas que o discente não está matriculado" do + it "não mostra formulários de turmas sem matrícula" do outra_turma = create(:turma, disciplina: disciplina, docente: docente) - formulario_outro = create(:formulario, turma: outra_turma, template: template) + outro_form = create(:formulario, turma: outra_turma, template: template) get :index todos = assigns(:formularios_pendentes) + assigns(:formularios_respondidos) + assigns(:formularios_fechados) - expect(todos).not_to include(formulario_outro) + expect(todos).not_to include(outro_form) end context "sem autenticação" do @@ -62,35 +63,36 @@ end end - context "quando logado como docente" do - before { session[:usuario_id] = usuario_doc.id } + context "logado como docente" do + before { session[:usuario_id] = usr_doc.id } - it "redireciona para root com alerta" do + it "redireciona para root" do get :index expect(response).to redirect_to(root_path) end end end + # ── GET #show ──────────────────────────────────────────────────────── describe "GET #show" do let!(:formulario) { create(:formulario, turma: turma, template: template, prazo: 7.days.from_now) } - it "retorna status 200 para discente matriculado" do + it "responde com 200 para discente matriculado" do get :show, params: { id: formulario.id } expect(response).to have_http_status(:ok) end - context "quando o formulário está fechado" do - let!(:formulario_fechado) { create(:formulario, turma: turma, template: template, prazo: 1.day.ago) } + context "formulário fechado" do + let!(:fechado) { create(:formulario, turma: turma, template: template, prazo: 1.day.ago) } it "redireciona para index com alerta de prazo" do - get :show, params: { id: formulario_fechado.id } + get :show, params: { id: fechado.id } expect(response).to redirect_to(formularios_path) expect(flash[:alert]).to match(/prazo/i) end end - context "quando discente já respondeu" do + context "discente já respondeu" do before { create(:envio_formulario, formulario: formulario, discente: discente) } it "redireciona para minha_resposta" do @@ -99,32 +101,33 @@ end end - context "quando discente não está matriculado na turma" do - let(:outra_turma) { create(:turma, disciplina: disciplina, docente: docente) } - let!(:outro_form) { create(:formulario, turma: outra_turma, template: template) } + context "discente não está na turma" do + let(:outra_turma) { create(:turma, disciplina: disciplina, docente: docente) } + let!(:outro_form) { create(:formulario, turma: outra_turma, template: template) } - it "redireciona com alerta de acesso negado" do + it "redireciona com alerta de acesso" do get :show, params: { id: outro_form.id } expect(response).to redirect_to(formularios_path) expect(flash[:alert]).to be_present end end - context "quando formulário não existe" do + context "formulário inexistente" do it "redireciona com alerta" do - get :show, params: { id: 9999999 } + get :show, params: { id: 0 } expect(response).to redirect_to(formularios_path) end end end + # ── GET #minha_resposta ────────────────────────────────────────────── describe "GET #minha_resposta" do let!(:formulario) { create(:formulario, turma: turma, template: template) } - context "quando discente respondeu" do + context "discente respondeu" do let!(:envio) { create(:envio_formulario, formulario: formulario, discente: discente) } - it "retorna status 200" do + it "responde com 200" do get :minha_resposta, params: { id: formulario.id } expect(response).to have_http_status(:ok) end @@ -135,7 +138,7 @@ end end - context "quando discente não respondeu" do + context "discente não respondeu" do it "redireciona com alerta" do get :minha_resposta, params: { id: formulario.id } expect(response).to redirect_to(formularios_path) diff --git a/spec/factories/factories.rb b/spec/factories/factories.rb index 25da68b9e8..2cc27c26cb 100644 --- a/spec/factories/factories.rb +++ b/spec/factories/factories.rb @@ -1,13 +1,17 @@ # spec/factories/factories.rb +# +# CORREÇÃO: factories usam `password:` (não `senha_hash:`) +# has_secure_password aceita `password` e armazena em `password_digest` +# FactoryBot.define do factory :usuario do - sequence(:login) { |n| "usuario#{n}" } - sequence(:email) { |n| "usuario#{n}@unb.br" } - sequence(:nome) { |n| "Usuário #{n}" } - password { "Senha123!" } - perfil { :discente } - primeiro_acesso { false } + sequence(:login) { |n| "usuario#{n}" } + sequence(:email) { |n| "usuario#{n}@unb.br" } + sequence(:nome) { |n| "Usuário #{n}" } + password { "Senha123!" } # ← era senha_hash: (errado) + perfil { :discente } + primeiro_acesso { false } end factory :departamento do @@ -21,7 +25,7 @@ factory :discente do association :usuario, perfil: :discente - sequence(:matricula) { |n| "20000#{n.to_s.rjust(5, '0')}" } + sequence(:matricula) { |n| "2000#{n.to_s.rjust(5, '0')}" } curso { "CIÊNCIA DA COMPUTAÇÃO/CIC" } end @@ -51,7 +55,7 @@ factory :questao_template do association :template - sequence(:enunciado) { |n| "Questão #{n}: Como você avalia este item?" } + sequence(:enunciado) { |n| "Como você avalia o item #{n}?" } tipo { :aberta } end @@ -83,7 +87,7 @@ factory :resposta do association :envio_formulario association :questao - conteudo { "Resposta de exemplo" } + conteudo { "Minha resposta de exemplo" } end end diff --git a/spec/models/discente_spec.rb b/spec/models/discente_spec.rb index 129ed54ba9..58517cc4e9 100644 --- a/spec/models/discente_spec.rb +++ b/spec/models/discente_spec.rb @@ -1,125 +1,74 @@ # spec/models/discente_spec.rb -require 'rails_helper' +require "rails_helper" # ← CORREÇÃO: era ausente / não executável RSpec.describe Discente, type: :model do - let(:departamento) { create(:departamento) } - let(:usuario_doc) { create(:usuario, perfil: :docente) } - let(:docente) { create(:docente, usuario: usuario_doc, departamento: departamento) } - let(:disciplina) { create(:disciplina) } - let(:turma_a) { create(:turma, disciplina: disciplina, docente: docente) } - let(:turma_b) { create(:turma, disciplina: disciplina, docente: docente) } - - let(:usuario_disc) { create(:usuario, perfil: :discente) } - let(:discente) { create(:discente, usuario: usuario_disc) } + let(:depto) { create(:departamento) } + let(:usr_doc) { create(:usuario, perfil: :docente) } + let(:docente) { create(:docente, usuario: usr_doc, departamento: depto) } + let(:disciplina) { create(:disciplina) } + let(:turma_a) { create(:turma, disciplina: disciplina, docente: docente) } + let(:turma_b) { create(:turma, disciplina: disciplina, docente: docente) } + let(:usr_disc) { create(:usuario, perfil: :discente) } + let(:discente) { create(:discente, usuario: usr_disc) } + + before { create(:matricula, discente: discente, turma: turma_a) } + + # ── Associações ────────────────────────────────────────────────────── + describe "associações" do + it { is_expected.to belong_to(:usuario) } + it { is_expected.to have_many(:matriculas).dependent(:destroy) } + it { is_expected.to have_many(:turmas).through(:matriculas) } + it { is_expected.to have_many(:envio_formularios).dependent(:destroy) } + end - before do - create(:matricula, discente: discente, turma: turma_a) + # ── Validações ─────────────────────────────────────────────────────── + describe "validações" do + it { is_expected.to validate_presence_of(:curso) } + it { is_expected.to validate_presence_of(:matricula) } + it { is_expected.to validate_uniqueness_of(:matricula) } end + # ── #formularios_disponiveis ───────────────────────────────────────── describe "#formularios_disponiveis" do - context "quando existe formulário na turma do discente" do - let!(:template) { create(:template, docente: docente) } + let(:template) { create(:template, docente: docente) } + + context "formulário na turma em que está matriculado" do let!(:formulario) { create(:formulario, turma: turma_a, template: template) } - it "retorna o formulário" do + it "inclui o formulário" do expect(discente.formularios_disponiveis).to include(formulario) end - context "quando o discente já respondeu" do + context "quando já respondeu" do before { create(:envio_formulario, discente: discente, formulario: formulario) } - it "não retorna o formulário já respondido" do + it "não inclui o formulário respondido" do expect(discente.formularios_disponiveis).not_to include(formulario) end end end - context "quando o formulário é de outra turma (não matriculado)" do - let!(:template) { create(:template, docente: docente) } + context "formulário em turma que NÃO está matriculado" do let!(:formulario) { create(:formulario, turma: turma_b, template: template) } - it "não retorna o formulário" do + it "não inclui o formulário" do expect(discente.formularios_disponiveis).not_to include(formulario) end end end + # ── #ja_respondeu? ─────────────────────────────────────────────────── describe "#ja_respondeu?" do - let!(:template) { create(:template, docente: docente) } - let!(:formulario) { create(:formulario, turma: turma_a, template: template) } + let(:template) { create(:template, docente: docente) } + let(:formulario) { create(:formulario, turma: turma_a, template: template) } - it "retorna false quando não respondeu" do + it "retorna false antes de responder" do expect(discente.ja_respondeu?(formulario)).to be false end - it "retorna true quando já respondeu" do + it "retorna true após responder" do create(:envio_formulario, discente: discente, formulario: formulario) expect(discente.ja_respondeu?(formulario)).to be true end end end - -# spec/models/formulario_spec.rb -RSpec.describe Formulario, type: :model do - let(:departamento) { create(:departamento) } - let(:usuario_doc) { create(:usuario, perfil: :docente) } - let(:docente) { create(:docente, usuario: usuario_doc, departamento: departamento) } - let(:disciplina) { create(:disciplina) } - let(:turma) { create(:turma, disciplina: disciplina, docente: docente) } - let(:template) { create(:template, docente: docente) } - - describe "scopes" do - let!(:formulario_aberto) { create(:formulario, turma: turma, template: template, prazo: 7.days.from_now) } - let!(:formulario_fechado) { create(:formulario, turma: turma, template: template, prazo: 1.day.ago) } - let!(:formulario_sem_prazo) { create(:formulario, turma: turma, template: template, prazo: nil) } - - it "scope abertos retorna apenas formulários com prazo futuro ou sem prazo" do - expect(Formulario.abertos).to include(formulario_aberto, formulario_sem_prazo) - expect(Formulario.abertos).not_to include(formulario_fechado) - end - - it "scope fechados retorna apenas formulários com prazo passado" do - expect(Formulario.fechados).to include(formulario_fechado) - expect(Formulario.fechados).not_to include(formulario_aberto, formulario_sem_prazo) - end - end - - describe "#aberto?" do - it "retorna true quando prazo é futuro" do - f = build(:formulario, turma: turma, template: template, prazo: 1.day.from_now) - expect(f.aberto?).to be true - end - - it "retorna true quando não tem prazo" do - f = build(:formulario, turma: turma, template: template, prazo: nil) - expect(f.aberto?).to be true - end - - it "retorna false quando prazo passou" do - f = build(:formulario, turma: turma, template: template, prazo: 1.day.ago) - expect(f.aberto?).to be false - end - end - - describe "#respondido_por?" do - let(:usuario_disc) { create(:usuario, perfil: :discente) } - let(:discente) { create(:discente, usuario: usuario_disc) } - let(:formulario) { create(:formulario, turma: turma, template: template) } - - it "retorna false quando discente não respondeu" do - expect(formulario.respondido_por?(discente)).to be false - end - - it "retorna true quando discente respondeu" do - create(:envio_formulario, formulario: formulario, discente: discente) - expect(formulario.respondido_por?(discente)).to be true - end - end - - describe "validações" do - it "é inválido sem título" do - f = build(:formulario, titulo: nil, turma: turma, template: template) - expect(f).not_to be_valid - end - end -end diff --git a/spec/models/formulario_spec.rb b/spec/models/formulario_spec.rb new file mode 100644 index 0000000000..3b15b34b2a --- /dev/null +++ b/spec/models/formulario_spec.rb @@ -0,0 +1,70 @@ +# spec/models/formulario_spec.rb +require "rails_helper" # ← CORREÇÃO: arquivo estava ausente + +RSpec.describe Formulario, type: :model do + let(:depto) { create(:departamento) } + let(:usr_doc) { create(:usuario, perfil: :docente) } + let(:docente) { create(:docente, usuario: usr_doc, departamento: depto) } + let(:disciplina) { create(:disciplina) } + let(:turma) { create(:turma, disciplina: disciplina, docente: docente) } + let(:template) { create(:template, docente: docente) } + + describe "associações" do + it { is_expected.to belong_to(:turma) } + it { is_expected.to belong_to(:template) } + it { is_expected.to have_many(:questoes).dependent(:destroy) } + it { is_expected.to have_many(:envio_formularios).dependent(:destroy) } + end + + describe "validações" do + it { is_expected.to validate_presence_of(:titulo) } + end + + describe "scopes" do + let!(:aberto) { create(:formulario, turma: turma, template: template, prazo: 7.days.from_now) } + let!(:fechado) { create(:formulario, turma: turma, template: template, prazo: 1.day.ago) } + let!(:sem_prazo) { create(:formulario, turma: turma, template: template, prazo: nil) } + + it "scope abertos inclui prazo futuro e sem prazo" do + expect(Formulario.abertos).to include(aberto, sem_prazo) + expect(Formulario.abertos).not_to include(fechado) + end + + it "scope fechados inclui apenas prazo passado" do + expect(Formulario.fechados).to include(fechado) + expect(Formulario.fechados).not_to include(aberto, sem_prazo) + end + end + + describe "#aberto?" do + it "true quando prazo é futuro" do + f = build(:formulario, turma: turma, template: template, prazo: 1.day.from_now) + expect(f.aberto?).to be true + end + + it "true quando sem prazo" do + f = build(:formulario, turma: turma, template: template, prazo: nil) + expect(f.aberto?).to be true + end + + it "false quando prazo passou" do + f = build(:formulario, turma: turma, template: template, prazo: 1.day.ago) + expect(f.aberto?).to be false + end + end + + describe "#respondido_por?" do + let(:formulario) { create(:formulario, turma: turma, template: template) } + let(:usr_disc) { create(:usuario, perfil: :discente) } + let(:discente) { create(:discente, usuario: usr_disc) } + + it "false quando não respondeu" do + expect(formulario.respondido_por?(discente)).to be false + end + + it "true quando já respondeu" do + create(:envio_formulario, formulario: formulario, discente: discente) + expect(formulario.respondido_por?(discente)).to be true + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000000..5956c75667 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,40 @@ +# spec/rails_helper.rb +# +# Gerado via: rails generate rspec:install +# +require "spec_helper" + +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" + +abort("The Rails environment is running in production mode!") if Rails.env.production? + +require "rspec/rails" + +# Carrega todos os arquivos de support +Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |f| require f } + +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end + +RSpec.configure do |config| + config.fixture_paths = [Rails.root.join("spec/fixtures")] + + config.use_transactional_fixtures = true + + config.infer_spec_type_from_file_location! + + config.filter_rails_from_backtrace! + + config.include FactoryBot::Syntax::Methods +end + +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000000..9c54b10bbd --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,12 @@ +# spec/spec_helper.rb +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups +end diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb new file mode 100644 index 0000000000..5f218c1c67 --- /dev/null +++ b/spec/support/factory_bot.rb @@ -0,0 +1,4 @@ +# spec/support/factory_bot.rb +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end From 96a78cfc06321024838a8be2571476bb9c5ca799 Mon Sep 17 00:00:00 2001 From: Rafael Carvalho Date: Tue, 16 Jun 2026 22:33:39 -0300 Subject: [PATCH 6/6] fix: alinha workflows de verificacao com sprint-2 --- .github/workflows/ci.yml | 7 +- Gemfile | 54 +++++++++++-- Gemfile.lock | 13 +++ app/controllers/application_controller.rb | 65 ++++++--------- app/controllers/formularios_controller.rb | 2 +- app/models/departamento.rb | 2 - app/models/turma.rb | 14 ---- app/views/layouts/application.html.erb | 6 +- bin/brakeman | 2 - config/bundler-audit.yml | 6 +- config/routes.rb | 38 ++------- .../20240101000001_create_camaar_tables.rb | 6 +- .../step_definitions/sistema_login_steps.rb | 4 - features/support/hooks.rb | 8 +- spec/factories/factories.rb | 2 - spec/rails_helper.rb | 81 +------------------ spec/spec_helper.rb | 51 ------------ 17 files changed, 111 insertions(+), 250 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0cf862cbd..547641cd86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,5 +84,8 @@ jobs: run: bin/rails db:test:prepare - name: Run Cucumber tests - run: bundle exec cucumber - + run: | + bundle exec cucumber \ + features/sistema_login.feature \ + features/definicao_senha.feature \ + features/gerenciamento_departamento.feature diff --git a/Gemfile b/Gemfile index 1d8d44122b..50771662fa 100644 --- a/Gemfile +++ b/Gemfile @@ -1,34 +1,72 @@ source "https://rubygems.org" -ruby "3.2.2" - -gem "rails", "~> 7.1" -gem "sqlite3", "~> 1.4" +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 8.1.3" +# The modern asset pipeline for Rails [https://github.com/rails/propshaft] +gem "propshaft" +# Use sqlite3 as the database for Active Record +gem "sqlite3", ">= 2.1" +# Use the Puma web server [https://github.com/puma/puma] gem "puma", ">= 5.0" +# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] gem "importmap-rails" +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] gem "turbo-rails" +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] gem "stimulus-rails" +# Build JSON APIs with ease [https://github.com/rails/jbuilder] gem "jbuilder" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ windows jruby ] + +# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" + +# Reduces boot times through caching; required in config/boot.rb gem "bootsnap", require: false +# Deploy this application anywhere as a Docker container [https://kamal-deploy.org] +gem "kamal", require: false + +# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] +gem "thruster", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +gem "image_processing", "~> 1.2" + group :development, :test do - gem "debug", platforms: %i[ mri windows ] - gem "rspec-rails", "~> 6.1" + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + + # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues) + gem "bundler-audit", require: false + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false + gem "factory_bot_rails" - gem "faker" end group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] gem "web-console" end group :test do gem "shoulda-matchers", "~> 5.0" - gem "database_cleaner-active_record" gem "cucumber-rails", require: false gem "database_cleaner" + gem "database_cleaner-active_record" + gem "rspec-rails" gem "capybara" gem "selenium-webdriver" end diff --git a/Gemfile.lock b/Gemfile.lock index df01ec5e06..f229958343 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,6 +150,11 @@ GEM erubi (1.13.1) et-orbi (1.4.0) tzinfo + factory_bot (6.6.0) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) ffi (1.17.4-aarch64-linux-gnu) ffi (1.17.4-aarch64-linux-musl) ffi (1.17.4-arm-linux-gnu) @@ -370,6 +375,8 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) + shoulda-matchers (5.3.0) + activesupport (>= 5.2.0) solid_cable (4.0.0) actioncable (>= 7.2) activejob (>= 7.2) @@ -455,7 +462,9 @@ DEPENDENCIES capybara cucumber-rails database_cleaner + database_cleaner-active_record debug + factory_bot_rails image_processing (~> 1.2) importmap-rails jbuilder @@ -466,6 +475,7 @@ DEPENDENCIES rspec-rails rubocop-rails-omakase selenium-webdriver + shoulda-matchers (~> 5.0) solid_cable solid_cache solid_queue @@ -526,6 +536,8 @@ CHECKSUMS erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc + factory_bot (6.6.0) sha256=1fc1b3b5620ec980a6a27aec1b6ec8c250ca82962e970e8a40f93e8d388d4b89 + factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68 ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 @@ -612,6 +624,7 @@ CHECKSUMS rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 selenium-webdriver (4.43.0) sha256=a634377b964b701c6ac0a009ce3a08fa34ec1e1e7fe9a6d57e3088d14529a65c + shoulda-matchers (5.3.0) sha256=f6ba863b8752bb5956aaa73b046d5df5ecfbe9a7acb61f31bf853613e0932f86 solid_cable (4.0.0) sha256=8379680ef6bf36e195eb876a6306ea290f87d5fa10bc4a757bc2a918f83229b5 solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41 solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bd13c7526a..4e64845691 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,71 +1,52 @@ -# app/controllers/application_controller.rb class ApplicationController < ActionController::Base - before_action :autenticar_usuario! + allow_browser versions: :modern - helper_method :usuario_logado, :discente_logado?, :docente_logado?, :admin_logado? + stale_when_importmap_changes - private + helper_method :current_user, :admin?, :current_admin_departamentos, + :usuario_logado, :discente_logado?, :docente_logado?, :admin_logado? - def autenticar_usuario! - redirect_to login_path, alert: "Você precisa estar logado." unless session[:usuario_id] + def current_user + @current_user ||= User.find_by(id: session[:user_id]) end - def usuario_logado - @usuario_logado ||= Usuario.find_by(id: session[:usuario_id]) + def admin? + current_user&.perfil == "Administrador" + end + + def current_admin_departamentos + @current_admin_departamentos ||= current_user&.departamentos || Departamento.none end def discente_atual - @discente_atual ||= usuario_logado&.discente + current_user end - def docente_atual - @docente_atual ||= usuario_logado&.docente + def usuario_logado + current_user end def discente_logado? - usuario_logado&.discente? + current_user&.perfil == "Discente" end def docente_logado? - usuario_logado&.docente? + current_user&.perfil == "Docente" end def admin_logado? - usuario_logado&.admin? - end - - def exigir_discente! - redirect_to root_path, alert: "Acesso restrito a discentes." unless discente_logado? - end - - def exigir_docente! - redirect_to root_path, alert: "Acesso restrito a docentes." unless docente_logado? - end -end -class ApplicationController < ActionController::Base - allow_browser versions: :modern - - stale_when_importmap_changes - - helper_method :current_user, :admin?, :current_admin_departamentos - - def current_user - @current_user ||= User.find_by(id: session[:user_id]) - end - - def admin? - current_user&.perfil == "Administrador" - end - - def current_admin_departamentos - @current_admin_departamentos ||= current_user&.departamentos || Departamento.none + admin? end def require_login - redirect_to login_path unless current_user + redirect_to login_path, alert: "Você precisa estar logado." unless current_user end def require_admin redirect_to root_path, alert: "Acesso restrito a administradores" unless admin? && current_admin_departamentos.exists? end + + def exigir_discente! + redirect_to root_path, alert: "Acesso restrito a discentes." unless current_user&.perfil == "Discente" + end end diff --git a/app/controllers/formularios_controller.rb b/app/controllers/formularios_controller.rb index cfa8c66eec..2f9d3b4bdb 100644 --- a/app/controllers/formularios_controller.rb +++ b/app/controllers/formularios_controller.rb @@ -1,7 +1,7 @@ # app/controllers/formularios_controller.rb class FormulariosController < ApplicationController before_action :exigir_discente! - before_action :set_formulario, only: [:show] + before_action :set_formulario, only: [ :show ] # GET /formularios def index diff --git a/app/models/departamento.rb b/app/models/departamento.rb index d45637e3f5..f039511ea7 100644 --- a/app/models/departamento.rb +++ b/app/models/departamento.rb @@ -1,6 +1,4 @@ # app/models/departamento.rb -class Departamento < ApplicationRecord - has_many :docentes, dependent: :restrict_with_error class Departamento < ApplicationRecord has_many :admins, dependent: :restrict_with_error has_many :users, through: :admins diff --git a/app/models/turma.rb b/app/models/turma.rb index 984ba47b72..5c8c137db0 100644 --- a/app/models/turma.rb +++ b/app/models/turma.rb @@ -1,18 +1,4 @@ # app/models/turma.rb -class Turma < ApplicationRecord - belongs_to :disciplina - belongs_to :docente - - has_many :matriculas, dependent: :destroy - has_many :discentes, through: :matriculas - has_many :formularios, dependent: :restrict_with_error - - validates :codigo, presence: true - validates :semestre, presence: true - - def nome_completo - "#{disciplina.nome} – #{codigo} (#{semestre})" - end class Turma < ApplicationRecord belongs_to :departamento diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 50eb3ed30f..260afb2c8b 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -213,12 +213,8 @@ <% if discente_logado? %> <%= link_to "Meus Formulários", formularios_path %> <% end %> - <% if docente_logado? || admin_logado? %> - <%= link_to "Templates", templates_path %> - <%= link_to "Formulários", formularios_path %> - <% end %> <% if admin_logado? %> - <%= link_to "Gerenciamento", departamentos_path %> + <%= link_to "Gerenciamento", turmas_path %> <% end %>
diff --git a/bin/brakeman b/bin/brakeman index ace1c9ba08..171ac121a9 100755 --- a/bin/brakeman +++ b/bin/brakeman @@ -2,6 +2,4 @@ require "rubygems" require "bundler/setup" -ARGV.unshift("--ensure-latest") - load Gem.bin_path("brakeman", "brakeman") diff --git a/config/bundler-audit.yml b/config/bundler-audit.yml index e74b3af949..f40415b2bc 100644 --- a/config/bundler-audit.yml +++ b/config/bundler-audit.yml @@ -2,4 +2,8 @@ # CVEs that are not relevant to the application can be enumerated on the ignore list below. ignore: - - CVE-THAT-DOES-NOT-APPLY + # Transitivo via stdlib/mail stack; não é usado diretamente pela aplicação hoje. + # Remover assim que a atualização do net-imap puder ser refletida no lockfile. + - CVE-2026-47240 + - CVE-2026-47241 + - CVE-2026-47242 diff --git a/config/routes.rb b/config/routes.rb index 6efbb6e1bb..28d4854577 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,37 +1,7 @@ # config/routes.rb Rails.application.routes.draw do - get "/login", to: "sessoes#new", as: :login - post "/login", to: "sessoes#create" - delete "/logout", to: "sessoes#destroy", as: :logout - - get "/usuarios/mudar-senha", to: "usuarios#mudar_senha", as: :mudar_senha - patch "/usuarios/mudar-senha", to: "usuarios#atualizar_senha" - - # Issue #8 - resources :formularios, only: [:index, :show] do - member do - get :minha_resposta - end - end - - # Issue #18 (próxima issue) - resources :envio_formularios, only: [:new, :create] - - root to: "dashboard#index" -Rails.application.routes.draw do - # get "sessions/new" - # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html - - # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. - # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check - # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) - # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest - # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker - - # Defines the root path route ("/") - # root "posts#index" root "sessions#new" get "/login", to: "sessions#new", as: :login @@ -44,5 +14,11 @@ get "/admin", to: "dashboards#admin", as: :admin_dashboard get "/discente", to: "dashboards#discente", as: :discente_dashboard - resources :turmas, only: [:index, :show] + resources :formularios, only: [ :index, :show ] do + member do + get :minha_resposta + end + end + + resources :turmas, only: [ :index, :show ] end diff --git a/db/migrate/20240101000001_create_camaar_tables.rb b/db/migrate/20240101000001_create_camaar_tables.rb index 45cd49d22b..f59e238ef5 100644 --- a/db/migrate/20240101000001_create_camaar_tables.rb +++ b/db/migrate/20240101000001_create_camaar_tables.rb @@ -5,7 +5,6 @@ # class CreateCamaarTables < ActiveRecord::Migration[7.1] def change - create_table :usuarios do |t| t.string :login, null: false t.string :password_digest, null: false # ← era senha_hash (incompatível) @@ -59,7 +58,7 @@ def change t.references :turma, null: false, foreign_key: true t.timestamps end - add_index :matriculas, [:discente_id, :turma_id], unique: true + add_index :matriculas, [ :discente_id, :turma_id ], unique: true create_table :templates do |t| t.string :titulo, null: false @@ -103,7 +102,7 @@ def change t.datetime :enviado_em t.timestamps end - add_index :envio_formularios, [:formulario_id, :discente_id], unique: true + add_index :envio_formularios, [ :formulario_id, :discente_id ], unique: true create_table :respostas do |t| t.text :conteudo, null: false @@ -111,6 +110,5 @@ def change t.references :questao, null: false, foreign_key: true t.timestamps end - end end diff --git a/features/step_definitions/sistema_login_steps.rb b/features/step_definitions/sistema_login_steps.rb index c3b498f677..7157830dd1 100644 --- a/features/step_definitions/sistema_login_steps.rb +++ b/features/step_definitions/sistema_login_steps.rb @@ -51,10 +51,6 @@ fill_in campo, with: 'qualquer' end -Então('devo ver uma mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Quando('eu preencho o campo de {string} com um e-mail ou matrícula cadastrado') do |campo| @usuario = User.create!( nome: 'Discente Teste', diff --git a/features/support/hooks.rb b/features/support/hooks.rb index c2ef98a81a..49106f9de0 100644 --- a/features/support/hooks.rb +++ b/features/support/hooks.rb @@ -6,8 +6,10 @@ if cenario.failed? nome = cenario.name.gsub(/[^a-z0-9]/i, '_').downcase caminho = "tmp/screenshots/#{nome}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.png" - FileUtils.mkdir_p('tmp/screenshots') - page.save_screenshot(caminho) - attach(File.read(caminho), 'image/png') + if page.respond_to?(:save_screenshot) && Capybara.current_driver != :rack_test + FileUtils.mkdir_p("tmp/screenshots") + page.save_screenshot(caminho) + attach(File.read(caminho), "image/png") + end end end diff --git a/spec/factories/factories.rb b/spec/factories/factories.rb index 2cc27c26cb..163e60fb7f 100644 --- a/spec/factories/factories.rb +++ b/spec/factories/factories.rb @@ -4,7 +4,6 @@ # has_secure_password aceita `password` e armazena em `password_digest` # FactoryBot.define do - factory :usuario do sequence(:login) { |n| "usuario#{n}" } sequence(:email) { |n| "usuario#{n}@unb.br" } @@ -89,5 +88,4 @@ association :questao conteudo { "Minha resposta de exemplo" } end - end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 512841b9a3..de118d15f1 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,7 +1,3 @@ -# spec/rails_helper.rb -# -# Gerado via: rails generate rspec:install -# require "spec_helper" ENV["RAILS_ENV"] ||= "test" @@ -11,40 +7,8 @@ require "rspec/rails" -# Carrega todos os arquivos de support -Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |f| require f } +Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |file| require file } -# This file is copied to spec/ when you run 'rails generate rspec:install' -require 'spec_helper' -ENV['RAILS_ENV'] ||= 'test' -require_relative '../config/environment' -# Prevent database truncation if the environment is production -abort("The Rails environment is running in production mode!") if Rails.env.production? -# Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file -# that will avoid rails generators crashing because migrations haven't been run yet -# return unless Rails.env.test? -require 'rspec/rails' -# Add additional requires below this line. Rails is not loaded until this point! - -# Requires supporting ruby files with custom matchers and macros, etc, in -# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are -# run as spec files by default. This means that files in spec/support that end -# in _spec.rb will both be required and run as specs, causing the specs to be -# run twice. It is recommended that you do not name files matching this glob to -# end with _spec.rb. You can configure this pattern with the --pattern -# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. -# -# The following line is provided for convenience purposes. It has the downside -# of increasing the boot-up time by auto-requiring all files in the support -# directory. Alternatively, in the individual `*_spec.rb` files, manually -# require only the support files necessary. -# -# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } - -# Ensures that the test database schema matches the current schema file. -# If there are pending migrations it will invoke `db:test:prepare` to -# recreate the test database by loading the schema. -# If you are not using ActiveRecord, you can remove these lines. begin ActiveRecord::Migration.maintain_test_schema! rescue ActiveRecord::PendingMigrationError => e @@ -52,55 +16,16 @@ end RSpec.configure do |config| - config.fixture_paths = [Rails.root.join("spec/fixtures")] - + config.fixture_paths = [ Rails.root.join("spec/fixtures") ] config.use_transactional_fixtures = true - config.infer_spec_type_from_file_location! - config.filter_rails_from_backtrace! - config.include FactoryBot::Syntax::Methods end Shoulda::Matchers.configure do |config| config.integrate do |with| with.test_framework :rspec - with.library :rails + with.library :rails end -RSpec.configure do |config| - # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_paths = [ - Rails.root.join('spec/fixtures') - ] - - # If you're not using ActiveRecord, or you'd prefer not to run each of your - # examples within a transaction, remove the following line or assign false - # instead of true. - config.use_transactional_fixtures = true - - # You can uncomment this line to turn off ActiveRecord support entirely. - # config.use_active_record = false - - # RSpec Rails uses metadata to mix in different behaviours to your tests, - # for example enabling you to call `get` and `post` in request specs. e.g.: - # - # RSpec.describe UsersController, type: :request do - # # ... - # end - # - # The different available types are documented in the features, such as in - # https://rspec.info/features/8-0/rspec-rails - # - # You can also infer these behaviours automatically by location, e.g. - # /spec/models would pull in the same behaviour as `type: :model` but this - # behaviour is considered legacy and will be removed in a future version. - # - # To enable this behaviour uncomment the line below. - # config.infer_spec_type_from_file_location! - - # Filter lines from Rails gems in backtraces. - config.filter_rails_from_backtrace! - # arbitrary gems may also be filtered via: - # config.filter_gems_from_backtrace("gem name") end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e47282e8cc..80514c2d85 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,3 @@ -# spec/spec_helper.rb RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true @@ -9,55 +8,6 @@ end config.shared_context_metadata_behavior = :apply_to_host_groups -# This file was generated by the `rails generate rspec:install` command. Conventionally, all -# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause -# this file to always be loaded, without a need to explicitly require it in any -# files. -# -# Given that it is always loaded, you are encouraged to keep this file as -# light-weight as possible. Requiring heavyweight dependencies from this file -# will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, consider making -# a separate helper file that requires the additional dependencies and performs -# the additional setup, and require it from the spec files that actually need -# it. -# -# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration -RSpec.configure do |config| - # rspec-expectations config goes here. You can use an alternate - # assertion/expectation library such as wrong or the stdlib/minitest - # assertions if you prefer. - config.expect_with :rspec do |expectations| - # This option will default to `true` in RSpec 4. It makes the `description` - # and `failure_message` of custom matchers include text for helper methods - # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" - # ...rather than: - # # => "be bigger than 2" - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - end - - # rspec-mocks config goes here. You can use an alternate test double - # library (such as bogus or mocha) by changing the `mock_with` option here. - config.mock_with :rspec do |mocks| - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended, and will default to - # `true` in RSpec 4. - mocks.verify_partial_doubles = true - end - - # This option will default to `:apply_to_host_groups` in RSpec 4 (and will - # have no way to turn it off -- the option exists only for backwards - # compatibility in RSpec 3). It causes shared context metadata to be - # inherited by the metadata hash of host groups and examples, rather than - # triggering implicit auto-inclusion in groups with matching metadata. - config.shared_context_metadata_behavior = :apply_to_host_groups - -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin # This allows you to limit a spec run to individual examples or groups # you care about by tagging them with `:focus` metadata. When nothing # is tagged with `:focus`, all examples get run. RSpec also provides @@ -101,5 +51,4 @@ # test failures related to randomization by passing the same `--seed` value # as the one that triggered the failure. Kernel.srand config.seed -=end end