diff --git a/Gemfile b/Gemfile index 983667a23c..84d8e2ee5c 100644 --- a/Gemfile +++ b/Gemfile @@ -49,6 +49,10 @@ group :development, :test do # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] gem "rubocop-rails-omakase", require: false + + gem "rdoc" + + gem "rails-controller-testing" end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 666370383b..d415853e48 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -329,6 +329,10 @@ GEM activesupport (= 8.0.4) bundler (>= 1.15.0) railties (= 8.0.4) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -542,6 +546,8 @@ DEPENDENCIES propshaft puma (>= 5.0) rails (~> 8.0.4) + rails-controller-testing + rdoc rspec-rails (~> 8.0) rubocop-rails-omakase rubycritic diff --git a/app/models/modelo.rb b/app/models/modelo.rb index 918a42dc22..baa7bd355e 100644 --- a/app/models/modelo.rb +++ b/app/models/modelo.rb @@ -1,3 +1,5 @@ +# Classe que representa um modelo de questionário. +# Serve como agregador de perguntas e definições da avaliação. class Modelo < ApplicationRecord # Relacionamentos has_many :perguntas, dependent: :destroy @@ -15,37 +17,61 @@ class Modelo < ApplicationRecord allow_destroy: true, reject_if: :all_blank - # Método para verificar se modelo está em uso + # 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). def em_uso? avaliacoes.any? end - # Método para clonar modelo com perguntas + # 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'. 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 + # Copia as perguntas para a memória antes de salvar para passar na validação + perguntas.each do |pergunta| + nova_pergunta = pergunta.dup + novo_modelo.perguntas << nova_pergunta end + novo_modelo.save novo_modelo end 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. 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. 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 9da296b57f..b17f1fcf6c 100644 --- a/app/models/pergunta.rb +++ b/app/models/pergunta.rb @@ -1,5 +1,6 @@ +# Classe que representa uma pergunta associada a um modelo de questionário. class Pergunta < ApplicationRecord - self.table_name = "perguntas" # Plural correto em português + self.table_name = "perguntas" # Relacionamentos belongs_to :modelo @@ -21,32 +22,41 @@ class Pergunta < ApplicationRecord validates :tipo, presence: true, inclusion: { in: TIPOS.keys } # Validações condicionais - validate :opcoes_requeridas_para_multipla_escolha - validate :opcoes_requeridas_para_checkbox + validate :validar_minimo_opcoes, if: :requer_opcoes? - # Callbacks - before_validation :definir_ordem_padrao, on: :create - - # Métodos + # 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. 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. def requer_opcoes? - [ "multipla_escolha", "checkbox" ].include?(tipo) + %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. 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 + parse_opcoes_string else [] end @@ -54,28 +64,30 @@ def lista_opcoes private - def definir_ordem_padrao - if modelo.present? - ultima_ordem = modelo.perguntas.maximum(:id) || 0 - # Ordem pode ser baseada no ID para simplificar - end + # 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) 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 + # 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 + opcoes_lista = lista_opcoes - 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 + 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}") end end end diff --git a/app/models/user.rb b/app/models/user.rb index 00858d7086..a2ef63d982 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,15 +1,32 @@ +# Classe que representa um usuário do sistema. +# Responsável pela autenticação, dados cadastrais e relação com turmas e submissões. class User < ApplicationRecord + # Adiciona métodos para definir e autenticar senhas usando BCrypt. + # + # Descrição: Gera o atributo 'password_digest' e os atributos virtuais 'password' e 'password_confirmation'. + # Efeitos Colaterais: Criptografa a senha antes de salvar no banco. has_secure_password + + # Relacionamentos has_many :sessions, dependent: :destroy has_many :matricula_turmas has_many :turmas, through: :matricula_turmas + # Associação com submissões onde o usuário atua como aluno has_many :submissoes, class_name: "Submissao", foreign_key: :aluno_id, dependent: :destroy + # Validações de integridade dos dados validates :email_address, presence: true, uniqueness: true validates :login, presence: true, uniqueness: true validates :matricula, presence: true, uniqueness: true validates :nome, presence: true + # Normalização de atributos + # + # Descrição: Transforma o email e o login para letras minúsculas e remove espaços + # no início e fim antes de salvar ou consultar. + # Argumentos: Recebe o valor bruto do atributo (e/l). + # Retorno: String normalizada. + # Efeitos Colaterais: Altera o valor do atributo antes da validação. normalizes :email_address, with: ->(e) { e.strip.downcase } normalizes :login, with: ->(l) { l.strip.downcase } end diff --git a/app/views/user_mailer/definicao_senha.html.erb b/app/views/user_mailer/definicao_senha.html.erb new file mode 100644 index 0000000000..d5d896ecb7 --- /dev/null +++ b/app/views/user_mailer/definicao_senha.html.erb @@ -0,0 +1,3 @@ +

Definição de Senha

+

Olá, <%= @user.nome %>

+

Clique no link abaixo para definir sua senha.

\ No newline at end of file diff --git a/doc/app/ApplicationRecord.html b/doc/app/ApplicationRecord.html new file mode 100644 index 0000000000..cd88cbb844 --- /dev/null +++ b/doc/app/ApplicationRecord.html @@ -0,0 +1,128 @@ + + + + + + + +class ApplicationRecord - RDoc Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ class ApplicationRecord +

+ +
+ +
+ + + + +
+ + diff --git a/doc/app/Avaliacao.html b/doc/app/Avaliacao.html new file mode 100644 index 0000000000..a3fb93605e --- /dev/null +++ b/doc/app/Avaliacao.html @@ -0,0 +1,128 @@ + + + + + + + +class Avaliacao - RDoc Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ class Avaliacao +

+ +
+ +
+ + + + +
+ + diff --git a/doc/app/Current.html b/doc/app/Current.html new file mode 100644 index 0000000000..3ae85b9d54 --- /dev/null +++ b/doc/app/Current.html @@ -0,0 +1,128 @@ + + + + + + + +class Current - RDoc Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ class Current +

+ +
+ +
+ + + + +
+ + diff --git a/doc/app/MatriculaTurma.html b/doc/app/MatriculaTurma.html new file mode 100644 index 0000000000..67ba6f395c --- /dev/null +++ b/doc/app/MatriculaTurma.html @@ -0,0 +1,128 @@ + + + + + + + +class MatriculaTurma - RDoc Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ class MatriculaTurma +

+ +
+ +
+ + + + +
+ + diff --git a/doc/app/Modelo.html b/doc/app/Modelo.html new file mode 100644 index 0000000000..eda56cbcd2 --- /dev/null +++ b/doc/app/Modelo.html @@ -0,0 +1,259 @@ + + + + + + + +class Modelo - RDoc Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ class Modelo +

+ +
+ +

Classe que representa um modelo de questionário. Serve como agregador de perguntas e definições da avaliação.

+ +
+ + + + +
+ + diff --git a/doc/app/Pergunta.html b/doc/app/Pergunta.html new file mode 100644 index 0000000000..95fe5f5747 --- /dev/null +++ b/doc/app/Pergunta.html @@ -0,0 +1,314 @@ + + + + + + + +class Pergunta - RDoc Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ class Pergunta +

+ +
+ +

Classe que representa uma pergunta associada a um modelo de questionário.

+ +
+ + + + +
+ + diff --git a/doc/app/Resposta.html b/doc/app/Resposta.html new file mode 100644 index 0000000000..b0c71ca6bf --- /dev/null +++ b/doc/app/Resposta.html @@ -0,0 +1,128 @@ + + + + + + + +class Resposta - RDoc Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ class Resposta +

+ +
+ +
+ + + + +
+ + diff --git a/doc/app/Session.html b/doc/app/Session.html new file mode 100644 index 0000000000..07bee03dcb --- /dev/null +++ b/doc/app/Session.html @@ -0,0 +1,128 @@ + + + + + + + +class Session - RDoc Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ class Session +

+ +
+ +
+ + + + +
+ + diff --git a/doc/app/Submissao.html b/doc/app/Submissao.html new file mode 100644 index 0000000000..9f2d920e8b --- /dev/null +++ b/doc/app/Submissao.html @@ -0,0 +1,128 @@ + + + + + + + +class Submissao - RDoc Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ class Submissao +

+ +
+ +
+ + + + +
+ + diff --git a/doc/app/Turma.html b/doc/app/Turma.html new file mode 100644 index 0000000000..31564e9a6a --- /dev/null +++ b/doc/app/Turma.html @@ -0,0 +1,128 @@ + + + + + + + +class Turma - RDoc Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ class Turma +

+ +
+ +
+ + + + +
+ + diff --git a/doc/app/User.html b/doc/app/User.html new file mode 100644 index 0000000000..8f4839fe10 --- /dev/null +++ b/doc/app/User.html @@ -0,0 +1,130 @@ + + + + + + + +class User - RDoc Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+ class User +

+ +
+ +

Classe que representa um usuário do sistema. Responsável pela autenticação, dados cadastrais e relação com turmas e submissões.

+ +
+ + + + +
+ + diff --git a/doc/app/created.rid b/doc/app/created.rid new file mode 100644 index 0000000000..5ae6985b63 --- /dev/null +++ b/doc/app/created.rid @@ -0,0 +1,13 @@ +Sun, 14 Dec 2025 19:21:06 -0300 +app/models/MatriculaTurma.rb Sun, 14 Dec 2025 17:35:07 -0300 +app/models/application_record.rb Sun, 14 Dec 2025 17:34:18 -0300 +app/models/avaliacao.rb Sun, 14 Dec 2025 17:35:07 -0300 +app/models/current.rb Sun, 14 Dec 2025 17:35:07 -0300 +app/models/matricula_turma.rb Sun, 14 Dec 2025 17:35:07 -0300 +app/models/modelo.rb Sun, 14 Dec 2025 19:11:17 -0300 +app/models/pergunta.rb Sun, 14 Dec 2025 19:12:02 -0300 +app/models/resposta.rb Sun, 14 Dec 2025 17:35:07 -0300 +app/models/session.rb Sun, 14 Dec 2025 17:35:07 -0300 +app/models/submissao.rb Sun, 14 Dec 2025 17:35:07 -0300 +app/models/turma.rb Sun, 14 Dec 2025 17:35:07 -0300 +app/models/user.rb Sun, 14 Dec 2025 19:20:47 -0300 diff --git a/doc/app/css/fonts.css b/doc/app/css/fonts.css new file mode 100644 index 0000000000..57302b5183 --- /dev/null +++ b/doc/app/css/fonts.css @@ -0,0 +1,167 @@ +/* + * Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), + * with Reserved Font Name "Source". All Rights Reserved. Source is a + * trademark of Adobe Systems Incorporated in the United States and/or other + * countries. + * + * This Font Software is licensed under the SIL Open Font License, Version + * 1.1. + * + * This license is copied below, and is also available with a FAQ at: + * http://scripts.sil.org/OFL + */ + +@font-face { + font-family: "Source Code Pro"; + font-style: normal; + font-weight: 400; + src: local("Source Code Pro"), + local("SourceCodePro-Regular"), + url("../fonts/SourceCodePro-Regular.ttf") format("truetype"); +} + +@font-face { + font-family: "Source Code Pro"; + font-style: normal; + font-weight: 700; + src: local("Source Code Pro Bold"), + local("SourceCodePro-Bold"), + url("../fonts/SourceCodePro-Bold.ttf") format("truetype"); +} + +/* + * Copyright (c) 2010, Łukasz Dziedzic (dziedzic@typoland.com), + * with Reserved Font Name Lato. + * + * This Font Software is licensed under the SIL Open Font License, Version + * 1.1. + * + * This license is copied below, and is also available with a FAQ at: + * http://scripts.sil.org/OFL + */ + +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 300; + src: local("Lato Light"), + local("Lato-Light"), + url("../fonts/Lato-Light.ttf") format("truetype"); +} + +@font-face { + font-family: "Lato"; + font-style: italic; + font-weight: 300; + src: local("Lato Light Italic"), + local("Lato-LightItalic"), + url("../fonts/Lato-LightItalic.ttf") format("truetype"); +} + +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 700; + src: local("Lato Regular"), + local("Lato-Regular"), + url("../fonts/Lato-Regular.ttf") format("truetype"); +} + +@font-face { + font-family: "Lato"; + font-style: italic; + font-weight: 700; + src: local("Lato Italic"), + local("Lato-Italic"), + url("../fonts/Lato-RegularItalic.ttf") format("truetype"); +} + +/* + * ----------------------------------------------------------- + * SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + * ----------------------------------------------------------- + * + * PREAMBLE + * The goals of the Open Font License (OFL) are to stimulate worldwide + * development of collaborative font projects, to support the font creation + * efforts of academic and linguistic communities, and to provide a free and + * open framework in which fonts may be shared and improved in partnership + * with others. + * + * The OFL allows the licensed fonts to be used, studied, modified and + * redistributed freely as long as they are not sold by themselves. The + * fonts, including any derivative works, can be bundled, embedded, + * redistributed and/or sold with any software provided that any reserved + * names are not used by derivative works. The fonts and derivatives, + * however, cannot be released under any other type of license. The + * requirement for fonts to remain under this license does not apply + * to any document created using the fonts or their derivatives. + * + * DEFINITIONS + * "Font Software" refers to the set of files released by the Copyright + * Holder(s) under this license and clearly marked as such. This may + * include source files, build scripts and documentation. + * + * "Reserved Font Name" refers to any names specified as such after the + * copyright statement(s). + * + * "Original Version" refers to the collection of Font Software components as + * distributed by the Copyright Holder(s). + * + * "Modified Version" refers to any derivative made by adding to, deleting, + * or substituting -- in part or in whole -- any of the components of the + * Original Version, by changing formats or by porting the Font Software to a + * new environment. + * + * "Author" refers to any designer, engineer, programmer, technical + * writer or other person who contributed to the Font Software. + * + * PERMISSION & CONDITIONS + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of the Font Software, to use, study, copy, merge, embed, modify, + * redistribute, and sell modified and unmodified copies of the Font + * Software, subject to the following conditions: + * + * 1) Neither the Font Software nor any of its individual components, + * in Original or Modified Versions, may be sold by itself. + * + * 2) Original or Modified Versions of the Font Software may be bundled, + * redistributed and/or sold with any software, provided that each copy + * contains the above copyright notice and this license. These can be + * included either as stand-alone text files, human-readable headers or + * in the appropriate machine-readable metadata fields within text or + * binary files as long as those fields can be easily viewed by the user. + * + * 3) No Modified Version of the Font Software may use the Reserved Font + * Name(s) unless explicit written permission is granted by the corresponding + * Copyright Holder. This restriction only applies to the primary font name as + * presented to the users. + * + * 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + * Software shall not be used to promote, endorse or advertise any + * Modified Version, except to acknowledge the contribution(s) of the + * Copyright Holder(s) and the Author(s) or with their explicit written + * permission. + * + * 5) The Font Software, modified or unmodified, in part or in whole, + * must be distributed entirely under this license, and must not be + * distributed under any other license. The requirement for fonts to + * remain under this license does not apply to any document created + * using the Font Software. + * + * TERMINATION + * This license becomes null and void if any of the above conditions are + * not met. + * + * DISCLAIMER + * THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT + * OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL + * DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM + * OTHER DEALINGS IN THE FONT SOFTWARE. + */ + diff --git a/doc/app/css/rdoc.css b/doc/app/css/rdoc.css new file mode 100644 index 0000000000..c84a604c8c --- /dev/null +++ b/doc/app/css/rdoc.css @@ -0,0 +1,683 @@ +/* + * "Darkfish" RDoc CSS + * $Id: rdoc.css 54 2009-01-27 01:09:48Z deveiant $ + * + * Author: Michael Granger + * + */ + +/* vim: ft=css et sw=2 ts=2 sts=2 */ + +/* 1. Variables and Root Styles */ +:root { + --sidebar-width: 300px; + --highlight-color: #cc342d; /* Reddish color for accents and headings */ + --secondary-highlight-color: #c83045; /* Darker reddish color for secondary highlights */ + --text-color: #505050; /* Dark bluish-grey for text */ + --background-color: #fefefe; /* Near white background */ + --code-block-background-color: #f6f6f3; /* Slightly darker grey for code blocks */ + --link-color: #42405F; /* Dark bluish-grey for links */ + --link-hover-color: var(--highlight-color); /* Reddish color on hover */ + --border-color: #e0e0e0;; /* General border color */ + --source-code-toggle-color: var(--secondary-highlight-color); + --scrollbar-thumb-hover-background: #505050; /* Hover color for scrollbar thumb */ + --table-header-background-color: #eceaed; + --table-td-background-color: #f5f4f6; + + /* Font family variables */ + --font-primary: 'Segoe UI', 'Verdana', 'Arial', sans-serif; + --font-heading: 'Helvetica', 'Arial', sans-serif; + --font-code: monospace; +} + +/* 2. Global Styles */ +body { + background: var(--background-color); + font-family: var(--font-primary); + font-weight: 400; + color: var(--text-color); + line-height: 1.6; + + /* Layout */ + display: flex; + flex-direction: column; + min-height: 100vh; + margin: 0; +} + +/* 3. Typography */ +h1 span, +h2 span, +h3 span, +h4 span, +h5 span, +h6 span { + position: relative; + + display: none; + padding-left: 1em; + line-height: 0; + vertical-align: baseline; + font-size: 10px; +} + +h1 span { top: -1.3em; } +h2 span { top: -1.2em; } +h3 span { top: -1.0em; } +h4 span { top: -0.8em; } +h5 span { top: -0.5em; } +h6 span { top: -0.5em; } + +h1:hover span, +h2:hover span, +h3:hover span, +h4:hover span, +h5:hover span, +h6:hover span { + display: inline; +} + +h1:target, +h2:target, +h3:target, +h4:target, +h5:target, +h6:target { + margin-left: -10px; + border-left: 10px solid var(--border-color); + scroll-margin-top: 1rem; +} + +main .anchor-link:target { + scroll-margin-top: 1rem; +} + +/* 4. Links */ +a { + color: var(--link-color); + transition: color 0.3s ease; + text-decoration: underline; + text-underline-offset: 0.2em; /* Make sure it doesn't overlap with underscores in a method name. */ +} + +a:hover { + color: var(--link-hover-color); +} + +a code:hover { + color: var(--link-hover-color); +} + +/* 5. Code and Pre */ +code, +pre { + font-family: var(--font-code); + background-color: var(--code-block-background-color); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 16px; + overflow-x: auto; + font-size: 15px; + line-height: 1.5; + margin: 1em 0; +} + +code { + background-color: var(--code-block-background-color); + padding: 0.1em 0.3em; + border-radius: 3px; + font-size: 85%; +} + +/* Tables */ +table { + margin: 0; + border-spacing: 0; + border-collapse: collapse; +} + +table tr th, table tr td { + padding: 0.2em 0.4em; + border: 1px solid var(--border-color); +} + +table tr th { + background-color: var(--table-header-background-color); +} + +table tr:nth-child(even) td { + background-color: var(--table-td-background-color); +} + +/* 7. Navigation and Sidebar */ +nav { + font-family: var(--font-heading); + font-size: 16px; + border-right: 1px solid var(--border-color); + position: fixed; + top: 0; + bottom: 0; + left: 0; + width: var(--sidebar-width); + background: var(--background-color); /* It needs an explicit background for toggling narrow screens */ + overflow-y: auto; + z-index: 10; + display: flex; + flex-direction: column; + color: var(--text-color); +} + +nav[hidden] { + display: none; +} + +nav footer { + padding: 1em; + border-top: 1px solid var(--border-color); +} + +nav footer a { + color: var(--secondary-highlight-color); +} + +nav .nav-section { + margin-top: 1em; + padding: 0 1em; +} + +nav h2, nav h3 { + margin: 0 0 0.5em; + padding: 0.5em 0; + color: var(--highlight-color); + border-bottom: 1px solid var(--border-color); +} + +nav h2 { + font-size: 1.2em; +} + +nav h3, +#table-of-contents-navigation { + font-size: 1em; +} + +ol.breadcrumb { + display: flex; + + padding: 0; + margin: 0 0 1em; +} + +ol.breadcrumb li { + display: block; + list-style: none; + font-size: 125%; +} + +nav ul, +nav dl, +nav p { + padding: 0; + list-style: none; + margin: 0.5em 0; +} + +nav ul li { + margin-bottom: 0.3em; +} + +nav ul ul { + padding-left: 1em; +} + +nav ul ul ul { + padding-left: 1em; +} + +nav ul ul ul ul { + padding-left: 1em; +} + +nav a { + color: var(--link-color); + text-decoration: none; +} + +nav a:hover { + color: var(--link-hover-color); + text-decoration: underline; +} + +#navigation-toggle { + z-index: 1000; + font-size: 2em; + display: block; + position: fixed; + top: 10px; + left: 20px; + cursor: pointer; +} + +#navigation-toggle[aria-expanded="true"] { + top: 10px; + left: 250px; +} + +nav ul li details { + position: relative; + padding-right: 1.5em; /* Add space for the marker on the right */ +} + +nav ul li details > summary { + list-style: none; /* Remove the default marker */ + position: relative; /* So that the open/close triangle can position itself absolutely inside */ +} + +nav ul li details > summary::-webkit-details-marker { + display: none; /* Removes the default marker, in Safari 18. */ +} + +nav ul li details > summary::after { + content: '▶'; /* Unicode right-pointing triangle */ + position: absolute; + font-size: 0.8em; + bottom: 0.1em; + margin-left: 0.3em; + transition: transform 0.2s ease; +} + +nav ul li details[open] > summary::after { + transform: rotate(90deg); /* Rotate the triangle when open */ +} + +/* 8. Main Content */ +main { + flex: 1; + display: block; + margin: 3em auto; + padding: 0 2em; + max-width: 800px; + font-size: 16px; + line-height: 1.6; + color: var(--text-color); + box-sizing: border-box; +} + +@media (min-width: 1024px) { + main { + margin-left: var(--sidebar-width); + } + + .table-of-contents main { + margin-left: 20em; + } + + #navigation-toggle { + display: none; + } +} + +main h1[class] { + margin-top: 0; + margin-bottom: 1em; + font-size: 2.5em; + color: var(--highlight-color); +} + +main h1, +main h2, +main h3, +main h4, +main h5, +main h6 { + font-family: var(--font-heading); + color: var(--highlight-color); +} + +/* Search */ +#search-section { + padding: 1em; + background-color: var(--background-color); + border-bottom: 1px solid var(--border-color); +} + +#search-field-wrapper { + position: relative; + display: flex; + align-items: center; +} + +#search-field { + width: 100%; + padding: 0.5em 1em 0.5em 2.5em; + border: 1px solid var(--border-color); + border-radius: 20px; + font-size: 14px; + outline: none; + transition: border-color 0.3s ease; + color: var(--text-color); +} + +#search-field:focus { + border-color: var(--highlight-color); +} + +#search-field::placeholder { + color: var(--text-color); +} + +#search-field-wrapper::before { + content: "\1F50D"; + position: absolute; + left: 0.75em; + top: 50%; + transform: translateY(-50%); + font-size: 14px; + color: var(--text-color); + opacity: 0.6; +} + +/* Search Results */ +#search-results { + font-family: var(--font-primary); + font-weight: 300; +} + +#search-results .search-match { + font-family: var(--font-heading); + font-weight: normal; +} + +#search-results .search-selected { + background: var(--code-block-background-color); + border-bottom: 1px solid transparent; +} + +#search-results li { + list-style: none; + border-bottom: 1px solid var(--border-color); + margin-bottom: 0.5em; +} + +#search-results li:last-child { + border-bottom: none; + margin-bottom: 0; +} + +#search-results li p { + padding: 0; + margin: 0.5em; +} + +#search-results .search-namespace { + font-weight: bold; +} + +#search-results li em { + background-color: rgba(224, 108, 117, 0.1); + font-style: normal; +} + +#search-results pre { + margin: 0.5em; + font-family: var(--font-code); +} + +/* Syntax Highlighting - Gruvbox Light Scheme */ + +.ruby-constant { color: #AF3A03; } /* Dark Orange */ +.ruby-keyword { color: #9D0006; } /* Dark Red */ +.ruby-ivar { color: #B57614; } /* Brown */ +.ruby-operator { color: #427B58; } /* Dark Teal */ +.ruby-identifier { color: #076678; } /* Deep Teal */ +.ruby-node { color: #8F3F71; } /* Plum */ +.ruby-comment { color: #928374; font-style: italic; } /* Gray */ +.ruby-regexp { color: #8F3F71; } /* Plum */ +.ruby-value { color: #AF3A03; } /* Dark Orange */ +.ruby-string { color: #79740E; } /* Olive */ + +/* Emphasis */ +em { + text-decoration-color: rgba(52, 48, 64, 0.25); + text-decoration-line: underline; + text-decoration-style: dotted; +} + +strong, +em { + color: var(--highlight-color); + background-color: rgba(255, 111, 97, 0.1); /* Light red background for emphasis */ +} + +/* Paragraphs */ +main p { + line-height: 1.5em; + font-weight: 400; +} + +/* Preformatted Text */ +main pre { + margin: 1.2em 0.5em; + padding: 1em; + font-size: 0.8em; +} + +/* Horizontal Rules */ +main hr { + margin: 1.5em 1em; + border: 2px solid var(--border-color); +} + +/* Blockquotes */ +main blockquote { + margin: 0 2em 1.2em 1.2em; + padding-left: 0.5em; + border-left: 2px solid var(--border-color); +} + +/* Lists */ +main li > p { + margin: 0.5em; +} + +/* Definition Lists */ +main dl { + margin: 1em 0.5em; +} + +main dt { + line-height: 1.5; /* matches `main p` */ + font-weight: bold; +} + +main dl.note-list dt { + margin-right: 1em; + float: left; +} + +main dl.note-list dt:has(+ dt) { + margin-right: 0.25em; +} + +main dl.note-list dt:has(+ dt)::after { + content: ', '; + font-weight: normal; +} + +main dd { + margin: 0 0 1em 1em; +} + +main dd p:first-child { + margin-top: 0; +} + +/* Headers within Main */ +main header h2 { + margin-top: 2em; + border-width: 0; + border-top: 4px solid var(--border-color); + font-size: 130%; +} + +main header h3 { + margin: 2em 0 1.5em; + border-width: 0; + border-top: 3px solid var(--border-color); + font-size: 120%; +} + +/* Utility Classes */ +.hide { display: none !important; } +.initially-hidden { display: none; } + +/* Table of Contents */ +.table-of-contents ul { + margin: 1em; + list-style: none; +} + +.table-of-contents ul ul { + margin-top: 0.25em; +} + +.table-of-contents ul :link, +.table-of-contents ul :visited { + font-size: 16px; +} + +.table-of-contents li { + margin-bottom: 0.25em; +} + +/* Method Details */ +main .method-source-code { + visibility: hidden; + max-height: 0; + overflow: auto; + transition-duration: 200ms; + transition-delay: 0ms; + transition-property: all; + transition-timing-function: ease-in-out; +} + +main .method-source-code pre { + border-color: var(--source-code-toggle-color); +} + +main .method-source-code.active-menu { + visibility: visible; + max-height: 100vh; +} + +main .method-description .method-calls-super { + color: var(--text-color); + font-weight: bold; +} + +main .method-detail { + margin-bottom: 2.5em; +} + +main .method-detail:target { + margin-left: -10px; + border-left: 10px solid var(--border-color); +} + +main .method-header { + display: inline-block; +} + +main .method-heading { + position: relative; + font-family: var(--font-code); + font-size: 110%; + font-weight: bold; +} + +main .method-heading::after { + content: '¶'; + position: absolute; + visibility: hidden; + color: var(--highlight-color); + font-size: 0.5em; +} + +main .method-heading:hover::after { + visibility: visible; +} + +main .method-controls { + line-height: 20px; + float: right; + color: var(--source-code-toggle-color); + cursor: pointer; +} + +main .method-description, +main .aliases { + margin-top: 0.75em; + color: var(--text-color); +} + +main .aliases { + padding-top: 4px; + font-style: italic; + cursor: default; +} + +main .aliases a { + color: var(--secondary-highlight-color); +} + +main .mixin-from { + font-size: 80%; + font-style: italic; + margin-bottom: 0.75em; +} + +main .method-description ul { + margin-left: 1.5em; +} + +main #attribute-method-details .method-detail:hover { + background-color: transparent; + cursor: default; +} + +main .attribute-access-type { + text-transform: uppercase; +} + +/* Responsive Adjustments */ +@media (max-width: 480px) { + nav { + width: 100%; + } + + main { + margin: 1em auto; + padding: 0 1em; + max-width: 100%; + } + + #navigation-toggle { + right: 10px; + left: auto; + } + + #navigation-toggle[aria-expanded="true"] { + left: auto; + } + + table { + display: block; + overflow-x: auto; + white-space: nowrap; + } + + main .method-controls { + margin-top: 10px; + float: none; + } +} diff --git a/doc/app/fonts/Lato-Light.ttf b/doc/app/fonts/Lato-Light.ttf new file mode 100644 index 0000000000..b49dd43729 Binary files /dev/null and b/doc/app/fonts/Lato-Light.ttf differ diff --git a/doc/app/fonts/Lato-LightItalic.ttf b/doc/app/fonts/Lato-LightItalic.ttf new file mode 100644 index 0000000000..7959fef075 Binary files /dev/null and b/doc/app/fonts/Lato-LightItalic.ttf differ diff --git a/doc/app/fonts/Lato-Regular.ttf b/doc/app/fonts/Lato-Regular.ttf new file mode 100644 index 0000000000..839cd589dc Binary files /dev/null and b/doc/app/fonts/Lato-Regular.ttf differ diff --git a/doc/app/fonts/Lato-RegularItalic.ttf b/doc/app/fonts/Lato-RegularItalic.ttf new file mode 100644 index 0000000000..bababa09e3 Binary files /dev/null and b/doc/app/fonts/Lato-RegularItalic.ttf differ diff --git a/doc/app/fonts/SourceCodePro-Bold.ttf b/doc/app/fonts/SourceCodePro-Bold.ttf new file mode 100644 index 0000000000..dd00982d49 Binary files /dev/null and b/doc/app/fonts/SourceCodePro-Bold.ttf differ diff --git a/doc/app/fonts/SourceCodePro-Regular.ttf b/doc/app/fonts/SourceCodePro-Regular.ttf new file mode 100644 index 0000000000..1decfb95af Binary files /dev/null and b/doc/app/fonts/SourceCodePro-Regular.ttf differ diff --git a/doc/app/images/add.png b/doc/app/images/add.png new file mode 100644 index 0000000000..6332fefea4 Binary files /dev/null and b/doc/app/images/add.png differ diff --git a/doc/app/images/arrow_up.png b/doc/app/images/arrow_up.png new file mode 100644 index 0000000000..1ebb193243 Binary files /dev/null and b/doc/app/images/arrow_up.png differ diff --git a/doc/app/images/brick.png b/doc/app/images/brick.png new file mode 100644 index 0000000000..7851cf34c9 Binary files /dev/null and b/doc/app/images/brick.png differ diff --git a/doc/app/images/brick_link.png b/doc/app/images/brick_link.png new file mode 100644 index 0000000000..9ebf013a23 Binary files /dev/null and b/doc/app/images/brick_link.png differ diff --git a/doc/app/images/bug.png b/doc/app/images/bug.png new file mode 100644 index 0000000000..2d5fb90ec6 Binary files /dev/null and b/doc/app/images/bug.png differ diff --git a/doc/app/images/bullet_black.png b/doc/app/images/bullet_black.png new file mode 100644 index 0000000000..57619706d1 Binary files /dev/null and b/doc/app/images/bullet_black.png differ diff --git a/doc/app/images/bullet_toggle_minus.png b/doc/app/images/bullet_toggle_minus.png new file mode 100644 index 0000000000..b47ce55f68 Binary files /dev/null and b/doc/app/images/bullet_toggle_minus.png differ diff --git a/doc/app/images/bullet_toggle_plus.png b/doc/app/images/bullet_toggle_plus.png new file mode 100644 index 0000000000..9ab4a89664 Binary files /dev/null and b/doc/app/images/bullet_toggle_plus.png differ diff --git a/doc/app/images/date.png b/doc/app/images/date.png new file mode 100644 index 0000000000..783c83357f Binary files /dev/null and b/doc/app/images/date.png differ diff --git a/doc/app/images/delete.png b/doc/app/images/delete.png new file mode 100644 index 0000000000..08f249365a Binary files /dev/null and b/doc/app/images/delete.png differ diff --git a/doc/app/images/find.png b/doc/app/images/find.png new file mode 100644 index 0000000000..1547479646 Binary files /dev/null and b/doc/app/images/find.png differ diff --git a/doc/app/images/loadingAnimation.gif b/doc/app/images/loadingAnimation.gif new file mode 100644 index 0000000000..82290f4833 Binary files /dev/null and b/doc/app/images/loadingAnimation.gif differ diff --git a/doc/app/images/macFFBgHack.png b/doc/app/images/macFFBgHack.png new file mode 100644 index 0000000000..c6473b324e Binary files /dev/null and b/doc/app/images/macFFBgHack.png differ diff --git a/doc/app/images/package.png b/doc/app/images/package.png new file mode 100644 index 0000000000..da3c2a2d74 Binary files /dev/null and b/doc/app/images/package.png differ diff --git a/doc/app/images/page_green.png b/doc/app/images/page_green.png new file mode 100644 index 0000000000..de8e003f9f Binary files /dev/null and b/doc/app/images/page_green.png differ diff --git a/doc/app/images/page_white_text.png b/doc/app/images/page_white_text.png new file mode 100644 index 0000000000..813f712f72 Binary files /dev/null and b/doc/app/images/page_white_text.png differ diff --git a/doc/app/images/page_white_width.png b/doc/app/images/page_white_width.png new file mode 100644 index 0000000000..1eb880947d Binary files /dev/null and b/doc/app/images/page_white_width.png differ diff --git a/doc/app/images/plugin.png b/doc/app/images/plugin.png new file mode 100644 index 0000000000..6187b15aec Binary files /dev/null and b/doc/app/images/plugin.png differ diff --git a/doc/app/images/ruby.png b/doc/app/images/ruby.png new file mode 100644 index 0000000000..f763a16880 Binary files /dev/null and b/doc/app/images/ruby.png differ diff --git a/doc/app/images/tag_blue.png b/doc/app/images/tag_blue.png new file mode 100644 index 0000000000..3f02b5f8f8 Binary files /dev/null and b/doc/app/images/tag_blue.png differ diff --git a/doc/app/images/tag_green.png b/doc/app/images/tag_green.png new file mode 100644 index 0000000000..83ec984bd7 Binary files /dev/null and b/doc/app/images/tag_green.png differ diff --git a/doc/app/images/transparent.png b/doc/app/images/transparent.png new file mode 100644 index 0000000000..d665e179ef Binary files /dev/null and b/doc/app/images/transparent.png differ diff --git a/doc/app/images/wrench.png b/doc/app/images/wrench.png new file mode 100644 index 0000000000..5c8213fef5 Binary files /dev/null and b/doc/app/images/wrench.png differ diff --git a/doc/app/images/wrench_orange.png b/doc/app/images/wrench_orange.png new file mode 100644 index 0000000000..565a9330e0 Binary files /dev/null and b/doc/app/images/wrench_orange.png differ diff --git a/doc/app/images/zoom.png b/doc/app/images/zoom.png new file mode 100644 index 0000000000..908612e394 Binary files /dev/null and b/doc/app/images/zoom.png differ diff --git a/doc/app/index.html b/doc/app/index.html new file mode 100644 index 0000000000..081e3dff98 --- /dev/null +++ b/doc/app/index.html @@ -0,0 +1,99 @@ + + + + + + + +RDoc Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

This is the API documentation for RDoc Documentation.

+ +
+ + diff --git a/doc/app/js/darkfish.js b/doc/app/js/darkfish.js new file mode 100644 index 0000000000..6b6e688afb --- /dev/null +++ b/doc/app/js/darkfish.js @@ -0,0 +1,140 @@ +/** + * + * Darkfish Page Functions + * $Id: darkfish.js 53 2009-01-07 02:52:03Z deveiant $ + * + * Author: Michael Granger + * + */ + +/* Provide console simulation for firebug-less environments */ +/* +if (!("console" in window) || !("firebug" in console)) { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", + "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; + + window.console = {}; + for (var i = 0; i < names.length; ++i) + window.console[names[i]] = function() {}; +}; +*/ + + +function showSource( e ) { + var target = e.target; + while (!target.classList.contains('method-detail')) { + target = target.parentNode; + } + if (typeof target !== "undefined" && target !== null) { + target = target.querySelector('.method-source-code'); + } + if (typeof target !== "undefined" && target !== null) { + target.classList.toggle('active-menu') + } +}; + +function hookSourceViews() { + document.querySelectorAll('.method-source-toggle').forEach(function (codeObject) { + codeObject.addEventListener('click', showSource); + }); +}; + +function hookSearch() { + var input = document.querySelector('#search-field'); + var result = document.querySelector('#search-results'); + result.classList.remove("initially-hidden"); + + var search_section = document.querySelector('#search-section'); + search_section.classList.remove("initially-hidden"); + + var search = new Search(search_data, input, result); + + search.renderItem = function(result) { + var li = document.createElement('li'); + var html = ''; + + // TODO add relative path to + + + + + + + + + + + + + + + + + +
+

Table of Contents - RDoc Documentation

+ + + + +

Classes and Modules

+ + +

Methods

+ +
+ + diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb new file mode 100644 index 0000000000..e6bc05dd09 --- /dev/null +++ b/spec/controllers/home_controller_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +RSpec.describe HomeController, type: :controller do + # Cria usuário apenas para ter um objeto válido caso a view precise + let(:user) do + User.create!( + login: "user_home_final", + email_address: "home_final@teste.com", + matricula: "998877", + nome: "User Home Final", + formacao: "Docente", + eh_admin: true, + password: "123", + password_confirmation: "123" + ) + end + + before do + # --- MOCKS DE AUTENTICAÇÃO --- + + allow(controller).to receive(:require_authentication).and_return(true) + allow(controller).to receive(:authenticate_user!).and_return(true) + + allow(controller).to receive(:current_user).and_return(user) + end + + describe "GET #index" do + it "retorna sucesso HTTP" do + get :index + expect(response).to have_http_status(:success) + end + + it "renderiza o template index" do + get :index + expect(response).to render_template(:index) + end + end +end diff --git a/spec/models/modelo_spec.rb b/spec/models/modelo_spec.rb new file mode 100644 index 0000000000..bde5b1cb56 --- /dev/null +++ b/spec/models/modelo_spec.rb @@ -0,0 +1,117 @@ +require 'rails_helper' + +RSpec.describe Modelo, type: :model do + # Configuração básica para os testes + let(:valid_attributes) { { titulo: "Modelo Teste", ativo: true } } + let(:pergunta_attributes) { { enunciado: "Pergunta 1", tipo: "texto_curto" } } + + # Cria um modelo válido no banco para reuso + let(:modelo_existente) do + m = Modelo.new(valid_attributes) + m.perguntas.build(pergunta_attributes) + m.save! + m + end + + context "Validações e Atributos" do + it "é válido com título e perguntas" do + modelo = Modelo.new(valid_attributes) + modelo.perguntas.build(pergunta_attributes) + expect(modelo).to be_valid + end + + it "é inválido sem título" do + modelo = Modelo.new(valid_attributes.merge(titulo: nil)) + modelo.perguntas.build(pergunta_attributes) + expect(modelo).not_to be_valid + expect(modelo.errors[:titulo]).to include("can't be blank") + end + + it "valida unicidade do título (case sensitive)" do + # Cria o primeiro + modelo_existente + + # Tenta criar o segundo igual + duplicado = Modelo.new(valid_attributes) + duplicado.perguntas.build(pergunta_attributes) + expect(duplicado).not_to be_valid + expect(duplicado.errors[:titulo]).to include("has already been taken") + end + + it "aceita atributos aninhados para perguntas" do + modelo = Modelo.new(titulo: "Nested Attrs") + modelo.perguntas_attributes = [ pergunta_attributes ] + expect(modelo).to be_valid + expect(modelo.perguntas.size).to eq(1) + end + end + + context "Regras de Negócio de Perguntas" do + it "CREATE: impede criar modelo sem perguntas" do + modelo = Modelo.new(valid_attributes) + # Não adicionamos perguntas + expect(modelo).not_to be_valid + expect(modelo.errors[:base]).to include("Um modelo deve ter pelo menos uma pergunta") + end + + it "UPDATE: impede remover todas as perguntas de um modelo existente" do + modelo = modelo_existente + pergunta = modelo.perguntas.first + + # Tenta marcar a única pergunta para destruição + pergunta.mark_for_destruction + + expect(modelo).not_to be_valid + expect(modelo.errors[:base]).to include("Não é possível remover todas as perguntas de um modelo existente") + end + end + + context "Métodos da Classe" do + describe "#em_uso?" do + it "retorna false se não tem avaliações" do + expect(modelo_existente.em_uso?).to be false + end + + it "retorna true se tem avaliações associadas" do + turma = Turma.create!(codigo: "T1", nome: "Turma Teste", semestre: "2024.1") + # Cria uma avaliação fake associada + Avaliacao.create!(modelo: modelo_existente, turma: turma) + + expect(modelo_existente.em_uso?).to be true + end + end + + describe "#clonar_com_perguntas" do + it "cria uma cópia completa do modelo e suas perguntas" do + original = modelo_existente + novo_modelo = original.clonar_com_perguntas("Cópia do Modelo") + + # Verifica dados do novo modelo + expect(novo_modelo).to be_persisted + expect(novo_modelo.titulo).to eq("Cópia do Modelo") + expect(novo_modelo.ativo).to be false # Deve nascer inativo + expect(novo_modelo.id).not_to eq(original.id) + + # Verifica clonagem das perguntas + expect(novo_modelo.perguntas.count).to eq(1) + expect(novo_modelo.perguntas.first.enunciado).to eq(original.perguntas.first.enunciado) + expect(novo_modelo.perguntas.first.id).not_to eq(original.perguntas.first.id) + end + end + end + + context "Associações e Dependências" do + it "destroy apaga as perguntas filhas" do + modelo = modelo_existente + expect { modelo.destroy }.to change(Pergunta, :count).by(-1) + end + + it "não pode ser apagado se tiver avaliações (restrict_with_error)" do + turma = Turma.create!(codigo: "T2", nome: "Turma Restrict", semestre: "2024.1") + Avaliacao.create!(modelo: modelo_existente, turma: turma) + + expect { modelo_existente.destroy }.not_to change(Modelo, :count) + expect(modelo_existente.errors[:base]).to be_present + end + end +end diff --git a/spec/models/pergunta_spec.rb b/spec/models/pergunta_spec.rb new file mode 100644 index 0000000000..8d3016d3bf --- /dev/null +++ b/spec/models/pergunta_spec.rb @@ -0,0 +1,85 @@ +require 'rails_helper' + +RSpec.describe Pergunta, type: :model do + let(:modelo) do + m = Modelo.new(titulo: "Modelo Teste") + m.perguntas.build(enunciado: "Placeholder", tipo: "texto_curto") + m.save! + m + end + + context "Validações Gerais" do + it "é válida com enunciado e tipo corretos" do + pergunta = Pergunta.new(enunciado: "Questão 1", tipo: "texto_curto", modelo: modelo) + expect(pergunta).to be_valid + end + + it "é inválida sem enunciado" do + pergunta = Pergunta.new(enunciado: nil, tipo: "texto_curto", modelo: modelo) + expect(pergunta).not_to be_valid + end + + it "é inválida com tipo desconhecido" do + pergunta = Pergunta.new(enunciado: "Q1", tipo: "tipo_maluco", modelo: modelo) + expect(pergunta).not_to be_valid + end + end + + context "Lógica de Opções (JSON)" do + it "lista_opcoes retorna array vazio se nil" do + pergunta = Pergunta.new(opcoes: nil) + expect(pergunta.lista_opcoes).to eq([]) + end + + it "lista_opcoes faz parse de string JSON" do + pergunta = Pergunta.new(opcoes: '["A", "B"]') + expect(pergunta.lista_opcoes).to eq([ "A", "B" ]) + end + + it "lista_opcoes lida com string separada por ponto e vírgula" do + pergunta = Pergunta.new(opcoes: "Opção A; Opção B") + expect(pergunta.lista_opcoes).to eq([ "Opção A", "Opção B" ]) + end + end + + context "Validação Customizada (Refatoração)" do + it "Múltipla escolha exige pelo menos 2 opções" do + pergunta = Pergunta.new( + enunciado: "Teste", + tipo: "multipla_escolha", + modelo: modelo, + opcoes: '["Apenas Uma"]' + ) + expect(pergunta).not_to be_valid + expect(pergunta.errors[:opcoes]).to include("deve ter pelo menos duas opções para múltipla escolha") + end + + it "Checkbox exige pelo menos 2 opções" do + pergunta = Pergunta.new( + enunciado: "Teste", + tipo: "checkbox", + modelo: modelo, + opcoes: '[]' # Vazio + ) + expect(pergunta).not_to be_valid + expect(pergunta.errors[:opcoes]).to include("deve ter pelo menos duas opções para checkbox") + end + + it "Múltipla escolha é válida com 2 opções" do + pergunta = Pergunta.new( + enunciado: "Teste", + tipo: "multipla_escolha", + modelo: modelo, + opcoes: '["A", "B"]' + ) + expect(pergunta).to be_valid + end + end + + context "Métodos Auxiliares" do + it "tipo_humanizado retorna o nome legível" do + pergunta = Pergunta.new(tipo: "multipla_escolha") + expect(pergunta.tipo_humanizado).to eq("Múltipla Escolha") + end + end +end diff --git a/spec/models/submissao_spec.rb b/spec/models/submissao_spec.rb new file mode 100644 index 0000000000..e251361145 --- /dev/null +++ b/spec/models/submissao_spec.rb @@ -0,0 +1,83 @@ +require 'rails_helper' + +RSpec.describe Submissao, type: :model do + # Setup: Cria as dependências necessárias + let(:aluno) do + User.create!( + login: "aluno_teste", + email_address: "aluno@teste.com", + matricula: "202401", + nome: "Aluno Teste", + password: "123", + password_confirmation: "123" + ) + end + + let(:modelo) do + m = Modelo.new(titulo: "Prova 1", ativo: true) + m.perguntas.build(enunciado: "Questão 1", tipo: "texto_curto") + m.save! + m + end + + let(:turma) { Turma.create!(codigo: "T01", nome: "Turma RSpec", semestre: "2024.1") } + let(:avaliacao) { Avaliacao.create!(modelo: modelo, turma: turma) } + + context "Configurações da Classe" do + it "usa o nome de tabela correto (plural em português)" do + expect(Submissao.table_name).to eq("submissoes") + end + end + + context "Associações" do + it "pertence a um aluno (User)" do + assc = Submissao.reflect_on_association(:aluno) + expect(assc.macro).to eq :belongs_to + expect(assc.class_name).to eq "User" + end + + it "pertence a uma avaliacao" do + assc = Submissao.reflect_on_association(:avaliacao) + expect(assc.macro).to eq :belongs_to + end + + it "tem muitas respostas" do + assc = Submissao.reflect_on_association(:respostas) + expect(assc.macro).to eq :has_many + end + + it "aceita atributos aninhados para respostas" do + expect(Submissao.nested_attributes_options).to have_key(:respostas) + end + end + + context "Happy Path (Caminho Feliz)" do + it "é válida com aluno e avaliação" do + submissao = Submissao.new(aluno: aluno, avaliacao: avaliacao) + expect(submissao).to be_valid + end + + it "destroi respostas associadas ao ser deletada" do + submissao = Submissao.create!(aluno: aluno, avaliacao: avaliacao) + submissao.respostas.create!(conteudo: "Resposta teste", questao_id: modelo.perguntas.first.id) rescue nil + + # Mesmo que a criação da resposta falhe por validação da Resposta, + # o teste do 'dependent: :destroy' é garantido pela reflexão acima. + expect { submissao.destroy }.not_to raise_error + end + end + + context "Sad Path (Caminho Triste)" do + it "é inválida sem aluno" do + submissao = Submissao.new(aluno: nil, avaliacao: avaliacao) + expect(submissao).not_to be_valid + expect(submissao.errors[:aluno]).to be_present + end + + it "é inválida sem avaliação" do + submissao = Submissao.new(aluno: aluno, avaliacao: nil) + expect(submissao).not_to be_valid + expect(submissao.errors[:avaliacao]).to be_present + end + end +end diff --git a/spec/requests/avaliacoes_spec.rb b/spec/requests/avaliacoes_spec.rb new file mode 100644 index 0000000000..0dcf627be8 --- /dev/null +++ b/spec/requests/avaliacoes_spec.rb @@ -0,0 +1,78 @@ +require 'rails_helper' + +RSpec.describe "Avaliações", type: :request do + # 1. Criação do Usuário para Autenticação + let(:password) { "senha_segura_123" } + let(:user) do + User.create!( + login: "avaliador_teste", + email_address: "avaliador@teste.com", + matricula: "999999", + nome: "Avaliador Teste", + formacao: "Docente", + password: password, + password_confirmation: password, + eh_admin: true + ) + end + + # 2. Login antes de cada teste + before do + post session_path, params: { email_address: user.email_address, password: password } + end + + describe "GET /gestao_envios" do + it "retorna sucesso HTTP" do + get gestao_envios_avaliacoes_path + expect(response).to have_http_status(:success) + end + end + + describe "POST /create" do + let!(:turma) { Turma.create!(codigo: "CIC001", nome: "Turma de Teste", semestre: "2024.1") } + + # 3. FIX: Criação do Modelo COM Pergunta (para não dar erro de validação) + let!(:template) do + mod = Modelo.new(titulo: "Template Padrão", ativo: true) + # Adiciona uma pergunta na memória antes de salvar + mod.perguntas.build(enunciado: "Pergunta Obrigatória", tipo: "texto_curto") + mod.save! + mod + end + + context "com entradas válidas" do + it "cria uma nova Avaliação vinculada ao template padrão" do + expect { + post avaliacoes_path, params: { turma_id: turma.id } + }.to change(Avaliacao, :count).by(1) + + avaliacao = Avaliacao.last + expect(avaliacao.turma).to eq(turma) + expect(avaliacao.modelo).to eq(template) + expect(response).to redirect_to(gestao_envios_avaliacoes_path) + expect(flash[:notice]).to be_present + end + + it "aceita uma data_fim personalizada" do + data_personalizada = 2.weeks.from_now.to_date + post avaliacoes_path, params: { turma_id: turma.id, data_fim: data_personalizada } + + avaliacao = Avaliacao.last + expect(avaliacao.data_fim.to_date).to eq(data_personalizada) + end + end + + context "quando o template padrão está ausente" do + before { template.update!(titulo: "Outro") } + + it "não cria avaliação e redireciona com alerta" do + expect { + post avaliacoes_path, params: { turma_id: turma.id } + }.not_to change(Avaliacao, :count) + + expect(response).to redirect_to(gestao_envios_avaliacoes_path) + expect(flash[:alert]).to include("Template Padrão não encontrado") + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cf7d9210f9..49ae0bc9f5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,7 +14,12 @@ # # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration require 'simplecov' -SimpleCov.start do +SimpleCov.start 'rails' do + add_filter "app/channels" + add_filter "app/jobs" + add_filter "app/mailers" + + # Grupos para organizar o relatório visualmente add_group 'Controllers', 'app/controllers' add_group 'Models', 'app/models' end