From 903eb184ae66aa25df8f805359dddf63c845cb33 Mon Sep 17 00:00:00 2001 From: vmg192 Date: Tue, 9 Dec 2025 00:15:41 -0300 Subject: [PATCH] =?UTF-8?q?Implementa=C3=A7=C3=B5es:=20Steps=20do=20teste?= =?UTF-8?q?=20de=20login=20Ajustes:=20Multiplos=20ajustes=20para=20que=20o?= =?UTF-8?q?=20site=20funcionasse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 4 + Gemfile.lock | 9 + README.md | 46 +++- app/controllers/application_controller.rb | 1 + app/controllers/avaliacoes_controller.rb | 26 +- app/controllers/concerns/authenticatable.rb | 20 ++ app/controllers/modelos_controller.rb | 96 +++++++ app/controllers/pages_controller.rb | 8 + app/controllers/respostas_controller.rb | 88 +++--- app/controllers/sessions_controller.rb | 10 +- app/controllers/sigaa_imports_controller.rb | 78 ++++++ app/mailers/user_mailer.rb | 15 +- app/models/avaliacao.rb | 4 +- app/models/formulario.rb | 24 -- app/models/matricula_turma.rb | 4 + app/models/modelo.rb | 55 +++- app/models/pergunta.rb | 79 +++++- app/models/resposta.rb | 22 +- app/models/submissao.rb | 9 + app/models/turma.rb | 2 +- app/models/user.rb | 1 + app/services/csv_formatter_service.rb | 5 +- app/services/sigaa_import_service.rb | 67 ++++- app/views/avaliacoes/gestao_envios.html.erb | 5 +- app/views/avaliacoes/index.html.erb | 63 ++++- app/views/avaliacoes/resultados.html.erb | 12 +- app/views/components/_card.html.erb | 52 ++-- app/views/components/_dashBoardAdmin.html.erb | 8 +- .../_frameBrancoMenuLateral.html.erb | 28 +- .../components/_frameRoxoMenuLateral.html.erb | 10 +- app/views/components/_header.html.erb | 2 +- app/views/components/_sidebar.html.erb | 3 + app/views/layouts/application.html.erb | 6 +- app/views/modelos/_form.html.erb | 258 ++++++++++++++++++ app/views/modelos/_pergunta_fields.html.erb | 86 ++++++ app/views/modelos/edit.html.erb | 1 + app/views/modelos/index.html.erb | 107 ++++++++ app/views/modelos/new.html.erb | 1 + app/views/modelos/show.html.erb | 69 +++++ app/views/pages/index.html.erb | 40 +++ app/views/respostas/new.html.erb | 138 +++++++--- app/views/sessions/new.html.erb | 4 +- app/views/sigaa_imports/new.html.erb | 24 ++ app/views/sigaa_imports/success.html.erb | 107 ++++++++ app/views/user_mailer/cadastro_email.html.erb | 61 +++++ app/views/user_mailer/cadastro_email.text.erb | 25 ++ config/initializers/mail.rb | 65 +++++ config/routes.rb | 17 ++ .../20251208190235_create_submissoes.rb | 11 + db/migrate/20251208190239_create_respostas.rb | 13 + db/schema.rb | 28 +- db/seeds.rb | 58 ++-- db/seeds_test.rb | 70 +++++ features/atualizar_base_de_dados.feature | 40 +-- "features/cadastra_usu\303\241rios.feature" | 58 ++-- features/cria_avaliacao.feature | 24 ++ features/cria_formulario.feature | 39 --- features/cria_template_formulario.feature | 4 +- ...3o_remo\303\247\303\243o_template.feature" | 4 +- features/gera_relatorio_adm.feature | 26 +- features/importa_dados_sigaa.feature | 39 +-- features/responder_formulario.feature | 52 ++-- features/sistema_login.feature | 82 ++---- .../atualizar_base_dados_steps.rb | 49 ++++ .../step_definitions/cria_avaliacao_steps.rb | 67 +++++ features/step_definitions/email_steps.rb | 63 +++++ .../importa_dados_sigaa_steps.rb | 140 ++++++++++ features/step_definitions/relatorio_steps.rb | 27 ++ .../step_definitions/resultados_adm_steps.rb | 59 ++++ features/step_definitions/resultados_steps.rb | 42 +++ features/step_definitions/shared_steps.rb | 79 ++++++ .../step_definitions/sistema_login_steps.rb | 10 + .../step_definitions/student_view_steps.rb | 39 +++ .../visualizacao_formulario_steps.rb | 33 +++ features/support/email.rb | 28 ++ features/support/test_data.rb | 31 +++ features/visualiza_templates.feature | 4 +- ...247\303\243o_formulario_resultado.feature" | 91 ++---- ...03\247\303\243o_formul\303\241rio.feature" | 43 +-- reset_db.sh | 21 ++ spec/services/csv_formatter_service_spec.rb | 24 +- 81 files changed, 2685 insertions(+), 578 deletions(-) create mode 100644 app/controllers/concerns/authenticatable.rb create mode 100644 app/controllers/modelos_controller.rb create mode 100644 app/controllers/sigaa_imports_controller.rb delete mode 100644 app/models/formulario.rb create mode 100644 app/models/matricula_turma.rb create mode 100644 app/views/modelos/_form.html.erb create mode 100644 app/views/modelos/_pergunta_fields.html.erb create mode 100644 app/views/modelos/edit.html.erb create mode 100644 app/views/modelos/index.html.erb create mode 100644 app/views/modelos/new.html.erb create mode 100644 app/views/modelos/show.html.erb create mode 100644 app/views/sigaa_imports/new.html.erb create mode 100644 app/views/sigaa_imports/success.html.erb create mode 100644 app/views/user_mailer/cadastro_email.html.erb create mode 100644 app/views/user_mailer/cadastro_email.text.erb create mode 100644 config/initializers/mail.rb create mode 100644 db/migrate/20251208190235_create_submissoes.rb create mode 100644 db/migrate/20251208190239_create_respostas.rb create mode 100644 db/seeds_test.rb create mode 100644 features/cria_avaliacao.feature delete mode 100644 features/cria_formulario.feature create mode 100644 features/step_definitions/atualizar_base_dados_steps.rb create mode 100644 features/step_definitions/cria_avaliacao_steps.rb create mode 100644 features/step_definitions/email_steps.rb create mode 100644 features/step_definitions/importa_dados_sigaa_steps.rb create mode 100644 features/step_definitions/relatorio_steps.rb create mode 100644 features/step_definitions/resultados_adm_steps.rb create mode 100644 features/step_definitions/resultados_steps.rb create mode 100644 features/step_definitions/shared_steps.rb create mode 100644 features/step_definitions/sistema_login_steps.rb create mode 100644 features/step_definitions/student_view_steps.rb create mode 100644 features/step_definitions/visualizacao_formulario_steps.rb create mode 100644 features/support/email.rb create mode 100644 features/support/test_data.rb create mode 100755 reset_db.sh diff --git a/Gemfile b/Gemfile index 5aaf96239b..97dd8108d0 100644 --- a/Gemfile +++ b/Gemfile @@ -69,3 +69,7 @@ gem "tailwindcss-rails", "~> 4.4" gem "rspec-rails", "~> 8.0", groups: [:development, :test] gem "csv", "~> 3.3" + +# Email preview in browser for development +gem 'letter_opener', group: :development + diff --git a/Gemfile.lock b/Gemfile.lock index 6422dd0a68..aaf3dbd3ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -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) @@ -447,6 +455,7 @@ DEPENDENCIES importmap-rails jbuilder kamal + letter_opener propshaft puma (>= 5.0) rails (~> 8.0.4) diff --git a/README.md b/README.md index 9d7fe1bf53..e03c0fa3cb 100644 --- a/README.md +++ b/README.md @@ -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 +``` diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8d68510c38..4763b17ed1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb index 12340a6b2e..43a931f5d1 100644 --- a/app/controllers/avaliacoes_controller.rb +++ b/app/controllers/avaliacoes_controller.rb @@ -1,8 +1,20 @@ class AvaliacoesController < ApplicationController - allow_unauthenticated_access only: %i[ index create gestao_envios ] - + # Requer autenticação para todas as actions + 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 + + 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 @@ -42,13 +54,11 @@ def create 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| diff --git a/app/controllers/concerns/authenticatable.rb b/app/controllers/concerns/authenticatable.rb new file mode 100644 index 0000000000..4d7f7a6e56 --- /dev/null +++ b/app/controllers/concerns/authenticatable.rb @@ -0,0 +1,20 @@ +# app/controllers/concerns/authenticatable.rb +module Authenticatable + extend ActiveSupport::Concern + + included do + helper_method :current_user, :user_signed_in? + end + + def authenticate_user! + redirect_to new_session_path, alert: "É necessário fazer login." unless user_signed_in? + end + + def current_user + Current.session&.user + end + + def user_signed_in? + current_user.present? + end +end diff --git a/app/controllers/modelos_controller.rb b/app/controllers/modelos_controller.rb new file mode 100644 index 0000000000..eb47f924bc --- /dev/null +++ b/app/controllers/modelos_controller.rb @@ -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 diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 2133554dcf..6c35aef970 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -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 diff --git a/app/controllers/respostas_controller.rb b/app/controllers/respostas_controller.rb index 46c62b684b..ce1639611b 100644 --- a/app/controllers/respostas_controller.rb +++ b/app/controllers/respostas_controller.rb @@ -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 diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index d7e14c97ad..6aa3c6cbf0 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -7,11 +7,15 @@ def new end def create - if user = User.authenticate_by(params.permit(:email_address, :password)) + # Tenta autenticar por email ou por login + user = User.authenticate_by(email_address: params[:email_address], password: params[:password]) || + User.authenticate_by(login: params[:email_address], password: params[:password]) + + if user start_new_session_for user - redirect_to after_authentication_url + redirect_to after_authentication_url, notice: "Login realizado com sucesso" else - redirect_to new_session_path, alert: "Try another email address or password." + redirect_to new_session_path, alert: "Falha na autenticação. Usuário ou senha inválidos." end end diff --git a/app/controllers/sigaa_imports_controller.rb b/app/controllers/sigaa_imports_controller.rb new file mode 100644 index 0000000000..c5e519ae8d --- /dev/null +++ b/app/controllers/sigaa_imports_controller.rb @@ -0,0 +1,78 @@ +class SigaaImportsController < ApplicationController + # Apenas administradores podem importar dados + before_action :require_admin + + def new + # Exibe formulário de upload + end + + def create + # Usa automaticamente o arquivo class_members.json do projeto + file_path = Rails.root.join('class_members.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) + @results = service.process + + if @results[:errors].any? + flash[:alert] = "Erros durante a importação: #{@results[:errors].join(', ')}" + redirect_to new_sigaa_import_path + else + # Armazena resultados no cache (session é muito pequena para ~40 usuários) + cache_key = "import_results_#{SecureRandom.hex(8)}" + Rails.cache.write(cache_key, @results, expires_in: 10.minutes) + + redirect_to success_sigaa_imports_path(key: cache_key) + end + end + + def update + # Usa automaticamente o arquivo class_members.json do projeto (atualização) + file_path = Rails.root.join('class_members.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) + @results = service.process + + if @results[:errors].any? + flash[:alert] = "Erros durante a atualização: #{@results[:errors].join(', ')}" + redirect_to new_sigaa_import_path + else + # Armazena resultados no cache (session é muito pequena para ~40 usuários) + cache_key = "import_results_#{SecureRandom.hex(8)}" + Rails.cache.write(cache_key, @results, expires_in: 10.minutes) + + redirect_to success_sigaa_imports_path(key: cache_key) + end + end + + def success + cache_key = params[:key] + @results = Rails.cache.read(cache_key) if cache_key + + unless @results + redirect_to root_path, alert: "Nenhum resultado de importação encontrado ou expirado." + return + end + + # Limpa o cache após carregar (usuário já viu) + Rails.cache.delete(cache_key) + end + + private + + def require_admin + unless Current.session&.user&.eh_admin? + redirect_to root_path, alert: "Acesso negado. Apenas administradores podem importar dados." + end + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 7470851785..80bf09188e 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,7 +1,20 @@ class UserMailer < ApplicationMailer + # Email para definição de senha (método existente) def definicao_senha(user) @user = user - @url = "http://localhost:3000/definicao_senha" #ajustar conforme o necessario + @url = "http://localhost:3000/definicao_senha" mail(to: @user.email, subject: 'Definição de Senha - Sistema de Gestão') end + + # Email de cadastro com senha temporária (novo método) + def cadastro_email(user, senha_temporaria) + @user = user + @senha = senha_temporaria + @login_url = new_session_url + + mail( + to: @user.email_address, + subject: 'Bem-vindo(a) ao CAMAAR - Sua senha de acesso' + ) + end end diff --git a/app/models/avaliacao.rb b/app/models/avaliacao.rb index 384c274692..55beaef3c1 100644 --- a/app/models/avaliacao.rb +++ b/app/models/avaliacao.rb @@ -2,5 +2,7 @@ class Avaliacao < ApplicationRecord belongs_to :turma belongs_to :modelo belongs_to :professor_alvo, class_name: 'User', optional: true - has_many :respostas + + has_many :submissoes, class_name: 'Submissao', dependent: :destroy + has_many :respostas, through: :submissoes end diff --git a/app/models/formulario.rb b/app/models/formulario.rb deleted file mode 100644 index cf64f5dd43..0000000000 --- a/app/models/formulario.rb +++ /dev/null @@ -1,24 +0,0 @@ -# app/models/formulario.rb -class Formulario < ApplicationRecord - belongs_to :template - belongs_to :turma - has_many :respostas, dependent: :destroy - has_many :questoes, through: :template - - validates :titulo, presence: true - validates :data_limite, presence: true - validates :template_id, presence: true - validates :turma_id, presence: true - - # Método para verificar se o formulário está disponível para resposta - def disponivel_para_resposta? - data_limite > Time.current - end - - # Método para verificar se um aluno já respondeu todas as questões - def aluno_respondeu_tudo?(aluno_id) - questoes_respondidas = respostas.where(aluno_id: aluno_id).pluck(:questao_id) - questoes_ids = questoes.pluck(:id) - (questoes_ids - questoes_respondidas).empty? - end -end diff --git a/app/models/matricula_turma.rb b/app/models/matricula_turma.rb new file mode 100644 index 0000000000..6e658baf36 --- /dev/null +++ b/app/models/matricula_turma.rb @@ -0,0 +1,4 @@ +class MatriculaTurma < ApplicationRecord + belongs_to :user + belongs_to :turma +end diff --git a/app/models/modelo.rb b/app/models/modelo.rb index 1a058b998e..20f7c0cb5d 100644 --- a/app/models/modelo.rb +++ b/app/models/modelo.rb @@ -1,5 +1,54 @@ class Modelo < ApplicationRecord - has_many :perguntas - - validates :titulo, presence: true + # Relacionamentos + has_many :perguntas, dependent: :destroy + has_many :avaliacoes, dependent: :restrict_with_error + + # Validações + validates :titulo, presence: true, uniqueness: { case_sensitive: false } + + # Validação customizada: não permitir modelo sem perguntas + validate :deve_ter_pelo_menos_uma_pergunta, on: :create + validate :nao_pode_remover_todas_perguntas, on: :update + + # Aceita atributos aninhados para perguntas + accepts_nested_attributes_for :perguntas, + allow_destroy: true, + reject_if: :all_blank + + # Método para verificar se modelo está em uso + def em_uso? + avaliacoes.any? + end + + # Método para clonar modelo com perguntas + def clonar_com_perguntas(novo_titulo) + novo_modelo = dup + novo_modelo.titulo = novo_titulo + novo_modelo.ativo = false # Clones começam inativos + novo_modelo.save + + if novo_modelo.persisted? + perguntas.each do |pergunta| + nova_pergunta = pergunta.dup + nova_pergunta.modelo = novo_modelo + nova_pergunta.save + end + end + + novo_modelo + end + + private + + def deve_ter_pelo_menos_uma_pergunta + if perguntas.empty? || perguntas.all? { |p| p.marked_for_destruction? } + errors.add(:base, "Um modelo deve ter pelo menos uma pergunta") + end + end + + def nao_pode_remover_todas_perguntas + if persisted? && (perguntas.empty? || perguntas.all? { |p| p.marked_for_destruction? }) + errors.add(:base, "Não é possível remover todas as perguntas de um modelo existente") + end + end end diff --git a/app/models/pergunta.rb b/app/models/pergunta.rb index cbb26f0609..524a57cc92 100644 --- a/app/models/pergunta.rb +++ b/app/models/pergunta.rb @@ -1,6 +1,81 @@ class Pergunta < ApplicationRecord + self.table_name = 'perguntas' # Plural correto em português + + # Relacionamentos belongs_to :modelo - + has_many :respostas, foreign_key: 'questao_id', dependent: :destroy + + # Tipos de perguntas disponíveis + TIPOS = { + 'texto_longo' => 'Texto Longo', + 'texto_curto' => 'Texto Curto', + 'multipla_escolha' => 'Múltipla Escolha', + 'checkbox' => 'Checkbox (Múltipla Seleção)', + 'escala' => 'Escala Likert (1-5)', + 'data' => 'Data', + 'hora' => 'Hora' + }.freeze + + # Validações validates :enunciado, presence: true - validates :tipo, presence: true + validates :tipo, presence: true, inclusion: { in: TIPOS.keys } + + # Validações condicionais + validate :opcoes_requeridas_para_multipla_escolha + validate :opcoes_requeridas_para_checkbox + + # Callbacks + before_validation :definir_ordem_padrao, on: :create + + # Métodos + def tipo_humanizado + TIPOS[tipo] || tipo + end + + def requer_opcoes? + ['multipla_escolha', 'checkbox'].include?(tipo) + end + + def lista_opcoes + return [] unless opcoes.present? + # Assume que opcoes é JSON array ou string separada por ; + if opcoes.is_a?(Array) + opcoes + elsif opcoes.is_a?(String) + begin + JSON.parse(opcoes) + rescue JSON::ParserError + opcoes.split(';').map(&:strip) + end + else + [] + end + end + + private + + def definir_ordem_padrao + if modelo.present? + ultima_ordem = modelo.perguntas.maximum(:id) || 0 + # Ordem pode ser baseada no ID para simplificar + end + end + + def opcoes_requeridas_para_multipla_escolha + if tipo == 'multipla_escolha' + opcoes_lista = lista_opcoes + if opcoes_lista.blank? || opcoes_lista.size < 2 + errors.add(:opcoes, 'deve ter pelo menos duas opções para múltipla escolha') + end + end + end + + def opcoes_requeridas_para_checkbox + if tipo == 'checkbox' + opcoes_lista = lista_opcoes + if opcoes_lista.blank? || opcoes_lista.size < 2 + errors.add(:opcoes, 'deve ter pelo menos duas opções para checkbox') + end + end + end end diff --git a/app/models/resposta.rb b/app/models/resposta.rb index 92dd79310f..e10bfad359 100644 --- a/app/models/resposta.rb +++ b/app/models/resposta.rb @@ -1,16 +1,12 @@ -# app/models/resposta.rb 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 - validates :questao_id, presence: true + self.table_name = 'respostas' # Plural correto em português + + belongs_to :submissao + belongs_to :pergunta, foreign_key: 'questao_id' # Coluna ainda é questao_id no banco + validates :conteudo, presence: true - - # Validação para garantir que um aluno não responda o mesmo formulário duas vezes - validates_uniqueness_of :aluno_id, scope: [ :formulario_id, :questao_id ], - message: "já respondeu esta questão neste formulário" + + # Alias para compatibilidade + alias_attribute :pergunta_id, :questao_id end + diff --git a/app/models/submissao.rb b/app/models/submissao.rb index e69de29bb2..96389f51d2 100644 --- a/app/models/submissao.rb +++ b/app/models/submissao.rb @@ -0,0 +1,9 @@ +class Submissao < ApplicationRecord + self.table_name = 'submissoes' # Plural correto em português + + belongs_to :aluno, class_name: 'User' + belongs_to :avaliacao + has_many :respostas, dependent: :destroy + + accepts_nested_attributes_for :respostas +end diff --git a/app/models/turma.rb b/app/models/turma.rb index 678a49a4ec..c40813c272 100644 --- a/app/models/turma.rb +++ b/app/models/turma.rb @@ -1,5 +1,5 @@ class Turma < ApplicationRecord has_many :avaliacoes - # has_many :matricula_turmas + has_many :matricula_turmas has_many :users, through: :matricula_turmas end diff --git a/app/models/user.rb b/app/models/user.rb index 2e47818df0..75c1f8c6e5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,6 +3,7 @@ class User < ApplicationRecord has_many :sessions, dependent: :destroy has_many :matricula_turmas has_many :turmas, through: :matricula_turmas + has_many :submissoes, class_name: 'Submissao', foreign_key: :aluno_id, dependent: :destroy validates :email_address, presence: true, uniqueness: true validates :login, presence: true, uniqueness: true diff --git a/app/services/csv_formatter_service.rb b/app/services/csv_formatter_service.rb index ca56214c3b..b2d02776a8 100644 --- a/app/services/csv_formatter_service.rb +++ b/app/services/csv_formatter_service.rb @@ -9,7 +9,8 @@ def generate CSV.generate(headers: true) do |csv| csv << headers - @avaliacao.respostas.includes(:aluno).group_by(&:aluno).each do |aluno, respostas| + @avaliacao.submissoes.includes(:aluno, :respostas).each do |submissao| + aluno = submissao.aluno row = [aluno.matricula, aluno.nome] # Organiza as respostas pela ordem das questões se possível, ou mapeamento simples @@ -18,7 +19,7 @@ def generate # 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| + submissao.respostas.each do |resposta| row << resposta.conteudo end diff --git a/app/services/sigaa_import_service.rb b/app/services/sigaa_import_service.rb index ad82709c80..2a3995bc15 100644 --- a/app/services/sigaa_import_service.rb +++ b/app/services/sigaa_import_service.rb @@ -7,8 +7,9 @@ def initialize(file_path) @results = { turmas_created: 0, turmas_updated: 0, - usuarios_created: 0, - usuarios_updated: 0, + users_created: 0, + users_updated: 0, + new_users: [], # Array de hashes com credenciais dos novos usuários errors: [] } end @@ -48,10 +49,42 @@ def process private def process_json - file_content = File.read(@file_path) - data = JSON.parse(file_content) + data = JSON.parse(File.read(@file_path)) + + # class_members.json é um array de turmas data.each do |turma_data| - process_turma(turma_data) + # 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 + 'semestre' => turma_data['semester'], + 'participantes' => [] + } + + # Processa dicentes (alunos) + if turma_data['dicente'] + turma_data['dicente'].each do |dicente| + normalized_data['participantes'] << { + 'nome' => dicente['nome'], + 'email' => dicente['email'], + 'matricula' => dicente['matricula'] || dicente['usuario'], + 'papel' => 'Discente' + } + end + end + + # Processa docente (professor) + if turma_data['docente'] + docente = turma_data['docente'] + normalized_data['participantes'] << { + 'nome' => docente['nome'], + 'email' => docente['email'], + 'matricula' => docente['usuario'], + 'papel' => 'Docente' + } + end + + process_turma(normalized_data) end end @@ -111,12 +144,15 @@ def process_participantes(turma, participantes_data) end def process_participante_single(turma, p_data) - # Usuario identificado pela matrícula + # User identificado pela matrícula user = User.find_or_initialize_by(matricula: p_data['matricula']) is_new_user = user.new_record? user.nome = p_data['nome'] - user.email = p_data['email'] + user.email_address = p_data['email'] + + # Generate login from matricula if not present (assuming matricula is unique and good for login) + user.login = p_data['matricula'] if user.login.blank? generated_password = nil if is_new_user @@ -126,10 +162,21 @@ def process_participante_single(turma, p_data) if user.save if is_new_user - @results[:usuarios_created] += 1 - UserMailer.cadastro_email(user, generated_password).deliver_now + @results[:users_created] += 1 + + # Armazena credenciais do novo usuário para exibir depois + @results[:new_users] << { + matricula: user.matricula, + nome: user.nome, + login: user.login, + password: generated_password, + email: user.email_address + } + + # Envia email com senha para novo usuário (COMENTADO - muito lento) + # UserMailer.cadastro_email(user, generated_password).deliver_now else - @results[:usuarios_updated] += 1 + @results[:users_updated] += 1 end matricula = MatriculaTurma.find_or_initialize_by(turma: turma, user: user) diff --git a/app/views/avaliacoes/gestao_envios.html.erb b/app/views/avaliacoes/gestao_envios.html.erb index c26f95dd4f..418a0b3d9f 100644 --- a/app/views/avaliacoes/gestao_envios.html.erb +++ b/app/views/avaliacoes/gestao_envios.html.erb @@ -60,7 +60,10 @@ <% 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" %> + + Ver Resultados (Última) +
<% end %> diff --git a/app/views/avaliacoes/index.html.erb b/app/views/avaliacoes/index.html.erb index 709d476d6b..1db9241a8d 100644 --- a/app/views/avaliacoes/index.html.erb +++ b/app/views/avaliacoes/index.html.erb @@ -1,4 +1,61 @@ -
-

Avaliacoes#index

-

Find me in app/views/avaliacoes/index.html.erb

+
+
+

Minhas Turmas

+ + <% if @turmas.any? %> +
+ <% @turmas.each do |turma| %> +
+
+
+

+ <%= turma.nome %> +

+

+ <%= turma.codigo %> - <%= turma.semestre %> +

+
+ + <% avaliacao_pendente = turma.avaliacoes.where('data_fim IS NULL OR data_fim >= ?', Time.current) + .where.not(id: current_user.submissoes.select(:avaliacao_id)) + .first %> + + <% if avaliacao_pendente %> + + Avaliação Pendente + + <% end %> +
+ + <% if avaliacao_pendente %> + <% prazo = avaliacao_pendente.data_fim %> +

+ <% if prazo %> + Prazo: <%= prazo.strftime('%d/%m/%Y') %> + <% else %> + Sem prazo definido + <% end %> +

+ + <%= link_to "Responder Avaliação", + new_avaliacao_resposta_path(avaliacao_pendente), + class: "block w-full text-center bg-project-green text-white py-2 px-4 rounded-md hover:bg-green-600 transition-colors font-medium" %> + <% else %> +

+ Nenhuma avaliação pendente +

+ <% end %> +
+ <% end %> +
+ <% else %> +
+ + + +

Nenhuma turma encontrada

+

Você ainda não está matriculado em nenhuma turma.

+
+ <% end %> +
diff --git a/app/views/avaliacoes/resultados.html.erb b/app/views/avaliacoes/resultados.html.erb index 3f17134a77..616a8bb53c 100644 --- a/app/views/avaliacoes/resultados.html.erb +++ b/app/views/avaliacoes/resultados.html.erb @@ -10,7 +10,7 @@

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

- <% if @respostas.any? %> + <% if @submissoes.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" %>
@@ -21,15 +21,15 @@ Matrícula Aluno - Conteúdo + Envio - <% @respostas.each do |resposta| %> + <% @submissoes.each do |submissao| %> - <%= resposta.aluno&.matricula %> - <%= resposta.aluno&.nome %> - <%= resposta.conteudo %> + <%= submissao.aluno&.matricula %> + <%= submissao.aluno&.nome %> + <%= submissao.respostas.count %> respostas <% end %> diff --git a/app/views/components/_card.html.erb b/app/views/components/_card.html.erb index 49998a35a5..3c00179059 100644 --- a/app/views/components/_card.html.erb +++ b/app/views/components/_card.html.erb @@ -1,23 +1,35 @@ -
-
-
-

Nome da matéria

-

semestre

-

Professor

-
+<%# + Card component para exibir avaliações + Uso: render 'components/card', turma: @turma, avaliacao: @avaliacao +%> +
+
+
+

<%= local_assigns[:turma]&.nome || 'Nome da turma' %>

+

<%= local_assigns[:turma]&.semestre || 'Semestre' %>

+

+ <%= local_assigns[:professor]&.nome || local_assigns[:avaliacao]&.professor_alvo&.nome || 'Professor' %> +

+
-
- + <% if local_assigns[:show_actions] %> +
+ <% if local_assigns[:edit_url] %> + <%= link_to local_assigns[:edit_url], class: "text-black hover:text-blue-600 transition-colors duration-200" do %> + + + + <% end %> + <% end %> - -
-
+ <% if local_assigns[:delete_url] %> + <%= button_to local_assigns[:delete_url], method: :delete, data: { confirm: 'Tem certeza?' }, class: "text-black hover:text-red-600 transition-colors duration-200" do %> + + + + <% end %> + <% end %> +
+ <% end %> +
\ No newline at end of file diff --git a/app/views/components/_dashBoardAdmin.html.erb b/app/views/components/_dashBoardAdmin.html.erb index ff92b35ac1..d590155a30 100644 --- a/app/views/components/_dashBoardAdmin.html.erb +++ b/app/views/components/_dashBoardAdmin.html.erb @@ -1,10 +1,10 @@
- - - - + <%= link_to "Importar dados", new_sigaa_import_path, class: "flex items-center justify-center bg-project-secondary-green hover:bg-project-green cursor-pointer w-[302px] h-[43px] no-underline text-white" %> + <%= link_to "Editar Templates", modelos_path, class: "flex items-center justify-center bg-project-secondary-green hover:bg-project-green cursor-pointer w-[302px] h-[43px] no-underline text-white" %> + <%= link_to "Enviar Formularios", gestao_envios_avaliacoes_path, class: "flex items-center justify-center bg-project-secondary-green hover:bg-project-green cursor-pointer w-[302px] h-[43px] no-underline text-white" %> + <%= link_to "Resultados", gestao_envios_avaliacoes_path, class: "flex items-center justify-center bg-project-secondary-green hover:bg-project-green cursor-pointer w-[302px] h-[43px] no-underline text-white" %>
\ No newline at end of file diff --git a/app/views/components/_frameBrancoMenuLateral.html.erb b/app/views/components/_frameBrancoMenuLateral.html.erb index 3aee7e7872..de705b7d8f 100644 --- a/app/views/components/_frameBrancoMenuLateral.html.erb +++ b/app/views/components/_frameBrancoMenuLateral.html.erb @@ -1,15 +1,13 @@ - \ No newline at end of file +<%= link_to "Gerenciamento", root_path, + class: "flex items-center justify-center + w-[257px] h-[46px] + bg-white + text-black + font-roboto + py-[11px] px-[50px] + border-b border-transparent + shadow-lg + rounded + cursor-pointer + gap-[10px] opacity-100 hover:bg-gray-100 no-underline" +%> \ No newline at end of file diff --git a/app/views/components/_frameRoxoMenuLateral.html.erb b/app/views/components/_frameRoxoMenuLateral.html.erb index 19c88d5282..56b3971a14 100644 --- a/app/views/components/_frameRoxoMenuLateral.html.erb +++ b/app/views/components/_frameRoxoMenuLateral.html.erb @@ -1,5 +1,5 @@ - \ No newline at end of file + gap-[10px] opacity-100 hover:bg-project-purple-dark no-underline" +%> \ No newline at end of file diff --git a/app/views/components/_header.html.erb b/app/views/components/_header.html.erb index b3305a0356..03def9e420 100644 --- a/app/views/components/_header.html.erb +++ b/app/views/components/_header.html.erb @@ -68,7 +68,7 @@