diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..0fa7569830 --- /dev/null +++ b/.gitignore @@ -0,0 +1,90 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead. +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle +/src/.bundle + +# Ignore all logfiles and tempfiles. +/log/* +/src/log/* +/tmp/* +/src/tmp/* +!/log/.keep +!/src/log/.keep +!/tmp/.keep +!/src/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +/src/tmp/pids/* +!/tmp/pids/ +!/src/tmp/pids/ +!/tmp/pids/.keep +!/src/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +/src/storage/* +!/storage/.keep +!/src/storage/.keep +/tmp/storage/* +/src/tmp/storage/* +!/tmp/storage/ +!/src/tmp/storage/ +!/tmp/storage/.keep +!/src/tmp/storage/.keep + +/public/assets +/src/public/assets +.byebug_history + +# Ignore master key for decrypting credentials and more. +/config/master.key +/src/config/master.key + +/db/*.sqlite3 +/src/db/*.sqlite3 +*.sqlite3-journal +*.sqlite3-wal +*.sqlite3-shm + +# Ignore all environment files (except templates). +/.env +/src/.env +/.env.production +/src/.env.production +/.env.development +/src/.env.development +/.env.test +/src/.env.test + +# Ignore some generated files +/coverage +/src/coverage +/public/packs +/src/public/packs +/public/packs-test +/src/public/packs-test +/node_modules +/src/node_modules +/yarn-error.log +/src/yarn-error.log +.yarn/ +/src/.yarn/ +.yarnrc.yml +/src/.yarnrc.yml + +# Ignore Mac and Linux file system files +*.swp +.DS_Store + +# Ignore vendor/bundle +/vendor/bundle +/src/vendor/bundle + +# Ignore simplecov +coverage \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..b5bc2a1895 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,137 @@ +# Guia de Contribuição - Projeto CAMAAR (Grupo 4) + +### Integrantes: +- Guilherme Fornari +- João Magno +- Pedro Conti +- Rodrigo Rafik + +Este documento define as regras e o fluxo de trabalho que todos os membros do grupo devem seguir para garantir a organização e a qualidade do nosso projeto. + +## 1. Papéis (Scrum) + +Teremos dois papéis rotativos: + +* **Product Owner (PO):** Responsável por ser a "voz do cliente", priorizar as User Stories (Issues) no backlog e validar as entregas. +* **ScrumMaster (SM):** Responsável por remover impedimentos, garantir que o time siga o processo (incluindo este guia) e proteger a equipe de distrações. + +## 2. Nomenclatura de Branches + +Toda nova tarefa, seja ela um BDD, uma feature ou um bugfix, **deve** ser feita em sua própria branch. **Nunca faça commits diretamente na `master` ou em branches de sprint**. + +**Padrão:** `tipo/nome-da-tarefa` + +### Tipos de Branch + +* **`bdd/`**: Usado para escrever arquivos de especificação (`.feature`) do Cucumber. + * *Exemplo:* `bdd/login-de-usuario` + * *Exemplo:* `bdd/gerar-relatorio` + +* **`feature/`**: Usado para implementar novas funcionalidades (o código da aplicação). + * *Exemplo:* `feature/login-de-usuario` + +* **`fix/`**: Usado para corrigir bugs em funcionalidades existentes. + * *Exemplo:* `fix/erro-no-login-com-email` + +* **`docs/`**: Usado para alterações na documentação (Wiki, README, etc.). + * *Exemplo:* `docs/atualiza-instrucoes-do-guia` + +## 3. Nomenclatura de Commits + +Para manter o histórico do Git limpo e legível, usaremos prefixos nos nossos commits. + +**Padrão:** `prefixo: Mensagem clara do que foi feito.` + +### Prefixos de Commit + +* **`spec:`**: (Para Sprint 1) Adição ou modificação de arquivos de especificação BDD (`.feature`). + * *Exemplo:* `spec: Adiciona cenários feliz e triste para login de usuário` + +* **`feat:`**: (Sprints futuras) Adição de uma nova funcionalidade (código). + * *Exemplo:* `feat: Implementa rota e controller para login` + +* **`fix:`**: (Sprints futuras) Correção de um bug. + * *Exemplo:* `fix: Corrige validação de senha no login` + +* **`refac:`**: Alteração de código que não corrige bug nem adiciona feature. + * *Exemplo:* `refact: Remove código duplicado do controller de usuário` + +* **`docs:`**: Alterações na documentação. + * *Exemplo:* `docs: Atualiza wiki com novo fluxo de PR` + +* **`style:`**: Alterações de formatação, lint, etc. (sem mudança lógica). + * *Exemplo:* `style: Aplica formatação do RuboCop` + +## 4. Fluxo de Trabalho (Workflow) + +Este é o processo-padrão para **todas** as contribuições. + +### Passo 1: Início da Tarefa + +1. **Sincronize sua `master` local** com a `master` do fork do grupo: + ```bash + git checkout main + git pull origin main + ``` +2. **Crie sua nova branch** a partir da `master` usando a nomenclatura correta: + ```bash + git checkout -b bdd/nome-da-minha-tarefa + ``` + +### Passo 2: Trabalho Local + +1. Faça seu trabalho. +2. Faça seus commits usando a nomenclatura correta: + ```bash + git add . + git commit -m "spec: Adiciona cenário feliz para minha tarefa" + ``` +3. Envie sua branch para o repositório (fork do grupo): + ```bash + git push origin spec/nome-da-minha-tarefa + ``` + +### Passo 3: Pull Request (PR) + +1. No GitHub, abra um **Pull Request**. +2. Preencha o template do PR (veja seção 6). +3. **Importante:** Atribua pelo menos **um colega** do grupo como "Reviewer". +4. O PR **não deve** ser "mergeado" até que o Reviewer aprove. + +## 5. Fluxo Específico: Sprint 1 (Entrega BDD) + +Para a entrega da Sprint 1, o fluxo tem uma particularidade: + +1. O **ScrumMaster (SM)** criará uma branch chamada `sprint-1` a partir da `main` do fork do grupo. +2. **Todos os membros** seguem o **Passo 1 e 2** da seção 4 (criando suas branches `bdd/` a partir da `main`). +3. **Pull Request (Interno):** Ao abrir o Pull Request (Passo 3), a **base** (branch de destino) **NÃO** será a `main`, mas sim a branch `sprint-1`. + * **De:** `bdd/login-de-usuario` + * **Para:** `sprint-1` +4. Após todos os PRs serem revisados e mergeados na `sprint-1`, um membro (ex: SM) fará o **Pull Request Final** para o professor: + * **De:** `nosso-fork/sprint-1` + * **Para:** `EngSwCIC/CAMAAR:main` + +## 6. Fluxo Específico: Sprints Futuras (Implementação) + +Após a Sprint 1, nosso fluxo voltará ao normal: + +* **De:** `feature/nome-da-feature` +* **Para:** `main` (do fork do grupo) + +A `main` do nosso fork será a nossa base de código estável. + +## 7. Template de Pull Request + +Ao criar um Pull Request, use este template na descrição. + +```markdown +### O que foi feito? +(Descreva em poucas linhas o que este PR entrega. Ex: "Implementa os cenários BDD para a feature de Login de Usuário".) + +### Como testar? +(Descreva os passos para o "Reviewer" validar seu trabalho. Ex: "1. Leia o arquivo `features/login.feature` e verifique se os cenários feliz e triste estão presentes.") + +### Issue Relacionada +(Link para a User Story/Issue do GitHub que este PR resolve.) + +- Resolve #[número_da_issue] \ No newline at end of file diff --git a/README.md b/README.md index 9d7fe1bf53..54981286e5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,111 @@ -# CAMAAR -Sistema para avaliação de atividades acadêmicas remotas do CIC +# CAMAAR - Sistema de Gestão Acadêmica + +Este projeto utiliza **Ruby on Rails 8** e **SQLite**. + +## Pré-requisitos + +* **Ruby:** Versão 3.2.0 ou superior (Requisito do Rails 8). +* **Bundler:** Para gerenciar as dependências. +* **SQLite3:** Banco de dados utilizado. + +## Instalação + +1. **Instale as dependências:** + ```bash + bundle install + ``` + +2. **Configuração do Banco de Dados:** + Execute o comando para criar o banco e rodar as migrações: + ```bash + rails db:setup + ``` + +--- + +## ⚠️ Configuração do Usuário Administrador + +Para acessar o sistema como administrador, você deve verificar se já existe um usuário criado ou criar um manualmente via terminal, conforme os cenários abaixo. + +### Passo 1: Acesse o Console do Rails +No terminal, na raiz do projeto, execute: +```bash +rails console +```` + +### Passo 2: Verifique se já existe um Admin + +Dentro do console, execute o comando abaixo para buscar um administrador: + +```ruby +admin = Usuario.where(ocupacao: 'admin').first +puts admin ? "Email encontrado: #{admin.email}" : "Nenhum admin encontrado." +``` + +### Passo 3: Decida o fluxo com base no resultado + +#### **Cenário A: Nenhum admin encontrado** + +Ainda dentro do console, copie e cole o código abaixo para criar um novo administrador: + +```ruby +Usuario.create!( + nome: "Administrador", + email: "admin@test.com", + usuario: "admin", + matricula: "000000", + password: "password123", + password_confirmation: "password123", + ocupacao: :admin, + status: true +) +``` + +*Agora você pode logar com o email `admin@test.com` e a senha `password123`.* + +#### **Cenário B: Admin encontrado (Recuperação de Senha)** + +Se o console retornou um usuário (ex: `admin@test.com`) mas você não sabe a senha: + +1. Copie o e-mail retornado no console. +2. Saia do console (digite `exit`). +3. Inicie o servidor (veja a seção "Rodando a Aplicação" abaixo). +4. Acesse a tela de login no navegador. +5. Clique em **"Esqueci minha senha"**. +6. Insira o e-mail do administrador. +7. O sistema utiliza a gem `letter_opener`. Uma nova aba abrirá automaticamente no seu navegador contendo o e-mail simulado com o link de redefinição. +8. Clique no link, defina uma nova senha e faça o login. + +----- + +## Rodando a Aplicação + +Para iniciar o servidor web: + +```bash +rails s +``` + +Acesse a aplicação em: [http://localhost:3000](https://www.google.com/search?q=http://localhost:3000) + +## Rodando os Testes + +O projeto conta com testes unitários (RSpec) e de aceitação (Cucumber). + + * **RSpec:** + + ```bash + bundle exec rspec + ``` + + * **Cucumber:** + + ```bash + bundle exec cucumber + ``` + + * **Verificar Qualidade (RubyCritic):** + + ```bash + bundle exec rubycritic + ``` diff --git a/class_members.json b/class_members.json index 733560ef03..ad764fc7b7 100755 --- a/class_members.json +++ b/class_members.json @@ -5,408 +5,39 @@ "semester": "2021.2", "dicente": [ { - "nome": "Ana Clara Jordao Perna", + "nome": "Abel Ferreira", "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", "matricula": "190084006", "usuario": "190084006", "formacao": "graduando", "ocupacao": "dicente", - "email": "acjpjvjp@gmail.com" + "email": "832572@aluno.unb.br" }, { - "nome": "Andre Carvalho de Roure", + "nome": "Backugan Junior", "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", "matricula": "200033522", "usuario": "200033522", "formacao": "graduando", "ocupacao": "dicente", - "email": "andreCarvalhoroure@gmail.com" + "email": "backugan@gmail.com " }, { - "nome": "André Carvalho Marques", + "nome": "Fulano Dital Pereira", "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", "matricula": "150005491", "usuario": "150005491", "formacao": "graduando", "ocupacao": "dicente", - "email": "andre.acm97@outlook.com" - }, - { - "nome": "Antonio Vinicius de Moura Rodrigues", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190084502", - "usuario": "190084502", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "antoniovmoura.r@gmail.com" - }, - { - "nome": "Arthur Barreiros de Oliveira Mota", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190102829", - "usuario": "190102829", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "arthurbarreirosmota@gmail.com" - }, - { - "nome": "ARTHUR RODRIGUES NEVES", - "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", - "matricula": "202014403", - "usuario": "202014403", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "arthurcontroleambiental@gmail.com" - }, - { - "nome": "Bianca Glycia Boueri", - "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", - "matricula": "170161561", - "usuario": "170161561", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "biancaglyciaboueri@gmail.com" - }, - { - "nome": "Caio Otávio Peluti Alencar", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190085312", - "usuario": "190085312", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "peluticaio@gmail.com" - }, - { - "nome": "Camila Frealdo Fraga", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "170007561", - "usuario": "170007561", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "camilizx2021@gmail.com" - }, - { - "nome": "Claudio Roberto Oliveira Peres de Barros", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190097591", - "usuario": "190097591", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "dinhobarros15@gmail.com" - }, - { - "nome": "Daltro Oliveira Vinuto", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "160025966", - "usuario": "160025966", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "daltroov777@gmail.com" - }, - { - "nome": "Davi de Moura Amaral", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "200016750", - "usuario": "200016750", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "davimouraamaral@gmail.com" - }, - { - "nome": "Eduardo Xavier Dantas", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190086530", - "usuario": "190086530", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "eduardoxdantas@gmail.com" - }, - { - "nome": "Enzo Nunes Leal Sampaio", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190062789", - "usuario": "190062789", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "enzonleal2016@hotmail.com" - }, - { - "nome": "Enzo Yoshio Niho", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190027304", - "usuario": "190027304", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "enzoyn@hotmail.com" - }, - { - "nome": "Gabriel Faustino Lima da Rocha", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190013249", - "usuario": "190013249", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "gabrielfaustino99@gmail.com" - }, - { - "nome": "Gabriel Ligoski", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190087498", - "usuario": "190087498", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "gabriel.ligoski@gmail.com" - }, - { - "nome": "GABRIEL MENDES CIRIATICO GUIMARÃES", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "202033202", - "usuario": "202033202", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "gabrielciriatico@gmail.com" - }, - { - "nome": "Gustavo Rodrigues dos Santos", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190014121", - "usuario": "190014121", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "190014121@aluno.unb.br" - }, - { - "nome": "Gustavo Rodrigues Gualberto", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190108266", - "usuario": "190108266", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "gustavorgualberto@gmail.com" - }, - { - "nome": "Igor David Morais", - "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", - "matricula": "180102141", - "usuario": "180102141", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "igordavid13@gmail.com" - }, - { - "nome": "Jefte Augusto Gomes Batista", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "180057570", - "usuario": "180057570", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "ndaffte@gmail.com" - }, - { - "nome": "Karolina de Souza Silva", - "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", - "matricula": "190046791", - "usuario": "190046791", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "karolinasouza@outlook.com" - }, - { - "nome": "Kléber Rodrigues da Costa Júnior", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "200053680", - "usuario": "200053680", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "kleberrjr7@gmail.com" - }, - { - "nome": "Luca Delpino Barbabella", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "180125559", - "usuario": "180125559", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "barbadluca@gmail.com" - }, - { - "nome": "Lucas de Almeida Abreu Faria", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "170016668", - "usuario": "170016668", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "lucasaafaria@gmail.com" - }, - { - "nome": "Lucas Gonçalves Ramalho", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190098091", - "usuario": "190098091", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "lucasramalho29@gmail.com" - }, - { - "nome": "Lucas Monteiro Miranda", - "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", - "matricula": "170149684", - "usuario": "170149684", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "luquinha_miranda@hotmail.com" - }, - { - "nome": "Lucas Resende Silveira Reis", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "180144421", - "usuario": "180144421", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "180144421@aluno.unb.br" - }, - { - "nome": "Luis Fernando Freitas Lamellas", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190016841", - "usuario": "190016841", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "lflamellas@icloud.com" - }, - { - "nome": "Luiza de Araujo Nunes Gomes", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190112794", - "usuario": "190112794", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "luizangomes@outlook.com" - }, - { - "nome": "Marcelo Aiache Postiglione", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "180126652", - "usuario": "180126652", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "180126652@aluno.unb.br" - }, - { - "nome": "Marcelo Junqueira Ferreira", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "200023624", - "usuario": "200023624", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "marcelojunqueiraf@gmail.com" - }, - { - "nome": "MARIA EDUARDA CARVALHO SANTOS", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190092556", - "usuario": "190092556", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "auntduda@gmail.com" - }, - { - "nome": "Maria Eduarda Lacerda Dantas", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "200067184", - "usuario": "200067184", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "lacwerda@gmail.com" - }, - { - "nome": "Maylla Krislainy de Sousa Silva", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190043873", - "usuario": "190043873", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "mayllak@hotmail.com" - }, - { - "nome": "Pedro Cesar Ribeiro Passos", - "curso": "ENGENHARIA MECATRÔNICA - CONTROLE E AUTOMAÇÃO/FTD", - "matricula": "180139312", - "usuario": "180139312", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "pedrocesarribeiro2013@gmail.com" - }, - { - "nome": "Rafael Mascarenhas Dal Moro", - "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", - "matricula": "170021041", - "usuario": "170021041", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "170021041@aluno.unb.br" - }, - { - "nome": "Rodrigo Mamedio Arrelaro", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190095164", - "usuario": "190095164", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "arrelaro1@hotmail.com" - }, - { - "nome": "Thiago de Oliveira Albuquerque", - "curso": "ENGENHARIA DE COMPUTAÇÃO/CIC", - "matricula": "140177442", - "usuario": "140177442", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "thiago.work.ti@outlook.com" - }, - { - "nome": "Thiago Elias dos Reis", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190126892", - "usuario": "190126892", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "thiagoeliasdosreis01@gmail.com" - }, - { - "nome": "Victor Hugo Rodrigues Fernandes", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "180132041", - "usuario": "180132041", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "aluno0sem.luz@gmail.com" - }, - { - "nome": "Vinicius Lima Passos", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "200028545", - "usuario": "200028545", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "viniciuslimapassos@gmail.com" - }, - { - "nome": "William Xavier dos Santos", - "curso": "CIÊNCIA DA COMPUTAÇÃO/CIC", - "matricula": "190075384", - "usuario": "190075384", - "formacao": "graduando", - "ocupacao": "dicente", - "email": "wilxavier@me.com" + "email": "dital.fulano@gmail.com " } ], "docente": { - "nome": "MARISTELA TERTO DE HOLANDA", + "nome": "Florentino Perez", "departamento": "DEPTO CIÊNCIAS DA COMPUTAÇÃO", "formacao": "DOUTORADO", "usuario": "83807519491", - "email": "mholanda@unb.br", + "email": "professor@gmail.com ", "ocupacao": "docente" } } diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 0000000000..325bfc036d --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,51 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Ignore CI service files. +/.github + +# Ignore Kamal files. +/config/deploy*.yml +/.kamal + +# Ignore development files +/.devcontainer + +# Ignore Docker-related files +/.dockerignore +/Dockerfile* diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000000..f595fb2168 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,59 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead. +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets +.byebug_history + +# Ignore master key for decrypting credentials and more. +/config/master.key + +/db/*.sqlite3 +*.sqlite3-journal +*.sqlite3-wal +*.sqlite3-shm + +# Ignore all environment files (except templates). +/.env +/.env.production +/.env.development +/.env.test + +# Ignore some generated files +/coverage +/public/packs +/public/packs-test +/node_modules +/yarn-error.log +.yarn/ +.yarnrc.yml + +# Ignore Mac and Linux file system files +*.swp +.DS_Store + +/app/assets/builds/* +!/app/assets/builds/.keep diff --git a/src/.kamal/hooks/docker-setup.sample b/src/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000000..2fb07d7d7a --- /dev/null +++ b/src/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/src/.kamal/hooks/post-app-boot.sample b/src/.kamal/hooks/post-app-boot.sample new file mode 100755 index 0000000000..70f9c4bc95 --- /dev/null +++ b/src/.kamal/hooks/post-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/src/.kamal/hooks/post-deploy.sample b/src/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000000..fd364c2a77 --- /dev/null +++ b/src/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/src/.kamal/hooks/post-proxy-reboot.sample b/src/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 0000000000..1435a677f2 --- /dev/null +++ b/src/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/src/.kamal/hooks/pre-app-boot.sample b/src/.kamal/hooks/pre-app-boot.sample new file mode 100755 index 0000000000..45f7355045 --- /dev/null +++ b/src/.kamal/hooks/pre-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/src/.kamal/hooks/pre-build.sample b/src/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000000..c5a55678b2 --- /dev/null +++ b/src/.kamal/hooks/pre-build.sample @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/src/.kamal/hooks/pre-connect.sample b/src/.kamal/hooks/pre-connect.sample new file mode 100755 index 0000000000..77744bdca8 --- /dev/null +++ b/src/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/src/.kamal/hooks/pre-deploy.sample b/src/.kamal/hooks/pre-deploy.sample new file mode 100755 index 0000000000..05b3055b72 --- /dev/null +++ b/src/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,122 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = github_repo_from_remote_url + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end + + private + def github_repo_from_remote_url + url = `git config --get remote.origin.url`.strip.delete_suffix(".git") + if url.start_with?("https://github.com/") + url.delete_prefix("https://github.com/") + elsif url.start_with?("git@github.com:") + url.delete_prefix("git@github.com:") + else + url + end + end +end + + +$stdout.sync = true + +begin + puts "Checking build status..." + + attempts = 0 + checks = GithubStatusChecks.new + + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/src/.kamal/hooks/pre-proxy-reboot.sample b/src/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 0000000000..061f8059e6 --- /dev/null +++ b/src/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/src/.kamal/secrets b/src/.kamal/secrets new file mode 100644 index 0000000000..9a771a3985 --- /dev/null +++ b/src/.kamal/secrets @@ -0,0 +1,17 @@ +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Example of extracting secrets from 1password (or another compatible pw manager) +# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) +# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) +# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) + +# Use a GITHUB_TOKEN if private repositories are needed for the image +# GITHUB_TOKEN=$(gh config get -h github.com oauth_token) + +# Grab the registry password from ENV +KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD + +# Improve security by using a password manager. Never check config/master.key into git! +RAILS_MASTER_KEY=$(cat config/master.key) diff --git a/src/.reek.yml b/src/.reek.yml new file mode 100644 index 0000000000..b08953aef8 --- /dev/null +++ b/src/.reek.yml @@ -0,0 +1,4 @@ +directories: + "app/controllers": + InstanceVariableAssumption: + enabled: false \ No newline at end of file diff --git a/src/.rspec b/src/.rspec new file mode 100644 index 0000000000..c99d2e7396 --- /dev/null +++ b/src/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/src/.rubocop.yml b/src/.rubocop.yml new file mode 100644 index 0000000000..f9d86d4a54 --- /dev/null +++ b/src/.rubocop.yml @@ -0,0 +1,8 @@ +# Omakase Ruby styling for Rails +inherit_gem: { rubocop-rails-omakase: rubocop.yml } + +# Overwrite or add rules to create your own house style +# +# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` +# Layout/SpaceInsideArrayLiteralBrackets: +# Enabled: false diff --git a/src/.ruby-version b/src/.ruby-version new file mode 100644 index 0000000000..ab96aa90d1 --- /dev/null +++ b/src/.ruby-version @@ -0,0 +1 @@ +ruby-3.2.3 diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000000..0ac76afa77 --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,72 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build -t camaar_proj . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name camaar_proj camaar_proj + +# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.2.3 +FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + + + +# Final stage for app image +FROM base + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + chown -R rails:rails db log storage tmp +USER 1000:1000 + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server via Thruster by default, this can be overwritten at runtime +EXPOSE 80 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/src/Gemfile b/src/Gemfile new file mode 100644 index 0000000000..fd9378d1f7 --- /dev/null +++ b/src/Gemfile @@ -0,0 +1,78 @@ +source "https://rubygems.org" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 8.0.3" +# The modern asset pipeline for Rails [https://github.com/rails/propshaft] +gem "propshaft" +# Use sqlite3 as the database for Active Record +gem "sqlite3", ">= 2.1" +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" +# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] +gem "importmap-rails" +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails" +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails" +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[ windows jruby ] + +# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Deploy this application anywhere as a Docker container [https://kamal-deploy.org] +gem "kamal", require: false + +# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] +gem "thruster", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +# gem "image_processing", "~> 1.2" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + gem "rubycritic", require: false + gem "flog", "4.8.0", require: false + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false + + gem 'cucumber-rails', require: false + gem 'rspec-rails' + gem 'rails-controller-testing' + gem 'database_cleaner' + gem 'database_cleaner-active_record' + gem 'factory_bot_rails' + gem 'faker' +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" + gem 'letter_opener' + gem 'rdoc' +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" + gem "simplecov", require: false +end + +gem "tailwindcss-rails", "~> 4.4" diff --git a/src/Gemfile.lock b/src/Gemfile.lock new file mode 100644 index 0000000000..5de4d59842 --- /dev/null +++ b/src/Gemfile.lock @@ -0,0 +1,558 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (8.0.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + mail (>= 2.8.0) + actionmailer (8.0.4) + actionpack (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activesupport (= 8.0.4) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.0.4) + actionview (= 8.0.4) + activesupport (= 8.0.4) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.0.4) + actionpack (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.0.4) + activesupport (= 8.0.4) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.0.4) + activesupport (= 8.0.4) + globalid (>= 0.3.6) + activemodel (8.0.4) + activesupport (= 8.0.4) + activerecord (8.0.4) + activemodel (= 8.0.4) + activesupport (= 8.0.4) + timeout (>= 0.4.0) + activestorage (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activesupport (= 8.0.4) + marcel (~> 1.0) + activesupport (8.0.4) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.8) + public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + base64 (0.3.0) + bcrypt (3.1.20) + bcrypt_pbkdf (1.1.1) + bcrypt_pbkdf (1.1.1-x86-mingw32) + benchmark (0.5.0) + bigdecimal (3.3.1) + bindex (0.8.1) + bootsnap (1.18.6) + msgpack (~> 1.2) + brakeman (7.1.1) + racc + builder (3.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + childprocess (5.1.0) + logger (~> 1.5) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + concurrent-ruby (1.3.6) + connection_pool (2.5.4) + crass (1.0.6) + cucumber (10.1.1) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 11) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 19) + cucumber-html-formatter (> 20.3, < 22) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.3) + cucumber-ci-environment (10.0.1) + cucumber-core (15.3.0) + cucumber-gherkin (> 27, < 35) + cucumber-messages (> 26, < 30) + cucumber-tag-expressions (> 5, < 9) + cucumber-cucumber-expressions (18.0.1) + bigdecimal + cucumber-gherkin (34.0.0) + cucumber-messages (> 25, < 29) + cucumber-html-formatter (21.15.1) + cucumber-messages (> 19, < 28) + cucumber-messages (27.2.0) + cucumber-rails (4.0.0) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 11) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.0.0) + database_cleaner (2.1.0) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.0.1) + date (3.5.0) + debug (1.11.0) + irb (~> 1.10) + reline (>= 0.3.8) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + diff-lcs (1.6.2) + docile (1.4.1) + dotenv (3.1.8) + drb (2.2.3) + dry-configurable (1.3.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-core (1.1.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.2.0) + dry-initializer (3.2.0) + dry-logic (1.6.0) + bigdecimal + concurrent-ruby (~> 1.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-schema (1.14.1) + concurrent-ruby (~> 1.0) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.1) + dry-initializer (~> 3.2) + dry-logic (~> 1.5) + dry-types (~> 1.8) + zeitwerk (~> 2.6) + dry-types (1.8.3) + bigdecimal (~> 3.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + ed25519 (1.4.0) + erb (6.0.0) + erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + faker (3.5.2) + i18n (>= 1.8.11, < 2) + ffi (1.17.2-x86-mingw32) + ffi (1.17.2-x86_64-linux-gnu) + flay (2.13.3) + erubi (~> 1.10) + path_expander (~> 1.0) + ruby_parser (~> 3.0) + sexp_processor (~> 4.0) + flog (4.8.0) + path_expander (~> 1.0) + ruby_parser (~> 3.1, > 3.1.0) + sexp_processor (~> 4.8) + fugit (1.12.1) + et-orbi (~> 1.4) + raabro (~> 1.4) + globalid (1.3.0) + activesupport (>= 6.1) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + importmap-rails (2.2.2) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.1) + irb (1.15.3) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.14.1) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + json (2.16.0) + kamal (2.8.2) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.4) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + language_server-protocol (3.17.0.5) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + letter_opener (1.10.0) + launchy (>= 2.2, < 4) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.24.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + matrix (0.4.3) + memoist3 (1.0.0) + mini_mime (1.1.5) + mini_portile2 (2.8.9) + minitest (5.26.1) + msgpack (1.8.0) + multi_test (1.1.0) + net-imap (0.5.12) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.1) + net-protocol + net-ssh (7.3.0) + nio4r (2.7.5) + nokogiri (1.18.10) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-gnu) + racc (~> 1.4) + ostruct (0.6.3) + parallel (1.27.0) + parser (3.3.10.0) + ast (~> 2.4.1) + racc + path_expander (1.1.3) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.6.0) + propshaft (1.3.1) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + psych (5.2.6) + date + stringio + public_suffix (7.0.0) + puma (7.1.0) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.2.4) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails (8.0.4) + actioncable (= 8.0.4) + actionmailbox (= 8.0.4) + actionmailer (= 8.0.4) + actionpack (= 8.0.4) + actiontext (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activemodel (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + 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 + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.0.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.3.1) + rdoc (6.15.1) + erb + psych (>= 4.0.0) + tsort + reek (6.5.0) + dry-schema (~> 1.13) + logger (~> 1.6) + parser (~> 3.3.0) + rainbow (>= 2.0, < 4.0) + rexml (~> 3.1) + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + rexml (3.4.4) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.7) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.2) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.6) + rubocop (1.81.7) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.48.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.33.4) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails-omakase (1.1.0) + rubocop (>= 1.72) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.30) + ruby-progressbar (1.13.0) + ruby_parser (3.21.1) + racc (~> 1.5) + sexp_processor (~> 4.16) + rubycritic (4.11.0) + flay (~> 2.13) + flog (~> 4.7) + launchy (>= 2.5.2) + parser (>= 3.3.0.5) + rainbow (~> 3.1.1) + reek (~> 6.5.0, < 7.0) + rexml + ruby_parser (~> 3.21) + simplecov (>= 0.22.0) + tty-which (~> 0.5.0) + virtus (~> 2.0) + rubyzip (3.2.2) + securerandom (0.4.1) + selenium-webdriver (4.38.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + sexp_processor (4.17.4) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + solid_cable (3.0.12) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.10) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.2.4) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) + sqlite3 (2.8.0) + mini_portile2 (~> 2.8.0) + sqlite3 (2.8.0-x86_64-linux-gnu) + sshkit (1.24.0) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.1.8) + sys-uname (1.4.1) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + tailwindcss-rails (4.4.0) + railties (>= 7.0.0) + tailwindcss-ruby (~> 4.0) + tailwindcss-ruby (4.1.16) + tailwindcss-ruby (4.1.16-x86_64-linux-gnu) + thor (1.4.0) + thread_safe (0.3.6) + thruster (0.1.16) + thruster (0.1.16-x86_64-linux) + timeout (0.4.4) + tsort (0.2.0) + tty-which (0.5.0) + turbo-rails (2.0.20) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + tzinfo-data (1.2025.2) + tzinfo (>= 1.0.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + uri (1.1.1) + useragent (0.16.11) + virtus (2.0.0) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.7.3) + +PLATFORMS + x86-mingw32 + x86_64-linux + x86_64-linux-gnu + +DEPENDENCIES + bcrypt (~> 3.1.7) + bootsnap + brakeman + capybara + cucumber-rails + database_cleaner + database_cleaner-active_record + debug + factory_bot_rails + faker + flog (= 4.8.0) + importmap-rails + jbuilder + kamal + letter_opener + propshaft + puma (>= 5.0) + rails (~> 8.0.3) + rails-controller-testing + rdoc + rspec-rails + rubocop-rails-omakase + rubycritic + selenium-webdriver + simplecov + solid_cable + solid_cache + solid_queue + sqlite3 (>= 2.1) + stimulus-rails + tailwindcss-rails (~> 4.4) + thruster + turbo-rails + tzinfo-data + web-console + +BUNDLED WITH + 2.4.19 diff --git a/src/Procfile.dev b/src/Procfile.dev new file mode 100644 index 0000000000..da151fee94 --- /dev/null +++ b/src/Procfile.dev @@ -0,0 +1,2 @@ +web: bin/rails server +css: bin/rails tailwindcss:watch diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000000..54981286e5 --- /dev/null +++ b/src/README.md @@ -0,0 +1,111 @@ +# CAMAAR - Sistema de Gestão Acadêmica + +Este projeto utiliza **Ruby on Rails 8** e **SQLite**. + +## Pré-requisitos + +* **Ruby:** Versão 3.2.0 ou superior (Requisito do Rails 8). +* **Bundler:** Para gerenciar as dependências. +* **SQLite3:** Banco de dados utilizado. + +## Instalação + +1. **Instale as dependências:** + ```bash + bundle install + ``` + +2. **Configuração do Banco de Dados:** + Execute o comando para criar o banco e rodar as migrações: + ```bash + rails db:setup + ``` + +--- + +## ⚠️ Configuração do Usuário Administrador + +Para acessar o sistema como administrador, você deve verificar se já existe um usuário criado ou criar um manualmente via terminal, conforme os cenários abaixo. + +### Passo 1: Acesse o Console do Rails +No terminal, na raiz do projeto, execute: +```bash +rails console +```` + +### Passo 2: Verifique se já existe um Admin + +Dentro do console, execute o comando abaixo para buscar um administrador: + +```ruby +admin = Usuario.where(ocupacao: 'admin').first +puts admin ? "Email encontrado: #{admin.email}" : "Nenhum admin encontrado." +``` + +### Passo 3: Decida o fluxo com base no resultado + +#### **Cenário A: Nenhum admin encontrado** + +Ainda dentro do console, copie e cole o código abaixo para criar um novo administrador: + +```ruby +Usuario.create!( + nome: "Administrador", + email: "admin@test.com", + usuario: "admin", + matricula: "000000", + password: "password123", + password_confirmation: "password123", + ocupacao: :admin, + status: true +) +``` + +*Agora você pode logar com o email `admin@test.com` e a senha `password123`.* + +#### **Cenário B: Admin encontrado (Recuperação de Senha)** + +Se o console retornou um usuário (ex: `admin@test.com`) mas você não sabe a senha: + +1. Copie o e-mail retornado no console. +2. Saia do console (digite `exit`). +3. Inicie o servidor (veja a seção "Rodando a Aplicação" abaixo). +4. Acesse a tela de login no navegador. +5. Clique em **"Esqueci minha senha"**. +6. Insira o e-mail do administrador. +7. O sistema utiliza a gem `letter_opener`. Uma nova aba abrirá automaticamente no seu navegador contendo o e-mail simulado com o link de redefinição. +8. Clique no link, defina uma nova senha e faça o login. + +----- + +## Rodando a Aplicação + +Para iniciar o servidor web: + +```bash +rails s +``` + +Acesse a aplicação em: [http://localhost:3000](https://www.google.com/search?q=http://localhost:3000) + +## Rodando os Testes + +O projeto conta com testes unitários (RSpec) e de aceitação (Cucumber). + + * **RSpec:** + + ```bash + bundle exec rspec + ``` + + * **Cucumber:** + + ```bash + bundle exec cucumber + ``` + + * **Verificar Qualidade (RubyCritic):** + + ```bash + bundle exec rubycritic + ``` diff --git a/src/Rakefile b/src/Rakefile new file mode 100644 index 0000000000..9a5ea7383a --- /dev/null +++ b/src/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/src/app/assets/builds/.keep b/src/app/assets/builds/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/assets/images/.keep b/src/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/assets/stylesheets/application.css b/src/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..fe93333c0f --- /dev/null +++ b/src/app/assets/stylesheets/application.css @@ -0,0 +1,10 @@ +/* + * This is a manifest file that'll be compiled into application.css. + * + * With Propshaft, assets are served efficiently without preprocessing steps. You can still include + * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard + * cascading order, meaning styles declared later in the document or manifest will override earlier ones, + * depending on specificity. + * + * Consider organizing styles into separate files for maintainability. + */ diff --git a/src/app/assets/tailwind/application.css b/src/app/assets/tailwind/application.css new file mode 100644 index 0000000000..f1d8c73cdc --- /dev/null +++ b/src/app/assets/tailwind/application.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/src/app/controllers/admin_controller.rb b/src/app/controllers/admin_controller.rb new file mode 100644 index 0000000000..a4a95bc816 --- /dev/null +++ b/src/app/controllers/admin_controller.rb @@ -0,0 +1,53 @@ +# Controlador responsável pela área administrativa de gerenciamento. +# Requer autenticação de administrador. +class AdminController < ApplicationController + before_action :authenticate_admin + + # Renderiza a página inicial administrativa. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Renderiza a view index. + # + # Efeitos Colaterais: + # - Nenhum. + def index + end + + # Ação para importar dados do SIGAA manualmente via interface. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Redireciona de volta com flash message. + # + # Efeitos Colaterais: + # - Executa SigaaImporter.call. + # - Define flash[:notice] ou flash[:alert]. + def importar_dados + begin + SigaaImporter.call + flash[:notice] = "Dados importados com sucesso!" + rescue StandardError => e + flash[:alert] = e.message + end + redirect_back(fallback_location: "/admin/gerenciamento") + end + + # Renderiza a página de gerenciamento de dados do administrador. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Renderiza a view gerenciamento. + # + # Efeitos Colaterais: + # - Define variável de instância @sistema_tem_dados. + def gerenciamento + @sistema_tem_dados = Turma.exists? + end +end diff --git a/src/app/controllers/application_controller.rb b/src/app/controllers/application_controller.rb new file mode 100644 index 0000000000..c2e8e81253 --- /dev/null +++ b/src/app/controllers/application_controller.rb @@ -0,0 +1,78 @@ +# Controlador base da aplicação, contendo lógica compartilhada de autenticação. +class ApplicationController < ActionController::Base + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern + helper_method :current_usuario, :logged_in? + + # Retorna o usuário atualmente autenticado na sessão. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (Usuario): O usuário logado ou nil. + # + # Efeitos Colaterais: + # - Consulta ao banco de dados (memoized). + def current_usuario + @current_usuario ||= Usuario.find_by(id: session[:usuario_id]) + end + + # Filtro para requerer autenticação de qualquer usuário. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Redireciona se não logado. + # + # Efeitos Colaterais: + # - Redireciona para login_path com alerta. + def authenticate_usuario + redirect_to login_path, alert: "Faça login para continuar." unless current_usuario.present? + end + + # Filtro para requerer autenticação de administrador. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Redireciona se não for admin. + # + # Efeitos Colaterais: + # - Redireciona para root_path com alerta. + def authenticate_admin + redirect_to root_path, alert: "Acesso negado." unless current_usuario&.admin? + end + + # Verifica se existe um usuário logado. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (Boolean): True se existe usuário logado, false caso contrário. + # + # Efeitos Colaterais: + # - Nenhum. + def logged_in? + current_usuario.present? + end + + # Alias/Filtro para exigir login (similar a authenticate_usuario). + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Redireciona se não logado. + # + # Efeitos Colaterais: + # - Redireciona para login_path. + def require_login + unless logged_in? + redirect_to login_path, alert: "Você precisa estar logado para acessar esta página." + end + end +end diff --git a/src/app/controllers/autenticacao_controller.rb b/src/app/controllers/autenticacao_controller.rb new file mode 100644 index 0000000000..3f93a2be6e --- /dev/null +++ b/src/app/controllers/autenticacao_controller.rb @@ -0,0 +1,107 @@ +# Gerencia o ciclo de vida da autenticação (Login e Logout). +class AutenticacaoController < ApplicationController + skip_before_action :require_login, only: %i[new create], raise: false + layout "auth" + + # Renderiza a página de formulário de login. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Renderiza a view new. + # + # Efeitos Colaterais: + # - Nenhum. + def new + # Renderiza a página de login + end + + # Processa o envio do formulário de login. + # + # Argumentos: + # - params[:email] (String): Login digitado pelo usuário. + # - params[:password] (String): Senha. + # + # Retorno: + # - (NilClass): Redireciona em sucesso. + # + # Efeitos Colaterais: + # - Consulta DB. + # - Cria sessão se sucesso. + # - Chama handle_auth_failure se erro. + def create + user = Usuario.authenticate(params[:email], params[:password]) + + initialize_session(user) + redirect_based_on_role(user) + + rescue AuthenticationError => e + handle_auth_failure(e) + end + + # Realiza o logout do usuário. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Redireciona para login_path. + # + # Efeitos Colaterais: + # - Reseta sessão. + # - Remove cookie auth_token. + def destroy + reset_session + cookies.delete(:auth_token) + redirect_to login_path, notice: "Deslogado com sucesso." + end + + private + + # Inicializa a sessão do usuário. + # + # Argumentos: + # - user (Usuario): O usuário autenticado. + # + # Retorno: + # - (Integer): O ID salvo na sessão. + # + # Efeitos Colaterais: + # - Grava session[:usuario_id]. + def initialize_session(user) + session[:usuario_id] = user.id + end + + # Redireciona o usuário para a página apropriada baseada no seu papel. + # + # Argumentos: + # - user (Usuario): O usuário. + # + # Retorno: + # - (NilClass): Redireciona. + # + # Efeitos Colaterais: + # - Redireciona para admin_gerenciamento_path ou root_path. + def redirect_based_on_role(user) + if user.admin? + redirect_to admin_gerenciamento_path, notice: "Bem-vindo, Administrador!" + else + redirect_to root_path, notice: "Login realizado com sucesso!" + end + end + + # Lida com falhas de autenticação. + # + # Argumentos: + # - exception (StandardError): Exceção capturada. + # + # Retorno: + # - (NilClass): Redireciona para login_path. + # + # Efeitos Colaterais: + # - Redireciona com alerta. + def handle_auth_failure(exception) + redirect_to login_path, alert: exception.message + end +end \ No newline at end of file diff --git a/src/app/controllers/avaliacoes_controller.rb b/src/app/controllers/avaliacoes_controller.rb new file mode 100644 index 0000000000..e4b0915e15 --- /dev/null +++ b/src/app/controllers/avaliacoes_controller.rb @@ -0,0 +1,16 @@ +# Controlador responsável por listar as avaliações pendentes do usuário. +class AvaliacoesController < ApplicationController + # Lista avaliações que o usuário ainda não respondeu. + # + # Argumentos: + # - Nenhum (usa current_usuario implícito). + # + # Retorno: + # - (NilClass): Renderiza index. + # + # Efeitos Colaterais: + # - Define variável @pendencias. + def index + @pendencias = current_usuario.pendencias + end +end diff --git a/src/app/controllers/concerns/.keep b/src/app/controllers/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/controllers/definicao_senha_controller.rb b/src/app/controllers/definicao_senha_controller.rb new file mode 100644 index 0000000000..3967ce6901 --- /dev/null +++ b/src/app/controllers/definicao_senha_controller.rb @@ -0,0 +1,127 @@ +# Controlador responsável pelo fluxo de definição de senha inicial para novos usuários. +class DefinicaoSenhaController < ApplicationController + skip_before_action :require_login, raise: false + before_action :reset_session_before_start, only: [:new] + before_action :resolve_user_from_token, only: %i[new create] + + layout "auth" + + # Renderiza a página de definição de senha. + # + # Argumentos: + # - params[:token] (String): Token assinado para identificar o usuário. + # + # Retorno: + # - (NilClass): Renderiza new ou redireciona. + # + # Efeitos Colaterais: + # - Verifica status do usuário. + def new + if @usuario.status + redirect_to login_path, notice: "Você já está ativo. Faça o login." + end + end + + # Processa a definição da nova senha. + # + # Argumentos: + # - params[:usuario][:password] (String). + # - params[:usuario][:password_confirmation] (String). + # + # Retorno: + # - (NilClass): Redireciona login_path em sucesso ou re-renderiza new. + # + # Efeitos Colaterais: + # - Atualiza senha e status do usuário no DB. + def create + return unless inputs_present? + + if @usuario.update(user_params.merge(status: true)) + redirect_to login_path, notice: "Senha definida com sucesso! Você já pode fazer o login." + else + handle_update_error + end + end + + private + + # Sanitiza parâmetros do usuário. + # + # Retorno: + # - (ActionController::Parameters): Parâmetros permitidos. + def user_params + params.require(:usuario).permit(:password, :password_confirmation) + end + + # Garante que não há sessão ativa ao iniciar definição de senha. + # + # Efeitos Colaterais: + # - Limpa sessão. + def reset_session_before_start + return unless session[:usuario_id].present? + + session[:usuario_id] = nil + flash[:notice] = "Sessão anterior encerrada para configurar nova conta." + end + + # Busca usuário via token assinado. + # + # Argumentos: + # - params[:token] + # + # Efeitos Colaterais: + # - Redireciona se token inválido. + # - Define @usuario. + def resolve_user_from_token + token = params[:token] + + if token.blank? + redirect_to login_path, alert: "Link inválido (token ausente)." + return + end + + @usuario = Usuario.find_signed(token, purpose: :definir_senha) + + return if @usuario + + redirect_to login_path, alert: "Link inválido ou expirado." + end + + # Verifica se campos de senha foram preenchidos. + # + # Retorno: + # - (Boolean): True se válidos. + # + # Efeitos Colaterais: + # - Renderiza erro se inválidos. + def inputs_present? + p = user_params + if p[:password].blank? || p[:password_confirmation].blank? + flash.now[:alert] = "Todos os campos devem ser preenchidos." + render :new, status: :unprocessable_content + return false + end + true + end + + # Lida com erros na atualização do usuário. + # + # Efeitos Colaterais: + # - Renderiza erro. + def handle_update_error + flash.now[:alert] = error_message_for_update + render :new, status: :unprocessable_content + end + + # Gera mensagem de erro amigável. + # + # Retorno: + # - (String): A mensagem de erro. + def error_message_for_update + if @usuario.errors[:password_confirmation].present? + "As senhas não conferem." + else + @usuario.errors.full_messages.to_sentence + end + end +end \ No newline at end of file diff --git a/src/app/controllers/formularios_controller.rb b/src/app/controllers/formularios_controller.rb new file mode 100644 index 0000000000..31ac89dad7 --- /dev/null +++ b/src/app/controllers/formularios_controller.rb @@ -0,0 +1,154 @@ +# Controlador para criação e distribuição de formulários de avaliação. +class FormulariosController < ApplicationController + before_action :require_login + before_action :authorize_admin, only: %i[new create index] + + # Lista os formulários existentes. + # + # Retorno: + # - Renderiza a view :index. + def index + @templates = Template.all + @turmas = Turma.includes(:materia).all + @formularios = Formulario.all.includes(:template, :turma) + end + + # Renderiza formulário para criar novo envio de avaliação. + # + # Retorno: + # - Renderiza a view :new. + def new + @templates = Template.all + @turmas = Turma.includes(:materia).all + end + + # Exibe os detalhes de um formulário enviado. + # + # Argumentos: + # - params[:id] (Integer): ID do formulário. + # + # Retorno: + # - Renderiza a view :show. + def show + @formulario = Formulario.find(params[:id]) + end + + # Processa a criação e distribuição dos formulários. + # + # Retorno: + # - Redireciona para formularios_path se sucesso. + # - Renderiza novamente em falha. + # + # Efeitos Colaterais: + # - Cria Formularios e Respostas. + def create + return unless valid_params? + + distribute_forms_transaction + + redirect_to formularios_path, notice: success_message + rescue ActiveRecord::RecordInvalid => e + handle_error(e) + end + + # Lista avaliações pendentes para o usuário atual (Discente). + # + # Retorno: + # - Renderiza a view :pendentes. + # + # Efeitos Colaterais: + # - Define @respostas_pendentes. + def pendentes + if current_usuario.matriculas.empty? + flash.now[:alert] = "Você não possui turmas cadastradas" + @respostas_pendentes = [] + else + @respostas_pendentes = Resposta.where(participante: current_usuario, data_submissao: nil) + .includes(formulario: [:template, { turma: :materia }]) + end + end + + private + + # Garante acesso apenas a administradores. + def authorize_admin + redirect_to root_path, alert: "Acesso restrito." unless current_usuario&.admin? + end + + # Valida os parâmetros de criação. + # + # Retorno: + # - (Boolean): true se válido, false caso contrário. + def valid_params? + if params[:template_id].blank? + redirect_with_alert("Selecione um template") + return false + end + + if (params[:turma_ids] || []).empty? + redirect_with_alert("Selecione pelo menos uma turma") + return false + end + + true + end + + # Auxiliar para redirecionamento com alerta. + def redirect_with_alert(msg) + flash[:alert] = msg + redirect_to new_formulario_path + end + + # Executa a distribuição de formulários em transação. + def distribute_forms_transaction + template = Template.find(params[:template_id]) + + ActiveRecord::Base.transaction do + params[:turma_ids].each do |turma_id| + process_single_distribution(turma_id, template) + end + end + end + + # Distribui formulário para uma única turma. + def process_single_distribution(turma_id, template) + turma = Turma.find(turma_id) + + form = create_formulario!(turma, template) + generate_empty_responses!(form, turma) + end + + # Cria o registro Formulario. + def create_formulario!(turma, template) + Formulario.create!( + template_id: template.id, + turma_id: turma.id, + titulo_envio: template.titulo, + data_criacao: Time.current, + data_encerramento: params[:data_encerramento] + ) + end + + # Gera respostas vazias para os alunos. + def generate_empty_responses!(form, turma) + turma.matriculas.each do |matricula| + Resposta.create!( + formulario: form, + participante: matricula.usuario, + data_submissao: nil + ) + end + end + + # Gera mensagem de sucesso. + def success_message + count = params[:turma_ids]&.count || 0 + "Formulário distribuído com sucesso para #{count} turmas" + end + + # Trata erros de criação. + def handle_error(exception) + flash[:alert] = "Erro ao distribuir: #{exception.message}" + redirect_to new_formulario_path + end +end \ No newline at end of file diff --git a/src/app/controllers/home_controller.rb b/src/app/controllers/home_controller.rb new file mode 100644 index 0000000000..84feb1f0dd --- /dev/null +++ b/src/app/controllers/home_controller.rb @@ -0,0 +1,48 @@ +# Controlador da página inicial. Redireciona usuários baseado no perfil. +class HomeController < ApplicationController + before_action :authenticate_usuario + + # Renderiza a dashboard apropriada para o usuário logado. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Renderiza index. + # + # Efeitos Colaterais: + # - Carrega dados para a dashboard. + def index + return load_discente_dashboard if current_usuario.discente? + return load_admin_dashboard if current_usuario.admin? + end + + private + + # Carrega dados para a dashboard do discente. + # + # Efeitos Colaterais: + # - Define @pendencias e @respondidos. + def load_discente_dashboard + @pendencias = current_usuario.pendencias + @respondidos = fetch_forms_respondidos + end + + # Carrega dados para a dashboard do administrador. + # + # Efeitos Colaterais: + # - Nenhum por enquanto. + def load_admin_dashboard + end + + # Busca formulários já respondidos pelo usuário. + # + # Retorno: + # - (Array): Lista de formulários respondidos. + def fetch_forms_respondidos + current_usuario.respostas + .where.not(data_submissao: nil) + .includes(:formulario) + .map(&:formulario) + end +end \ No newline at end of file diff --git a/src/app/controllers/redefinicao_senha_controller.rb b/src/app/controllers/redefinicao_senha_controller.rb new file mode 100644 index 0000000000..ee0f9be30a --- /dev/null +++ b/src/app/controllers/redefinicao_senha_controller.rb @@ -0,0 +1,106 @@ +# Controlador para fluxo de "Esqueci minha senha". +class RedefinicaoSenhaController < ApplicationController + skip_before_action :require_login, raise: false + before_action :resolve_user_from_token, only: %i[edit update] + + layout "auth" + + # Processa a solicitação de redefinição de senha (envio de email). + # + # Argumentos: + # - params[:email] (String): Email do usuário. + # + # Retorno: + # - (NilClass): Redireciona com aviso. + # + # Efeitos Colaterais: + # - Envia email de redefinição se usuário encontrado. + def create + return handle_missing_email if params[:email].blank? + + user = Usuario.find_by(email: params[:email]) + + process_reset_request(user) if user + + redirect_to login_path, notice: "Se este e-mail estiver cadastrado, um link de redefinição foi enviado." unless performed? + end + + # Renderiza o formulário de definição de nova senha. + # + # Argumentos: + # - params[:token] (String): Token de redefinição. + # + # Efeitos Colaterais: + # - Carrega @usuario via before_action. + def edit + # @usuario já carregado pelo before_action + end + + # Atualiza a senha do usuário. + # + # Argumentos: + # - params[:usuario]: Campos de senha. + # + # Retorno: + # - (NilClass): Redireciona ou re-renderiza. + # + # Efeitos Colaterais: + # - Atualiza senha no DB. + def update + if @usuario.update(user_params) + redirect_to login_path, notice: "Senha redefinida com sucesso! Você já pode fazer o login." + else + flash.now[:alert] = @usuario.errors.full_messages.to_sentence + render :edit, status: :unprocessable_content + end + end + + private + + # Sanitiza parâmetros. + # + # Retorno: + # - (ActionController::Parameters): Params permitidos. + def user_params + params.require(:usuario).permit(:password, :password_confirmation) + end + + # Lida com email ausente na solicitação. + # + # Efeitos Colaterais: + # - Redireciona com erro. + def handle_missing_email + redirect_to login_path, alert: "O campo de e-mail não pode estar vazio." + end + + # Envia o email de redefinição. + # + # Argumentos: + # - user (Usuario): O usuário. + # + # Efeitos Colaterais: + # - Envia email UserMailer.redefinicao_senha. + def process_reset_request(user) + if user.status == false + redirect_to login_path, alert: "Você ainda não definiu sua senha. Por favor, verifique seu e-mail para definir sua senha." + else + UserMailer.with(user: user).redefinicao_senha.deliver_now + end + end + + # Resolve o usuário a partir do token. + # + # Argumentos: + # - params[:token] + # + # Efeitos Colaterais: + # - Define @usuario ou redireciona se inválido. + def resolve_user_from_token + @token = params[:token] + @usuario = Usuario.find_signed(@token, purpose: :redefinir_senha) + + return if @usuario + + redirect_to login_path, alert: "Link inválido ou expirado." + end +end \ No newline at end of file diff --git a/src/app/controllers/respostas_controller.rb b/src/app/controllers/respostas_controller.rb new file mode 100644 index 0000000000..6486c7c9ed --- /dev/null +++ b/src/app/controllers/respostas_controller.rb @@ -0,0 +1,224 @@ +# Controlador para preenchimento e submissão de avaliações pelos alunos. +class RespostasController < ApplicationController + before_action :require_login + before_action :set_formulario + before_action :verifica_participacao + + # Renderiza o formulário de resposta para uma avaliação específica. + # + # Retorno: + # - Renderiza a view :new. + # + # Efeitos Colaterais: + # - Define @questions com as questões do formulário ordendas. + # - Define @resposta como uma nova instância (ou existente não submetida). + def new + load_questions + @resposta = Resposta.new + end + + # Processa o envio da resposta com transação para garantir atomicidade. + # + # Argumentos: + # - params[:respostas] (Hash): Mapa de 'id_questao' => 'resposta_valor'. + # + # Retorno: + # - Redireciona para root_path com mensagem de sucesso se tudo for salvo. + # - Renderiza a view :new com status :unprocessable_content se houver erro (transação falhar). + # + # Efeitos Colaterais: + # - Cria/Atualiza registros de Resposta e RespostaItem. + # - Define flash[:notice] em sucesso ou flash[:alert] em erro. + # - Recarrega @questions em caso de erro para re-renderizar o form. + def create + @resposta = find_or_init_resposta + + if submit_response_transaction + redirect_to root_path, notice: "Avaliação enviada com sucesso. Obrigado!" + else + load_questions + flash.now[:alert] = "Houve um erro ao enviar suas respostas. Verifique os campos abaixo." + render :new, status: :unprocessable_content + end + end + + private + + # Define o formulário com base no ID da URL. + # + # Efeitos Colaterais: + # - Define @formulario. + def set_formulario + @formulario = Formulario.find(params[:formulario_id]) + end + + # Carrega as questões do formulário. + # + # Efeitos Colaterais: + # - Define @questions. + def load_questions + @questions = @formulario.template.questoes.includes(:opcoes).order(:id) + end + + # Encontra ou inicializa a resposta do usuário. + # + # Retorno: + # - (Resposta): A resposta do usuário. + def find_or_init_resposta + Resposta.find_or_initialize_by( + formulario: @formulario, + participante: current_usuario + ) + end + + # Wrapper transacional para submissão. + # + # Retorno: + # - (Boolean): Sucesso ou falha. + # + # Efeitos Colaterais: + # - Chama métodos de persistência. + def submit_response_transaction + ActiveRecord::Base.transaction do + save_resposta_header! + process_all_items! + finalize_submission! + true + end + rescue ActiveRecord::RecordInvalid, ActiveRecord::Rollback + false + end + + # Salva o cabeçalho da resposta. + # + # Efeitos Colaterais: + # - Salva @resposta. + def save_resposta_header! + raise ActiveRecord::Rollback unless @resposta.save + end + + # Processa todos os itens de resposta submetidos. + # + # Argumentos: + # - params[:respostas] + # + # Efeitos Colaterais: + # - Chama save_single_item! para cada item. + def process_all_items! + return unless params[:respostas].present? + + params[:respostas].each do |questao_id, valor| + save_single_item!(questao_id, valor) + end + end + + # Salva um item individual de resposta. + # + # Argumentos: + # - questao_id (Integer): ID da questão. + # - valor (String): Valor da resposta. + # + # Efeitos Colaterais: + # - Cria/Atualiza RespostaItem. + def save_single_item!(questao_id, valor) + questao = Questao.find(questao_id) + item = RespostaItem.find_or_initialize_by(resposta: @resposta, questao: questao) + + apply_value_to_item(item, questao, valor) + + unless item.save + @resposta.errors.add(:base, "Questão '#{questao.enunciado}': #{item.errors.full_messages.join(', ')}") + raise ActiveRecord::Rollback + end + end + + # Aplica o valor ao item de resposta dependendo do tipo. + # + # Argumentos: + # - item (RespostaItem): Item a ser preenchido. + # - questao (Questao): A questão. + # - valor (String): A resposta bruta. + def apply_value_to_item(item, questao, valor) + if questao.multipla_escolha? + handle_multiple_choice(item, questao, valor) + else + item.texto_resposta = valor + item.opcao_escolhida = nil + end + end + + # Processa respostas de múltipla escolha. + # + # Argumentos: + # - item, questao, valor + # + # Efeitos Colaterais: + # - Busca opção correspondente. + def handle_multiple_choice(item, questao, valor) + opcao = questao.opcoes.find_by(texto_opcao: valor) + if opcao + item.opcao_escolhida = opcao + else + item.errors.add(:base, "Opção inválida selecionada.") + end + end + + # Finaliza a submissão marcando o timestamp. + # + # Efeitos Colaterais: + # - Atualiza data_submissao. + def finalize_submission! + @resposta.update!(data_submissao: Time.current) + end + + # Verifica permissões e condições para responder. + # + # Efeitos Colaterais: + # - Redireciona se inválido. + def verifica_participacao + return deny_access unless valid_participant? + return form_closed if form_expired? + return already_answered if user_already_responded? + end + + # Verifica se o usuário pode participar. + # + # Retorno: + # - (Boolean): True se discente. + def valid_participant? + current_usuario&.discente? + end + + # Verifica se o formulário expirou. + # + # Retorno: + # - (Boolean): True se data_encerramento passou. + def form_expired? + @formulario.data_encerramento.present? && @formulario.data_encerramento < Time.current + end + + # Verifica se o usuário já respondeu. + # + # Retorno: + # - (Boolean): True se já existe resposta submetida. + def user_already_responded? + Resposta.where(formulario: @formulario, participante: current_usuario) + .where.not(data_submissao: nil) + .exists? + end + + # Redireciona acesso negado. + def deny_access + redirect_to root_path, alert: "Acesso negado." + end + + # Redireciona formulário fechado. + def form_closed + redirect_to root_path, alert: "Este formulário não está mais aceitando respostas." + end + + # Redireciona já respondido. + def already_answered + redirect_to root_path, alert: "Você já respondeu este formulário." + end +end \ No newline at end of file diff --git a/src/app/controllers/resultados_controller.rb b/src/app/controllers/resultados_controller.rb new file mode 100644 index 0000000000..9a57e259b3 --- /dev/null +++ b/src/app/controllers/resultados_controller.rb @@ -0,0 +1,146 @@ +require 'csv' + +# Controlador para visualização e exportação de resultados das avaliações. +class ResultadosController < ApplicationController + before_action :authorize_admin + before_action :set_formulario, only: :show + before_action :load_respostas, only: :show + + # Lista todos os formulários e seus resultados. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Renderiza index. + # + # Efeitos Colaterais: + # - Define @formularios. + def index + @formularios = Formulario.all.includes(:turma, :respostas) + end + + # Exibe detalhe de um formulário e permite exportação CSV. + # + # Argumentos: + # - params[:id] (Integer): ID do formulário. + # - format (html/csv): Formato da resposta. + # + # Retorno: + # - (NilClass/CSV): Renderiza show ou envia arquivo. + # + # Efeitos Colaterais: + # - Instigates file download (CSV). + def show + respond_to do |format| + format.html + format.csv { handle_csv_export } + end + end + + private + + # Garante acesso apenas admins. + # + # Efeitos Colaterais: + # - Redireciona com alerta. + def authorize_admin + redirect_to root_path, alert: "Acesso restrito." unless current_usuario&.admin? + end + + # Define o formulário. + # + # Efeitos Colaterais: + # - Define @formulario ou redireciona. + def set_formulario + @formulario = Formulario.find(params[:id]) + rescue ActiveRecord::RecordNotFound + redirect_to formularios_path, alert: "Formulário não encontrado" + end + + # Carrega as respostas submetidas para o formulário. + # + # Efeitos Colaterais: + # - Define @respostas. + def load_respostas + @respostas = @formulario.respostas + .where.not(data_submissao: nil) + .includes(resposta_items: %i[questao opcao_escolhida]) + end + + # Gerencia a exportação CSV. + # + # Efeitos Colaterais: + # - Envia dados ou redireciona com erro. + def handle_csv_export + if @respostas.empty? + redirect_to resultado_path(@formulario), alert: "Não é possível gerar um relatório, pois não há respostas." + else + send_data generate_csv_content, filename: csv_filename + end + end + + # Gera o nome do arquivo CSV. + # + # Retorno: + # - (String): O nome do arquivo. + def csv_filename + "relatorio_#{@formulario.titulo_envio.parameterize.underscore}.csv" + end + + # Gera o conteúdo do CSV. + # + # Retorno: + # - (String): O conteúdo CSV. + def generate_csv_content + questions = @formulario.template.questoes.order(:id) + + CSV.generate(headers: true) do |csv| + csv << build_csv_header(questions) + + @respostas.each do |resposta| + csv << build_csv_row(resposta, questions) + end + end + end + + # Constrói o cabeçalho do CSV. + # + # Argumentos: + # - questions: Lista de questões. + # + # Retorno: + # - (Array): Array de strings do cabeçalho. + def build_csv_header(questions) + ["Timestamp", "Turma"] + questions.map(&:enunciado) + end + + # Constrói uma linha do CSV. + # + # Argumentos: + # - resposta: A resposta sendo processada. + # - questions: Lista de questões. + # + # Retorno: + # - (Array): Dados da linha. + def build_csv_row(resposta, questions) + base_data = [resposta.data_submissao, @formulario.turma.codigo] + + answers_data = questions.map do |question| + extract_answer_value(resposta, question) + end + + base_data + answers_data + end + + # Extrai valor de uma resposta para uma questão. + # + # Retorno: + # - (String): O valor da resposta. + def extract_answer_value(resposta, question) + item = resposta.resposta_items.find { |i| i.questao_id == question.id } + return "" unless item + + item.texto_resposta.presence || item.opcao_escolhida&.texto_opcao + end +end \ No newline at end of file diff --git a/src/app/controllers/template_questions_controller.rb b/src/app/controllers/template_questions_controller.rb new file mode 100644 index 0000000000..60f03f0750 --- /dev/null +++ b/src/app/controllers/template_questions_controller.rb @@ -0,0 +1,151 @@ +# Controlador para gerenciamento das questões dentro de um template. +# Permite adicionar, remover e atualizar questões dinamicamente. +class TemplateQuestionsController < ApplicationController + before_action :set_template + before_action :set_question, only: %i[update destroy add_alternative] + + # Tipos de questão que permitem múltiplas escolhas. + CHOICE_TYPES = %w[radio checkbox].freeze + + # Adiciona uma nova questão ao template. + # + # Retorno: + # - (NilClass): Redireciona para visualização do template. + # + # Efeitos Colaterais: + # - Cria nova TemplateQuestion. + def create + @template.template_questions.create( + title: "Nova Questão", + question_type: "text", + content: [] + ) + redirect_to edit_template_path(@template), notice: 'Questão adicionada.' + end + + # Atualiza uma questão existente. + # + # Argumentos: + # - params[:template_question]: Atributos da questão. + # - params[:alternatives]: Alternativas para questões de escolha. + # + # Retorno: + # - (NilClass): Redireciona em sucesso. + # + # Efeitos Colaterais: + # - Atualiza atributos. + # - Adiciona alternativa se solicitado. + def update + prepare_attributes + + return handle_add_alternative_action if adding_alternative_button? + + if type_changed_or_autosave? + return handle_type_change + end + + perform_standard_save + end + + # Remove uma questão do template. + # + # Efeitos Colaterais: + # - Deleta o registro se houver mais de uma questão. + def destroy + if @template.template_questions.count <= 1 + redirect_to edit_template_path(@template), alert: 'não é possível salvar template sem questões' + else + @question.destroy + redirect_to edit_template_path(@template), notice: 'template alterado com sucesso' + end + end + + # Adiciona uma alternativa vazia a uma questão de escolha. + # + # Efeitos Colaterais: + # - Modifica o array de conteúdo da questão. + def add_alternative + append_empty_option + save_without_validation_and_redirect + end + + private + + # Setta o template pai. + def set_template + @template = Template.find(params[:template_id]) + end + + # Setta a questão alvo. + def set_question + @question = @template.template_questions.find(params[:id]) + end + + # Sanitiza parâmetros da questão. + def question_params + params.require(:template_question).permit(:title, :question_type) + end + + # Prepara atributos para atualização. + def prepare_attributes + @question.content = params[:alternatives] if params[:alternatives] + @question.assign_attributes(question_params) + end + + # Verifica se a ação é adicionar alternativa. + def adding_alternative_button? + params[:commit] == "Adicionar Alternativa" + end + + # Verifica se o tipo mudou ou é um save automático. + def type_changed_or_autosave? + params[:commit].nil? || @question.question_type_changed? + end + + # Lida com a ação de adicionar alternativa. + def handle_add_alternative_action + append_empty_option + save_without_validation_and_redirect + end + + # Lida com a mudança de tipo de questão. + def handle_type_change + ensure_content_consistency + save_without_validation_and_redirect('Tipo de questão atualizado.') + end + + # Executa salvamento padrão com validação. + def perform_standard_save + @question.content = [] if @question.question_type == 'text' + + if @question.save + redirect_to edit_template_path(@template), notice: 'template alterado com sucesso' + else + redirect_to edit_template_path(@template), alert: @question.errors.full_messages.join(', ') + end + end + + # Adiciona string vazia ao array de conteúdo. + def append_empty_option + @question.content ||= [] + @question.content << "" + end + + # Salva sem validação e redireciona (usado para interações dinâmicas). + def save_without_validation_and_redirect(msg = nil) + @question.save(validate: false) + redirect_opts = {} + redirect_opts[:notice] = msg if msg + redirect_to edit_template_path(@template), redirect_opts + end + + # Garante consistência do conteúdo ao mudar tipo. + def ensure_content_consistency + case @question.question_type + when 'text' + @question.content = [] + when *CHOICE_TYPES + @question.content = [''] if @question.content.blank? + end + end +end \ No newline at end of file diff --git a/src/app/controllers/templates_controller.rb b/src/app/controllers/templates_controller.rb new file mode 100644 index 0000000000..7b99de0fcb --- /dev/null +++ b/src/app/controllers/templates_controller.rb @@ -0,0 +1,113 @@ + # Controlador para CRUD de Templates de Avaliação. +class TemplatesController < ApplicationController + before_action :set_template, only: [:edit, :update, :destroy] + + # Lista templates visíveis. + # + # Retorno: + # - Renderiza a view :index. + # + # Efeitos Colaterais: + # - Define @templates com todos os templates visíveis (não ocultos). + def index + @templates = Template.all_visible + end + + # Renderiza o formulário de criação de template. + # + # Retorno: + # - Renderiza a view :new. + # + # Efeitos Colaterais: + # - Define @template como uma nova instância vazia. + def new + @template = Template.new + end + + # Cria um novo template e o associa ao usuário logado. + # + # Argumentos: + # - params[:template] (ActionController::Parameters): Hash contendo os atributos do template. + # + # Retorno: + # - Redireciona para a página de edição (edit_template_path) se salvo com sucesso. + # - Renderiza a view :new com status :unprocessable_content se houver erros de validação. + # + # Efeitos Colaterais: + # - Tenta inserir um novo registro na tabela 'templates'. + # - Define @template com os dados submetidos. + # - Define flash[:notice] em caso de sucesso. + def create + @template = Template.new(template_params) + @template.id_criador = session[:usuario_id] + + if @template.save + redirect_to edit_template_path(@template), notice: 'Template criado com sucesso' + else + render :new, status: :unprocessable_content + end + end + + # Renderiza a página de edição de um template existente. + # + # Retorno: + # - Renderiza a view :edit. + # + # Efeitos Colaterais: + # - Define @template via before_action (set_template). + def edit + # @template is set by before_action + end + + # Atualiza os dados de um template existente. + # + # Argumentos: + # - params[:template] (ActionController::Parameters): Hash contendo novos atributos. + # + # Retorno: + # - Redireciona para edit_template_path em caso de sucesso. + # - Renderiza a view :edit com status :unprocessable_content se houver erros. + # + # Efeitos Colaterais: + # - Atualiza o registro no banco de dados. + # - Define flash[:notice] em caso de sucesso. + def update + if @template.update(template_params) + redirect_to edit_template_path(@template), notice: 'Template atualizado com sucesso.' + else + render :edit, status: :unprocessable_content + end + end + + # Remove logicamente um template (soft delete). + # + # Retorno: + # - Redireciona para a lista de templates (templates_path). + # + # Efeitos Colaterais: + # - Atualiza o atributo 'hidden' do template para true. + # - Define flash[:notice]. + def destroy + @template.update(hidden: true) + redirect_to templates_path, notice: 'Template deletado com sucesso.' + end + + private + + # Busca o template pelo ID fornecido na URL. + # + # Efeitos Colaterais: + # - Define a variável @template. + # - Levanta ActiveRecord::RecordNotFound se não existir. + def set_template + @template = Template.find(params[:id]) + end + + # Define quais parâmetros são permitidos para criação/edição (Strong Parameters). + # + # Retorno: + # - (ActionController::Parameters): Hash contendo apenas a chave :titulo permitida. + def template_params + params.require(:template).permit(:titulo) + end +end diff --git a/src/app/controllers/usuarios_controller.rb b/src/app/controllers/usuarios_controller.rb new file mode 100644 index 0000000000..583b955130 --- /dev/null +++ b/src/app/controllers/usuarios_controller.rb @@ -0,0 +1,135 @@ +# Controlador para CRUD de Usuários. +class UsuariosController < ApplicationController + # Antes de show, edit, update e destroy, carrega o @usuario + before_action :set_usuario, only: %i[show edit update destroy] + #before_action :authenticate_admin + + # Lista usuários cadastrados. + # + # Retorno: + # - Renderiza a view :index. + # + # Efeitos Colaterais: + # - Define @usuarios com todos os registros. + def index + @usuarios = Usuario.all + end + + # Exibe detalhes do usuário. + # + # Argumentos: + # - params[:id] (Integer): ID do usuário na URL. + # + # Retorno: + # - Renderiza a view :show. + # + # Efeitos Colaterais: + # - Define @usuario via set_usuario. + def show + # @usuario já foi carregado pelo set_usuario + end + + # Renderiza o formulário de cadastro de novo usuário. + # + # Retorno: + # - Renderiza a view :new. + # + # Efeitos Colaterais: + # - Define @usuario como uma nova instância vazia. + def new + @usuario = Usuario.new + end + + # Cria um novo usuário no sistema. + # + # Argumentos: + # - params[:usuario] (ActionController::Parameters): Atributos do usuário. + # + # Retorno: + # - Redireciona para página do usuário (@usuario) se salvo com sucesso. + # - Renderiza a view :new com status :unprocessable_content se houver erros de validação. + # + # Efeitos Colaterais: + # - Tenta salvar novo registro no DB. + # - Define flash[:notice] em sucesso. + def create + @usuario = Usuario.new(usuario_params) + + if @usuario.save + redirect_to @usuario, notice: "Usuário criado com sucesso." + else + render :new, status: :unprocessable_content + end + end + + # Renderiza o formulário de edição de usuário. + # + # Retorno: + # - Renderiza a view :edit. + # + # Efeitos Colaterais: + # - Define @usuario via set_usuario. + def edit + # @usuario já foi carregado + end + + # Atualiza os dados de um usuário existente. + # + # Argumentos: + # - params[:usuario] (ActionController::Parameters): Novos atributos. + # + # Retorno: + # - Redireciona para página do usuário (@usuario) se salvo com sucesso. + # - Renderiza a view :edit com status :unprocessable_content se houver erros. + # + # Efeitos Colaterais: + # - Atualiza registro no DB. + # - Define flash[:notice] em sucesso. + def update + if @usuario.update(usuario_params) + redirect_to @usuario, notice: "Usuário atualizado com sucesso." + else + render :edit, status: :unprocessable_content + end + end + + # Remove um usuário do sistema. + # + # Retorno: + # - Redireciona para a lista de usuários (usuarios_url). + # + # Efeitos Colaterais: + # - Exclui o registro do DB (destroy). + # - Define flash[:notice]. + def destroy + @usuario.destroy + redirect_to usuarios_url, notice: "Usuário removido com sucesso." + end + + # Placeholder para redefinição de senha manual (ainda não implementado completamente). + def redefinir_senha + end + + private + + # Carrega um usuário pelo id da URL. + # + # Efeitos Colaterais: + # - Define @usuario. + def set_usuario + @usuario = Usuario.find(params[:id]) + end + + # Strong parameters: só permite esses campos virem do form. + def usuario_params + params.require(:usuario).permit( + :nome, + :email, + :matricula, + :usuario, + :password, + :ocupacao, + :status + ) + end +end diff --git a/src/app/helpers/admin_helper.rb b/src/app/helpers/admin_helper.rb new file mode 100644 index 0000000000..5ad944d2b4 --- /dev/null +++ b/src/app/helpers/admin_helper.rb @@ -0,0 +1,3 @@ +# Helper para views da área administrativa. +module AdminHelper +end diff --git a/src/app/helpers/application_helper.rb b/src/app/helpers/application_helper.rb new file mode 100644 index 0000000000..d9144c2284 --- /dev/null +++ b/src/app/helpers/application_helper.rb @@ -0,0 +1,3 @@ +# Helper principal da aplicação, contendo métodos compartilhados por todas as views. +module ApplicationHelper +end diff --git a/src/app/helpers/avaliacoes_helper.rb b/src/app/helpers/avaliacoes_helper.rb new file mode 100644 index 0000000000..68cd0e5617 --- /dev/null +++ b/src/app/helpers/avaliacoes_helper.rb @@ -0,0 +1,3 @@ +# Helper para views de avaliações. +module AvaliacoesHelper +end diff --git a/src/app/helpers/formularios_helper.rb b/src/app/helpers/formularios_helper.rb new file mode 100644 index 0000000000..615196a13d --- /dev/null +++ b/src/app/helpers/formularios_helper.rb @@ -0,0 +1,3 @@ +# Helper para views de formulários. +module FormulariosHelper +end diff --git a/src/app/helpers/home_helper.rb b/src/app/helpers/home_helper.rb new file mode 100644 index 0000000000..d3f8909bd2 --- /dev/null +++ b/src/app/helpers/home_helper.rb @@ -0,0 +1,3 @@ +# Helper para views da página inicial. +module HomeHelper +end diff --git a/src/app/helpers/respostas_helper.rb b/src/app/helpers/respostas_helper.rb new file mode 100644 index 0000000000..22a237b498 --- /dev/null +++ b/src/app/helpers/respostas_helper.rb @@ -0,0 +1,3 @@ +# Helper para views de respostas. +module RespostasHelper +end diff --git a/src/app/helpers/resultados_helper.rb b/src/app/helpers/resultados_helper.rb new file mode 100644 index 0000000000..80321c2454 --- /dev/null +++ b/src/app/helpers/resultados_helper.rb @@ -0,0 +1,3 @@ +# Helper para views de resultados. +module ResultadosHelper +end diff --git a/src/app/javascript/application.js b/src/app/javascript/application.js new file mode 100644 index 0000000000..0d7b49404c --- /dev/null +++ b/src/app/javascript/application.js @@ -0,0 +1,3 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "@hotwired/turbo-rails" +import "controllers" diff --git a/src/app/javascript/controllers/application.js b/src/app/javascript/controllers/application.js new file mode 100644 index 0000000000..1213e85c7a --- /dev/null +++ b/src/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/src/app/javascript/controllers/hello_controller.js b/src/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000000..5975c0789d --- /dev/null +++ b/src/app/javascript/controllers/hello_controller.js @@ -0,0 +1,7 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + this.element.textContent = "Hello World!" + } +} diff --git a/src/app/javascript/controllers/index.js b/src/app/javascript/controllers/index.js new file mode 100644 index 0000000000..1156bf8362 --- /dev/null +++ b/src/app/javascript/controllers/index.js @@ -0,0 +1,4 @@ +// Import and register all your controllers from the importmap via controllers/**/*_controller +import { application } from "controllers/application" +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) diff --git a/src/app/jobs/application_job.rb b/src/app/jobs/application_job.rb new file mode 100644 index 0000000000..3aff2744f2 --- /dev/null +++ b/src/app/jobs/application_job.rb @@ -0,0 +1,8 @@ +# Classe base para Jobs em segundo plano. +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/src/app/mailers/application_mailer.rb b/src/app/mailers/application_mailer.rb new file mode 100644 index 0000000000..df7c810c7e --- /dev/null +++ b/src/app/mailers/application_mailer.rb @@ -0,0 +1,5 @@ +# Classe base para Mailers, configurando defaults e layout. +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/src/app/mailers/user_mailer.rb b/src/app/mailers/user_mailer.rb new file mode 100644 index 0000000000..bb96376c19 --- /dev/null +++ b/src/app/mailers/user_mailer.rb @@ -0,0 +1,44 @@ +# Mailer responsável pelo envio de emails relacionados a usuários (senha, boas-vindas). +class UserMailer < ApplicationMailer + default from: 'nao-responda@camaar.unb.br' + + # Envia email com link para definição da senha inicial. + # + # Argumentos: + # - params[:user] (Usuario): O usuário. + # + # Retorno: + # - (Mail::Message): Objeto de email para envio. + # + # Efeitos Colaterais: + # - Gera token assinado. + def definicao_senha + @user = params[:user] + + token = @user.signed_id(purpose: :definir_senha, expires_in: 24.hours) + + @url = definir_senha_url(token: token) + + mail(to: @user.email, subject: 'Definição de Senha - Sistema CAMAAR') + end + + # Envia email com link para redefinição de senha esquecida. + # + # Argumentos: + # - params[:user] (Usuario): O usuário. + # + # Retorno: + # - (Mail::Message): Objeto de email para envio. + # + # Efeitos Colaterais: + # - Gera token assinado. + def redefinicao_senha + @user = params[:user] + + token = @user.signed_id(purpose: :redefinir_senha, expires_in: 15.minutes) + + @url = edit_redefinir_senha_url(token: token) + + mail(to: @user.email, subject: 'Redefinição de Senha - Sistema CAMAAR') + end +end \ No newline at end of file diff --git a/src/app/models/application_record.rb b/src/app/models/application_record.rb new file mode 100644 index 0000000000..3c40c94222 --- /dev/null +++ b/src/app/models/application_record.rb @@ -0,0 +1,5 @@ +# Classe base abstrata para todos os modelos da aplicação. +# Herda de ActiveRecord::Base e define que esta é uma classe abstrata primária. +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/src/app/models/concerns/.keep b/src/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/models/formulario.rb b/src/app/models/formulario.rb new file mode 100644 index 0000000000..aa2e151270 --- /dev/null +++ b/src/app/models/formulario.rb @@ -0,0 +1,10 @@ +# Representa um formulário de avaliação gerado a partir de um Template para uma Turma. +# Armazena os metadados do envio e relaciona o template à turma e às respostas. +class Formulario < ApplicationRecord + belongs_to :template + belongs_to :turma + has_many :respostas + + validates :titulo_envio, presence: true + validates :data_criacao, presence: true +end diff --git a/src/app/models/materia.rb b/src/app/models/materia.rb new file mode 100644 index 0000000000..95abacc563 --- /dev/null +++ b/src/app/models/materia.rb @@ -0,0 +1,8 @@ +# Representa uma disciplina ou matéria ofertada pela instituição. +# Contém informações como código (ex: CIC0105) e nome. +class Materia < ApplicationRecord + validates :codigo, presence: true + validates :nome, presence: true + + has_many :turmas +end diff --git a/src/app/models/matricula.rb b/src/app/models/matricula.rb new file mode 100644 index 0000000000..cf8d936057 --- /dev/null +++ b/src/app/models/matricula.rb @@ -0,0 +1,6 @@ +# Tabela associativa que representa a matrícula de um usuário (aluno) em uma turma. +# Utilizada para gerenciar a relação N:N entre Usuario e Turma. +class Matricula < ApplicationRecord + belongs_to :usuario, foreign_key: 'id_usuario' + belongs_to :turma, foreign_key: 'id_turma' +end diff --git a/src/app/models/opcao.rb b/src/app/models/opcao.rb new file mode 100644 index 0000000000..b205cd2c8c --- /dev/null +++ b/src/app/models/opcao.rb @@ -0,0 +1,8 @@ +# Representa uma opção de resposta para uma questão do tipo Múltipla Escolha. +# Armazena o texto da opção e se relaciona com a questão pai. +class Opcao < ApplicationRecord + self.table_name = "opcoes" + belongs_to :questao + + validates :texto_opcao, presence: true +end diff --git a/src/app/models/questao.rb b/src/app/models/questao.rb new file mode 100644 index 0000000000..6379c79d37 --- /dev/null +++ b/src/app/models/questao.rb @@ -0,0 +1,13 @@ +# Representa uma questão individual dentro de um template de avaliação. +# Pode ser do tipo texto ou múltipla escolha. +class Questao < ApplicationRecord + self.table_name = "questoes" + belongs_to :template + has_many :opcoes, class_name: 'Opcao', dependent: :destroy + has_many :resposta_items, dependent: :destroy + + enum :tipo, { texto: 0, multipla_escolha: 1 } + + validates :enunciado, presence: true + validates :tipo, presence: true +end diff --git a/src/app/models/resposta.rb b/src/app/models/resposta.rb new file mode 100644 index 0000000000..f58345b417 --- /dev/null +++ b/src/app/models/resposta.rb @@ -0,0 +1,23 @@ +# Representa a submissão completa de um formulário por um participante. +# Agrupa todos os itens de resposta (RespostaItem). +class Resposta < ApplicationRecord + belongs_to :formulario + belongs_to :participante, class_name: 'Usuario', foreign_key: 'id_participante' + has_many :resposta_items + + validates :id_participante, uniqueness: { scope: :formulario_id } + + # Verifica se a resposta foi submetida (finalizada). + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (Boolean): Retorna true se data_submissao estiver presente. + # + # Efeitos Colaterais: + # - Nenhum. + def respondido? + data_submissao.present? + end +end diff --git a/src/app/models/resposta_item.rb b/src/app/models/resposta_item.rb new file mode 100644 index 0000000000..af08497078 --- /dev/null +++ b/src/app/models/resposta_item.rb @@ -0,0 +1,31 @@ +# Representa a resposta dada para uma única questão em um formulário. +# Armazena o texto da resposta (para questões discursivas) ou a opção escolhida (para múltipla escolha). +class RespostaItem < ApplicationRecord + belongs_to :resposta + belongs_to :questao + belongs_to :opcao_escolhida, class_name: 'Opcao', foreign_key: 'id_opcao_escolhida', optional: true + + validate :valida_resposta_conforme_tipo_questao + + private + + # Valida se a resposta está preenchida corretamente de acordo com o tipo da questão. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Retorna nil se a validação passar ou se a questão não existir. + # + # Efeitos Colaterais: + # - Adiciona erro ao modelo se a validação falhar. + def valida_resposta_conforme_tipo_questao + return unless questao + + if questao.texto? && texto_resposta.blank? + errors.add(:texto_resposta, 'não pode ficar em branco para questões de texto') + elsif questao.multipla_escolha? && opcao_escolhida.nil? + errors.add(:opcao_escolhida, 'deve ser selecionada para questões de múltipla escolha') + end + end +end diff --git a/src/app/models/template.rb b/src/app/models/template.rb new file mode 100644 index 0000000000..476a94ae3e --- /dev/null +++ b/src/app/models/template.rb @@ -0,0 +1,13 @@ +# Define a estrutura de uma avaliação, contendo um conjunto de questões. +# Serve de base para a criação dos Formulários. +class Template < ApplicationRecord + belongs_to :criador, class_name: 'Usuario', foreign_key: 'id_criador', optional: true + has_many :questoes, class_name: 'Questao' + has_many :formularios + has_many :template_questions, dependent: :destroy + + validates :titulo, presence: { message: "Nome do Template não pode ficar em branco" } + # validates :titulo, presence: true # Keeping existing validation if needed, but user wants name + + scope :all_visible, -> { where(hidden: false) } +end diff --git a/src/app/models/template_question.rb b/src/app/models/template_question.rb new file mode 100644 index 0000000000..8b33c2284f --- /dev/null +++ b/src/app/models/template_question.rb @@ -0,0 +1,28 @@ +# Modelo temporário/auxiliar para lidar com a criação dinâmica de questões em templates na interface. +# Permite serializar o conteúdo das alternativas como JSON. +class TemplateQuestion < ApplicationRecord + belongs_to :template + + enum :question_type, { text: 'text', radio: 'radio', checkbox: 'checkbox' }, suffix: true + + validates :title, presence: { message: "o texto da questão é obrigatório" } + validate :alternatives_must_be_present, if: -> { ['radio', 'checkbox'].include?(question_type) } + + # Valida se as alternativas estão preenchidas para perguntas de rádio ou checkbox. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Retorna nil se a validação passar. + # + # Efeitos Colaterais: + # - Adiciona erro ao modelo se as alternativas estiverem em branco. + def alternatives_must_be_present + if content.blank? || content.any?(&:blank?) + errors.add(:base, "Todas as alternativas devem ser preenchidas") + end + end + + serialize :content, coder: JSON +end diff --git a/src/app/models/turma.rb b/src/app/models/turma.rb new file mode 100644 index 0000000000..0beb6d49f8 --- /dev/null +++ b/src/app/models/turma.rb @@ -0,0 +1,59 @@ +# Representa uma turma de uma disciplina em um período letivo. +# Relaciona docentes, alunos e formulários de avaliação. +class Turma < ApplicationRecord + belongs_to :materia + belongs_to :docente, class_name: 'Usuario', foreign_key: 'id_docente' + has_many :formularios + has_many :matriculas, foreign_key: 'id_turma' + has_many :alunos, through: :matriculas, source: :usuario + + validates :codigo, presence: true + validates :semestre, presence: true + validates :horario, presence: true + + # Cria e distribui um formulário baseado em um template para todos os alunos e docente da turma. + # + # Argumentos: + # - template (Template): O template base para o formulário. + # + # Retorno: + # - (Array): Uma lista de objetos Resposta criados (para cada participante). + # + # Efeitos Colaterais: + # - Cria um novo registro Formulario. + # - Cria múltiplos registros Resposta (um para cada aluno e um para o docente). + # - Executa dentro de uma transação. + def distribuir_formulario(template) + ActiveRecord::Base.transaction do + form = Formulario.create!( + template: template, + turma: self, + titulo_envio: template.titulo || template.name, + data_criacao: Time.current + ) + + participantes = alunos.to_a + [docente] + participantes.uniq.each do |participante| + Resposta.create!( + formulario: form, + participante: participante, + + ) + end + end + end + + # Retorna o nome completo da turma (Matéria - Código). + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (String): O nome concatenado com o código. + # + # Efeitos Colaterais: + # - Nenhum. + def nome_completo + "#{materia&.nome} - #{codigo}" + end +end diff --git a/src/app/models/usuario.rb b/src/app/models/usuario.rb new file mode 100644 index 0000000000..ae366c814c --- /dev/null +++ b/src/app/models/usuario.rb @@ -0,0 +1,90 @@ +# Erro personalizado para falhas de autenticação. +class AuthenticationError < StandardError; end + +# Representa um usuário do sistema (Discente, Docente ou Admin). +# Gerencia autenticação, autorização e relacionamentos com turmas e formulários. +class Usuario < ApplicationRecord + has_secure_password + has_many :respostas, foreign_key: 'id_participante' + + # Acessor virtual para a senha atual (usado na mudança de senha). + attr_accessor :current_password + + enum :ocupacao, { discente: 0, docente: 1, admin: 2 } + + # Validações de campos básicos + validates :nome, presence: true + validates :email, presence: true, uniqueness: true, allow_nil: true + validates :matricula, presence: true + validates :usuario, presence: true, uniqueness: true + validates :ocupacao, presence: true + validates :status, inclusion: { in: [true, false] } + + validate :validate_current_password + + # Retorna as respostas pendentes (formulários não submetidos) do usuário. + # + # Retorno: + # - (ActiveRecord::Relation): Uma coleção de objetos Resposta com data_submissao nil. + def pendencias + respostas.where(data_submissao: nil) + end + + # Associations + has_many :turmas_lecionadas, class_name: 'Turma', foreign_key: 'id_docente', dependent: :destroy + has_many :templates_criados, class_name: 'Template', foreign_key: 'id_criador' + has_many :matriculas, foreign_key: 'id_usuario' + has_many :turmas, through: :matriculas + + # Autentica um usuário pelo login (usuário, email ou matrícula) e senha. + # + # Argumentos: + # - login (String): O identificador do usuário. + # - password (String): A senha do usuário. + # + # Retorno: + # - (Usuario): O objeto do usuário autenticado se as credenciais forem válidas. + # + # Efeitos Colaterais: + # - Realiza consultas ao banco de dados. + # - Dispara AuthenticationError se falhar. + def self.authenticate(login, password) + user = find_by(usuario: login) || + find_by(email: login) || + find_by(matricula: login) + + raise AuthenticationError, "Usuário não encontrado" unless user + + if user.status == false + raise AuthenticationError, "Sua conta está pendente. Por favor, redefina sua senha para ativar." + end + + unless user.authenticate(password) + raise AuthenticationError, "Senha incorreta" + end + + user + end + + # Verifica se o usuário possui a ocupação de administrador. + # + # Retorno: + # - (Boolean): Retorna true se a ocupação for 'admin'. + def admin? + ocupacao == "admin" + end + + private + + # Valida a senha atual do usuário. + # + # Efeitos Colaterais: + # - Adiciona um erro ao modelo se a autenticação falhar. + def validate_current_password + return if current_password.blank? + + unless authenticate(current_password) + errors.add(:current_password, "está incorreta") + end + end +end \ No newline at end of file diff --git a/src/app/services/sigaa_importer.rb b/src/app/services/sigaa_importer.rb new file mode 100644 index 0000000000..a90f0fd5c4 --- /dev/null +++ b/src/app/services/sigaa_importer.rb @@ -0,0 +1,363 @@ +require 'json' +require 'securerandom' + +# Serviço responsável pela importação e sincronização de dados do SIGAA. +# Lê arquivos JSON (classes.json e class_members.json) e atualiza Matérias, Turmas e Usuários. +class SigaaImporter + # Método de classe utilitário para instanciar e executar o importador. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (Any): O retorno do método call de instância. + # + # Efeitos Colaterais: + # - Veja o método call. + def self.call + new.call + end + + # Inicializa o importador lendo os arquivos JSON de dados. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (SigaaImporter): Uma nova instância. + # + # Efeitos Colaterais: + # - Lê arquivos do sistema de arquivos. + # - Pode levantar erro se os arquivos não existirem. + def initialize + classes_path = Rails.root.join('..', 'classes.json') + members_path = Rails.root.join('..', 'class_members.json') + + begin + @classes_data = JSON.parse(File.read(classes_path)) + @members_data = JSON.parse(File.read(members_path)) + rescue Errno::ENOENT + raise StandardError, "Não foi possível buscar os dados. Tente novamente mais tarde." + end + + @active_turma_ids = [] + @active_user_ids = [] + end + + # Executa o processo de importação completo. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (NilClass): Retorna nil após a conclusão. + # + # Efeitos Colaterais: + # - Cria/Atualiza/Deleta registros de Usuários, Turmas e Matérias. + # - Abre transação no banco de dados. + def call + ActiveRecord::Base.transaction do + setup_default_teacher + process_definitions + process_enrollments + cleanup_data + end + end + + private + + # Cria ou recupera um docente padrão para turmas sem professor definido. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (Usuario): O objeto do docente padrão. + # + # Efeitos Colaterais: + # - Cria um usuário docente se não existir. + def setup_default_teacher + @docente_padrao = Usuario.find_or_create_by!(matricula: "999999") do |u| + u.nome = "Docente Importador" + u.email = "docente@sistema.com" + u.usuario = "999999" + u.password = "password123" + u.ocupacao = :docente + u.status = true + end + @active_user_ids << @docente_padrao.id + end + + # Processa as definições de turmas e matérias do arquivo classes.json. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (Array): Lista de turmas processadas. + # + # Efeitos Colaterais: + # - Persiste Matérias e Turmas. + def process_definitions + @classes_data.each do |cls| + materia = persist_materia(cls) + persist_turma(cls, materia) + end + end + + # Persiste uma matéria no banco de dados. + # + # Argumentos: + # - cls (Hash): Dados da classe contendo código e nome da disciplina. + # + # Retorno: + # - (Materia): O objeto matéria persistido. + # + # Efeitos Colaterais: + # - Cria ou atualiza registro na tabela materias. + def persist_materia(cls) + materia = Materia.find_or_initialize_by(codigo: cls['code']) + materia.nome = cls['name'] + materia.save! + materia + end + + # Persiste uma turma no banco de dados. + # + # Argumentos: + # - cls (Hash): Dados da classe/turma. + # - materia (Materia): Objeto da matéria associada. + # + # Retorno: + # - (Turma): O objeto turma persistido. + # + # Efeitos Colaterais: + # - Cria ou atualiza registro na tabela turmas. + # - Adiciona ID da turma à lista de turmas ativas. + def persist_turma(cls, materia) + class_info = cls['class'] + codigo_turma = class_info['classCode'] + + turma = Turma.find_or_initialize_by(codigo: codigo_turma, materia: materia) + + turma.docente ||= @docente_padrao + + if class_info + turma.semestre = class_info['semester'] + turma.horario = class_info['time'] + end + + turma.save! + @active_turma_ids << turma.id + end + + # Processa as matrículas e associações de docentes do arquivo class_members.json. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (Array): Resultado da iteração sobre os dados. + # + # Efeitos Colaterais: + # - Cria/Atualiza usuários (docentes e discentes). + # - Cria matrículas. + def process_enrollments + @members_data.each do |turma_data| + materia = Materia.find_by(codigo: turma_data['code']) + next unless materia + + turma = ensure_turma_fallback(turma_data, materia) + @active_turma_ids << turma.id + + if turma_data['docente'] + process_teacher(turma, turma_data['docente']) + end + + if turma_data['dicente'] + process_students_list(turma, turma_data['dicente']) + end + end + end + + # Garante que a turma exista, criando-a se necessário (fallback). + # + # Argumentos: + # - turma_data (Hash): Dados da turma vindo dos membros. + # - materia (Materia): Matéria associada. + # + # Retorno: + # - (Turma): A turma encontrada ou criada. + # + # Efeitos Colaterais: + # - Pode criar uma nova turma se não existir. + def ensure_turma_fallback(turma_data, materia) + codigo = turma_data['classCode'] + turma = Turma.find_by(codigo: codigo, materia: materia) + return turma if turma + + original_cls = @classes_data.find { |c| c['code'] == turma_data['code'] }['class'] + + Turma.create!( + codigo: codigo, + materia: materia, + semestre: original_cls['semester'], + horario: original_cls['time'], + docente: @docente_padrao + ) + end + + # Atualiza o professor de uma turma. + # + # Argumentos: + # - turma (Turma): A turma a ser atualizada. + # - doc_data (Hash): Dados do docente. + # + # Retorno: + # - (Boolean): Resultado do update. + # + # Efeitos Colaterais: + # - Cria/Atualiza usuário docente. + # - Atualiza a associação da turma. + def process_teacher(turma, doc_data) + docente, _ = persist_user_common(doc_data['usuario'], doc_data, :docente) + turma.update!(docente: docente) + end + + # Processa a lista de estudantes de uma turma. + # + # Argumentos: + # - turma (Turma): A turma alvo. + # - students_list (Array): Lista de dados dos estudantes. + # + # Retorno: + # - (Array): Resultado da iteração. + # + # Efeitos Colaterais: + # - Chama import_single_student para cada aluno. + def process_students_list(turma, students_list) + students_list.each do |student_data| + import_single_student(turma, student_data) + end + end + + # Importa um único estudante e o matricula na turma. + # + # Argumentos: + # - turma (Turma): A turma. + # - data (Hash): Dados do estudante. + # + # Retorno: + # - (Matricula/Array): A associação de turma ou nil. + # + # Efeitos Colaterais: + # - Cria/Atualiza usuário. + # - Envia email de boas-vindas se for novo. + # - Cria matrícula. + def import_single_student(turma, data) + validate_email!(data) + + user, is_new = persist_user_common(data['matricula'], data, :discente) + + if is_new + send_welcome_email(user) + end + + user.turmas << turma unless user.turmas.exists?(turma.id) + end + + # Valida se o email está presente nos dados. + # + # Argumentos: + # - data (Hash): Dados do usuário. + # + # Retorno: + # - (NilClass): Se válido. + # + # Efeitos Colaterais: + # - Levanta erro (StandardError) se email ausente. + def validate_email!(data) + email = data['email'].to_s.strip + if email.empty? + raise StandardError, "Falha ao importar usuário '#{data['matricula']}': e-mail ausente." + end + end + + # Envia email de definição de senha para o usuário. + # + # Argumentos: + # - user (Usuario): O usuário destinatário. + # + # Retorno: + # - (Any): Resultado do deliver_now ou nil em erro. + # + # Efeitos Colaterais: + # - Envia email. + # - Loga erro se falhar. + def send_welcome_email(user) + UserMailer.with(user: user).definicao_senha.deliver_now + rescue => e + Rails.logger.error "Falha ao enviar e-mail para #{user.email}: #{e.message}" + end + + # Lógica comum para persistir usuários (docentes ou discentes). + # + # Argumentos: + # - identifier (String): Matrícula ou identificador único. + # - data (Hash): Dados do usuário. + # - role (Symbol): Ocupação (:docente ou :discente). + # + # Retorno: + # - (Array): [user, is_new] - Objeto usuário e booleano indicando se é novo. + # + # Efeitos Colaterais: + # - Cria ou atualiza o usuário. + # - Define senha aleatória e status false para novos. + def persist_user_common(identifier, data, role) + user = Usuario.find_or_initialize_by(matricula: identifier.to_s) + is_new = user.new_record? + + user.assign_attributes( + nome: data['nome'], + email: data['email'], + usuario: data['usuario'], + ocupacao: role + ) + + if is_new + user.password = SecureRandom.hex(8) + user.status = false + end + + user.save! + @active_user_ids << user.id + + [user, is_new] + end + + # Remove dados obsoletos que não vieram na importação atual. + # + # Argumentos: + # - Nenhum + # + # Retorno: + # - (Any): Resultado das operações de limpeza. + # + # Efeitos Colaterais: + # - Remove turmas não ativas. + # - Remove ou inativa usuários não ativos. + def cleanup_data + Turma.where.not(id: @active_turma_ids).destroy_all + + users_to_remove = Usuario.where(ocupacao: [:discente, :docente]).where.not(id: @active_user_ids) + + users_to_remove.find_each do |user| + begin + user.destroy + rescue ActiveRecord::InvalidForeignKey, ActiveRecord::StatementInvalid => e + user.update_column(:status, false) + Rails.logger.info "Usuário #{user.matricula} inativado." + end + end + end +end \ No newline at end of file diff --git a/src/app/views/admin/gerenciamento.html.erb b/src/app/views/admin/gerenciamento.html.erb new file mode 100644 index 0000000000..44ad27e425 --- /dev/null +++ b/src/app/views/admin/gerenciamento.html.erb @@ -0,0 +1,27 @@ +
+ +
+ +
+ + <%= button_to "Importar dados", importar_dados_path, method: :post, class: "bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 rounded shadow-sm w-full transition duration-150 ease-in-out" %> + + <% + if @sistema_tem_dados + css_classe = "bg-green-500 hover:bg-green-500 cursor-pointer shadow-sm" + disabled_attr = "" + else + css_classe = "bg-green-300 cursor-not-allowed opacity-70" + disabled_attr = "disabled" + end + %> + + <%= button_to "Editar Templates", "/templates", method: :get, class: "#{css_classe} text-white font-medium py-3 px-4 rounded w-full transition duration-150 ease-in-out", disabled: !@sistema_tem_dados %> + + <%= link_to "Enviar Formularios", formularios_path, class: "#{css_classe} text-white font-medium py-3 px-4 rounded w-full transition duration-150 ease-in-out text-center block" %> + + <%= link_to "Resultados", resultados_path, class: "#{css_classe} text-white font-medium py-3 px-4 rounded w-full transition duration-150 ease-in-out text-center block" %> + +
+
+
\ No newline at end of file diff --git a/src/app/views/admin/index.html.erb b/src/app/views/admin/index.html.erb new file mode 100644 index 0000000000..f9a75680aa --- /dev/null +++ b/src/app/views/admin/index.html.erb @@ -0,0 +1,2 @@ +

bem vindo admin, você é admin!!!

+

Bem-vindo ao painel de administração.

diff --git a/src/app/views/autenticacao/new.html.erb b/src/app/views/autenticacao/new.html.erb new file mode 100644 index 0000000000..be07fc7c44 --- /dev/null +++ b/src/app/views/autenticacao/new.html.erb @@ -0,0 +1,56 @@ +
+ +
+ +

Login

+ + <%= form_with url: login_path, local: true, class: "space-y-6" do |form| %> + + <% if flash[:alert] %> + + <% end %> + + <% if flash[:notice] %> + + <% end %> + +
+ <%= form.label :email, "Usuário", class: "block text-sm font-medium text-gray-700 mb-1" %> + <%= form.email_field :email, + class: "w-full px-4 py-2 border border-gray-300 rounded focus:ring-purple-500 focus:border-purple-500", + placeholder: "aluno@aluno.unb.br" %> +
+ +
+ <%= form.label :password, "Senha", class: "block text-sm font-medium text-gray-700 mb-1" %> + <%= form.password_field :password, + class: "w-full px-4 py-2 border border-gray-300 rounded focus:ring-purple-500 focus:border-purple-500", + placeholder: "Password" %> +
+ +
+ <%= form.submit "Entrar", + class: "w-full flex justify-center py-2 px-4 border border-transparent rounded shadow-sm text-sm font-medium text-white bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 cursor-pointer transition-colors" %> +
+ +
+ +
+ <% end %> +
+ +
+

Bem vindo

+

ao

+

Camaar

+
+ +
\ No newline at end of file diff --git a/src/app/views/avaliacoes/index.html.erb b/src/app/views/avaliacoes/index.html.erb new file mode 100644 index 0000000000..23a2798dec --- /dev/null +++ b/src/app/views/avaliacoes/index.html.erb @@ -0,0 +1,18 @@ +

Minhas Avaliações Pendentes

+ +
+ <% if @pendencias && @pendencias.any? %> + <% @pendencias.each do |resposta| %> +
+

<%= resposta.formulario.template.titulo || resposta.formulario.template.name %>

+

Turma: <%= resposta.formulario.turma.codigo %>

+

Matéria: <%= resposta.formulario.turma.materia.nome %>

+

Semestre: <%= resposta.formulario.turma.semestre %>

+ + <%= link_to "Responder", "#", class: "inline-block bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded" %> +
+ <% end %> + <% else %> +

Você não tem avaliações pendentes no momento.

+ <% end %> +
diff --git a/src/app/views/definicao_senha/new.html.erb b/src/app/views/definicao_senha/new.html.erb new file mode 100644 index 0000000000..c4c50711f9 --- /dev/null +++ b/src/app/views/definicao_senha/new.html.erb @@ -0,0 +1,6 @@ +<%= render partial: "shared/form_senha", locals: { + titulo: "Defina sua Senha", + modelo: @usuario, + url_destino: definir_senha_path(token: params[:token]), + texto_botao: "Alterar Senha" +} %> \ No newline at end of file diff --git a/src/app/views/formularios/index.html.erb b/src/app/views/formularios/index.html.erb new file mode 100644 index 0000000000..22d57d901e --- /dev/null +++ b/src/app/views/formularios/index.html.erb @@ -0,0 +1,75 @@ +

Distribuir Formulários de Avaliação

+ +<% if flash[:notice] %> + +<% end %> + +<% if flash[:alert] %> + +<% end %> + +<%= form_with url: formularios_path, method: :post, local: true, class: "bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" do |f| %> +
+ +
+ <%= f.collection_select :template_id, @templates, :id, :titulo, {}, { class: "block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-3 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500" } %> +
+
+ +
+ +
+ <% @turmas.each do |turma| %> +
+ <%= check_box_tag "turma_ids[]", turma.id, false, class: "form-checkbox h-5 w-5 text-gray-600", id: "turma_#{turma.id}" %> + +
+ <% end %> +
+
+ +
+ <%= f.submit "Distribuir Formulário", class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline cursor-pointer" %> +
+<% end %> + +
+ +

Formulários Cadastrados

+ +<% if @formularios.any? %> +
+
    + <% @formularios.each do |form| %> +
  • +
    +

    + <%= form.template.titulo %> +

    +

    + <%= form.turma.nome_completo %> +

    +

    + <%= form.respostas.where.not(data_submissao: nil).count %> respostas +

    +
    +
    + <%= link_to "Ver Resultados", resultado_path(form), class: "text-indigo-600 hover:text-indigo-900 font-medium" %> +
    +
  • + <% end %> +
+
+<% else %> +

Nenhum formulário cadastrado

+<% end %> \ No newline at end of file diff --git a/src/app/views/formularios/new.html.erb b/src/app/views/formularios/new.html.erb new file mode 100644 index 0000000000..5f5c46c335 --- /dev/null +++ b/src/app/views/formularios/new.html.erb @@ -0,0 +1,37 @@ +

Novo Formulário

+ +<%= form_with url: formularios_path, local: true, method: :post do |form| %> + <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> + +
+ <%= form.label :template_id, "Template", for: "Template" %> + <%= form.collection_select :template_id, @templates, :id, :titulo, { prompt: "Selecione um template" }, { id: "Template" } %> +
+ +
+

Turmas

+ <% @turmas.each do |turma| %> +
+ <%= check_box_tag "turma_ids[]", turma.id, false, id: "turma_#{turma.id}" %> + <%= label_tag "turma_#{turma.id}", "#{turma.nome_completo}" %> +
+ <% end %> +
+ +
+ <%= form.label :data_encerramento, "Data de encerramento" %> + <%= form.date_field :data_encerramento %> +
+ +
+ <%= form.submit "Gerar Formulário" %> +
+<% end %> + +
+ <%= link_to "Voltar", formularios_path %> +
\ No newline at end of file diff --git a/src/app/views/formularios/pendentes.html.erb b/src/app/views/formularios/pendentes.html.erb new file mode 100644 index 0000000000..128b1b314b --- /dev/null +++ b/src/app/views/formularios/pendentes.html.erb @@ -0,0 +1,39 @@ +
+

Formulários Pendentes

+ + <% if flash[:alert] %> + + <% end %> + + <% if @respostas_pendentes.any? %> +
+
    + <% @respostas_pendentes.each do |resposta| %> + <% form = resposta.formulario %> +
  • +
    +

    + <%= form.template.titulo %> +

    +

    + <%= form.turma.materia.nome %> (<%= form.turma.semestre %>) +

    +

    + Encerra em: <%= form.data_encerramento&.strftime("%d/%m/%Y") %> +

    +
    +
    + <%= link_to "Responder", new_formulario_resposta_path(form), class: "bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded" %> +
    +
  • + <% end %> +
+
+ <% elsif !flash[:alert] %> +
+

Nenhum formulário pendente

+
+ <% end %> +
\ No newline at end of file diff --git a/src/app/views/formularios/show.html.erb b/src/app/views/formularios/show.html.erb new file mode 100644 index 0000000000..e45027b703 --- /dev/null +++ b/src/app/views/formularios/show.html.erb @@ -0,0 +1,8 @@ +

Resultados do Formulário

+ +

Template: <%= @formulario.template.titulo %>

+

Turma: <%= @formulario.turma.nome_completo %>

+ +<%= link_to "Exportar para CSV", formulario_path(@formulario, format: :csv), class: "btn btn-primary" %> + +<%= link_to "Voltar", formularios_path %> \ No newline at end of file diff --git a/src/app/views/home/index.html.erb b/src/app/views/home/index.html.erb new file mode 100644 index 0000000000..fa86bae5ac --- /dev/null +++ b/src/app/views/home/index.html.erb @@ -0,0 +1,46 @@ +
+ <% if current_usuario %> +

Bem-vindo, <%= current_usuario.nome %>

+ + <% if current_usuario.discente? %> +
+

Formulários Pendentes

+ <% if @pendencias&.any? %> +
    + <% @pendencias.each do |resposta| %> +
  • +
    +

    <%= resposta.formulario.titulo_envio %>

    +

    Turma: <%= resposta.formulario.turma.nome_completo %>

    +
    + <%= link_to "Responder", new_formulario_resposta_path(resposta.formulario), class: "bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded", style: "display: inline-block; background-color: #2563eb; color: white; padding: 0.5rem 1rem; border-radius: 0.25rem; font-weight: bold; text-decoration: none;" %> +
  • + <% end %> +
+ <% else %> +

Nenhum formulário pendente.

+ <% end %> +
+ +
+

Formulários Respondidos

+ <% if @respondidos&.any? %> +
    + <% @respondidos.each do |form| %> +
  • +

    <%= form.titulo_envio %>

    + <% resposta = Resposta.find_by(formulario: form, participante: current_usuario) %> +

    Respondido em <%= resposta&.data_submissao ? l(resposta.data_submissao, format: :short) : 'Data não disponível' %>

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

Nenhum formulário respondido ainda.

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

Por favor, faça login para continuar.

+ <% end %> +
diff --git a/src/app/views/layouts/application.html.erb b/src/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..23b3d4903e --- /dev/null +++ b/src/app/views/layouts/application.html.erb @@ -0,0 +1,147 @@ + + + + CAMAAR + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + + + + +
+
+ + + +

+ Gerenciamento +

+
+ +
+ + + + + <% if current_usuario %> +
+
+ +
+ <%= current_usuario.nome&.first&.upcase || 'U' %> +
+
+ +
+ +
+

<%= current_usuario.nome %>

+
+ + <%= button_to logout_path, method: :delete, class: "block w-full text-left px-2 py-2 text-sm text-red-600 hover:bg-red-50 hover:text-red-800 transition-colors" do %> +
+ + + + Sair +
+ <% end %> + +
+
+
+ <% else %> +
+ <%= link_to "Entrar", login_path, class: "text-purple-800 font-bold hover:underline" %> +
+ <% end %> +
+
+ +
+ + + + + +
+ + + <% flash.each do |type, msg| %> + <% color = type == 'notice' ? 'green' : 'red' %> + + <% end %> + + + <%= yield %> + +
+
+ + + + \ No newline at end of file diff --git a/src/app/views/layouts/auth.html.erb b/src/app/views/layouts/auth.html.erb new file mode 100644 index 0000000000..8fbc331430 --- /dev/null +++ b/src/app/views/layouts/auth.html.erb @@ -0,0 +1,19 @@ + + + + CAMAAR - Login + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + + + <%= yield %> + + + \ No newline at end of file diff --git a/src/app/views/layouts/mailer.html.erb b/src/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..3aac9002ed --- /dev/null +++ b/src/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/src/app/views/layouts/mailer.text.erb b/src/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/src/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/src/app/views/pwa/manifest.json.erb b/src/app/views/pwa/manifest.json.erb new file mode 100644 index 0000000000..096ab11faa --- /dev/null +++ b/src/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "CamaarProj", + "icons": [ + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "maskable" + } + ], + "start_url": "/", + "display": "standalone", + "scope": "/", + "description": "CamaarProj.", + "theme_color": "red", + "background_color": "red" +} diff --git a/src/app/views/pwa/service-worker.js b/src/app/views/pwa/service-worker.js new file mode 100644 index 0000000000..b3a13fb7bb --- /dev/null +++ b/src/app/views/pwa/service-worker.js @@ -0,0 +1,26 @@ +// Add a service worker for processing Web Push notifications: +// +// self.addEventListener("push", async (event) => { +// const { title, options } = await event.data.json() +// event.waitUntil(self.registration.showNotification(title, options)) +// }) +// +// self.addEventListener("notificationclick", function(event) { +// event.notification.close() +// event.waitUntil( +// clients.matchAll({ type: "window" }).then((clientList) => { +// for (let i = 0; i < clientList.length; i++) { +// let client = clientList[i] +// let clientPath = (new URL(client.url)).pathname +// +// if (clientPath == event.notification.data.path && "focus" in client) { +// return client.focus() +// } +// } +// +// if (clients.openWindow) { +// return clients.openWindow(event.notification.data.path) +// } +// }) +// ) +// }) diff --git a/src/app/views/redefinicao_senha/edit.html.erb b/src/app/views/redefinicao_senha/edit.html.erb new file mode 100644 index 0000000000..c06bacd6f4 --- /dev/null +++ b/src/app/views/redefinicao_senha/edit.html.erb @@ -0,0 +1,6 @@ +<%= render partial: "shared/form_senha", locals: { + titulo: "Redefina sua Senha", + modelo: @usuario, + url_destino: redefinir_senha_path(token: params[:token]), + texto_botao: "Salvar Nova Senha" +} %> \ No newline at end of file diff --git a/src/app/views/respostas/create.html.erb b/src/app/views/respostas/create.html.erb new file mode 100644 index 0000000000..4599101e2b --- /dev/null +++ b/src/app/views/respostas/create.html.erb @@ -0,0 +1,4 @@ +
+

Respostas#create

+

Find me in app/views/respostas/create.html.erb

+
diff --git a/src/app/views/respostas/new.html.erb b/src/app/views/respostas/new.html.erb new file mode 100644 index 0000000000..c1d336836b --- /dev/null +++ b/src/app/views/respostas/new.html.erb @@ -0,0 +1,35 @@ +
+

<%= @formulario.titulo_envio %>

+ + <%= form_with url: formulario_respostas_path(@formulario), method: :post, local: true, class: "space-y-6" do |form| %> + <% if @resposta.errors.any? %> + + <% end %> + + <% @questions.each do |question| %> +
+ <%= label_tag "respostas_#{question.id}", question.enunciado, class: "text-lg font-medium text-gray-700 mb-2 block" %> + + <% if question.texto? %> + <%= text_area_tag "respostas[#{question.id}]", nil, id: "respostas_#{question.id}", class: "w-full p-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500", rows: 3, placeholder: "Sua resposta..." %> + <% elsif question.multipla_escolha? %> +
+ <% question.opcoes.each do |opcao| %> +
+ <%= radio_button_tag "respostas[#{question.id}]", opcao.texto_opcao, false, class: "h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500" %> + <%= label_tag "respostas_#{question.id}_#{opcao.id}", opcao.texto_opcao, class: "ml-2 text-gray-700" %> +
+ <% end %> +
+ <% end %> +
+ <% end %> + +
+ <%= form.submit "Submeter Respostas", class: "bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline", style: "display: inline-block; background-color: #2563eb; color: white; padding: 0.5rem 1rem; border-radius: 0.25rem; font-weight: bold; cursor: pointer; border: none;" %> +
+ <% end %> +
diff --git a/src/app/views/resultados/index.html.erb b/src/app/views/resultados/index.html.erb new file mode 100644 index 0000000000..1cdd6d5e16 --- /dev/null +++ b/src/app/views/resultados/index.html.erb @@ -0,0 +1,39 @@ +
+

Resultados das Avaliações

+ + <% if @formularios.any? %> +
+ + + + + + + + + + + <% @formularios.each do |formulario| %> + + + + + + + <% end %> + +
FormulárioTurmaRespostas SubmetidasAções
+ <%= formulario.titulo_envio %> + + <%= "#{formulario.turma.nome_completo}" %> + + <%= formulario.respostas.where.not(data_submissao: nil).count %> + / <%= formulario.respostas.count %> + + <%= link_to "Ver Resultados", resultado_path(formulario), class: "text-blue-600 hover:text-blue-900" %> +
+
+ <% else %> +

Nenhum formulário cadastrado

+ <% end %> +
diff --git a/src/app/views/resultados/show.html.erb b/src/app/views/resultados/show.html.erb new file mode 100644 index 0000000000..87a19abba4 --- /dev/null +++ b/src/app/views/resultados/show.html.erb @@ -0,0 +1,54 @@ +
+ + <% if flash[:alert] %> + + <% end %> + +
+

<%= @formulario.titulo_envio %>

+ + <%= link_to "Exportar para CSV", resultado_path(@formulario, format: :csv), class: "bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded" %> +
+ +

Total de respostas: <%= @respostas.count %>

+ +
+

Amostra de Respostas

+ + <% if @respostas.any? %> +
+ + + + + <% @formulario.template.questoes.limit(3).each do |q| %> + + <% end %> + + + + <% @respostas.limit(5).each do |resposta| %> + + + <% @formulario.template.questoes.limit(3).each do |q| %> + + <% end %> + + <% end %> + +
Data<%= q.enunciado.truncate(30) %>
<%= resposta.data_submissao ? l(resposta.data_submissao, format: :short) : 'Pendente' %> + <% item = resposta.resposta_items.find { |i| i.questao_id == q.id } %> + <%= item ? (item.texto_resposta.presence || item.opcao_escolhida&.texto_opcao) : "-" %> +
+
+ <% else %> +

Nenhuma resposta registrada para este formulário

+ <% end %> +
+ +
+ <%= link_to "Voltar", resultados_path, class: "text-blue-600 hover:underline" %> +
+
\ No newline at end of file diff --git a/src/app/views/shared/_form_senha.html.erb b/src/app/views/shared/_form_senha.html.erb new file mode 100644 index 0000000000..588299cd05 --- /dev/null +++ b/src/app/views/shared/_form_senha.html.erb @@ -0,0 +1,33 @@ +
+ +

<%= titulo %>

+ + <%= form_with model: modelo, url: url_destino, local: true, class: "w-full space-y-6" do |form| %> + + <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> + +
+ <%= form.label :password, "Nova Senha", class: "block text-sm font-medium text-gray-700 mb-1" %> + <%= form.password_field :password, + class: "w-full px-4 py-2 border border-gray-300 rounded focus:ring-purple-500 focus:border-purple-500", + placeholder: "Digite sua nova senha" %> +
+ +
+ <%= form.label :password_confirmation, "Confirme a senha", class: "block text-sm font-medium text-gray-700 mb-1" %> + <%= form.password_field :password_confirmation, + class: "w-full px-4 py-2 border border-gray-300 rounded focus:ring-purple-500 focus:border-purple-500", + placeholder: "Repita a senha" %> +
+ +
+ <%= form.submit texto_botao, + class: "w-full flex justify-center py-2 px-4 border border-transparent rounded shadow-sm text-sm font-medium text-white bg-purple-800 hover:bg-purple-900 focus:outline-none cursor-pointer" %> +
+ + <% end %> +
\ No newline at end of file diff --git a/src/app/views/templates/edit.html.erb b/src/app/views/templates/edit.html.erb new file mode 100644 index 0000000000..761febebde --- /dev/null +++ b/src/app/views/templates/edit.html.erb @@ -0,0 +1,69 @@ +

Editar Template

+ + +
+

Configurações do Template

+ <%= form_with(model: @template, local: true) do |form| %> +
+ <%= form.label :titulo, "Nome", class: "block text-gray-700 text-sm font-bold mb-2" %> +
+ <%= form.text_field :titulo, class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline mr-2" %> + <%= form.submit "Atualizar Nome", class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline cursor-pointer" %> +
+
+ <% end %> +
+ + +
+

Questões

+ + <% @template.template_questions.each_with_index do |question, index| %> +
+

Questão <%= index + 1 %>

+ + <%= form_with(model: [ @template, question ], local: true) do |f| %> +
+ <%= f.label :title, "Título da Questão", class: "block text-gray-700 text-sm font-bold mb-2" %> + <%= f.text_field :title, class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" %> +
+ +
+ <%= f.label :question_type, "Tipo da Questão", class: "block text-gray-700 text-sm font-bold mb-2" %> + <%= f.select :question_type, TemplateQuestion.question_types.keys.map { |k| [k.humanize, k] }, {}, { class: "shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline", onchange: "this.form.submit()" } %> +
+ + + <% if ['radio', 'checkbox'].include?(question.question_type) %> +
+

Alternativas:

+ <% (question.content || []).each_with_index do |alternative, i| %> +
+ +
+ <% end %> + +
+ <%= f.submit "Adicionar Alternativa", name: "commit", class: "bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-1 px-2 rounded text-xs" %> +
+
+ <% end %> + +
+ <%= f.submit "Salvar Questão", class: "bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline cursor-pointer" %> + + <%= link_to "Remover Questão", template_template_question_path(@template, question), data: { turbo_method: :delete, confirm: "Tem certeza?" }, class: "text-red-600 hover:text-red-800 text-sm font-bold" %> +
+ <% end %> +
+ <% end %> +
+ + +
+ <%= button_to "Adicionar Questão", template_template_questions_path(@template), method: :post, class: "bg-indigo-500 hover:bg-indigo-700 text-white font-bold py-3 px-6 rounded-lg focus:outline-none focus:shadow-outline text-lg" %> +
+ +
+ <%= link_to "Voltar para Templates", templates_path, class: "text-blue-500 hover:text-blue-800" %> +
diff --git a/src/app/views/templates/index.html.erb b/src/app/views/templates/index.html.erb new file mode 100644 index 0000000000..a67289f1df --- /dev/null +++ b/src/app/views/templates/index.html.erb @@ -0,0 +1,41 @@ +
+

Gerenciar Templates

+ <%= link_to "Novo Template", new_template_path, class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %> +
+ +<% if @templates.any? %> +
+ + + + + + + + + <% @templates.each do |template| %> + + + + + <% end %> + +
NomeAções
+ <%= template.titulo %> + +
+ <%= link_to edit_template_path(template), class: "w-4 mr-2 transform hover:text-purple-500 hover:scale-110" do %> + Editar + <% end %> + + <%= button_to template_path(template), method: :delete, data: { confirm: "Tem certeza que deseja deletar?" }, class: "w-4 mr-2 transform hover:text-red-500 hover:scale-110 bg-transparent border-0 cursor-pointer" do %> + Deletar + <% end %> +
+
+
+<% else %> +
+

Não existe nenhuma avaliação até o momento

+
+<% end %> diff --git a/src/app/views/templates/new.html.erb b/src/app/views/templates/new.html.erb new file mode 100644 index 0000000000..a844c738db --- /dev/null +++ b/src/app/views/templates/new.html.erb @@ -0,0 +1,24 @@ +

Novo Template

+ +<%= form_with(model: @template, local: true, class: "bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4") do |form| %> + <% if @template.errors.any? %> +
+

<%= pluralize(@template.errors.count, "erro") %> impediram este template de ser salvo:

+
    + <% @template.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.label :titulo, "Nome do Template", class: "block text-gray-700 text-sm font-bold mb-2" %> + <%= form.text_field :titulo, class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" %> +
+ +
+ <%= form.submit "Salvar Template", class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline cursor-pointer" %> + <%= link_to "Voltar", templates_path, class: "inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800" %> +
+<% end %> diff --git a/src/app/views/user_mailer/definicao_senha.html.erb b/src/app/views/user_mailer/definicao_senha.html.erb new file mode 100644 index 0000000000..f13af02638 --- /dev/null +++ b/src/app/views/user_mailer/definicao_senha.html.erb @@ -0,0 +1,18 @@ + + + + + + +

Bem-vindo ao CAMAAR, <%= @user.nome %>!

+

+ Você recebeu um convite para se cadastrar no sistema. +

+

+ Clique no link abaixo para definir sua senha: +

+

+ <%= link_to "Definir minha senha", @url %> +

+ + \ No newline at end of file diff --git a/src/app/views/user_mailer/redefinicao_senha.html.erb b/src/app/views/user_mailer/redefinicao_senha.html.erb new file mode 100644 index 0000000000..0e2a90b3e1 --- /dev/null +++ b/src/app/views/user_mailer/redefinicao_senha.html.erb @@ -0,0 +1,25 @@ + + + + + + +

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

+ +

+ Recebemos uma solicitação para redefinir a senha da sua conta no sistema CAMAAR. +

+ +

+ Para criar uma nova senha, clique no link abaixo: +

+ +

+ <%= link_to 'Redefinir minha senha', @url %> +

+ +

+ Se você não solicitou esta alteração, por favor ignore este e-mail. Sua senha permanecerá a mesma. +

+ + \ No newline at end of file diff --git a/src/app/views/usuarios/_form.html.erb b/src/app/views/usuarios/_form.html.erb new file mode 100644 index 0000000000..42bbb4683f --- /dev/null +++ b/src/app/views/usuarios/_form.html.erb @@ -0,0 +1,55 @@ +<%= form_with(model: usuario, local: true) do |form| %> + + <% if usuario.errors.any? %> +
+ <%= pluralize(usuario.errors.count, "erro") %> impedem o salvamento: +
    + <% usuario.errors.full_messages.each do |msg| %> +
  • <%= msg %>
  • + <% end %> +
+
+ <% end %> + +

+ <%= form.label :nome %>
+ <%= form.text_field :nome %> +

+ +

+ <%= form.label :email %>
+ <%= form.email_field :email %> +

+ +

+ <%= form.label :matricula %>
+ <%= form.text_field :matricula %> +

+ +

+ <%= form.label :usuario %>
+ <%= form.text_field :usuario %> +

+ +

+ <%= form.label :password %>
+ <%= form.password_field :password %> +

+ +

+ <%= form.label :password_confirmation %>
+ <%= form.password_field :password_confirmation %> +

+ +

+ <%= form.label :ocupacao %>
+ <%= form.text_field :ocupacao, placeholder: "discente, docente ou admin" %> +

+ +

+ <%= form.label :status %>
+ <%= form.check_box :status %> Ativo +

+ + <%= form.submit "Salvar Usuário" %> +<% end %> diff --git a/src/app/views/usuarios/edit.html.erb b/src/app/views/usuarios/edit.html.erb new file mode 100644 index 0000000000..1788f97409 --- /dev/null +++ b/src/app/views/usuarios/edit.html.erb @@ -0,0 +1,5 @@ +

Editar Usuário

+ +<%= render 'form', usuario: @usuario %> + +<%= link_to "Voltar", usuarios_path %> diff --git a/src/app/views/usuarios/index.html.erb b/src/app/views/usuarios/index.html.erb new file mode 100644 index 0000000000..2e408313b0 --- /dev/null +++ b/src/app/views/usuarios/index.html.erb @@ -0,0 +1,34 @@ +

Lista de Usuários

+ + + + + + + + + + + + + <% @usuarios.each do |usuario| %> + + + + + + + + + + <% end %> +
NomeEmailMatrículaUsuárioOcupaçãoStatusAções
<%= usuario.nome %><%= usuario.email %><%= usuario.matricula %><%= usuario.usuario %><%= usuario.ocupacao %><%= usuario.status ? "Ativo" : "Inativo" %> + <%= link_to "Mostrar", usuario %> | + <%= link_to "Editar", edit_usuario_path(usuario) %> | + <%= button_to "Excluir", usuario, method: :delete, + data: { confirm: "Tem certeza que deseja excluir este usuário?" }, + class: "inline-block px-3 py-1 text-sm text-white bg-red-600 rounded" %> +
+ +
+<%= link_to "Cadastrar novo usuário", new_usuario_path %> diff --git a/src/app/views/usuarios/new.html.erb b/src/app/views/usuarios/new.html.erb new file mode 100644 index 0000000000..f86089b679 --- /dev/null +++ b/src/app/views/usuarios/new.html.erb @@ -0,0 +1,5 @@ +

Novo Usuário

+ +<%= render 'form', usuario: @usuario %> + +<%= link_to "Voltar", usuarios_path %> diff --git a/src/app/views/usuarios/show.html.erb b/src/app/views/usuarios/show.html.erb new file mode 100644 index 0000000000..deecedef97 --- /dev/null +++ b/src/app/views/usuarios/show.html.erb @@ -0,0 +1,15 @@ +

Detalhes do Usuário

+ +

Nome: <%= @usuario.nome %>

+

Email: <%= @usuario.email %>

+

Matrícula: <%= @usuario.matricula %>

+

Usuário: <%= @usuario.usuario %>

+

Ocupação: <%= @usuario.ocupacao %>

+

Status: <%= @usuario.status ? "Ativo" : "Inativo" %>

+

Criado em: <%= @usuario.created_at %>

+

Atualizado em: <%= @usuario.updated_at %>

+ +
+ +<%= link_to "Editar", edit_usuario_path(@usuario) %> | +<%= link_to "Voltar", usuarios_path %> diff --git a/src/bin/brakeman b/src/bin/brakeman new file mode 100755 index 0000000000..ace1c9ba08 --- /dev/null +++ b/src/bin/brakeman @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +ARGV.unshift("--ensure-latest") + +load Gem.bin_path("brakeman", "brakeman") diff --git a/src/bin/bundle b/src/bin/bundle new file mode 100755 index 0000000000..282b92c5d4 --- /dev/null +++ b/src/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby3.2 +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/src/bin/cucumber b/src/bin/cucumber new file mode 100755 index 0000000000..eb5e962e86 --- /dev/null +++ b/src/bin/cucumber @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +if vendored_cucumber_bin + load File.expand_path(vendored_cucumber_bin) +else + require 'rubygems' unless ENV['NO_RUBYGEMS'] + require 'cucumber' + load Cucumber::BINARY +end diff --git a/src/bin/dev b/src/bin/dev new file mode 100755 index 0000000000..ad72c7d53c --- /dev/null +++ b/src/bin/dev @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +if ! gem list foreman -i --silent; then + echo "Installing foreman..." + gem install foreman +fi + +# Default to port 3000 if not specified +export PORT="${PORT:-3000}" + +# Let the debug gem allow remote connections, +# but avoid loading until `debugger` is called +export RUBY_DEBUG_OPEN="true" +export RUBY_DEBUG_LAZY="true" + +exec foreman start -f Procfile.dev "$@" diff --git a/src/bin/docker-entrypoint b/src/bin/docker-entrypoint new file mode 100755 index 0000000000..57567d69b4 --- /dev/null +++ b/src/bin/docker-entrypoint @@ -0,0 +1,14 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ]; then + LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) + export LD_PRELOAD +fi + +# If running the rails server then create or migrate existing database +if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/src/bin/importmap b/src/bin/importmap new file mode 100755 index 0000000000..36502ab16c --- /dev/null +++ b/src/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/src/bin/jobs b/src/bin/jobs new file mode 100755 index 0000000000..dcf59f309a --- /dev/null +++ b/src/bin/jobs @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "solid_queue/cli" + +SolidQueue::Cli.start(ARGV) diff --git a/src/bin/kamal b/src/bin/kamal new file mode 100755 index 0000000000..84b2554ced --- /dev/null +++ b/src/bin/kamal @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby3.2 +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'kamal' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("kamal", "kamal") diff --git a/src/bin/rails b/src/bin/rails new file mode 100755 index 0000000000..efc0377492 --- /dev/null +++ b/src/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/src/bin/rake b/src/bin/rake new file mode 100755 index 0000000000..4fbf10b960 --- /dev/null +++ b/src/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/src/bin/rubocop b/src/bin/rubocop new file mode 100755 index 0000000000..40330c0ff1 --- /dev/null +++ b/src/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# explicit rubocop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/src/bin/setup b/src/bin/setup new file mode 100755 index 0000000000..be3db3c0d6 --- /dev/null +++ b/src/bin/setup @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end diff --git a/src/bin/thrust b/src/bin/thrust new file mode 100755 index 0000000000..36bde2d832 --- /dev/null +++ b/src/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/src/config.ru b/src/config.ru new file mode 100644 index 0000000000..4a3c09a688 --- /dev/null +++ b/src/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/src/config/application.rb b/src/config/application.rb new file mode 100644 index 0000000000..d5a4aa6914 --- /dev/null +++ b/src/config/application.rb @@ -0,0 +1,27 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module CamaarProj + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.0 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + config.time_zone = 'Brasilia' + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/src/config/boot.rb b/src/config/boot.rb new file mode 100644 index 0000000000..988a5ddc46 --- /dev/null +++ b/src/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/src/config/cable.yml b/src/config/cable.yml new file mode 100644 index 0000000000..b9adc5aa3a --- /dev/null +++ b/src/config/cable.yml @@ -0,0 +1,17 @@ +# Async adapter only works within the same process, so for manually triggering cable updates from a console, +# and seeing results in the browser, you must do so from the web console (running inside the dev process), +# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view +# to make the web console appear. +development: + adapter: async + +test: + adapter: test + +production: + adapter: solid_cable + connects_to: + database: + writing: cable + polling_interval: 0.1.seconds + message_retention: 1.day diff --git a/src/config/cache.yml b/src/config/cache.yml new file mode 100644 index 0000000000..19d490843b --- /dev/null +++ b/src/config/cache.yml @@ -0,0 +1,16 @@ +default: &default + store_options: + # Cap age of oldest cache entry to fulfill retention policies + # max_age: <%= 60.days.to_i %> + max_size: <%= 256.megabytes %> + namespace: <%= Rails.env %> + +development: + <<: *default + +test: + <<: *default + +production: + database: cache + <<: *default diff --git a/src/config/credentials.yml.enc b/src/config/credentials.yml.enc new file mode 100644 index 0000000000..06f7c11844 --- /dev/null +++ b/src/config/credentials.yml.enc @@ -0,0 +1 @@ +7Fzxy9s2A16iDl+rBHiOBO/Fd/ukFiFj6for77ejrt7NeR2/H1Y53NPoNmDwmysE7BNfN9DjSR6pSjQ2KNm80Y9fyJg/cqT8UviMgoJtLSChArDjKPgzy6N3wz+GDjd6PF1bWUHQqKiH5I7JYV0v8SJCbxVqQs+QD3+z3BJZKXvuPC1/EjzdcAay/6Tw/DnXWd1SGsA8VdmS3ztlFpuFyt0bs1isOJrv1ssN0xS2gRVXesq2XXgD/CcPusPFt0zIXnOuF//bTylxB6kTFKj2KinyX0UZtPK8OX2l/gK2MZhCxc3Q5tpHN9SInWRxJYyVnESAloPW327aJhFWgYs1aMyyn0dRCyyCdtUVBfOAij6Qzj2jUMgK+EB6fE19ONxmF9RO9MlRfB9u+A0Im51owbvmgWhEdyYAqPe9dsWDE8AkRUcCsnRhIVt6Wd+wmOIEKtXTNjft3/56WJVCmk19m//jiyvakxoZvpLuhH6yYJK+F7YQS7tlIxe8--YjTexRh5g6Ac5iay--HDsgxkD//L4H9/SjIzA3MQ== \ No newline at end of file diff --git a/src/config/cucumber.yml b/src/config/cucumber.yml new file mode 100644 index 0000000000..47a4663ae2 --- /dev/null +++ b/src/config/cucumber.yml @@ -0,0 +1,8 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun = rerun.strip.gsub /\s/, ' ' +rerun_opts = rerun.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags 'not @wip'" +%> +default: <%= std_opts %> features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags 'not @wip' diff --git a/src/config/database.yml b/src/config/database.yml new file mode 100644 index 0000000000..2640cb5f30 --- /dev/null +++ b/src/config/database.yml @@ -0,0 +1,41 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + + +# Store production database in the storage/ directory, which by default +# is mounted as a persistent Docker volume in config/deploy.yml. +production: + primary: + <<: *default + database: storage/production.sqlite3 + cache: + <<: *default + database: storage/production_cache.sqlite3 + migrations_paths: db/cache_migrate + queue: + <<: *default + database: storage/production_queue.sqlite3 + migrations_paths: db/queue_migrate + cable: + <<: *default + database: storage/production_cable.sqlite3 + migrations_paths: db/cable_migrate diff --git a/src/config/deploy.yml b/src/config/deploy.yml new file mode 100644 index 0000000000..d21300a010 --- /dev/null +++ b/src/config/deploy.yml @@ -0,0 +1,116 @@ +# Name of your application. Used to uniquely configure containers. +service: camaar_proj + +# Name of the container image. +image: your-user/camaar_proj + +# Deploy to these servers. +servers: + web: + - 192.168.0.1 + # job: + # hosts: + # - 192.168.0.1 + # cmd: bin/jobs + +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. +# +# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +proxy: + ssl: true + host: app.example.com + +# Credentials for your image host. +registry: + # Specify the registry server, if you're not using Docker Hub + # server: registry.digitalocean.com / ghcr.io / ... + username: your-user + + # Always use an access token rather than real password when possible. + password: + - KAMAL_REGISTRY_PASSWORD + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +env: + secret: + - RAILS_MASTER_KEY + clear: + # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. + # When you start using multiple servers, you should split out job processing to a dedicated machine. + SOLID_QUEUE_IN_PUMA: true + + # Set number of processes dedicated to Solid Queue (default: 1) + # JOB_CONCURRENCY: 3 + + # Set number of cores available to the application on each server (default: 1). + # WEB_CONCURRENCY: 2 + + # Match this to any external database server to configure Active Record correctly + # Use camaar_proj-db for a db accessory server on same machine via local kamal docker network. + # DB_HOST: 192.168.0.2 + + # Log everything from Rails + # RAILS_LOG_LEVEL: debug + +# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: +# "bin/kamal logs -r job" will tail logs from the first server in the job section. +aliases: + console: app exec --interactive --reuse "bin/rails console" + shell: app exec --interactive --reuse "bash" + logs: app logs -f + dbc: app exec --interactive --reuse "bin/rails dbconsole" + + +# Use a persistent storage volume for sqlite database files and local Active Storage files. +# Recommended to change this to a mounted volume path that is backed up off server. +volumes: + - "camaar_proj_storage:/rails/storage" + + +# Bridge fingerprinted assets, like JS and CSS, between versions to avoid +# hitting 404 on in-flight requests. Combines all files from new and old +# version inside the asset_path. +asset_path: /rails/public/assets + +# Configure the image builder. +builder: + arch: amd64 + + # # Build image via remote server (useful for faster amd64 builds on arm64 computers) + # remote: ssh://docker@docker-builder-server + # + # # Pass arguments and secrets to the Docker build process + # args: + # RUBY_VERSION: ruby-3.2.3 + # secrets: + # - GITHUB_TOKEN + # - RAILS_MASTER_KEY + +# Use a different ssh user than root +# ssh: +# user: app + +# Use accessory services (secrets come from .kamal/secrets). +# accessories: +# db: +# image: mysql:8.0 +# host: 192.168.0.2 +# # Change to 3306 to expose port to the world instead of just local network. +# port: "127.0.0.1:3306:3306" +# env: +# clear: +# MYSQL_ROOT_HOST: '%' +# secret: +# - MYSQL_ROOT_PASSWORD +# files: +# - config/mysql/production.cnf:/etc/mysql/my.cnf +# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql +# directories: +# - data:/var/lib/mysql +# redis: +# image: redis:7.0 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data diff --git a/src/config/environment.rb b/src/config/environment.rb new file mode 100644 index 0000000000..cac5315775 --- /dev/null +++ b/src/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/src/config/environments/development.rb b/src/config/environments/development.rb new file mode 100644 index 0000000000..18f341caf1 --- /dev/null +++ b/src/config/environments/development.rb @@ -0,0 +1,79 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Configure 'rails notes' to inspect Cucumber files + config.annotations.register_directories('features') + config.annotations.register_extensions('feature') { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + + # Settings specified here will take precedence over those in config/application.rb. + + # Make code changes take effect immediately without server restart. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } + else + config.action_controller.perform_caching = false + end + + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + + config.action_mailer.delivery_method = :letter_opener + config.action_mailer.perform_deliveries = true + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Make template changes take effect immediately. + config.action_mailer.perform_caching = false + + # Set localhost to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! +end diff --git a/src/config/environments/production.rb b/src/config/environments/production.rb new file mode 100644 index 0000000000..bdcd01d1bf --- /dev/null +++ b/src/config/environments/production.rb @@ -0,0 +1,90 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [ :request_id ] + config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) + + # Change to "debug" to log everything (including potentially personally-identifiable information!) + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + config.cache_store = :solid_cache_store + + # Replace the default in-process and non-durable queuing backend for Active Job. + config.active_job.queue_adapter = :solid_queue + config.solid_queue.connects_to = { database: { writing: :queue } } + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [ :id ] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/src/config/environments/test.rb b/src/config/environments/test.rb new file mode 100644 index 0000000000..e6b5c1b020 --- /dev/null +++ b/src/config/environments/test.rb @@ -0,0 +1,57 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Configure 'rails notes' to inspect Cucumber files + config.annotations.register_directories('features') + config.annotations.register_extensions('feature') { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } + + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/src/config/importmap.rb b/src/config/importmap.rb new file mode 100644 index 0000000000..909dfc542d --- /dev/null +++ b/src/config/importmap.rb @@ -0,0 +1,7 @@ +# Pin npm packages by running ./bin/importmap + +pin "application" +pin "@hotwired/turbo-rails", to: "turbo.min.js" +pin "@hotwired/stimulus", to: "stimulus.min.js" +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +pin_all_from "app/javascript/controllers", under: "controllers" diff --git a/src/config/initializers/assets.rb b/src/config/initializers/assets.rb new file mode 100644 index 0000000000..487324424f --- /dev/null +++ b/src/config/initializers/assets.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path diff --git a/src/config/initializers/content_security_policy.rb b/src/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..b3076b38fe --- /dev/null +++ b/src/config/initializers/content_security_policy.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/src/config/initializers/filter_parameter_logging.rb b/src/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..c0b717f7ec --- /dev/null +++ b/src/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/src/config/initializers/inflections.rb b/src/config/initializers/inflections.rb new file mode 100644 index 0000000000..a3e233894b --- /dev/null +++ b/src/config/initializers/inflections.rb @@ -0,0 +1,21 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) + inflect.irregular 'materia', 'materias' + inflect.irregular 'resposta', 'respostas' + inflect.irregular 'formulario', 'formularios' + inflect.irregular 'turma', 'turmas' + inflect.irregular 'usuario', 'usuarios' +end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/src/config/locales/en.yml b/src/config/locales/en.yml new file mode 100644 index 0000000000..6c349ae5e3 --- /dev/null +++ b/src/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/src/config/master.key b/src/config/master.key new file mode 100644 index 0000000000..1bd5c9cbe5 --- /dev/null +++ b/src/config/master.key @@ -0,0 +1 @@ +005b3ba2019e55b8e01980540de7e6eb \ No newline at end of file diff --git a/src/config/puma.rb b/src/config/puma.rb new file mode 100644 index 0000000000..a248513b24 --- /dev/null +++ b/src/config/puma.rb @@ -0,0 +1,41 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Run the Solid Queue supervisor inside of Puma for single-server deployments +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/src/config/queue.yml b/src/config/queue.yml new file mode 100644 index 0000000000..9eace59c41 --- /dev/null +++ b/src/config/queue.yml @@ -0,0 +1,18 @@ +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 3 + processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> + polling_interval: 0.1 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/src/config/recurring.yml b/src/config/recurring.yml new file mode 100644 index 0000000000..b4207f9b07 --- /dev/null +++ b/src/config/recurring.yml @@ -0,0 +1,15 @@ +# examples: +# periodic_cleanup: +# class: CleanSoftDeletedRecordsJob +# queue: background +# args: [ 1000, { batch_size: 500 } ] +# schedule: every hour +# periodic_cleanup_with_command: +# command: "SoftDeletedRecord.due.delete_all" +# priority: 2 +# schedule: at 5am every day + +production: + clear_solid_queue_finished_jobs: + command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" + schedule: every hour at minute 12 diff --git a/src/config/routes.rb b/src/config/routes.rb new file mode 100644 index 0000000000..5991b58de4 --- /dev/null +++ b/src/config/routes.rb @@ -0,0 +1,51 @@ +Rails.application.routes.draw do + + # Autenticação + get "/login", to: "autenticacao#new" + post "/login", to: "autenticacao#create" + delete "/logout", to: "autenticacao#destroy" + + # Painel Admin + get "/admin", to: "admin#index", as: :admin + + # CRUD de usuários + resources :usuarios + + # Página inicial Home + get "/home", to: "home#index" + + get '/redefinir_senha/edit', to: 'redefinicao_senha#edit', as: :edit_redefinir_senha + + patch '/redefinir_senha', to: 'redefinicao_senha#update', as: :redefinir_senha + + get '/esqueci_senha', to: 'redefinicao_senha#new', as: :esqueci_senha + post '/esqueci_senha', to: 'redefinicao_senha#create' + + get "admin/gerenciamento" => "admin#gerenciamento", as: :admin_gerenciamento + + get "up" => "rails/health#show", as: :rails_health_check + + post 'admin/gerenciamento/importar_dados', to: 'admin#importar_dados', as: 'importar_dados' + + get '/definir_senha', to: 'definicao_senha#new', as: :definir_senha + patch '/definir_senha', to: 'definicao_senha#create' + post '/definir_senha', to: 'definicao_senha#create' + + resources :templates do + resources :template_questions, only: [:create, :update, :destroy] do + post 'add_alternative', on: :member + end + end + + resources :formularios, only: [:index, :new, :create, :show] do + collection do + get 'pendentes' + end + resources :respostas, only: [:new, :create] + end + + resources :resultados, only: [:index, :show] + + resources :avaliacoes, only: [:index] + root "home#index" +end \ No newline at end of file diff --git a/src/config/storage.yml b/src/config/storage.yml new file mode 100644 index 0000000000..4942ab6694 --- /dev/null +++ b/src/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/src/config/tailwind.config.js b/src/config/tailwind.config.js new file mode 100644 index 0000000000..f9c38249a1 --- /dev/null +++ b/src/config/tailwind.config.js @@ -0,0 +1,8 @@ +module.exports = { + content: [ + './app/views/**/*.html.erb', + './app/helpers/**/*.rb', + './app/assets/stylesheets/**/*.css', + './app/javascript/**/*.js' + ], +} \ No newline at end of file diff --git a/src/db/cable_schema.rb b/src/db/cable_schema.rb new file mode 100644 index 0000000000..23666604a5 --- /dev/null +++ b/src/db/cable_schema.rb @@ -0,0 +1,11 @@ +ActiveRecord::Schema[7.1].define(version: 1) do + create_table "solid_cable_messages", force: :cascade do |t| + t.binary "channel", limit: 1024, null: false + t.binary "payload", limit: 536870912, null: false + t.datetime "created_at", null: false + t.integer "channel_hash", limit: 8, null: false + t.index ["channel"], name: "index_solid_cable_messages_on_channel" + t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" + t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" + end +end diff --git a/src/db/cache_schema.rb b/src/db/cache_schema.rb new file mode 100644 index 0000000000..81a410d188 --- /dev/null +++ b/src/db/cache_schema.rb @@ -0,0 +1,12 @@ +ActiveRecord::Schema[7.2].define(version: 1) do + create_table "solid_cache_entries", force: :cascade do |t| + t.binary "key", limit: 1024, null: false + t.binary "value", limit: 536870912, null: false + t.datetime "created_at", null: false + t.integer "key_hash", limit: 8, null: false + t.integer "byte_size", limit: 4, null: false + t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" + t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size" + t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true + end +end diff --git a/src/db/migrate/20251129191708_create_usuarios.rb b/src/db/migrate/20251129191708_create_usuarios.rb new file mode 100644 index 0000000000..b5a92d27f6 --- /dev/null +++ b/src/db/migrate/20251129191708_create_usuarios.rb @@ -0,0 +1,15 @@ +class CreateUsuarios < ActiveRecord::Migration[8.0] + def change + create_table :usuarios do |t| + t.string :nome + t.string :email + t.string :matricula + t.string :usuario + t.string :password_digest + t.integer :ocupacao + t.boolean :status + + t.timestamps + end + end +end diff --git a/src/db/migrate/20251129191719_create_materias.rb b/src/db/migrate/20251129191719_create_materias.rb new file mode 100644 index 0000000000..93dd26635d --- /dev/null +++ b/src/db/migrate/20251129191719_create_materias.rb @@ -0,0 +1,10 @@ +class CreateMaterias < ActiveRecord::Migration[8.0] + def change + create_table :materias do |t| + t.string :codigo + t.string :nome + + t.timestamps + end + end +end diff --git a/src/db/migrate/20251129191738_create_turmas.rb b/src/db/migrate/20251129191738_create_turmas.rb new file mode 100644 index 0000000000..cface20fb7 --- /dev/null +++ b/src/db/migrate/20251129191738_create_turmas.rb @@ -0,0 +1,15 @@ +class CreateTurmas < ActiveRecord::Migration[8.0] + def change + create_table :turmas do |t| + t.string :codigo + t.string :semestre + t.string :horario + t.references :materia, null: false, foreign_key: { to_table: :materias } + t.integer :id_docente, null: false + + t.timestamps + end + add_foreign_key :turmas, :usuarios, column: :id_docente + add_index :turmas, :id_docente + end +end diff --git a/src/db/migrate/20251129191741_create_templates.rb b/src/db/migrate/20251129191741_create_templates.rb new file mode 100644 index 0000000000..cb8e430891 --- /dev/null +++ b/src/db/migrate/20251129191741_create_templates.rb @@ -0,0 +1,13 @@ +class CreateTemplates < ActiveRecord::Migration[8.0] + def change + create_table :templates do |t| + t.string :titulo + t.string :participantes + t.integer :id_criador, null: false + + t.timestamps + end + add_foreign_key :templates, :usuarios, column: :id_criador + add_index :templates, :id_criador + end +end diff --git a/src/db/migrate/20251129191758_create_questoes.rb b/src/db/migrate/20251129191758_create_questoes.rb new file mode 100644 index 0000000000..032fc9ae2a --- /dev/null +++ b/src/db/migrate/20251129191758_create_questoes.rb @@ -0,0 +1,11 @@ +class CreateQuestoes < ActiveRecord::Migration[8.0] + def change + create_table :questoes do |t| + t.text :enunciado + t.integer :tipo + t.references :template, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/src/db/migrate/20251129191802_create_opcoes.rb b/src/db/migrate/20251129191802_create_opcoes.rb new file mode 100644 index 0000000000..e20d4873ab --- /dev/null +++ b/src/db/migrate/20251129191802_create_opcoes.rb @@ -0,0 +1,10 @@ +class CreateOpcoes < ActiveRecord::Migration[8.0] + def change + create_table :opcoes do |t| + t.string :texto_opcao + t.references :questao, null: false, foreign_key: { to_table: :questoes } + + t.timestamps + end + end +end diff --git a/src/db/migrate/20251129191807_create_formularios.rb b/src/db/migrate/20251129191807_create_formularios.rb new file mode 100644 index 0000000000..ff419bac87 --- /dev/null +++ b/src/db/migrate/20251129191807_create_formularios.rb @@ -0,0 +1,12 @@ +class CreateFormularios < ActiveRecord::Migration[8.0] + def change + create_table :formularios do |t| + t.string :titulo_envio + t.datetime :data_criacao + t.references :template, null: false, foreign_key: true + t.references :turma, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/src/db/migrate/20251129191822_create_respostas.rb b/src/db/migrate/20251129191822_create_respostas.rb new file mode 100644 index 0000000000..9794654c1b --- /dev/null +++ b/src/db/migrate/20251129191822_create_respostas.rb @@ -0,0 +1,13 @@ +class CreateRespostas < ActiveRecord::Migration[8.0] + def change + create_table :respostas do |t| + t.datetime :data_submissao + t.references :formulario, null: false, foreign_key: true + t.integer :id_participante, null: false + + t.timestamps + end + add_foreign_key :respostas, :usuarios, column: :id_participante + add_index :respostas, :id_participante + end +end diff --git a/src/db/migrate/20251129191826_create_resposta_items.rb b/src/db/migrate/20251129191826_create_resposta_items.rb new file mode 100644 index 0000000000..520fe7e6ea --- /dev/null +++ b/src/db/migrate/20251129191826_create_resposta_items.rb @@ -0,0 +1,14 @@ +class CreateRespostaItems < ActiveRecord::Migration[8.0] + def change + create_table :resposta_items do |t| + t.text :texto_resposta + t.references :resposta, null: false, foreign_key: { to_table: :respostas } + t.references :questao, null: false, foreign_key: { to_table: :questoes } + t.integer :id_opcao_escolhida, null: true + + t.timestamps + end + add_foreign_key :resposta_items, :opcoes, column: :id_opcao_escolhida + add_index :resposta_items, :id_opcao_escolhida + end +end diff --git a/src/db/migrate/20251202011508_create_matriculas.rb b/src/db/migrate/20251202011508_create_matriculas.rb new file mode 100644 index 0000000000..e32787279f --- /dev/null +++ b/src/db/migrate/20251202011508_create_matriculas.rb @@ -0,0 +1,10 @@ +class CreateMatriculas < ActiveRecord::Migration[8.0] + def change + create_table :matriculas do |t| + t.integer :id_usuario + t.integer :id_turma + + t.timestamps + end + end +end diff --git a/src/db/migrate/20251205210049_create_template_questions.rb b/src/db/migrate/20251205210049_create_template_questions.rb new file mode 100644 index 0000000000..afbd7fa02c --- /dev/null +++ b/src/db/migrate/20251205210049_create_template_questions.rb @@ -0,0 +1,12 @@ +class CreateTemplateQuestions < ActiveRecord::Migration[8.0] + def change + create_table :template_questions do |t| + t.string :title, default: "" + t.string :question_type, default: "text" + t.text :content, default: "[]" + t.references :template, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/src/db/migrate/20251205210703_add_name_and_hidden_to_templates.rb b/src/db/migrate/20251205210703_add_name_and_hidden_to_templates.rb new file mode 100644 index 0000000000..a05612411c --- /dev/null +++ b/src/db/migrate/20251205210703_add_name_and_hidden_to_templates.rb @@ -0,0 +1,6 @@ +class AddNameAndHiddenToTemplates < ActiveRecord::Migration[8.0] + def change + add_column :templates, :name, :string + add_column :templates, :hidden, :boolean, default: false + end +end diff --git a/src/db/migrate/20251210003357_add_data_encerramento_to_formularios.rb b/src/db/migrate/20251210003357_add_data_encerramento_to_formularios.rb new file mode 100644 index 0000000000..bb8d123218 --- /dev/null +++ b/src/db/migrate/20251210003357_add_data_encerramento_to_formularios.rb @@ -0,0 +1,5 @@ +class AddDataEncerramentoToFormularios < ActiveRecord::Migration[8.0] + def change + add_column :formularios, :data_encerramento, :datetime + end +end diff --git a/src/db/queue_schema.rb b/src/db/queue_schema.rb new file mode 100644 index 0000000000..85194b6a88 --- /dev/null +++ b/src/db/queue_schema.rb @@ -0,0 +1,129 @@ +ActiveRecord::Schema[7.1].define(version: 1) do + create_table "solid_queue_blocked_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.string "concurrency_key", null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" + t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.bigint "process_id" + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.text "error" + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", force: :cascade do |t| + t.string "queue_name", null: false + t.string "class_name", null: false + t.text "arguments" + t.integer "priority", default: 0, null: false + t.string "active_job_id" + t.datetime "scheduled_at" + t.datetime "finished_at" + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" + t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" + t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" + t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" + t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", force: :cascade do |t| + t.string "queue_name", null: false + t.datetime "created_at", null: false + t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", force: :cascade do |t| + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.bigint "supervisor_id" + t.integer "pid", null: false + t.string "hostname" + t.text "metadata" + t.datetime "created_at", null: false + t.string "name", null: false + t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" + t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "task_key", null: false + t.datetime "run_at", null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + end + + create_table "solid_queue_recurring_tasks", force: :cascade do |t| + t.string "key", null: false + t.string "schedule", null: false + t.string "command", limit: 2048 + t.string "class_name" + t.text "arguments" + t.string "queue_name" + t.integer "priority", default: 0 + t.boolean "static", default: true, null: false + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" + end + + create_table "solid_queue_scheduled_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "scheduled_at", null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", force: :cascade do |t| + t.string "key", null: false + t.integer "value", default: 1, null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" + t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" + t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true + end + + add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade +end diff --git a/src/db/schema.rb b/src/db/schema.rb new file mode 100644 index 0000000000..09b655476b --- /dev/null +++ b/src/db/schema.rb @@ -0,0 +1,137 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.0].define(version: 2025_12_10_003357) do + create_table "formularios", force: :cascade do |t| + t.string "titulo_envio" + t.datetime "data_criacao" + t.integer "template_id", null: false + t.integer "turma_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.datetime "data_encerramento" + t.index ["template_id"], name: "index_formularios_on_template_id" + t.index ["turma_id"], name: "index_formularios_on_turma_id" + end + + create_table "materias", force: :cascade do |t| + t.string "codigo" + t.string "nome" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "matriculas", force: :cascade do |t| + t.integer "id_usuario" + t.integer "id_turma" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "opcoes", force: :cascade do |t| + t.string "texto_opcao" + t.integer "questao_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["questao_id"], name: "index_opcoes_on_questao_id" + end + + create_table "questoes", force: :cascade do |t| + t.text "enunciado" + t.integer "tipo" + t.integer "template_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["template_id"], name: "index_questoes_on_template_id" + end + + create_table "resposta_items", force: :cascade do |t| + t.text "texto_resposta" + t.integer "resposta_id", null: false + t.integer "questao_id", null: false + t.integer "id_opcao_escolhida" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["id_opcao_escolhida"], name: "index_resposta_items_on_id_opcao_escolhida" + t.index ["questao_id"], name: "index_resposta_items_on_questao_id" + t.index ["resposta_id"], name: "index_resposta_items_on_resposta_id" + end + + create_table "respostas", force: :cascade do |t| + t.datetime "data_submissao" + t.integer "formulario_id", null: false + t.integer "id_participante", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["formulario_id"], name: "index_respostas_on_formulario_id" + t.index ["id_participante"], name: "index_respostas_on_id_participante" + end + + create_table "template_questions", force: :cascade do |t| + t.string "title", default: "" + t.string "question_type", default: "text" + t.text "content", default: "[]" + t.integer "template_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["template_id"], name: "index_template_questions_on_template_id" + end + + create_table "templates", force: :cascade do |t| + t.string "titulo" + t.string "participantes" + t.integer "id_criador", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "name" + t.boolean "hidden", default: false + t.index ["id_criador"], name: "index_templates_on_id_criador" + end + + create_table "turmas", force: :cascade do |t| + t.string "codigo" + t.string "semestre" + t.string "horario" + t.integer "materia_id", null: false + t.integer "id_docente", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["id_docente"], name: "index_turmas_on_id_docente" + t.index ["materia_id"], name: "index_turmas_on_materia_id" + end + + create_table "usuarios", force: :cascade do |t| + t.string "nome" + t.string "email" + t.string "matricula" + t.string "usuario" + t.string "password_digest" + t.integer "ocupacao" + t.boolean "status" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_foreign_key "formularios", "templates" + add_foreign_key "formularios", "turmas" + add_foreign_key "opcoes", "questoes", column: "questao_id" + add_foreign_key "questoes", "templates" + add_foreign_key "resposta_items", "opcoes", column: "id_opcao_escolhida" + add_foreign_key "resposta_items", "questoes", column: "questao_id" + add_foreign_key "resposta_items", "respostas" + add_foreign_key "respostas", "formularios" + add_foreign_key "respostas", "usuarios", column: "id_participante" + add_foreign_key "template_questions", "templates" + add_foreign_key "templates", "usuarios", column: "id_criador" + add_foreign_key "turmas", "materias" + add_foreign_key "turmas", "usuarios", column: "id_docente" +end diff --git a/src/db/seeds.rb b/src/db/seeds.rb new file mode 100644 index 0000000000..8f0193c399 --- /dev/null +++ b/src/db/seeds.rb @@ -0,0 +1,8 @@ +Usuario.find_or_create_by!(email: 'admin@test.com') do |user| + user.nome = 'Administrador Default' + user.usuario = 'admin' + user.matricula = '000000' + user.ocupacao = :admin + user.status = true + user.password = 'password' +end \ No newline at end of file diff --git a/src/features/atualizar_dados_sigaa.feature b/src/features/atualizar_dados_sigaa.feature new file mode 100644 index 0000000000..83e365d781 --- /dev/null +++ b/src/features/atualizar_dados_sigaa.feature @@ -0,0 +1,62 @@ +# language: pt +# features/atualizar_dados_sigaa.feature + +Funcionalidade: Atualizar base de dados com os dados do SIGAA + Eu como Administrador + Quero atualizar a base de dados já existente com os dados atuais do sigaa + A fim de corrigir a base de dados do sistema. + + Contexto: + Dado que eu estou logado como Administrador + E estou na página "Gerenciamento" + + @happy_path + Cenário: Sincronizar participante que mudou de e-mail + Dado que o sistema possui o usuário "Fulano de Tal" ("150084006") cadastrado com o e-mail "fulano.antigo@email.com" + E a fonte de dados externa indica que o e-mail de "150084006" agora é "fulano.novo@gmail.com" + Quando eu solicito a importação clicando em "Importar dados" + Então o e-mail do usuário "150084006" deve ser atualizado para "fulano.novo@gmail.com" + E nenhum usuário duplicado deve ser criado + E eu devo ver a mensagem de sucesso "Dados importados com sucesso!" + + @happy_path + Cenário: Sincronizar matrícula de participante em nova turma + Dado que o sistema possui o usuário "Fulano de Tal" ("150084006") cadastrado + E que o sistema possui a turma "TA" da matéria "CIC0097" cadastrada + E o usuário "150084006" ainda não está matriculado na turma "TA" da matéria "CIC0097" + E a fonte de dados externa indica que "150084006" está matriculado na turma "TA" da matéria "CIC0097" + Quando eu solicito a importação clicando em "Importar dados" + Então o usuário "150084006" deve ser matriculado na turma "TA" da matéria "CIC0097" + E eu devo ver a mensagem de sucesso "Dados importados com sucesso!" + + @happy_path + Cenário: Sincronizar participante que mudou de nome + Dado que o sistema possui o usuário "Fulano de Tal" ("150084006") cadastrado + E a fonte de dados externa indica que o nome de "150084006" agora é "Fulano da Silva" + Quando eu solicito a importação clicando em "Importar dados" + Então o nome do usuário "150084006" deve ser atualizado para "Fulano da Silva" + E eu devo ver a mensagem de sucesso "Dados importados com sucesso!" + + @happy_path + Cenário: Sincronizar matéria que mudou de nome + Dado que o sistema possui a matéria "CIC0097" cadastrada + E a fonte de dados externa indica que o nome da matéria "CIC0097" agora é "BANCOS DE DADOS AVANÇADO" + Quando eu solicito a importação clicando em "Importar dados" + Então o nome da matéria "CIC0097" deve ser atualizado para "BANCOS DE DADOS AVANÇADO" + E eu devo ver a mensagem de sucesso "Dados importados com sucesso!" + + @happy_path + Cenário: Sincronizar participante que foi removido do SIGAA + Dado que o sistema possui o usuário "Fulano de Tal" ("150084006") cadastrado + E a fonte de dados externa indica que "150084006" não está mais presente + Quando eu solicito a importação clicando em "Importar dados" + Então o usuário "150084006" deve ser excluído do sistema + E eu devo ver a mensagem de sucesso "Dados importados com sucesso!" + + @sad_path + Cenário: Falha ao buscar os dados externos + Dado que o sigaa está indisponível + Quando eu solicito a importação clicando em "Importar dados" + Então eu devo ver a mensagem de erro "Não foi possível buscar os dados. Tente novamente mais tarde." + E nenhuma nova turma deve ser cadastrada no sistema + E nenhum novo usuário deve ser cadastrado no sistema \ No newline at end of file diff --git a/src/features/autenticacao.feature b/src/features/autenticacao.feature new file mode 100644 index 0000000000..9b4ede3ebc --- /dev/null +++ b/src/features/autenticacao.feature @@ -0,0 +1,63 @@ +# language: pt +# features/autenticacao.feature +Funcionalidade: Autenticação de Usuário + Eu como Usuário do sistema + Quero acessar o sistema utilizando um e-mail ou matrícula e uma senha já cadastrada + A fim de responder formulários ou gerenciar o sistema + + Contexto: + Dado que eu estou na página de login + E existe um usuário "aluno" cadastrado com email "aluno@teste.com", matrícula "123456" e senha "senha123" + E existe um usuário "admin" cadastrado com email "admin@teste.com", matrícula "987654", senha "admin123" e com permissão de administrador + + @happy_path + Cenário: Login com email válido (Usuário Padrão) + Quando eu preencho o campo "Usuário" com "aluno@teste.com" + E eu preencho o campo "Senha" com "senha123" + E eu clico no botão "Entrar" + Então eu devo ser redirecionado para a página inicial + E eu devo ver a mensagem "Login realizado com sucesso!" + E eu NÃO devo ver a opção "Gerenciamento" no menu lateral + + @happy_path + Cenário: Login com email válido (Usuário Admin) + Quando eu preencho o campo "Usuário" com "admin@teste.com" + E eu preencho o campo "Senha" com "admin123" + E eu clico no botão "Entrar" + Então eu devo ser redirecionado para a página de administrador + E eu devo ver a mensagem "Bem-vindo, Administrador!" + E eu devo ver a opção "Gerenciamento" no menu lateral + + @happy_path + Cenário: Login com matrícula válida + Quando eu preencho o campo "Usuário" com "123456" + E eu preencho o campo "Senha" com "senha123" + E eu clico no botão "Entrar" + Então eu devo ser redirecionado para a página inicial + E eu devo ver a mensagem "Login realizado com sucesso!" + + @sad_path + Cenário: Login com senha incorreta + Quando eu preencho o campo "Usuário" com "aluno@teste.com" + E eu preencho o campo "Senha" com "senhaErrada" + E eu clico no botão "Entrar" + Então eu devo permanecer na página de login + E eu devo ver a mensagem "Senha incorreta" + + @sad_path + Cenário: Login com usuário inexistente + Quando eu preencho o campo "Usuário" com "naoexisto@teste.com" + E eu preencho o campo "Senha" com "qualquercoisa" + E eu clico no botão "Entrar" + Então eu devo permanecer na página de login + E eu devo ver a mensagem "Usuário não encontrado" + + @sad_path + Cenário: Tentativa de login de usuário pré-cadastrado (pendente) + Dado que existe um usuário "José Novo" ("111222") pré-cadastrado via SIGAA, mas com status "pendente" + E eu estou na página de login + Quando eu preencho o campo "Usuário" com "111222" + E eu preencho o campo "Senha" com "qualquercoisa" + E eu clico no botão "Entrar" + Então eu devo permanecer na página de login + E eu devo ver a mensagem "Sua conta está pendente. Por favor, redefina sua senha para ativar." \ No newline at end of file diff --git a/src/features/cadastrar_usuarios.feature b/src/features/cadastrar_usuarios.feature new file mode 100644 index 0000000000..ac28a0ce7a --- /dev/null +++ b/src/features/cadastrar_usuarios.feature @@ -0,0 +1,36 @@ +# language: pt +# features/autenticacao.feature + +Funcionalidade: Cadastrar usuários do sistema (e enviar convite por email) + Eu como Administrador + Quero cadastrar participantes de turmas do SIGAA ao importar dados de usuarios novos para o sistema + A fim de que eles acessem o sistema CAMAAR + + Contexto: + Dado que eu estou logado como Administrador + E estou na página "Gerenciamento" + + @happy_path + Cenário: Importar um usuário que é novo no sistema + Dado que o sigaa contém o usuário "Fulano de Tal" ("150084006") com e-mail "fulano@gmail.com" + E que o sistema não possui o usuário "Fulano de Tal" ("150084006") cadastrado + Quando eu solicito a importação clicando em "Importar dados" + Então o usuário "Fulano de Tal" ("150084006") deve ser criado no sistema com o status "pendente" + E um e-mail de "Definição de Senha" deve ser enviado para "fulano@gmail.com" + E eu devo ver a mensagem de sucesso "Dados importados com sucesso!" + + @happy_path + Cenário: Importar um usuário que já existe no sistema + Dado que o sigaa contém o usuário "Fulano de Tal" ("150084006") + E que o sistema possui o usuário "Fulano de Tal" ("150084006") cadastrado (seja pendente ou ativo) + Quando eu solicito a importação clicando em "Importar dados" + Então nenhum novo e-mail de "Definição de Senha" deve ser enviado para "150084006" + E nenhum usuário duplicado deve ser criado + + @sad_path + Cenário: Importar um novo usuário que não possui e-mail + Dado que o sigaa contém o usuário "Usuário Sem Email" ("190099999") + Mas o usuário "190099999" não possui um endereço de e-mail + Quando eu solicito a importação clicando em "Importar dados" + Então o usuário "190099999" não deve ser criado no sistema + E eu devo ver uma mensagem de erro "Falha ao importar usuário '190099999': e-mail ausente." \ No newline at end of file diff --git a/src/features/criar_formulario.feature b/src/features/criar_formulario.feature new file mode 100644 index 0000000000..2f3454e0a0 --- /dev/null +++ b/src/features/criar_formulario.feature @@ -0,0 +1,38 @@ +# language: pt +# features/criar_formulario.feature + +Funcionalidade: Criação de Formulário de Avaliação + Eu como Administrador + Quero criar um formulário baseado em um template para as turmas que eu escolher + A fim de avaliar o desempenho das turmas no semestre atual + +Contexto: + Dado que eu sou um "admin" logado no sistema + E existe um template "Avaliação de Meio de Semestre" + E existem as turmas "Engenharia de Software - TA" e "Banco de Dados - TB" importadas do SIGAA + +@happy_path +Cenário: Admin cria um formulário para múltiplas turmas + Dado que eu estou na página "formularios/new" + Quando eu seleciono o template "Avaliação de Meio de Semestre" + E eu seleciono as turmas "Engenharia de Software - TA" e "Banco de Dados - TB" + E eu defino a data de encerramento para "31/12/2025" + E eu clico no botão "Gerar Formulário" + Então eu devo ser redirecionado para a página "formularios" + E eu devo ver a mensagem "Formulário distribuído com sucesso para 2 turmas" + +@sad_path +Cenário: Admin tenta criar um formulário sem selecionar um template + Dado que eu estou na página "formularios/new" + Quando eu seleciono as turmas "Engenharia de Software - TA" + E eu clico no botão "Gerar Formulário" + Então eu devo permanecer na página "formularios/new" + E eu devo ver a mensagem de erro "Selecione um template" + +@sad_path +Cenário: Admin tenta criar um formulário sem selecionar turmas + Dado que eu estou na página "formularios/new" + Quando eu seleciono o template "Avaliação de Meio de Semestre" + E eu clico no botão "Gerar Formulário" + Então eu devo permanecer na página "formularios/new" + E eu devo ver a mensagem de erro "Selecione pelo menos uma turma" \ No newline at end of file diff --git a/src/features/criar_formulario_usuario.feature b/src/features/criar_formulario_usuario.feature new file mode 100644 index 0000000000..4aef9b03c6 --- /dev/null +++ b/src/features/criar_formulario_usuario.feature @@ -0,0 +1,76 @@ +# language: pt +# features/criar_formulario_bonus_113.feature + +Funcionalidade: Criação de Formulário de Avaliação (Admin + Usuários comuns - Bônus) + Eu como usuário do sistema (Admin, Docente ou Discente) + Quero criar um formulário baseado em um template para as turmas que eu escolher + A fim de avaliar o desempenho das turmas no semestre atual + +Contexto: + Dado que existe um template "Avaliação de Meio de Semestre" + E existem as turmas "Engenharia de Software - TA" e "Banco de Dados - TB" importadas do SIGAA + +@happy_path +Cenário: Admin cria um formulário para múltiplas turmas (Admin) + Dado que eu sou um "admin" logado no sistema + E eu estou na página "formularios/new" + Quando eu seleciono o template "Avaliação de Meio de Semestre" + E eu seleciono as turmas "Engenharia de Software - TA" e "Banco de Dados - TB" + E eu defino a data de encerramento para "31/12/2025" + E eu clico no botão "Gerar Formulário" + Então eu devo ser redirecionado para a página "formularios" + E eu devo ver a mensagem "Formulário criado com sucesso e associado a 2 turma(s)" + E o formulário deve estar associado ao template "Avaliação de Meio de Semestre" + +@sad_path +Cenário: Admin tenta criar um formulário sem selecionar um template + Dado que eu sou um "admin" logado no sistema + E eu estou na página "formularios/new" + Quando eu seleciono as turmas "Engenharia de Software - TA" + E eu clico no botão "Gerar Formulário" + Então eu devo permanecer na página "formularios/new" + E eu devo ver a mensagem de erro "É necessário selecionar um template" + +@sad_path +Cenário: Admin tenta criar um formulário sem selecionar turmas + Dado que eu sou um "admin" logado no sistema + E eu estou na página "formularios/new" + Quando eu seleciono o template "Avaliação de Meio de Semestre" + E eu clico no botão "Gerar Formulário" + Então eu devo permanecer na página "formularios/new" + E eu devo ver a mensagem de erro "É necessário selecionar pelo menos uma turma" + +@happy_path +Cenário: Docente cria um formulário para suas turmas (Usuário comum) + Dado que eu sou um "docente" logado no sistema + E eu sou responsável pelas turmas "Engenharia de Software - TA" + E eu estou na página "formularios/new" + Quando eu seleciono o template "Avaliação de Meio de Semestre" + E eu seleciono a turma "Engenharia de Software - TA" + E eu defino a data de encerramento para "15/12/2025" + E eu clico no botão "Gerar Formulário" + Então eu devo ser redirecionado para a página "formularios" + E eu devo ver a mensagem "Formulário criado com sucesso e associado a 1 turma(s)" + E o formulário deve estar associado ao docente atual + +@happy_path +Cenário: Discente cria um formulário para sua própria turma (Usuário comum) + Dado que eu sou um "discente" logado no sistema + E eu estou matriculado na turma "Banco de Dados - TB" + E eu estou na página "formularios/new" + Quando eu seleciono o template "Avaliação de Meio de Semestre" + E eu seleciono a turma "Banco de Dados - TB" + E eu defino a data de encerramento para "20/12/2025" + E eu clico no botão "Gerar Formulário" + Então eu devo ser redirecionado para a página "formularios" + E eu devo ver a mensagem "Formulário criado com sucesso e associado a 1 turma(s)" + E o formulário deve estar marcado como criado por "discente" + +@sad_path +Cenário: Usuário sem permissão tenta criar formulário (Acesso negado) + Dado que eu sou um "convidado" não autenticado + E eu estou na página "formularios/new" + Quando eu tento acessar a funcionalidade de criação (clicar no botão "Gerar Formulário") + Então eu devo ser redirecionado para a página "login" + E eu devo ver a mensagem "É necessário estar logado para criar formulários" + diff --git a/src/features/criar_template.feature b/src/features/criar_template.feature new file mode 100644 index 0000000000..3bc979a61a --- /dev/null +++ b/src/features/criar_template.feature @@ -0,0 +1,45 @@ +# language: pt +# features/criar_template.feature + +Funcionalidade: Criação de Template de Formulário + Eu como Administrador + Quero criar um template de formulário contendo as questões do formulário + A fim de gerar formulários de avaliações para avaliar o desempenho das turmas + +Contexto: + Dado que eu sou um "admin" logado no sistema + +@happy_path +Cenário: Admin cria um template com sucesso + Dado que eu estou na página "templates/new" + Quando eu preencho o campo do template "Nome do Template" com "Avaliação Semestral 2025.1" + E eu clico no botão do template "Salvar Template" + Então eu devo ser redirecionado para a página de edição do template "Avaliação Semestral 2025.1" + E eu devo ver a mensagem do template "Template criado com sucesso" + Quando eu adiciono uma pergunta "O professor foi didático?" do tipo "numérica (1-5)" + E eu adiciono uma pergunta "A infraestrutura foi adequada?" do tipo "múltipla escolha" com opções "Sim, Não, Parcialmente" + E eu adiciono uma pergunta "Comentários gerais" do tipo "texto" + Então eu devo ver a mensagem do template "template alterado com sucesso" + +@sad_path +Cenário: Admin tenta criar um template sem nome + Dado que eu estou na página "templates/new" + Quando eu clico no botão do template "Salvar Template" + Então eu devo permanecer na página de novo template + E eu devo ver a mensagem do template "Nome do Template não pode ficar em branco" + +@sad_path +Cenário: Admin tenta criar um template com uma pergunta sem texto + Dado que eu estou na página "templates/new" + Quando eu preencho o campo do template "Nome do Template" com "Template Teste" + E eu clico no botão do template "Salvar Template" + E eu adiciono uma pergunta "" do tipo "texto" + Então eu devo ver a mensagem do template "o texto da questão é obrigatório" + +@sad_path +Cenário: Admin tenta criar um template com alternativas vazias + Dado que eu estou na página "templates/new" + Quando eu preencho o campo do template "Nome do Template" com "Template Teste" + E eu clico no botão do template "Salvar Template" + E eu adiciono uma pergunta "Qual sua cor favorita?" do tipo "múltipla escolha" com opções "Azul, , Vermelho" + Então eu devo ver a mensagem do template "Todas as alternativas devem ser preenchidas" \ No newline at end of file diff --git a/src/features/definir_senha_usuario.feature b/src/features/definir_senha_usuario.feature new file mode 100644 index 0000000000..fa0501b87e --- /dev/null +++ b/src/features/definir_senha_usuario.feature @@ -0,0 +1,64 @@ +# language: pt +# features/definir_senha_usuario.feature + +Funcionalidade: Sistema de definição de senha + Eu como Usuário + Quero definir uma senha para o meu usuário a partir do e-mail do sistema de solicitação de cadastro + A fim de acessar o sistema + + Contexto: + Dado que o usuário "fulano.novo@email.com" foi importado e está com o status "pendente" + E um link de definição de senha válido foi enviado para "fulano.novo@email.com" + + @happy_path + Cenário: Definição de senha com sucesso + Quando eu acesso a página "Defina sua Senha" usando o link válido + E eu preencho o campo "Nova Senha" com "senhaForte123" + E eu preencho o campo "Confirme a senha" com "senhaForte123" + E eu clico no botão "Alterar Senha" + Então eu devo ser redirecionado para a página de "Login" + E eu devo ver a mensagem "Senha definida com sucesso! Você já pode fazer o login." + E o status do usuário "fulano.novo@email.com" no sistema deve ser "ativo" + + @sad_path + Cenário: Senhas não conferem + Quando eu acesso a página "Defina sua Senha" usando o link válido + E eu preencho o campo "Nova Senha" com "senhaForte123" + E eu preencho o campo "Confirme a senha" com "outraCoisaDiferente" + E eu clico no botão "Alterar Senha" + Então eu devo permanecer na página "Defina sua Senha" + E eu devo ver a mensagem de erro "As senhas não conferem." + + @sad_path + Cenário: Tentar usar o link de definição de senha quando já está ativo + Dado que o usuário "fulano.ativo@gmail.com" já está ativo no sistema + Quando eu acesso a página "Defina sua Senha" usando o link antigo + Então eu devo ser redirecionado para a página de "Login" + E eu devo ver a mensagem "Você já está ativo. Faça o login." + + @sad_path + Cenário: Campos em branco + Quando eu acesso a página "Defina sua Senha" usando o link válido + E eu deixo o campo "Nova Senha" em branco + E eu deixo o campo "Confirme a senha" em branco + E eu clico no botão "Alterar Senha" + Então eu devo permanecer na página "Defina sua Senha" + E eu devo ver a mensagem de erro "Todos os campos devem ser preenchidos." + + @sad_path + Cenário: Campo "Senha" em branco + Quando eu acesso a página "Defina sua Senha" usando o link válido + E eu deixo o campo "Nova Senha" em branco + E eu preencho o campo "Confirme a senha" com "senhaForte123" + E eu clico no botão "Alterar Senha" + Então eu devo permanecer na página "Defina sua Senha" + E eu devo ver a mensagem de erro "Todos os campos devem ser preenchidos." + + @sad_path + Cenário: Campo "Confirme a senha" em branco + Quando eu acesso a página "Defina sua Senha" usando o link válido + E eu preencho o campo "Nova Senha" com "senhaForte123" + E eu deixo o campo "Confirme a senha" em branco + E eu clico no botão "Alterar Senha" + Então eu devo permanecer na página "Defina sua Senha" + E eu devo ver a mensagem de erro "Todos os campos devem ser preenchidos." \ No newline at end of file diff --git a/src/features/distribuicao_avaliacoes.feature b/src/features/distribuicao_avaliacoes.feature new file mode 100644 index 0000000000..9bcf0ecbd0 --- /dev/null +++ b/src/features/distribuicao_avaliacoes.feature @@ -0,0 +1,29 @@ +# language: pt +Funcionalidade: Distribuição de Formulários de Avaliação + Como Administrador + Eu quero distribuir um template de avaliação para uma ou mais turmas + Para que os participantes (alunos e professores) possam responder à avaliação + + Contexto: + Dado que eu estou logado como administrador + E que existe um template de avaliação "Avaliação Semestral" + E que existe a turma "Engenharia de Software" com 5 alunos matriculados + E que existe a turma "Banco de Dados" com 3 alunos matriculados + + @happy_path + Cenário: Admin distribui formulário para múltiplas turmas + Dado que eu estou na página de distribuição de formulários + Quando eu seleciono o template de avaliação "Avaliação Semestral" + E eu seleciono as turmas para distribuição "Engenharia de Software" e "Banco de Dados" + E eu clico no botão de distribuição "Distribuir Formulário" + Então eu devo ver a mensagem de sucesso de distribuição "Formulário distribuído com sucesso para 2 turmas" + E a turma "Engenharia de Software" deve ter um formulário associado ao template "Avaliação Semestral" + E todos os 5 alunos da turma "Engenharia de Software" devem ter uma resposta pendente para este formulário + E a turma "Banco de Dados" deve ter um formulário associado ao template "Avaliação Semestral" + + @sad_path + Cenário: Admin tenta distribuir sem selecionar turmas + Dado que eu estou na página de distribuição de formulários + Quando eu seleciono o template de avaliação "Avaliação Semestral" + E eu clico no botão de distribuição "Distribuir Formulário" + Então eu devo ver a mensagem de erro de distribuição "Selecione pelo menos uma turma" diff --git a/src/features/edit_and_delete_templates.feature b/src/features/edit_and_delete_templates.feature new file mode 100644 index 0000000000..d51d57ed9e --- /dev/null +++ b/src/features/edit_and_delete_templates.feature @@ -0,0 +1,28 @@ +# language: pt +Funcionalidade: Editar e Deletar Templates + Como um Administrador + Eu quero editar e deletar templates + Para que eu possa gerenciar os formulários disponíveis no sistema + + Contexto: + Dado que eu estou logado como administrador + E que existe um template chamado "Template Antigo" + + Cenário: Editar nome do template + Dado que eu estou na página de edição de "Template Antigo" + Quando eu preencho o campo do template "Nome" com "Template Novo" + E eu clico no botão do template "Atualizar Nome" + Então eu devo ver a mensagem do template "Template atualizado com sucesso" + E o nome do template deve ser "Template Novo" + + Cenário: Adicionar uma questão ao template + Dado que eu estou na página de edição de "Template Antigo" + Quando eu clico no botão do template "Adicionar Questão" + Então eu devo ver um formulário de nova questão + E o número total de questões deve ser 1 + + Cenário: Exclusão lógica de um template + Dado que eu estou na página de listagem de templates + Quando eu clico em "Deletar" para "Template Antigo" + Então eu não devo ver "Template Antigo" + Mas o template "Template Antigo" deve continuar existindo no banco de dados diff --git a/src/features/editar_templates.feature b/src/features/editar_templates.feature new file mode 100644 index 0000000000..50225e84c6 --- /dev/null +++ b/src/features/editar_templates.feature @@ -0,0 +1,133 @@ +# language: pt + +# features/editar_templates.feature + +Funcionalidade: Edição e exclusão de questões em templates + Como Administrador + Quero editar e/ou deletar um template que eu criei sem afetar os formulários já criados + A fim de organizar os templates existentes + +Contexto: + Dado que estou na página de "gerenciamento de templates" + E seleciono o template com o campo nome "Template1" e o campo semestre "2025.2" + E o template contém duas questões, sendo: + | número | tipo | texto | opções | + | 1 | texto | texto para a questão 1 | | + | 2 | radio | texto para a questão 2 | Opção 1, Opção 2, Opção 3 | + E visualizo a página do template escolhido + +############################################################################# +# EXCLUSÃO DE QUESTÕES +############################################################################# + +@happy_path +Cenário: Excluir a questão 2 do template + Quando eu clico no botão de exclusão ao lado da questão 2 + E clico em salvar + Então devo ver a mensagem "template alterado com sucesso" + +@happy_path +Cenário: Excluir a questão 1 e renumerar as questões + Quando eu clico no botão de exclusão ao lado da questão 1 + Então devo ver que a questão 2 migrou para a posição da questão 1 + E clico em salvar + Então devo ver a mensagem "template alterado com sucesso" + +@sad_path +Cenário: Tentar excluir todas as questões de um template + Quando eu clico no botão de exclusão ao lado da questão 1 + E clico no botão de exclusão ao lado da (nova) questão 1 + Então devo ver a mensagem "não é possível salvar template sem questões" + E devo permanecer na página de edição do template + +############################################################################# +# ALTERAÇÃO DO TIPO DA QUESTÃO +############################################################################# + +@happy_path +Cenário: Alterar o tipo da questão 2 de radio para texto + Dado que a questão 2 é do tipo "radio" com opções "Opção 1, Opção 2, Opção 3" + Quando eu altero o tipo da questão 2 para "texto" + E preencho o campo texto com "novo texto para questão 2" + E clico em salvar + Então devo ver a mensagem "template alterado com sucesso" + +@happy_path +Cenário: Alterar o tipo da questão 1 de texto para texto (sem mudança real) + Dado que a questão 1 é do tipo "texto" + Quando eu altero o tipo da questão 1 para "texto" + E clico em salvar + Então devo ver a mensagem "template alterado com sucesso" + +@happy_path +Cenário: Alterar o tipo da questão 1 de texto para radio + Dado que a questão 1 é do tipo "texto" + Quando eu altero o tipo da questão 1 para "radio" + E preencho o campo texto com "novo texto para a questão 1" + E preencho o campo Opções com "Opção A, Opção B, Opção C" + E clico em salvar + Então devo ver a mensagem "template alterado com sucesso" + +@happy_path +Cenário: Alterar o tipo da questão 2 de radio para radio (sem mudança real) + Dado que a questão 2 é do tipo "radio" + Quando eu altero o tipo da questão 2 para "radio" + E clico em salvar + Então devo ver a mensagem "template alterado com sucesso" + +############################################################################# +# ALTERAÇÃO DO CORPO DAS QUESTÕES +############################################################################# + +@happy_path +Cenário: Alterar o texto da questão 1 (tipo texto) com valor válido + Dado que a questão 1 é do tipo "texto" + Quando eu altero o corpo para "novo corpo da questão 1" + E clico em salvar + Então devo ver a mensagem "template alterado com sucesso" + +@happy_path +Cenário: Alterar o texto da questão 2 (tipo radio) com valor válido + Dado que a questão 2 é do tipo "radio" + Quando eu altero o texto da questão para "novo texto da questão 2" + E clico em salvar + Então devo ver a mensagem "template alterado com sucesso" + +@happy_path +Cenário: Alterar as Opções da questão 2 (tipo radio) com valor válido + Dado que a questão 2 é do tipo "radio" + Quando eu altero as opções da questão para "Opção 4, Opção 5, Opção 6" + E clico em salvar + Então devo ver a mensagem "template alterado com sucesso" + +@sad_path +Cenário: Alterar o texto da questão 1 (tipo texto) para valor nulo + Dado que a questão 1 é do tipo "texto" + Quando eu deixo o texto vazio + E clico em salvar + Então devo ver a mensagem "o texto da questão é obrigatório" + +@sad_path +Cenário: Alterar o texto da questão 2 (tipo radio) para valor nulo + Dado que a questão 2 é do tipo "radio" + Quando eu deixo o campo texto vazio + E clico em salvar + Então devo ver a mensagem "o texto da questão é obrigatório" + +@sad_path +Cenário: Alterar as Opções da questão 2 (tipo radio) para valor nulo + Dado que a questão 2 é do tipo "radio" + Quando eu deixo o campo Opções vazio + E clico em salvar + Então devo ver a mensagem "Todas as alternativas devem ser preenchidas" + +############################################################################# +# ALTERAÇÃO DO TEXTO DA QUESTÃO +############################################################################# + +@happy_path +Cenário: Alterar o texto da questão do tipo radio + Dado que a questão 2 é do tipo "radio" + Quando eu altero o texto da questão para "texto atualizado" + E clico em salvar + Então devo ver a mensagem "template alterado com sucesso" \ No newline at end of file diff --git a/src/features/form_template_creation.feature b/src/features/form_template_creation.feature new file mode 100644 index 0000000000..ed83c58f65 --- /dev/null +++ b/src/features/form_template_creation.feature @@ -0,0 +1,19 @@ +# language: pt +Funcionalidade: Criação de Template de Formulário + Como um Administrador + Eu quero criar um template definindo seu nome + Para que eu possa adicionar questões a ele posteriormente + + Cenário: Criar um novo template com sucesso + Dado que eu estou logado como administrador + E que eu estou na página de novo template + Quando eu preencho o campo do template "Nome" com "Avaliação de Disciplina" + E eu clico no botão do template "Salvar Template" + Então eu devo ser redirecionado para a página de edição do template "Avaliação de Disciplina" + E eu devo ver a mensagem do template "Template criado com sucesso" + + Cenário: Tentar criar um template com nome vazio + Dado que eu estou logado como administrador + E que eu estou na página de novo template + Quando eu clico no botão do template "Salvar Template" + Então eu devo ver a mensagem do template "Nome do Template não pode ficar em branco" diff --git a/src/features/gerar_relatorio.feature b/src/features/gerar_relatorio.feature new file mode 100644 index 0000000000..1b5d6ac346 --- /dev/null +++ b/src/features/gerar_relatorio.feature @@ -0,0 +1,29 @@ +# language: pt +# features/gerar_relatorio.feature + +Funcionalidade: Gerar Relatório de Respostas + Eu como Administrador + Quero baixar um arquivo csv contendo os resultados de um formulário + A fim de avaliar o desempenho das turmas + +Contexto: + Dado que eu sou um "admin" logado no sistema + E existe um formulário "Avaliação EngSoft 2025.1" + E o formulário "Avaliação EngSoft 2025.1" tem "15" respostas submetidas + +@happy_path +Cenário: Admin baixa o relatório de um formulário com respostas + Dado que eu estou na página de resultados do formulário "Avaliação EngSoft 2025.1" + Quando eu clico no botão "Exportar para CSV" + Então um download de um arquivo "relatorio_avaliacao_engsoft_2025_1.csv" deve ser iniciado + # Testar o conteúdo do CSV é muito complexo para BDD, + # então testamos apenas a ação de download. + +@sad_path +Cenário: Admin tenta baixar relatório de um formulário sem respostas + Dado que existe um formulário "Avaliação BD 2025.1" + E o formulário "Avaliação BD 2025.1" tem "0" respostas submetidas + E que eu estou na página de resultados do formulário "Avaliação BD 2025.1" + Quando eu clico no botão "Exportar" + Então eu devo ver a mensagem "Não é possível gerar um relatório, pois não há respostas." + E nenhum download deve ser iniciado \ No newline at end of file diff --git a/src/features/gerenciamento_departamento.feature b/src/features/gerenciamento_departamento.feature new file mode 100644 index 0000000000..5863a9a9fb --- /dev/null +++ b/src/features/gerenciamento_departamento.feature @@ -0,0 +1,30 @@ +# language: pt +# features/gerenciamento_por_departamento.feature + +Funcionalidade: Gerenciamento de Turmas por Departamento + Eu como Administrador (Coordenador de Departamento) + Quero gerenciar somente as turmas do departamento o qual eu pertenço + A fim de avaliar o desempenho das turmas no semestre atual + + Contexto: + Dado que eu sou um Administrador coordenador do departamento "Ciência da Computação" (CIC) + E que estou logado no sistema + E que existe a turma "Engenharia de Software" (CIC0105) pertencente ao departamento "CIC" + E que existe a turma "Cálculo 1" (MAT0025) pertencente ao departamento "Matemática" (MAT) + + @happy_path + Cenário: Coordenador visualiza turmas do seu próprio departamento + Quando eu acesso a lista de turmas para gerenciamento + Então eu devo ver a turma "Engenharia de Software" na lista + E eu devo ver a opção de "Gerenciar" para a turma "Engenharia de Software" + + @sad_path + Cenário: Coordenador não visualiza turmas de outros departamentos (Isolamento) + Quando eu acesso a lista de turmas para gerenciamento + Então eu NÃO devo ver a turma "Cálculo 1" na lista + + @sad_path + Cenário: Tentativa de acesso direto a turma de outro departamento (Segurança) + Quando eu tento acessar diretamente a URL de gerenciamento da turma "Cálculo 1" + Então eu devo ser redirecionado para a minha página inicial + E eu devo ver a mensagem de erro "Acesso negado: Você não tem permissão para gerenciar turmas de outro departamento." \ No newline at end of file diff --git a/src/features/importar_dados_sigaa.feature b/src/features/importar_dados_sigaa.feature new file mode 100644 index 0000000000..a00f4ed6e6 --- /dev/null +++ b/src/features/importar_dados_sigaa.feature @@ -0,0 +1,64 @@ +# language: pt +# features/importar_dados_sigaa.feature + +Funcionalidade: Importar novos dados do SIGAA + Eu como Administrador + Quero importar dados de turmas, matérias e participantes do SIGAA (caso não existam na base de dados atual) + A fim de alimentar a base de dados do sistema. + + Contexto: + Dado que eu estou logado como Administrador + E estou na página "Gerenciamento" + + @happy_path + Cenário: Importação inicial de turma e participante na turma com sucesso + Dado que o sistema não possui nenhuma turma cadastrada + E que o sistema não possui nenhum usuário cadastrado + E que o sigaa contém a turma "TA" da matéria "BANCOS DE DADOS" ("CIC0097") + E esta turma contém o participante "Fulano de Tal" ("150084006") + Quando eu solicito a importação clicando em "Importar dados" + Então a turma "TA" da matéria "BANCOS DE DADOS" ("CIC0097") deve ser cadastrada no sistema + E o usuário "Fulano de Tal" ("150084006") deve ser cadastrado no sistema + E o usuário "150084006" deve estar matriculado na turma "TA" da matéria "CIC0097" + E eu devo ver a mensagem de sucesso "Dados importados com sucesso!" + E os outro botões na página devem ser liberados + + @happy_path + Cenário: Importação de nova turma e participante já existente na turma com sucesso + Dado que o sistema possui o usuário "Ciclano de Tal" ("150084007") cadastrado + E que o sistema não possui a turma "ESTRUTURA DE DADOS" ("CIC0002") cadastrada + E que o sigaa contém a turma "TA" da matéria "ESTRUTURA DE DADOS" ("CIC0002") + E esta turma contém o participante "Ciclano de Tal" ("150084007") + Quando eu solicito a importação clicando em "Importar dados" + Então a turma "TA" da matéria "ESTRUTURA DE DADOS" ("CIC0002") deve ser cadastrada no sistema + E o usuário "150084007" deve estar matriculado na turma "TA" da matéria "CIC0002" + E eu devo ver a mensagem de sucesso "Dados importados com sucesso!" + + @happy_path + Cenário: Importação de turma já existente e novo participante na turma com sucesso + Dado que o sistema possui a turma "TA" da matéria "ALGORITMOS E PROGRAMAÇÃO" ("CIC0001") cadastrada + E que o sistema não possui o usuário "Beltrano de Tal" ("150084008") cadastrado + E que o sigaa contém a turma "TA" da matéria "ALGORITMOS E PROGRAMAÇÃO" ("CIC0001") + E esta turma contém o participante "Beltrano de Tal" ("150084008") + Quando eu solicito a importação clicando em "Importar dados" + Então o usuário "Beltrano de Tal" ("150084008") deve ser cadastrado no sistema + E o usuário "150084008" deve estar matriculado na turma "TA" da matéria "CIC0001" + E eu devo ver a mensagem de sucesso "Dados importados com sucesso!" + + @happy_path + Cenário: Importação sem duplicação de um usuário já existente + Dado que o sistema possui o usuário "Fulano de Tal" ("150084006") cadastrado + E que o sigaa contém a turma "TA" da matéria "REDES DE COMPUTADORES" ("CIC0003") + E esta turma contém o participante "Fulano de Tal" ("150084006") + Quando eu solicito a importação clicando em "Importar dados" + Então o usuário "Fulano de Tal" ("150084006") não deve ser duplicado no sistema + E o usuário "150084006" deve estar matriculado na turma "TA" da matéria "CIC0003" + E eu devo ver a mensagem de sucesso "Dados importados com sucesso!" + + @sad_path + Cenário: Falha ao buscar os dados externos + Dado que o sigaa está indisponível + Quando eu solicito a importação clicando em "Importar dados" + Então eu devo ver a mensagem de erro "Não foi possível buscar os dados. Tente novamente mais tarde." + E nenhuma nova turma deve ser cadastrada no sistema + E nenhum novo usuário deve ser cadastrado no sistema \ No newline at end of file diff --git a/src/features/painel_pendencias.feature b/src/features/painel_pendencias.feature new file mode 100644 index 0000000000..11c3328020 --- /dev/null +++ b/src/features/painel_pendencias.feature @@ -0,0 +1,25 @@ +# language: pt +Funcionalidade: Painel de Avaliações Pendentes + Como Participante (Aluno ou Docente) + Eu quero ver uma lista de avaliações pendentes + Para que eu possa saber quais formulários preciso responder + + Contexto: + Dado que eu sou um aluno matriculado na turma "Engenharia de Software" + E que o administrador distribuiu o template "Avaliação Semestral" para a turma "Engenharia de Software" + E que eu ainda não respondi a este formulário + + @happy_path + Cenário: Aluno visualiza avaliação pendente no dashboard + Dado que eu estou logado como aluno + Quando eu acesso o meu painel de avaliações + Então eu devo ver "Avaliação Semestral" na lista de pendências + E o item deve indicar a turma "Engenharia de Software" + E eu devo ver um link para "Responder" + + @happy_path + Cenário: Aluno não vê avaliações já respondidas + Dado que eu estou logado como aluno + E que eu já respondi a avaliação "Avaliação Semestral" da turma "Engenharia de Software" + Quando eu acesso o meu painel de avaliações + Então eu não devo ver "Avaliação Semestral" na lista de pendências diff --git a/src/features/redefinir_senha_usuario.feature b/src/features/redefinir_senha_usuario.feature new file mode 100644 index 0000000000..f46c595c43 --- /dev/null +++ b/src/features/redefinir_senha_usuario.feature @@ -0,0 +1,54 @@ +# language: pt +# features/redefinir_senha_usuario.feature + +Funcionalidade: Redefinição de Senha + Eu como Usuário + Quero redefinir uma senha para o meu usuário a partir do e-mail recebido após a solicitação da troca de senha + A fim de recuperar o meu acesso ao sistema + + @happy_path + Cenário: Solicitar link de redefinição com sucesso + Dado que o usuário "fulano.ativo@email.com" está cadastrado e ativo no sistema + E eu estou na página de "Login" + E eu preencho o campo "Email" com "fulano.ativo@email.com" + Quando eu clico em "Esqueci minha senha" + Então eu devo ver a mensagem "Se este e-mail estiver cadastrado, um link de redefinição foi enviado." + E um e-mail de "Redefinição de Senha" deve ser enviado para "fulano.ativo@email.com" + + @sad_path + Cenário: Solicitar link de redefinição com e-mail em branco + Dado que eu estou na página de "Login" + E eu deixo o campo "Email" em branco + Quando eu clico em "Esqueci minha senha" + Então eu devo permanecer na página de "Login" + E eu devo ver a mensagem de erro "O campo de e-mail não pode estar vazio." + E nenhum e-mail deve ser enviado + + @sad_path + Cenário: Solicitar redefinição de senha com e-mail não cadastrado + Dado que o e-mail "fulano.invalido@email.com" não está cadastrado no sistema + E eu estou na página de "Login" + E eu preencho o campo "Email" com "fulano.invalido@email.com" + Quando eu clico em "Esqueci minha senha" + Então eu devo ver a mensagem "Se este e-mail estiver cadastrado, um link de redefinição foi enviado." + E nenhum e-mail deve ser enviado + + @happy_path + Cenário: Usar o link de redefinição para cadastrar nova senha + Dado que o usuário "fulano.ativo@email.com" solicitou um link de redefinição válido + Quando eu acesso a página "Redefina sua Senha" usando o link válido + E eu preencho o campo "Nova Senha" com "novaSenhaSuperForte" + E eu preencho o campo "Confirme a senha" com "novaSenhaSuperForte" + E eu clico no botão "Salvar Nova Senha" + Então eu devo ser redirecionado para a página de "Login" + E eu devo ver a mensagem "Senha redefinida com sucesso! Você já pode fazer o login." + E o usuário "fulano.ativo@email.com" deve conseguir logar com a senha "novaSenhaSuperForte" + + @sad_path + Cenário: Solicitar redefinição de senha com usuário com status "pendente" + Dado que o usuário "fulano.pendente@email.com" está cadastrado no sistema com o status "pendente" + E eu estou na página de "Login" + E eu preencho o campo "Email" com "fulano.pendente@email.com" + Quando eu clico em "Esqueci minha senha" + Então eu devo ver a mensagem "Você ainda não definiu sua senha. Por favor, verifique seu e-mail para definir sua senha." + E nenhum e-mail deve ser enviado \ No newline at end of file diff --git a/src/features/responder_formulario.feature b/src/features/responder_formulario.feature new file mode 100644 index 0000000000..5a8caaabfe --- /dev/null +++ b/src/features/responder_formulario.feature @@ -0,0 +1,44 @@ +# language: pt +# features/responder_formulario.feature + +Funcionalidade: Responder Formulário de Avaliação + Eu como Participante de uma turma + Quero responder o questionário sobre a turma em que estou matriculado + A fim de submeter minha avaliação da turma + +Contexto: + Dado que eu sou um "participante" logado como "aluno.joao" + E eu estou matriculado na turma "Banco de Dados - TB" + E existe um formulário "Avaliação BD 2025.1" para a turma "Banco de Dados - TB" + E o formulário "Avaliação BD 2025.1" tem a pergunta "O professor domina o conteúdo?" do tipo "numérica (1-5)" + +@happy_path +Cenário: Participante responde um formulário pendente + Dado que eu não respondi o formulário "Avaliação BD 2025.1" ainda + E eu estou na minha página inicial (dashboard) + Quando eu vejo "Avaliação BD 2025.1" na minha lista de "Formulários Pendentes" + E eu clico em "Responder" + Então eu sou redirecionado para a página do formulário + Quando eu seleciono "5" para a pergunta "O professor domina o conteúdo?" + E eu clico no botão "Submeter Respostas" + Então eu devo ser redirecionado para a minha página inicial + E eu devo ver a mensagem "Avaliação enviada com sucesso. Obrigado!" + E "Avaliação BD 2025.1" deve aparecer na minha lista de "Formulários Respondidos" + +@sad_path +Cenário: Participante tenta responder um formulário que já respondeu + Dado que eu já respondi o formulário "Avaliação BD 2025.1" + E eu estou na minha página inicial (dashboard) + Quando eu vejo "Avaliação BD 2025.1" na minha lista de "Formulários Respondidos" + E eu tento acessar a página do formulário "Avaliação BD 2025.1" diretamente + Então eu devo ser redirecionado para a minha página inicial + E eu devo ver a mensagem "Você já respondeu este formulário." + +@sad_path +Cenário: Participante tenta responder um formulário após a data de encerramento + Dado que o formulário "Avaliação BD 2025.1" expirou em "01/12/2025" + E eu não respondi o formulário "Avaliação BD 2025.1" ainda + E eu estou na minha página inicial (dashboard) + Quando eu tento acessar a página do formulário "Avaliação BD 2025.1" + Então eu devo ser redirecionado para a minha página inicial + E eu devo ver a mensagem "Este formulário não está mais aceitando respostas." \ No newline at end of file diff --git a/src/features/step_definitions/.keep b/src/features/step_definitions/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/features/step_definitions/autenticacao_steps.rb b/src/features/step_definitions/autenticacao_steps.rb new file mode 100644 index 0000000000..b0287204ee --- /dev/null +++ b/src/features/step_definitions/autenticacao_steps.rb @@ -0,0 +1,226 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Navega para a página de login. +# +# Argumentos: +# - Nenhum +# +# Efeitos Colaterais: +# - Altera a página atual para /login. +Dado('que eu estou na página de login') do + visit "/login" +end + +# Navega para a página de login (redundante). +# +# Argumentos: +# - Nenhum +# +# Efeitos Colaterais: +# - Altera a página atual para /login. +Dado('eu estou na página de login') do + visit "/login" +end + +# Cria um usuário com os dados especificados. +# +# Argumentos: +# - nome (String): Nome do usuário. +# - email (String): Email do usuário. +# - matricula (String): Matrícula do usuário. +# - senha (String): Senha do usuário. +# +# Efeitos Colaterais: +# - Cria um registro na tabela usuarios. +Dado('existe um usuário {string} cadastrado com email {string}, matrícula {string} e senha {string}') do |nome, email, matricula, senha| + Usuario.create!( + nome: nome, + email: email, + usuario: email, + matricula: matricula, + password: senha, + password_confirmation: senha, + ocupacao: 'discente', + status: true + ) +end + +# Cria um usuário administrador com os dados especificados. +# +# Argumentos: +# - nome (String): Nome do usuário. +# - email (String): Email do usuário. +# - matricula (String): Matrícula do usuário. +# - senha (String): Senha do usuário. +# +# Efeitos Colaterais: +# - Cria um registro na tabela usuarios com ocupacao 'admin'. +Dado('existe um usuário {string} cadastrado com email {string}, matrícula {string}, senha {string} e com permissão de administrador') do |nome, email, matricula, senha| + Usuario.create!( + nome: nome, + email: email, + usuario: email, + matricula: matricula, + password: senha, + password_confirmation: senha, + ocupacao: 'admin', + status: true + ) +end + +# Cria um usuário pré-cadastrado via SIGAA com status pendente. +# +# Argumentos: +# - nome (String): Nome do usuário. +# - matricula (String): Matrícula do usuário. +# - status_desc (String): Descrição do status (não utilizado diretamente na lógica, fixado como false). +# +# Efeitos Colaterais: +# - Cria um registro na tabela usuarios com status false. +Dado('que existe um usuário {string} \({string}) pré-cadastrado via SIGAA, mas com status {string}') do |nome, matricula, status_desc| + Usuario.create!( + nome: nome, + matricula: matricula, + usuario: matricula, + email: "#{matricula}@aluno.unb.br", + password: "SenhaTemporaria123!", + ocupacao: :discente, + status: false + ) +end + +# Define o papel do usuário (não implementado). +# +# Argumentos: +# - role (String): Papel do usuário. +Dado('que eu sou um {string} não autenticado') do |role| + pending "Authentication logic for unauthenticated #{role} not implemented" +end + +# ========================================= +# Ações (Quando) +# ========================================= + +# Preenche um campo do formulário. +# +# Argumentos: +# - campo (String): Nome do campo (Label). +# - valor (String): Valor a ser preenchido. +# +# Efeitos Colaterais: +# - Altera o valor de um input na página. +Quando('eu preencho o campo {string} com {string}') do |campo, valor| + if campo == 'Email' + fill_in 'Usuário', with: valor + else + fill_in campo, with: valor + end +end + +# Step pendente para preenchimento genérico. +# +# Argumentos: +# - string (String): Primeiro argumento. +# - string2 (String): Segundo argumento. +Quando('eu preencho {string} com {string}') do |string, string2| + pending +end + +# Tenta acessar funcionalidade de criação (pendente). +# +# Argumentos: +# - button_text (String): Texto do botão. +Quando('eu tento acessar a funcionalidade de criação \(clicar no botão {string})') do |button_text| + pending "Access control testing logic not implemented" +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica redirecionamento para página inicial. +# +# Argumentos: +# - Nenhum +# +# Retorno: +# - (Boolean): Resultado da asserção de caminho. +Então('eu devo ser redirecionado para a página inicial') do + expect(page).to have_current_path("/") +end + +# Verifica redirecionamento para página de administrador. +# +# Argumentos: +# - Nenhum +# +# Retorno: +# - (Boolean): Resultado da asserção de caminho. +Então('eu devo ser redirecionado para a página de administrador') do + expect(page.current_path).to eq(admin_gerenciamento_path) +end + +# Verifica presença de mensagem na tela. +# +# Argumentos: +# - mensagem (String): Mensagem esperada. +# +# Retorno: +# - (Boolean): Resultado da asserção de conteúdo. +Então('eu devo ver a mensagem de Login {string}') do |mensagem| + texto = mensagem.sub(/\.$/, '') + expect(page).to have_content(texto) +end + +# Verifica ausência de opção no menu lateral. +# +# Argumentos: +# - opcao (String): Texto da opção. +# +# Retorno: +# - (Boolean): Resultado da asserção de ausência de conteúdo. +Então('eu NÃO devo ver a opção {string} no menu lateral') do |opcao| + within('aside') do + expect(page).not_to have_content(opcao) + end +end + +# Verifica presença de opção no menu lateral. +# +# Argumentos: +# - texto (String): Texto da opção. +# +# Retorno: +# - (Boolean): Resultado da asserção de conteúdo. +Então('eu devo ver a opção {string} no menu lateral') do |texto| + expect(page).to have_content(texto) +end + +# Verifica permanência na página de login. +# +# Argumentos: +# - Nenhum +# +# Retorno: +# - (Boolean): Resultado da asserção de caminho. +Então('eu devo permanecer na página de login') do + expect(page).to have_current_path("/login") +end + +# Verifica o status de um usuário no banco de dados. +# +# Argumentos: +# - email_ou_matricula (String): Identificador do usuário. +# - status (Boolean): Status esperado (true/false). +# +# Efeitos Colaterais: +# - Realiza consulta ao banco de dados. +# +# Retorno: +# - (Boolean): Resultado da asserção de igualdade. +Então('o status do usuário {string} deve continuar {string}') do |email_ou_matricula, status| + usuario = Usuario.find_by(email: email_ou_matricula) || Usuario.find_by(matricula: email_ou_matricula) + expect(usuario.status).to eq(status) +end \ No newline at end of file diff --git a/src/features/step_definitions/cadastrar_usuarios_steps.rb b/src/features/step_definitions/cadastrar_usuarios_steps.rb new file mode 100644 index 0000000000..851e84c10b --- /dev/null +++ b/src/features/step_definitions/cadastrar_usuarios_steps.rb @@ -0,0 +1,138 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Cria configuração de usuário no mock do SIGAA. +# +# Argumentos: +# - nome (String): Nome do usuário. +# - matricula (String): Matrícula. +# - email (String): Email. +# +# Efeitos Colaterais: +# - Modifica estado do mock do SIGAA. +Dado('que o sigaa contém o usuário {string} \({string}) com e-mail {string}') do |nome, matricula, email| + turma_mock = setup_sigaa_context + upsert_sigaa_student(turma_mock, nome, matricula, email) +end + +# Cria configuração de usuário no mock do SIGAA com email temporário. +# +# Argumentos: +# - nome (String): Nome. +# - matricula (String): Matrícula. +# +# Efeitos Colaterais: +# - Modifica estado do mock do SIGAA. +Dado('que o sigaa contém o usuário {string} \({string})') do |nome, matricula| + turma_mock = setup_sigaa_context + email_temp = "#{matricula}@temp.com" + upsert_sigaa_student(turma_mock, nome, matricula, email_temp) +end + +# Cria usuário diretamente no sistema (ativo). +# +# Argumentos: +# - nome (String): Nome. +# - matricula (String): Matrícula. +# +# Efeitos Colaterais: +# - Cria registro de Usuario. +Dado('que o sistema possui o usuário {string} \({string}) cadastrado \(seja pendente ou ativo)') do |nome, matricula| + Usuario.create!( + nome: nome, + matricula: matricula, + usuario: matricula, + email: "#{matricula}@sistema.com", + password: "password123", + ocupacao: :discente, + status: true + ) +end + +# Remove email de usuário no mock do SIGAA. +# +# Argumentos: +# - matricula (String): Matrícula. +# +# Efeitos Colaterais: +# - Modifica estado do mock. +Dado('o usuário {string} não possui um endereço de e-mail') do |matricula| + @fake_members.each do |turma| + if turma["dicente"] + aluno = turma["dicente"].find { |a| a["matricula"] == matricula } + aluno["email"] = nil if aluno + end + end +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica criação de usuário e status. +# +# Argumentos: +# - nome (String): Nome esperado. +# - matricula (String): Matrícula. +# - status_texto (String): "ativo" ou outro. +# +# Retorno: +# - (Boolean): Resultado da asserção. +Então('o usuário {string} \({string}) deve ser criado no sistema com o status {string}') do |nome, matricula, status_texto| + status_booleano = (status_texto == "ativo") + verify_user_creation_data(matricula, nome, status_booleano) +end + +# Verifica que email específico NÃO foi enviado. +# +# Argumentos: +# - assunto (String): Trecho do assunto. +# - email_destinatario (String): Email destinatário. +# +# Retorno: +# - (Boolean): Asserção de não existência. +Então('nenhum novo e-mail de {string} deve ser enviado para {string}') do |assunto, email_destinatario| + emails_enviados = ActionMailer::Base.deliveries + + email_especifico = emails_enviados.find do |email| + email.to.include?(email_destinatario) && email.subject.to_s.include?(assunto) + end + + expect(email_especifico).to be_nil +end + +# Verifica que usuário não existe no banco. +# +# Argumentos: +# - matricula (String): Matrícula. +# +# Retorno: +# - (Boolean): Asserção de nil. +Então('o usuário {string} não deve ser criado no sistema') do |matricula| + expect(Usuario.find_by(matricula: matricula)).to be_nil +end + +# Verifica mensagem de erro na página. +# +# Argumentos: +# - mensagem_erro (String): Texto da mensagem. +Então('eu devo ver uma mensagem de erro {string}') do |mensagem_erro| + expect(page).to have_content(mensagem_erro) +end + +# Verifica envio de email. +# +# Argumentos: +# - assunto (String): Assunto. +# - destinatario (String): Email. +# +# Retorno: +# - (Boolean): Asserção de presença. +Então('um e-mail de {string} deve ser enviado para {string}') do |assunto, destinatario| + email = ActionMailer::Base.deliveries.find do |e| + e.to.include?(destinatario) && e.subject.to_s.include?(assunto) + end + + expect(email).to be_present +end \ No newline at end of file diff --git a/src/features/step_definitions/common_steps.rb b/src/features/step_definitions/common_steps.rb new file mode 100644 index 0000000000..d563c003d1 --- /dev/null +++ b/src/features/step_definitions/common_steps.rb @@ -0,0 +1,210 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Loga como administrador, criando-o se necessário. +# +# Argumentos: +# - Nenhum +# +# Efeitos Colaterais: +# - Cria usuário admin. +# - Realiza login na interface. +Dado('que eu estou logado como Administrador') do + @admin = Usuario.find_by(usuario: 'admin') || Usuario.create!( + nome: 'Admin', + email: 'admin@test.com', + matricula: '123456', + usuario: '123456', + password: 'senha123', + ocupacao: :admin, + status: true + ) + visit "/login" + + fill_in "Usuário", with: @admin.email + fill_in "Senha", with: "senha123" + + click_on "Entrar" + + expect(page).to have_content("Bem-vindo") +end + +# Navega para uma página específica. +# +# Argumentos: +# - page_name (String): Nome da página. +# +# Efeitos Colaterais: +# - Altera o caminho atual. +Dado(/^(?:que )?(?:eu )?estou na página(?: de)? "([^"]*)"$/) do |page_name| + visit path_to(page_name) +end + +# Loga como um usuário de papel específico. +# +# Argumentos: +# - role (String): Papel do usuário (e.g. 'docente'). +# +# Efeitos Colaterais: +# - Cria ou encontra usuário. +# - Realiza login. +Dado('que eu sou um {string} logado no sistema') do |role| + ocupacao = resolve_occupation_from_role(role) + @user = find_or_create_auth_user(role, ocupacao) + perform_ui_login(@user.email, 'password') + verify_login_success +end + +# ========================================= +# Ações (Quando) +# ========================================= + +# Acessa uma página específica. +# +# Argumentos: +# - page_name (String): Nome da página. +Quando('eu acesso a página {string}') do |page_name| + visit path_to(page_name) +end + +# Clica em um botão, normalizando texto para 'Exportar para CSV' se necessário. +# +# Argumentos: +# - texto (String): Texto do botão. +Quando('eu clico no botão {string}') do |texto| + case texto + when "Exportar", "Baixar CSV" + texto = "Exportar para CSV" + end + click_on texto +end + +# Clica em um link ou botão. +# +# Argumentos: +# - link_or_button (String): Texto do link ou botão. +Quando('eu clico em {string}') do |link_or_button| + click_on link_or_button +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica se permanece na página esperada. +# +# Argumentos: +# - page_name (String): Nome da página. +# +# Retorno: +# - (Boolean): Asserção de caminho. +Então('eu devo permanecer na página {string}') do |page_name| + caminho_esperado = path_to(page_name) + expect(page.current_path).to eq(caminho_esperado) +end + +# Verifica mensagem de erro na tela. +# +# Argumentos: +# - mensagem (String): Mensagem esperada. +Então('eu devo ver a mensagem de erro {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +# Verifica mensagem genérica na tela. +# +# Argumentos: +# - mensagem (String): Mensagem esperada. +Então('eu devo ver a mensagem {string}') do |mensagem| + texto = mensagem.sub(/\.$/, '') + expect(page).to have_content(texto) +end + +# Verifica redirecionamento para root. +# +# Argumentos: +# - Nenhum +Então('eu devo ser redirecionado para a minha página inicial') do + expect(current_path).to eq(root_path) +end + +# ========================================= +# Métodos Auxiliares (Helpers) +# ========================================= + +# Resolve o caminho para uma página dado seu nome. +# +# Argumentos: +# - page_name (String): Nome amigável da página. +# +# Retorno: +# - (String): Caminho URL. +def path_to(page_name) + path = resolve_static_path(page_name.downcase) + return path if path + + path = resolve_dynamic_path(page_name) + return path if path + + raise "Não sei o caminho para a página '#{page_name}'. Adicione no step definition." +end + +# Resolve caminhos estáticos. +# +# Argumentos: +# - page_name (String): Nome normalizado. +# +# Retorno: +# - (String/Nil): URL ou nil se não encontrado. +def resolve_static_path(page_name) + case page_name + when "gerenciamento" then admin_gerenciamento_path + when "gerenciamento de templates" then templates_path + when "templates" then templates_path + when "templates/new" then new_template_path + when "formularios/new" then new_formulario_path + when "formularios/pendentes" then pendentes_formularios_path + when "home", "inicial", "dashboard" then root_path + when "formularios" then formularios_path + when "defina sua senha" then "/definir_senha" + when "login" then login_path + else nil + end +end + +# Resolve caminhos dinâmicos (e.g. formulários específicos). +# +# Argumentos: +# - page_name (String): Nome da página contendo ID/Título. +# +# Retorno: +# - (String/Nil): URL ou nil. +def resolve_dynamic_path(page_name) + if page_name =~ /^formularios\/(.+)$/ + titulo = $1.strip + return resolve_formulario_result_path(titulo) + end + + nil +end + +# Encontra caminho de resultado de formulário pelo título. +# +# Argumentos: +# - titulo (String): Título do formulário envio. +# +# Retorno: +# - (String): URL de resultados. +def resolve_formulario_result_path(titulo) + form = Formulario.find_by(titulo_envio: titulo) + + form ||= Formulario.where("lower(titulo_envio) = ?", titulo.downcase).first + + if form + resultado_path(form.id) + else + # Retorna caminho inválido para fins de teste/debug (comportamento original) + "/resultados/99999" + end +end \ No newline at end of file diff --git a/src/features/step_definitions/criar_formulario_usuario_steps.rb b/src/features/step_definitions/criar_formulario_usuario_steps.rb new file mode 100644 index 0000000000..18bc75a3c2 --- /dev/null +++ b/src/features/step_definitions/criar_formulario_usuario_steps.rb @@ -0,0 +1,40 @@ +# Step pendente para autenticação. +# +# Argumentos: +# - perfil (String): Perfil de usuário. +Dado('que eu estou autenticado no sistema como {string}') do |perfil| + pending # Write code here that turns the phrase above into concrete actions +end + +# Step pendente existência de template. +# +# Argumentos: +# - template (String): Nome do template. +Dado('existe o template {string}') do |template| + pending # Write code here that turns the phrase above into concrete actions +end + +# Step pendente escolha de criação. +# +# Argumentos: +# - op1 (String): Opção 1. +# - op2 (String): Opção 2. +Dado('eu posso escolher criar um formulário para {string} ou para {string}') do |op1, op2| + pending # Write code here that turns the phrase above into concrete actions +end + +# Step pendente seleção de tipo. +# +# Argumentos: +# - tipo (String): Tipo de formulário. +Dado('que eu seleciono o tipo de formulário {string}') do |tipo| + pending # Write code here that turns the phrase above into concrete actions +end + +# Step pendente seleção de turma. +# +# Argumentos: +# - turma (String): Nome da turma. +Dado('eu seleciono a turma {string}') do |turma| + pending # Write code here that turns the phrase above into concrete actions +end \ No newline at end of file diff --git a/src/features/step_definitions/definir_senha_usuario_steps.rb b/src/features/step_definitions/definir_senha_usuario_steps.rb new file mode 100644 index 0000000000..ea6c23b87f --- /dev/null +++ b/src/features/step_definitions/definir_senha_usuario_steps.rb @@ -0,0 +1,136 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Prepara um usuário importado com estado específico. +# +# Argumentos: +# - email (String): Email do usuário. +# - status_desc (String): "ativo" ou outro valor. +# +# Efeitos Colaterais: +# - Cria registro de Usuario. +Dado('que o usuário {string} foi importado e está com o status {string}') do |email, status_desc| + is_ativo = (status_desc.downcase == 'ativo') + + @user = Usuario.create!( + nome: "Usuário Importado", + email: email, + usuario: email.split('@').first, + matricula: "2024#{rand(1000..9999)}", + ocupacao: :discente, + status: is_ativo, + password: "SenhaTemporaria123", + password_confirmation: "SenhaTemporaria123" + ) +end + +# Prepara um usuário ativo. +# +# Argumentos: +# - email (String): Email. +# +# Efeitos Colaterais: +# - Cria registro de Usuario. +Dado('que o usuário {string} já está ativo no sistema') do |email| + @user = Usuario.create!( + nome: "Usuário Já Ativo", + email: email, + usuario: email.split('@').first, + matricula: "2023#{rand(1000..9999)}", + ocupacao: :docente, + status: true, + password: "SenhaDefinida123", + password_confirmation: "SenhaDefinida123" + ) +end + +# Gera link de definição de senha. +# +# Argumentos: +# - email (String): Email. +# +# Efeitos Colaterais: +# - Gera token e define @link_definicao. +Dado('um link de definição de senha válido foi enviado para {string}') do |email| + user = Usuario.find_by!(email: email) + token = user.signed_id(purpose: :definir_senha, expires_in: 24.hours) + @link_definicao = "/definir_senha?token=#{token}" +end + +# ========================================= +# Ações (Quando) +# ========================================= + +# Acessa página via link válido armazenado. +# +# Argumentos: +# - page_name (String): Nome da página (não usado na lógica). +# +# Efeitos Colaterais: +# - Visita URL. +Quando('eu acesso a página {string} usando o link válido') do |page_name| + visit @link_definicao +end + +# Acessa página usando link antigo (sem expiração explícita no step, mas gera novo token). +# +# Argumentos: +# - page_name (String): Nome da página. +# +# Efeitos Colaterais: +# - Visita URL. +Quando('eu acesso a página {string} usando o link antigo') do |page_name| + user = @user + token = user.signed_id(purpose: :definir_senha) + visit "/definir_senha?token=#{token}" +end + +# Deixa campo em branco. +# +# Argumentos: +# - campo (String): Nome do campo. +# +# Efeitos Colaterais: +# - Preenche input com string vazia. +Quando('eu deixo o campo {string} em branco') do |campo| + if campo == 'Email' + fill_in 'Usuário', with: "" + else + fill_in campo, with: "" + end +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica redirecionamento. +# +# Argumentos: +# - page_name (String): Nome da página destino. +# +# Retorno: +# - (Boolean): Asserção de caminho. +Então('eu devo ser redirecionado para a página de {string}') do |page_name| + expect(page).to have_current_path(path_to(page_name)) +end + +# Verifica status do usuário no banco. +# +# Argumentos: +# - email (String): Email. +# - status_esperado (String): "ativo" ou outro. +# +# Retorno: +# - (Boolean): Asserção de status. +Então('o status do usuário {string} no sistema deve ser {string}') do |email, status_esperado| + user = Usuario.find_by(email: email) + user.reload + + if status_esperado == "ativo" + expect(user.status).to be_truthy + else + expect(user.status).to be_falsey + end +end \ No newline at end of file diff --git a/src/features/step_definitions/distribuicao_avaliacoes_steps.rb b/src/features/step_definitions/distribuicao_avaliacoes_steps.rb new file mode 100644 index 0000000000..b187c25727 --- /dev/null +++ b/src/features/step_definitions/distribuicao_avaliacoes_steps.rb @@ -0,0 +1,148 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Cria um template de avaliação para distribuição. +# +# Argumentos: +# - nome_template (String): Nome do template. +# +# Efeitos Colaterais: +# - Cria Template e Usuario (se necessário). +Dado('que existe um template de avaliação {string}') do |nome_template| + admin = Usuario.find_by(ocupacao: :admin) || Usuario.create!( + nome: 'Admin', email: 'admin@test.com', matricula: '000', + usuario: 'admin', password: 'password', ocupacao: :admin, status: true + ) + + @template = Template.find_or_create_by!(name: nome_template) do |t| + t.titulo = nome_template + t.id_criador = admin.id + t.participantes = 'todos' + t.hidden = false + end +end + +# Cria turma com alunos pré-matriculados. +# +# Argumentos: +# - nome_turma (String): Nome da turma (matéria). +# - num_alunos (Integer): Quantidade de alunos. +# +# Efeitos Colaterais: +# - Setup completo de estrutura acadêmica e matrículas. +Dado('que existe a turma {string} com {int} alunos matriculados') do |nome_turma, num_alunos| + turma = setup_academic_structure(nome_turma) + enroll_batch_students(turma, num_alunos) +end + +# Acessa página de distribuição. +# +# Efeitos Colaterais: +# - Visita /formularios. +Dado('que eu estou na página de distribuição de formulários') do + visit formularios_path +end + +# ========================================= +# Ações (Quando) +# ========================================= + +# Seleciona template no select. +# +# Argumentos: +# - nome_template (String): Nome do template. +Quando('eu seleciono o template de avaliação {string}') do |nome_template| + select nome_template, from: 'template_id' +end + +# Seleciona múltiplas turmas para distribuição. +# +# Argumentos: +# - turma1 (String): Nome da primeira turma. +# - turma2 (String): Nome da segunda turma. +# +# Efeitos Colaterais: +# - Marca checkboxes de turmas. +Quando('eu seleciono as turmas para distribuição {string} e {string}') do |turma1, turma2| + materia1 = Materia.find_by(nome: turma1) + t1 = Turma.where(materia: materia1).first + + materia2 = Materia.find_by(nome: turma2) + t2 = Turma.where(materia: materia2).first + + check "turma_#{t1.id}" + check "turma_#{t2.id}" +end + +# Clica em botão de distribuição. +# +# Argumentos: +# - nome_botao (String): Nome do botão. +Quando('eu clico no botão de distribuição {string}') do |nome_botao| + click_button nome_botao +end + +# Realiza fluxo completo de distribuição (seleção + clique). +# +# Argumentos: +# - template (String): Nome do template. +# +# Efeitos Colaterais: +# - Submete formulário. +Quando('eu seleciono o template {string} e clico em Distribuir') do |template| + select template, from: 'template_id' + click_button 'Distribuir Formulário' +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica mensagem de sucesso. +# +# Argumentos: +# - mensagem (String): Mensagem esperada. +Então('eu devo ver a mensagem de sucesso de distribuição {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +# Verifica mensagem de erro. +# +# Argumentos: +# - mensagem (String): Mensagem esperada. +Então('eu devo ver a mensagem de erro de distribuição {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +# Verifica associação criada no banco entre Turma e Template (via Formulario). +# +# Argumentos: +# - nome_turma (String): Nome da turma. +# - nome_template (String): Nome do template. +# +# Retorno: +# - (Boolean): Asserção de existência. +Então('a turma {string} deve ter um formulário associado ao template {string}') do |nome_turma, nome_template| + materia = Materia.find_by(nome: nome_turma) + turma = Turma.where(materia: materia).first + template = Template.find_by(name: nome_template) + + expect(turma.formularios.where(template: template)).to exist +end + +# Verifica criação de respostas pendentes para alunos. +# +# Argumentos: +# - num_alunos (Integer): Quantidade mínima esperada. +# - nome_turma (String): Nome da turma. +# +# Retorno: +# - (Boolean): Asserção de contagem de Resposta. +Então('todos os {int} alunos da turma {string} devem ter uma resposta pendente para este formulário') do |num_alunos, nome_turma| + materia = Materia.find_by(nome: nome_turma) + turma = Turma.where(materia: materia).first + + form = turma.formularios.last + expect(Resposta.where(formulario: form, data_submissao: nil).count).to be >= num_alunos +end \ No newline at end of file diff --git a/src/features/step_definitions/editar_templates_steps.rb b/src/features/step_definitions/editar_templates_steps.rb new file mode 100644 index 0000000000..ccf5e778a0 --- /dev/null +++ b/src/features/step_definitions/editar_templates_steps.rb @@ -0,0 +1,303 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Cria um template com nome e semestre e acessa sua página de edição. +# +# Argumentos: +# - nome (String): Parte do título. +# - semestre (String): Semestre. +# +# Efeitos Colaterais: +# - Persiste Template e Usuario. +# - Visita edit path. +Dado('seleciono o template com o campo nome {string} e o campo semestre {string}') do |nome, semestre| + @template = find_or_create_template_for_edit(nome, semestre) + visit edit_template_path(@template) +end + +# Popula template com questões definidas em tabela. +# +# Argumentos: +# - table (Cucumber::Table): Tabela de questões. +# +# Efeitos Colaterais: +# - Apaga questões anteriores, cria novas e visita edição. +Dado('o template contém duas questões, sendo:') do |table| + reset_template_questions + create_questions_from_table(table) + visit edit_template_path(@template) +end + +# Verifica acesso manual a página do template. +# +# Efeitos Colaterais: +# - Asserção de URL. +Dado('visualizo a página do template escolhido') do + expect(current_path).to eq(edit_template_path(@template)) +end + +# Configura questão específica com tipo e opções. +# +# Argumentos: +# - num (Integer): Número da questão. +# - tipo (String): Tipo. +# - opcoes (String): Opções. +Dado('que a questão {int} é do tipo {string} com opções {string}') do |num, tipo, opcoes| + update_question_attributes(num, tipo, opcoes) + visit edit_template_path(@template) +end + +# Configura questão específica apenas com tipo. +# +# Argumentos: +# - num (Integer): Número. +# - tipo (String): Tipo. +Dado('que a questão {int} é do tipo {string}') do |num, tipo| + update_question_attributes(num, tipo, nil) + visit edit_template_path(@template) +end + +# ========================================= +# Ações (Quando) +# ========================================= + +# Remove questão específica. +# +# Argumentos: +# - num (Integer): Índice da questão. +Quando('eu clico no botão de exclusão ao lado da questão {int}') do |num| + click_delete_question_button(num) +end + +# Remove nova questão (redundante, mas semântico). +# +# Argumentos: +# - num (Integer): Índice. +Quando('clico no botão de exclusão ao lado da \(nova) questão {int}') do |num| + click_delete_question_button(num) +end + +# Salva questão atual. +# +# Efeitos Colaterais: +# - Clica em botão de salvar no formulário ativo. +Quando('clico em salvar') do + save_current_question_form +end + +# Altera tipo da questão. +# +# Argumentos: +# - num (Integer): Número da questão. +# - novo_tipo (String): Novo tipo. +Quando('eu altero o tipo da questão {int} para {string}') do |num, novo_tipo| + @current_question_index = num - 1 + change_question_type_in_form(@current_question_index, novo_tipo) +end + +# Preenche título da questão atual. +# +# Argumentos: +# - texto (String): Título. +Quando('preencho o campo texto com {string}') do |texto| + fill_question_title(texto) +end + +# Preenche opções da questão atual. +# +# Argumentos: +# - opcoes_str (String): Opções separadas por vírgula. +Quando('preencho o campo Opções com {string}') do |opcoes_str| + options_list = parse_options_list(opcoes_str) + + options_list.each_with_index do |option_text, index| + fill_single_option(option_text, index) + end +end + +# Limpa inputs de opções. +Quando('eu deixo o campo Opções vazio') do + clear_all_options_inputs +end + +# Passo composto: altera tipo e preenche texto. +Quando('eu altero o tipo da questão {int} para {string} e preencho o campo texto com {string}') do |num, tipo, texto| + step "eu altero o tipo da questão #{num} para \"#{tipo}\"" + step "preencho o campo texto com \"#{texto}\"" +end + +# Passo composto completo: tipo, texto e opções. +Quando('eu altero o tipo da questão {int} para {string} e preencho o campo texto com {string} e preencho o campo Opções com {string}') do |num, tipo, texto, opcoes| + step "eu altero o tipo da questão #{num} para \"#{tipo}\"" + step "preencho o campo texto com \"#{texto}\"" + step "preencho o campo Opções com \"#{opcoes}\"" +end + +# Alias para preencher texto. +Quando('eu altero o corpo para {string}') do |texto| + step "preencho o campo texto com \"#{texto}\"" +end + +# Alias para preencher texto. +Quando('eu altero o texto da questão para {string}') do |texto| + step "preencho o campo texto com \"#{texto}\"" +end + +# Alias para preencher opções. +Quando('eu altero as opções da questão para {string}') do |opcoes| + step "preencho o campo Opções com \"#{opcoes}\"" +end + +# Alias para limpar texto. +Quando('eu deixo o texto vazio') do + step "preencho o campo texto com \"\"" +end + +# Alias para limpar texto. +Quando('eu deixo o campo texto vazio') do + step "preencho o campo texto com \"\"" +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica mensagem. +# +# Argumentos: +# - mensagem (String): Mensagem esperada. +Então('devo ver a mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +# Verifica migração de posição (drag and drop simulado ou reordenação). +# +# Argumentos: +# - origem (Integer): Posição original (não usado na lógica de verificação direta). +# - destino (Integer): Posição nova. +Então('devo ver que a questão {int} migrou para a posição da questão {int}') do |origem, destino| + verify_question_migration(destino) +end + +# Verifica permanência na página. +Então('devo permanecer na página de edição do template') do + expect(current_path).to eq(edit_template_path(@template)) +end + +# ========================================= +# Métodos Auxiliares (Helpers) +# ========================================= + +# Encontra ou cria template para edição. +# +# Argumentos: +# - nome (String): Parte do título. +# - semestre (String): Semestre. +# +# Retorno: +# - (Template): Template. +def find_or_create_template_for_edit(nome, semestre) + template = Template.where("titulo LIKE ?", "%#{nome}%").first + return template if template + + criador = Usuario.first || Usuario.create!( + nome: 'Admin', email: 'admin@test.com', matricula: '123', + usuario: 'admin', password: 'password', ocupacao: :admin, status: true + ) + Template.create!(titulo: "#{nome} - #{semestre}", criador: criador) +end + +# Atualiza atributos de uma questão. +# +# Argumentos: +# - num (Integer): Número da questão. +# - tipo (String): Tipo. +# - opcoes (String): Opções (opcional). +def update_question_attributes(num, tipo, opcoes) + @current_question_index = num - 1 + question = @template.template_questions[num - 1] + + type_map = { 'texto' => 'text', 'radio' => 'radio', 'checkbox' => 'checkbox' } + attributes = { question_type: type_map[tipo] || 'text' } + attributes[:content] = opcoes.split(',').map(&:strip) if opcoes + + question.update!(attributes) +end + +# Clica no botão remover questão. +# +# Argumentos: +# - num (Integer): Índice. +def click_delete_question_button(num) + within all('.question-form')[num - 1] do + click_link "Remover Questão" + end +end + +# Salva questão atual. +# +# Efeitos Colaterais: +# - Clica em Salvar Questão. +def save_current_question_form + index = @current_question_index || (all('.question-form').count - 1) + within all('.question-form')[index] do + click_button "Salvar Questão" + end +end + +# Altera tipo de questão no formulário. +# +# Argumentos: +# - index (Integer): Índice do form. +# - novo_tipo (String): Novo tipo. +def change_question_type_in_form(index, novo_tipo) + within all('.question-form')[index] do + select_option = resolve_select_option_label(novo_tipo) + select select_option, from: "Tipo da Questão" + click_button "Salvar Questão" + end +end + +# Resolve label do select. +# +# Argumentos: +# - tipo (String): Tipo interno. +# +# Retorno: +# - (String): Label UI. +def resolve_select_option_label(tipo) + case tipo + when "texto" then "Text" + when "radio" then "Radio" + else tipo.humanize + end +end + +# Preenche título da questão. +# +# Argumentos: +# - texto (String): Título. +def fill_question_title(texto) + within all('.question-form')[@current_question_index] do + fill_in "Título da Questão", with: texto + end +end + +# Limpa inputs de opções. +def clear_all_options_inputs + within all('.question-form')[@current_question_index] do + all('input[name="alternatives[]"]').each { |input| input.set("") } + end +end + +# Verifica migração de questão. +# +# Argumentos: +# - destino_index (Integer): Posição esperada. +def verify_question_migration(destino_index) + # Verifica se o título esperado (da questão que moveu) está na posição de destino + form = all('.question-form')[destino_index - 1] + title = form.find('input[name*="[title]"]').value + expect(title).to include("texto para a questão 2") +end \ No newline at end of file diff --git a/src/features/step_definitions/formulario_steps.rb b/src/features/step_definitions/formulario_steps.rb new file mode 100644 index 0000000000..f637f316e3 --- /dev/null +++ b/src/features/step_definitions/formulario_steps.rb @@ -0,0 +1,124 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Cria turmas mockadas do SIGAA com estrutura completa. +# +# Argumentos: +# - nome_turma1 (String): Nome da primeira turma. +# - nome_turma2 (String): Nome da segunda turma. +# +# Efeitos Colaterais: +# - Persiste User, Materia, Turma. +Dado('existem as turmas {string} e {string} importadas do SIGAA') do |nome_turma1, nome_turma2| + docente = find_or_create_default_teacher + + [nome_turma1, nome_turma2].each do |nome_completo| + create_sigaa_class_structure(nome_completo, docente) + end +end + +# Cria template básico. +# +# Argumentos: +# - nome_template (String): Nome do template. +# +# Efeitos Colaterais: +# - Persiste Template e Usuario criador. +Dado('existe um template {string}') do |nome_template| + find_or_create_template(nome_template) +end + +# ========================================= +# Ações (Quando) +# ========================================= + +# Seleciona template no combobox. +# +# Argumentos: +# - nome_template (String): Nome. +Quando('eu seleciono o template {string}') do |nome_template| + select nome_template, from: "Template" +end + +# Seleciona duas turmas (checkbox). +# +# Argumentos: +# - turma1 (String): Nome turma 1. +# - turma2 (String): Nome turma 2. +Quando('eu seleciono as turmas {string} e {string}') do |turma1, turma2| + check turma1 + check turma2 +end + +# Seleciona uma turma (checkbox). +# +# Argumentos: +# - turma1 (String): Nome. +Quando('eu seleciono as turmas {string}') do |turma1| + check turma1 +end + +# Preenche data de encerramento. +# +# Argumentos: +# - data (String): Data em string. +Quando('eu defino a data de encerramento para {string}') do |data| + fill_in "Data de encerramento", with: data +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica redirecionamento por nome de rota. +# +# Argumentos: +# - page_name (String): Nome da rota/página. +Então('eu devo ser redirecionado para a página {string}') do |page_name| + expect(page).to have_current_path(path_to(page_name)) +end + +# Verifica associação do último formulário criado. +# +# Argumentos: +# - nome_template (String): Nome do template esperado. +# +# Retorno: +# - (Boolean): Asserção de igualdade. +Então('o formulário deve estar associado ao template {string}') do |nome_template| + verify_last_form_template(nome_template) +end + +# ========================================= +# Métodos Auxiliares (Helpers) +# ========================================= + +# Encontra ou cria template. +# +# Argumentos: +# - nome_template (String): Nome. +# +# Retorno: +# - (Template): Objeto template. +def find_or_create_template(nome_template) + criador = Usuario.first || Usuario.create!( + nome: "Admin", email: "admin@test.com", matricula: "0000", + usuario: "admin", password: "password", ocupacao: :admin, status: true + ) + + Template.find_or_create_by!(name: nome_template) do |t| + t.titulo = nome_template + t.participantes = "todos" + t.id_criador = criador.id + end +end + +# Verifica template do último formulário. +# +# Argumentos: +# - nome_template (String): Nome esperado. +def verify_last_form_template(nome_template) + formulario = Formulario.last + expect(formulario.template.name).to eq(nome_template) +end \ No newline at end of file diff --git a/src/features/step_definitions/gerar_relatorio_steps.rb b/src/features/step_definitions/gerar_relatorio_steps.rb new file mode 100644 index 0000000000..86772b1b65 --- /dev/null +++ b/src/features/step_definitions/gerar_relatorio_steps.rb @@ -0,0 +1,63 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Cria um formulário para relatório. +# +# Argumentos: +# - titulo_form (String): Título do formulário. +# +# Efeitos Colaterais: +# - Persiste User, Template, Formulario. +Dado('(que )existe um formulário {string}') do |titulo_form| + contexto = find_or_create_form_dependencies + @formulario_relatorio = create_formulario_relatorio(titulo_form, contexto) +end + +# Cria respostas mockadas para um formulário. +# +# Argumentos: +# - titulo_form (String): Título do formulário. +# - qtd_str (String): Quantidade de respostas. +# +# Efeitos Colaterais: +# - Cria Usuario, Matricula, Resposta. +Dado('o formulário {string} tem {string} respostas submetidas') do |titulo_form, qtd_str| + form = Formulario.find_by(titulo_envio: titulo_form) + + qtd_str.to_i.times do |index| + create_student_submission(form, index) + end +end + +# Acessa página de resultados do formulário. +# +# Argumentos: +# - titulo_form (String): Título do formulário. +# +# Efeitos Colaterais: +# - Visita URL de resultados. +Dado('que eu estou na página de resultados do formulário {string}') do |titulo_form| + form = Formulario.find_by(titulo_envio: titulo_form) + visit resultado_path(form) +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica download de arquivo CSV. +# +# Argumentos: +# - nome_arquivo (String): Nome esperado do arquivo. +# +# Retorno: +# - (Boolean): Asserção de headers. +Então('um download de um arquivo {string} deve ser iniciado') do |nome_arquivo| + verify_csv_download_response(nome_arquivo) +end + +# Verifica que nenhum download ocorreu. +Então('nenhum download deve ser iniciado') do + verify_no_file_download_occurred +end \ No newline at end of file diff --git a/src/features/step_definitions/gerenciamento_departamento_steps.rb b/src/features/step_definitions/gerenciamento_departamento_steps.rb new file mode 100644 index 0000000000..62dc0cd992 --- /dev/null +++ b/src/features/step_definitions/gerenciamento_departamento_steps.rb @@ -0,0 +1,68 @@ +# Step pendente coordenador. +# +# Argumentos: +# - string (String): Depto. +Dado('que eu sou um Administrador coordenador do departamento {string} \(CIC)') do |string| + pending # Write code here that turns the phrase above into concrete actions +end + +# Step pendente login. +Dado('que estou logado no sistema') do + pending # Write code here that turns the phrase above into concrete actions +end + +# Step pendente turma CIC. +# +# Argumentos: +# - string (String): Turma. +# - string2 (String): Depto. +Dado('que existe a turma {string} \(CIC0105) pertencente ao departamento {string}') do |string, string2| + pending # Write code here that turns the phrase above into concrete actions +end + +# Step pendente turma MAT. +# +# Argumentos: +# - string (String): Turma. +# - string2 (String): Depto. +Dado('que existe a turma {string} \(MAT0025) pertencente ao departamento {string} \(MAT)') do |string, string2| + pending # Write code here that turns the phrase above into concrete actions +end + +# Step pendente acesso lista. +Quando('eu acesso a lista de turmas para gerenciamento') do + pending # Write code here that turns the phrase above into concrete actions +end + +# Step pendente ver turma. +# +# Argumentos: +# - string (String): Turma. +Então('eu devo ver a turma {string} na lista') do |string| + pending # Write code here that turns the phrase above into concrete actions +end + +# Step pendente opção de turma. +# +# Argumentos: +# - string (String): Opção. +# - string2 (String): Turma. +Então('eu devo ver a opção de {string} para a turma {string}') do |string, string2| + pending # Write code here that turns the phrase above into concrete actions +end + +# Step pendente não ver turma. +# +# Argumentos: +# - string (String): Turma. +Então('eu NÃO devo ver a turma {string} na lista') do |string| + pending # Write code here that turns the phrase above into concrete actions +end + +# Step pendente acesso direto. +# +# Argumentos: +# - string (String): Turma. +Quando('eu tento acessar diretamente a URL de gerenciamento da turma {string}') do |string| + pending # Write code here that turns the phrase above into concrete actions +end diff --git a/src/features/step_definitions/importar_dados_sigaa_steps.rb b/src/features/step_definitions/importar_dados_sigaa_steps.rb new file mode 100644 index 0000000000..1a96c56a17 --- /dev/null +++ b/src/features/step_definitions/importar_dados_sigaa_steps.rb @@ -0,0 +1,493 @@ +require 'json' + +# Setup inicial do cenário. +Before do + @fake_classes = [] + @fake_members = [] +end + +# ========================================= +# Contexto (Dado) +# ========================================= + +# Limpa turmas e matérias do sistema. +# +# Efeitos Colaterais: +# - Remove registros de Turma e Materia. +Dado('que o sistema não possui nenhuma turma cadastrada') do + Turma.destroy_all + Materia.destroy_all +end + +# Limpa usuários não-admin. +# +# Efeitos Colaterais: +# - Remove registros de Usuario. +Dado('que o sistema não possui nenhum usuário cadastrado') do + Usuario.where.not(ocupacao: :admin).destroy_all +end + +# Adiciona turma ao mock do SIGAA. +# +# Argumentos: +# - codigo_turma (String): Código. +# - nome_materia (String): Nome. +# - codigo_materia (String): Código Matéria. +# +# Efeitos Colaterais: +# - Adiciona dados a @fake_classes. +Dado('que o sigaa contém a turma {string} da matéria {string} \({string})') do |codigo_turma, nome_materia, codigo_materia| + add_class_to_sigaa_mock(codigo_turma, nome_materia, codigo_materia) +end + +# Adiciona participante ao mock de turma. +# +# Argumentos: +# - nome (String): Nome. +# - matricula (String): Matrícula. +# +# Efeitos Colaterais: +# - Adiciona membro a @fake_members. +Dado('esta turma contém o participante {string} \({string})') do |nome, matricula| + contexto = resolve_current_class_context + member_record = find_or_create_member_record(contexto[:code], contexto[:class_code]) + add_student_to_member_record(member_record, nome, matricula) +end + +# Cria usuário discente. +# +# Argumentos: +# - nome (String): Nome. +# - matricula (String): Matrícula. +# +# Efeitos Colaterais: +# - Persiste Usuario. +Dado('que o sistema possui o usuário {string} \({string}) cadastrado') do |nome, matricula| + create_discente(nome, matricula) +end + +# Cria usuário discente com email. +# +# Argumentos: +# - nome (String): Nome. +# - matricula (String): Matrícula. +# - email (String): Email. +# +# Efeitos Colaterais: +# - Persiste Usuario. +Dado('que o sistema possui o usuário {string} \({string}) cadastrado com o e-mail {string}') do |nome, matricula, email| + create_discente(nome, matricula, email) +end + +# Verifica inexistência de turma. +# +# Argumentos: +# - nome_turma (String): Nome matéria. +# - codigo_turma (String): Código turma. +Dado('que o sistema não possui a turma {string} \({string}) cadastrada') do |nome_turma, codigo_turma| + expect(Turma.joins(:materia).where(materias: { nome: nome_turma }, codigo: codigo_turma).count).to eq(0) +end + +# Cria turma completa no sistema. +# +# Argumentos: +# - codigo_turma (String): Código turma. +# - nome_materia (String): Nome matéria. +# - codigo_materia (String): Código matéria. +# +# Efeitos Colaterais: +# - Persiste Materia, Turma. +Dado('que o sistema possui a turma {string} da matéria {string} \({string}) cadastrada') do |codigo_turma, nome_materia, codigo_materia| + create_full_system_class(codigo_turma, nome_materia, codigo_materia) +end + +# Cria turma com matéria (nome padrão). +# +# Argumentos: +# - codigo_turma (String): Código turma. +# - codigo_materia (String): Código matéria. +# +# Efeitos Colaterais: +# - Persiste Materia, Turma. +Dado('que o sistema possui a turma {string} da matéria {string} cadastrada') do |codigo_turma, codigo_materia| + create_full_system_class(codigo_turma, "Matéria #{codigo_materia}", codigo_materia) +end + +# Cria matéria. +# +# Argumentos: +# - codigo_materia (String): Código. +# +# Efeitos Colaterais: +# - Persiste Materia. +Dado('que o sistema possui a matéria {string} cadastrada') do |codigo_materia| + find_or_create_materia_by_code(codigo_materia) +end + +# Verifica inexistência de usuário. +# +# Argumentos: +# - nome (String): Nome (não usado na busca). +# - matricula (String): Matrícula. +Dado('que o sistema não possui o usuário {string} \({string}) cadastrado') do |nome, matricula| + expect(Usuario.where(matricula: matricula).count).to eq(0) +end + +# Simula erro no SIGAA mock. +# +# Efeitos Colaterais: +# - Define flag de erro. +Dado('que o sigaa está indisponível') do + @simular_erro_arquivo = true +end + +# Atualiza email no mock. +# +# Argumentos: +# - matricula (String): Matrícula. +# - novo_email (String): Novo email. +# +# Efeitos Colaterais: +# - Modifica mock de aluno. +Dado('a fonte de dados externa indica que o e-mail de {string} agora é {string}') do |matricula, novo_email| + matricula_str = matricula.to_s + turma_mock = find_or_create_mock_class_for(matricula_str) + ensure_class_definition_exists(turma_mock["code"]) + update_mock_student_email(turma_mock, matricula_str, novo_email) +end + +# Verifica que usuário não está matriculado. +# +# Argumentos: +# - matricula_usuario (String): Matrícula usuário. +# - codigo_turma (String): Código turma. +# - codigo_materia (String): Código matéria. +Dado('o usuário {string} ainda não está matriculado na turma {string} da matéria {string}') do |matricula_usuario, codigo_turma, codigo_materia| + verify_user_not_enrolled(matricula_usuario, codigo_turma, codigo_materia) +end + +# Define matrícula no mock. +# +# Argumentos: +# - matricula (String): Aluno. +# - codigo_turma (String): Turma. +# - codigo_materia (String): Matéria. +# +# Efeitos Colaterais: +# - Cria estrutura mockada se não existir. +Dado('a fonte de dados externa indica que {string} está matriculado na turma {string} da matéria {string}') do |matricula, codigo_turma, codigo_materia| + ensure_imported_class_definition(codigo_materia, codigo_turma) + create_class_with_student_if_missing(codigo_materia, codigo_turma, matricula) +end + +# Atualiza nome no mock. +# +# Argumentos: +# - matricula (String): Matrícula. +# - novo_nome (String): Novo nome. +Dado('a fonte de dados externa indica que o nome de {string} agora é {string}') do |matricula, novo_nome| + ensure_class_definition_exists("CIC0097") + turma_mock = ensure_default_class_member_exists + upsert_student_with_name(turma_mock, matricula, novo_nome) +end + +# Atualiza nome da matéria no mock. +# +# Argumentos: +# - codigo_materia (String): Código. +# - novo_nome (String): Novo nome. +Dado('a fonte de dados externa indica que o nome da matéria {string} agora é {string}') do |codigo_materia, novo_nome| + update_sigaa_subject_name(codigo_materia, novo_nome) +end + +# Remove dados do mock. +# +# Argumentos: +# - identificador (String): Código ou matrícula. +Dado('a fonte de dados externa indica que {string} não está mais presente') do |identificador| + remove_mock_class_data(identificador) + remove_mock_student_data(identificador) +end + +# ========================================= +# Ações (Quando) +# ========================================= + +# Dispara importação via UI. +# +# Argumentos: +# - botao (String): Texto do botão. +# +# Efeitos Colaterais: +# - Mocka FS, clica botão, realiza POST. +Quando('eu solicito a importação clicando em {string}') do |botao| + capture_initial_database_counts + setup_file_system_mocking + click_button botao +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica existência da turma. +# +# Argumentos: +# - codigo_turma (String): Turma. +# - nome_materia (String): Matéria. +# - codigo_materia (String): Código Matéria. +Então('a turma {string} da matéria {string} \({string}) deve ser cadastrada no sistema') do |codigo_turma, nome_materia, codigo_materia| + verify_class_existence(codigo_turma, nome_materia, codigo_materia) +end + +# Verifica existência do usuário. +# +# Argumentos: +# - nome (String): Nome. +# - matricula (String): Matrícula. +Então('o usuário {string} \({string}) deve ser cadastrado no sistema') do |nome, matricula| + verify_user_existence(nome, matricula) +end + +# Verifica consistência de matrícula. +# +# Argumentos: +# - matricula (String): Aluno. +# - codigo_turma (String): Turma. +# - codigo_materia (String): Matéria. +Então('o usuário {string} deve estar matriculado na turma {string} da matéria {string}') do |matricula, codigo_turma, codigo_materia| + verify_enrollment_consistency(matricula, codigo_turma, codigo_materia) +end + +# Alias para verificação de matrícula. +Então('o usuário {string} deve ser matriculado na turma {string} da matéria {string}') do |matricula, codigo_turma, codigo_materia| + verify_enrollment_consistency(matricula, codigo_turma, codigo_materia) +end + +# Verifica mensagem de sucesso. +# +# Argumentos: +# - mensagem (String): Mensagem. +Então('eu devo ver a mensagem de sucesso {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +# Verifica que contagem de turmas não mudou. +Então('nenhuma nova turma deve ser cadastrada no sistema') do + expect(Turma.count).to eq(@quantidade_inicial_turmas) +end + +# Verifica que contagem de usuários não mudou. +Então('nenhum novo usuário deve ser cadastrado no sistema') do + expect(Usuario.count).to eq(@quantidade_inicial_usuarios) +end + +# Verifica unicidade de usuário. +# +# Argumentos: +# - nome (String): Nome (ignorado). +# - matricula (String): Matrícula. +Então('o usuário {string} \({string}) não deve ser duplicado no sistema') do |nome, matricula| + expect(Usuario.where(matricula: matricula).count).to eq(1) +end + +# Verifica ausência geral de duplicatas. +Então('nenhum usuário duplicado deve ser criado') do + duplicados = Usuario.group(:matricula).having('COUNT(*) > 1').count + expect(duplicados).to be_empty +end + +# Verifica estado da UI após ação. +# +# Efeitos Colaterais: +# - Asserções de UI. +Então('os outro botões na página devem ser liberados') do + verify_edit_button_active + verify_navigation_links_active +end + +# Verifica atualização de email. +# +# Argumentos: +# - matricula (String): Matrícula. +# - novo_email (String): Novo email. +Então('o e-mail do usuário {string} deve ser atualizado para {string}') do |matricula, novo_email| + usuario = Usuario.find_by(matricula: matricula) + expect(usuario).to be_present + expect(usuario.email).to eq(novo_email) +end + +# Verifica atualização de nome de usuário. +# +# Argumentos: +# - matricula (String): Matrícula. +# - novo_nome (String): Novo nome. +Então('o nome do usuário {string} deve ser atualizado para {string}') do |matricula, novo_nome| + usuario = Usuario.find_by(matricula: matricula) + expect(usuario).to be_present + expect(usuario.nome).to eq(novo_nome) +end + +# Verifica atualização de nome da matéria. +# +# Argumentos: +# - codigo_materia (String): Código. +# - novo_nome (String): Novo nome. +Então('o nome da matéria {string} deve ser atualizado para {string}') do |codigo_materia, novo_nome| + materia = Materia.find_by(codigo: codigo_materia) + expect(materia).to be_present + expect(materia.nome).to eq(novo_nome) +end + +# Verifica exclusão de usuário. +# +# Argumentos: +# - matricula (String): Matrícula. +Então('o usuário {string} deve ser excluído do sistema') do |matricula| + expect(Usuario.find_by(matricula: matricula)).to be_nil +end + +# ========================================= +# Métodos Auxiliares (Helpers) +# ========================================= + +# Adiciona turma mock SIGAA. +# +# Argumentos: +# - codigo_turma (String): Código turma. +# - nome_materia (String): Nome matéria. +# - codigo_materia (String): Código matéria. +def add_class_to_sigaa_mock(codigo_turma, nome_materia, codigo_materia) + @fake_classes << { + "name" => nome_materia, + "code" => codigo_materia, + "class" => { + "classCode" => codigo_turma, + "semester" => "2024.1", + "time" => "35T23" + } + } +end + +# Cria discente no sistema. +# +# Argumentos: +# - nome (String): Nome. +# - matricula (String): Matrícula. +# - email (String): Email (opcional). +def create_discente(nome, matricula, email = nil) + email ||= "#{matricula}@exemplo.com" + Usuario.create!( + nome: nome, + matricula: matricula, + email: email, + usuario: matricula, + password: "password123", + ocupacao: :discente, + status: true + ) +end + +# Cria estrutura completa de turma/materia/docente. +# +# Argumentos: +# - codigo_turma (String): Código. +# - nome_materia (String): Nome. +# - codigo_materia (String): Código. +def create_full_system_class(codigo_turma, nome_materia, codigo_materia) + materia = find_or_create_materia_by_code(codigo_materia, nome_materia) + docente = find_or_create_default_docente + + Turma.find_or_create_by!(codigo: codigo_turma) do |t| + t.materia = materia + t.docente = docente + t.semestre = "2024.1" + t.horario = "35T23" + end +end + +# Cria matéria por código. +# +# Argumentos: +# - codigo (String): Código. +# - nome (String): Nome (opcional). +def find_or_create_materia_by_code(codigo, nome = nil) + Materia.find_or_create_by!(codigo: codigo) do |m| + m.nome = nome || "Matéria #{codigo}" + end +end + +# Cria docente padrão. +# +# Retorno: +# - (Usuario): Docente default. +def find_or_create_default_docente + Usuario.find_by(ocupacao: :docente) || Usuario.create!( + nome: "Professor Teste", + matricula: "999999", + usuario: "999999", + email: "prof_teste_local@unb.br", + password: "123", + ocupacao: :docente, + status: true + ) +end + +# Atualiza nome de matéria mock SIGAA. +# +# Argumentos: +# - codigo_materia (String): Código. +# - novo_nome (String): Novo nome. +def update_sigaa_subject_name(codigo_materia, novo_nome) + class_mock = @fake_classes.find { |c| c["code"] == codigo_materia } + + unless class_mock + class_mock = { + "code" => codigo_materia, + "name" => "Nome Antigo", + "class" => { "semester" => "2024.1", "time" => "35T23", "classCode" => "TA" } + } + @fake_classes << class_mock + end + + class_mock["name"] = novo_nome +end + +# Verifica existência de turma e relação com matéria. +# +# Argumentos: +# - codigo_turma (String): Código turma. +# - nome_materia (String): Nome matéria. +# - codigo_materia (String): Código matéria. +def verify_class_existence(codigo_turma, nome_materia, codigo_materia) + materia = Materia.find_by(codigo: codigo_materia) + turma = Turma.joins(:materia).find_by(codigo: codigo_turma, materia: materia) + expect(turma).to be_present + expect(turma.materia.nome).to eq(nome_materia) +end + +# Verifica existência de usuário. +# +# Argumentos: +# - nome (String): Nome. +# - matricula (String): Matrícula. +def verify_user_existence(nome, matricula) + usuario = Usuario.find_by(matricula: matricula) + expect(usuario).to be_present + expect(usuario.nome).to eq(nome) +end + +# Verifica que usuário NÃO está na turma. +# +# Argumentos: +# - matricula_usuario (String): Matrícula. +# - codigo_turma (String): Código turma. +# - codigo_materia (String): Código matéria. +def verify_user_not_enrolled(matricula_usuario, codigo_turma, codigo_materia) + user = Usuario.find_by(matricula: matricula_usuario) + turma = Turma.joins(:materia).find_by(codigo: codigo_turma, materias: { codigo: codigo_materia }) + + if turma + expect(user.turmas).not_to include(turma) + end +end \ No newline at end of file diff --git a/src/features/step_definitions/painel_pendencias_steps.rb b/src/features/step_definitions/painel_pendencias_steps.rb new file mode 100644 index 0000000000..7b0097a8c8 --- /dev/null +++ b/src/features/step_definitions/painel_pendencias_steps.rb @@ -0,0 +1,180 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Configura matrícula do aluno na turma. +# +# Argumentos: +# - nome_turma (String): Nome da turma. +# +# Efeitos Colaterais: +# - Setup de dados (Materias, Turmas, Usuario, Matricula). +Dado('que eu sou um aluno matriculado na turma {string}') do |nome_turma| + setup_student_enrollment(nome_turma) +end + +# Distribui formulário para turma. +# +# Argumentos: +# - nome_template (String): Nome do template. +# - nome_turma (String): Nome da turma (ignorado no helper). +# +# Efeitos Colaterais: +# - Cria Template e distribui. +Dado('que o administrador distribuiu o template {string} para a turma {string}') do |nome_template, nome_turma| + distribute_template_to_class(nome_template) +end + +# Garante que resposta está pendente. +# +# Efeitos Colaterais: +# - Atualiza data_submissao para nil. +Dado('que eu ainda não respondi a este formulário') do + ensure_response_is_pending +end + +# Simula login. +# +# Efeitos Colaterais: +# - Mocka current_usuario na sessao. +Dado('que eu estou logado como aluno') do + mock_student_login +end + +# Marca resposta como submetida. +# +# Argumentos: +# - nome_template (String): Nome (ignorado). +# - nome_turma (String): Turma (ignorado). +# +# Efeitos Colaterais: +# - Atualiza data_submissao. +Dado('que eu já respondi a avaliação {string} da turma {string}') do |nome_template, nome_turma| + mark_response_as_submitted +end + +# ========================================= +# Ações (Quando) +# ========================================= + +# Acessa path de avaliações. +# +# Efeitos Colaterais: +# - Visit /avaliacoes. +Quando('eu acesso o meu painel de avaliações') do + visit avaliacoes_path +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica texto na página. +# +# Argumentos: +# - titulo_template (String): Texto esperado. +Então('eu devo ver {string} na lista de pendências') do |titulo_template| + expect(page).to have_content(titulo_template) +end + +# Verifica indicação da turma. +# +# Argumentos: +# - codigo_turma (String): Código. +Então('o item deve indicar a turma {string}') do |codigo_turma| + expect(page).to have_content(@turma.codigo) +end + +# Verifica existência de link. +# +# Argumentos: +# - texto_link (String): Texto do link. +Então('eu devo ver um link para {string}') do |texto_link| + expect(page).to have_link(texto_link) +end + +# Verifica ausência de texto. +# +# Argumentos: +# - titulo_template (String): Texto. +Então('eu não devo ver {string} na lista de pendências') do |titulo_template| + expect(page).not_to have_content(titulo_template) +end + +# ========================================= +# Métodos Auxiliares (Helpers) +# ========================================= + +# Configura contexto de matrícula. +# +# Argumentos: +# - nome_turma (String): Nome da turma. +# +# Efeitos Colaterais: +# - Cria dados reais no banco de teste. +def setup_student_enrollment(nome_turma) + materia = Materia.create!(nome: nome_turma, codigo: "MAT_PEND") + + docente = Usuario.find_by(ocupacao: :docente) || Usuario.create!( + nome: "Prof. Teste", email: "prof_teste@test.com", matricula: "PROF123", + usuario: "prof_teste", password: "password", ocupacao: :docente, status: true + ) + + @turma = Turma.create!( + codigo: "T_PEND", + semestre: '2024.1', + horario: '35T', + materia: materia, + docente: docente + ) + + @meu_usuario = Usuario.create!( + nome: "Aluno Logado", + email: "aluno_logado@test.com", + matricula: "20240001", + usuario: "aluno_logado", + password: 'password', + ocupacao: :discente, + status: true + ) + + Matricula.create!(usuario: @meu_usuario, turma: @turma) +end + +# Distribui template criado. +# +# Argumentos: +# - nome_template (String): Nome. +def distribute_template_to_class(nome_template) + @template = Template.create!( + name: nome_template, + titulo: nome_template, + id_criador: Usuario.first.id, + participantes: 'todos' + ) + @turma.distribuir_formulario(@template) +end + +# Garante resposta pendente. +# +# Efeitos Colaterais: +# - Update DB. +def ensure_response_is_pending + form = @turma.formularios.last + resposta = Resposta.find_by(formulario: form, participante: @meu_usuario) + resposta.update!(data_submissao: nil) +end + +# Mocka login via controller helper (se suportado) ou apenas lógica interna. +# Note: Na prática `allow_any_instance_of` é desencorajado no RSpec moderno, +# mas mantendo conformidade com o código original. +def mock_student_login + allow_any_instance_of(ApplicationController).to receive(:current_usuario).and_return(@meu_usuario) +end + +# Marca resposta como submetida. +def mark_response_as_submitted + form = @turma.formularios.last + resposta = Resposta.find_by(formulario: form, participante: @meu_usuario) + resposta.update!(data_submissao: Time.now) +end \ No newline at end of file diff --git a/src/features/step_definitions/question_steps.rb b/src/features/step_definitions/question_steps.rb new file mode 100644 index 0000000000..6587cf8641 --- /dev/null +++ b/src/features/step_definitions/question_steps.rb @@ -0,0 +1,19 @@ +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica presença do formulário de nova questão. +# +# Efeitos Colaterais: +# - Asserção de seletor CSS. +Então('eu devo ver um formulário de nova questão') do + expect(page).to have_selector('.question-form') +end + +# Verifica contagem de questões de template. +# +# Argumentos: +# - count (Integer): Quantidade esperada. +Então('o número total de questões deve ser {int}') do |count| + expect(TemplateQuestion.count).to eq(count) +end diff --git a/src/features/step_definitions/redefinir_senha_usuario_steps.rb b/src/features/step_definitions/redefinir_senha_usuario_steps.rb new file mode 100644 index 0000000000..e0ec0e561d --- /dev/null +++ b/src/features/step_definitions/redefinir_senha_usuario_steps.rb @@ -0,0 +1,163 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Prepara um usuário ativo. +# +# Argumentos: +# - email (String): Email. +# +# Efeitos Colaterais: +# - Cria Usuario ativo. +Dado('que o usuário {string} está cadastrado e ativo no sistema') do |email| + create_active_test_user(email) +end + +# Garante inexistência de email. +# +# Argumentos: +# - email (String): Email. +# +# Efeitos Colaterais: +# - Remove usuários com esse email. +Dado('que o e-mail {string} não está cadastrado no sistema') do |email| + ensure_email_not_registered(email) +end + +# Gera link de redefinição e salva em @link_definicao. +# +# Argumentos: +# - email (String): Email. +# +# Efeitos Colaterais: +# - Cria Usuario (se faltar), gera token. +Dado('que o usuário {string} solicitou um link de redefinição válido') do |email| + generate_valid_reset_link(email) +end + +# Prepara usuário com status específico. +# +# Argumentos: +# - email (String): Email. +# - status_desc (String): "ativo" ou outro. +# +# Efeitos Colaterais: +# - Cria Usuario. +Dado('que o usuário {string} está cadastrado no sistema com o status {string}') do |email, status_desc| + create_user_with_specific_status(email, status_desc) +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica permanência na página. +# +# Argumentos: +# - page_name (String): Nome da página. +Então('eu devo permanecer na página de {string}') do |page_name| + expect(page).to have_current_path(path_to(page_name)) +end + +# Verifica que nenhum email foi enviado. +Então('nenhum e-mail deve ser enviado') do + expect(ActionMailer::Base.deliveries.count).to eq(0) +end + +# Verifica login com nova senha. +# +# Argumentos: +# - email (String): Email. +# - nova_senha (String): Senha. +# +# Efeitos Colaterais: +# - Realiza fluxo de login via UI. +Então('o usuário {string} deve conseguir logar com a senha {string}') do |email, nova_senha| + perform_login_verification(email, nova_senha) +end + +# ========================================= +# Métodos Auxiliares (Helpers) +# ========================================= + +# Cria usuário de teste ativo. +# +# Argumentos: +# - email (String): Email. +def create_active_test_user(email) + Usuario.create!( + nome: "Usuário Teste", + email: email, + usuario: "2023#{rand(1000..9999)}", + matricula: "2023#{rand(1000..9999)}", + password: "Password123!", + ocupacao: :discente, + status: true + ) +end + +# Remove usuário por email. +# +# Argumentos: +# - email (String): Email. +def ensure_email_not_registered(email) + Usuario.where(email: email).destroy_all +end + +# Gera link e token. +# +# Argumentos: +# - email (String): Email. +def generate_valid_reset_link(email) + user = Usuario.find_by(email: email) || create_user_for_reset(email) + token = user.signed_id(purpose: :redefinir_senha, expires_in: 15.minutes) + @link_definicao = "/redefinir_senha/edit?token=#{token}" +end + +# Cria usuário para reset. +# +# Argumentos: +# - email (String): Email. +def create_user_for_reset(email) + Usuario.create!( + nome: "Usuário Teste", + email: email, + usuario: "2023#{rand(1000..9999)}", + matricula: "2023#{rand(1000..9999)}", + password: "PasswordAntiga123!", + ocupacao: :discente, + status: true + ) +end + +# Cria usuário com status. +# +# Argumentos: +# - email (String): Email. +# - status_desc (String): "ativo" ou outro. +def create_user_with_specific_status(email, status_desc) + is_ativo = (status_desc.downcase == 'ativo') + + Usuario.create!( + nome: "Usuário Status", + email: email, + usuario: "2023#{rand(1000..9999)}", + matricula: "2023#{rand(1000..9999)}", + password: "Password123!", + ocupacao: :discente, + status: is_ativo + ) +end + +# Tenta login na UI. +# +# Argumentos: +# - email (String): Email. +# - nova_senha (String): Senha. +def perform_login_verification(email, nova_senha) + visit '/login' + fill_in 'Usuário', with: email + fill_in 'Senha', with: nova_senha + click_on 'Entrar' + expect(page).to have_no_content("Entrar") +end \ No newline at end of file diff --git a/src/features/step_definitions/responder_steps.rb b/src/features/step_definitions/responder_steps.rb new file mode 100644 index 0000000000..6a334086c9 --- /dev/null +++ b/src/features/step_definitions/responder_steps.rb @@ -0,0 +1,316 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Loga como um usuário com papel específico. +# +# Argumentos: +# - role (String): Papel (e.g. aluno). +# - username (String): Nome usuário. +# +# Efeitos Colaterais: +# - Cria Usuario, realiza login UI. +Dado('que eu sou um {string} logado como {string}') do |role, username| + ocupacao = resolve_responder_occupation(role) + @user = find_or_create_responder_user(username, ocupacao) + perform_ui_login(@user.email, 'password') +end + +# Matricula usuario na turma. +# +# Argumentos: +# - turma_nome (String): Nome da turma. +# +# Efeitos Colaterais: +# - Cria Turma, Materia, Matricula. +Dado('eu estou matriculado na turma {string}') do |turma_nome| + create_student_enrollment(turma_nome) +end + +# Cria formulário para turma. +# +# Argumentos: +# - titulo_form (String): Título do formulário. +# - turma_nome (String): Nome da turma. +# +# Efeitos Colaterais: +# - Cria Template, Formulario. +Dado('existe um formulário {string} para a turma {string}') do |titulo_form, turma_nome| + create_form_for_class(titulo_form, turma_nome) +end + +# Adiciona pergunta ao formulário atual. +# +# Argumentos: +# - titulo_form (String): Título form (ignorado na lógica, usa @formulario). +# - pergunta_texto (String): Pergunta. +# - tipo (String): Tipo de pergunta. +# +# Efeitos Colaterais: +# - Cria TemplateQuestion, Questao. +Dado('o formulário {string} tem a pergunta {string} do tipo {string}') do |titulo_form, pergunta_texto, tipo| + # O código original ignora o titulo_form e usa a variável de instância @formulario + add_question_to_current_form(pergunta_texto, tipo) +end + +# Garante que não há resposta submetida. +# +# Argumentos: +# - titulo_form (String): Título. +# +# Efeitos Colaterais: +# - Limpa data_submissao. +Dado('que eu não respondi o formulário {string} ainda') do |titulo_form| + ensure_unanswered_response(titulo_form) +end + +# Visita dashboard. +Dado('eu estou na minha página inicial \(dashboard)') do + visit root_path +end + +# Cria resposta já submetida. +# +# Argumentos: +# - titulo_form (String): Título. +# +# Efeitos Colaterais: +# - Cria Resposta com data_submissao. +Dado('que eu já respondi o formulário {string}') do |titulo_form| + create_submitted_response(titulo_form) +end + +# Expira formulário. +# +# Argumentos: +# - titulo_form (String): Título. +# - data (String): Data passada. +# +# Efeitos Colaterais: +# - Update data_encerramento. +Dado('que o formulário {string} expirou em {string}') do |titulo_form, data| + expire_form_at_date(titulo_form, data) +end + +# Remove respostas existentes. +# +# Argumentos: +# - titulo_form (String): Título. +# +# Efeitos Colaterais: +# - Deleta Resposta. +Dado('eu não respondi o formulário {string} ainda') do |titulo_form| + delete_existing_responses(titulo_form) +end + +# ========================================= +# Ações (Quando) +# ========================================= + +# Verifica conteúdo em lista. +# +# Argumentos: +# - texto (String): Conteúdo. +# - lista_nome (String): Nome lista (ignorado). +Quando('eu vejo {string} na minha lista de {string}') do |texto, lista_nome| + expect(page).to have_content(texto) +end + +# Preenche pergunta no formulário. +# +# Argumentos: +# - valor (String): Resposta. +# - pergunta (String): Pergunta. +# +# Efeitos Colaterais: +# - Interação UI (choose/fill_in). +Quando('eu seleciono {string} para a pergunta {string}') do |valor, pergunta| + fill_form_question(valor, pergunta) +end + +# Tenta acessar página de resposta. +# +# Argumentos: +# - titulo_form (String): Título. +# +# Efeitos Colaterais: +# - Visit URL. +Quando('eu tento acessar a página do formulário {string} diretamente') do |titulo_form| + visit_form_response_page(titulo_form) +end + +# Alias para acesso a página. +Quando('eu tento acessar a página do formulário {string}') do |titulo_form| + visit_form_response_page(titulo_form) +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica redirecionamento para resposta. +# +# Efeitos Colaterais: +# - Asserção de URL. +Então('eu sou redirecionado para a página do formulário') do + expect(current_path).to match(/respostas\/new/) +end + +# Verifica presença na lista. +# +# Argumentos: +# - texto (String): Texto. +# - lista (String): Lista (ignorado). +Então('{string} deve aparecer na minha lista de {string}') do |texto, lista| + expect(page).to have_content(texto) +end + +# ========================================= +# Métodos Auxiliares (Helpers) +# ========================================= + +# Cria matrícula de estudante. +# +# Argumentos: +# - turma_nome (String): Nome turma. +def create_student_enrollment(turma_nome) + materia = Materia.find_by(nome: turma_nome) || Materia.create!(nome: turma_nome, codigo: '123') + + docente = Usuario.where(ocupacao: :docente).first || Usuario.create!( + nome: 'Docente', email: 'doc@test.com', usuario: 'doc', + password: 'password', ocupacao: :docente, status: true, matricula: 'DOC123' + ) + + @turma = Turma.create!( + codigo: 'T1', + semestre: '2025.1', + horario: '10:00', + materia: materia, + docente: docente + ) + + Matricula.create!(usuario: @user, turma: @turma) +end + +# Cria formulário para turma. +# +# Argumentos: +# - titulo_form (String): Título. +# - turma_nome (String): Turma. +def create_form_for_class(titulo_form, turma_nome) + materia = Materia.find_by(nome: turma_nome) + turma = Turma.joins(:materia).find_by(materias: { nome: turma_nome }) + + @template = Template.create!( + titulo: 'Template Teste', + participantes: 'alunos', + criador: Usuario.first || @user, + name: 'Template Name' + ) + + @formulario = Formulario.create!( + titulo_envio: titulo_form, + data_criacao: Time.now, + template: @template, + turma: turma + ) +end + +# Adiciona pergunta ao form. +# +# Argumentos: +# - pergunta_texto (String): Texto. +# - tipo (String): Tipo. +def add_question_to_current_form(pergunta_texto, tipo) + q_type = case tipo + when /numérica/ then 'text' + when /texto/ then 'text' + else 'text' + end + + TemplateQuestion.create!( + title: pergunta_texto, + question_type: q_type, + template: @formulario.template, + content: [] + ) + + tipo_int = case tipo + when /numérica/ then 0 + when /texto/ then 0 + else 0 + end + + Questao.create!( + enunciado: pergunta_texto, + tipo: tipo_int, + template: @formulario.template + ) +end + +# Garante resposta não respondida. +# +# Argumentos: +# - titulo_form (String): Título. +def ensure_unanswered_response(titulo_form) + form = Formulario.find_by(titulo_envio: titulo_form) + resposta = Resposta.find_or_create_by!(formulario: form, participante: @user) + resposta.update!(data_submissao: nil) if resposta.data_submissao.present? +end + +# Preenche questão no form. +# +# Argumentos: +# - valor (String): Valor. +# - pergunta (String): Pergunta. +def fill_form_question(valor, pergunta) + begin + choose valor + rescue Capybara::ElementNotFound + fill_in pergunta, with: valor + rescue + find('label', text: pergunta).find(:xpath, "..//input | ..//textarea").set(valor) + end +end + +# Cria resposta submetida. +# +# Argumentos: +# - titulo_form (String): Título. +def create_submitted_response(titulo_form) + form = Formulario.find_by(titulo_envio: titulo_form) + Resposta.create!( + formulario: form, + participante: @user, + data_submissao: Time.now + ) +end + +# Expira formulário. +# +# Argumentos: +# - titulo_form (String): Título. +# - data (String): Data fim. +def expire_form_at_date(titulo_form, data) + form = Formulario.find_by(titulo_envio: titulo_form) + data_expiracao = Date.strptime(data, "%d/%m/%Y").end_of_day - 1.day + form.update!(data_encerramento: data_expiracao) +end + +# Remove respostas. +# +# Argumentos: +# - titulo_form (String): Título. +def delete_existing_responses(titulo_form) + form = Formulario.find_by(titulo_envio: titulo_form) + Resposta.where(formulario: form, participante: @user).destroy_all +end + +# Visita página de resposta. +# +# Argumentos: +# - titulo_form (String): Título. +def visit_form_response_page(titulo_form) + form = Formulario.find_by(titulo_envio: titulo_form) + visit new_formulario_resposta_path(form.id) +end \ No newline at end of file diff --git a/src/features/step_definitions/template_steps.rb b/src/features/step_definitions/template_steps.rb new file mode 100644 index 0000000000..2b308c4f1e --- /dev/null +++ b/src/features/step_definitions/template_steps.rb @@ -0,0 +1,160 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Alias para login admin. +Dado('que eu estou logado como administrador') do + step 'que eu estou logado como Administrador' +end + +# Visita página de criação de template. +Dado('que eu estou na página de novo template') do + visit new_template_path +end + +# Cria template com título. +# +# Argumentos: +# - titulo (String): Título. +# +# Efeitos Colaterais: +# - Cria Template, Usuario (se necessário). +Dado('que existe um template chamado {string}') do |titulo| + criador = @admin || Usuario.first || Usuario.create!(nome: 'Admin', email: 'admin@test.com', matricula: '123', usuario: 'admin', password: 'password', ocupacao: :admin, status: true) + Template.create!(titulo: titulo, criador: criador) +end + +# Varição do passo anterior. +Dado('que existe um template {string}') do |template_name| + criador = @admin || Usuario.first || Usuario.create!(nome: 'Admin', email: 'admin@test.com', matricula: '123', usuario: 'admin', password: 'password', ocupacao: :admin, status: true) + Template.create!(titulo: template_name, criador: criador) +end + +# Visita edição de template. +# +# Argumentos: +# - titulo (String): Título. +Dado('que eu estou na página de edição de {string}') do |titulo| + template = Template.find_by!(titulo: titulo) + visit edit_template_path(template) +end + +# Visita listagem de templates. +Dado('que eu estou na página de listagem de templates') do + visit templates_path +end + +# ========================================= +# Ações (Quando) +# ========================================= + +# Preenche campo de formulário. +# +# Argumentos: +# - campo (String): Label. +# - valor (String): Valor. +Quando('eu preencho o campo do template {string} com {string}') do |campo, valor| + fill_in campo, with: valor +end + +# Clica em botão. +# +# Argumentos: +# - botao (String): Texto do botão. +Quando('eu clico no botão do template {string}') do |botao| + click_button botao +end + +# Clica em link/botão dentro de uma linha de tabela. +# +# Argumentos: +# - link_text (String): Ação. +# - template_titulo (String): Linha alvo. +Quando('eu clico em {string} para {string}') do |link_text, template_titulo| + row = find('tr', text: template_titulo) + within(row) do + click_link_or_button link_text + end +end + +# Adiciona questão via UI. +# +# Argumentos: +# - question_text (String): Título questão. +# - type (String): Tipo. +# +# Efeitos Colaterais: +# - Adiciona fieldset dinâmico, preenche e salva. +Quando('eu adiciono uma pergunta {string} do tipo {string}') do |question_text, type| + click_button "Adicionar Questão" + within all('.question-form').last do + fill_in "Título da Questão", with: question_text + + select_option = case type + when "texto" then "Text" + when "numérica (1-5)" then "Text" + when "múltipla escolha" then "Radio" + else type.humanize + end + + select select_option, from: "Tipo da Questão" + click_button "Salvar Questão" + end +end + +# Cria questão com opções mocks (sem UI completa). +# +# Argumentos: +# - question_text (String): Texto. +# - type (String): Tipo. +# - options (String): Opções. +# +# Efeitos Colaterais: +# - Usa helpers de factories_helpers e template_helpers. +Quando('eu adiciono uma pergunta {string} do tipo {string} com opções {string}') do |question_text, type, options| + create_base_question(question_text, type) + add_options_to_last_question(options) +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica redirecionamento para edição. +# +# Argumentos: +# - titulo (String): Título. +Então('eu devo ser redirecionado para a página de edição do template {string}') do |titulo| + template = Template.find_by!(titulo: titulo) + expect(current_path).to eq(edit_template_path(template)) +end + +# Verifica mensagem na tela. +Então('eu devo ver a mensagem do template {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +# Verifica ausência de conteúdo. +Então('eu não devo ver {string}') do |conteudo| + expect(page).not_to have_content(conteudo) +end + +# Verifica título do último template. +Então('o nome do template deve ser {string}') do |titulo| + expect(Template.last.titulo).to eq(titulo) +end + +# Verifica persistência e status hidden (soft delete provável). +# +# Argumentos: +# - titulo (String): Título. +Então('o template {string} deve continuar existindo no banco de dados') do |titulo| + template = Template.unscoped.find_by(titulo: titulo) + expect(template).not_to be_nil + expect(template.hidden).to be true +end + +# Verifica cabeçalho da página. +Então('eu devo permanecer na página de novo template') do + expect(page).to have_css('h1', text: 'Novo Template') +end \ No newline at end of file diff --git a/src/features/step_definitions/visualiza_templates_steps.rb b/src/features/step_definitions/visualiza_templates_steps.rb new file mode 100644 index 0000000000..8c1d19c9d9 --- /dev/null +++ b/src/features/step_definitions/visualiza_templates_steps.rb @@ -0,0 +1,51 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Step simplificado para login. +Dado('que estou logado') do + step "que eu estou logado como Administrador" +end + +# Cria template com dados específicos. +# +# Argumentos: +# - field1, value1, ...: Pares campo/valor (ignora campo, usa valores para título). +# +# Efeitos Colaterais: +# - Cria Template com título concatenado e hidden=false. +Dado('que existe um template criado com o campo {string} preenchido com {string}, e o campo {string} preenchido com {string}, e o campo {string} preenchido com {string}') do |field1, value1, field2, value2, field3, value3| + title = "#{value1} - #{value2} - #{value3}" + criador = Usuario.first || Usuario.create!(nome: 'Admin', email: 'admin@test.com', matricula: '123', usuario: 'admin', password: 'password', ocupacao: :admin, status: true) + Template.create!(titulo: title, criador: criador, hidden: false) +end + +# Limpa templates. +# +# Efeitos Colaterais: +# - Destroy all templates. +Dado('que não existe nenhum template criado') do + Template.destroy_all +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica conteúdo do cartão/template. +# +# Argumentos: +# - val1, val2, val3 (String): Valores esperados. +Então('devo ver um cartão da disciplina contendo: {string},{string},{string}') do |val1, val2, val3| + expect(page).to have_content(val1) + expect(page).to have_content(val2) + expect(page).to have_content(val3) +end + +# Verifica mensagem geral. +# +# Argumentos: +# - mensagem (String): Mensagem esperada. +Então('devo visualizar a mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) +end \ No newline at end of file diff --git a/src/features/step_definitions/visualizar_form.rb b/src/features/step_definitions/visualizar_form.rb new file mode 100644 index 0000000000..7eb6e8abca --- /dev/null +++ b/src/features/step_definitions/visualizar_form.rb @@ -0,0 +1,119 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +Dado('estou matriculado nas turmas {string} e {string}') do |turma1, turma2| + [turma1, turma2].each do |nome_turma| + step "estou matriculado na turma \"#{nome_turma}\"" + end +end + +Dado('estou matriculado na turma {string}') do |nome_turma| + create_enrollment_for_current_user(nome_turma) +end + +Dado('a turma {string} possui os formulários {string} e {string}') do |nome_turma, form1, form2| + create_forms_for_class(nome_turma, [form1, form2]) +end + +Dado('eu já respondi apenas o formulário {string}') do |titulo_formulario| + mark_specific_form_as_answered(titulo_formulario) +end + +Dado('todos os formulários desta turma já foram respondidos por mim') do + mark_all_pending_responses_as_answered +end + +Dado('não estou matriculado em nenhuma turma') do + clear_student_enrollments +end + +# ========================================= +# Verificações (Então) +# ========================================= + +Então('eu devo ver o formulário {string}') do |titulo| + expect(page).to have_content(titulo) +end + +Então('eu não devo ver o formulário {string}') do |titulo| + expect(page).not_to have_content(titulo) +end + +Então('eu devo ver a mensaagem {string}') do |mensagem| + step "eu devo ver a mensagem \"#{mensagem}\"" +end + +Então('não devo ver lista de formulários') do + expect(page).not_to have_css('ul.lista-formularios') + expect(page).not_to have_css('table.tabela-formularios') +end + +Então('devo permanecer na página {string}') do |page_name| + expect(page).to have_current_path(path_to(page_name)) +end + +# ========================================= +# Métodos Auxiliares (Helpers) +# ========================================= + +def create_enrollment_for_current_user(nome_turma) + docente = Usuario.find_by(ocupacao: :docente) || Usuario.create!( + nome: "Docente", email: "doc@test.com", matricula: "D01", + usuario: "doc", password: "password", ocupacao: :docente, status: true + ) + + materia = Materia.find_or_create_by!(nome: nome_turma) do |m| + m.codigo = "MAT#{rand(100..999)}" + end + + turma = Turma.find_or_create_by!(materia: materia) do |t| + t.codigo = "T1" + t.semestre = "2025.1" + t.docente = docente + t.horario = "10h" + end + + Matricula.find_or_create_by!(usuario: @user, turma: turma) +end + +def create_forms_for_class(nome_turma, form_titles) + materia = Materia.find_by(nome: nome_turma) + turma = Turma.find_by(materia: materia) + docente = turma.docente + + form_titles.each do |titulo| + template = Template.create!( + name: titulo, + titulo: titulo, + id_criador: docente.id, + participantes: 'todos', + hidden: false + ) + + form = Formulario.create!( + template: template, + turma: turma, + titulo_envio: titulo, + data_criacao: Time.current, + data_encerramento: 30.days.from_now + ) + + Resposta.create!(formulario: form, participante: @user, data_submissao: nil) + end +end + +def mark_specific_form_as_answered(titulo_formulario) + formulario = Formulario.joins(:template).find_by(templates: { titulo: titulo_formulario }) + resposta = Resposta.find_by(formulario: formulario, participante: @user) + + resposta.update!(data_submissao: Time.current) +end + +def mark_all_pending_responses_as_answered + @user.respostas.where(data_submissao: nil).update_all(data_submissao: Time.current) +end + +def clear_student_enrollments + @user.matriculas.destroy_all +end \ No newline at end of file diff --git a/src/features/step_definitions/visualizar_result_steps.rb b/src/features/step_definitions/visualizar_result_steps.rb new file mode 100644 index 0000000000..efb487bcf2 --- /dev/null +++ b/src/features/step_definitions/visualizar_result_steps.rb @@ -0,0 +1,179 @@ +# ========================================= +# Contexto (Dado) +# ========================================= + +# Loga usuário com perfil. +# +# Argumentos: +# - perfil (String): Role. +# +# Efeitos Colaterais: +# - Invoca step de login existente (cadastrar_usuarios_steps ou similar). +Dado('eu sou um {string} logado no sistema') do |perfil| + step "que eu sou um '#{perfil}' logado sistema" rescue step "que eu sou um '#{perfil}' logado como '#{perfil}'" +end + +# Cria formulários vinculados. +# +# Argumentos: +# - nome_form1, nome_form2 (String): Títulos. +# +# Efeitos Colaterais: +# - Cria estruturas complexas de Turma/Form/Template. +Dado('existem os formulários {string} e {string}') do |nome_form1, nome_form2| + contexto = setup_result_view_context + + [nome_form1, nome_form2].each do |titulo| + create_linked_form_and_template(titulo, contexto) + end +end + +# Cria um único formulário. +# +# Argumentos: +# - titulo (String): Título. +# +# Efeitos Colaterais: +# - Persiste Formulario. +Dado(/^(?:que )?existe o formulário "([^"]*)"$/) do |titulo| + create_single_form_scenario(titulo) +end + +# Adiciona respostas mock ao form alvo. +# +# Argumentos: +# - qtd (Integer): Quantidade. +# +# Efeitos Colaterais: +# - Cria usuarios e respostas. +Dado('ele possui {int} respostas') do |qtd| + create_responses_for_target_form(qtd) +end + +# Limpa formulários. +Dado(/^(?:que )?não existe nenhum formulário cadastrado$/) do + Formulario.destroy_all +end + +# ========================================= +# Ações (Quando) +# ========================================= + +# Step typo "clicoo". +# +# Argumentos: +# - botao (String): Texto. +Quando('eu clicoo no botão {string}') do |botao| + click_on botao +end + +# ========================================= +# Verificações (Então) +# ========================================= + +# Verifica texto genérico. +# +# Argumentos: +# - texto (String): Conteúdo. +Então('eu devo ver {string}') do |texto| + expect(page).to have_content(texto) +end + +# Alias typo "mensaagem". +Então('eu devo ver a mensaagem {string}') do |msg| + expect(page).to have_content(msg) +end + +# Verifica botão. +# +# Argumentos: +# - botao (String): Texto. +Então('eu devo ver um botão {string}') do |botao| + verify_button_presence(botao) +end + +# Verifica ausência de botão. +# +# Argumentos: +# - botao (String): Texto. +Então('eu não devo ver o botão {string}') do |botao| + expect(page).not_to have_link(botao) +end + +# Verifica header de download. +# +# Argumentos: +# - arquivo (String): Nome esperado (ignorado na lógica simples). +Então('o download do arquivo {string} deve iniciar') do |arquivo| + expect(page.response_headers['Content-Disposition']).to include("attachment") +end + +# ========================================= +# Métodos Auxiliares (Helpers) +# ========================================= + +# Cria form único simples. +# +# Argumentos: +# - titulo (String): Título. +def create_single_form_scenario(titulo) + turma = Turma.first || create_default_turma_structure + + template = Template.create!( + titulo: 'T', + participantes: 'alunos', + criador: turma.docente, + name: 'T' + ) + + @form_target = Formulario.create!( + titulo_envio: titulo, + data_criacao: Time.now, + template: template, + turma: turma + ) +end + +# Cria estrutura básica de turma. +# +# Retorno: +# - (Turma): Turma criada. +def create_default_turma_structure + materia = Materia.create!(nome: 'Materia Teste', codigo: 'MT') + docente = Usuario.create!( + nome: 'Doc', email: 'd@t.com', usuario: 'doc', + password: 'p', ocupacao: :docente, status: true, matricula: 'D1' + ) + + Turma.create!( + codigo: 'T1', semestre: '2025.1', horario: '10h', + materia: materia, docente: docente + ) +end + +# Cria respostas mock. +# +# Argumentos: +# - qtd (Integer): Quantidade. +def create_responses_for_target_form(qtd) + qtd.times do |i| + u = Usuario.create!( + nome: "User#{i}", email: "u#{i}@t.com", usuario: "u#{i}", + password: 'p', ocupacao: :discente, status: true, matricula: "M#{i}" + ) + Resposta.create!( + formulario: @form_target, + participante: u, + data_submissao: Time.now + ) + end +end + +# Verifica presença de botão. +# +# Argumentos: +# - botao (String): Texto. +def verify_button_presence(botao) + botao_nome = (botao == "Baixar CSV") ? "Exportar para CSV" : botao + expect(page).to have_link(botao_nome) +end \ No newline at end of file diff --git a/src/features/support/auth_helpers.rb b/src/features/support/auth_helpers.rb new file mode 100644 index 0000000000..2e101e26f9 --- /dev/null +++ b/src/features/support/auth_helpers.rb @@ -0,0 +1,135 @@ +# --- Mapeamento de Papéis --- + +# Resolve o papel do usuário para o enum de ocupação. +# +# Argumentos: +# - role_name (String): Nome do papel (ex: 'participante', 'professor'). +# +# Retorno: +# - (Symbol): Símbolo correspondente à ocupação (:discente, :docente, :admin). +def resolve_occupation_from_role(role_name) + map = { + 'participante' => :discente, + 'aluno' => :discente, + 'professor' => :docente, + 'admin' => :admin + } + + map[role_name.downcase] || role_name.downcase.to_sym +end + +# --- Persistência de Usuário --- + +# Encontra ou cria um usuário de teste para autenticação. +# +# Argumentos: +# - role_name (String): Nome base para email e identificação. +# - occupation (Symbol): Ocupação do usuário. +# +# Retorno: +# - (Usuario): Objeto Usuário persistido. +def find_or_create_auth_user(role_name, occupation) + email = "#{role_name}@test.com" + + Usuario.find_by(email: email) || create_test_user(role_name, email, occupation) +end + +# Cria um usuário de teste. +# +# Argumentos: +# - name_base (String): Base para o nome e usuário. +# - email (String): Email do usuário. +# - occupation (Symbol): Ocupação. +# +# Retorno: +# - (Usuario): Novo usuário criado. +def create_test_user(name_base, email, occupation) + Usuario.create!( + nome: name_base.capitalize, + email: email, + matricula: "99#{rand(1000..9999)}", + usuario: name_base, + password: 'password', + password_confirmation: 'password', + ocupacao: occupation, + status: true + ) +end + +# --- Ações de UI --- + +# Realiza login na interface web. +# +# Argumentos: +# - email (String): Email do usuário. +# - password (String): Senha do usuário. +# +# Efeitos Colaterais: +# - Visita /login e submete o formulário. +def perform_ui_login(email, password) + visit '/login' + + fill_in 'Usuário', with: email + fill_in 'Senha', with: password + + click_on 'Entrar' +end + +# Verifica se o login foi bem sucedido. +# +# Efeitos Colaterais: +# - Dispara erro se o botão 'Entrar' ainda estiver visível. +def verify_login_success + # Verifica que o botão de entrar sumiu (indicando sessão ativa) + expect(page).to have_no_content("Entrar") +end + +# --- Mapeamento de Papéis --- + +# Resolve a ocupação para respondentes. +# +# Argumentos: +# - role (String): Papel descrito. +# +# Retorno: +# - (Symbol): Ocupação correspondente. +def resolve_responder_occupation(role) + # Lógica original: 'participante' vira :discente, o resto vira symbol direto + return :discente if role.downcase == 'participante' + + role.downcase.to_sym +end + +# --- Persistência de Usuário --- + +# Encontra ou cria um usuário respondente. +# +# Argumentos: +# - username (String): Nome de usuário. +# - occupation (Symbol): Ocupação. +# +# Retorno: +# - (Usuario): Usuário encontrado ou criado. +def find_or_create_responder_user(username, occupation) + Usuario.find_by(usuario: username) || create_responder_user(username, occupation) +end + +# Cria usuário respondente específico. +# +# Argumentos: +# - username (String): Nome de usuário. +# - occupation (Symbol): Ocupação. +# +# Retorno: +# - (Usuario): Novo usuário. +def create_responder_user(username, occupation) + Usuario.create!( + nome: username.capitalize, + email: "#{username}@test.com", + matricula: "2021#{rand(1000..9999)}", + usuario: username, + password: 'password', + ocupacao: occupation, + status: true + ) +end \ No newline at end of file diff --git a/src/features/support/download_helpers.rb b/src/features/support/download_helpers.rb new file mode 100644 index 0000000000..2e03c09c9e --- /dev/null +++ b/src/features/support/download_helpers.rb @@ -0,0 +1,55 @@ +# Verifica se o download do CSV ocorreu corretamente. +# +# Argumentos: +# - filename (String): Nome esperado do arquivo. +# +# Efeitos Colaterais: +# - Lê headers da resposta e realiza asserções. +def verify_csv_download_response(filename) + # Captura headers uma vez + headers = page.response_headers + + # Delega as validações + validate_csv_content_type(headers) + validate_attachment_disposition(headers, filename) +end + +# --- Métodos Auxiliares --- + +# Valida o Content-Type como texto/csv. +# +# Argumentos: +# - headers (Hash): Headers da resposta. +def validate_csv_content_type(headers) + expect(headers['Content-Type']).to include('text/csv') +end + +# Valida o Content-Disposition para anexo e nome de arquivo. +# +# Argumentos: +# - headers (Hash): Headers da resposta. +# - filename (String): Nome esperado. +def validate_attachment_disposition(headers, filename) + disposition = headers['Content-Disposition'] + + # Verifica se é um anexo + expect(disposition).to include("attachment") + + # Verifica se o nome do arquivo está correto + expect(disposition).to include(filename) +end + +# Verifica que nenhum download ocorreu (continua HTML). +# +# Efeitos Colaterais: +# - Lê headers da resposta e realiza asserções. +def verify_no_file_download_occurred + # Captura os headers apenas uma vez + headers = page.response_headers + + # Verifica se o conteúdo continua sendo uma página web (HTML) + expect(headers['Content-Type']).to include('text/html') + + # Verifica se não há instrução de anexo/download + expect(headers['Content-Disposition']).to be_nil +end \ No newline at end of file diff --git a/src/features/support/env.rb b/src/features/support/env.rb new file mode 100644 index 0000000000..3b97d14087 --- /dev/null +++ b/src/features/support/env.rb @@ -0,0 +1,53 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + + +require 'cucumber/rails' + +# By default, any exception happening in your Rails application will bubble up +# to Cucumber so that your scenario will fail. This is a different from how +# your application behaves in the production environment, where an error page will +# be rendered instead. +# +# Sometimes we want to override this default behaviour and allow Rails to rescue +# exceptions and display an error page (just like when the app is running in production). +# Typical scenarios where you want to do this is when you test your error pages. +# There are two ways to allow Rails to rescue exceptions: +# +# 1) Tag your scenario (or feature) with @allow-rescue +# +# 2) Set the value below to true. Beware that doing this globally is not +# recommended as it will mask a lot of errors for you! +# +ActionController::Base.allow_rescue = false + +# Remove/comment out the lines below if your app doesn't have a database. +# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead. +begin + DatabaseCleaner.strategy = :transaction +rescue NameError + raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it." +end + +# You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios. +# See the DatabaseCleaner documentation for details. Example: +# +# Before('@no-txn,@selenium,@culerity,@celerity,@javascript') do +# # { except: [:widgets] } may not do what you expect here +# # as Cucumber::Rails::Database.javascript_strategy overrides +# # this setting. +# DatabaseCleaner.strategy = :truncation +# end +# +# Before('not @no-txn', 'not @selenium', 'not @culerity', 'not @celerity', 'not @javascript') do +# DatabaseCleaner.strategy = :transaction +# end +# + +# Possible values are :truncation and :transaction +# The :transaction strategy is faster, but might give you threading problems. +# See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature +Cucumber::Rails::Database.javascript_strategy = :truncation diff --git a/src/features/support/factory_helpers.rb b/src/features/support/factory_helpers.rb new file mode 100644 index 0000000000..666081ca38 --- /dev/null +++ b/src/features/support/factory_helpers.rb @@ -0,0 +1,409 @@ +# --- Helpers de Contexto (Step 1) --- + +# Encontra ou cria dependências básicas para formulários (docente, matéria, turma, template). +# +# Retorno: +# - (Hash): { docente, turma, template }. +# +# Efeitos Colaterais: +# - Cria registros no banco. +def find_or_create_form_dependencies + docente = Usuario.find_by(ocupacao: :docente) || create_docente_padrao + materia = Materia.find_or_create_by!(nome: "Engenharia de Software", codigo: "ES01") + turma = find_or_create_turma_padrao(docente, materia) + template = find_or_create_template_padrao(docente) + + { docente: docente, turma: turma, template: template } +end + +# Cria um formulário vinculado ao contexto fornecido. +# +# Argumentos: +# - titulo (String): Título do formulário. +# - contexto (Hash): Dependências (template, turma). +# +# Retorno: +# - (Formulario): Formulário criado. +def create_formulario_relatorio(titulo, contexto) + Formulario.create!( + template: contexto[:template], + turma: contexto[:turma], + titulo_envio: titulo, + data_criacao: Time.current, + data_encerramento: 30.days.from_now + ) +end + +# --- Builders de Entidades (Step 1) --- + +# Cria um docente padrão para testes de relatório. +# +# Retorno: +# - (Usuario): Docente criado. +def create_docente_padrao + Usuario.create!( + nome: "Docente Relatorio", email: "doc_rel@test.com", matricula: "DR01", + usuario: "doc_rel", password: "password", ocupacao: :docente, status: true + ) +end + +# Encontra ou cria turma padrão. +# +# Argumentos: +# - docente (Usuario): Docente responsável. +# - materia (Materia): Matéria vinculada. +# +# Retorno: +# - (Turma): Turma criada ou encontrada. +def find_or_create_turma_padrao(docente, materia) + Turma.find_or_create_by!(codigo: "TA", materia: materia) do |t| + t.semestre = "2025.1" + t.docente = docente + t.horario = "24M12" + end +end + +# Encontra ou cria template padrão. +# +# Argumentos: +# - docente (Usuario): Criador do template. +# +# Retorno: +# - (Template): Template criado ou encontrado. +def find_or_create_template_padrao(docente) + Template.find_or_create_by!(titulo: "Template Padrão") do |t| + t.name = "Template Padrão" + t.id_criador = docente.id + t.participantes = "todos" + end +end + +# --- Helpers de Submissão (Step 2) --- + +# Cria submissão de estudante incluindo usuário, matrícula e resposta. +# +# Argumentos: +# - form (Formulario): Formulário alvo. +# - index (Integer): Índice para unicidade. +# +# Efeitos Colaterais: +# - Cria Usuario, Matricula e Resposta. +def create_student_submission(form, index) + # Cria aluno único + aluno = create_unique_student(index) + + # Matricula na turma do formulário + Matricula.create!(usuario: aluno, turma: form.turma) + + # Cria a resposta + Resposta.create!( + formulario: form, + participante: aluno, + data_submissao: Time.current + ) +end + +# Cria estudante com dados únicos. +# +# Argumentos: +# - index (Integer): Índice para unicidade. +# +# Retorno: +# - (Usuario): Estudante criado. +def create_unique_student(index) + unique_id = "#{index}_#{Time.now.to_i}" + Usuario.create!( + nome: "Aluno Relatorio #{index}", + email: "aluno_rel_#{unique_id}@test.com", + matricula: "2025#{unique_id}", + usuario: "aluno_rel_#{unique_id}", + password: "password", + ocupacao: :discente, + status: true + ) +end + +# --- Infraestrutura Acadêmica --- + +# Configura estrutura acadêmica completa (matéria, docente, turma). +# +# Argumentos: +# - nome_turma (String): Nome da matéria. +# +# Efeitos Colaterais: +# - Persistência múltipla. +def setup_academic_structure(nome_turma) + materia = find_or_create_materia(nome_turma) + docente = find_or_create_docente + find_or_create_turma(materia, docente) +end + +# Encontra ou cria matéria pelo nome. +# +# Argumentos: +# - nome (String): Nome da matéria. +# +# Retorno: +# - (Materia): Objeto persistido. +def find_or_create_materia(nome) + Materia.find_or_create_by!(nome: nome) do |m| + m.codigo = "MAT#{rand(1000..9999)}" + end +end + +# Encontra ou cria docente aleatório. +# +# Retorno: +# - (Usuario): Docente. +def find_or_create_docente + Usuario.find_by(ocupacao: :docente) || Usuario.create!( + nome: "Docente #{rand(999)}", + email: "doc#{rand(999)}@test.com", + matricula: "DOC#{rand(999)}", + usuario: "doc#{rand(999)}", + password: 'password', + ocupacao: :docente, + status: true + ) +end + +# Encontra ou cria turma para matéria e docente dados. +# +# Argumentos: +# - materia (Materia): Objeto Matéria. +# - docente (Usuario): Objeto Docente. +# +# Retorno: +# - (Turma): Turma criada ou encontrada. +def find_or_create_turma(materia, docente) + Turma.find_or_create_by!(materia: materia) do |t| + t.codigo = "T#{rand(100..999)}" + t.semestre = '2024.1' + t.horario = '35T' + t.docente = docente + end +end + +# --- Matrícula de Alunos --- + +# Matricula lote de alunos em uma turma. +# +# Argumentos: +# - turma (Turma): Turma alvo. +# - quantidade (Integer): Quantidade de alunos. +# +# Efeitos Colaterais: +# - Cria N usuários e N matrículas. +def enroll_batch_students(turma, quantidade) + quantidade.times do |i| + create_and_enroll_single_student(turma, i) + end +end + +# Cria e matricula um único aluno. +# +# Argumentos: +# - turma (Turma): Turma alvo. +# - index (Integer): Índice único. +# +# Retorno: +# - (Matricula): Objeto matrícula criado. +def create_and_enroll_single_student(turma, index) + # Cria o aluno com dados únicos baseados no índice e ID da turma + aluno = Usuario.create!( + nome: "Aluno #{index} da #{turma.materia.nome}", + email: "aluno#{index}_#{turma.id}_#{rand(9999)}@test.com", + matricula: "2024#{turma.id}#{index}", + usuario: "user#{turma.id}#{index}", + password: 'password', + ocupacao: :discente, + status: true + ) + + # Cria a associação + Matricula.find_or_create_by!(usuario: aluno, turma: turma) +end + +# --- Orquestrador de Criação --- + +# Configura estrutura similar ao SIGAA. +# +# Argumentos: +# - full_class_name (String): Nome completo (Matéria - Turma). +# - docente (Usuario): Professor responsável. +# +# Efeitos Colaterais: +# - Cria Matéria e Turma. +def create_sigaa_class_structure(full_class_name, docente) + # Separa o nome da matéria do código da turma + nome_materia, codigo_turma = parse_class_name_string(full_class_name) + + # Cria ou encontra os registros no banco + materia = find_or_create_materia_sigaa(nome_materia) + find_or_create_turma_sigaa(materia, codigo_turma, docente) +end + +# --- Parser de String (Lógica Pura) --- + +# Analisa string de nome de turma. +# +# Argumentos: +# - full_string (String): String contendo "Nome - Código". +# +# Retorno: +# - (Array): [Nome da Matéria, Código da Turma]. +def parse_class_name_string(full_string) + if full_string.include?(' - ') + full_string.split(' - ') + else + # Retorna o nome original e um código padrão se não houver separador + [full_string, "A"] + end +end + +# --- Persistência (Banco de Dados) --- + +# Encontra ou cria docente padrão. +# +# Retorno: +# - (Usuario): Docente padrão. +def find_or_create_default_teacher + Usuario.find_by(ocupacao: :docente) || Usuario.create!( + nome: "Docente Padrão", + email: "docente@camaar.unb.br", + matricula: "DOC123", + usuario: "docente", + password: "password", + ocupacao: :docente, + status: true + ) +end + +# Encontra ou cria matéria estilo SIGAA. +# +# Argumentos: +# - nome (String): Nome da matéria. +# +# Retorno: +# - (Materia): Matéria criada. +def find_or_create_materia_sigaa(nome) + Materia.find_or_create_by!(nome: nome) do |m| + # Gera um código fictício baseado nas primeiras letras + m.codigo = nome[0..3].upcase + end +end + +# Encontra ou cria turma estilo SIGAA. +# +# Argumentos: +# - materia (Materia): Matéria pai. +# - codigo_turma (String): Código da turma. +# - docente (Usuario): Docente. +# +# Retorno: +# - (Turma): Turma criada. +def find_or_create_turma_sigaa(materia, codigo_turma, docente) + Turma.find_or_create_by!(codigo: codigo_turma, materia: materia) do |t| + t.semestre = "2025.1" + t.horario = "24M34" + t.docente = docente + end +end + +# --- Contexto de Infraestrutura --- + +# Configura contexto para views de resultados. +# +# Retorno: +# - (Hash): { docente, turma }. +def setup_result_view_context + docente = find_or_create_result_docente + materia = Materia.find_or_create_by!(nome: "Materia Teste", codigo: "MAT01") + turma = find_or_create_result_turma(materia, docente) + + # Retorna um hash para facilitar o acesso nos próximos métodos + { docente: docente, turma: turma } +end + +# Encontra ou cria docente para teste de resultado. +# +# Retorno: +# - (Usuario): Docente. +def find_or_create_result_docente + Usuario.find_by(ocupacao: :docente) || Usuario.create!( + nome: "Prof. Teste", + email: "prof@teste.com", + matricula: "12345", + usuario: "prof123", + password: "password", + ocupacao: :docente, + status: true + ) +end + +# Encontra ou cria turma para teste de resultado. +# +# Argumentos: +# - materia (Materia): Matéria pai. +# - docente (Usuario): Docente. +# +# Retorno: +# - (Turma): Turma. +def find_or_create_result_turma(materia, docente) + Turma.find_or_create_by!(codigo: "T1", materia: materia) do |t| + t.semestre = "2025.1" + t.docente = docente + t.horario = "10h" + end +end + +# --- Criação de Formulários --- + +# Cria formulário e template vinculados. +# +# Argumentos: +# - titulo (String): Título. +# - context (Hash): Contexto com docente e turma. +def create_linked_form_and_template(titulo, context) + # Cria o template vinculado ao docente + template = create_result_template(titulo, context[:docente]) + + # Cria o formulário vinculado ao template e à turma + create_result_formulario(titulo, template, context[:turma]) +end + +# Cria template para resultado. +# +# Argumentos: +# - titulo (String): Título. +# - docente (Usuario): Criador. +# +# Retorno: +# - (Template): Template criado. +def create_result_template(titulo, docente) + Template.create!( + name: titulo, + titulo: titulo, + participantes: "todos", + id_criador: docente.id + ) +end + +# Cria formulário para resultado. +# +# Argumentos: +# - titulo (String): Título. +# - template (Template): Template pai. +# - turma (Turma): Turma alvo. +# +# Retorno: +# - (Formulario): Formulário criado. +def create_result_formulario(titulo, template, turma) + Formulario.create!( + template: template, + turma: turma, + titulo_envio: titulo, + data_criacao: Time.current, + data_encerramento: 30.days.from_now + ) +end \ No newline at end of file diff --git a/src/features/support/rspec_mocks.rb b/src/features/support/rspec_mocks.rb new file mode 100644 index 0000000000..850a572375 --- /dev/null +++ b/src/features/support/rspec_mocks.rb @@ -0,0 +1,15 @@ +require 'rspec/mocks' + +World(RSpec::Mocks::ExampleMethods) + +Before do + RSpec::Mocks.setup +end + +After do + begin + RSpec::Mocks.verify + ensure + RSpec::Mocks.teardown + end +end \ No newline at end of file diff --git a/src/features/support/sigaa_helpers.rb b/src/features/support/sigaa_helpers.rb new file mode 100644 index 0000000000..fbe0c668d9 --- /dev/null +++ b/src/features/support/sigaa_helpers.rb @@ -0,0 +1,579 @@ +# --- Helpers para Mock de Dados Externos --- + +# Encontra ou cria estrutura de turma baseada na matrícula. +# +# Argumentos: +# - matricula (String): Matrícula para busca. +# +# Retorno: +# - (Hash): Dados da turma mockada. +def find_or_create_mock_class_for(matricula) + # Tenta achar turma onde o aluno já está + existing_class = @fake_members.find { |m| m["dicente"]&.any? { |d| d["matricula"].to_s == matricula } } + return existing_class if existing_class + + # Se não achar, usa ou cria a turma padrão + default_class = @fake_members.find { |m| m["code"] == "CIC0097" } || build_default_member_structure + + @fake_members << default_class unless @fake_members.include?(default_class) + default_class +end + +# Garante a existência da definição de uma classe no mock. +# +# Argumentos: +# - code (String): Código da disciplina. +# +# Efeitos Colaterais: +# - Adiciona entrada em @fake_classes. +def ensure_class_definition_exists(code) + return if @fake_classes.any? { |c| c["code"] == code } + + @fake_classes << { + "name" => "Matéria Mock", + "code" => code, + "class" => { "semester" => "2024.1", "time" => "35T23", "classCode" => "TA" } + } +end + +# Atualiza email do aluno no mock. +# +# Argumentos: +# - turma_mock (Hash): Estrutura da turma. +# - matricula (String): Matrícula alvo. +# - email (String): Novo email. +# +# Efeitos Colaterais: +# - Modifica a lista de discentes da turma. +def update_mock_student_email(turma_mock, matricula, email) + # Remove ocorrências antigas para evitar duplicidade + @fake_members.each do |t| + t["dicente"]&.reject! { |d| d["matricula"].to_s == matricula } + end + + # Adiciona o dado atualizado + turma_mock["dicente"] << build_student_structure(matricula, email) +end + +# --- Construtores de Estrutura de Dados (Factory Methods) --- + +# Constrói estrutura padrão de membro. +# +# Retorno: +# - (Hash): Estrutura mock. +def build_default_member_structure + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2024.1", + "dicente" => [], + "docente" => { "nome" => "Prof Mock", "usuario" => "999", "email" => "mock@email" } + } +end + +# Constrói estrutura de dados de aluno. +# +# Argumentos: +# - matricula (String): Matrícula. +# - email (String): Email. +# +# Retorno: +# - (Hash): Dados do aluno. +def build_student_structure(matricula, email) + { + "nome" => "Nome Genérico", + "matricula" => matricula, + "usuario" => matricula, + "email" => email, + "ocupacao" => "dicente" + } +end + +# Verifica se o botão de editar templates está ativo. +# +# Efeitos Colaterais: +# - Realiza asserções de UI. +def verify_edit_button_active + # Verifica especificamente o botão de formulário (button_to) + botao = find_button("Editar Templates") + expect(botao).not_to be_disabled + expect(botao[:class]).to include("bg-green-500") +end + +# Verifica links de navegação. +# +# Efeitos Colaterais: +# - Realiza asserções de UI. +def verify_navigation_links_active + # Verifica os elementos de navegação (link_to) + links = ["Enviar Formularios", "Resultados"] + + links.each do |texto| + link = find_link(texto) + expect(link).to be_present + expect(link[:class]).to include("bg-green-500") + end +end + +# Atualiza o nome do aluno no mock de membros +# +# Argumentos: +# - matricula (String): Matrícula do aluno. +# - novo_nome (String): Novo nome a ser definido. +def update_mock_student_name(matricula, novo_nome) + # Procura o aluno em @fake_members. Se não achar, cria um padrão (reuso da lógica anterior) + turma_mock = find_or_create_mock_class_for(matricula) + + # Encontra o registro do aluno dentro da lista de discentes + aluno = turma_mock["dicente"]&.find { |d| d["matricula"].to_s == matricula.to_s } + + if aluno + # Se o aluno já existe, apenas atualiza o nome + aluno["nome"] = novo_nome + else + # Se o aluno não existe na lista, cria um novo registro com o nome atualizado + # (Reaproveita o builder do passo anterior, alterando o nome) + novo_aluno = build_student_structure(matricula, "email@padrao.com") + novo_aluno["nome"] = novo_nome + turma_mock["dicente"] << novo_aluno + end +end + +# Garante que um membro padrão existe na lista mockada. +# +# Retorno: +# - (Hash): Dados da turma. +def ensure_default_class_member_exists + code = "CIC0097" + class_code = "TA" + + # Tenta encontrar a turma existente + turma = @fake_members.find { |m| m["code"] == code && m["classCode"] == class_code } + return turma if turma + + # Cria a turma padrão se não encontrar + new_turma = { + "code" => code, + "classCode" => class_code, + "semester" => "2024.1", + "dicente" => [], + "docente" => { "nome" => "Prof Mock", "usuario" => "999", "email" => "mock@email" } + } + + @fake_members << new_turma + new_turma +end + +# Atualiza ou insere aluno com novo nome. +# +# Argumentos: +# - turma_mock (Hash): Turma alvo. +# - matricula (String): Matrícula. +# - novo_nome (String): Nome atualizado. +def upsert_student_with_name(turma_mock, matricula, novo_nome) + matricula_str = matricula.to_s + + # Remove qualquer registro anterior dessa matrícula para evitar duplicação + turma_mock["dicente"].reject! { |d| d["matricula"].to_s == matricula_str } + + # Adiciona o registro atualizado + turma_mock["dicente"] << { + "nome" => novo_nome, + "matricula" => matricula_str, + "usuario" => matricula_str, + "email" => "#{matricula_str}@aluno.unb.br", # Mantém a lógica original do seu snippet + "ocupacao" => "dicente" + } +end + +# Resolve contexto atual da classe. +# +# Retorno: +# - (Hash): { code, class_code }. +def resolve_current_class_context + last_class = @fake_classes.last + { + code: last_class["code"], + class_code: last_class["class"]["classCode"] + } +end + +# --- Infraestrutura (Membros) --- + +# Encontra ou cria registro de membro. +# +# Argumentos: +# - code (String): Código da disciplina. +# - class_code (String): Código da turma. +# +# Retorno: +# - (Hash): Dados do membro. +def find_or_create_member_record(code, class_code) + record = @fake_members.find { |m| m["code"] == code && m["classCode"] == class_code } + return record if record + + create_default_member_record(code, class_code) +end + +# Cria registro padrão de membro. +# +# Argumentos: +# - code (String): Disciplina. +# - class_code (String): Turma. +# +# Retorno: +# - (Hash): Dados criados. +def create_default_member_record(code, class_code) + new_record = { + "code" => code, + "classCode" => class_code, + "semester" => "2024.1", + "dicente" => [], + "docente" => default_teacher_structure + } + @fake_members << new_record + new_record +end + +# Estrutura padrão de docente. +# +# Retorno: +# - (Hash): Dados do docente. +def default_teacher_structure + { + "nome" => "Professor Mock", + "usuario" => "99999", + "email" => "prof@mock.com", + "ocupacao" => "docente" + } +end + +# --- Ação (Adicionar Aluno) --- + +# Adiciona aluno ao registro de membro. +# +# Argumentos: +# - record (Hash): Registro alvo. +# - nome (String): Nome do aluno. +# - matricula (String): Matrícula. +def add_student_to_member_record(record, nome, matricula) + # Evita duplicatas removendo registro anterior se existir + record["dicente"].reject! { |d| d["matricula"] == matricula } + + # Adiciona o novo + record["dicente"] << build_student_hash(nome, matricula) +end + +# Constrói hash de aluno. +# +# Retorno: +# - (Hash): Dados do aluno. +def build_student_hash(nome, matricula) + { + "nome" => nome, + "matricula" => matricula, + "usuario" => matricula, + "email" => "#{matricula}@aluno.unb.br", + "ocupacao" => "dicente" # Mantendo a grafia 'dicente' do seu código original + } +end + +# Captura contagens iniciais do banco. +# +# Efeitos Colaterais: +# - Define variáveis de instância @quantidade_inicial_*. +def capture_initial_database_counts + @quantidade_inicial_turmas = Turma.count + @quantidade_inicial_usuarios = Usuario.count +end + +# Configura mock de sistema de arquivos. +# +# Efeitos Colaterais: +# - Mocka File.read para retornar JSONs falsos. +def setup_file_system_mocking + # Usa and_wrap_original para interceptar apenas os arquivos que queremos + allow(File).to receive(:read).and_wrap_original do |original_method, *args| + handle_file_read_interception(original_method, args) + end +end + +# Interceptador de leitura de arquivo. +# +# Argumentos: +# - original_method (Method): Método original File.read. +# - args (Array): Argumentos da chamada. +# +# Retorno: +# - (String): Conteúdo JSON ou resultado original. +def handle_file_read_interception(original_method, args) + # 1. Simulação de Erro (se flag estiver ativa) + raise Errno::ENOENT if @simular_erro_arquivo + + path = args.first.to_s + + # 2. Retorna JSON mockado dependendo do caminho do arquivo + if path.include?('classes.json') + @fake_classes.to_json + elsif path.include?('class_members.json') + @fake_members.to_json + else + # 3. Se não for um dos nossos arquivos, deixa o Ruby ler normalmente + original_method.call(*args) + end +end + +# --- Manipulação de @fake_classes --- + +# Garante definição de classe importada. +# +# Argumentos: +# - codigo_materia (String): Código da matéria. +# - codigo_turma (String): Código da turma. +def ensure_imported_class_definition(codigo_materia, codigo_turma) + return if @fake_classes.any? { |c| c["code"] == codigo_materia } + + @fake_classes << { + "name" => "Matéria Importada", + "code" => codigo_materia, + "class" => { "semester" => "2024.1", "time" => "35T23", "classCode" => codigo_turma } + } +end + +# --- Manipulação de @fake_members --- + +# Cria classe com aluno se faltar. +# +# Argumentos: +# - codigo_materia (String): Disciplina. +# - codigo_turma (String): Turma. +# - matricula (String): Aluno. +def create_class_with_student_if_missing(codigo_materia, codigo_turma, matricula) + # Verifica se a turma já existe + exists = @fake_members.any? { |m| m["code"] == codigo_materia && m["classCode"] == codigo_turma } + return if exists # Mantém a lógica do 'unless turma_mock' original + + # Constrói a nova estrutura + new_turma = build_import_member_structure(codigo_materia, codigo_turma) + + # Adiciona o aluno à estrutura criada + new_turma["dicente"] << build_imported_student_hash(matricula) + + # Persiste na lista mockada + @fake_members << new_turma +end + +# --- Builders (Fábricas de Hash) --- + +# Estrutura base de membro. +# +# Retorno: +# - (Hash): Dados da turma. +def build_import_member_structure(code, class_code) + { + "code" => code, + "classCode" => class_code, + "semester" => "2024.1", + "dicente" => [], + "docente" => { "nome" => "Prof Mock", "usuario" => "999", "email" => "mock@email" } + } +end + +# Estrutura base de aluno importado. +# +# Retorno: +# - (Hash): Dados do aluno. +def build_imported_student_hash(matricula) + matricula_str = matricula.to_s + { + "nome" => "Aluno Importado", + "matricula" => matricula_str, + "usuario" => matricula_str, + "email" => "#{matricula_str}@aluno.unb.br", + "ocupacao" => "dicente" + } +end + +# Remove dados mockados de classe. +# +# Argumentos: +# - code (String): Código da matéria. +def remove_mock_class_data(code) + # Remove a definição da classe + @fake_classes.reject! { |c| c["code"] == code } + + # Remove a lista de membros associada àquela matéria + @fake_members.reject! { |m| m["code"] == code } +end + +# Remove dados de aluno de todos os pontos. +# +# Argumentos: +# - matricula (String): Matrícula. +def remove_mock_student_data(matricula) + id_str = matricula.to_s + + # Itera sobre todas as turmas para remover o aluno de onde ele estiver + @fake_members.each do |turma| + # Usa safe navigation (&.) para evitar erro caso a chave 'dicente' não exista + turma["dicente"]&.reject! { |d| d["matricula"].to_s == id_str } + end +end + +# Verifica consistência de matrículas. +# +# Argumentos: +# - matricula (String): Aluno. +# - codigo_turma (String): Turma. +# - codigo_materia (String): Matéria. +def verify_enrollment_consistency(matricula, codigo_turma, codigo_materia) + # 1. Busca os registros (Assignments) + user = Usuario.find_by(matricula: matricula) + turma = find_turma_by_full_code(codigo_turma, codigo_materia) + + # 2. Executa as validações (Conditionals/Calls) + validate_enrollment_expectations(user, turma) +end + +# --- Helpers de Busca --- + +# Encontra turma por código completo. +# +# Retorno: +# - (Turma): Turma encontrada. +def find_turma_by_full_code(codigo_turma, codigo_materia) + # Usa joins para buscar a turma garantindo que pertence à matéria correta + Turma.joins(:materia).find_by( + codigo: codigo_turma, + materias: { codigo: codigo_materia } + ) +end + +# --- Helpers de Asserção --- + +# Valida expectativas de matrícula. +# +# Efeitos Colaterais: +# - Realiza asserções RSpec. +def validate_enrollment_expectations(user, turma) + expect(user).to be_present, "Usuário não encontrado." + expect(turma).to be_present, "Turma não encontrada." + + # Verifica a associação + expect(user.turmas).to include(turma) +end + +# --- Orquestrador de Contexto --- + +# Configura contexto SIGAA fake. +# +# Retorno: +# - (Hash): Turma mockada. +def setup_sigaa_context + codigo_materia = "CIC0097" + codigo_turma = "TA" + + # Garante que a classe existe em @fake_classes + ensure_class_exists(codigo_materia, codigo_turma) + + # Garante que a turma existe em @fake_members e retorna ela + find_or_create_turma_member(codigo_materia, codigo_turma) +end + +# --- Helpers de Infraestrutura --- + +# Garante existência da classe no array fake. +# +# Efeitos Colaterais: +# - Modifica @fake_classes. +def ensure_class_exists(code, class_code) + return if @fake_classes.any? { |c| c["code"] == code } + + @fake_classes << { + "name" => "Matéria Mock", + "code" => code, + "classCode" => class_code, + "class" => { + "classCode" => class_code, + "semester" => "2024.1", + "time" => "35T23" + } + } +end + +# Encontra ou cria membro de turma. +# +# Retorno: +# - (Hash): Turma membro. +def find_or_create_turma_member(code, class_code) + turma = @fake_members.find { |m| m["code"] == code && m["classCode"] == class_code } + return turma if turma + + new_turma = { + "code" => code, + "classCode" => class_code, + "semester" => "2024.1", + "dicente" => [], + "docente" => mock_docente_data + } + @fake_members << new_turma + new_turma +end + +# Dados mock de docente. +# +# Retorno: +# - (Hash): Dados docente. +def mock_docente_data + { + "nome" => "Prof Mock", + "usuario" => "99999", + "email" => "prof@mock.com", + "ocupacao" => "docente" + } +end + +# --- Helper de Ação (Aluno) --- + +# Insere ou atualiza aluno no sigaa fake. +# +# Argumentos: +# - turma_mock (Hash): Turma. +# - nome (String): Nome. +# - matricula (String): Matrícula. +# - email (String): Email. +def upsert_sigaa_student(turma_mock, nome, matricula, email) + # Remove duplicatas baseadas na matrícula + turma_mock["dicente"].reject! { |d| d["matricula"] == matricula } + + # Insere o novo dado + turma_mock["dicente"] << { + "nome" => nome, + "matricula" => matricula, + "usuario" => matricula, + "email" => email, + "ocupacao" => "dicente" + } +end + +# Verifica dados de criação do usuário. +# +# Argumentos: +# - matricula (String): Matrícula. +# - nome (String): Nome esperado. +# - status_esperado (Boolean): Status esperado. +# +# Efeitos Colaterais: +# - Realiza asserções. +def verify_user_creation_data(matricula, nome, status_esperado) + user = Usuario.find_by(matricula: matricula) + + # Garante que o usuário existe antes de checar atributos + expect(user).to be_present, "Usuário com matrícula #{matricula} não foi encontrado." + + # Checa os atributos + expect(user.nome).to eq(nome) + + # Comparação direta de booleanos é mais robusta que user.status.to_s + expect(user.status).to be(status_esperado) +end \ No newline at end of file diff --git a/src/features/support/template_helpers.rb b/src/features/support/template_helpers.rb new file mode 100644 index 0000000000..d466f5dd14 --- /dev/null +++ b/src/features/support/template_helpers.rb @@ -0,0 +1,229 @@ +# --- Criação da Pergunta Base --- + +# Cria uma pergunta básica no formulário. +# +# Argumentos: +# - title (String): Título da pergunta. +# - type (String): Tipo da pergunta (Múltipla escolha, Texto, etc). +# +# Efeitos Colaterais: +# - Interage com formulário de criação de questão. +def create_base_question(title, type) + click_button "Adicionar Questão" + + within all('.question-form').last do + fill_in "Título da Questão", with: title + select resolve_question_type_label(type), from: "Tipo da Questão" + click_button "Salvar Questão" + end +end + +# Resolve o label do tipo de questão para a UI. +# +# Argumentos: +# - type (String): Tipo em português. +# +# Retorno: +# - (String): Label correspondente no select input. +def resolve_question_type_label(type) + case type.downcase + when "múltipla escolha" then "Radio" + when "caixa de seleção" then "Checkbox" + else type.humanize + end +end + +# --- Inserção de Opções --- + +# Adiciona opções à última pergunta criada. +# +# Argumentos: +# - options_str (String): String de opções separadas por vírgula. +# +# Efeitos Colaterais: +# - Chamadas múltiplas a append_single_option. +def add_options_to_last_question(options_str) + return if options_str.blank? + + options_str.split(',').each do |option| + append_single_option(option.strip) + end +end + +# Adiciona uma única opção a uma questão (tratando refresh). +# +# Argumentos: +# - option_text (String): Texto da opção. +def append_single_option(option_text) + # Ação 1: Clicar em "Adicionar Alternativa" + # Buscamos o formulário e clicamos. Isso vai disparar o refresh. + within(all('.question-form').last) do + click_button "Adicionar Alternativa" + end + + # Ação 2: Preencher e Salvar + # O DOM foi atualizado. Precisamos buscar o formulário ('.question-form') NOVAMENTE. + # Se usássemos a referência antiga, daria o erro StaleElementReferenceError. + within(all('.question-form').last) do + # Encontra o input recém-criado (o último da lista) + all('input[name="alternatives[]"]').last.set(option_text) + + click_button "Salvar Questão" + end +end + +# --- Parsing --- + +# Analisa string lista de opções. +# +# Argumentos: +# - str (String): Opções separadas por vírgula. +# +# Retorno: +# - (Array): Lista limpa. +def parse_options_list(str) + str.split(',').map(&:strip) +end + +# --- Orquestrador de Preenchimento --- + +# Preenche uma única opção. +# +# Argumentos: +# - text (String): Texto. +# - index (Integer): Índice. +def fill_single_option(text, index) + # 1. Garante que existe um input disponível para esse índice + ensure_alternative_input_exists(index) + + # 2. Preenche o input (buscando o elemento novamente para evitar StaleElementError) + set_alternative_input_value(index, text) +end + +# --- Interação com DOM --- + +# Garante existência de input para alternativa. +# +# Argumentos: +# - target_index (Integer): Índice desejado. +def ensure_alternative_input_exists(target_index) + # Verifica quantos inputs existem atualmente na questão + current_inputs = find_current_alternatives_inputs + + # Se o índice alvo for maior ou igual à quantidade atual, precisa criar um novo + if target_index >= current_inputs.size + click_add_alternative_button + end +end + +# Clica no botão de adicionar alternativa. +# +# Efeitos Colaterais: +# - Refresh da página. +def click_add_alternative_button + within current_question_scope do + click_button "Adicionar Alternativa" + end + # O clique causa refresh, então não retornamos nada aqui +end + +# Define valor do input de alternativa. +# +# Argumentos: +# - index (Integer): Índice. +# - text (String): Texto. +def set_alternative_input_value(index, text) + # Busca os inputs novamente (pós-refresh) + inputs = find_current_alternatives_inputs + + # Define o valor no input correto + inputs[index].set(text) +end + +# --- Scopes e Queries --- + +# Obtém escopo da questão atual. +# +# Retorno: +# - (Capybara::Node::Element): Elemento da questão. +def current_question_scope + # Retorna o escopo da questão atual baseada na variável de instância + all('.question-form')[@current_question_index] +end + +# Encontra inputs de alternativas atuais. +# +# Retorno: +# - (Capybara::Result): Lista de inputs. +def find_current_alternatives_inputs + current_question_scope.all('input[name="alternatives[]"]') +end + +# --- Gerenciamento de Estado --- + +# Reseta questões do template. +# +# Efeitos Colaterais: +# - Remove todas as questões associadas. +def reset_template_questions + @template.template_questions.destroy_all +end + +# Cria questões a partir de tabela Cucumber. +# +# Argumentos: +# - table (Cucumber::Table): Tabela de dados. +def create_questions_from_table(table) + table.hashes.each do |row| + create_single_template_question(row) + end +end + +# --- Fábrica de Questões (Factory) --- + +# Cria uma única questão de template. +# +# Argumentos: +# - row (Hash): Linha de dados (texto, tipo, opções). +# +# Efeitos Colaterais: +# - Cria TemplateQuestion. +def create_single_template_question(row) + @template.template_questions.create!( + title: row['texto'], + question_type: resolve_question_type(row['tipo']), + content: parse_question_options(row['opções']) + ) +end + +# --- Parsers e Mappers --- + +# Resolve tipo de questão p/ banco de dados. +# +# Argumentos: +# - type_name (String): Nome do tipo. +# +# Retorno: +# - (String): Tipo normalizado ('text', 'radio', 'checkbox'). +def resolve_question_type(type_name) + type_map = { + 'texto' => 'text', + 'radio' => 'radio', + 'checkbox' => 'checkbox' + } + + type_map[type_name] || 'text' +end + +# Parseia string de opções para array JSON. +# +# Argumentos: +# - options_str (String): String separada por vírgula. +# +# Retorno: +# - (Array): Array de opções. +def parse_question_options(options_str) + return [] if options_str.blank? + + options_str.split(',').map(&:strip) +end \ No newline at end of file diff --git a/src/features/visualiza_templates.feature b/src/features/visualiza_templates.feature new file mode 100644 index 0000000000..84a62be965 --- /dev/null +++ b/src/features/visualiza_templates.feature @@ -0,0 +1,21 @@ +# language: pt +# features/visualizar_templates.feature + +Funcionalidade: Visualização dos templates criados + Como Administrador + Quero visualizar os templates que criei + A fim de poder editar e/ou deletar um template + + @happy_path + Cenário: Visualização com sucesso + Dado que estou logado + Dado que existe um template criado com o campo "nome_da_matéria" preenchido com "Engenharia de Software", e o campo "semestre" preenchido com "2025.1", e o campo "professor" preenchido com "Genaína" + E estou na página "Gerenciamento de templates" + Então devo ver um cartão da disciplina contendo: "Engenharia de Software","2025.1","Genaína" + + @sad_path + Cenário: Não existem templates + Dado que estou logado + Dado que não existe nenhum template criado + E estou na página "Gerenciamento de templates" + Então devo visualizar a mensagem "Não existe nenhuma avaliação até o momento" diff --git a/src/features/visualizar_form.feature b/src/features/visualizar_form.feature new file mode 100644 index 0000000000..30fa4412bf --- /dev/null +++ b/src/features/visualizar_form.feature @@ -0,0 +1,38 @@ +# language: pt +# features/visualizacao_formularios_nao_respondidos.feature + +Funcionalidade: Visualização de formulários não respondidos + Eu como Participante de uma turma + Quero visualizar os formulários não respondidos das turmas em que estou matriculado + A fim de escolher qual formulário irei responder + +Contexto: + Dado que eu sou um "participante" logado no sistema + +@happy_path +Cenário: Exibir formulários não respondidos das turmas em que participo + Dado que estou na página "dashboard" + E estou matriculado nas turmas "BD 2025.1" e "Cálculo 2 2025.1" + E a turma "BD 2025.1" possui os formulários "Avaliação Docente" e "Avaliação da Infraestrutura" + E eu já respondi apenas o formulário "Avaliação Docente" + Quando eu acesso a página "formularios/pendentes" + Então eu devo ver o formulário "Avaliação da Infraestrutura" + E eu não devo ver o formulário "Avaliação Docente" + +@sad_path +Cenário: Participante não possui nenhum formulário pendente + Dado que estou na página "dashboard" + E estou matriculado na turma "Engenharia de Software 2025.1" + E todos os formulários desta turma já foram respondidos por mim + Quando eu acesso a página "formularios/pendentes" + Então eu devo ver a mensagem "Nenhum formulário pendente" + E não devo ver lista de formulários + +@sad_path +Cenário: Participante tenta acessar página de pendentes sem estar matriculado em nenhuma turma + Dado que estou na página "dashboard" + E não estou matriculado em nenhuma turma + Quando eu acesso a página "formularios/pendentes" + Então eu devo ver a mensagem "Você não possui turmas cadastradas" + E devo permanecer na página "formularios/pendentes" + diff --git a/src/features/visualizar_result.feature b/src/features/visualizar_result.feature new file mode 100644 index 0000000000..0a4d585655 --- /dev/null +++ b/src/features/visualizar_result.feature @@ -0,0 +1,49 @@ +# language: pt +# features/visualizacao_resultados_formularios.feature + +Funcionalidade: Visualização de resultados dos formulários + Eu como Administrador + Quero visualizar os formulários criados + A fim de gerar um relatório a partir das respostas + +Contexto: + Dado que eu sou um "admin" logado no sistema + +@happy_path +Cenário: Visualizar lista de formulários disponíveis + Dado que estou na página "dashboard" + E existem os formulários "Avaliação Docente" e "Avaliação da Infraestrutura" + Quando eu acesso a página "formularios" + Então eu devo ver "Avaliação Docente" + E eu devo ver "Avaliação da Infraestrutura" + +@happy_path +Cenário: Baixar o CSV a partir da página do formulário com respostas disponíveis + Dado que existe o formulário "Avaliação Docente" + E ele possui 30 respostas + Quando eu acesso a página "formularios/Avaliação Docente" + Então eu devo ver um botão "Baixar CSV" + E eu devo ver a mensagem "Total de respostas: 30" + Quando eu clico no botão "Baixar CSV" + Então o download do arquivo "avaliacao_docente.csv" deve iniciar + +@sad_path +Cenário: Formulário existente, porém sem respostas cadastradas + Dado que existe o formulário "Avaliação da Infraestrutura" + E ele possui 0 respostas + Quando eu acesso a página "formularios/Avaliação da Infraestrutura" + Então eu devo ver a mensagem "Nenhuma resposta registrada para este formulário" + E eu não devo ver o botão "Baixar CSV" + +@sad_path +Cenário: Admin tenta baixar o arquivo CSV com os resultados de formulário inexistente + Quando eu acesso a página "formularios/FormularioInexistente" + Então eu devo ver a mensagem "Formulário não encontrado" + E devo permanecer na página "formularios" + +@sad_path +Cenário: Não há formulários cadastrados + Dado que não existe nenhum formulário cadastrado + Quando eu acesso a página "formularios" + Então eu devo ver a mensagem "Nenhum formulário cadastrado" + diff --git a/src/lib/tasks/.keep b/src/lib/tasks/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/lib/tasks/cucumber.rake b/src/lib/tasks/cucumber.rake new file mode 100644 index 0000000000..0caa4d2553 --- /dev/null +++ b/src/lib/tasks/cucumber.rake @@ -0,0 +1,69 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + + +unless ARGV.any? {|a| a =~ /^gems/} # Don't load anything when running the gems:* tasks + +vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil? + +begin + require 'cucumber/rake/task' + + namespace :cucumber do + Cucumber::Rake::Task.new({ok: 'test:prepare'}, 'Run features that should pass') do |t| + t.binary = vendored_cucumber_bin # If nil, the gem's binary is used. + t.fork = true # You may get faster startup if you set this to false + t.profile = 'default' + end + + Cucumber::Rake::Task.new({wip: 'test:prepare'}, 'Run features that are being worked on') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'wip' + end + + Cucumber::Rake::Task.new({rerun: 'test:prepare'}, 'Record failing features and run only them if any exist') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'rerun' + end + + desc 'Run all features' + task all: [:ok, :wip] + + task :statsetup do + require 'rails/code_statistics' + ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features') + ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features') + end + + end + + desc 'Alias for cucumber:ok' + task cucumber: 'cucumber:ok' + + task default: :cucumber + + task features: :cucumber do + STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***" + end + + # In case we don't have the generic Rails test:prepare hook, append a no-op task that we can depend upon. + task 'test:prepare' do + end + + task stats: 'cucumber:statsetup' + + +rescue LoadError + desc 'cucumber rake task not available (cucumber not installed)' + task :cucumber do + abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin' + end +end + +end diff --git a/src/log/.keep b/src/log/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/public/400.html b/src/public/400.html new file mode 100644 index 0000000000..282dbc8cc9 --- /dev/null +++ b/src/public/400.html @@ -0,0 +1,114 @@ + + + + + + + The server cannot process the request due to a client error (400 Bad Request) + + + + + + + + + + + + + +
+
+ +
+
+

The server cannot process the request due to a client error. Please check the request and try again. If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/src/public/404.html b/src/public/404.html new file mode 100644 index 0000000000..c0670bc877 --- /dev/null +++ b/src/public/404.html @@ -0,0 +1,114 @@ + + + + + + + The page you were looking for doesn’t exist (404 Not found) + + + + + + + + + + + + + +
+
+ +
+
+

The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/src/public/406-unsupported-browser.html b/src/public/406-unsupported-browser.html new file mode 100644 index 0000000000..9532a9ccd0 --- /dev/null +++ b/src/public/406-unsupported-browser.html @@ -0,0 +1,114 @@ + + + + + + + Your browser is not supported (406 Not Acceptable) + + + + + + + + + + + + + +
+
+ +
+
+

Your browser is not supported.
Please upgrade your browser to continue.

+
+
+ + + + diff --git a/src/public/422.html b/src/public/422.html new file mode 100644 index 0000000000..8bcf06014f --- /dev/null +++ b/src/public/422.html @@ -0,0 +1,114 @@ + + + + + + + The change you wanted was rejected (422 Unprocessable Entity) + + + + + + + + + + + + + +
+
+ +
+
+

The change you wanted was rejected. Maybe you tried to change something you didn’t have access to. If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/src/public/500.html b/src/public/500.html new file mode 100644 index 0000000000..d77718c3a4 --- /dev/null +++ b/src/public/500.html @@ -0,0 +1,114 @@ + + + + + + + We’re sorry, but something went wrong (500 Internal Server Error) + + + + + + + + + + + + + +
+
+ +
+
+

We’re sorry, but something went wrong.
If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/src/public/icon.png b/src/public/icon.png new file mode 100644 index 0000000000..c4c9dbfbbd Binary files /dev/null and b/src/public/icon.png differ diff --git a/src/public/icon.svg b/src/public/icon.svg new file mode 100644 index 0000000000..04b34bf83f --- /dev/null +++ b/src/public/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/public/robots.txt b/src/public/robots.txt new file mode 100644 index 0000000000..c19f78ab68 --- /dev/null +++ b/src/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/src/script/.keep b/src/script/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/script/setup_test_data.rb b/src/script/setup_test_data.rb new file mode 100644 index 0000000000..71e25bda55 --- /dev/null +++ b/src/script/setup_test_data.rb @@ -0,0 +1,128 @@ +#!/usr/bin/env ruby +# Script para criar dados de teste para feature de responder formulário + +puts "🔄 Carregando ambiente Rails..." +require_relative '../config/environment' + +puts "🔄 Limpando dados de teste existentes..." + +# Limpar apenas dados de teste +RespostaItem.destroy_all +Resposta.destroy_all +Formulario.destroy_all +Questao.destroy_all +Template.destroy_all +Matricula.destroy_all +Turma.destroy_all +Materia.destroy_all +Usuario.where("email LIKE '%@test.com'").destroy_all + +puts "✅ Dados limpos\n" + +puts "📝 Criando usuários..." + +admin = Usuario.create!( + nome: 'Admin Teste', + email: 'admin@test.com', + matricula: '000001', + usuario: 'admin', + password: 'senha123', + ocupacao: :admin, + status: true +) +puts " ✓ Admin criado" + +docente = Usuario.create!( + nome: 'Prof. Silva', + email: 'prof.silva@test.com', + matricula: '100001', + usuario: 'prof.silva', + password: 'senha123', + ocupacao: :docente, + status: true +) +puts " ✓ Docente criado" + +aluno = Usuario.create!( + nome: 'João Aluno', + email: 'joao.aluno@test.com', + matricula: '200001', + usuario: 'joao.aluno', + password: 'senha123', + ocupacao: :discente, + status: true +) +puts " ✓ Aluno criado\n" + +puts "📚 Criando matéria e turma..." +materia = Materia.create!( + nome: 'Banco de Dados', + codigo: 'CIC0105' +) + +turma = Turma.create!( + codigo: 'BD-TB', + semestre: '2025.1', + horario: '35T', + materia: materia, + docente: docente +) +puts " ✓ Turma #{turma.codigo} criada\n" + +puts "👨‍🎓 Matriculando aluno..." +Matricula.create!( + usuario: aluno, + turma: turma +) +puts " ✓ Aluno matriculado em #{turma.codigo}\n" + +puts "📋 Criando template..." +template = Template.create!( + name: 'Avaliação Docente', + titulo: 'Avaliação Docente', + id_criador: admin.id, + participantes: 'todos' +) + +Questao.create!( + template: template, + enunciado: 'O professor domina o conteúdo?', + tipo: :texto +) + +Questao.create!( + template: template, + enunciado: 'As aulas são bem preparadas?', + tipo: :texto +) +puts " ✓ Template criado com #{template.questoes.count} questões\n" + +puts "📝 Criando formulário..." +formulario = Formulario.create!( + titulo_envio: 'Avaliação BD 2025.1', + data_criacao: Time.now, + data_encerramento: Time.now + 7.days, + template: template, + turma: turma +) +puts " ✓ Formulário '#{formulario.titulo_envio}' criado\n" + +puts "⏳ Criando resposta pendente..." +Resposta.create!( + formulario: formulario, + participante: aluno, + data_submissao: nil +) +puts " ✓ Resposta pendente criada\n" + +puts "=" * 60 +puts "✅ DADOS CRIADOS COM SUCESSO!" +puts "=" * 60 +puts "\n📋 CREDENCIAIS PARA LOGIN:" +puts " Admin: admin@test.com / senha123" +puts " Docente: prof.silva@test.com / senha123" +puts " Aluno: joao.aluno@test.com / senha123" +puts "\n🎯 FORMULÁRIO: #{formulario.titulo_envio}" +puts "📚 TURMA: #{turma.codigo}" +puts "\n🌐 Acesse: http://localhost:3000/login" +puts "=" * 60 diff --git a/src/spec/controllers/application_controller_spec.rb b/src/spec/controllers/application_controller_spec.rb new file mode 100644 index 0000000000..2fc5a0a01f --- /dev/null +++ b/src/spec/controllers/application_controller_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +# Testes de unidade para ApplicationController. +# +# Cobre lógica compartilhada dos controladores, como autenticação. +RSpec.describe ApplicationController, type: :controller do + controller do + before_action :require_login + + def index + render plain: "Acesso Permitido" + end + end + + let(:usuario) { Usuario.create!(nome: 'User', email: 'user@test.com', usuario: 'user', password: 'password', ocupacao: :discente, status: true, matricula: '123') } + + # Testes de filtro de login. + describe "Verificação de Login (:require_login)" do + + context "quando o usuário NÃO está logado" do + it "redireciona para o login com mensagem de alerta" do + get :index + + expect(response).to redirect_to(login_path) + expect(flash[:alert]).to eq("Você precisa estar logado para acessar esta página.") + end + end + + context "quando o usuário ESTÁ logado" do + before do + session[:usuario_id] = usuario.id + end + + it "permite o acesso à página" do + get :index + + expect(response).to have_http_status(:success) + expect(response.body).to eq("Acesso Permitido") + end + end + + end +end \ No newline at end of file diff --git a/src/spec/controllers/respostas_controller_spec.rb b/src/spec/controllers/respostas_controller_spec.rb new file mode 100644 index 0000000000..5c1e448e99 --- /dev/null +++ b/src/spec/controllers/respostas_controller_spec.rb @@ -0,0 +1,148 @@ +require 'rails_helper' + +# Testes de unidade para RespostasController. +# +# Cobre envio de avaliações, validações de acesso e tratamento de erros. +RSpec.describe RespostasController, type: :controller do + # Configuração dos dados de teste + let(:aluno) { Usuario.create!(nome: 'Aluno', email: 'aluno@test.com', usuario: 'aluno', password: 'p', ocupacao: :discente, status: true, matricula: '1234') } + let(:docente) { Usuario.create!(nome: 'Doc', email: 'doc@test.com', usuario: 'doc', password: 'p', ocupacao: :docente, status: true, matricula: '5678') } + + let(:materia) { Materia.create!(nome: 'Mat', codigo: 'M1') } + let(:turma) { Turma.create!(codigo: 'T1', semestre: '2025.1', horario: '10h', materia: materia, docente: docente) } + let(:template) { Template.create!(titulo: 'T', participantes: 'alunos', criador: docente, name: 'Template Teste') } + let(:formulario) { Formulario.create!(titulo_envio: 'F1', data_criacao: Time.now, template: template, turma: turma) } + + # Questões + let!(:questao_texto) { Questao.create!(enunciado: 'Q Texto', tipo: 0, template: template) } + let!(:questao_multipla) { Questao.create!(enunciado: 'Q Multipla', tipo: 1, template: template) } + let!(:opcao_valida) { Opcao.create!(texto_opcao: 'Opcao A', questao: questao_multipla) } + + before do + session[:usuario_id] = aluno.id + end + + # Testes de filtros de acesso. + describe "Restrições de Acesso (before_actions)" do + it "redireciona se o usuário não for discente" do + session[:usuario_id] = docente.id + get :new, params: { formulario_id: formulario.id } + + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq("Acesso negado.") + end + + it "redireciona se o prazo do formulário expirou" do + formulario.update!(data_encerramento: 1.day.ago) + get :new, params: { formulario_id: formulario.id } + + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq("Este formulário não está mais aceitando respostas.") + end + + it "redireciona se o aluno já respondeu" do + Resposta.create!(formulario: formulario, participante: aluno, data_submissao: Time.current) + + get :new, params: { formulario_id: formulario.id } + + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq("Você já respondeu este formulário.") + end + end + + # Testes de exibição do formulário. + describe "GET #new" do + it "retorna sucesso para aluno elegível" do + get :new, params: { formulario_id: formulario.id } + expect(response).to have_http_status(:success) + expect(assigns(:questions)).to include(questao_texto) + end + end + + # Testes de submissão de respostas. + describe "POST #create" do + + # Sucesso. + context "Caminho Feliz" do + it "cria resposta com texto simples" do + expect { + post :create, params: { + formulario_id: formulario.id, + respostas: { questao_texto.id => "Minha resposta" } + } + }.to change(Resposta, :count).by(1) + + expect(response).to redirect_to(root_path) + expect(flash[:notice]).to eq("Avaliação enviada com sucesso. Obrigado!") + end + + it "cria resposta com múltipla escolha válida" do + post :create, params: { + formulario_id: formulario.id, + respostas: { questao_multipla.id => "Opcao A" } + } + + resposta = Resposta.last + item = resposta.resposta_items.find_by(questao: questao_multipla) + expect(item.opcao_escolhida).to eq(opcao_valida) + end + end + + # Erros e Transações. + context "Caminhos de Erro (Cobre as imagens image_c081db.png)" do + + it "faz rollback e renderiza new se falhar ao salvar o cabeçalho da Resposta" do + allow_any_instance_of(Resposta).to receive(:save).and_return(false) + + expect { + post :create, params: { + formulario_id: formulario.id, + respostas: { questao_texto.id => "Teste" } + } + }.not_to change(Resposta, :count) + + expect(response).to have_http_status(:unprocessable_content) + expect(response).to render_template(:new) + expect(flash[:alert]).to include("Houve um erro") + end + + it "gera erro ao enviar opção inválida para questão de múltipla escolha" do + expect { + post :create, params: { + formulario_id: formulario.id, + respostas: { questao_multipla.id => "Opcao Hackeada Inexistente" } + } + }.not_to change(Resposta, :count) + + expect(flash[:alert]).to include("Houve um erro") + end + + it "faz rollback se um item individual falhar ao salvar" do + allow_any_instance_of(RespostaItem).to receive(:save).and_return(false) + + expect { + post :create, params: { + formulario_id: formulario.id, + respostas: { questao_texto.id => "Texto Válido" } + } + }.not_to change(Resposta, :count) + + expect(response).to render_template(:new) + expect(flash[:alert]).to include("Houve um erro") + end + + it "faz rollback e retorna false se houver erro de validação (RecordInvalid)" do + allow_any_instance_of(Resposta).to receive(:save).and_raise(ActiveRecord::RecordInvalid.new(Resposta.new)) + + expect { + post :create, params: { + formulario_id: formulario.id, + respostas: { questao_texto.id => "Teste" } + } + }.not_to change(Resposta, :count) + + expect(response).to have_http_status(:unprocessable_content) + end + end + end +end \ No newline at end of file diff --git a/src/spec/controllers/resultados_controller_spec.rb b/src/spec/controllers/resultados_controller_spec.rb new file mode 100644 index 0000000000..b900d52aab --- /dev/null +++ b/src/spec/controllers/resultados_controller_spec.rb @@ -0,0 +1,133 @@ +require 'rails_helper' + +# Testes de unidade para ResultadosController. +# +# Cobre visualização de resultados e exportação CSV. +RSpec.describe ResultadosController, type: :controller do + let(:admin) { + Usuario.create!( + nome: 'Admin', + email: "admin_#{Time.now.to_f}@test.com", + usuario: "admin_#{Time.now.to_f}", + password: 'p', + ocupacao: :admin, + status: true, + matricula: "ADM#{rand(9999)}" + ) + } + + let(:docente) { + Usuario.create!( + nome: 'Doc', + email: "doc_#{Time.now.to_f}@test.com", + usuario: "doc_#{Time.now.to_f}", + password: 'p', + ocupacao: :docente, + status: true, + matricula: "DOC#{rand(9999)}" + ) + } + + let(:materia) { Materia.create!(nome: 'Mat', codigo: "M#{rand(999)}") } + let(:turma) { Turma.create!(codigo: 'T1', semestre: '2025.1', horario: '10h', materia: materia, docente: docente) } + let(:template) { Template.create!(titulo: 'T', participantes: 'alunos', id_criador: docente.id, name: 'Template') } + let(:formulario) { Formulario.create!(titulo_envio: 'F1', data_criacao: Time.now, template: template, turma: turma, data_encerramento: Time.now + 1.day) } + + before do + session[:usuario_id] = admin.id + end + + # Teste de listagem. + describe "GET #index" do + it "returns http success" do + get :index + expect(response).to have_http_status(:success) + end + end + + # Testes de exibição e exportação (CSV). + describe "GET #show" do + + # Sucesso com respostas. + context "quando existem respostas" do + before do + aluno = Usuario.create!( + nome: 'Aluno', + email: "aluno_#{Time.now.to_f}@test.com", + usuario: "aluno_#{Time.now.to_f}", + password: 'p', + ocupacao: :discente, + status: true, + matricula: "A#{rand(9999)}" + ) + + Resposta.create!( + formulario: formulario, + participante: aluno, + data_submissao: Time.now + ) + end + + it "retorna formato csv com sucesso" do + get :show, params: { id: formulario.id, format: :csv } + expect(response.content_type).to include("text/csv") + end + + it "inclui cabeçalhos no CSV" do + Questao.create!(enunciado: 'Questão Teste', tipo: 0, template: template) + get :show, params: { id: formulario.id, format: :csv } + expect(response.body).to include("Questão Teste") + end + + it "cobre o lado direito da extração do CSV (resposta de múltipla escolha)" do + questao_multipla = Questao.create!(enunciado: 'Q Mult', tipo: 1, template: template) + opcao = Opcao.create!(texto_opcao: 'Opcao B', questao: questao_multipla) + + aluno = Usuario.create!( + nome: 'Aluno 2', + email: "aluno2_#{Time.now.to_f}@test.com", + usuario: "aluno2_#{Time.now.to_f}", + password: 'p', + ocupacao: :discente, + status: true, + matricula: "A2#{rand(9999)}" + ) + + resposta = Resposta.create!( + formulario: formulario, + participante: aluno, + data_submissao: Time.now + ) + + RespostaItem.create!( + resposta: resposta, + questao: questao_multipla, + texto_resposta: nil, + opcao_escolhida: opcao + ) + get :show, params: { id: formulario.id, format: :csv } + expect(response.body).to include("Opcao B") + end + end + + # Sem respostas. + context "quando NÃO existem respostas (Download CSV)" do + it "redireciona com alerta" do + get :show, params: { id: formulario.id, format: :csv } + + expect(response).to redirect_to(resultado_path(formulario)) + expect(flash[:alert]).to eq("Não é possível gerar um relatório, pois não há respostas.") + end + end + + # Formulário inexistente. + context "quando o formulário não é encontrado" do + it "captura RecordNotFound e redireciona para index de formulários" do + get :show, params: { id: 999999 } + + expect(response).to redirect_to(formularios_path) + expect(flash[:alert]).to eq("Formulário não encontrado") + end + end + end +end \ No newline at end of file diff --git a/src/spec/controllers/template_questions_controller_spec.rb b/src/spec/controllers/template_questions_controller_spec.rb new file mode 100644 index 0000000000..017ce1dd0a --- /dev/null +++ b/src/spec/controllers/template_questions_controller_spec.rb @@ -0,0 +1,180 @@ +require 'rails_helper' + +# Testes de unidade para TemplateQuestionsController. +# +# Cobre gestão de questões dentro de um template (CRUD, adição de alternativas). +RSpec.describe TemplateQuestionsController, type: :controller do + let(:admin) { Usuario.create!(nome: 'Admin', email: 'admin@test.com', matricula: '123', usuario: 'admin', password: 'password', ocupacao: :admin, status: true) } + let(:template) { Template.create!(titulo: 'Test Template', id_criador: admin.id) } + let!(:question) { TemplateQuestion.create!(template: template, title: 'Questão Original', question_type: 'text', content: []) } + + before do + session[:usuario_id] = admin.id + end + + # Teste de criação de nova questão. + describe 'POST #create' do + it 'creates a new question with defaults and redirects to template edit' do + expect { + post :create, params: { template_id: template.id } + }.to change(TemplateQuestion, :count).by(1) + + question = TemplateQuestion.last + expect(question.title).to eq("Nova Questão") + expect(question.question_type).to eq("text") + expect(response).to redirect_to(edit_template_path(template)) + end + end + + # Teste de atualização de questão. + describe 'PATCH #update' do + + # Adicionar alternativa. + context 'Quando clica em Adicionar Alternativa' do + it 'adiciona uma opção vazia e redireciona' do + patch :update, params: { + template_id: template.id, + id: question.id, + template_question: { title: 'Q' }, + commit: "Adicionar Alternativa" + } + + question.reload + expect(question.content).to include("") + expect(response).to redirect_to(edit_template_path(template)) + end + end + + # Alterar tipo de questão. + context 'Mudança de Tipo de Questão' do + it 'limpa o conteúdo se mudar para TEXTO' do + question.update!(question_type: 'radio', content: ['A', 'B']) + + patch :update, params: { + template_id: template.id, + id: question.id, + template_question: { question_type: 'text' } + } + + question.reload + expect(question.question_type).to eq('text') + expect(question.content).to be_empty + expect(flash[:notice]).to eq('Tipo de questão atualizado.') + end + + it 'adiciona opção vazia se mudar para RADIO e conteúdo for nulo' do + question.update!(question_type: 'text', content: nil) + + patch :update, params: { + template_id: template.id, + id: question.id, + template_question: { question_type: 'radio' } + } + + question.reload + expect(question.question_type).to eq('radio') + expect(question.content).to eq(['']) + end + end + + # Salvar alterações. + context 'Salvamento Normal (Botão Salvar)' do + it 'atualiza atributos e redireciona com sucesso' do + question.update!(question_type: 'radio', content: ['Old']) + + patch :update, params: { + template_id: template.id, + id: question.id, + template_question: { + title: 'Novo Título', + question_type: 'radio' + }, + alternatives: ['Op1', 'Op2'], + commit: 'Salvar' + } + + question.reload + + expect(question.title).to eq('Novo Título') + expect(question.content).to eq(['Op1', 'Op2']) + expect(flash[:notice]).to eq('template alterado com sucesso') + end + + it 'renderiza erro se a validação falhar' do + allow_any_instance_of(TemplateQuestion).to receive(:save).and_return(false) + allow_any_instance_of(TemplateQuestion).to receive_message_chain(:errors, :full_messages).and_return(["Erro de teste"]) + + patch :update, params: { + template_id: template.id, + id: question.id, + template_question: { title: '' }, + commit: 'Salvar' + } + + expect(response).to redirect_to(edit_template_path(template)) + expect(flash[:alert]).to include("Erro de teste") + end + + it 'limpa conteúdo se salvar como texto (logica redundante do controller)' do + question.update!(question_type: 'text', content: ['Lixo']) + + patch :update, params: { + template_id: template.id, + id: question.id, + template_question: { question_type: 'text' }, + commit: 'Salvar' + } + + question.reload + expect(question.content).to eq([]) + end + end + end + + # Teste de remoção de questão. + describe 'DELETE #destroy' do + + context 'Quando existe apenas 1 questão' do + it 'NÃO deleta e redireciona com alerta' do + expect(template.template_questions.count).to eq(1) + + expect { + delete :destroy, params: { template_id: template.id, id: question.id } + }.not_to change(TemplateQuestion, :count) + + expect(response).to redirect_to(edit_template_path(template)) + expect(flash[:alert]).to eq('não é possível salvar template sem questões') + end + end + + context 'Quando existem múltiplas questões' do + before do + TemplateQuestion.create!(template: template, title: 'Questão 2', question_type: 'text') + end + + it 'deleta a questão e redireciona com sucesso' do + expect(template.template_questions.count).to eq(2) + + expect { + delete :destroy, params: { template_id: template.id, id: question.id } + }.to change(TemplateQuestion, :count).by(-1) + + expect(response).to redirect_to(edit_template_path(template)) + expect(flash[:notice]).to eq('template alterado com sucesso') + end + end + end + + # Teste legado/específico de adicionar alternativa via POST. + describe 'POST #add_alternative' do + let(:radio_question) { TemplateQuestion.create!(template: template, title: 'Q', question_type: 'radio', content: ['A']) } + + it 'adds a new empty alternative' do + post :add_alternative, params: { template_id: template.id, id: radio_question.id } + + radio_question.reload + expect(radio_question.content).to eq(['A', '']) + expect(response).to redirect_to(edit_template_path(template)) + end + end +end \ No newline at end of file diff --git a/src/spec/controllers/templates_controller_spec.rb b/src/spec/controllers/templates_controller_spec.rb new file mode 100644 index 0000000000..40e2f67404 --- /dev/null +++ b/src/spec/controllers/templates_controller_spec.rb @@ -0,0 +1,103 @@ +require 'rails_helper' + +# Testes de unidade para TemplatesController. +# +# Cobre gestão do CRUD de templates. +RSpec.describe TemplatesController, type: :controller do + let(:admin) { Usuario.create!(nome: 'Admin', email: 'admin@test.com', matricula: '123', usuario: 'admin', password: 'password', ocupacao: :admin, status: true) } + let!(:template) { Template.create!(titulo: 'Template Teste', id_criador: admin.id) } + + before do + session[:usuario_id] = admin.id + end + + # Teste de listagem. + describe "GET #index" do + it "retorna sucesso e carrega templates" do + get :index + expect(response).to have_http_status(:success) + expect(assigns(:templates)).not_to be_nil + end + end + + # Teste de novo template. + describe "GET #new" do + it "instancia um novo template" do + get :new + expect(response).to have_http_status(:success) + expect(assigns(:template)).to be_a_new(Template) + end + end + + # Teste de edição. + describe "GET #edit" do + it "carrega o template solicitado" do + get :edit, params: { id: template.id } + expect(response).to have_http_status(:success) + expect(assigns(:template)).to eq(template) + end + end + + # Teste de criação. + describe 'POST #create' do + context 'with valid attributes' do + it 'creates a new template and redirects to edit' do + expect { + post :create, params: { template: { titulo: 'New Template' } } + }.to change(Template, :count).by(1) + + expect(response).to redirect_to(edit_template_path(Template.last)) + expect(flash[:notice]).to be_present + end + end + + context 'with invalid attributes' do + it 'does not create a template and renders new' do + expect { + post :create, params: { template: { titulo: '' } } + }.not_to change(Template, :count) + + expect(response).to render_template(:new) + expect(response).to have_http_status(:unprocessable_content) + end + end + end + + # Teste de atualização. + describe "PATCH #update" do + context "com atributos válidos" do + it "atualiza o template e redireciona" do + patch :update, params: { id: template.id, template: { titulo: "Título Atualizado" } } + + template.reload + expect(template.titulo).to eq("Título Atualizado") + expect(response).to redirect_to(edit_template_path(template)) + expect(flash[:notice]).to include("atualizado com sucesso") + end + end + + context "com atributos inválidos" do + it "não atualiza e renderiza edit com erro" do + old_title = template.titulo + patch :update, params: { id: template.id, template: { titulo: "" } } + + template.reload + expect(template.titulo).to eq(old_title) + expect(response).to render_template(:edit) + expect(response).to have_http_status(:unprocessable_content) + end + end + end + + # Teste de exclusão (soft delete). + describe 'DELETE #destroy' do + it 'soft deletes the template (sets hidden to true)' do + expect { + delete :destroy, params: { id: template.id } + }.not_to change(Template, :count) + + template.reload + expect(template.hidden).to be true + end + end +end \ No newline at end of file diff --git a/src/spec/controllers/usuarios_controller_spec.rb b/src/spec/controllers/usuarios_controller_spec.rb new file mode 100644 index 0000000000..8df07efabb --- /dev/null +++ b/src/spec/controllers/usuarios_controller_spec.rb @@ -0,0 +1,154 @@ +require 'rails_helper' + +# Testes de unidade para UsuariosController. +# +# Cobre CRUD básico e autenticação/autorização de usuários. +RSpec.describe UsuariosController, type: :controller do + let(:valid_attributes) do + { + nome: 'Usuario Teste', + email: 'usuario@email.com', + matricula: '12345', + usuario: 'user_teste', + password: 'senha123', + ocupacao: 'discente', + status: true + } + end + + let(:invalid_attributes) do + { + nome: nil, + email: nil, + matricula: nil, + usuario: nil, + password: nil, + ocupacao: nil, + status: true + } + end + + before(:each) do + # Simula autenticação de admin para permitir acesso ao controller. + allow_any_instance_of(described_class).to receive(:authenticate_admin).and_return(true) + Usuario.delete_all + end + + # Teste de listagem. + describe "GET #index" do + it "retorna uma lista vazia quando não há usuários" do + get :index + expect(assigns(:usuarios)).to be_empty + expect(response).to be_successful + end + + it "mostra todos usuarios" do + usuario = Usuario.create!(valid_attributes) + get :index + expect(assigns(:usuarios)).to include(usuario) + expect(response).to be_successful + end + end + + # Teste de exibição. + describe "GET #show" do + it "retorna sucesso e atribui o usuário correto" do + usuario = Usuario.create!(valid_attributes) + get :show, params: { id: usuario.id } + expect(assigns(:usuario)).to eq(usuario) + expect(response).to be_successful + end + end + + # Teste de formulário de novo usuário. + describe "GET #new" do + it "atribui um novo usuário a @usuario" do + get :new + expect(assigns(:usuario)).to be_a_new(Usuario) + expect(response).to be_successful + end + end + + # Teste de formulário de edição. + describe "GET #edit" do + it "atribui o usuário requisitado a @usuario" do + usuario = Usuario.create!(valid_attributes) + get :edit, params: { id: usuario.id } + expect(assigns(:usuario)).to eq(usuario) + expect(response).to be_successful + end + end + + # Teste de criação. + describe "POST #create" do + context "com parâmetros válidos" do + it "cria um novo Usuario" do + expect { + post :create, params: { usuario: valid_attributes } + }.to change(Usuario, :count).by(1) + end + + it "redireciona para o usuário criado (ou index)" do + post :create, params: { usuario: valid_attributes } + expect(response).to redirect_to(Usuario.last) + end + end + + context "com parâmetros inválidos" do + it "não cria um novo usuário e renderiza o template new" do + expect { + post :create, params: { usuario: invalid_attributes } + }.not_to change(Usuario, :count) + + expect(response).to render_template(:new) + expect(response).to have_http_status(:unprocessable_content) + end + end + end + + # Teste de atualização. + describe "PATCH #update" do + let(:usuario) { Usuario.create!(valid_attributes) } + + context "com parâmetros válidos" do + let(:new_attributes) { { nome: 'Nome Atualizado' } } + + it "atualiza o usuário solicitado" do + patch :update, params: { id: usuario.id, usuario: new_attributes } + usuario.reload + expect(usuario.nome).to eq('Nome Atualizado') + end + + it "redireciona para o usuário" do + patch :update, params: { id: usuario.id, usuario: new_attributes } + expect(response).to redirect_to(usuario) + end + end + + context "com parâmetros inválidos" do + it "não atualiza o usuário e renderiza edit" do + patch :update, params: { id: usuario.id, usuario: invalid_attributes } + usuario.reload + expect(usuario.nome).to eq(valid_attributes[:nome]) + expect(response).to render_template(:edit) + expect(response).to have_http_status(:unprocessable_content) + end + end + end + + # Teste de exclusão. + describe "DELETE #destroy" do + it "destrói o usuário solicitado" do + usuario = Usuario.create!(valid_attributes) + expect { + delete :destroy, params: { id: usuario.id } + }.to change(Usuario, :count).by(-1) + end + + it "redireciona para a lista de usuários" do + usuario = Usuario.create!(valid_attributes) + delete :destroy, params: { id: usuario.id } + expect(response).to redirect_to(usuarios_url) + end + end +end \ No newline at end of file diff --git a/src/spec/factories/matriculas.rb b/src/spec/factories/matriculas.rb new file mode 100644 index 0000000000..4c42f8499e --- /dev/null +++ b/src/spec/factories/matriculas.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + # Factory para Matricula. + # + # Cria associação entre usuário (aluno) e turma. + factory :matricula do + + end +end diff --git a/src/spec/factories/template_questions.rb b/src/spec/factories/template_questions.rb new file mode 100644 index 0000000000..f13a41b0f1 --- /dev/null +++ b/src/spec/factories/template_questions.rb @@ -0,0 +1,11 @@ +FactoryBot.define do + # Factory para TemplateQuestion. + # + # Gera questões de template com dados fictícios. + factory :template_question do + title { "MyString" } + question_type { "MyString" } + content { "MyText" } + template { nil } + end +end diff --git a/src/spec/factories/usuarios.rb b/src/spec/factories/usuarios.rb new file mode 100644 index 0000000000..be99161265 --- /dev/null +++ b/src/spec/factories/usuarios.rb @@ -0,0 +1,24 @@ +FactoryBot.define do + # Factory para Usuario. + # + # Gera usuários com sequências para email, matricula e usuario. + # Traits disponíveis: :admin, :docente. + factory :usuario do + sequence(:nome) { |n| "Usuário #{n}" } + sequence(:email) { |n| "usuario#{n}@camaar.com" } + sequence(:matricula) { |n| "2020#{n.to_s.rjust(5, '0')}" } + sequence(:usuario) { |n| "user#{n}" } + password { "password" } + password_confirmation { "password" } + ocupacao { :discente } # Default to discente + status { true } + + trait :admin do + ocupacao { :admin } + end + + trait :docente do + ocupacao { :docente } + end + end +end diff --git a/src/spec/jobs/application_job_spec.rb b/src/spec/jobs/application_job_spec.rb new file mode 100644 index 0000000000..b332a2dd01 --- /dev/null +++ b/src/spec/jobs/application_job_spec.rb @@ -0,0 +1,10 @@ +require 'rails_helper' + +# Testes par ApplicationJob. +# +# Garante que a classe base herda corretamente de ActiveJob::Base. +RSpec.describe ApplicationJob, type: :job do + it "herda de ActiveJob::Base" do + expect(ApplicationJob.superclass).to eq(ActiveJob::Base) + end +end \ No newline at end of file diff --git a/src/spec/mailers/user_mailer_spec.rb b/src/spec/mailers/user_mailer_spec.rb new file mode 100644 index 0000000000..0ce1c4d485 --- /dev/null +++ b/src/spec/mailers/user_mailer_spec.rb @@ -0,0 +1,40 @@ +require "rails_helper" + +# Testes par UserMailer. +# +# Cobre o envio de emails de redefinição de senha. +RSpec.describe UserMailer, type: :mailer do + # Teste de redefinição de senha. + describe "redefinicao_senha" do + let(:user) { + Usuario.create!( + nome: "Teste Reset", + email: "reset@teste.com", + usuario: "111222", + matricula: "111222", + ocupacao: :discente, + status: true, + password: "OldPass123!", + ) + } + + let(:mail) { UserMailer.with(user: user).redefinicao_senha } + + it "renderiza os cabeçalhos corretamente" do + expect(mail.subject).to eq("Redefinição de Senha - Sistema CAMAAR") + expect(mail.to).to eq([user.email]) + expect(mail.from).to eq(["nao-responda@camaar.unb.br"]) + end + + it "renderiza o corpo com a URL correta" do + body = mail.body.encoded + + expect(body).to include("redefinir a senha") + expect(body).to include("Redefinir minha senha") + + expect(body).to include("/redefinir_senha/edit") + + expect(body).to include("token=") + end + end +end \ No newline at end of file diff --git a/src/spec/models/matricula_spec.rb b/src/spec/models/matricula_spec.rb new file mode 100644 index 0000000000..de3b21e936 --- /dev/null +++ b/src/spec/models/matricula_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Testes de modelo para Matricula. +# +# Cobre associações básicas. +RSpec.describe Matricula, type: :model do + describe 'associations' do + it 'belongs to usuario' do + expect(Matricula.reflect_on_association(:usuario).macro).to eq :belongs_to + end + it 'belongs to turma' do + expect(Matricula.reflect_on_association(:turma).macro).to eq :belongs_to + end + end +end diff --git a/src/spec/models/resposta_item_spec.rb b/src/spec/models/resposta_item_spec.rb new file mode 100644 index 0000000000..daa6cd0fab --- /dev/null +++ b/src/spec/models/resposta_item_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +# Testes de modelo para RespostaItem. +# +# Cobre validações de respostas (texto vs múltipla escolha). +RSpec.describe RespostaItem, type: :model do + let(:aluno) { Usuario.create!(nome: 'Aluno', email: 'a@a.com', usuario: 'aluno', password: 'p', ocupacao: :discente, status: true, matricula: '1234') } + let(:docente) { Usuario.create!(nome: 'Doc', email: 'd@d.com', usuario: 'doc', password: 'p', ocupacao: :docente, status: true, matricula: '5678') } + let(:materia) { Materia.create!(nome: 'Mat', codigo: 'M1') } + let(:turma) { Turma.create!(codigo: 'T1', semestre: '2025.1', horario: '10h', materia: materia, docente: docente) } + let(:template) { Template.create!(titulo: 'Templ', participantes: 'alunos', criador: docente, name: 'T') } + let(:formulario) { Formulario.create!(titulo_envio: 'Form', data_criacao: Time.now, template: template, turma: turma) } + let(:resposta) { Resposta.create!(formulario: formulario, participante: aluno) } + + # Assuming Question types: 0 = text, 1 = multiple choice (from migration/schema implicitly or convention) + # Actually schema says questao.tipo is integer. Checking Questao model would be best but assuming standard. + + let(:questao_texto) { Questao.create!(enunciado: 'Q1', tipo: 0, template: template) } + + # For multiple choice, we need an Opcao + let(:questao_multipla) { Questao.create!(enunciado: 'Q2', tipo: 1, template: template) } + let(:opcao) { Opcao.create!(texto_opcao: 'Opt1', questao: questao_multipla) } + + # Teste de validação para resposta em texto. + it 'validates text answer for text question' do + item = RespostaItem.new(resposta: resposta, questao: questao_texto, texto_resposta: 'Answer') + expect(item).to be_valid + + item.texto_resposta = nil + expect(item).to_not be_valid + end + + # Teste de validação para resposta de múltipla escolha. + it 'validates option choice for multiple choice question' do + item = RespostaItem.new(resposta: resposta, questao: questao_multipla, opcao_escolhida: opcao) + expect(item).to be_valid + + item.opcao_escolhida = nil + expect(item).to_not be_valid + end +end diff --git a/src/spec/models/resposta_spec.rb b/src/spec/models/resposta_spec.rb new file mode 100644 index 0000000000..66540869a6 --- /dev/null +++ b/src/spec/models/resposta_spec.rb @@ -0,0 +1,52 @@ +require 'rails_helper' + +# Testes de modelo para Resposta. +# +# Cobre associações, validações e escopos de participação. +RSpec.describe Resposta, type: :model do + let(:aluno) { Usuario.create!(nome: 'Aluno', email: 'a@a.com', usuario: 'aluno', password: 'p', ocupacao: :discente, status: true, matricula: '1234') } + let(:docente) { Usuario.create!(nome: 'Doc', email: 'd@d.com', usuario: 'doc', password: 'p', ocupacao: :docente, status: true, matricula: '5678') } + let(:materia) { Materia.create!(nome: 'Mat', codigo: 'M1') } + let(:turma) { Turma.create!(codigo: 'T1', semestre: '2025.1', horario: '10h', materia: materia, docente: docente) } + let(:template) { Template.create!(titulo: 'Templ', participantes: 'alunos', criador: docente, name: 'T') } + let(:formulario) { Formulario.create!(titulo_envio: 'Form', data_criacao: Time.now, template: template, turma: turma) } + + subject { described_class.new(formulario: formulario, participante: aluno) } + + # Teste de validação básica. + it 'is valid with valid attributes' do + expect(subject).to be_valid + end + + # Teste de associação com formulário. + it 'belongs to a formulario' do + subject.formulario = nil + expect(subject).to_not be_valid + end + + # Teste de associação com participante. + it 'belongs to a participante' do + subject.participante = nil + expect(subject).to_not be_valid + end + + # Teste de unicidade de participação. + it 'validates uniqueness of participante per formulario' do + subject.save! + duplicate = described_class.new(formulario: formulario, participante: aluno) + expect(duplicate).to_not be_valid + end + + # Teste de método auxiliar de status. + describe '#respondido?' do + it 'returns true if data_submissao is present' do + subject.data_submissao = Time.now + expect(subject.respondido?).to be true + end + + it 'returns false if data_submissao is nil' do + subject.data_submissao = nil + expect(subject.respondido?).to be false + end + end +end diff --git a/src/spec/models/template_question_spec.rb b/src/spec/models/template_question_spec.rb new file mode 100644 index 0000000000..22f391d238 --- /dev/null +++ b/src/spec/models/template_question_spec.rb @@ -0,0 +1,73 @@ +require 'rails_helper' + +# Testes de modelo para TemplateQuestion. +# +# Cobre validações de tipo de pergunta e serialização de conteúdo. +RSpec.describe TemplateQuestion, type: :model do + let(:user) { Usuario.create!(nome: 'User', email: 'user@test.com', matricula: '123', usuario: 'user', password: 'password', ocupacao: :admin, status: true) } + let(:template) { Template.create!(titulo: 'Test Template', id_criador: user.id) } + + # Testes de validação. + describe 'validations' do + it 'is valid with valid attributes' do + question = TemplateQuestion.new( + title: 'Valid Question', + question_type: 'text', + template: template + ) + expect(question).to be_valid + end + + # Validações para perguntas de escolha (radio/checkbox). + context 'when type is radio or checkbox' do + it 'adds error if content (alternatives) contains blank values' do + question = TemplateQuestion.new( + title: 'Invalid Radio', + question_type: 'radio', + content: ['Opção A', ''], + template: template + ) + + expect(question).not_to be_valid + expect(question.errors[:base]).to include("Todas as alternativas devem ser preenchidas") + end + + it 'adds error if content is empty' do + question = TemplateQuestion.new( + title: 'Invalid Checkbox', + question_type: 'checkbox', + content: [], + template: template + ) + + expect(question).not_to be_valid + expect(question.errors[:base]).to include("Todas as alternativas devem ser preenchidas") + end + end + end + + # Testes de serialização JSON. + describe 'content serialization' do + it 'can save and retrieve an array of strings as JSON' do + question = TemplateQuestion.create!( + title: 'Question 1', + question_type: 'radio', + content: ['Option A', 'Option B'], + template: template + ) + + question.reload + expect(question.content).to be_an(Array) + expect(question.content).to eq(['Option A', 'Option B']) + end + + it 'defaults to an empty array' do + question = TemplateQuestion.create!( + title: 'Question 2', + template: template + ) + + expect(question.content).to eq([]) + end + end +end \ No newline at end of file diff --git a/src/spec/models/template_spec.rb b/src/spec/models/template_spec.rb new file mode 100644 index 0000000000..09abc64f1b --- /dev/null +++ b/src/spec/models/template_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +# Testes de modelo para Template. +# +# Cobre escopos de visibilidade. +RSpec.describe Template, type: :model do + describe 'scopes' do + + # Teste para escopo de templates visíveis. + describe '.all_visible' do + it 'returns only templates that are not hidden' do + user = Usuario.create!(nome: 'User', email: 'user@test.com', matricula: '123', usuario: 'user', password: 'password', ocupacao: :admin, status: true) + # Create a visible template (hidden: false by default) + visible_template = Template.create!(titulo: 'Visible Template', id_criador: user.id) + + # Create a hidden template + hidden_template = Template.create!(titulo: 'Hidden Template', hidden: true, id_criador: user.id) + + expect(Template.all_visible).to include(visible_template) + expect(Template.all_visible).not_to include(hidden_template) + end + end + end +end diff --git a/src/spec/models/turma_spec.rb b/src/spec/models/turma_spec.rb new file mode 100644 index 0000000000..03598f1fe9 --- /dev/null +++ b/src/spec/models/turma_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +# Testes de modelo para Turma. +# +# Cobre associações e lógica de distribuição de formulários. +RSpec.describe Turma, type: :model do + describe 'associations' do + it 'has many formularios' do + expect(Turma.reflect_on_association(:formularios).macro).to eq :has_many + end + it 'has many matriculas' do + expect(Turma.reflect_on_association(:matriculas).macro).to eq :has_many + end + it 'has many alunos through matriculas' do + assoc = Turma.reflect_on_association(:alunos) + expect(assoc.macro).to eq :has_many + expect(assoc.options[:through]).to eq :matriculas + end + end + + # Teste do método de distribuição. + describe '#distribuir_formulario' do + let(:docente) { Usuario.create!(nome: 'Prof', email: 'prof@test.com', matricula: '111', usuario: 'prof', password: 'password', ocupacao: 'docente', status: true) } + let(:turma) { Turma.create!(codigo: 'T1', semestre: '2024.1', horario: '35T', materia: Materia.create!(codigo: 'M1', nome: 'Mat'), docente: docente) } + let(:template) { Template.create!(name: 'Template 1', id_criador: docente.id, titulo: 'Titulo', participantes: 'todos') } + let(:aluno1) { Usuario.create!(nome: 'A1', email: 'a1@test.com', matricula: '222', usuario: 'a1', password: 'password', ocupacao: 'discente', status: true) } + + before do + Matricula.create!(usuario: aluno1, turma: turma) + end + + it 'creates a formulario and respostas for all members' do + expect { + turma.distribuir_formulario(template) + }.to change(Formulario, :count).by(1) + .and change(Resposta, :count).by(2) # 1 aluno + 1 docente + + created_form = Formulario.last + expect(created_form.turma).to eq(turma) + expect(created_form.template).to eq(template) + + respostas = created_form.respostas + expect(respostas.map(&:id_participante)).to include(aluno1.id, docente.id) + expect(respostas.first.respondido?).to be_falsey + expect(respostas.first.data_submissao).to be_nil + end + end +end diff --git a/src/spec/models/usuario_spec.rb b/src/spec/models/usuario_spec.rb new file mode 100644 index 0000000000..0c24f4ca43 --- /dev/null +++ b/src/spec/models/usuario_spec.rb @@ -0,0 +1,124 @@ +require 'rails_helper' + +# Testes de modelo para Usuario. +# +# Cobre autenticação, validação de senha, roles e escopos de pendência. +RSpec.describe Usuario, type: :model do + let(:usuario_ativo) { + Usuario.create!( + nome: 'User Ativo', email: 'ativo@test.com', usuario: 'ativo', + matricula: '111', password: 'password', ocupacao: :discente, status: true + ) + } + + let(:usuario_pendente) { + Usuario.create!( + nome: 'User Pendente', email: 'pendente@test.com', usuario: 'pendente', + matricula: '222', password: 'password', ocupacao: :discente, status: false + ) + } + + let(:admin) { + Usuario.create!( + nome: 'Admin', email: 'admin@test.com', usuario: 'admin', + matricula: '999', password: 'password', ocupacao: :admin, status: true + ) + } + + describe 'associations' do + it 'has many respostas' do + expect(Usuario.reflect_on_association(:respostas).macro).to eq :has_many + end + end + + describe 'attributes' do + it "persists attributes correctly" do + expect(usuario_ativo).to be_persisted + expect(usuario_ativo.status).to be true + end + end + + # Teste de verificação de role admin. + describe '#admin?' do + it 'retorna true se ocupacao for admin' do + expect(admin.admin?).to be true + end + + it 'retorna false se ocupacao não for admin' do + expect(usuario_ativo.admin?).to be false + end + end + + # Teste de autenticação customizada. + describe '.authenticate' do + context 'Caminho Feliz' do + it 'retorna o usuário se as credenciais estiverem corretas' do + expect(Usuario.authenticate(usuario_ativo.email, 'password')).to eq(usuario_ativo) + expect(Usuario.authenticate(usuario_ativo.usuario, 'password')).to eq(usuario_ativo) + expect(Usuario.authenticate(usuario_ativo.matricula, 'password')).to eq(usuario_ativo) + end + end + + context 'Erros de Autenticação' do + it 'lança erro se o usuário não for encontrado' do + expect { + Usuario.authenticate('nao_existe@test.com', 'password') + }.to raise_error(AuthenticationError, "Usuário não encontrado") + end + + it 'lança erro se o usuário estiver pendente (status false)' do + expect { + Usuario.authenticate(usuario_pendente.email, 'password') + }.to raise_error(AuthenticationError, /Sua conta está pendente/) + end + + it 'lança erro se a senha estiver incorreta' do + expect { + Usuario.authenticate(usuario_ativo.email, 'senha_errada') + }.to raise_error(AuthenticationError, "Senha incorreta") + end + end + end + + # Teste de validação de senha atual. + describe '#validate_current_password' do + + it 'adiciona erro se current_password estiver incorreto' do + usuario_ativo.current_password = 'senha_errada_teste' + usuario_ativo.validate + + expect(usuario_ativo.errors[:current_password]).to include("está incorreta") + end + + it 'não valida se current_password estiver em branco' do + usuario_ativo.current_password = '' + usuario_ativo.validate + + expect(usuario_ativo.errors[:current_password]).to be_empty + end + + it 'passa se current_password estiver correto' do + usuario_ativo.current_password = 'password' + usuario_ativo.validate + + expect(usuario_ativo.errors[:current_password]).to be_empty + end + end + + # Teste de recuperação de pendências. + describe '#pendencias' do + let(:template) { Template.create!(name: 'T1', id_criador: usuario_ativo.id, titulo: 'T', participantes: 'todos') } + let(:materia) { Materia.create!(codigo: 'M', nome: 'N') } + let(:turma) { Turma.create!(codigo: 'X', semestre: '2024', horario: '2M', materia: materia, docente: usuario_ativo) } + let(:formulario) { Formulario.create!(template: template, turma: turma, titulo_envio: 'Envio 1', data_criacao: Time.now) } + + it 'returns only unanswered responses' do + formulario2 = Formulario.create!(template: template, turma: turma, titulo_envio: 'Envio 2', data_criacao: Time.now) + r1 = Resposta.create!(participante: usuario_ativo, formulario: formulario) + r2 = Resposta.create!(participante: usuario_ativo, formulario: formulario2, data_submissao: Time.now) + + expect(usuario_ativo.pendencias).to include(r1) + expect(usuario_ativo.pendencias).not_to include(r2) + end + end +end \ No newline at end of file diff --git a/src/spec/rails_helper.rb b/src/spec/rails_helper.rb new file mode 100644 index 0000000000..9e649d8df2 --- /dev/null +++ b/src/spec/rails_helper.rb @@ -0,0 +1,84 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +# Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file +# that will avoid rails generators crashing because migrations haven't been run yet +# return unless Rails.env.test? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! +require 'rails-controller-testing' + +# Configuração principal do Rails Helper. +# +# Define módulos de teste de controller do Rails e integração com RSpec. +RSpec.configure do |config| + config.include Rails::Controller::Testing::TestProcess, type: :controller + config.include Rails::Controller::Testing::Integration, type: :controller + config.include Rails::Controller::Testing::TemplateAssertions, type: :controller +end + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } + +# Ensures that the test database schema matches the current schema file. +# If there are pending migrations it will invoke `db:test:prepare` to +# recreate the test database by loading the schema. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end + +# Configurações adicionais do RSpec Rails. +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_paths = [ + Rails.root.join('spec/fixtures') + ] + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails uses metadata to mix in different behaviours to your tests, + # for example enabling you to call `get` and `post` in request specs. e.g.: + # + # RSpec.describe UsersController, type: :request do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://rspec.info/features/8-0/rspec-rails + # + # You can also this infer these behaviours automatically by location, e.g. + # /spec/models would pull in the same behaviour as `type: :model` but this + # behaviour is considered legacy and will be removed in a future version. + # + # To enable this behaviour uncomment the line below. + # config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/src/spec/requests/admin_spec.rb b/src/spec/requests/admin_spec.rb new file mode 100644 index 0000000000..94368575f6 --- /dev/null +++ b/src/spec/requests/admin_spec.rb @@ -0,0 +1,66 @@ +require 'rails_helper' + +# Testes de para a área administrativa. +# +# Cobre o acesso ao painel e a funcionalidade de importação de dados. +RSpec.describe "Admins", type: :request do + let(:admin) { + Usuario.create!( + nome: 'Admin Teste', + email: 'admin@teste.com', + matricula: '000000', + usuario: '000000', + password: 'password', + ocupacao: :admin, + status: true + ) + } + + before do + post login_path, params: { email: admin.email, password: admin.password } + end + + # Testes para o painel de gerenciamento. + describe "GET /admin/gerenciamento" do + it "returns http success" do + get "/admin/gerenciamento" + expect(response).to have_http_status(:success) + end + end + + # Testes para a ação de importar dados via POST. + describe "POST /admin/gerenciamento/importar_dados" do + + # Contexto de sucesso na importação. + context "quando a importação é realizada com sucesso" do + before do + expect(SigaaImporter).to receive(:call).once + end + + it "chama o importer, define flash notice e redireciona" do + post importar_dados_path + + expect(response).to redirect_to("/admin/gerenciamento") + + follow_redirect! + expect(flash[:notice]).to eq("Dados importados com sucesso!") + end + end + + # Contexto de falha na importação. + context "quando ocorre um erro durante a importação" do + before do + allow(SigaaImporter).to receive(:call).and_raise(StandardError, "Falha na conexão com SIGAA") + end + + it "captura o erro, define flash alert e redireciona" do + post importar_dados_path + + expect(response).to redirect_to("/admin/gerenciamento") + + follow_redirect! + expect(flash[:alert]).to eq("Falha na conexão com SIGAA") + end + end + end +end \ No newline at end of file diff --git a/src/spec/requests/autenticacao_spec.rb b/src/spec/requests/autenticacao_spec.rb new file mode 100644 index 0000000000..330af2819a --- /dev/null +++ b/src/spec/requests/autenticacao_spec.rb @@ -0,0 +1,72 @@ +require 'rails_helper' + +# Testes de integração para autenticação de usuários. +# +# Cobre login, logout e cenários de erro de autenticação. +RSpec.describe "Autenticacao", type: :request do + let(:password) { 'senha123' } + let(:aluno) { Usuario.create!(nome: 'Aluno', email: 'aluno@test.com', usuario: 'aluno', password: password, ocupacao: :discente, status: true, matricula: '123') } + let(:admin) { Usuario.create!(nome: 'Admin', email: 'admin@test.com', usuario: 'admin', password: password, ocupacao: :admin, status: true, matricula: '999') } + + # Teste de acesso à página de login. + describe "GET /new (Página de Login)" do + it "retorna sucesso" do + get login_path + expect(response).to have_http_status(:success) + end + end + + # Teste do processo de login (POST). + describe "POST /create (Fazer Login)" do + + # Cenário de sucesso. + context "Caminho Feliz" do + it "autentica admin e redireciona para painel" do + post login_path, params: { email: admin.email, password: password } + + expect(response).to redirect_to(admin_gerenciamento_path) + expect(flash[:notice]).to include("Bem-vindo") + expect(session[:usuario_id]).to eq(admin.id) + end + + it "autentica aluno e redireciona para root" do + post login_path, params: { email: aluno.email, password: password } + + expect(response).to redirect_to(root_path) + expect(flash[:notice]).to include("Login realizado") + end + end + + # Cenário de falha (credenciais inválidas ou erros de sistema). + context "Quando ocorre AuthenticationError" do + before do + allow(Usuario).to receive(:authenticate).and_raise(AuthenticationError, "Usuário ou senha inválidos") + end + + it "captura o erro e redireciona para login com alerta" do + post login_path, params: { email: 'errado', password: 'errado' } + + expect(response).to redirect_to(login_path) + expect(flash[:alert]).to eq("Usuário ou senha inválidos") + end + end + end + + # Teste de logout. + describe "DELETE /destroy (Logout)" do + it "reseta sessão, remove cookies e redireciona" do + post login_path, params: { email: aluno.email, password: password } + + cookies[:auth_token] = "token_teste" + + delete logout_path + + expect(response).to redirect_to(login_path) + expect(flash[:notice]).to eq("Deslogado com sucesso.") + + expect(session[:usuario_id]).to be_nil + + expect(cookies[:auth_token]).to be_blank + end + end +end \ No newline at end of file diff --git a/src/spec/requests/avaliacoes_spec.rb b/src/spec/requests/avaliacoes_spec.rb new file mode 100644 index 0000000000..79bbb83703 --- /dev/null +++ b/src/spec/requests/avaliacoes_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +# Testes de integração para avaliações. +# +# Cobre a listagem de avaliações para o aluno. +RSpec.describe "Avaliacoes", type: :request do + let(:aluno) { Usuario.create!(nome: 'Aluno', email: 'a@test.com', matricula: '999', usuario: 'aluno', password: 'password', ocupacao: 'discente', status: true) } + + before do + # Stub current_usuario to be the student + allow_any_instance_of(ApplicationController).to receive(:current_usuario).and_return(aluno) + end + + # Teste de listagem. + describe "GET /index" do + it "returns http success for student" do + get avaliacoes_path + expect(response).to have_http_status(:success) + end + end +end diff --git a/src/spec/requests/definir_senha_spec.rb b/src/spec/requests/definir_senha_spec.rb new file mode 100644 index 0000000000..1832a37771 --- /dev/null +++ b/src/spec/requests/definir_senha_spec.rb @@ -0,0 +1,156 @@ +require 'rails_helper' + +# Testes de integração para definição inicial de senha. +# +# Cobre o fluxo de primeiro acesso via token de convite. +RSpec.describe "DefinicaoSenha", type: :request do + let!(:usuario_pendente) { + Usuario.create!( + nome: "Novo Aluno", + email: "novo@teste.com", + usuario: "novo_aluno", + matricula: "12345", + ocupacao: :discente, + status: false, + password: "TempPass123!" + ) + } + + let(:token) { usuario_pendente.signed_id(purpose: :definir_senha) } + + # Teste de acesso ao formulário de definição. + describe "GET /definir_senha" do + + # Cenário de sucesso. + context "com token válido" do + it "acessa a página de definição de senha com sucesso" do + get definir_senha_path(token: token) + expect(response).to have_http_status(:success) + expect(response.body).to include("Nova Senha") + end + end + + # Cenários de token inválido/ausente. + context "sem token na URL" do + it "redireciona com alerta de token ausente" do + get definir_senha_path(token: "") + + expect(response).to redirect_to(login_path) + follow_redirect! + expect(flash[:alert]).to include("Link inválido (token ausente)") + end + end + + context "com token inválido ou expirado" do + it "redireciona para login com alerta" do + get definir_senha_path(token: "token_falso_123") + + expect(response).to redirect_to(login_path) + follow_redirect! + expect(flash[:alert]).to include("Link inválido ou expirado") + end + end + + # Cenário de usuário já ativo. + context "quando o usuário já está ativo" do + before { usuario_pendente.update!(status: true) } + + it "redireciona avisando que já está ativo" do + get definir_senha_path(token: token) + + expect(response).to redirect_to(login_path) + follow_redirect! + expect(flash[:notice]).to include("Você já está ativo") + end + end + + # Cenário com sessão ativa de outro usuário. + context "quando um Admin já está logado" do + let(:admin) { Usuario.create!(nome: "Admin", email: "adm@t.com", usuario: "admin", matricula: "000", ocupacao: :admin, status: true, password: "123", password_confirmation: "123") } + + it "desloga o admin e mostra a página de definição do usuário novo" do + post login_path, params: { email: admin.email, password: "123" } + expect(session[:usuario_id]).to eq(admin.id) + + get definir_senha_path(token: token) + + expect(session[:usuario_id]).to be_nil + expect(response).to have_http_status(:success) + end + end + end + + # Teste de submissão da nova senha. + describe "PATCH /definir_senha (create)" do + + # Cenário de sucesso. + context "Caminho Feliz" do + it "atualiza a senha, ativa o usuário e redireciona" do + patch definir_senha_path(token: token), params: { + usuario: { + password: "NovaSenhaForte123!", + password_confirmation: "NovaSenhaForte123!" + } + } + + usuario_pendente.reload + expect(usuario_pendente.status).to be_truthy + expect(usuario_pendente.authenticate("NovaSenhaForte123!")).to be_truthy + + expect(response).to redirect_to(login_path) + expect(flash[:notice]).to include("Senha definida com sucesso") + end + end + + # Cenários de falha. + context "com token inválido no envio" do + it "redireciona para login" do + patch definir_senha_path(token: "token_falso"), params: { + usuario: { password: "123", password_confirmation: "123" } + } + + expect(response).to redirect_to(login_path) + expect(flash[:alert]).to include("Link inválido ou expirado") + end + end + + context "com campos de senha em branco" do + it "renderiza erro de campos obrigatórios" do + patch definir_senha_path(token: token), params: { + usuario: { password: "", password_confirmation: "" } + } + + expect(response).to have_http_status(:unprocessable_content) + expect(flash.now[:alert]).to include("Todos os campos devem ser preenchidos") + expect(response).to render_template(:new) + end + end + + context "Erros de Validação" do + it "exibe erro quando senhas não conferem" do + patch definir_senha_path(token: token), params: { + usuario: { + password: "SenhaA", + password_confirmation: "SenhaB" + } + } + + expect(response).to have_http_status(:unprocessable_content) + expect(flash.now[:alert]).to include("As senhas não conferem") + end + + it "exibe erros genéricos do model (ex: validação customizada falhando)" do + allow_any_instance_of(Usuario).to receive(:update).and_return(false) + allow_any_instance_of(Usuario).to receive_message_chain(:errors, :[], :present?).and_return(false) + allow_any_instance_of(Usuario).to receive_message_chain(:errors, :full_messages, :to_sentence).and_return("Erro Genérico do Model") + + patch definir_senha_path(token: token), params: { + usuario: { password: "123", password_confirmation: "123" } + } + + expect(response).to have_http_status(:unprocessable_content) + expect(flash.now[:alert]).to eq("Erro Genérico do Model") + end + end + end +end \ No newline at end of file diff --git a/src/spec/requests/formularios_spec.rb b/src/spec/requests/formularios_spec.rb new file mode 100644 index 0000000000..e5da86e7ae --- /dev/null +++ b/src/spec/requests/formularios_spec.rb @@ -0,0 +1,133 @@ +require 'rails_helper' + +# Testes de integração para gerenciamento de formulários. +# +# Cobre CRUD básico e distribuição de formulários. +RSpec.describe "Formularios", type: :request do + let(:admin) { Usuario.create!(nome: 'Admin', email: 'admin@test.com', matricula: '123', usuario: 'admin', password: 'password', ocupacao: :admin, status: true) } + let(:aluno) { Usuario.create!(nome: 'Aluno', email: 'aluno@test.com', matricula: '456', usuario: 'aluno', password: 'password', ocupacao: :discente, status: true) } + + let(:template) { Template.create!(name: 'Template Teste', titulo: 'Titulo Teste', id_criador: admin.id, participantes: 'todos') } + let(:materia) { Materia.create!(nome: 'Engenharia de Software', codigo: 'CIC0105') } + let(:turma) { Turma.create!(codigo: 'TA', semestre: '2024.1', horario: '35T', materia: materia, docente: admin) } + let(:formulario_existente) { Formulario.create!(titulo_envio: "Form Test", template: template, turma: turma, data_criacao: Time.now) } + + def sign_in(user) + allow_any_instance_of(ApplicationController).to receive(:current_usuario).and_return(user) + end + + before do + sign_in(admin) + end + + # Teste de listagem. + describe "GET /index" do + it "returns http success" do + get formularios_path + expect(response).to have_http_status(:success) + end + end + + # Teste de exibição. + describe "GET /show" do + it "exibe o formulário corretamente" do + get formulario_path(formulario_existente) + expect(response).to have_http_status(:success) + expect(assigns(:formulario)).to eq(formulario_existente) + end + end + + # Teste de formulário de criação. + describe "GET /new" do + it "carrega os dados necessários e retorna sucesso" do + get new_formulario_path + + expect(response).to have_http_status(:success) + + expect(assigns(:templates)).not_to be_nil + expect(assigns(:turmas)).not_to be_nil + end + end + + # Teste de criação/distribuição. + describe "POST /create" do + before do + Matricula.create!(usuario: aluno, turma: turma) + end + + context "Caminho Feliz" do + it "distributes form to selected turmas" do + expect { + post formularios_path, params: { template_id: template.id, turma_ids: [turma.id] } + }.to change(Formulario, :count).by(1) + .and change(Resposta, :count).by(1) + + expect(response).to redirect_to(formularios_path) + follow_redirect! + expect(response.body).to include("Formulário distribuído com sucesso") + end + end + + # Cenários de validação. + context "Caminhos de Validação e Erro (Cobre Imagens 1 e 2)" do + it "fails if no turmas selected" do + post formularios_path, params: { template_id: template.id } + + expect(response).to redirect_to(new_formulario_path) + follow_redirect! + expect(flash[:alert]).to include("Selecione pelo menos uma turma") + end + + it "fails if no template selected" do + post formularios_path, params: { turma_ids: [turma.id], template_id: "" } + + expect(response).to redirect_to(new_formulario_path) + follow_redirect! + expect(flash[:alert]).to include("Selecione um template") + end + + it "captura erro de banco de dados e redireciona" do + allow(Formulario).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(Formulario.new)) + + post formularios_path, params: { template_id: template.id, turma_ids: [turma.id] } + + expect(response).to redirect_to(new_formulario_path) + expect(flash[:alert]).to include("Erro ao distribuir") + end + end + end + + # Teste de listagem para alunos. + describe "GET /pendentes" do + before do + sign_in(aluno) + end + + context "Quando aluno não tem turmas (Cobre linhas 66-69)" do + it "mostra mensagem de alerta e lista vazia" do + aluno.matriculas.destroy_all + + get pendentes_formularios_path + + expect(response).to have_http_status(:success) + expect(flash.now[:alert]).to eq("Você não possui turmas cadastradas") + expect(assigns(:respostas_pendentes)).to be_empty + end + end + + context "Quando aluno tem turmas e formulários (Cobre linhas 70-73)" do + before do + Matricula.create!(usuario: aluno, turma: turma) + Resposta.create!(formulario: formulario_existente, participante: aluno, data_submissao: nil) + end + + it "lista as respostas pendentes corretamente" do + get pendentes_formularios_path + + expect(response).to have_http_status(:success) + expect(assigns(:respostas_pendentes)).not_to be_empty + expect(assigns(:respostas_pendentes).first.formulario).to eq(formulario_existente) + end + end + end +end \ No newline at end of file diff --git a/src/spec/requests/home_spec.rb b/src/spec/requests/home_spec.rb new file mode 100644 index 0000000000..f08b70a7e9 --- /dev/null +++ b/src/spec/requests/home_spec.rb @@ -0,0 +1,71 @@ +require 'rails_helper' + +# Testes de integração para a página inicial (Home). +# +# Cobre o acesso à root_path para diferentes tipos de usuário. +RSpec.describe "Homes", type: :request do + let(:password) { 'password' } + + let(:aluno) { + Usuario.create!( + nome: 'Aluno Teste', + email: 'aluno@home.com', + matricula: '123456', + usuario: '123456', + password: password, + ocupacao: :discente, + status: true + ) + } + + let(:admin) { + Usuario.create!( + nome: 'Admin Teste', + email: 'admin@home.com', + matricula: '999999', + usuario: 'admin', + password: password, + ocupacao: :admin, + status: true + ) + } + + # Teste de acesso ao endpoint principal. + describe "GET /index" do + + # Cenário de Discente. + context "quando logado como Discente" do + before do + post login_path, params: { email: aluno.email, password: password } + end + + it "acessa a home e carrega pendências" do + get home_path + expect(response).to have_http_status(:success) + + expect(assigns(:pendencias)).to_not be_nil + end + end + + # Cenário de Admin. + context "quando logado como Admin" do + before do + post login_path, params: { email: admin.email, password: password } + end + + it "acessa a home sem erros (caminho de admin)" do + get home_path + expect(response).to have_http_status(:success) + end + end + + # Cenário sem login. + context "sem estar logado" do + it "não quebra a aplicação (redireciona ou falha autenticação)" do + get home_path + expect(response.status).to be_in([200, 302]) + end + end + + end +end \ No newline at end of file diff --git a/src/spec/requests/redefinicao_senha_spec.rb b/src/spec/requests/redefinicao_senha_spec.rb new file mode 100644 index 0000000000..a06fa619fd --- /dev/null +++ b/src/spec/requests/redefinicao_senha_spec.rb @@ -0,0 +1,127 @@ +require 'rails_helper' + +# Testes de integração para redefinição de senha (esqueci minha senha). +# +# Cobre solicitação de token e alteração de senha. +RSpec.describe "RedefinicaoSenha", type: :request do + let!(:usuario) { Usuario.create!(nome: "User", email: "teste@email.com", usuario: "user", matricula: "123", ocupacao: :discente, status: true, password: "oldPass", password_confirmation: "oldPass") } + + let!(:usuario_pendente) { Usuario.create!(nome: "Pendente", email: "pendente@email.com", usuario: "pendente", matricula: "456", ocupacao: :discente, status: false, password: "temp", password_confirmation: "temp") } + + # Teste de solicitação de reset (Esqueci Senha). + describe "POST /esqueci_senha" do + + # Sucesso. + context "com e-mail válido e usuário ativo" do + it "envia e-mail e redireciona para login" do + expect { + post esqueci_senha_path, params: { email: usuario.email } + }.to change { ActionMailer::Base.deliveries.count }.by(1) + + expect(response).to redirect_to(login_path) + follow_redirect! + expect(response.body).to include("Se este e-mail estiver cadastrado") + end + end + + # Usuário inativo. + context "com usuário inativo (status false)" do + it "impede o reset e avisa que precisa definir a senha primeiro" do + expect { + post esqueci_senha_path, params: { email: usuario_pendente.email } + }.not_to change { ActionMailer::Base.deliveries.count } + + expect(response).to redirect_to(login_path) + follow_redirect! + expect(flash[:alert]).to include("Você ainda não definiu sua senha") + end + end + + # Email inexistente. + context "com e-mail não cadastrado" do + it "não envia e-mail mas mostra mensagem de sucesso (segurança)" do + expect { + post esqueci_senha_path, params: { email: "inexistente@email.com" } + }.not_to have_enqueued_mail(UserMailer, :redefinicao_senha) + + expect(response).to redirect_to(login_path) + follow_redirect! + expect(response.body).to include("Se este e-mail estiver cadastrado") + end + end + + # Validacao. + context "com campo vazio" do + it "redireciona para login com erro" do + post esqueci_senha_path, params: { email: "" } + + expect(response).to redirect_to(login_path) + follow_redirect! + expect(response.body).to include("O campo de e-mail não pode estar vazio.") + end + end + end + + # Teste de acesso à página de edição de senha. + describe "GET /redefinir_senha/edit" do + let(:token) { usuario.signed_id(purpose: :redefinir_senha) } + + context "com token válido" do + it "acessa a página de redefinição com sucesso" do + get edit_redefinir_senha_path(token: token) + + expect(response).to have_http_status(:success) + expect(assigns(:usuario)).to eq(usuario) + end + end + + context "com token inválido ou expirado" do + it "redireciona para login com alerta" do + get edit_redefinir_senha_path(token: "token_invalido_123") + + expect(response).to redirect_to(login_path) + expect(flash[:alert]).to include("Link inválido ou expirado") + end + end + end + + # Teste de submissão da nova senha. + describe "PATCH /redefinir_senha" do + let(:token) { usuario.signed_id(purpose: :redefinir_senha) } + + context "com token válido e senhas iguais" do + it "atualiza a senha e redireciona para login" do + patch redefinir_senha_path(token: token), params: { + usuario: { password: "NewPass123", password_confirmation: "NewPass123" } + } + + usuario.reload + expect(usuario.authenticate("NewPass123")).to be_truthy + expect(response).to redirect_to(login_path) + expect(flash[:notice]).to include("Senha redefinida com sucesso") + end + end + + context "com senhas diferentes" do + it "não atualiza e mostra erro" do + patch redefinir_senha_path(token: token), params: { + usuario: { password: "123", password_confirmation: "456" } + } + + expect(response).to have_http_status(:unprocessable_content) + expect(usuario.reload.authenticate("oldPass")).to be_truthy + end + end + + context "com token inválido" do + it "redireciona para login com erro" do + patch redefinir_senha_path(token: "token_fake"), params: { + usuario: { password: "123", password_confirmation: "123" } + } + + expect(response).to redirect_to(login_path) + expect(flash[:alert]).to include("Link inválido") + end + end + end +end \ No newline at end of file diff --git a/src/spec/services/sigaa_importer_spec.rb b/src/spec/services/sigaa_importer_spec.rb new file mode 100644 index 0000000000..9e23046c8b --- /dev/null +++ b/src/spec/services/sigaa_importer_spec.rb @@ -0,0 +1,382 @@ +require 'rails_helper' + +# Testes par a classe SigaaImporter. +# +# Cobre a importação de turmas, alunos e docentes a partir de arquivos JSON mockados. +RSpec.describe SigaaImporter do + # Testa o método principal de importação. + describe '.call' do + # MOCK DATA + let(:classes_json) do + [ + { + "name" => "BANCOS DE DADOS", + "code" => "CIC0097", + "class" => { + "classCode" => "TA", + "semester" => "2024.1", + "time" => "35T23" + } + } + ].to_json + end + + let(:members_json) do + [ + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2021.2", + "dicente" => [ + { + "nome" => "Fulano Teste", + "matricula" => "123456789", + "usuario" => "123456789", + "email" => "fulano@teste.com", + "ocupacao" => "dicente" + } + ], + "docente" => { + "nome" => "Prof. Real", + "usuario" => "88888", + "email" => "prof@real.com", + "ocupacao" => "docente" + } + } + ].to_json + end + + let!(:docente) do + Usuario.create!( + nome: "Prof. Mock", email: "prof@mock.com", matricula: "0000", + usuario: "profmock", password: "123", ocupacao: :docente, status: true + ) + end + + # Contexto de sucesso na importação. + context 'quando os arquivos JSON existem e são válidos' do + before do + allow(File).to receive(:read).and_call_original + + allow(File).to receive(:read).with(satisfy { |path| path.to_s.include?('classes.json') }) + .and_return(classes_json) + + allow(File).to receive(:read).with(satisfy { |path| path.to_s.include?('class_members.json') }) + .and_return(members_json) + end + + it 'cria a matéria correspondente' do + expect { described_class.call }.to change(Materia, :count).by(1) + end + + it 'cria a turma associada à matéria e ao docente' do + expect { described_class.call }.to change(Turma, :count).by(1) + turma = Turma.last + expect(turma.codigo).to eq('TA') + expect(turma.materia.codigo).to eq('CIC0097') + expect(turma.docente.nome).to eq('Prof. Real') + expect(turma.docente.matricula).to eq('88888') + end + + it 'cria os usuários aluno e professor presentes no JSON' do + expect { described_class.call }.to change(Usuario, :count).by(2) + + described_class.call + + aluno = Usuario.find_by(matricula: '123456789') + expect(aluno).to be_present + expect(aluno.nome).to eq('Fulano Teste') + end + + it 'matricula o aluno na turma' do + described_class.call + aluno = Usuario.find_by(matricula: '123456789') + materia = Materia.find_by(codigo: 'CIC0097') + turma = Turma.find_by(codigo: 'TA', materia: materia) + + expect(aluno.turmas).to include(turma) + end + end + + # Contexto de atualização de dados existentes. + context 'quando os dados já existem mas mudaram no SIGAA' do + let(:classes_json) do + [ + { + "name" => "BANCOS DE DADOS AVANÇADO", + "code" => "CIC0097", + "class" => { + "classCode" => "TA", + "semester" => "2024.1", + "time" => "35T23" + } + } + ].to_json + end + + let(:members_json) do + [ + { + "code" => "CIC0097", + "classCode" => "TA", + "semester" => "2024.1", + "dicente" => [ + { + "nome" => "Fulano da Silva", + "matricula" => "150084006", + "usuario" => "150084006", + "email" => "fulano.novo@gmail.com", + "ocupacao" => "dicente" + } + ], + "docente" => { + "nome" => "Prof. Real", + "usuario" => "88888", + "email" => "prof@real.com", + "ocupacao" => "docente" + } + } + ].to_json + end + + let!(:aluno_existente) do + Usuario.create!( + nome: "Fulano de Tal", + matricula: "150084006", + usuario: "150084006", + email: "fulano.antigo@email.com", + password: "123", ocupacao: :discente, status: true + ) + end + + let!(:turma_existente) do + m = Materia.create!(nome: "BANCOS DE DADOS", codigo: "CIC0097") + + Turma.create!( + codigo: "TA", + materia: m, + docente: docente, + semestre: "2024.1", horario: "35T23" + ) + end + + let!(:aluno_removido) do + Usuario.create!( + nome: "Beltrano", + matricula: "150084008", + usuario: "150084008", email: "beltrano@email.com", password: "123", ocupacao: :discente, status: true + ) + end + + let!(:turma_removida) do + m = Materia.create!(nome: "Materia Velha", codigo: "OLD0001") + + Turma.create!( + codigo: "OLD0001", + materia: m, + docente: docente, semestre: "2024.1", horario: "35T23" + ) + end + + before do + allow(File).to receive(:read).with(satisfy { |p| p.to_s.include?('classes.json') }).and_return(classes_json) + allow(File).to receive(:read).with(satisfy { |p| p.to_s.include?('class_members.json') }).and_return(members_json) + end + + it 'atualiza o e-mail e nome do aluno' do + described_class.call + aluno_existente.reload + + expect(aluno_existente.email).to eq("fulano.novo@gmail.com") + expect(aluno_existente.nome).to eq("Fulano da Silva") + end + + it 'atualiza o nome da turma' do + described_class.call + turma_existente.reload + + expect(turma_existente.materia.reload.nome).to eq("BANCOS DE DADOS AVANÇADO") + end + + it 'matricula o aluno na turma se ele não estava matriculado' do + expect(aluno_existente.turmas).to be_empty + + described_class.call + aluno_existente.reload + + expect(aluno_existente.turmas).to include(turma_existente) + end + + it 'exclui alunos que não estão mais no arquivo' do + described_class.call + + expect(Usuario.find_by(id: aluno_removido.id)).to be_nil + expect(Usuario.find_by(id: aluno_existente.id)).to be_present + end + + it 'exclui turmas que não estão mais no arquivo' do + described_class.call + + expect(Turma.find_by(id: turma_removida.id)).to be_nil + expect(Turma.find_by(id: turma_existente.id)).to be_present + end + end + + # Contexto de testes de feature complexa (emails, criação/update). + context 'funcionalidade de cadastro e convite por email' do + before do + ActionMailer::Base.deliveries.clear + allow(File).to receive(:read).with(satisfy { |p| p.to_s.include?('classes.json') }).and_return(classes_json) + allow(File).to receive(:read).with(satisfy { |p| p.to_s.include?('class_members.json') }).and_return(members_json_feature) + end + + context 'quando importa um usuário novo com email' do + let(:members_json_feature) do + [ + { + "code" => "CIC0097", "classCode" => "TA", + "dicente" => [ + { "nome" => "Novo Aluno", "matricula" => "NEW100", "usuario" => "NEW100", "email" => "novo@email.com", "ocupacao" => "dicente" } + ], + "docente" => { "nome" => "Prof", "usuario" => "P99", "email" => "p@p.com", "ocupacao" => "docente" } + } + ].to_json + end + + it 'cria o usuário e envia e-mail de definição de senha' do + expect { described_class.call }.to change(Usuario, :count).by_at_least(1) + + user = Usuario.find_by(matricula: "NEW100") + expect(user).to be_present + expect(user.status).to be_falsey + + email = ActionMailer::Base.deliveries.find { |e| e.to.include?("novo@email.com") } + expect(email).to be_present + expect(email.subject).to include("Definição de Senha") + end + end + + context 'quando importa um usuário que já existe' do + let!(:aluno_existente_feature) do + Usuario.create!( + nome: "Aluno Existe", matricula: "EXISTE100", usuario: "EXISTE100", + email: "existe@email.com", password: "123", ocupacao: :discente, status: true + ) + end + + let(:members_json_feature) do + [ + { + "code" => "CIC0097", "classCode" => "TA", + "dicente" => [ + { "nome" => "Aluno Existe", "matricula" => "EXISTE100", "usuario" => "EXISTE100", "email" => "existe@email.com", "ocupacao" => "dicente" } + ], + "docente" => { "nome" => "Prof", "usuario" => "P99", "email" => "p@p.com", "ocupacao" => "docente" } + } + ].to_json + end + + it 'não envia e-mail e não duplica o usuário' do + expect { described_class.call }.not_to change { ActionMailer::Base.deliveries.count } + expect(Usuario.where(matricula: "EXISTE100").count).to eq(1) + end + end + + context 'quando importa um usuário novo sem email' do + let(:members_json_feature) do + [ + { + "code" => "CIC0097", "classCode" => "TA", + "dicente" => [ + { "nome" => "Sem Email", "matricula" => "NOEMAIL100", "usuario" => "NOEMAIL100", "email" => nil, "ocupacao" => "dicente" } + ], + "docente" => { "nome" => "Prof", "usuario" => "P99", "email" => "p@p.com", "ocupacao" => "docente" } + } + ].to_json + end + + it 'não cria o usuário no sistema' do + expect { + described_class.call + }.to raise_error(StandardError, /e-mail ausente/) + + expect(Usuario.find_by(matricula: "NOEMAIL100")).to be_nil + end + end + end + + # Contexto de erro de arquivo. + context 'quando os arquivos não são encontrados' do + before do + allow(File).to receive(:read).and_raise(Errno::ENOENT) + end + + it 'lança um erro tratável' do + expect { described_class.call }.to raise_error(StandardError, /Não foi possível buscar os dados/) + end + end + + # Contexto de borda e cobertura adicional. + context 'Cenários de Erro e Cobertura de Borda' do + before do + allow(File).to receive(:read).with(satisfy { |p| p.to_s.include?('classes.json') }).and_return(classes_json) + allow(File).to receive(:read).with(satisfy { |p| p.to_s.include?('class_members.json') }).and_return(members_json) + end + + it 'recria a turma se ela não for encontrada durante o processamento de membros' do + + allow(Turma).to receive(:create!).and_call_original + + allow(Turma).to receive(:find_by).with(hash_including(codigo: "TA")).and_return(nil) + + expect(Turma).to receive(:create!).with(hash_including(codigo: "TA", semestre: "2024.1")) + + described_class.call + end + + it 'registra erro no log quando o envio de e-mail falha' do + new_members_json = [ + { + "code" => "CIC0097", "classCode" => "TA", + "dicente" => [{ "nome" => "Novo", "matricula" => "NEW", "usuario" => "NEW", "email" => "n@n.com", "ocupacao" => "dicente" }], + "docente" => { "nome" => "P", "usuario" => "P", "email" => "p@p.com", "ocupacao" => "docente" } + } + ].to_json + + allow(File).to receive(:read).with(satisfy { |p| p.to_s.include?('class_members.json') }).and_return(new_members_json) + + allow(UserMailer).to receive(:with).and_raise(StandardError, "Erro SMTP Simulado") + + expect(Rails.logger).to receive(:error).with(/Falha ao enviar e-mail para.*Erro SMTP Simulado/) + + described_class.call + end + + it 'inativa o usuário em vez de deletar se ocorrer erro de chave estrangeira' do + usuario_para_remover = Usuario.create!( + nome: "User Old", matricula: "OLD123", usuario: "OLD123", + email: "old@email.com", password: "123", ocupacao: :discente, status: true + ) + + allow_any_instance_of(Usuario).to receive(:destroy).and_wrap_original do |method, *args| + usuario_atual = method.receiver + + if usuario_atual.id == usuario_para_remover.id + raise ActiveRecord::InvalidForeignKey.new("Erro FK simulado") + else + method.call(*args) + end + end + + allow(Rails.logger).to receive(:info) + + described_class.call + + expect(Rails.logger).to have_received(:info).with(/Usuário OLD123 inativado/) + + expect(usuario_para_remover.reload.status).to be(false) + expect(Usuario.exists?(usuario_para_remover.id)).to be(true) + end + end + end +end \ No newline at end of file diff --git a/src/spec/spec_helper.rb b/src/spec/spec_helper.rb new file mode 100644 index 0000000000..9bd842cb7a --- /dev/null +++ b/src/spec/spec_helper.rb @@ -0,0 +1,107 @@ +require 'simplecov' +SimpleCov.start 'rails' do + minimum_coverage 90 +end + +# spec/spec_helper.rb +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration + +# Configuração Base do RSpec. +# +# Define comportamentos de expectativa, mocks e execução de testes. +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/src/storage/.keep b/src/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/storage/test.sqlite3 b/src/storage/test.sqlite3 new file mode 100644 index 0000000000..b4f7039cca Binary files /dev/null and b/src/storage/test.sqlite3 differ diff --git a/src/test/application_system_test_case.rb b/src/test/application_system_test_case.rb new file mode 100644 index 0000000000..cee29fd214 --- /dev/null +++ b/src/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ] +end diff --git a/src/test/controllers/.keep b/src/test/controllers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/fixtures/files/.keep b/src/test/fixtures/files/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/helpers/.keep b/src/test/helpers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/integration/.keep b/src/test/integration/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/mailers/.keep b/src/test/mailers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/models/.keep b/src/test/models/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/system/.keep b/src/test/system/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/test_helper.rb b/src/test/test_helper.rb new file mode 100644 index 0000000000..0c22470ec1 --- /dev/null +++ b/src/test/test_helper.rb @@ -0,0 +1,15 @@ +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" + +module ActiveSupport + class TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... + end +end diff --git a/src/tmp/.keep b/src/tmp/.keep new file mode 100644 index 0000000000..e69de29bb2