From 5c9ff773aaff46187320c457177cb6af71bf6801 Mon Sep 17 00:00:00 2001 From: vmg192 Date: Mon, 15 Dec 2025 23:06:50 -0300 Subject: [PATCH] =?UTF-8?q?Adiciona=20documenta=C3=A7=C3=A3o=20RDoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 9 +++ app/controllers/application_controller.rb | 6 +- app/controllers/avaliacoes_controller.rb | 25 ++++--- app/controllers/concerns/authenticatable.rb | 9 ++- app/controllers/concerns/authentication.rb | 80 +++++++++++++-------- app/controllers/home_controller.rb | 3 + app/controllers/modelos_controller.rb | 40 ++++++++--- app/controllers/pages_controller.rb | 6 +- app/controllers/passwords_controller.rb | 24 +++++-- app/controllers/respostas_controller.rb | 24 ++++--- app/controllers/sessions_controller.rb | 10 ++- app/controllers/sigaa_imports_controller.rb | 25 ++++--- app/models/application_record.rb | 1 + app/models/avaliacao.rb | 2 + app/models/current.rb | 1 + app/models/matricula_turma.rb | 1 + app/models/modelo.rb | 46 +++--------- app/models/pergunta.rb | 76 +++++++++----------- app/models/resposta.rb | 6 +- app/models/session.rb | 1 + app/models/submissao.rb | 3 +- app/models/turma.rb | 1 + app/models/user.rb | 3 +- app/services/csv_formatter_service.rb | 19 ++--- app/services/sigaa_import_service.rb | 47 +++++++----- 26 files changed, 278 insertions(+), 191 deletions(-) diff --git a/.gitignore b/.gitignore index ffa8b8a4b1..a3579ddf70 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ !/app/assets/builds/.keep .idea/ +doc/ diff --git a/README.md b/README.md index af77f53a93..589cda5611 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,12 @@ bundle exec cucumber --tags "not @wip" bundle exec cucumber features/sistema_login.feature ``` +## Documentação + +```bash +# Gerar documentação RDoc +rdoc app/controllers app/models app/services --output doc + +# Abrir no navegador +firefox doc/index.html +``` diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4763b17ed1..ed54ba8fd1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,9 +1,13 @@ +# Controlador base para todos os controladores da aplicação +# Inclui autenticação e compatibilidade de navegador 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. + # Apenas permite navegadores modernos allow_browser versions: :modern + # Ação padrão do index + # @return [void] def index end end diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb index 8ff5e9b7dd..010108c713 100644 --- a/app/controllers/avaliacoes_controller.rb +++ b/app/controllers/avaliacoes_controller.rb @@ -1,26 +1,29 @@ +# Gerencia avaliações de turmas +# Listagem, criação e visualização de resultados class AvaliacoesController < ApplicationController - # Requer autenticação para todas as actions - + # Lista avaliações ou turmas do aluno + # @return [void] Renderiza index ou redireciona para login def index - # Se for admin, mostrar todas as avaliações - # Se for aluno, mostrar todas as turmas matriculadas - @turmas = [] # Inicializa como array vazio por padrão + @turmas = [] 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 + # View de gestão de envios (admin) + # @return [void] Renderiza view de gestão def gestao_envios @turmas = Turma.all end + # Cria nova avaliação para turma + # @return [void] Redireciona com mensagem de sucesso/erro + # @efeito_colateral Cria registro Avaliacao no banco def create turma_id = params[:turma_id] turma = Turma.find_by(id: turma_id) @@ -51,9 +54,10 @@ def create end end + # Exibe resultados com estatísticas + # @return [void] Renderiza HTML ou download CSV def resultados @avaliacao = Avaliacao.find(params[:id]) - # Pré-carrega dependências para evitar N+1. begin @submissoes = @avaliacao.submissoes.includes(:aluno, :respostas) @perguntas = @avaliacao.modelo.perguntas.order(:id) @@ -76,6 +80,9 @@ def resultados private + # Constrói hash de estatísticas por pergunta + # @param avaliacao [Avaliacao] Avaliação para analisar + # @return [Hash] Estatísticas por ID da pergunta def build_question_statistics(avaliacao) avaliacao.modelo.perguntas.each_with_object({}) do |pergunta, stats| respostas = Resposta.joins(:submissao) @@ -83,7 +90,6 @@ def build_question_statistics(avaliacao) .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, @@ -91,7 +97,6 @@ def build_question_statistics(avaliacao) responses: [] } else - # Para texto, inclui as respostas para exibição text_responses = respostas.pluck(:conteudo).compact.reject(&:blank?) stats[pergunta.id] = { type: pergunta.tipo, diff --git a/app/controllers/concerns/authenticatable.rb b/app/controllers/concerns/authenticatable.rb index 3669bd8cfc..869c574b27 100644 --- a/app/controllers/concerns/authenticatable.rb +++ b/app/controllers/concerns/authenticatable.rb @@ -1,4 +1,5 @@ -# app/controllers/concerns/authenticatable.rb +# Módulo com helpers de autenticação +# Fornece current_user e user_signed_in? module Authenticatable extend ActiveSupport::Concern @@ -6,14 +7,20 @@ module Authenticatable helper_method :current_user, :user_signed_in? end + # Requer login ou redireciona + # @return [void] def authenticate_user! redirect_to new_session_path, alert: "É necessário fazer login." unless user_signed_in? end + # Retorna usuário atual logado + # @return [User, nil] def current_user Current.session&.user end + # Verifica se há usuário logado + # @return [Boolean] def user_signed_in? current_user.present? end diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 3538f485c5..3bfb4ec55f 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -1,3 +1,5 @@ +# Módulo de autenticação para sessões +# Gerencia login, logout e verificação de autenticação module Authentication extend ActiveSupport::Concern @@ -7,46 +9,68 @@ module Authentication end class_methods do + # Permite acesso sem autenticação + # @param options [Hash] Opções para skip_before_action def allow_unauthenticated_access(**options) skip_before_action :require_authentication, **options end end private - def authenticated? - resume_session - end - def require_authentication - resume_session || request_authentication - end + # Verifica se usuário está autenticado + # @return [Boolean] + def authenticated? + resume_session + end - def resume_session - Current.session ||= find_session_by_cookie - end + # Requer autenticação ou redireciona + # @return [Session, nil] + def require_authentication + resume_session || request_authentication + end - def find_session_by_cookie - Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id] - end + # Retoma sessão existente + # @return [Session, nil] + def resume_session + Current.session ||= find_session_by_cookie + end - def request_authentication - session[:return_to_after_authenticating] = request.url - redirect_to new_session_path - end + # Encontra sessão pelo cookie + # @return [Session, nil] + def find_session_by_cookie + Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id] + end - def after_authentication_url - session.delete(:return_to_after_authenticating) || root_url - end + # Redireciona para login + # @return [void] + def request_authentication + session[:return_to_after_authenticating] = request.url + redirect_to new_session_path + end - def start_new_session_for(user) - user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session| - Current.session = session - cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax } - end - end + # URL para redirecionar após login + # @return [String] + def after_authentication_url + session.delete(:return_to_after_authenticating) || root_url + end - def terminate_session - Current.session.destroy - cookies.delete(:session_id) + # Inicia nova sessão para usuário + # @param user [User] Usuário para autenticar + # @return [Session] Sessão criada + # @efeito_colateral Cria Session e define cookie + def start_new_session_for(user) + user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session| + Current.session = session + cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax } end + end + + # Encerra sessão atual + # @return [void] + # @efeito_colateral Destrói Session e remove cookie + def terminate_session + Current.session.destroy + cookies.delete(:session_id) + end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 95f29929ca..ea757cdff4 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,4 +1,7 @@ +# Controlador legado (redirecionamentos tratados por PagesController) class HomeController < ApplicationController + # Ação home padrão + # @return [void] def index end end diff --git a/app/controllers/modelos_controller.rb b/app/controllers/modelos_controller.rb index 6115403c38..63473a749c 100644 --- a/app/controllers/modelos_controller.rb +++ b/app/controllers/modelos_controller.rb @@ -1,42 +1,50 @@ -# app/controllers/modelos_controller.rb +# Gerencia templates de avaliação com perguntas +# Acesso restrito a administradores class ModelosController < ApplicationController before_action :require_admin before_action :set_modelo, only: [ :show, :edit, :update, :destroy, :clone ] - # GET /modelos + # Lista todos os templates + # @return [void] Renderiza index com modelos def index @modelos = Modelo.includes(:perguntas).order(created_at: :desc) end - # GET /modelos/1 + # Exibe detalhes do template + # @return [void] Renderiza view show def show end - # GET /modelos/new + # Formulário para novo template + # @return [void] Renderiza form com 3 perguntas em branco def new @modelo = Modelo.new - 3.times { @modelo.perguntas.build } # Cria 3 perguntas em branco por padrão + 3.times { @modelo.perguntas.build } end - # GET /modelos/1/edit + # Formulário para editar template + # @return [void] Renderiza form de edição def edit @modelo.perguntas.build if @modelo.perguntas.empty? end - # POST /modelos + # Cria novo template + # @return [void] Redireciona para show ou renderiza form com erros + # @efeito_colateral Cria registros Modelo e Pergunta 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 + # Atualiza template existente + # @return [void] Redireciona para show ou renderiza form com erros + # @efeito_colateral Atualiza registros Modelo e Pergunta def update if @modelo.update(modelo_params) redirect_to @modelo, notice: "Modelo atualizado com sucesso." @@ -45,7 +53,9 @@ def update end end - # DELETE /modelos/1 + # Exclui template se não estiver em uso + # @return [void] Redireciona para index com mensagem + # @efeito_colateral Destrói registro Modelo se não em uso def destroy if @modelo.em_uso? redirect_to modelos_url, alert: "Não é possível excluir um modelo que está em uso." @@ -55,7 +65,9 @@ def destroy end end - # POST /modelos/1/clone + # Duplica template com todas as perguntas + # @return [void] Redireciona para editar template clonado + # @efeito_colateral Cria novo Modelo com Perguntas copiadas def clone novo_titulo = "#{@modelo.titulo} (Cópia)" novo_modelo = @modelo.clonar_com_perguntas(novo_titulo) @@ -70,10 +82,14 @@ def clone private + # Encontra modelo por ID + # @return [Modelo] def set_modelo @modelo = Modelo.find(params[:id]) end + # Parâmetros permitidos para modelo + # @return [ActionController::Parameters] def modelo_params params.require(:modelo).permit( :titulo, @@ -88,6 +104,8 @@ def modelo_params ) end + # Verifica se usuário é admin + # @return [void] Redireciona não-admins def require_admin unless Current.session&.user&.eh_admin? redirect_to root_path, alert: "Acesso restrito a administradores." diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 5794ab5b6a..a374ff8fc2 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -1,13 +1,13 @@ +# Controlador principal para views de dashboard class PagesController < ApplicationController layout "application" + # Exibe dashboard principal + # @return [void] Redireciona alunos para avaliações, renderiza dashboard para admins def index - # Feature 109: Redirecionar alunos para ver suas turmas if Current.session&.user && !Current.session.user.eh_admin? redirect_to avaliacoes_path nil end - - # Admins veem dashboard admin end end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 8c765297dd..13d71f68a6 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -1,11 +1,17 @@ +# Gerencia recuperação de senha class PasswordsController < ApplicationController layout "login" allow_unauthenticated_access before_action :set_user_by_token, only: %i[ edit update ] + # Formulário para solicitar reset + # @return [void] Renderiza form de email def new end + # Envia email de reset de senha + # @return [void] Redireciona para login com aviso + # @efeito_colateral Envia email se usuário existir def create if user = User.find_by(email_address: params[:email_address]) PasswordsMailer.reset(user).deliver_later @@ -14,9 +20,14 @@ def create redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)." end + # Formulário para nova senha + # @return [void] Renderiza form de senha def edit end + # Atualiza senha do usuário + # @return [void] Redireciona para login em sucesso, form em erro + # @efeito_colateral Atualiza senha no banco def update if @user.update(params.permit(:password, :password_confirmation)) redirect_to new_session_path, notice: "Password has been reset." @@ -26,9 +37,12 @@ def update end private - def set_user_by_token - @user = User.find_by_password_reset_token!(params[:token]) - rescue ActiveSupport::MessageVerifier::InvalidSignature - redirect_to new_password_path, alert: "Password reset link is invalid or has expired." - end + + # Encontra usuário pelo token de reset + # @return [User] Usuário correspondente ao token + def set_user_by_token + @user = User.find_by_password_reset_token!(params[:token]) + rescue ActiveSupport::MessageVerifier::InvalidSignature + redirect_to new_password_path, alert: "Password reset link is invalid or has expired." + end end diff --git a/app/controllers/respostas_controller.rb b/app/controllers/respostas_controller.rb index 0f00bc81ea..9a1d50d90d 100644 --- a/app/controllers/respostas_controller.rb +++ b/app/controllers/respostas_controller.rb @@ -1,34 +1,36 @@ -# app/controllers/respostas_controller.rb +# Gerencia respostas de avaliações pelos alunos class RespostasController < ApplicationController before_action :authenticate_user! before_action :set_avaliacao, only: [ :new, :create ] before_action :verificar_disponibilidade, only: [ :new, :create ] before_action :verificar_nao_respondeu, only: [ :new, :create ] + # Redireciona para root + # @return [void] Redirect para página principal def index - # Feature 109: Listagem de avaliações pendentes (já implementado em pages#index) redirect_to root_path end + # Exibe formulário de resposta + # @return [void] Renderiza form com perguntas def 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 + # Salva respostas da avaliação + # @return [void] Redireciona para root em sucesso, form em erro + # @efeito_colateral Cria registros Submissao e Resposta def create - # 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) @@ -50,12 +52,15 @@ def create private + # Encontra avaliação por ID + # @return [Avaliacao] def set_avaliacao @avaliacao = Avaliacao.find(params[:avaliacao_id]) end + # Verifica se avaliação está no prazo + # @return [void] Redireciona se expirada ou não iniciada def verificar_disponibilidade - # 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 @@ -63,13 +68,16 @@ def verificar_disponibilidade end end + # Verifica se aluno já respondeu + # @return [void] Redireciona se já respondeu def verificar_nao_respondeu - # 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 + # Parâmetros permitidos para submissão + # @return [ActionController::Parameters] def submissao_params params.require(:submissao).permit( respostas_attributes: [ :pergunta_id, :conteudo, :snapshot_enunciado, :snapshot_opcoes ] diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index ac65c1deb4..a276d16542 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,13 +1,18 @@ +# Gerencia sessões de autenticação (login/logout) class SessionsController < ApplicationController layout "login" allow_unauthenticated_access only: %i[ new create ] rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." } + # Exibe formulário de login + # @return [void] Renderiza view de login def new end + # Autentica usuário por email ou login + # @return [void] Redireciona para root em sucesso, login em falha + # @efeito_colateral Cria registro Session em sucesso def create - # 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]) @@ -19,6 +24,9 @@ def create end end + # Encerra sessão do usuário + # @return [void] Redireciona para página de login + # @efeito_colateral Destrói registro Session atual def destroy terminate_session redirect_to new_session_path diff --git a/app/controllers/sigaa_imports_controller.rb b/app/controllers/sigaa_imports_controller.rb index 40dff6936f..56a385b280 100644 --- a/app/controllers/sigaa_imports_controller.rb +++ b/app/controllers/sigaa_imports_controller.rb @@ -1,13 +1,17 @@ +# Gerencia importação de dados SIGAA +# Acesso restrito a administradores class SigaaImportsController < ApplicationController - # Apenas administradores podem importar dados before_action :require_admin + # Formulário de importação + # @return [void] Renderiza form de import def new - # Exibe formulário de upload end + # Importa dados SIGAA do class_members.json + # @return [void] Redireciona para sucesso ou volta com erros + # @efeito_colateral Cria registros Turma, User, MatriculaTurma 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") @@ -16,7 +20,6 @@ def create return end - # Processa a importação (passa classes.json se existir) service = SigaaImportService.new(file_path, classes_file_path) @results = service.process @@ -24,16 +27,16 @@ def create 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 + # Atualiza banco com dados SIGAA + # @return [void] Redireciona para sucesso ou volta com erros + # @efeito_colateral Atualiza/cria registros Turma, User, MatriculaTurma 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") @@ -49,14 +52,15 @@ def update 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 + # Exibe resultados da importação + # @return [void] Renderiza view ou redireciona se sem resultados + # @efeito_colateral Limpa cache após exibição def success cache_key = params[:key] @results = Rails.cache.read(cache_key) if cache_key @@ -66,12 +70,13 @@ def success return end - # Limpa o cache após carregar (usuário já viu) Rails.cache.delete(cache_key) end private + # Verifica se usuário é admin + # @return [void] Redireciona não-admins def require_admin unless Current.session&.user&.eh_admin? redirect_to root_path, alert: "Acesso negado. Apenas administradores podem importar dados." diff --git a/app/models/application_record.rb b/app/models/application_record.rb index b63caeb8a5..0f963e56a1 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,4 @@ +# Classe base para todos os models da aplicação class ApplicationRecord < ActiveRecord::Base primary_abstract_class end diff --git a/app/models/avaliacao.rb b/app/models/avaliacao.rb index 64a3d035d4..7a6282e431 100644 --- a/app/models/avaliacao.rb +++ b/app/models/avaliacao.rb @@ -1,3 +1,5 @@ +# Representa uma avaliação de uma turma +# Vincula turma a um modelo de formulário class Avaliacao < ApplicationRecord belongs_to :turma belongs_to :modelo diff --git a/app/models/current.rb b/app/models/current.rb index 2bef56dada..08db8b6d72 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -1,3 +1,4 @@ +# Armazena atributos da requisição atual (sessão, usuário) class Current < ActiveSupport::CurrentAttributes attribute :session delegate :user, to: :session, allow_nil: true diff --git a/app/models/matricula_turma.rb b/app/models/matricula_turma.rb index 6e658baf36..b92aa66057 100644 --- a/app/models/matricula_turma.rb +++ b/app/models/matricula_turma.rb @@ -1,3 +1,4 @@ +# Representa a matrícula de um usuário em uma turma class MatriculaTurma < ApplicationRecord belongs_to :user belongs_to :turma diff --git a/app/models/modelo.rb b/app/models/modelo.rb index baa7bd355e..081b2ba230 100644 --- a/app/models/modelo.rb +++ b/app/models/modelo.rb @@ -1,46 +1,32 @@ -# Classe que representa um modelo de questionário. -# Serve como agregador de perguntas e definições da avaliação. +# Representa um template de formulário com perguntas class Modelo < ApplicationRecord - # 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 - # Verifica se o modelo já foi utilizado em alguma avaliação. - # - # Descrição: Checa se existem registros na tabela de avaliações associados a este modelo. - # Argumentos: Nenhum. - # Retorno: Boolean (true se estiver em uso, false caso contrário). - # Efeitos Colaterais: Nenhum (apenas leitura). + # Verifica se modelo está em uso + # @return [Boolean] true se há avaliações usando este modelo def em_uso? avaliacoes.any? end - # Cria uma cópia profunda do modelo e suas perguntas. - # - # Descrição: Duplica o objeto Modelo e itera sobre suas perguntas para duplicá-las também, - # associando-as ao novo modelo. O novo modelo nasce inativo. - # Argumentos: - # - novo_titulo (String): O título que será atribuído ao novo modelo clonado. - # Retorno: Objeto Modelo (a nova instância criada e salva). - # Efeitos Colaterais: - # - Cria um novo registro na tabela 'modelos'. - # - Cria novos registros na tabela 'perguntas'. + # Duplica modelo com todas as perguntas + # @param novo_titulo [String] Título para o novo modelo + # @return [Modelo] Novo modelo criado + # @efeito_colateral Cria novo Modelo e Perguntas no banco def clonar_com_perguntas(novo_titulo) novo_modelo = dup novo_modelo.titulo = novo_titulo - novo_modelo.ativo = false # Clones começam inativos + novo_modelo.ativo = false + novo_modelo.save # Copia as perguntas para a memória antes de salvar para passar na validação perguntas.each do |pergunta| @@ -54,24 +40,14 @@ def clonar_com_perguntas(novo_titulo) private - # Valida se o modelo possui perguntas na criação. - # - # Descrição: Garante a regra de negócio de que um modelo não pode existir vazio. - # Argumentos: Nenhum. - # Retorno: Adiciona erro ao objeto se falhar. - # Efeitos Colaterais: Nenhum. + # Valida que modelo tenha pelo menos uma pergunta 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 - # Valida se o usuário está tentando remover todas as perguntas na edição. - # - # Descrição: Impede que um update deixe o modelo órfão de perguntas. - # Argumentos: Nenhum. - # Retorno: Adiciona erro ao objeto se falhar. - # Efeitos Colaterais: Nenhum. + # Valida que não remova todas as perguntas 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") diff --git a/app/models/pergunta.rb b/app/models/pergunta.rb index b17f1fcf6c..9e7c631952 100644 --- a/app/models/pergunta.rb +++ b/app/models/pergunta.rb @@ -1,8 +1,7 @@ -# Classe que representa uma pergunta associada a um modelo de questionário. +# Representa uma pergunta de um modelo de avaliação class Pergunta < ApplicationRecord self.table_name = "perguntas" - # Relacionamentos belongs_to :modelo has_many :respostas, foreign_key: "questao_id", dependent: :destroy @@ -17,42 +16,30 @@ class Pergunta < ApplicationRecord "hora" => "Hora" }.freeze - # Validações validates :enunciado, presence: true validates :tipo, presence: true, inclusion: { in: TIPOS.keys } - # Validações condicionais - validate :validar_minimo_opcoes, if: :requer_opcoes? + validate :opcoes_requeridas_para_multipla_escolha + validate :opcoes_requeridas_para_checkbox - # Retorna o nome legível do tipo da pergunta. - # - # Descrição: Converte a chave do tipo (ex: 'texto_curto') para o valor legível (ex: 'Texto Curto'). - # Argumentos: Nenhum. - # Retorno: String (O nome formatado do tipo). - # Efeitos Colaterais: Nenhum. + before_validation :definir_ordem_padrao, on: :create + + # Retorna tipo legível + # @return [String] def tipo_humanizado TIPOS[tipo] || tipo end - # Verifica se o tipo de pergunta exige opções de resposta. - # - # Descrição: Checa se a pergunta é do tipo múltipla escolha ou checkbox. - # Argumentos: Nenhum. - # Retorno: Boolean. - # Efeitos Colaterais: Nenhum. + # Verifica se tipo requer opções + # @return [Boolean] def requer_opcoes? %w[multipla_escolha checkbox].include?(tipo) end - # Processa e retorna as opções da pergunta. - # - # Descrição: Normaliza o campo 'opcoes', lidando com Array, String JSON ou String separada por ponto e vírgula. - # Argumentos: Nenhum. - # Retorno: Array (Lista de strings com as opções). - # Efeitos Colaterais: Pode realizar parse de JSON. + # Retorna lista de opções + # @return [Array] def lista_opcoes return [] unless opcoes.present? - if opcoes.is_a?(Array) opcoes elsif opcoes.is_a?(String) @@ -64,30 +51,33 @@ def lista_opcoes private - # Auxiliar para converter string de opções em array. - # - # Descrição: Tenta parsear JSON, se falhar, usa split por ';'. - # Argumentos: Nenhum (usa atributo interno). - # Retorno: Array de strings. - # Efeitos Colaterais: Nenhum. - def parse_opcoes_string - JSON.parse(opcoes) - rescue JSON::ParserError - opcoes.split(";").map(&:strip) + def definir_ordem_padrao + if modelo.present? + ultima_ordem = modelo.perguntas.maximum(:id) || 0 + end end - # Validação de quantidade mínima de opções. - # - # Descrição: Garante que perguntas de múltipla escolha tenham ao menos 2 alternativas. - # Argumentos: Nenhum. - # Retorno: Adiciona erro ao objeto se falhar. - # Efeitos Colaterais: Nenhum. - def validar_minimo_opcoes + def opcoes_requeridas_para_multipla_escolha + return unless 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 + + def opcoes_requeridas_para_checkbox + return unless tipo == "checkbox" + opcoes_lista = lista_opcoes if opcoes_lista.blank? || opcoes_lista.size < 2 - nome_tipo = tipo == "multipla_escolha" ? "múltipla escolha" : "checkbox" - errors.add(:opcoes, "deve ter pelo menos duas opções para #{nome_tipo}") + errors.add(:opcoes, "deve ter pelo menos duas opções para checkbox") end end + + def parse_opcoes_string + JSON.parse(opcoes) + rescue JSON::ParserError + opcoes.split(";").map(&:strip) + end end diff --git a/app/models/resposta.rb b/app/models/resposta.rb index 02623d7d9d..f690112ffe 100644 --- a/app/models/resposta.rb +++ b/app/models/resposta.rb @@ -1,11 +1,11 @@ +# Representa a resposta de um aluno a uma pergunta class Resposta < ApplicationRecord - self.table_name = "respostas" # Plural correto em português + self.table_name = "respostas" belongs_to :submissao - belongs_to :pergunta, foreign_key: "questao_id" # Coluna ainda é questao_id no banco + belongs_to :pergunta, foreign_key: "questao_id" validates :conteudo, presence: true - # Alias para compatibilidade alias_attribute :pergunta_id, :questao_id end diff --git a/app/models/session.rb b/app/models/session.rb index cf376fb280..a613cf6cf6 100644 --- a/app/models/session.rb +++ b/app/models/session.rb @@ -1,3 +1,4 @@ +# Representa uma sessão de login do usuário class Session < ApplicationRecord belongs_to :user end diff --git a/app/models/submissao.rb b/app/models/submissao.rb index 8d65a1f31b..46531fbef5 100644 --- a/app/models/submissao.rb +++ b/app/models/submissao.rb @@ -1,5 +1,6 @@ +# Representa uma submissão de avaliação por um aluno class Submissao < ApplicationRecord - self.table_name = "submissoes" # Plural correto em português + self.table_name = "submissoes" belongs_to :aluno, class_name: "User" belongs_to :avaliacao diff --git a/app/models/turma.rb b/app/models/turma.rb index c40813c272..27cd3d645c 100644 --- a/app/models/turma.rb +++ b/app/models/turma.rb @@ -1,3 +1,4 @@ +# Representa uma turma/disciplina do SIGAA class Turma < ApplicationRecord has_many :avaliacoes has_many :matricula_turmas diff --git a/app/models/user.rb b/app/models/user.rb index a2ef63d982..b4dd20ec07 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,4 @@ -# Classe que representa um usuário do sistema. -# Responsável pela autenticação, dados cadastrais e relação com turmas e submissões. +# Representa um usuário do sistema (admin, professor ou aluno) class User < ApplicationRecord # Adiciona métodos para definir e autenticar senhas usando BCrypt. # diff --git a/app/services/csv_formatter_service.rb b/app/services/csv_formatter_service.rb index 20744f31e7..1eccc258e7 100644 --- a/app/services/csv_formatter_service.rb +++ b/app/services/csv_formatter_service.rb @@ -1,24 +1,22 @@ require "csv" +# Serviço para gerar CSV com resultados de avaliação class CsvFormatterService + # Inicializa com avaliação + # @param avaliacao [Avaliacao] Avaliação para exportar def initialize(avaliacao) @avaliacao = avaliacao end + # Gera string CSV com respostas + # @return [String] Conteúdo CSV formatado def generate CSV.generate(headers: true) do |csv| csv << headers @avaliacao.submissoes.includes(:aluno, :respostas).each do |submissao| - # 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 - - # 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 - submissao.respostas.each do |resposta| row << resposta.conteudo end @@ -30,14 +28,11 @@ def generate private + # Gera cabeçalhos do CSV + # @return [Array] Lista de cabeçalhos def headers - # 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 - # 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}" } diff --git a/app/services/sigaa_import_service.rb b/app/services/sigaa_import_service.rb index 967f9f78ec..c8ad956652 100644 --- a/app/services/sigaa_import_service.rb +++ b/app/services/sigaa_import_service.rb @@ -1,7 +1,12 @@ require "json" require "csv" +# Serviço para importar dados do SIGAA +# Processa JSON ou CSV com turmas e usuários class SigaaImportService + # Inicializa serviço + # @param file_path [Pathname] Caminho do arquivo class_members.json + # @param classes_file_path [Pathname] Caminho do arquivo classes.json (opcional) def initialize(file_path, classes_file_path = nil) @file_path = file_path @classes_file_path = classes_file_path @@ -10,11 +15,14 @@ def initialize(file_path, classes_file_path = nil) turmas_updated: 0, users_created: 0, users_updated: 0, - new_users: [], # Array de hashes com credenciais dos novos usuários + new_users: [], errors: [] } end + # Processa arquivo e importa dados + # @return [Hash] Resultados com contagens e erros + # @efeito_colateral Cria/atualiza Turma, User, MatriculaTurma def process unless File.exist?(@file_path) @results[:errors] << "Arquivo não encontrado: #{@file_path}" @@ -49,17 +57,16 @@ def process private + # Processa arquivo JSON + # @return [void] 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" => class_name, @@ -67,7 +74,6 @@ def process_json "participantes" => [] } - # Processa dicentes (alunos) if turma_data["dicente"] turma_data["dicente"].each do |dicente| normalized_data["participantes"] << { @@ -79,7 +85,6 @@ def process_json end end - # Processa docente (professor) if turma_data["docente"] docente = turma_data["docente"] normalized_data["participantes"] << { @@ -94,14 +99,14 @@ def process_json end end - # Constrói um hash de lookup para nomes de turmas a partir de classes.json + # Constrói lookup de nomes de turmas + # @return [Hash] Mapa code+semester => nome 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 @@ -111,9 +116,10 @@ def build_classes_lookup end end + # Processa arquivo CSV + # @return [void] def process_csv CSV.foreach(@file_path, headers: true, col_sep: ",") do |row| - # Assumindo estrutura do CSV turma_data = { "codigo" => row["codigo_turma"], "nome" => row["nome_turma"], @@ -134,6 +140,9 @@ def process_csv end end + # Processa dados de turma + # @param data [Hash] Dados da turma + # @return [void] def process_turma(data) turma = process_turma_record(data) if turma&.persisted? @@ -141,6 +150,9 @@ def process_turma(data) end end + # Cria/atualiza registro de turma + # @param data [Hash] Dados da turma + # @return [Turma, nil] def process_turma_record(data) turma = Turma.find_or_initialize_by(codigo: data["codigo"], semestre: data["semestre"]) @@ -160,21 +172,27 @@ def process_turma_record(data) end end + # Processa lista de participantes + # @param turma [Turma] + # @param participantes_data [Array] + # @return [void] def process_participantes(turma, participantes_data) participantes_data.each do |p_data| process_participante_single(turma, p_data) end end + # Processa um participante + # @param turma [Turma] + # @param p_data [Hash] Dados do participante + # @return [void] + # @efeito_colateral Cria/atualiza User e MatriculaTurma def process_participante_single(turma, p_data) - # 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_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 @@ -186,8 +204,6 @@ def process_participante_single(turma, p_data) if user.save if is_new_user @results[:users_created] += 1 - - # Armazena credenciais do novo usuário para exibir depois @results[:new_users] << { matricula: user.matricula, nome: user.nome, @@ -195,9 +211,6 @@ def process_participante_single(turma, p_data) 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[:users_updated] += 1 end