+
+ <%# 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 %>
+
+ <% end %>
+
<%= yield %>
-
+
+
+
+
+
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 5ff4a99277..427fc122a4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,17 +1,7 @@
+# config/routes.rb
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/*.
- # 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
@@ -24,6 +14,11 @@
get "/admin", to: "dashboards#admin", as: :admin_dashboard
get "/discente", to: "dashboards#discente", as: :discente_dashboard
- resources :templates, only: [ :index ]
+ 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
new file mode 100644
index 0000000000..f59e238ef5
--- /dev/null
+++ b/db/migrate/20240101000001_create_camaar_tables.rb
@@ -0,0 +1,114 @@
+# 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 :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: 0=discente 1=docente 2=admin
+ 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
+ add_index :departamentos, :nome, unique: true
+
+ 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 :matricula, null: false
+ t.string :curso, 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 # 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.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
index 8df9d0af38..73517a88c9 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,3 +1,80 @@
+# db/seeds.rb
+#
+# CORREÇÃO: `password:` usado em vez de `senha_hash:`
+# has_secure_password armazena automaticamente em `password_digest`
+#
+require "json"
+
+puts "🌱 Iniciando seed do CAMAAR..."
+
+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.find_or_create_by!(codigo: item["code"]) do |d|
+ d.nome = item["name"]
+ end
+end
+puts " ✓ Disciplinas: #{Disciplina.count}"
+
+# ── Docentes, Turmas e Discentes ─────────────────────────────────────
+members_data.each do |turma_data|
+ 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)
+
+ 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
+
+ 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 = horario
+ end
+
+ 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"]
+ 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
+end
+puts " ✓ Docentes: #{Docente.count}"
+puts " ✓ Discentes: #{Discente.count}"
+puts " ✓ Turmas: #{Turma.count}"
+puts " ✓ Matrículas: #{Matricula.count}"
+
+# ── 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 criado (admin@cic.unb.br / Admin@2024)"
+
+puts "\n✅ Seed concluído! Todos os usuários devem trocar a senha no primeiro acesso."
# Issue #13 — Login: usuários com senha já definida
User.find_or_create_by!(email: "discente@camaar.com") do |user|
user.nome = "Discente Teste"
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/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
diff --git a/spec/controllers/formularios_controller_spec.rb b/spec/controllers/formularios_controller_spec.rb
new file mode 100644
index 0000000000..a8e9c8b9ed
--- /dev/null
+++ b/spec/controllers/formularios_controller_spec.rb
@@ -0,0 +1,149 @@
+# spec/controllers/formularios_controller_spec.rb
+require "rails_helper" # ← CORREÇÃO: era ausente
+
+RSpec.describe FormulariosController, type: :controller 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) }
+
+ let(:usr_disc) { create(:usuario, perfil: :discente, primeiro_acesso: false) }
+ let(:discente) { create(:discente, usuario: usr_disc) }
+
+ before do
+ session[:usuario_id] = usr_disc.id
+ create(:matricula, discente: discente, turma: turma)
+ end
+
+ # ── GET #index ───────────────────────────────────────────────────────
+ describe "GET #index" 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!(:fechado) { create(:formulario, turma: turma, template: template, prazo: 1.day.ago) }
+
+ it "responde com 200" do
+ get :index
+ expect(response).to have_http_status(:ok)
+ end
+
+ it "separa corretamente pendentes, respondidos e fechados" do
+ get :index
+ expect(assigns(:formularios_pendentes)).to include(pendente)
+ expect(assigns(:formularios_respondidos)).to include(respondido)
+ expect(assigns(:formularios_fechados)).to include(fechado)
+ end
+
+ it "formulário respondido não aparece nos pendentes" do
+ get :index
+ expect(assigns(:formularios_pendentes)).not_to include(respondido)
+ end
+
+ it "não mostra formulários de turmas sem matrícula" do
+ outra_turma = create(:turma, disciplina: disciplina, docente: docente)
+ 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(outro_form)
+ 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 "logado como docente" do
+ before { session[:usuario_id] = usr_doc.id }
+
+ 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 "responde com 200 para discente matriculado" do
+ get :show, params: { id: formulario.id }
+ expect(response).to have_http_status(:ok)
+ end
+
+ 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: fechado.id }
+ expect(response).to redirect_to(formularios_path)
+ expect(flash[:alert]).to match(/prazo/i)
+ end
+ end
+
+ context "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 "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" do
+ get :show, params: { id: outro_form.id }
+ expect(response).to redirect_to(formularios_path)
+ expect(flash[:alert]).to be_present
+ end
+ end
+
+ context "formulário inexistente" do
+ it "redireciona com alerta" do
+ 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 "discente respondeu" do
+ let!(:envio) { create(:envio_formulario, formulario: formulario, discente: discente) }
+
+ it "responde com 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 "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..163e60fb7f
--- /dev/null
+++ b/spec/factories/factories.rb
@@ -0,0 +1,91 @@
+# 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!" } # ← era senha_hash: (errado)
+ 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| "2000#{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| "Como você avalia o item #{n}?" }
+ 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 { "Minha 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..58517cc4e9
--- /dev/null
+++ b/spec/models/discente_spec.rb
@@ -0,0 +1,74 @@
+# spec/models/discente_spec.rb
+require "rails_helper" # ← CORREÇÃO: era ausente / não executável
+
+RSpec.describe Discente, 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_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
+
+ # ── 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
+ 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 "inclui o formulário" do
+ expect(discente.formularios_disponiveis).to include(formulario)
+ end
+
+ context "quando já respondeu" do
+ before { create(:envio_formulario, discente: discente, formulario: formulario) }
+
+ it "não inclui o formulário respondido" do
+ expect(discente.formularios_disponiveis).not_to include(formulario)
+ end
+ end
+ end
+
+ context "formulário em turma que NÃO está matriculado" do
+ let!(:formulario) { create(:formulario, turma: turma_b, template: template) }
+
+ 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) }
+
+ it "retorna false antes de responder" do
+ expect(discente.ja_respondeu?(formulario)).to be false
+ end
+
+ 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
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
index ef75d46770..de118d15f1 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -1,72 +1,31 @@
-# 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
+require "spec_helper"
+
+ENV["RAILS_ENV"] ||= "test"
+require_relative "../config/environment"
+
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 }
+require "rspec/rails"
+
+Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |file| require file }
-# 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
abort e.to_s.strip
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.
+RSpec.configure do |config|
+ config.fixture_paths = [ Rails.root.join("spec/fixtures") ]
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.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
- # arbitrary gems may also be filtered via:
- # config.filter_gems_from_backtrace("gem name")
+ 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
index 327b58ea1f..80514c2d85 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,52 +1,13 @@
-# 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
@@ -90,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
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