diff --git a/Gemfile b/Gemfile index 1ff1bafae8..5aaf96239b 100644 --- a/Gemfile +++ b/Gemfile @@ -67,3 +67,5 @@ end gem "tailwindcss-rails", "~> 4.4" gem "rspec-rails", "~> 8.0", groups: [:development, :test] + +gem "csv", "~> 3.3" diff --git a/Gemfile.lock b/Gemfile.lock index e57f6437fc..6422dd0a68 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,6 +98,7 @@ GEM concurrent-ruby (1.3.5) connection_pool (2.5.4) crass (1.0.6) + csv (3.3.5) cucumber (10.1.1) base64 (~> 0.2) builder (~> 3.2) @@ -439,6 +440,7 @@ DEPENDENCIES bootsnap brakeman capybara + csv (~> 3.3) cucumber-rails database_cleaner debug diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb index 8b2fe38e4f..12340a6b2e 100644 --- a/app/controllers/avaliacoes_controller.rb +++ b/app/controllers/avaliacoes_controller.rb @@ -38,4 +38,25 @@ def create redirect_to gestao_envios_avaliacoes_path, alert: "Erro ao criar avaliação: #{@avaliacao.errors.full_messages.join(', ')}" end end + + 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) + rescue ActiveRecord::StatementInvalid + @respostas = [] + flash.now[:alert] = "A tabela de respostas ainda não está disponível." + end + + respond_to do |format| + format.html + format.csv do + send_data CsvFormatterService.new(@avaliacao).generate, + filename: "resultados-avaliacao-#{@avaliacao.id}-#{Date.today}.csv" + end + end + end end diff --git a/app/models/avaliacao.rb b/app/models/avaliacao.rb index 6e8cd2f838..384c274692 100644 --- a/app/models/avaliacao.rb +++ b/app/models/avaliacao.rb @@ -2,4 +2,5 @@ class Avaliacao < ApplicationRecord belongs_to :turma belongs_to :modelo belongs_to :professor_alvo, class_name: 'User', optional: true + has_many :respostas end diff --git a/app/models/resposta.rb b/app/models/resposta.rb index 5bd07d1d29..92dd79310f 100644 --- a/app/models/resposta.rb +++ b/app/models/resposta.rb @@ -3,6 +3,7 @@ class Resposta < ApplicationRecord belongs_to :aluno, class_name: "User" belongs_to :formulario belongs_to :questao + belongs_to :avaliacao, optional: true validates :aluno_id, presence: true validates :formulario_id, presence: true diff --git a/app/services/csv_formatter_service.rb b/app/services/csv_formatter_service.rb new file mode 100644 index 0000000000..ca56214c3b --- /dev/null +++ b/app/services/csv_formatter_service.rb @@ -0,0 +1,45 @@ +require 'csv' + +class CsvFormatterService + def initialize(avaliacao) + @avaliacao = avaliacao + end + + def generate + CSV.generate(headers: true) do |csv| + csv << headers + + @avaliacao.respostas.includes(:aluno).group_by(&:aluno).each do |aluno, respostas| + row = [aluno.matricula, aluno.nome] + + # Organiza as respostas pela ordem das questões se possível, ou mapeamento simples + # Assumindo que queremos mapear questões para colunas + + # Para este MVP, vamos apenas despejar o conteúdo na ordem das questões encontradas + # Uma solução mais robusta ordenaria por ID da questão ou número + + respostas.each do |resposta| + row << resposta.conteudo + end + + csv << row + end + end + end + + private + + def headers + # Cabeçalhos estáticos para informações do Aluno + base_headers = ["Matrícula", "Nome"] + + # Cabeçalhos dinâmicos para questões + # Identificando questões únicas respondidas ou todas as questões do modelo + # Para o MVP, vamos assumir que queremos todas as questões do modelo + + questoes = @avaliacao.modelo.perguntas + question_headers = questoes.map.with_index { |q, i| "Questão #{i + 1}" } + + base_headers + question_headers + end +end diff --git a/app/views/avaliacoes/gestao_envios.html.erb b/app/views/avaliacoes/gestao_envios.html.erb index 89c9c80484..c26f95dd4f 100644 --- a/app/views/avaliacoes/gestao_envios.html.erb +++ b/app/views/avaliacoes/gestao_envios.html.erb @@ -57,6 +57,12 @@ %> <%= f.submit "Gerar Avaliação", class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-150 ease-in-out cursor-pointer" %> <% end %> + + <% if (ultima_avaliacao = turma.avaliacoes.last) %> +
+ <%= link_to "Ver Resultados (Última)", resultados_avaliacao_path(ultima_avaliacao), class: "text-blue-500 hover:text-blue-800 text-xs font-semibold" %> +
+ <% end %> <% end %> diff --git a/app/views/avaliacoes/resultados.html.erb b/app/views/avaliacoes/resultados.html.erb new file mode 100644 index 0000000000..3f17134a77 --- /dev/null +++ b/app/views/avaliacoes/resultados.html.erb @@ -0,0 +1,48 @@ +
+
+

Resultados da Avaliação

+ <%= link_to "Voltar", gestao_envios_avaliacoes_path, class: "bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded" %> +
+ +
+
+

Turma: <%= @avaliacao.turma.codigo %> - <%= @avaliacao.turma.nome %>

+

Template: <%= @avaliacao.modelo.titulo %>

+
+ + <% if @respostas.any? %> +
+ <%= 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" %> +
+ +
+ + + + + + + + + + <% @respostas.each do |resposta| %> + + + + + + <% end %> + +
MatrículaAlunoConteúdo
<%= resposta.aluno&.matricula %><%= resposta.aluno&.nome %><%= resposta.conteudo %>
+
+ <% else %> + + <% end %> +
+
diff --git a/config/routes.rb b/config/routes.rb index 5caf2531a4..71be84da79 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,6 +4,9 @@ collection do get :gestao_envios end + member do + get :resultados + end end resource :session diff --git a/db/seeds.rb b/db/seeds.rb index 47e62bdf56..a54d07ce7d 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,33 @@ -# This file should ensure the existence of records required to run the application in every environment (production, -# development, test). The code here should be idempotent so that it can be executed at any point in every environment. -# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# Admin User +admin = User.find_or_create_by!(login: 'admin') do |u| + u.email_address = 'admin@camaar.unb.br' + u.nome = 'Administrador' + u.matricula = '000000000' + u.password = 'password' + u.password_confirmation = 'password' + u.eh_admin = true +end +puts "Usuário Admin garantido (ID: #{admin.id})" -Modelo.find_or_create_by!(titulo: "Template Padrão") do |m| +# Template Padrão +modelo = Modelo.find_or_create_by!(titulo: 'Template Padrão') do |m| m.ativo = true end +puts "Modelo 'Template Padrão' garantido (ID: #{modelo.id})" + +# Perguntas do Template Padrão +perguntas_data = [ + { enunciado: 'O professor demonstrou domínio do conteúdo?', tipo: 'escala', opcoes: { min: 1, max: 5 } }, + { enunciado: 'O plano de ensino foi seguido?', tipo: 'escala', opcoes: { min: 1, max: 5 } }, + { enunciado: 'Como você avalia a didática do professor?', tipo: 'escala', opcoes: { min: 1, max: 5 } }, + { enunciado: 'Pontos positivos:', tipo: 'texto', opcoes: {} }, + { enunciado: 'Pontos a melhorar:', tipo: 'texto', opcoes: {} } +] + +perguntas_data.each do |p_data| + Pergunta.find_or_create_by!(modelo: modelo, enunciado: p_data[:enunciado]) do |p| + p.tipo = p_data[:tipo] + p.opcoes = p_data[:opcoes] + end +end +puts "#{modelo.perguntas.count} perguntas garantidas para o Template Padrão." diff --git a/spec/helpers/avaliacoes_helper_spec.rb b/spec/helpers/avaliacoes_helper_spec.rb deleted file mode 100644 index 95cf479b6f..0000000000 --- a/spec/helpers/avaliacoes_helper_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'rails_helper' - -# Specs in this file have access to a helper object that includes -# the AvaliacoesHelper. For example: -# -# describe AvaliacoesHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end -RSpec.describe AvaliacoesHelper, type: :helper do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/services/csv_formatter_service_spec.rb b/spec/services/csv_formatter_service_spec.rb new file mode 100644 index 0000000000..12b5cb6174 --- /dev/null +++ b/spec/services/csv_formatter_service_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +RSpec.describe CsvFormatterService do + describe '#generate' do + let(:modelo) { double('Modelo', perguntas: [ + double('Pergunta', enunciado: 'Q1'), + double('Pergunta', enunciado: 'Q2') + ])} + + let(:avaliacao) { double('Avaliacao', id: 1, modelo: modelo) } + + let(:aluno1) { double('User', matricula: '123', nome: 'Alice') } + let(:aluno2) { double('User', matricula: '456', nome: 'Bob') } + + let(:resposta_a1_q1) { double('Resposta', aluno: aluno1, conteudo: 'Ans 1A') } + let(:resposta_a1_q2) { double('Resposta', aluno: aluno1, conteudo: 'Ans 1B') } + let(:resposta_a2_q1) { double('Resposta', aluno: aluno2, conteudo: 'Ans 2A') } + + # Estrutura agrupada por aluno simulando resultado de query ActiveRecord + let(:grouped_responses) do + { + aluno1 => [resposta_a1_q1, resposta_a1_q2], + aluno2 => [resposta_a2_q1] + } + end + + before do + # Mock da cadeia: avaliacao.respostas.includes.group_by + allow(avaliacao).to receive_message_chain(:respostas, :includes, :group_by).and_return(grouped_responses) + end + + it 'gera uma string CSV válida com cabeçalhos e linhas' do + csv_string = described_class.new(avaliacao).generate + rows = csv_string.split("\n") + + # Cabeçalhos: Matrícula, Nome, Questão 1, Questão 2 + expect(rows[0]).to include("Matrícula,Nome,Questão 1,Questão 2") + + # Linha 1: Respostas da Alice + expect(rows[1]).to include("123,Alice,Ans 1A,Ans 1B") + + # Linha 2: Respostas do Bob + expect(rows[2]).to include("456,Bob,Ans 2A") + end + end +end diff --git a/spec/views/avaliacoes/create.html.tailwindcss_spec.rb b/spec/views/avaliacoes/create.html.tailwindcss_spec.rb deleted file mode 100644 index 794cc9b1e6..0000000000 --- a/spec/views/avaliacoes/create.html.tailwindcss_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe "avaliacoes/create.html.tailwindcss", type: :view do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/views/avaliacoes/gestao_envios.html.tailwindcss_spec.rb b/spec/views/avaliacoes/gestao_envios.html.tailwindcss_spec.rb deleted file mode 100644 index c056da0962..0000000000 --- a/spec/views/avaliacoes/gestao_envios.html.tailwindcss_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe "avaliacoes/gestao_envios.html.tailwindcss", type: :view do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/views/avaliacoes/index.html.tailwindcss_spec.rb b/spec/views/avaliacoes/index.html.tailwindcss_spec.rb deleted file mode 100644 index b6bebe4cbb..0000000000 --- a/spec/views/avaliacoes/index.html.tailwindcss_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe "avaliacoes/index.html.tailwindcss", type: :view do - pending "add some examples to (or delete) #{__FILE__}" -end