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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ Acesse: http://localhost:3000
## Testes

```bash
# Rodar testes BDD
# Rodar todos os testes BDD (25 cenários, 133 steps)
bundle exec cucumber --tags "not @wip"

# Rodar feature específica
bundle exec cucumber features/sistema_login.feature
```

33 changes: 33 additions & 0 deletions app/controllers/avaliacoes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ def resultados
# Pré-carrega dependências para evitar N+1.
begin
@submissoes = @avaliacao.submissoes.includes(:aluno, :respostas)
@perguntas = @avaliacao.modelo.perguntas.order(:id)
@question_stats = build_question_statistics(@avaliacao)
rescue ActiveRecord::StatementInvalid
@submissoes = []
@perguntas = []
@question_stats = {}
flash.now[:alert] = "Erro ao carregar submissões."
end

Expand All @@ -69,4 +73,33 @@ def resultados
end
end
end

private

def build_question_statistics(avaliacao)
avaliacao.modelo.perguntas.each_with_object({}) do |pergunta, stats|
respostas = Resposta.joins(:submissao)
.where(submissoes: { avaliacao_id: avaliacao.id })
.where(questao_id: pergunta.id)

if [ "multipla_escolha", "checkbox", "escala" ].include?(pergunta.tipo)
# Conta cada opção escolhida
stats[pergunta.id] = {
type: pergunta.tipo,
data: respostas.group(:conteudo).count,
total: respostas.count,
responses: []
}
else
# Para texto, inclui as respostas para exibição
text_responses = respostas.pluck(:conteudo).compact.reject(&:blank?)
stats[pergunta.id] = {
type: pergunta.tipo,
data: {},
total: respostas.count,
responses: text_responses
}
end
end
end
end
8 changes: 5 additions & 3 deletions app/controllers/sigaa_imports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ def new
def create
# Usa automaticamente o arquivo class_members.json do projeto
file_path = Rails.root.join("class_members.json")
classes_file_path = Rails.root.join("classes.json")

unless File.exist?(file_path)
redirect_to new_sigaa_import_path, alert: "Arquivo class_members.json não encontrado no projeto."
return
end

# Processa a importação
service = SigaaImportService.new(file_path)
# Processa a importação (passa classes.json se existir)
service = SigaaImportService.new(file_path, classes_file_path)
@results = service.process

if @results[:errors].any?
Expand All @@ -34,13 +35,14 @@ def create
def update
# Usa automaticamente o arquivo class_members.json do projeto (atualização)
file_path = Rails.root.join("class_members.json")
classes_file_path = Rails.root.join("classes.json")

unless File.exist?(file_path)
redirect_to new_sigaa_import_path, alert: "Arquivo class_members.json não encontrado no projeto."
return
end

service = SigaaImportService.new(file_path)
service = SigaaImportService.new(file_path, classes_file_path)
@results = service.process

if @results[:errors].any?
Expand Down
8 changes: 4 additions & 4 deletions app/services/csv_formatter_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ def generate
csv << headers

@avaliacao.submissoes.includes(:aluno, :respostas).each do |submissao|
aluno = submissao.aluno
row = [ aluno.matricula, aluno.nome ]
# Usar ID anônimo em vez do nome/matrícula do aluno para privacidade
row = [ submissao.id ]

# Organiza as respostas pela ordem das questões se possível, ou mapeamento simples
# Assumindo que queremos mapear questões para colunas
Expand All @@ -31,8 +31,8 @@ def generate
private

def headers
# Cabeçalhos estáticos para informações do Aluno
base_headers = [ "Matrícula", "Nome" ]
# Cabeçalho anônimo (sem identificação do aluno para privacidade)
base_headers = [ "Submissão" ]

# Cabeçalhos dinâmicos para questões
# Identificando questões únicas respondidas ou todas as questões do modelo
Expand Down
27 changes: 25 additions & 2 deletions app/services/sigaa_import_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
require "csv"

class SigaaImportService
def initialize(file_path)
def initialize(file_path, classes_file_path = nil)
@file_path = file_path
@classes_file_path = classes_file_path
@results = {
turmas_created: 0,
turmas_updated: 0,
Expand Down Expand Up @@ -50,13 +51,18 @@ def process

def process_json
data = JSON.parse(File.read(@file_path))
classes_lookup = build_classes_lookup

# class_members.json é um array de turmas
data.each do |turma_data|
# Busca o nome real da turma em classes.json
class_key = [ turma_data["code"], turma_data["semester"] ]
class_name = classes_lookup[class_key] || turma_data["code"]

# Mapeia campos do formato real para o esperado
normalized_data = {
"codigo" => turma_data["code"],
"nome" => turma_data["code"], # Usa o código como nome se não tiver
"nome" => class_name,
"semestre" => turma_data["semester"],
"participantes" => []
}
Expand Down Expand Up @@ -88,6 +94,23 @@ def process_json
end
end

# Constrói um hash de lookup para nomes de turmas a partir de classes.json
def build_classes_lookup
return {} unless @classes_file_path && File.exist?(@classes_file_path)

begin
classes_data = JSON.parse(File.read(@classes_file_path))
classes_data.each_with_object({}) do |item, hash|
# Usa code + semester como chave composta
key = [ item["code"], item.dig("class", "semester") ]
hash[key] = item["name"]
end
rescue JSON::ParserError
@results[:errors] << "Arquivo classes.json inválido"
{}
end
end

def process_csv
CSV.foreach(@file_path, headers: true, col_sep: ",") do |row|
# Assumindo estrutura do CSV
Expand Down
123 changes: 102 additions & 21 deletions app/views/avaliacoes/resultados.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<% content_for :page_title, "Gerenciamento - Gestão de Envios - Resultados" %>

<!-- Chart.js CDN - MUST be loaded before charts are created -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-800">Resultados da Avaliação</h1>
Expand All @@ -9,33 +13,110 @@
<div class="mb-4">
<p class="text-gray-700 text-sm font-bold mb-2">Turma: <span class="font-normal"><%= @avaliacao.turma.codigo %> - <%= @avaliacao.turma.nome %></span></p>
<p class="text-gray-700 text-sm font-bold mb-2">Template: <span class="font-normal"><%= @avaliacao.modelo.titulo %></span></p>
<p class="text-gray-700 text-sm font-bold mb-2">Total de Submissões: <span class="font-normal"><%= @submissoes.count %></span></p>
</div>

<% if @submissoes.any? %>
<div class="flex justify-end mb-4">
<div class="flex justify-end mb-6">
<%= link_to "Download CSV", resultados_avaliacao_path(@avaliacao, format: :csv), class: "bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded" %>
</div>

<div class="overflow-x-auto">
<table class="min-w-full leading-normal">
<thead>
<tr>
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Matrícula</th>
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Aluno</th>
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Envio</th>
</tr>
</thead>
<tbody>
<% @submissoes.each do |submissao| %>
<tr>
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm"><%= submissao.aluno&.matricula %></td>
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm"><%= submissao.aluno&.nome %></td>
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm"><%= submissao.respostas.count %> respostas</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<!-- Estatísticas por Pergunta -->
<h2 class="text-2xl font-bold text-gray-800 mb-4">Estatísticas das Respostas</h2>

<% @perguntas.each_with_index do |pergunta, index| %>
<div class="bg-gray-50 rounded-lg p-6 mb-4">
<h3 class="text-lg font-semibold text-gray-700 mb-3">
Questão <%= index + 1 %>: <%= pergunta.enunciado %>
</h3>
<p class="text-sm text-gray-500 mb-2">Tipo: <%= pergunta.tipo_humanizado %></p>

<% stats = @question_stats[pergunta.id] || { type: pergunta.tipo, data: {}, total: 0, responses: [] } %>

<% if %w[multipla_escolha checkbox escala].include?(pergunta.tipo) && stats[:data].any? %>
<!-- Gráfico para perguntas de múltipla escolha/checkbox/escala -->
<div class="mt-4" style="max-width: 500px;">
<canvas id="chart-<%= pergunta.id %>" width="400" height="200"></canvas>
</div>
<script>
(function() {
var ctx = document.getElementById('chart-<%= pergunta.id %>').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: <%= raw stats[:data].keys.to_json %>,
datasets: [{
label: 'Respostas',
data: <%= raw stats[:data].values.to_json %>,
backgroundColor: [
'rgba(54, 162, 235, 0.7)',
'rgba(255, 99, 132, 0.7)',
'rgba(255, 206, 86, 0.7)',
'rgba(75, 192, 192, 0.7)',
'rgba(153, 102, 255, 0.7)',
'rgba(255, 159, 64, 0.7)'
],
borderColor: [
'rgba(54, 162, 235, 1)',
'rgba(255, 99, 132, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
})();
</script>
<% elsif %w[texto_curto texto_longo].include?(pergunta.tipo) %>
<!-- Respostas de texto em área scrollável -->
<div class="bg-white rounded p-4 border border-gray-200">
<p class="text-gray-600 mb-3">
<span class="font-semibold"><%= stats[:total] %></span> respostas recebidas
</p>
<% if stats[:responses].present? && stats[:responses].any? %>
<div class="max-h-64 overflow-y-auto border border-gray-300 rounded p-3 bg-gray-50">
<% stats[:responses].each_with_index do |response, resp_index| %>
<div class="<%= resp_index > 0 ? 'border-t border-gray-200 pt-2 mt-2' : '' %>">
<p class="text-gray-700 text-sm whitespace-pre-wrap"><%= response %></p>
</div>
<% end %>
</div>
<% elsif stats[:total] > 0 %>
<p class="text-sm text-gray-500 italic">
Nenhuma resposta de texto disponível.
</p>
<% end %>
</div>
<% else %>
<!-- Outros tipos sem dados -->
<div class="bg-white rounded p-4 border border-gray-200">
<p class="text-gray-600">
<span class="font-semibold"><%= stats[:total] %></span> respostas recebidas
</p>
</div>
<% end %>
</div>
<% end %>

<% else %>
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4" role="alert">
<p class="font-bold">Atenção</p>
Expand Down
5 changes: 0 additions & 5 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 23 additions & 1 deletion db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,30 @@
end
puts "Usuário Admin garantido (ID: #{admin.id})"

# Aluno de teste
aluno = User.find_or_create_by!(login: 'aluno123') do |u|
u.email_address = 'aluno123@aluno.unb.br'
u.nome = 'Aluno Teste'
u.matricula = '123456789'
u.password = 'senha123'
u.password_confirmation = 'senha123'
u.eh_admin = false
end
puts "Usuário Aluno garantido (ID: #{aluno.id})"

# Professor de teste
prof = User.find_or_create_by!(login: 'prof') do |u|
u.email_address = 'prof@unb.br'
u.nome = 'Professor Teste'
u.matricula = '987654321'
u.password = 'senha123'
u.password_confirmation = 'senha123'
u.eh_admin = false
end
puts "Usuário Professor garantido (ID: #{prof.id})"

# Template Padrão
# Garantir que exista pelo menos um modelo com perguntas
puts "Modelo 'Template Padrão' garantido (ID: 1)"
modelo = Modelo.find_or_initialize_by(id: 1)
modelo.assign_attributes(
titulo: 'Template Padrão',
Expand Down Expand Up @@ -52,4 +73,5 @@
end

modelo.save!
puts "Modelo 'Template Padrão' garantido (ID: #{modelo.id})"
puts "#{modelo.perguntas.count} perguntas garantidas para o Template Padrão."
9 changes: 5 additions & 4 deletions features/atualizar_base_de_dados.feature
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ Funcionalidade: Atualizar base de dados com SIGAA #108
E devo ver um resumo das alterações realizadas

@108.2
Cenário: 108.2 - Quando um administrador tenta atualizar a base de dados com o SIGAA, mas os dados fornecidos forem inválidos, então deve mostrar mensagem de erro
Quando faço upload de um arquivo inválido para atualização
Então os dados não devem ser alterados
E devo ver uma mensagem de erro de formato
Cenário: 108.2 - Quando um administrador atualiza a base mas os dados já estão atualizados, deve ver mensagem informativa
Dado que os dados do SIGAA já foram importados anteriormente
Quando faço upload de um arquivo CSV do SIGAA com dados atualizados
E confirmo a operação
Então devo ver uma mensagem indicando que os dados já estão atualizados
20 changes: 20 additions & 0 deletions features/cadastra_usuarios.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# language: pt
#2 pontos

Funcionalidade: Cadastrar usuários do sistema #100
Eu como Administrador
Quero cadastrar participantes de turmas do SIGAA ao importar dados de usuarios novos para o sistema
A fim de que eles acessem o sistema

# Nota: O cadastro de usuários acontece automaticamente durante a importação de dados do SIGAA (Feature #98).
# Os usuários são criados com senhas temporárias exibidas na tela de sucesso.

Contexto:
Dado que o o banco de dados está "vazio"
E que está na tela "Gerenciamento"

@100.1
Cenário: 100.1 - Quando um Administrador tenta registrar novos usuários do Sigaa, deve salvar os novos alunos no banco de dados e enviar emails para cadastrar a senha.
Quando importo um arquivo de dados do SIGAA contendo novos usuários
Então os novos usuários devem ser salvos no banco de dados
E um email de boas-vindas deve ser enviado para cada um
Loading