Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@

gem "tailwindcss-rails", "~> 4.4"

gem "rspec-rails", "~> 8.0", groups: [:development, :test]

Check failure on line 69 in Gemfile

View workflow job for this annotation

GitHub Actions / lint

Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.

Check failure on line 69 in Gemfile

View workflow job for this annotation

GitHub Actions / lint

Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.

gem "csv", "~> 3.3"

# Email preview in browser for development
gem 'letter_opener', group: :development

Check failure on line 74 in Gemfile

View workflow job for this annotation

GitHub Actions / lint

Style/StringLiterals: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

Check failure on line 75 in Gemfile

View workflow job for this annotation

GitHub Actions / lint

Layout/TrailingEmptyLines: 1 trailing blank lines detected.
9 changes: 9 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
childprocess (5.1.0)
logger (~> 1.5)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
crass (1.0.6)
Expand Down Expand Up @@ -184,6 +186,12 @@ GEM
thor (~> 1.3)
zeitwerk (>= 2.6.18, < 3.0)
language_server-protocol (3.17.0.5)
launchy (3.1.1)
addressable (~> 2.8)
childprocess (~> 5.0)
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.24.1)
Expand Down Expand Up @@ -447,6 +455,7 @@ DEPENDENCIES
importmap-rails
jbuilder
kamal
letter_opener
propshaft
puma (>= 5.0)
rails (~> 8.0.4)
Expand Down
46 changes: 44 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,44 @@
# CAMAAR
Sistema para avaliação de atividades acadêmicas remotas do CIC
# CAMAAR - Sistema de Avaliação Acadêmica

Sistema web para avaliação de disciplinas e docentes na Universidade de Brasília.

## Instalação

### Pré-requisitos
- Ruby 3.4.5
- Bundler
- Node.js

### Passos

```bash
# Clone o repositório
git clone https://github.com/seu-usuario/CAMAAR.git
cd CAMAAR

# Instale as dependências
bundle install

# Configure o banco de dados
./reset_db.sh

# Inicie o servidor
bin/dev
```

Acesse: http://localhost:3000

## Credenciais

| Usuário | Login | Senha |
|---------|-------|-------|
| Admin | admin | password |
| Aluno | aluno123 | senha123 |
| Professor | prof | senha123 |

## Testes

```bash
# Rodar testes BDD
bundle exec cucumber features/sistema_login.feature
```
1 change: 1 addition & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class ApplicationController < ActionController::Base
include Authentication
include Authenticatable
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern

Expand Down
26 changes: 18 additions & 8 deletions app/controllers/avaliacoes_controller.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
class AvaliacoesController < ApplicationController
allow_unauthenticated_access only: %i[ index create gestao_envios ]

# Requer autenticação para todas as actions

Check failure on line 3 in app/controllers/avaliacoes_controller.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/TrailingWhitespace: Trailing whitespace detected.
def index
@avaliacoes = Avaliacao.all
# Se for admin, mostrar todas as avaliações
# Se for aluno, mostrar todas as turmas matriculadas
@turmas = [] # Inicializa como array vazio por padrão

Check failure on line 8 in app/controllers/avaliacoes_controller.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/TrailingWhitespace: Trailing whitespace detected.
if current_user&.eh_admin?
@avaliacoes = Avaliacao.all
elsif current_user
# Alunos veem suas turmas matriculadas
@turmas = current_user.turmas.includes(:avaliacoes)
else
# Não logado - redireciona para login
redirect_to new_session_path
end
end

def gestao_envios
Expand Down Expand Up @@ -42,13 +54,11 @@
def resultados
@avaliacao = Avaliacao.find(params[:id])
# Pré-carrega dependências para evitar N+1.
# Nota: a associação 'respostas' existe no Modelo, mesmo que a tabela esteja pendente.
# Usamos array vazio como fallback por segurança se o BD falhar.
begin
@respostas = @avaliacao.respostas.includes(:aluno)
@submissoes = @avaliacao.submissoes.includes(:aluno, :respostas)
rescue ActiveRecord::StatementInvalid
@respostas = []
flash.now[:alert] = "A tabela de respostas ainda não está disponível."
@submissoes = []
flash.now[:alert] = "Erro ao carregar submissões."
end

respond_to do |format|
Expand Down
20 changes: 20 additions & 0 deletions app/controllers/concerns/authenticatable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# app/controllers/concerns/authenticatable.rb
module Authenticatable
extend ActiveSupport::Concern

Check failure on line 4 in app/controllers/concerns/authenticatable.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/TrailingWhitespace: Trailing whitespace detected.
included do
helper_method :current_user, :user_signed_in?
end

Check failure on line 8 in app/controllers/concerns/authenticatable.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/TrailingWhitespace: Trailing whitespace detected.
def authenticate_user!
redirect_to new_session_path, alert: "É necessário fazer login." unless user_signed_in?
end

Check failure on line 12 in app/controllers/concerns/authenticatable.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/TrailingWhitespace: Trailing whitespace detected.
def current_user
Current.session&.user
end

Check failure on line 16 in app/controllers/concerns/authenticatable.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/TrailingWhitespace: Trailing whitespace detected.
def user_signed_in?
current_user.present?
end
end
96 changes: 96 additions & 0 deletions app/controllers/modelos_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# app/controllers/modelos_controller.rb
class ModelosController < ApplicationController
before_action :require_admin
before_action :set_modelo, only: [:show, :edit, :update, :destroy, :clone]

# GET /modelos
def index
@modelos = Modelo.includes(:perguntas).order(created_at: :desc)
end

# GET /modelos/1
def show
end

# GET /modelos/new
def new
@modelo = Modelo.new
3.times { @modelo.perguntas.build } # Cria 3 perguntas em branco por padrão
end

# GET /modelos/1/edit
def edit
@modelo.perguntas.build if @modelo.perguntas.empty?
end

# POST /modelos
def create
@modelo = Modelo.new(modelo_params)

if @modelo.save
redirect_to @modelo, notice: 'Modelo criado com sucesso.'
else
# Garante que tenha pelo menos uma pergunta para mostrar no formulário
@modelo.perguntas.build if @modelo.perguntas.empty?
render :new, status: :unprocessable_entity
end
end

# PATCH/PUT /modelos/1
def update
if @modelo.update(modelo_params)
redirect_to @modelo, notice: 'Modelo atualizado com sucesso.'
else
render :edit, status: :unprocessable_entity
end
end

# DELETE /modelos/1
def destroy
if @modelo.em_uso?
redirect_to modelos_url, alert: 'Não é possível excluir um modelo que está em uso.'
else
@modelo.destroy
redirect_to modelos_url, notice: 'Modelo excluído com sucesso.'
end
end

# POST /modelos/1/clone
def clone
novo_titulo = "#{@modelo.titulo} (Cópia)"
novo_modelo = @modelo.clonar_com_perguntas(novo_titulo)

if novo_modelo.persisted?
redirect_to edit_modelo_path(novo_modelo),
notice: 'Modelo clonado com sucesso. Edite o título se necessário.'
else
redirect_to @modelo, alert: 'Erro ao clonar modelo.'
end
end

private

def set_modelo
@modelo = Modelo.find(params[:id])
end

def modelo_params
params.require(:modelo).permit(
:titulo,
:ativo,
perguntas_attributes: [
:id,
:enunciado,
:tipo,
:opcoes,
:_destroy
]
)
end

def require_admin
unless Current.session&.user&.eh_admin?
redirect_to root_path, alert: 'Acesso restrito a administradores.'
end
end
end
8 changes: 8 additions & 0 deletions app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
class PagesController < ApplicationController
layout "application"

def index
# Feature 109: Redirecionar alunos para ver suas turmas
if Current.session&.user && !Current.session.user.eh_admin?
redirect_to avaliacoes_path
return
end

# Admins veem dashboard admin
end
end
88 changes: 47 additions & 41 deletions app/controllers/respostas_controller.rb
Original file line number Diff line number Diff line change
@@ -1,72 +1,78 @@
# app/controllers/respostas_controller.rb
class RespostasController < ApplicationController
before_action :authenticate_user!
before_action :set_formulario, only: [ :new, :create ]
before_action :verificar_disponibilidade, only: [ :new, :create ]
before_action :verificar_nao_respondeu, only: [ :new, :create ]

before_action :set_avaliacao, only: [:new, :create]
before_action :verificar_disponibilidade, only: [:new, :create]
before_action :verificar_nao_respondeu, only: [:new, :create]

def index
# Listagem de formulários pendentes para o aluno
@formularios_pendentes = Formulario.joins(:turma)
.where(turmas: { id: current_user.turma_id })
.where("data_limite > ?", Time.current)
.where.not(id: Resposta.where(aluno_id: current_user.id).select(:formulario_id).distinct)
.order(data_limite: :asc)
# Feature 109: Listagem de avaliações pendentes (já implementado em pages#index)
redirect_to root_path
end

def new
# Tela de resposta - renderizar as questões do template
@questoes = @formulario.questoes.order(:ordem)
@resposta = Resposta.new
# Feature 99: Tela para responder avaliação
@submissao = Submissao.new
@perguntas = @avaliacao.modelo.perguntas.order(:id)

# Pre-build respostas para nested attributes
@perguntas.each do |pergunta|
@submissao.respostas.build(pergunta_id: pergunta.id)
end
end

def create
# Salvar respostas no banco
success = true
resposta_params[:respostas].each do |questao_id, conteudo|
resposta = Resposta.new(
aluno_id: current_user.id,
formulario_id: @formulario.id,
questao_id: questao_id,
conteudo: conteudo
)

# Validar obrigatoriedade das respostas antes de salvar
unless resposta.save
success = false
flash[:alert] = "Todas as questões são obrigatórias"
break
# Feature 99: Salvar respostas
@submissao = Submissao.new(submissao_params)
@submissao.avaliacao = @avaliacao
@submissao.aluno = current_user
@submissao.data_envio = Time.current

# Adicionar snapshots nas respostas
@submissao.respostas.each do |resposta|
if resposta.pergunta_id
pergunta = Pergunta.find_by(id: resposta.pergunta_id)
if pergunta
resposta.snapshot_enunciado = pergunta.enunciado
resposta.snapshot_opcoes = pergunta.opcoes
end
end
end

if success
redirect_to respostas_path, notice: "Formulário respondido com sucesso!"
if @submissao.save
redirect_to root_path, notice: "Avaliação enviada com sucesso! Obrigado pela sua participação."
else
@questoes = @formulario.questoes.order(:ordem)
render :new
@perguntas = @avaliacao.modelo.perguntas.order(:id)
flash.now[:alert] = "Por favor, responda todas as perguntas obrigatórias."
render :new, status: :unprocessable_entity
end
end

private

def set_formulario
@formulario = Formulario.find(params[:formulario_id])
def set_avaliacao
@avaliacao = Avaliacao.find(params[:avaliacao_id])
end

def verificar_disponibilidade
unless @formulario.disponivel_para_resposta?
redirect_to respostas_path, alert: "Este formulário não está mais disponível para resposta."
# Verifica se avaliação ainda está no prazo
if @avaliacao.data_fim && @avaliacao.data_fim < Time.current
redirect_to root_path, alert: "Esta avaliação já foi encerrada."
elsif @avaliacao.data_inicio && @avaliacao.data_inicio > Time.current
redirect_to root_path, alert: "Esta avaliação ainda não está disponível."
end
end

def verificar_nao_respondeu
if @formulario.aluno_respondeu_tudo?(current_user.id)
redirect_to respostas_path, alert: "Você já respondeu este formulário."
# Verifica se aluno já respondeu
if Submissao.exists?(avaliacao: @avaliacao, aluno: current_user)
redirect_to root_path, alert: "Você já respondeu esta avaliação."
end
end

def resposta_params
params.require(:formulario).permit(respostas: {})
def submissao_params
params.require(:submissao).permit(
respostas_attributes: [:pergunta_id, :conteudo, :snapshot_enunciado, :snapshot_opcoes]
)
end
end
Loading
Loading