From 091e0bcf533fb5d28204d717d1ed68d76cabf81e Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Fri, 22 May 2026 13:58:34 -0300 Subject: [PATCH 01/87] =?UTF-8?q?=F0=9F=8E=89=20Rails=20project=20started?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 51 +++ .gitattributes | 9 + .github/dependabot.yml | 12 + .github/workflows/ci.yml | 130 ++++++ .gitignore | 35 ++ .kamal/hooks/docker-setup.sample | 3 + .kamal/hooks/post-app-boot.sample | 3 + .kamal/hooks/post-deploy.sample | 14 + .kamal/hooks/post-proxy-reboot.sample | 3 + .kamal/hooks/pre-app-boot.sample | 3 + .kamal/hooks/pre-build.sample | 51 +++ .kamal/hooks/pre-connect.sample | 47 ++ .kamal/hooks/pre-deploy.sample | 122 +++++ .kamal/hooks/pre-proxy-reboot.sample | 3 + .kamal/secrets | 20 + .rubocop.yml | 8 + .ruby-version | 1 + Dockerfile | 77 ++++ Gemfile | 66 +++ Gemfile.lock | 417 ++++++++++++++++++ README.md | 26 +- Rakefile | 6 + app/assets/images/.keep | 0 app/assets/stylesheets/application.css | 10 + app/controllers/application_controller.rb | 7 + app/controllers/concerns/.keep | 0 app/helpers/application_helper.rb | 2 + app/javascript/application.js | 3 + app/javascript/controllers/application.js | 9 + .../controllers/hello_controller.js | 7 + app/javascript/controllers/index.js | 4 + app/jobs/application_job.rb | 7 + app/mailers/application_mailer.rb | 4 + app/models/application_record.rb | 3 + app/models/concerns/.keep | 0 app/views/layouts/application.html.erb | 29 ++ app/views/layouts/mailer.html.erb | 13 + app/views/layouts/mailer.text.erb | 1 + app/views/pwa/manifest.json.erb | 22 + app/views/pwa/service-worker.js | 26 ++ bin/brakeman | 7 + bin/bundler-audit | 6 + bin/ci | 6 + bin/dev | 2 + bin/docker-entrypoint | 8 + bin/importmap | 4 + bin/jobs | 6 + bin/kamal | 27 ++ bin/rails | 4 + bin/rake | 4 + bin/rubocop | 8 + bin/setup | 35 ++ bin/thrust | 5 + config.ru | 6 + config/application.rb | 27 ++ config/boot.rb | 4 + config/bundler-audit.yml | 5 + config/cable.yml | 17 + config/cache.yml | 16 + config/ci.rb | 24 + config/credentials.yml.enc | 1 + config/database.yml | 40 ++ config/deploy.yml | 119 +++++ config/environment.rb | 5 + config/environments/development.rb | 78 ++++ config/environments/production.rb | 90 ++++ config/environments/test.rb | 53 +++ config/importmap.rb | 7 + config/initializers/assets.rb | 7 + .../initializers/content_security_policy.rb | 29 ++ .../initializers/filter_parameter_logging.rb | 8 + config/initializers/inflections.rb | 16 + config/locales/en.yml | 31 ++ config/puma.rb | 42 ++ config/queue.yml | 18 + config/recurring.yml | 15 + config/routes.rb | 14 + config/storage.yml | 27 ++ db/cable_schema.rb | 11 + db/cache_schema.rb | 12 + db/queue_schema.rb | 129 ++++++ db/seeds.rb | 9 + lib/tasks/.keep | 0 log/.keep | 0 public/400.html | 135 ++++++ public/404.html | 135 ++++++ public/406-unsupported-browser.html | 135 ++++++ public/422.html | 135 ++++++ public/500.html | 135 ++++++ public/icon.png | Bin 0 -> 4166 bytes public/icon.svg | 3 + public/robots.txt | 1 + script/.keep | 0 storage/.keep | 0 test/controllers/.keep | 0 test/fixtures/files/.keep | 0 test/helpers/.keep | 0 test/integration/.keep | 0 test/mailers/.keep | 0 test/models/.keep | 0 test/test_helper.rb | 15 + tmp/.keep | 0 tmp/pids/.keep | 0 tmp/storage/.keep | 0 vendor/.keep | 0 vendor/javascript/.keep | 0 106 files changed, 2928 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100755 .kamal/hooks/docker-setup.sample create mode 100755 .kamal/hooks/post-app-boot.sample create mode 100755 .kamal/hooks/post-deploy.sample create mode 100755 .kamal/hooks/post-proxy-reboot.sample create mode 100755 .kamal/hooks/pre-app-boot.sample create mode 100755 .kamal/hooks/pre-build.sample create mode 100755 .kamal/hooks/pre-connect.sample create mode 100755 .kamal/hooks/pre-deploy.sample create mode 100755 .kamal/hooks/pre-proxy-reboot.sample create mode 100644 .kamal/secrets create mode 100644 .rubocop.yml create mode 100644 .ruby-version create mode 100644 Dockerfile create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 Rakefile create mode 100644 app/assets/images/.keep create mode 100644 app/assets/stylesheets/application.css create mode 100644 app/controllers/application_controller.rb create mode 100644 app/controllers/concerns/.keep create mode 100644 app/helpers/application_helper.rb create mode 100644 app/javascript/application.js create mode 100644 app/javascript/controllers/application.js create mode 100644 app/javascript/controllers/hello_controller.js create mode 100644 app/javascript/controllers/index.js create mode 100644 app/jobs/application_job.rb create mode 100644 app/mailers/application_mailer.rb create mode 100644 app/models/application_record.rb create mode 100644 app/models/concerns/.keep create mode 100644 app/views/layouts/application.html.erb create mode 100644 app/views/layouts/mailer.html.erb create mode 100644 app/views/layouts/mailer.text.erb create mode 100644 app/views/pwa/manifest.json.erb create mode 100644 app/views/pwa/service-worker.js create mode 100755 bin/brakeman create mode 100755 bin/bundler-audit create mode 100755 bin/ci create mode 100755 bin/dev create mode 100755 bin/docker-entrypoint create mode 100755 bin/importmap create mode 100755 bin/jobs create mode 100755 bin/kamal create mode 100755 bin/rails create mode 100755 bin/rake create mode 100755 bin/rubocop create mode 100755 bin/setup create mode 100755 bin/thrust create mode 100644 config.ru create mode 100644 config/application.rb create mode 100644 config/boot.rb create mode 100644 config/bundler-audit.yml create mode 100644 config/cable.yml create mode 100644 config/cache.yml create mode 100644 config/ci.rb create mode 100644 config/credentials.yml.enc create mode 100644 config/database.yml create mode 100644 config/deploy.yml create mode 100644 config/environment.rb create mode 100644 config/environments/development.rb create mode 100644 config/environments/production.rb create mode 100644 config/environments/test.rb create mode 100644 config/importmap.rb create mode 100644 config/initializers/assets.rb create mode 100644 config/initializers/content_security_policy.rb create mode 100644 config/initializers/filter_parameter_logging.rb create mode 100644 config/initializers/inflections.rb create mode 100644 config/locales/en.yml create mode 100644 config/puma.rb create mode 100644 config/queue.yml create mode 100644 config/recurring.yml create mode 100644 config/routes.rb create mode 100644 config/storage.yml create mode 100644 db/cable_schema.rb create mode 100644 db/cache_schema.rb create mode 100644 db/queue_schema.rb create mode 100644 db/seeds.rb create mode 100644 lib/tasks/.keep create mode 100644 log/.keep create mode 100644 public/400.html create mode 100644 public/404.html create mode 100644 public/406-unsupported-browser.html create mode 100644 public/422.html create mode 100644 public/500.html create mode 100644 public/icon.png create mode 100644 public/icon.svg create mode 100644 public/robots.txt create mode 100644 script/.keep create mode 100644 storage/.keep create mode 100644 test/controllers/.keep create mode 100644 test/fixtures/files/.keep create mode 100644 test/helpers/.keep create mode 100644 test/integration/.keep create mode 100644 test/mailers/.keep create mode 100644 test/models/.keep create mode 100644 test/test_helper.rb create mode 100644 tmp/.keep create mode 100644 tmp/pids/.keep create mode 100644 tmp/storage/.keep create mode 100644 vendor/.keep create mode 100644 vendor/javascript/.keep diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..325bfc036d --- /dev/null +++ b/.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/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..8dc4323435 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..83610cfa4c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..1a34dc2f17 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,130 @@ +name: CI + +on: + pull_request: + push: + branches: [ main ] + +jobs: + scan_ruby: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Scan for common Rails security vulnerabilities using static analysis + run: bin/brakeman --no-pager + + - name: Scan for known security vulnerabilities in gems used + run: bin/bundler-audit + + scan_js: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Scan for security vulnerabilities in JavaScript dependencies + run: bin/importmap audit + + lint: + runs-on: ubuntu-latest + env: + RUBOCOP_CACHE_ROOT: tmp/rubocop + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Prepare RuboCop cache + uses: actions/cache@v4 + env: + DEPENDENCIES_HASH: ${{ hashFiles('.ruby-version', '**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }} + with: + path: ${{ env.RUBOCOP_CACHE_ROOT }} + key: rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }} + restore-keys: | + rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}- + + - name: Lint code for consistent style + run: bin/rubocop -f github + + test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: valkey/valkey:8 + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Install packages + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libvips + + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run tests + env: + RAILS_ENV: test + # RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test + + system-test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: valkey/valkey:8 + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Install packages + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libvips + + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run System Tests + env: + RAILS_ENV: test + # RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test:system + + - name: Keep screenshots from failed system tests + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots + path: ${{ github.workspace }}/tmp/screenshots + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..fbcab405eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# Temporary files generated by your text editor or operating system +# belong in git's global ignore instead: +# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# 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 + +# Ignore key files for decrypting credentials and more. +/config/*.key + diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000000..2fb07d7d7a --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample new file mode 100755 index 0000000000..70f9c4bc95 --- /dev/null +++ b/.kamal/hooks/post-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000000..fd364c2a77 --- /dev/null +++ b/.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/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 0000000000..1435a677f2 --- /dev/null +++ b/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample new file mode 100755 index 0000000000..45f7355045 --- /dev/null +++ b/.kamal/hooks/pre-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000000..c5a55678b2 --- /dev/null +++ b/.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/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100755 index 0000000000..77744bdca8 --- /dev/null +++ b/.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/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100755 index 0000000000..05b3055b72 --- /dev/null +++ b/.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/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 0000000000..061f8059e6 --- /dev/null +++ b/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets b/.kamal/secrets new file mode 100644 index 0000000000..b3089d6f5a --- /dev/null +++ b/.kamal/secrets @@ -0,0 +1,20 @@ +# 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}) + +# Example of extracting secrets from Rails credentials +# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password) + +# 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/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000000..f9d86d4a54 --- /dev/null +++ b/.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/.ruby-version b/.ruby-version new file mode 100644 index 0000000000..61a52c9149 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-3.4.9 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..265e7fc660 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,77 @@ +# 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 trabalho_final_eng_soft . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name trabalho_final_eng_soft trabalho_final_eng_soft + +# 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.4.9 +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 && \ + ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment variables and enable jemalloc for reduced memory usage and latency. +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" \ + LD_PRELOAD="/usr/local/lib/libjemalloc.so" + +# 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 libvips libyaml-dev pkg-config && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY vendor/* ./vendor/ +COPY Gemfile Gemfile.lock ./ + +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + # -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495 + bundle exec bootsnap precompile -j 1 --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times. +# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495 +RUN bundle exec bootsnap precompile -j 1 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 + +# 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 +USER 1000:1000 + +# Copy built artifacts: gems, application +COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --chown=rails:rails --from=build /rails /rails + +# 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/Gemfile b/Gemfile new file mode 100644 index 0000000000..5509758eee --- /dev/null +++ b/Gemfile @@ -0,0 +1,66 @@ +source "https://rubygems.org" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 8.1.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" + + # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues) + gem "bundler-audit", 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 +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..c19d246c69 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,417 @@ +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.19) + railties + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) + 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.1.3) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.3) + activesupport (= 8.1.3) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.3.6) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) + timeout (>= 0.4.0) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) + marcel (~> 1.0) + activesupport (8.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + base64 (0.3.0) + bcrypt_pbkdf (1.1.2) + bigdecimal (4.1.2) + bindex (0.8.1) + bootsnap (1.24.5) + msgpack (~> 1.2) + brakeman (8.0.4) + racc + builder (3.3.0) + bundler-audit (0.9.3) + bundler (>= 1.2.0) + thor (~> 1.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) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + crass (1.0.6) + date (3.5.1) + debug (1.11.1) + irb (~> 1.10) + reline (>= 0.3.8) + dotenv (3.2.0) + drb (2.2.3) + ed25519 (1.4.0) + erb (6.0.4) + erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + fugit (1.12.1) + et-orbi (~> 1.4) + raabro (~> 1.4) + globalid (1.3.0) + activesupport (>= 6.1) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.15.0) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + json (2.19.5) + kamal (2.11.0) + 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) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.25.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.2.1) + matrix (0.4.3) + mini_magick (5.3.1) + logger + mini_mime (1.1.5) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + msgpack (1.8.0) + net-imap (0.6.4) + 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.2) + nio4r (2.7.5) + nokogiri (1.19.3-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.3-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.3-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.3-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.3-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.3-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.3-x86_64-linux-musl) + racc (~> 1.4) + ostruct (0.6.3) + parallel (2.1.0) + parser (3.3.11.1) + ast (~> 2.4.1) + racc + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + propshaft (1.3.2) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + psych (5.3.1) + date + stringio + public_suffix (7.0.5) + puma (8.0.1) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.2.6) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + bundler (>= 1.15.0) + railties (= 8.1.3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + 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.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + 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.4.2) + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.12.0) + reline (0.6.3) + io-console (~> 0.5) + rexml (3.4.4) + rubocop (1.86.2) + 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.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + 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.35.2) + 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-vips (2.3.0) + ffi (~> 1.12) + logger + rubyzip (3.3.0) + securerandom (0.4.1) + selenium-webdriver (4.44.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + 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.4.0) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) + sqlite3 (2.9.4-aarch64-linux-gnu) + sqlite3 (2.9.4-aarch64-linux-musl) + sqlite3 (2.9.4-arm-linux-gnu) + sqlite3 (2.9.4-arm-linux-musl) + sqlite3 (2.9.4-arm64-darwin) + sqlite3 (2.9.4-x86_64-linux-gnu) + sqlite3 (2.9.4-x86_64-linux-musl) + sshkit (1.25.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.2.0) + thor (1.5.0) + thruster (0.1.21) + thruster (0.1.21-aarch64-linux) + thruster (0.1.21-arm64-darwin) + thruster (0.1.21-x86_64-linux) + timeout (0.6.1) + tsort (0.2.0) + turbo-rails (2.0.23) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + useragent (0.16.11) + web-console (4.3.0) + actionview (>= 8.0.0) + bindex (>= 0.4.0) + railties (>= 8.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.8.1) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin-24 + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + bootsnap + brakeman + bundler-audit + capybara + debug + image_processing (~> 1.2) + importmap-rails + jbuilder + kamal + propshaft + puma (>= 5.0) + rails (~> 8.1.3) + rubocop-rails-omakase + selenium-webdriver + solid_cable + solid_cache + solid_queue + sqlite3 (>= 2.1) + stimulus-rails + thruster + turbo-rails + tzinfo-data + web-console + +BUNDLED WITH + 2.6.9 diff --git a/README.md b/README.md index 9d7fe1bf53..7db80e4ca1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,24 @@ -# CAMAAR -Sistema para avaliação de atividades acadêmicas remotas do CIC +# README + +This README would normally document whatever steps are necessary to get the +application up and running. + +Things you may want to cover: + +* Ruby version + +* System dependencies + +* Configuration + +* Database creation + +* Database initialization + +* How to run the test suite + +* Services (job queues, cache servers, search engines, etc.) + +* Deployment instructions + +* ... diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000000..9a5ea7383a --- /dev/null +++ b/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/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..fe93333c0f --- /dev/null +++ b/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/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000000..c3537563da --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,7 @@ +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 + + # Changes to the importmap will invalidate the etag for HTML responses + stale_when_importmap_changes +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000000..de6be7945c --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000000..0d7b49404c --- /dev/null +++ b/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/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 0000000000..1213e85c7a --- /dev/null +++ b/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/app/javascript/controllers/hello_controller.js b/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000000..5975c0789d --- /dev/null +++ b/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/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 0000000000..1156bf8362 --- /dev/null +++ b/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/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000000..d394c3d106 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +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/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000000..3c34c8148f --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000000..b63caeb8a5 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..4c9138b1e4 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,29 @@ + + + + <%= content_for(:title) || "Trabalho Final Eng Soft" %> + + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> + + + + + + <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..3aac9002ed --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb new file mode 100644 index 0000000000..6c50be8c6b --- /dev/null +++ b/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "TrabalhoFinalEngSoft", + "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": "TrabalhoFinalEngSoft.", + "theme_color": "red", + "background_color": "red" +} diff --git a/app/views/pwa/service-worker.js b/app/views/pwa/service-worker.js new file mode 100644 index 0000000000..b3a13fb7bb --- /dev/null +++ b/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/bin/brakeman b/bin/brakeman new file mode 100755 index 0000000000..ace1c9ba08 --- /dev/null +++ b/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/bin/bundler-audit b/bin/bundler-audit new file mode 100755 index 0000000000..e2ef22690c --- /dev/null +++ b/bin/bundler-audit @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "bundler/audit/cli" + +ARGV.concat %w[ --config config/bundler-audit.yml ] if ARGV.empty? || ARGV.include?("check") +Bundler::Audit::CLI.start diff --git a/bin/ci b/bin/ci new file mode 100755 index 0000000000..4137ad5bb0 --- /dev/null +++ b/bin/ci @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "active_support/continuous_integration" + +CI = ActiveSupport::ContinuousIntegration +require_relative "../config/ci.rb" diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000000..5f91c20545 --- /dev/null +++ b/bin/dev @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000000..ed31659f40 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# 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/bin/importmap b/bin/importmap new file mode 100755 index 0000000000..36502ab16c --- /dev/null +++ b/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/bin/jobs b/bin/jobs new file mode 100755 index 0000000000..dcf59f309a --- /dev/null +++ b/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/bin/kamal b/bin/kamal new file mode 100755 index 0000000000..cbe59b95ed --- /dev/null +++ b/bin/kamal @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# 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/bin/rails b/bin/rails new file mode 100755 index 0000000000..efc0377492 --- /dev/null +++ b/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/bin/rake b/bin/rake new file mode 100755 index 0000000000..4fbf10b960 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 0000000000..5a20504716 --- /dev/null +++ b/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/bin/setup b/bin/setup new file mode 100755 index 0000000000..81be011e87 --- /dev/null +++ b/bin/setup @@ -0,0 +1,35 @@ +#!/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" + system! "bin/rails db:reset" if ARGV.include?("--reset") + + 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/bin/thrust b/bin/thrust new file mode 100755 index 0000000000..36bde2d832 --- /dev/null +++ b/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/config.ru b/config.ru new file mode 100644 index 0000000000..4a3c09a688 --- /dev/null +++ b/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/config/application.rb b/config/application.rb new file mode 100644 index 0000000000..924ee2d890 --- /dev/null +++ b/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 TrabalhoFinalEngSoft + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.1 + + # 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 = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000000..988a5ddc46 --- /dev/null +++ b/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/config/bundler-audit.yml b/config/bundler-audit.yml new file mode 100644 index 0000000000..e74b3af949 --- /dev/null +++ b/config/bundler-audit.yml @@ -0,0 +1,5 @@ +# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit. +# CVEs that are not relevant to the application can be enumerated on the ignore list below. + +ignore: + - CVE-THAT-DOES-NOT-APPLY diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000000..b9adc5aa3a --- /dev/null +++ b/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/config/cache.yml b/config/cache.yml new file mode 100644 index 0000000000..19d490843b --- /dev/null +++ b/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/config/ci.rb b/config/ci.rb new file mode 100644 index 0000000000..1712cc1127 --- /dev/null +++ b/config/ci.rb @@ -0,0 +1,24 @@ +# Run using bin/ci + +CI.run do + step "Setup", "bin/setup --skip-server" + + step "Style: Ruby", "bin/rubocop" + + step "Security: Gem audit", "bin/bundler-audit" + step "Security: Importmap vulnerability audit", "bin/importmap audit" + step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" + step "Tests: Rails", "bin/rails test" + step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant" + + # Optional: Run system tests + # step "Tests: System", "bin/rails test:system" + + # Optional: set a green GitHub commit status to unblock PR merge. + # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`. + # if success? + # step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" + # else + # failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again." + # end +end diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000000..7fb22fc0cf --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +mjRyVXhSn6f910REf8LItBjEXxR40VKHHdlTAA6yJ4+GmBQCzKbOSA7YfXjLyMLdi4WxB/wOwfE8EytiCyaq8wiTCsamkXTRaHXZer4XG/z9LNK4llEA5nDcrn/7pq/W+IsZnnMJnPaKnysQm+7IzV8q0V1A0HuKU5Q3MffCSbv6Qpu79zIUJd2iJ9JPiyKejI1ng3HoM/wK8vofZGEYo0JbyviVW3SKXDdPwsRE8hgDioYlQsddbWZKRej6tHaSZRIZv4dEwaTXMm2TewLJquTGE2I964iJD4XNz+wOhOIJsNzbhKUr+b2/8CdnGLoERt3tPcBW2xF86+RX8SecXL5hZiBYRb82b94gkP9SnC5pJp7Tc9GEK972OMNKDebtTqq4+ZYk9G6WXls479QkUWaIyiDs9w0NiozRq4KTSVCSQ9WZuj8V4pfyhx7hT/iatYFv19shjNas+lg//JEuUmbZhT2wU4GpHkWmORY4S3zG5JoE05m9GiUt--zP8eXi+0+r0z/0Wa--cv+BnPP1pzPpcLmCyxQ74g== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000000..302d638c96 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,40 @@ +# 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 + max_connections: <%= 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/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000000..0583990c5e --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,119 @@ +# Name of your application. Used to uniquely configure containers. +service: trabalho_final_eng_soft + +# Name of the container image (use your-user/app-name on external registries). +image: trabalho_final_eng_soft + +# 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. +# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +# +# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb! +# +# Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer). +# +# proxy: +# ssl: true +# host: app.example.com + +# Where you keep your container images. +registry: + # Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ... + server: localhost:5555 + + # Needed for authenticated registries. + # 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 trabalho_final_eng_soft-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 --include-password" + +# 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: + - "trabalho_final_eng_soft_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.4.9 + # 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: valkey/valkey:8 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000000..cac5315775 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000000..75243c3d0f --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,78 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # 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 + + # 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 + + # Highlight code that triggered redirect in logs. + config.action_dispatch.verbose_redirect_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = 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/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000000..f5763e04e5 --- /dev/null +++ b/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 bin/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/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000000..c2095b1174 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,53 @@ +# 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 + # 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/config/importmap.rb b/config/importmap.rb new file mode 100644 index 0000000000..909dfc542d --- /dev/null +++ b/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/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000000..487324424f --- /dev/null +++ b/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/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..d51d713979 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,29 @@ +# 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) +# +# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag` +# # if the corresponding directives are specified in `content_security_policy_nonce_directives`. +# # config.content_security_policy_nonce_auto = true +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..c0b717f7ec --- /dev/null +++ b/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/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000000..3860f659ea --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# 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 ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000000..6c349ae5e3 --- /dev/null +++ b/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/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000000..38c4b86596 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,42 @@ +# 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. You can set it to `auto` to automatically start a worker +# for each available processor. +# +# 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/config/queue.yml b/config/queue.yml new file mode 100644 index 0000000000..9eace59c41 --- /dev/null +++ b/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/config/recurring.yml b/config/recurring.yml new file mode 100644 index 0000000000..b4207f9b07 --- /dev/null +++ b/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/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000000..48254e88ed --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,14 @@ +Rails.application.routes.draw do + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. + # Can be used by load balancers and uptime monitors to verify that the app is live. + get "up" => "rails/health#show", as: :rails_health_check + + # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) + # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest + # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker + + # Defines the root path route ("/") + # root "posts#index" +end diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000000..927dc537c8 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,27 @@ +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 %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/db/cable_schema.rb b/db/cable_schema.rb new file mode 100644 index 0000000000..23666604a5 --- /dev/null +++ b/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/db/cache_schema.rb b/db/cache_schema.rb new file mode 100644 index 0000000000..81a410d188 --- /dev/null +++ b/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/db/queue_schema.rb b/db/queue_schema.rb new file mode 100644 index 0000000000..85194b6a88 --- /dev/null +++ b/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/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000000..4fbd6ed970 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,9 @@ +# This file should ensure the existence of records required to run the application in every environment (production, +# development, test). The code here should be idempotent so that it can be executed at any point in every environment. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Example: +# +# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| +# MovieGenre.find_or_create_by!(name: genre_name) +# end diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/400.html b/public/400.html new file mode 100644 index 0000000000..640de03397 --- /dev/null +++ b/public/400.html @@ -0,0 +1,135 @@ + + + + + + + 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/public/404.html b/public/404.html new file mode 100644 index 0000000000..d7f0f14222 --- /dev/null +++ b/public/404.html @@ -0,0 +1,135 @@ + + + + + + + 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/public/406-unsupported-browser.html b/public/406-unsupported-browser.html new file mode 100644 index 0000000000..43d2811e8c --- /dev/null +++ b/public/406-unsupported-browser.html @@ -0,0 +1,135 @@ + + + + + + + Your browser is not supported (406 Not Acceptable) + + + + + + + + + + + + + +
+
+ +
+
+

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

+
+
+ + + + diff --git a/public/422.html b/public/422.html new file mode 100644 index 0000000000..f12fb4aa17 --- /dev/null +++ b/public/422.html @@ -0,0 +1,135 @@ + + + + + + + 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/public/500.html b/public/500.html new file mode 100644 index 0000000000..e4eb18a759 --- /dev/null +++ b/public/500.html @@ -0,0 +1,135 @@ + + + + + + + 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/public/icon.png b/public/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c4c9dbfbbd2f7c1421ffd5727188146213abbcef GIT binary patch literal 4166 zcmd6qU;WFw?|v@m)Sk^&NvB8tcujdV-r1b=i(NJxn&7{KTb zX$3(M+3TP2o^#KAo{#tIjl&t~(8D-k004kqPglzn0HFG(Q~(I*AKsD#M*g7!XK0T7 zN6P7j>HcT8rZgKl$v!xr806dyN19Bd4C0x_R*I-a?#zsTvb_89cyhuC&T**i|Rc zq5b8M;+{8KvoJ~uj9`u~d_f6`V&3+&ZX9x5pc8s)d175;@pjm(?dapmBcm0&vl9+W zx1ZD2o^nuyUHWj|^A8r>lUorO`wFF;>9XL-Jy!P}UXC{(z!FO%SH~8k`#|9;Q|eue zqWL0^Bp(fg_+Pkm!fDKRSY;+^@BF?AJE zCUWpXPst~hi_~u)SzYBDZroR+Z4xeHIlm_3Yc_9nZ(o_gg!jDgVa=E}Y8uDgem9`b zf=mfJ_@(BXSkW53B)F2s!&?_R4ptb1fYXlF++@vPhd=marQgEGRZS@B4g1Mu?euknL= z67P~tZ?*>-Hmi7GwlisNHHJDku-dSm7g@!=a}9cSL6Pa^w^2?&?$Oi8ibrr>w)xqx zOH_EMU@m05)9kuNR>>4@H%|){U$^yvVQ(YgOlh;5oU_-vivG-p4=LrN-k7D?*?u1u zsWly%tfAzKd6Fb=`eU2un_uaTXmcT#tlOL+aRS=kZZf}A7qT8lvcTx~7j` z*b>=z)mwg7%B2_!D0!1IZ?Nq{^Y$uI4Qx*6T!E2Col&2{k?ImCO=dD~A&9f9diXy^$x{6CwkBimn|1E09 zAMSezYtiL?O6hS37KpvDM?22&d{l)7h-!F)C-d3j8Z`c@($?mfd{R82)H>Qe`h{~G z!I}(2j(|49{LR?w4Jspl_i!(4T{31|dqCOpI52r5NhxYV+cDAu(xp*4iqZ2e-$YP= zoFOPmm|u*7C?S{Fp43y+V;>~@FFR76bCl@pTtyB93vNWy5yf;HKr8^0d7&GVIslYm zo3Tgt@M!`8B6IW&lK{Xk>%zp41G%`(DR&^u z5^pwD4>E6-w<8Kl2DzJ%a@~QDE$(e87lNhy?-Qgep!$b?5f7+&EM7$e>|WrX+=zCb z=!f5P>MxFyy;mIRxjc(H*}mceXw5a*IpC0PEYJ8Y3{JdoIW)@t97{wcUB@u+$FCCO z;s2Qe(d~oJC^`m$7DE-dsha`glrtu&v&93IZadvl_yjp!c89>zo;Krk+d&DEG4?x$ zufC1n+c1XD7dolX1q|7}uelR$`pT0Z)1jun<39$Sn2V5g&|(j~Z!wOddfYiZo7)A< z!dK`aBHOOk+-E_xbWCA3VR-+o$i5eO9`rMI#p_0xQ}rjEpGW;U!&&PKnivOcG(|m9 z!C8?WC6nCXw25WVa*eew)zQ=h45k8jSIPbq&?VE{oG%?4>9rwEeB4&qe#?-y_es4c|7ufw%+H5EY#oCgv!Lzv291#-oNlX~X+Jl5(riC~r z=0M|wMOP)Tt8@hNg&%V@Z9@J|Q#K*hE>sr6@oguas9&6^-=~$*2Gs%h#GF@h)i=Im z^iKk~ipWJg1VrvKS;_2lgs3n1zvNvxb27nGM=NXE!D4C!U`f*K2B@^^&ij9y}DTLB*FI zEnBL6y{jc?JqXWbkIZd7I16hA>(f9T!iwbIxJj~bKPfrO;>%*5nk&Lf?G@c2wvGrY&41$W{7HM9+b@&XY@>NZM5s|EK_Dp zQX60CBuantx>|d#DsaZ*8MW(we|#KTYZ=vNa#d*DJQe6hr~J6{_rI#?wi@s|&O}FR zG$kfPxheXh1?IZ{bDT-CWB4FTvO-k5scW^mi8?iY5Q`f8JcnnCxiy@m@D-%lO;y0pTLhh6i6l@x52j=#^$5_U^os}OFg zzdHbo(QI`%9#o*r8GCW~T3UdV`szO#~)^&X_(VW>o~umY9-ns9-V4lf~j z`QBD~pJ4a#b`*6bJ^3RS5y?RAgF7K5$ll97Y8#WZduZ`j?IEY~H(s^doZg>7-tk*t z4_QE1%%bb^p~4F5SB$t2i1>DBG1cIo;2(xTaj*Y~hlM{tSDHojL-QPg%Mo%6^7FrpB*{ z4G0@T{-77Por4DCMF zB_5Y~Phv%EQ64W8^GS6h?x6xh;w2{z3$rhC;m+;uD&pR74j+i22P5DS-tE8ABvH(U~indEbBUTAAAXfHZg5QpB@TgV9eI<)JrAkOI z8!TSOgfAJiWAXeM&vR4Glh;VxH}WG&V$bVb`a`g}GSpwggti*&)taV1@Ak|{WrV|5 zmNYx)Ans=S{c52qv@+jmGQ&vd6>6yX6IKq9O$3r&0xUTdZ!m1!irzn`SY+F23Rl6# zFRxws&gV-kM1NX(3(gnKpGi0Q)Dxi~#?nyzOR9!en;Ij>YJZVFAL*=R%7y%Mz9hU% zs>+ZB?qRmZ)nISx7wxY)y#cd$iaC~{k0avD>BjyF1q^mNQ1QcwsxiTySe<6C&cC6P zE`vwO9^k-d`9hZ!+r@Jnr+MF*2;2l8WjZ}DrwDUHzSF{WoG zucbSWguA!3KgB3MU%HH`R;XqVv0CcaGq?+;v_A5A2kpmk5V%qZE3yzQ7R5XWhq=eR zyUezH=@V)y>L9T-M-?tW(PQYTRBKZSVb_!$^H-Pn%ea;!vS_?M<~Tm>_rWIW43sPW z=!lY&fWc1g7+r?R)0p8(%zp&vl+FK4HRkns%BW+Up&wK8!lQ2~bja|9bD12WrKn#M zK)Yl9*8$SI7MAwSK$%)dMd>o+1UD<2&aQMhyjS5R{-vV+M;Q4bzl~Z~=4HFj_#2V9 zB)Gfzx3ncy@uzx?yzi}6>d%-?WE}h7v*w)Jr_gBl!2P&F3DX>j_1#--yjpL%<;JMR z*b70Gr)MMIBWDo~#<5F^Q0$VKI;SBIRneuR7)yVsN~A9I@gZTXe)E?iVII+X5h0~H zx^c(fP&4>!*q>fb6dAOC?MI>Cz3kld#J*;uik+Ps49cwm1B4 zZc1|ZxYyTv;{Z!?qS=D)sgRKx^1AYf%;y_V&VgZglfU>d+Ufk5&LV$sKv}Hoj+s; xK3FZRYdhbXT_@RW*ff3@`D1#ps#~H)p+y&j#(J|vk^lW{fF9OJt5(B-_&*Xgn9~3N literal 0 HcmV?d00001 diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000000..04b34bf83f --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000000..c19f78ab68 --- /dev/null +++ b/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/script/.keep b/script/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000000..0c22470ec1 --- /dev/null +++ b/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/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tmp/storage/.keep b/tmp/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep new file mode 100644 index 0000000000..e69de29bb2 From 5c78a65d9f5dfc45857750e63188328ef0b441b9 Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Fri, 22 May 2026 14:00:11 -0300 Subject: [PATCH 02/87] =?UTF-8?q?=F0=9F=94=A7=20Cucumber=20installed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 6 +++ Gemfile.lock | 43 +++++++++++++++++++ bin/cucumber | 11 +++++ config/cucumber.yml | 8 ++++ config/environments/development.rb | 4 ++ config/environments/test.rb | 4 ++ features/step_definitions/.keep | 0 features/support/env.rb | 53 +++++++++++++++++++++++ lib/tasks/cucumber.rake | 69 ++++++++++++++++++++++++++++++ 9 files changed, 198 insertions(+) create mode 100755 bin/cucumber create mode 100644 config/cucumber.yml create mode 100644 features/step_definitions/.keep create mode 100644 features/support/env.rb create mode 100644 lib/tasks/cucumber.rake diff --git a/Gemfile b/Gemfile index 5509758eee..933e364877 100644 --- a/Gemfile +++ b/Gemfile @@ -64,3 +64,9 @@ group :test do gem "capybara" gem "selenium-webdriver" end + +group :test do + gem 'cucumber-rails', require: false + # database_cleaner is not mandatory, but highly recommended + gem 'database_cleaner' +end diff --git a/Gemfile.lock b/Gemfile.lock index c19d246c69..9473bbd233 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -102,10 +102,46 @@ GEM concurrent-ruby (1.3.6) connection_pool (3.0.2) crass (1.0.6) + cucumber (10.2.0) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 12) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 20) + cucumber-html-formatter (> 21, < 23) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.3) + cucumber-ci-environment (11.0.0) + cucumber-core (16.2.0) + cucumber-gherkin (> 36, < 40) + cucumber-messages (> 31, < 33) + cucumber-tag-expressions (> 6, < 9) + cucumber-cucumber-expressions (19.0.0) + bigdecimal + cucumber-gherkin (39.1.0) + cucumber-messages (>= 31, < 33) + cucumber-html-formatter (22.3.0) + cucumber-messages (> 23, < 33) + cucumber-messages (32.3.1) + cucumber-rails (4.0.1) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 11) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.1.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.1) debug (1.11.1) irb (~> 1.10) reline (>= 0.3.8) + diff-lcs (1.6.2) dotenv (3.2.0) drb (2.2.3) ed25519 (1.4.0) @@ -169,6 +205,7 @@ GEM net-smtp marcel (1.2.1) matrix (0.4.3) + memoist3 (1.0.0) mini_magick (5.3.1) logger mini_mime (1.1.5) @@ -176,6 +213,7 @@ GEM drb (~> 2.0) prism (~> 1.5) msgpack (1.8.0) + multi_test (1.1.0) net-imap (0.6.4) date net-protocol @@ -347,6 +385,9 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.2.0) + sys-uname (1.5.1) + ffi (~> 1.1) + memoist3 (~> 1.0.0) thor (1.5.0) thruster (0.1.21) thruster (0.1.21-aarch64-linux) @@ -393,6 +434,8 @@ DEPENDENCIES brakeman bundler-audit capybara + cucumber-rails + database_cleaner debug image_processing (~> 1.2) importmap-rails diff --git a/bin/cucumber b/bin/cucumber new file mode 100755 index 0000000000..eb5e962e86 --- /dev/null +++ b/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/config/cucumber.yml b/config/cucumber.yml new file mode 100644 index 0000000000..47a4663ae2 --- /dev/null +++ b/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/config/environments/development.rb b/config/environments/development.rb index 75243c3d0f..7d1b179ef2 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,6 +1,10 @@ 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. diff --git a/config/environments/test.rb b/config/environments/test.rb index c2095b1174..e6b5c1b020 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -4,6 +4,10 @@ # 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. diff --git a/features/step_definitions/.keep b/features/step_definitions/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 0000000000..3b97d14087 --- /dev/null +++ b/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/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake new file mode 100644 index 0000000000..0caa4d2553 --- /dev/null +++ b/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 From b80ece02237d9c30ebc9e2810d7ae8f907e9e926 Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Fri, 22 May 2026 14:23:15 -0300 Subject: [PATCH 03/87] =?UTF-8?q?=E2=9C=85=20Adiciona=20feature=20de=20log?= =?UTF-8?q?in=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/login.feature | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 features/login.feature diff --git a/features/login.feature b/features/login.feature new file mode 100644 index 0000000000..1d1a12a5b6 --- /dev/null +++ b/features/login.feature @@ -0,0 +1,62 @@ +# language: pt + +Funcionalidade: Login no sistema CAMAAR + Como um 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 estou na página de login do CAMAAR + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Login bem-sucedido com e-mail válido + Quando preencho o campo "E-mail ou Matrícula" com "user@gmail.com" + E preencho o campo "Senha" com "senhaUser" + E clico no botão "Entrar" + Então sou redirecionado para o painel principal + E vejo a mensagem de boas-vindas "Bem-vindo(a), Usuário" + + Cenário: Login bem-sucedido com matrícula válida + Quando preencho o campo "E-mail ou Matrícula" com "190084006" + E preencho o campo "Senha" com "senhaUser" + E clico no botão "Entrar" + Então sou redirecionado para o painel principal + E vejo a mensagem de boas-vindas "Bem-vindo(a), Usuário" + + Cenário: Login como administrador exibe menu de gerenciamento + Quando preencho o campo "E-mail ou Matrícula" com "admin@gmail.com" + E preencho o campo "Senha" com "senhaAdmin" + E clico no botão "Entrar" + Então sou redirecionado para o painel principal + E vejo a opção "Gerenciamento" no menu lateral + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Login com senha incorreta + Quando preencho o campo "E-mail ou Matrícula" com "user@gmail.com" + E preencho o campo "Senha" com "senhaErrada" + E clico no botão "Entrar" + Então permaneço na página de login + E vejo a mensagem de erro "E-mail/matrícula ou senha inválidos" + + Cenário: Login com usuário não cadastrado + Quando preencho o campo "E-mail ou Matrícula" com "nao.cadastrado@gmail.com" + E preencho o campo "Senha" com "qualquerSenha" + E clico no botão "Entrar" + Então permaneço na página de login + E vejo a mensagem de erro "E-mail/matrícula ou senha inválidos" + + Cenário: Login com campos vazios + Quando deixo o campo "E-mail ou Matrícula" em branco + E deixo o campo "Senha" em branco + E clico no botão "Entrar" + Então permaneço na página de login + E vejo a mensagem de erro "Preencha todos os campos obrigatórios" + + Cenário: Login como administrador não exibe menu de gerenciamento + Quando preencho o campo "E-mail ou Matrícula" com "admin@gmail.com" + E preencho o campo "Senha" com "senhaAdmin" + E clico no botão "Entrar" + Então sou redirecionado para o painel principal + E não vejo a opção "Gerenciamento" no menu lateral \ No newline at end of file From 2d9c3b2d427af2a7ad40636226695664fcfe81ca Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Fri, 22 May 2026 14:30:44 -0300 Subject: [PATCH 04/87] =?UTF-8?q?=E2=9C=85=20Adiciona=20feature=20de=20def?= =?UTF-8?q?ini=C3=A7=C3=A3o=20de=20senha=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/def_senha.feature | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 features/def_senha.feature diff --git a/features/def_senha.feature b/features/def_senha.feature new file mode 100644 index 0000000000..74383c2580 --- /dev/null +++ b/features/def_senha.feature @@ -0,0 +1,49 @@ +# language: pt + +Funcionalidade: Definir senha a partir do e-mail de solicitação de cadastro + Como um 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 recebi um e-mail de solicitação de cadastro no endereço "user@gmail.com" + E cliquei no link de definição de senha contido no e-mail + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Definição de senha com sucesso + Quando preencho o campo "Nova Senha" com "senhaUser" + E preencho o campo "Confirmar Senha" com "senhaUser" + E clico no botão "Definir Senha" + Então sou redirecionado para a página de login + E vejo a mensagem "Senha definida com sucesso. Faça seu login." + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Senhas digitadas não coincidem + Quando preencho o campo "Nova Senha" com "senhaUser" + E preencho o campo "Confirmar Senha" com "senhaUserErrada" + E clico no botão "Definir Senha" + Então permaneço na página de definição de senha + E vejo a mensagem de erro "As senhas não coincidem" + + Cenário: Senha não atende aos requisitos mínimos + Quando preencho o campo "Nova Senha" com "senha" + E preencho o campo "Confirmar Senha" com "senha" + E clico no botão "Definir Senha" + Então permaneço na página de definição de senha + E vejo a mensagem de erro "A senha deve ter no mínimo 8 caracteres" + + Cenário: Campos de senha deixados em branco + Quando deixo o campo "Nova Senha" em branco + E deixo o campo "Confirmar Senha" em branco + E clico no botão "Definir Senha" + Então permaneço na página de definição de senha + E vejo a mensagem de erro "Preencha todos os campos obrigatórios" + + Cenário: Link de definição de senha expirado + Dado que o link de definição de senha foi enviado há mais de 24 horas + Quando preencho o campo "Nova Senha" com "senhaUser" + E preencho o campo "Confirmar Senha" com "senhaUser" + E clico no botão "Definir Senha" + Então vejo a mensagem de erro "Este link expirou. Solicite um novo cadastro." From ed504a8c8a28d4c111ff5f832e3d93f67507398a Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Fri, 22 May 2026 14:38:18 -0300 Subject: [PATCH 05/87] =?UTF-8?q?=E2=9C=85=20Adiciona=20feature=20de=20red?= =?UTF-8?q?efini=C3=A7=C3=A3o=20de=20senha=20#10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/redef_senha.feature | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 features/redef_senha.feature diff --git a/features/redef_senha.feature b/features/redef_senha.feature new file mode 100644 index 0000000000..58274a9660 --- /dev/null +++ b/features/redef_senha.feature @@ -0,0 +1,49 @@ +# language: pt + +Funcionalidade: ReRedefinir senha a partir do e-mail de solicitação de troca de senha + Como usuario + 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 + + Contexto: + Dado que recebi um e-mail de solicitação de troca de senha no endereço "user@gmail.com" + E cliquei no link de redefinição de senha contido no e-mail + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Redefinição de senha com sucesso + Quando preencho o campo "Nova Senha" com "senhaUser" + E preencho o campo "Confirmar Senha" com "senhaUser" + E clico no botão "Redefinir Senha" + Então sou redirecionado para a página de login + E vejo a mensagem "Senha redefinida com sucesso. Faça seu login." + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Senhas digitadas não coincidem + Quando preencho o campo "Nova Senha" com "senhaUser" + E preencho o campo "Confirmar Senha" com "senhaUserErrada" + E clico no botão "Redefinir Senha" + Então permaneço na página de redefinição de senha + E vejo a mensagem de erro "As senhas não coincidem" + + Cenário: Senha não atende aos requisitos mínimos + Quando preencho o campo "Nova Senha" com "senha" + E preencho o campo "Confirmar Senha" com "senha" + E clico no botão "Redefinir Senha" + Então permaneço na página de redefinição de senha + E vejo a mensagem de erro "A senha deve ter no mínimo 8 caracteres" + + Cenário: Campos de senha deixados em branco + Quando deixo o campo "Nova Senha" em branco + E deixo o campo "Confirmar Senha" em branco + E clico no botão "Redefinir Senha" + Então permaneço na página de redefinição de senha + E vejo a mensagem de erro "Preencha todos os campos obrigatórios" + + Cenário: Link de redefinição de senha expirado + Dado que o link de redefinição de senha foi enviado há mais de 24 horas + Quando preencho o campo "Nova Senha" com "senhaUser" + E preencho o campo "Confirmar Senha" com "senhaUser" + E clico no botão "Redefinir Senha" + Então vejo a mensagem de erro "Este link expirou. Solicite um novo link." From 0d8eeeb2251c10ade22049c0d49dab11b8c1ca2e Mon Sep 17 00:00:00 2001 From: diamondxorb Date: Mon, 25 May 2026 11:18:13 -0300 Subject: [PATCH 06/87] =?UTF-8?q?=E2=9C=85=20Adiciona=20feature=20de=20rel?= =?UTF-8?q?at=C3=B3rio=20do=20administrador=20#4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/relatorio_admin.feature | 53 ++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 features/relatorio_admin.feature diff --git a/features/relatorio_admin.feature b/features/relatorio_admin.feature new file mode 100644 index 0000000000..bb625aa903 --- /dev/null +++ b/features/relatorio_admin.feature @@ -0,0 +1,53 @@ +#language: pt + +Funcionalidade: Gerar relatório do Administrador + Como um Administrador + Quero baixar um arquivo CSV contendo os resultados de um formulário + A fim de avaliar o desempenho das turmas + + Contexto: + Dado que estou logado como Administrador + E estou na página de resultados de um formulário de avaliação + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Download do CSV com resultados de um formulário preenchido + Dado que o formulário "Avaliação Engenharia de Software - 2026.1" possui respostas registradas + Quando clico no botão "Baixar CSV" + Então um arquivo CSV é baixado com o nome "relatorio_engenharia_software_2026_1.csv" + E o arquivo contém as colunas "Turma", "Discente", "Docente", "Questão", "Resposta" + E cada linha corresponde a uma resposta submetida + + Cenário: Download do CSV com resultados filtrados por turma + Dado que o formulário "Avaliação Engenharia de Software - 2026.1" possui respostas de múltiplas turmas + Quando seleciono a turma "Turma 01 - Engenharia de Software (2026.1)" + E clico no botão "Baixar CSV" + Então um arquivo CSV é baixado contendo apenas respostas da turma selecionada + E o arquivo não contém respostas de outras turmas + + Cenário: Download do CSV contendo todos os campos do formulário + Dado que o formulário "Avaliação Engenharia de Software - 2026.1" possui questões de múltiplos tipos + Quando clico no botão "Baixar CSV" + Então o arquivo CSV baixado contém uma coluna para cada questão do formulário + E os valores de cada coluna correspondem às respostas dos discentes + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Tentativa de download de CSV de formulário sem respostas + Dado que o formulário "Avaliação Engenharia de Software - 2026.1" não possui nenhuma resposta registrada + Quando clico no botão "Baixar CSV" + Então nenhum arquivo é baixado + E vejo a mensagem de aviso "Este formulário ainda não possui respostas registradas" + + Cenário: Tentativa de acesso à geração de relatório por usuário não administrador + Dado que estou logado como User + Quando tento acessar a página de resultados de um formulário de avaliação + Então sou redirecionado para o painel do usuário + E vejo a mensagem de erro "Acesso não autorizado" + + Cenário: Erro ao gerar CSV por falha interna + Dado que o formulário "Avaliação Engenharia de Software - 2026.1" possui respostas registradas + E o sistema está com falha no serviço de exportação + Quando clico no botão "Baixar CSV" + Então nenhum arquivo é baixado + E vejo a mensagem de erro "Não foi possível gerar o relatório. Tente novamente" From 228f3e1abdd377e6aad664d4c09ed462af8998f5 Mon Sep 17 00:00:00 2001 From: diamondxorb Date: Mon, 25 May 2026 14:03:21 -0300 Subject: [PATCH 07/87] =?UTF-8?q?=E2=9C=85=20Ajustes=20na=20feature=20do?= =?UTF-8?q?=20relat=C3=B3rio=20do=20administrador=20#4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/relatorio_admin.feature | 64 ++++++++++++++++---------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/features/relatorio_admin.feature b/features/relatorio_admin.feature index bb625aa903..bc40ed55c6 100644 --- a/features/relatorio_admin.feature +++ b/features/relatorio_admin.feature @@ -7,47 +7,47 @@ Funcionalidade: Gerar relatório do Administrador Contexto: Dado que estou logado como Administrador - E estou na página de resultados de um formulário de avaliação + E estou na página "Gerenciamento" + E clico no botão "Resultados" # ==================== CENÁRIOS FELIZES ==================== - Cenário: Download do CSV com resultados de um formulário preenchido - Dado que o formulário "Avaliação Engenharia de Software - 2026.1" possui respostas registradas - Quando clico no botão "Baixar CSV" - Então um arquivo CSV é baixado com o nome "relatorio_engenharia_software_2026_1.csv" - E o arquivo contém as colunas "Turma", "Discente", "Docente", "Questão", "Resposta" - E cada linha corresponde a uma resposta submetida - - Cenário: Download do CSV com resultados filtrados por turma - Dado que o formulário "Avaliação Engenharia de Software - 2026.1" possui respostas de múltiplas turmas - Quando seleciono a turma "Turma 01 - Engenharia de Software (2026.1)" + Cenário: Visualizar listagem de turmas com resultados disponíveis + Então vejo uma listagem de cards com turmas + E cada card exibe o "Nome da matéria", o "semestre" e o "Professor" + + Cenário: Download do CSV com resultados de uma turma + Dado que a turma "Engenharia de Software - 2026.1 - Prof. Genaina" possui respostas registradas + Quando clico no card da turma "Engenharia de Software - 2026.1 - Prof. Genaina" + E clico no botão "Baixar CSV" + Então um arquivo CSV é baixado com os resultados da turma selecionada + E o arquivo contém as colunas "Turma", "Discente", "Questão" e "Resposta" + E cada linha corresponde a uma resposta submetida por um discente + + Cenário: Download do CSV contendo todos os tipos de resposta do formulário + Dado que a turma "Engenharia de Software - 2026.1 - Prof. Genaina" possui respostas de questões do tipo Radio e Texto + Quando clico no card da turma "Engenharia de Software - 2026.1 - Prof. Genaina" E clico no botão "Baixar CSV" - Então um arquivo CSV é baixado contendo apenas respostas da turma selecionada - E o arquivo não contém respostas de outras turmas - - Cenário: Download do CSV contendo todos os campos do formulário - Dado que o formulário "Avaliação Engenharia de Software - 2026.1" possui questões de múltiplos tipos - Quando clico no botão "Baixar CSV" Então o arquivo CSV baixado contém uma coluna para cada questão do formulário - E os valores de cada coluna correspondem às respostas dos discentes + E os valores correspondem às respostas selecionadas ou digitadas pelos discentes # ==================== CENÁRIOS TRISTES ==================== - Cenário: Tentativa de download de CSV de formulário sem respostas - Dado que o formulário "Avaliação Engenharia de Software - 2026.1" não possui nenhuma resposta registrada - Quando clico no botão "Baixar CSV" + Cenário: Tentativa de download de CSV de turma sem respostas registradas + Dado que a turma "Software Básico - 2026.1 - Prof. Ladeira" não possui nenhuma resposta registrada + Quando clico no card da turma "Software Básico - 2026.1 - Prof. Ladeira" + E clico no botão "Baixar CSV" Então nenhum arquivo é baixado - E vejo a mensagem de aviso "Este formulário ainda não possui respostas registradas" + E vejo a mensagem de aviso "Esta turma ainda não possui respostas registradas" + + Cenário: Nenhuma turma disponível na tela de Resultados + Dado que nenhum formulário foi enviado para nenhuma turma + Quando acesso "Gerenciamento - Resultados" + Então vejo a mensagem "Nenhum resultado disponível" + E nenhum card de turma é exibido - Cenário: Tentativa de acesso à geração de relatório por usuário não administrador - Dado que estou logado como User - Quando tento acessar a página de resultados de um formulário de avaliação + Cenário: Tentativa de acesso à tela de Resultados por usuário não administrador + Dado que estou logado como Discente + Quando tento acessar a página "Gerenciamento - Resultados" Então sou redirecionado para o painel do usuário E vejo a mensagem de erro "Acesso não autorizado" - - Cenário: Erro ao gerar CSV por falha interna - Dado que o formulário "Avaliação Engenharia de Software - 2026.1" possui respostas registradas - E o sistema está com falha no serviço de exportação - Quando clico no botão "Baixar CSV" - Então nenhum arquivo é baixado - E vejo a mensagem de erro "Não foi possível gerar o relatório. Tente novamente" From 49f771225c755dd3a508a71fd7830828e2d36a8f Mon Sep 17 00:00:00 2001 From: diamondxorb Date: Mon, 25 May 2026 14:14:19 -0300 Subject: [PATCH 08/87] =?UTF-8?q?=E2=9C=85=20Adicionado=20feature=20gerar?= =?UTF-8?q?=20formul=C3=A1rio=20de=20avalia=C3=A7=C3=A3o=20do=20administra?= =?UTF-8?q?dor=20#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/form_avaliacao_admin.feature | 81 +++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 features/form_avaliacao_admin.feature diff --git a/features/form_avaliacao_admin.feature b/features/form_avaliacao_admin.feature new file mode 100644 index 0000000000..8325b65660 --- /dev/null +++ b/features/form_avaliacao_admin.feature @@ -0,0 +1,81 @@ +# language: pt + +Funcionalidade: Gerar formulário de avaliação + Como um 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 estou logado como Administrador + E estou na página "Gerenciamento" + E clico no botão "Enviar Formulários" + Então um modal é exibido com um dropdown de "Template" e uma tabela de turmas + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Visualizar modal de envio de formulário com turmas disponíveis + Então o modal exibe o campo "Template" com um dropdown + E o modal exibe uma tabela com as colunas "Nome", "Semestre" e "Código" + E cada linha da tabela possui um checkbox para seleção + E vejo o botão "Enviar" + + Cenário: Enviar formulário para uma turma selecionada + Dado que existem turmas cadastradas no sistema + Quando seleciono o template "Avaliação Engenharia de Software" no dropdown "Template" + E marco o checkbox da turma "Estudos Em" com semestre "2026.1" e código "CIC0105" + E clico no botão "Enviar" + Então o modal é fechado + E vejo a mensagem de sucesso "Formulário enviado com sucesso" + E o formulário fica disponível para os discentes da turma "CIC0105" + + Cenário: Enviar formulário para múltiplas turmas simultaneamente + Dado que existem ao menos duas turmas cadastradas no sistema + Quando seleciono o template "Avaliação Engenharia de Software" no dropdown "Template" + E marco o checkbox da turma "Estudos Em" com semestre "2026.1" e código "CIC0105" + E marco o checkbox da segunda turma "Estudos Em" com semestre "2026.1" e código "CIC0106" + E clico no botão "Enviar" + Então o modal é fechado + E vejo a mensagem de sucesso "Formulário enviado com sucesso" + E o formulário fica disponível para os discentes das duas turmas selecionadas + + Cenário: Formulário enviado aparece disponível para os discentes da turma + Dado que o formulário baseado no template "Avaliação Engenharia de Software" foi enviado para a turma "CIC0105" + Quando um Discente matriculado na turma "CIC0105" acessa o painel de Avaliações + Então o discente visualiza o card da turma "CIC0105" disponível para resposta + + Cenário: Discente responde o formulário enviado pelo administrador + Dado que o formulário foi enviado para a turma "CIC0105" + E o Discente acessou o card da turma "CIC0105" + Quando o Discente responde as questões exibidas e clica no botão de envio + Então a resposta é registrada no sistema + E o resultado fica disponível na tela "Gerenciamento - Resultados" do Administrador + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Tentativa de enviar formulário sem selecionar um template + Dado que existem turmas cadastradas no sistema + Quando não seleciono nenhum template no dropdown "Template" + E marco o checkbox de uma turma na tabela + E clico no botão "Enviar" + Então o modal permanece aberto + E vejo a mensagem de erro "Selecione um template para o formulário" + + Cenário: Tentativa de enviar formulário sem selecionar nenhuma turma + Quando seleciono o template "Avaliação Engenharia de Software" no dropdown "Template" + E não marco o checkbox de nenhuma turma na tabela + E clico no botão "Enviar" + Então o modal permanece aberto + E vejo a mensagem de erro "Selecione ao menos uma turma" + + Cenário: Nenhuma turma cadastrada exibida na tabela do modal + Dado que não existem turmas cadastradas no sistema + Quando o modal de "Enviar Formulários" é aberto + Então a tabela de turmas está vazia + E vejo a mensagem "Nenhuma turma disponível" + E o botão "Enviar" está desabilitado + + Cenário: Tentativa de acesso ao envio de formulários por usuário não administrador + Dado que estou logado como Discente + Quando tento acessar a página "Gerenciamento" + Então sou redirecionado para o painel do usuário + E vejo a mensagem de erro "Acesso não autorizado" From 59c4e4948ac3e65989416cb560631fd8ec4b2b42 Mon Sep 17 00:00:00 2001 From: diamondxorb Date: Mon, 25 May 2026 14:30:49 -0300 Subject: [PATCH 09/87] =?UTF-8?q?=E2=9C=85=20Adicionado=20feature=20gerar?= =?UTF-8?q?=20template=20de=20formul=C3=A1rio=20administrador=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/template_form_admin.feature | 109 +++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 features/template_form_admin.feature diff --git a/features/template_form_admin.feature b/features/template_form_admin.feature new file mode 100644 index 0000000000..c8b92e7ad9 --- /dev/null +++ b/features/template_form_admin.feature @@ -0,0 +1,109 @@ +# language: pt + +Funcionalidade: Gerar template de formulário + Como um 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 estou logado como Administrador + E estou na página "Gerenciamento" + E clico no botão "Editar Templates" + Então sou redirecionado para a página "Gerenciamento - Templates" + E vejo os templates existentes exibidos em cards com nome e semestre + E cada card possui os ícones de editar e excluir + E vejo um card com o símbolo "+" para criar um novo template + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Abrir modal de criação de novo template + Quando clico no card "+" + Então um modal é exibido com o campo "Nome do template" + E o modal contém ao menos uma questão com os campos "Tipo", "Texto" e "Opções" + E vejo o botão "Criar" + + Cenário: Criar um novo template com questão do tipo Radio + Quando clico no card "+" + E preencho o campo "Nome do template" com "Avaliação Engenharia de Software" + E seleciono o tipo "Radio" na "Questão 1" + E preencho o campo "Texto" da "Questão 1" com "Como você avalia a didática do professor?" + E preencho o campo "Opções" da "Questão 1" com "Muito bom" + E clico no botão "+" dentro da "Questão 1" para adicionar mais uma opção + E preencho a nova opção com "Bom" + E clico no botão "Criar" + Então o modal é fechado + E vejo a mensagem de sucesso "Template criado com sucesso" + E o template "Avaliação Engenharia de Software" aparece na listagem de cards + + Cenário: Criar um novo template com questão do tipo Texto + Quando clico no card "+" + E preencho o campo "Nome do template" com "Avaliação Aberta" + E seleciono o tipo "Texto" na "Questão 1" + E preencho o campo "Texto" da "Questão 1" com "Deixe seu comentário sobre o professor" + E clico no botão "Criar" + Então o modal é fechado + E vejo a mensagem de sucesso "Template criado com sucesso" + E o template "Avaliação Aberta" aparece na listagem de cards + + Cenário: Criar template com múltiplas questões de tipos diferentes + Quando clico no card "+" + E preencho o campo "Nome do template" com "Avaliação Completa" + E seleciono o tipo "Radio" na "Questão 1" + E preencho o campo "Texto" da "Questão 1" com "Avalie o desempenho geral" + E clico no botão "+" roxo para adicionar uma nova questão + E seleciono o tipo "Texto" na "Questão 2" + E preencho o campo "Texto" da "Questão 2" com "Comentários adicionais" + E clico no botão "Criar" + Então o modal é fechado + E o template "Avaliação Completa" é salvo com 2 questões + + Cenário: Editar um template existente + Dado que existe o card do template "Avaliação Engenharia de Software" na listagem + Quando clico no ícone de editar do template "Avaliação Engenharia de Software" + Então o modal de edição é exibido com os dados atuais do template preenchidos + Quando altero o campo "Nome do template" para "Avaliação Engenharia de Software Revisada" + E clico no botão "Criar" + Então o modal é fechado + E vejo a mensagem de sucesso "Template atualizado com sucesso" + E o card "Avaliação Engenharia de Software Revisada" aparece na listagem + + Cenário: Excluir um template existente + Dado que existe o card do template "Avaliação Antiga" na listagem + Quando clico no ícone de excluir do template "Avaliação Antiga" + Então vejo uma mensagem de confirmação "Deseja excluir o template Avaliação Antiga?" + Quando confirmo a exclusão + Então o card "Avaliação Antiga" é removido da listagem + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Tentativa de criar template sem preencher o nome + Quando clico no card "+" + E deixo o campo "Nome do template" em branco + E preencho o campo "Texto" da "Questão 1" com "Como você avalia o professor?" + E clico no botão "Criar" + Então o modal permanece aberto + E vejo a mensagem de erro "O nome do template é obrigatório" + + Cenário: Tentativa de criar template sem preencher o texto de uma questão + Quando clico no card "+" + E preencho o campo "Nome do template" com "Template Incompleto" + E deixo o campo "Texto" da "Questão 1" em branco + E clico no botão "Criar" + Então o modal permanece aberto + E vejo a mensagem de erro "O texto da questão é obrigatório" + + Cenário: Tentativa de criar template do tipo Radio sem nenhuma opção + Quando clico no card "+" + E preencho o campo "Nome do template" com "Template Inválido" + E seleciono o tipo "Radio" na "Questão 1" + E preencho o campo "Texto" da "Questão 1" com "Avalie o professor" + E deixo o campo "Opções" em branco + E clico no botão "Criar" + Então o modal permanece aberto + E vejo a mensagem de erro "Questões do tipo Radio devem ter ao menos uma opção" + + Cenário: Tentativa de acesso à tela de Templates por usuário não administrador + Dado que estou logado como Discente + Quando tento acessar a página "Gerenciamento - Templates" + Então sou redirecionado para o painel do usuário + E vejo a mensagem de erro "Acesso não autorizado" From 3cdeea8e7ef1f8afc3f526d6ba900ef41e453ba2 Mon Sep 17 00:00:00 2001 From: Vitor Alencar Date: Tue, 26 May 2026 01:03:49 -0300 Subject: [PATCH 10/87] =?UTF-8?q?implementei=20a=20feature=20de=20cria?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20formul=C3=A1rio=20para=20docentes=20ou=20d?= =?UTF-8?q?icentes=20#16?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/criacao_form_para_dic_doc.feature | 129 +++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 features/criacao_form_para_dic_doc.feature diff --git a/features/criacao_form_para_dic_doc.feature b/features/criacao_form_para_dic_doc.feature new file mode 100644 index 0000000000..150e84193e --- /dev/null +++ b/features/criacao_form_para_dic_doc.feature @@ -0,0 +1,129 @@ +# language: pt + +Funcionalidade: Criar formulário para Docentes e Dicentes + Como um Administrador + Quero escolher criar um formulário para os docentes ou os dicentes de uma turma + A fim de avaliar o desempenho de uma matéria + + Contexto: + Dado que estou logado como Administrador + E estou na página "Gerenciamento" + E clico no botão "Criar Formulário" + Então um modal é exibido com os campos "Tipo de Destinatário", "Template", "Turma(s)" e data de disponibilidade + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Criar formulário para dicentes de uma turma + Quando seleciono "Dicentes" no campo "Tipo de Destinatário" + E seleciono o template "Avaliação Engenharia de Software" no dropdown "Template" + E seleciono a turma "CIC0105" no campo "Turma(s)" + E preencho a data de início com "26/05/2026" + E preencho a data de término com "01/06/2026" + E clico no botão "Criar" + Então o modal é fechado + E vejo a mensagem de sucesso "Formulário criado com sucesso para dicentes" + E o formulário fica disponível para os dicentes da turma "CIC0105" + + Cenário: Criar formulário para docentes de uma turma + Quando seleciono "Docentes" no campo "Tipo de Destinatário" + E seleciono o template "Avaliação de Didática" no dropdown "Template" + E seleciono a turma "CIC0105" no campo "Turma(s)" + E preencho a data de início com "26/05/2026" + E preencho a data de término com "01/06/2026" + E clico no botão "Criar" + Então o modal é fechado + E vejo a mensagem de sucesso "Formulário criado com sucesso para docentes" + E o formulário fica disponível para os docentes da turma "CIC0105" + + Cenário: Criar formulário para múltiplas turmas simultaneamente + Quando seleciono "Dicentes" no campo "Tipo de Destinatário" + E seleciono o template "Avaliação Engenharia de Software" no dropdown "Template" + E seleciono a turma "CIC0105" no campo "Turma(s)" + E clico no campo "Turma(s)" novamente para adicionar mais turmas + E seleciono a turma "CIC0202" no campo "Turma(s)" + E preencho a data de início com "26/05/2026" + E preencho a data de término com "01/06/2026" + E clico no botão "Criar" + Então o modal é fechado + E vejo a mensagem de sucesso "Formulário criado com sucesso para 2 turmas" + E o formulário fica disponível para os dicentes das turmas "CIC0105" e "CIC0202" + + Cenário: Visualizar formulário criado na lista de formulários ativos + Dado que criei um formulário para dicentes da turma "CIC0105" com o template "Avaliação Engenharia de Software" + Quando acesso a página "Gerenciamento - Formulários Ativos" + Então vejo na tabela uma linha com os dados do formulário criado + E a linha contém "Dicentes", "CIC0105", "Avaliação Engenharia de Software" e as datas de vigência + + Cenário: Dicente visualiza formulário criado no painel de Avaliações + Dado que um formulário foi criado para dicentes da turma "CIC0105" + Quando um Dicente matriculado na turma "CIC0105" acessa o painel de Avaliações + Então o dicente visualiza o card do formulário dentro do período de vigência + E o card exibe o nome do template e a data de término + + Cenário: Docente visualiza formulário criado no painel de Avaliações + Dado que um formulário foi criado para docentes da turma "CIC0105" + Quando um Docente vinculado à turma "CIC0105" acessa o painel de Avaliações + Então o docente visualiza o card do formulário dentro do período de vigência + E o card exibe o nome do template e a data de término + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Tentativa de criar formulário sem selecionar tipo de destinatário + Quando deixo o campo "Tipo de Destinatário" em branco + E seleciono o template "Avaliação Engenharia de Software" no dropdown "Template" + E seleciono a turma "CIC0105" no campo "Turma(s)" + E preencho as datas de vigência + E clico no botão "Criar" + Então o modal permanece aberto + E vejo a mensagem de erro "Selecione o tipo de destinatário (Dicentes ou Docentes)" + + Cenário: Tentativa de criar formulário sem selecionar template + Quando seleciono "Dicentes" no campo "Tipo de Destinatário" + E não seleciono nenhum template no dropdown "Template" + E seleciono a turma "CIC0105" no campo "Turma(s)" + E preencho as datas de vigência + E clico no botão "Criar" + Então o modal permanece aberto + E vejo a mensagem de erro "Selecione um template para o formulário" + + Cenário: Tentativa de criar formulário sem selecionar nenhuma turma + Quando seleciono "Dicentes" no campo "Tipo de Destinatário" + E seleciono o template "Avaliação Engenharia de Software" no dropdown "Template" + E não seleciono nenhuma turma no campo "Turma(s)" + E preencho as datas de vigência + E clico no botão "Criar" + Então o modal permanece aberto + E vejo a mensagem de erro "Selecione ao menos uma turma" + + Cenário: Tentativa de criar formulário com data de término anterior à data de início + Quando seleciono "Dicentes" no campo "Tipo de Destinatário" + E seleciono o template "Avaliação Engenharia de Software" no dropdown "Template" + E seleciono a turma "CIC0105" no campo "Turma(s)" + E preencho a data de início com "01/06/2026" + E preencho a data de término com "26/05/2026" + E clico no botão "Criar" + Então o modal permanece aberto + E vejo a mensagem de erro "A data de término não pode ser anterior à data de início" + + Cenário: Tentativa de criar formulário sem preencher as datas de vigência + Quando seleciono "Dicentes" no campo "Tipo de Destinatário" + E seleciono o template "Avaliação Engenharia de Software" no dropdown "Template" + E seleciono a turma "CIC0105" no campo "Turma(s)" + E deixo os campos de data em branco + E clico no botão "Criar" + Então o modal permanece aberto + E vejo a mensagem de erro "As datas de vigência são obrigatórias" + + Cenário: Tentativa de acesso à criação de formulários por usuário não administrador + Dado que estou logado como Dicente + Quando tento acessar a página "Gerenciamento" + Então sou redirecionado para o painel do usuário + E vejo a mensagem de erro "Acesso não autorizado" + + Cenário: Nenhuma turma disponível para criação de formulário + Dado que não existem turmas cadastradas no sistema + Quando clico no botão "Criar Formulário" + Então o modal é exibido + E o campo "Turma(s)" está vazio + E vejo a mensagem "Nenhuma turma disponível" + E o botão "Criar" está desabilitado From 18de66b2c8d511527cfa27e4177a1dc4765ee63b Mon Sep 17 00:00:00 2001 From: Vitor Alencar Date: Tue, 26 May 2026 01:13:58 -0300 Subject: [PATCH 11/87] =?UTF-8?q?implementei=20a=20feature=20da=20visualiz?= =?UTF-8?q?a=C3=A7=C3=A3o=20de=20resultados=20dos=20formul=C3=A1rios=20#13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/visu_resul_form.feature | 132 +++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 features/visu_resul_form.feature diff --git a/features/visu_resul_form.feature b/features/visu_resul_form.feature new file mode 100644 index 0000000000..a7dd7293be --- /dev/null +++ b/features/visu_resul_form.feature @@ -0,0 +1,132 @@ +# language: pt + +Funcionalidade: Visualizar Resultados de Formulários + Como um Administrador + Quero visualizar os formulários criados + A fim de poder gerar um relatório a partir das respostas + + Contexto: + Dado que estou logado como Administrador + E estou na página "Gerenciamento" + E clico no botão "Resultados" + Então sou redirecionado para a página "Gerenciamento - Resultados" + E vejo uma tabela com os formulários enviados + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Visualizar tabela de formulários com todas as informações + Quando acesso a página "Gerenciamento - Resultados" + Então vejo uma tabela com as colunas "Template", "Turma", "Destinatário", "Respostas Recebidas" e "Status" + E cada linha exibe as informações do formulário enviado + E vejo o botão "Ver Detalhes" em cada linha + + Cenário: Visualizar detalhes de um formulário com respostas recebidas + Dado que existem formulários com respostas já registradas + Quando clico no botão "Ver Detalhes" de um formulário + Então um modal é exibido com o nome do template + E vejo a quantidade total de respostas recebidas + E vejo as questões do formulário com o resumo das respostas + E as questões do tipo "Radio" exibem um gráfico com as opções e seus percentuais + E as questões do tipo "Texto" exibem as respostas em formato de lista + + Cenário: Visualizar gráfico de respostas para questão do tipo Radio + Dado que abri os detalhes de um formulário com questões do tipo Radio + Quando visualizo a questão "Como você avalia a didática do professor?" + Então vejo um gráfico com as opções e seus respectivos percentuais + E a barra com maior percentual é destacada com uma cor diferente + + Cenário: Visualizar respostas para questão do tipo Texto + Dado que abri os detalhes de um formulário com questões do tipo Texto + Quando visualizo a questão "Deixe seu comentário sobre o professor" + Então vejo uma lista com todas as respostas recebidas + E cada resposta exibe o nome do respondente e a data de envio + + Cenário: Filtrar formulários por turma + Dado que existem formulários de múltiplas turmas + Quando seleciono a turma "CIC0105" no filtro + Então a tabela exibe apenas os formulários da turma "CIC0105" + E a quantidade de linhas é reduzida + + Cenário: Filtrar formulários por tipo de destinatário + Dado que existem formulários para dicentes e docentes + Quando seleciono "Dicentes" no filtro "Tipo de Destinatário" + Então a tabela exibe apenas os formulários destinados a dicentes + E os formulários para docentes deixam de aparecer + + Cenário: Filtrar formulários por período + Dado que existem formulários de períodos diferentes + Quando seleciono a data de início "01/05/2026" no filtro "Data de Início" + E seleciono a data de término "31/05/2026" no filtro "Data de Término" + Então a tabela exibe apenas os formulários dentro do período selecionado + + Cenário: Exportar dados de um formulário em formato CSV + Dado que abri os detalhes de um formulário + Quando clico no botão "Exportar CSV" + Então um arquivo com as respostas do formulário é baixado + E o arquivo contém todas as questões e respostas em formato CSV + + Cenário: Exportar dados de um formulário em formato PDF + Dado que abri os detalhes de um formulário + Quando clico no botão "Exportar PDF" + Então um arquivo com as respostas do formulário é baixado em formato PDF + E o PDF contém um relatório formatado com gráficos e respostas de texto + + Cenário: Visualizar formulário sem respostas ainda recebidas + Dado que existe um formulário criado recentemente sem respostas + Quando clico em "Ver Detalhes" do formulário + Então o modal exibe a mensagem "Nenhuma resposta recebida ainda" + E não vejo questões nem gráficos + E vejo o botão "Voltar" + + Cenário: Ordenar tabela por coluna "Respostas Recebidas" + Dado que existem múltiplos formulários com quantidades diferentes de respostas + Quando clico no cabeçalho da coluna "Respostas Recebidas" + Então a tabela é ordenada em ordem decrescente de respostas + Quando clico novamente no cabeçalho + Então a tabela é ordenada em ordem crescente + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Tentativa de acesso à visualização de resultados por usuário não administrador + Dado que estou logado como Dicente + Quando tento acessar a página "Gerenciamento - Resultados" + Então sou redirecionado para o painel do usuário + E vejo a mensagem de erro "Acesso não autorizado" + + Cenário: Nenhum formulário enviado no sistema + Dado que nenhum formulário foi criado ou enviado + Quando acesso a página "Gerenciamento - Resultados" + Então a tabela está vazia + E vejo a mensagem "Nenhum formulário enviado ainda" + E o botão "Ver Detalhes" não está disponível + + Cenário: Tentativa de exportar formulário sem permissão + Dado que sou um Docente + Quando abro os detalhes de um formulário respondido por seus alunos + Então não vejo os botões "Exportar CSV" ou "Exportar PDF" + E vejo a mensagem "Você não tem permissão para exportar este formulário" + + Cenário: Tentativa de acessar detalhes de um formulário deletado + Dado que um formulário foi deletado do sistema + Quando tento acessar os detalhes deste formulário através de um link antigo + Então vejo a mensagem de erro "Este formulário não existe mais" + E sou redirecionado para a página "Gerenciamento - Resultados" + + Cenário: Filtrar com período inválido + Quando seleciono a data de início "31/05/2026" no filtro "Data de Início" + E seleciono a data de término "01/05/2026" no filtro "Data de Término" + E clico no botão "Filtrar" + Então a tabela permanece sem aplicar o filtro + E vejo a mensagem de erro "A data de término não pode ser anterior à data de início" + + Cenário: Gráfico não carrega corretamente para formulário com muitas respostas + Dado que existe um formulário com mais de 1000 respostas + Quando abro os detalhes deste formulário + Então o gráfico não é exibido + E vejo a mensagem "Muitas respostas. Use a exportação em CSV para análise detalhada" + + Cenário: Falha na exportação de arquivo + Dado que abri os detalhes de um formulário + Quando clico no botão "Exportar CSV" e a exportação falha + Então vejo a mensagem de erro "Erro ao exportar. Tente novamente" + E o arquivo não é baixado From 4c80fc56c1eb0262fb5554b3cd52dfefebcf6892 Mon Sep 17 00:00:00 2001 From: Vitor Alencar Date: Tue, 26 May 2026 01:18:52 -0300 Subject: [PATCH 12/87] implementei a feature do Sistema de gerenciamento por departamento #9 --- features/sist_geren_depart.feature | 153 +++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 features/sist_geren_depart.feature diff --git a/features/sist_geren_depart.feature b/features/sist_geren_depart.feature new file mode 100644 index 0000000000..805a45a8b0 --- /dev/null +++ b/features/sist_geren_depart.feature @@ -0,0 +1,153 @@ +# language: pt + +Funcionalidade: Gerenciamento de Turmas por Departamento + Como um Administrador + 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 estou logado como Administrador do departamento "CIC" + E estou na página "Gerenciamento" + E clico no botão "Turmas" + Então sou redirecionado para a página "Gerenciamento - Turmas" + E vejo uma tabela com as turmas do departamento "CIC" + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Visualizar turmas do próprio departamento + Quando acesso a página "Gerenciamento - Turmas" + Então vejo a tabela com colunas "Código", "Nome", "Semestre", "Docente" e "Status" + E vejo apenas turmas que pertencem ao departamento "CIC" + E turmas de outros departamentos não aparecem na tabela + + Cenário: Visualizar detalhes de uma turma do departamento + Dado que existe a turma "CIC0105" no departamento "CIC" + Quando clico na turma "CIC0105" + Então sou redirecionado para a página de detalhes da turma + E vejo informações como "Código", "Nome", "Docente", "Semestre", "Horário" e "Sala" + E vejo o botão "Editar Turma" + + Cenário: Editar informações de uma turma do departamento + Dado que estou visualizando os detalhes da turma "CIC0105" + Quando clico no botão "Editar Turma" + Então um formulário é exibido com os dados atuais da turma + Quando altero o campo "Sala" para "Sala 101" + E clico no botão "Salvar" + Então vejo a mensagem de sucesso "Turma atualizada com sucesso" + E a turma "CIC0105" agora exibe "Sala 101" + + Cenário: Visualizar lista de dicentes de uma turma + Dado que estou na página de detalhes da turma "CIC0105" + Quando clico no botão "Ver Dicentes" + Então um modal é exibido com a lista de dicentes matriculados + E vejo colunas "Matrícula", "Nome", "Email" e "Status de Respostas" + E cada dicente mostra se respondeu formulários ou não + + Cenário: Visualizar lista de docentes de uma turma + Dado que estou na página de detalhes da turma "CIC0105" + Quando clico no botão "Ver Docentes" + Então um modal é exibido com a lista de docentes vinculados + E vejo colunas "Matrícula", "Nome", "Email" e "Especialidade" + + Cenário: Visualizar estatísticas de respostas de uma turma + Dado que existem formulários enviados para a turma "CIC0105" + Quando clico no botão "Estatísticas" + Então vejo um painel com gráficos de desempenho + E vejo a taxa de resposta dos dicentes em percentual + E vejo a taxa de resposta dos docentes em percentual + E vejo a data da última resposta recebida + + Cenário: Filtrar turmas por semestre + Dado que existem turmas de semestres diferentes no departamento + Quando seleciono o semestre "2026.1" no filtro + Então a tabela exibe apenas as turmas do semestre "2026.1" + + Cenário: Filtrar turmas por docente + Dado que existem turmas de diferentes docentes + Quando seleciono o docente "Prof. João" no filtro + Então a tabela exibe apenas as turmas lecionadas por "Prof. João" + + Cenário: Filtrar turmas por status + Dado que existem turmas com status "Ativa" e "Encerrada" + Quando seleciono "Ativa" no filtro "Status" + Então a tabela exibe apenas as turmas ativas + E as turmas encerradas deixam de aparecer + + Cenário: Ordenar tabela de turmas por coluna + Quando clico no cabeçalho da coluna "Nome" + Então a tabela é ordenada alfabeticamente por nome + Quando clico novamente + Então a tabela é ordenada em ordem reversa + + Cenário: Acessar painel de relatório de uma turma + Dado que estou visualizando os detalhes da turma "CIC0105" + Quando clico no botão "Gerar Relatório" + Então um modal de seleção de filtros é exibido + E vejo opções para selecionar "Tipo de Relatório", "Período" e "Formato" + E clico no botão "Gerar" + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Tentativa de acesso ao gerenciamento de turmas por usuário não administrador + Dado que estou logado como Dicente + Quando tento acessar a página "Gerenciamento - Turmas" + Então sou redirecionado para o painel do usuário + E vejo a mensagem de erro "Acesso não autorizado" + + Cenário: Tentativa de gerenciar turma de outro departamento + Dado que sou um Administrador do departamento "CIC" + Quando tento acessar a página de edição da turma "ENG0150" que pertence ao departamento "ENG" + Então vejo a mensagem de erro "Você não tem permissão para gerenciar esta turma" + E sou redirecionado para a página "Gerenciamento - Turmas" + + Cenário: Nenhuma turma disponível para o departamento + Dado que sou um Administrador de um departamento sem turmas cadastradas + Quando acesso a página "Gerenciamento - Turmas" + Então a tabela está vazia + E vejo a mensagem "Nenhuma turma disponível no seu departamento" + + Cenário: Tentativa de editar turma sem preencher campo obrigatório + Dado que estou no formulário de edição da turma "CIC0105" + Quando limpo o campo "Sala" + E clico no botão "Salvar" + Então o formulário permanece aberto + E vejo a mensagem de erro "O campo 'Sala' é obrigatório" + + Cenário: Tentativa de editar turma com horário inválido + Dado que estou no formulário de edição da turma "CIC0105" + Quando altero o campo "Horário" para "99:99" + E clico no botão "Salvar" + Então vejo a mensagem de erro "Horário inválido" + E a turma não é atualizada + + Cenário: Tentativa de visualizar dicentes sem conexão com o sistema + Dado que estou na página de detalhes da turma "CIC0105" + E a conexão com o servidor foi perdida + Quando clico no botão "Ver Dicentes" + Então vejo a mensagem de erro "Erro ao carregar dicentes. Verifique sua conexão" + + Cenário: Lista de dicentes vazia + Dado que a turma "CIC0105" não possui nenhum dicente matriculado + Quando clico no botão "Ver Dicentes" + Então o modal exibe a mensagem "Nenhum dicente matriculado nesta turma" + + Cenário: Gerar relatório com período inválido + Dado que estou no modal de geração de relatório + Quando seleciono a data de início "31/05/2026" + E seleciono a data de término "01/05/2026" + E clico no botão "Gerar" + Então o modal permanece aberto + E vejo a mensagem de erro "A data de término não pode ser anterior à data de início" + + Cenário: Falha ao gerar relatório + Dado que estou no modal de geração de relatório + Quando seleciono todos os filtros corretamente + E clico no botão "Gerar" e o servidor retorna erro + Então vejo a mensagem de erro "Erro ao gerar relatório. Tente novamente" + E o arquivo não é gerado + + Cenário: Administrador removido do departamento não consegue acessar turmas + Dado que minha vinculação com o departamento "CIC" foi removida + Quando tento acessar a página "Gerenciamento - Turmas" + Então vejo a mensagem de erro "Você não está vinculado a nenhum departamento" + E sou redirecionado para o painel de login From 12329d7a12411ca8f07cd8c495df4620c14d99f2 Mon Sep 17 00:00:00 2001 From: Vitor Alencar Date: Tue, 26 May 2026 01:23:16 -0300 Subject: [PATCH 13/87] =?UTF-8?q?implementei=20a=20feature=20da=20Visualiz?= =?UTF-8?q?a=C3=A7=C3=A3o=20de=20formul=C3=A1rios=20para=20responder=20#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/visu_form_resp.feature | 167 ++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 features/visu_form_resp.feature diff --git a/features/visu_form_resp.feature b/features/visu_form_resp.feature new file mode 100644 index 0000000000..1dccabcab1 --- /dev/null +++ b/features/visu_form_resp.feature @@ -0,0 +1,167 @@ +# language: pt + +Funcionalidade: Visualizar Formulários Não Respondidos + Como um Participante de uma turma + Quero visualizar os formulários não respondidos das turmas em que estou matriculado + A fim de poder escolher qual irei responder + + Contexto: + Dado que estou logado como Participante + E estou na página "Avaliações" + Então vejo uma seção com os formulários não respondidos das minhas turmas + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Visualizar lista de formulários não respondidos + Quando acesso a página "Avaliações" + Então vejo uma lista com cards de formulários não respondidos + E cada card exibe "Turma", "Template", "Data de Término" e um botão "Responder" + E os formulários são agrupados por turma + + Cenário: Visualizar informações completas de um formulário + Dado que existem formulários não respondidos + Quando clico no card de um formulário + Então vejo os detalhes completos como "Turma", "Template", "Data de Início", "Data de Término" + E vejo um botão "Responder Agora" + E vejo a quantidade de questões do formulário + + Cenário: Filtrar formulários por turma + Dado que estou matriculado em múltiplas turmas com formulários + Quando seleciono a turma "CIC0105" no filtro + Então a lista exibe apenas os formulários da turma "CIC0105" + E os formulários de outras turmas deixam de aparecer + + Cenário: Filtrar formulários por tipo de destinatário + Dado que sou um Dicente com formulários para dicentes e também Docente com formulários para docentes + Quando seleciono "Dicentes" no filtro "Tipo de Destinatário" + Então a lista exibe apenas os formulários destinados a dicentes + E os formulários para docentes deixam de aparecer + + Cenário: Ordenar formulários por data de término + Dado que existem múltiplos formulários com datas diferentes + Quando clico no filtro "Ordenar por" + E seleciono "Data de Término" + Então os formulários aparecem ordenados por data de término (mais próximos primeiro) + + Cenário: Visualizar tempo restante para responder um formulário + Dado que existe um formulário com deadline em 2 dias + Quando visualizo o card do formulário + Então vejo a mensagem "Tempo restante: 2 dias" + E o card exibe uma barra de progresso de tempo em cor verde + + Cenário: Visualizar formulário com prazo próximo de vencer + Dado que existe um formulário vencendo em 1 hora + Quando visualizo o card do formulário + Então vejo a mensagem de alerta "Prazo vence em 1 hora" + E o card exibe uma barra de progresso de tempo em cor vermelha + + Cenário: Acessar formulário para responder + Dado que estou visualizando um formulário não respondido + Quando clico no botão "Responder Agora" + Então sou redirecionado para a página de preenchimento do formulário + E vejo todas as questões do template + E vejo o botão "Enviar Resposta" + + Cenário: Salvar formulário como rascunho + Dado que estou preenchendo um formulário + Quando preencho algumas questões + E clico no botão "Salvar como Rascunho" + Então vejo a mensagem "Formulário salvo como rascunho" + E sou redirecionado para a lista de formulários + E o card do formulário exibe "Rascunho iniciado" + + Cenário: Continuar preenchimento de um rascunho + Dado que salvei um formulário como rascunho anteriormente + Quando clico no card do formulário + Então vejo o formulário com os dados previamente preenchidos + E vejo um botão "Continuar Preenchimento" + E as respostas anteriores estão mantidas + + Cenário: Visualizar quantidade de formulários respondidos vs não respondidos + Quando acesso a página "Avaliações" + Então vejo um resumo no topo da página + E vejo a informação "Formulários respondidos: X" + E vejo a informação "Formulários não respondidos: Y" + E vejo um percentual de progresso + + Cenário: Visualizar abas separadas por status + Quando acesso a página "Avaliações" + Então vejo a aba "Não Respondidos" selecionada por padrão + E vejo a aba "Rascunhos" com formulários salvos como rascunho + E vejo a aba "Respondidos" com formulários já submetidos + E posso clicar para mudar entre abas + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Nenhum formulário não respondido disponível + Dado que respondeu todos os formulários das suas turmas + Quando acesso a página "Avaliações" + Então vejo a mensagem "Nenhum formulário não respondido" + E a lista está vazia + E vejo o botão "Ver Respondidos" + + Cenário: Tentar visualizar formulário de turma em que não estou matriculado + Dado que tentei acessar um formulário através de um link antigo + Quando abro o link de um formulário da turma "CIC0106" em que não estou matriculado + Então vejo a mensagem de erro "Você não está matriculado nesta turma" + E sou redirecionado para a página "Avaliações" + + Cenário: Formulário expirado não aparece na lista + Dado que existe um formulário com deadline já vencido há 1 dia + Quando acesso a página "Avaliações" + Então o formulário expirado não aparece na lista + E vejo a mensagem "O formulário expirou em 26/05/2026" + E vejo um botão "Ver Formulários Expirados" + + Cenário: Tentativa de responder formulário após o deadline + Dado que tentei abrir um formulário após seu deadline + Quando clico no botão "Responder Agora" + Então vejo a mensagem de erro "Este formulário expirou em 26/05/2026" + E não consigo acessar o formulário + E sou redirecionado para a página "Avaliações" + + Cenário: Erro ao carregar lista de formulários + Quando acesso a página "Avaliações" e o servidor retorna erro + Então vejo a mensagem de erro "Erro ao carregar os formulários" + E vejo um botão "Tentar Novamente" + E nenhum formulário é exibido + + Cenário: Sem conexão ao tentar abrir formulário + Dado que cliquei em um formulário para responder + Quando a conexão com o servidor é perdida + Então vejo a mensagem "Erro de conexão. Verifique sua internet" + E o botão "Tentar Novamente" é exibido + + Cenário: Formulário foi deletado enquanto visualizava + Dado que estava visualizando os detalhes de um formulário + Quando o administrador deleta o formulário + E atualizo a página + Então vejo a mensagem "Este formulário não existe mais" + E o formulário é removido da lista + + Cenário: Perda de dados ao fechar formulário sem salvar + Dado que estava preenchendo um formulário + Quando fecho a aba do navegador sem clicar em "Salvar como Rascunho" + E abro novamente o formulário + Então vejo a mensagem "Seus dados não foram salvos" + E o formulário começa vazio novamente + + Cenário: Erro ao salvar formulário como rascunho + Dado que estou preenchendo um formulário + Quando clico em "Salvar como Rascunho" e o servidor retorna erro + Então vejo a mensagem de erro "Erro ao salvar como rascunho. Tente novamente" + E permaneço na página de preenchimento + E meus dados continuam no formulário + + Cenário: Limite de rascunhos atingido + Dado que salvei 10 formulários como rascunho + Quando tento salvar outro formulário como rascunho + Então vejo a mensagem "Limite de rascunhos atingido (máximo 10)" + E preciso deletar um rascunho anterior + E o formulário não é salvo + + Cenário: Sem permissão para responder formulário + Dado que sou um Dicente + Quando um formulário é enviado especificamente para Docentes + Então esse formulário não aparece na minha lista de não respondidos + E não consigo acessá-lo mesmo tendo o link direto From 4e7de41ff268fd15d7d9cb256f6973da374a1041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius?= Date: Tue, 26 May 2026 02:08:34 -0300 Subject: [PATCH 14/87] features templates --- features/atualizar_base_com_sigaa.feature | 65 +++++++++++++++++ features/edicao_delecao_templates.feature | 85 +++++++++++++++++++++++ features/visualizacao_templates.feature | 63 +++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 features/atualizar_base_com_sigaa.feature create mode 100644 features/edicao_delecao_templates.feature create mode 100644 features/visualizacao_templates.feature diff --git a/features/atualizar_base_com_sigaa.feature b/features/atualizar_base_com_sigaa.feature new file mode 100644 index 0000000000..3bfcdda06a --- /dev/null +++ b/features/atualizar_base_com_sigaa.feature @@ -0,0 +1,65 @@ +# language: pt + +Funcionalidade: Atualização da base de dados com o SIGAA + + 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 estou logado como administrador + E o sistema possui integração ativa com o SIGAA + + Cenário: Atualização da base de dados com sucesso + Quando solicito a atualização dos dados a partir do SIGAA + E o SIGAA retorna dados válidos + Então a base de dados do sistema deve ser atualizada com os novos dados + E devo ver uma mensagem de sucesso na atualização + + Cenário: SIGAA retorna dados inconsistentes + Quando solicito a atualização dos dados a partir do SIGAA + E o SIGAA retorna dados inconsistentes + Então a atualização não deve ser concluída + E devo ver uma mensagem de erro informando inconsistência de dados + + Cenário: Falha de conexão com o SIGAA + Quando solicito a atualização dos dados a partir do SIGAA + E não há conexão com o SIGAA + Então a atualização não deve ser realizada + E devo ver uma mensagem informando falha de comunicação + + Cenário: Usuário sem permissão tenta atualizar base + Dado que estou logado como usuário comum + Quando tento solicitar a atualização da base de dados + Então devo ser impedido de realizar a ação + E devo ver uma mensagem de acesso negado + + Cenário: Atualização parcial dos dados + Quando solicito a atualização dos dados a partir do SIGAA + E o SIGAA retorna parcialmente os dados com sucesso + Então o sistema deve atualizar apenas os dados válidos + E deve registrar os dados que falharam na atualização + + Cenário: Evitar atualização duplicada da base de dados + Dado que já existe uma atualização em andamento + Quando solicito uma nova atualização dos dados a partir do SIGAA + Então devo ser impedido de iniciar outra atualização + E devo ver uma mensagem informando que já existe um processo em execução + + Cenário: Reprocessar atualização após falha + Dado que uma atualização anterior falhou + Quando solicito uma nova tentativa de atualização + Então o sistema deve reprocessar os dados do SIGAA + E a base de dados deve ser atualizada com sucesso caso os dados sejam válidos + + Cenário: Atualização sem dados retornados pelo SIGAA + Quando solicito a atualização dos dados a partir do SIGAA + E o SIGAA não retorna nenhum dado + Então a atualização não deve ser realizada + E devo ver uma mensagem informando ausência de dados + + Cenário: Registro de auditoria da atualização + Quando solicito a atualização dos dados a partir do SIGAA + E a atualização é executada + Então o sistema deve registrar um log da atualização + E o log deve conter data, usuário responsável e resultado da operação \ No newline at end of file diff --git a/features/edicao_delecao_templates.feature b/features/edicao_delecao_templates.feature new file mode 100644 index 0000000000..cb513baba5 --- /dev/null +++ b/features/edicao_delecao_templates.feature @@ -0,0 +1,85 @@ +# language: pt + +Funcionalidade: Edição e deleção de templates + + Como administrador + Quero editar e/ou deletar templates que eu criei sem afetar os formulários já criados + A fim de organizar os templates existentes + + Contexto: + Dado que estou logado como administrador + E existem templates cadastrados no sistema + + Cenário: Editar template gera nova versão sem afetar formulários existentes + Dado que existe um template criado + E existem formulários já criados baseados na versão atual do template + Quando eu edito o template + E salvo as alterações + Então uma nova versão do template deve ser criada + E a versão anterior deve ser preservada + E os formulários já criados devem permanecer vinculados à versão antiga + + Cenário: Excluir template com sucesso + Dado que existe um template criado + E o template não está em uso por formulários + Quando solicito a exclusão do template + E confirmo a exclusão + Então o template deve ser removido com sucesso + + Cenário: Cancelar exclusão de template + Dado que existe um template criado + Quando solicito a exclusão do template + Então devo ser solicitado a confirmar a exclusão + Quando cancelo a exclusão + Então o template não deve ser removido + + Cenário: Excluir template inexistente + Quando tento excluir um template que não existe + Então devo ver uma mensagem de erro informando que o template não foi encontrado + + Cenário: Editar template inexistente + Quando tento editar um template que não existe + Então devo ver uma mensagem de erro informando que o template não foi encontrado + + Cenário: Excluir template em uso por formulários + Dado que existe um template criado + E existem formulários baseados nesse template + Quando tento excluir o template + Então devo ser impedido de excluir + E devo ver uma mensagem informando que o template está em uso + + Cenário: Editar template com dados inválidos + Dado que existe um template criado + Quando tento salvar o template com dados inválidos + Então devo ver mensagens de validação + E nenhuma nova versão deve ser criada + + Cenário: Editar template com campo obrigatório vazio + Dado que existe um template criado + Quando tento salvar o template com o campo nome vazio + Então devo ver mensagem de erro indicando campo obrigatório + E nenhuma nova versão deve ser criada + + Cenário: Usuário sem permissão tenta editar template + Dado que estou logado como usuário comum + Quando tento editar um template + Então devo ser impedido de realizar a ação + E devo ver uma mensagem de acesso negado + + Cenário: Usuário sem permissão tenta excluir template + Dado que estou logado como usuário comum + Quando tento excluir um template + Então devo ser impedido de realizar a ação + E devo ver uma mensagem de acesso negado + + Cenário: Registrar histórico de versões do template + Dado que existe um template criado + Quando eu edito o template + Então a criação da nova versão deve ser registrada no histórico + E deve conter usuário e data da alteração + + Cenário: Editar template com múltiplos formulários vinculados + Dado que existe um template com muitos formulários vinculados + Quando eu edito o template + Então a nova versão deve ser criada com sucesso + E os formulários devem permanecer vinculados à versão anterior \ No newline at end of file diff --git a/features/visualizacao_templates.feature b/features/visualizacao_templates.feature new file mode 100644 index 0000000000..9a42a54bd0 --- /dev/null +++ b/features/visualizacao_templates.feature @@ -0,0 +1,63 @@ +# language: pt + +Funcionalidade: Visualização dos templates criados + + Como administrador + Quero visualizar os templates criados + A fim de poder editar e/ou deletar um template que eu criei + + Contexto: + Dado que estou logado como administrador + + Cenário: Visualizar templates com sucesso + Dado que existem templates cadastrados no sistema + Quando acesso a página de templates + Então devo ver a lista de templates criados + E devo poder selecionar um template para visualizar, editar ou excluir + + Cenário: Não existem templates cadastrados + Dado que não existem templates cadastrados no sistema + Quando acesso a página de templates + Então devo ver uma mensagem informando que não há templates disponíveis + + Cenário: Usuário não autenticado tenta acessar templates + Dado que não estou autenticado + Quando tento acessar a página de templates + Então devo ser redirecionado para a página de login + + Cenário: Usuário sem permissão tenta acessar templates + Dado que estou logado como usuário comum + Quando tento acessar a página de templates + Então devo ser impedido de acessar a página + E devo ver uma mensagem de acesso negado + + Cenário: Buscar template por nome + Dado que existem templates cadastrados no sistema + Quando busco o template pelo nome "Relatório da Disciplina" + Então devo ver apenas os templates correspondentes à busca + + Cenário: Visualizar detalhes de um template + Dado que existem templates cadastrados no sistema + Quando seleciono um template da lista + Então devo ver os detalhes do template + + Cenário: Ordenar lista de templates por nome + Dado que existem templates cadastrados no sistema + Quando ordeno os templates por nome em ordem alfabética + Então devo ver os templates ordenados por nome + + Cenário: Visualizar templates com paginação + Dado que existem muitos templates cadastrados no sistema + Quando acesso a página de templates + Então devo ver os templates divididos em páginas + E devo poder navegar entre páginas + + Cenário: Erro ao carregar lista de templates + Dado que ocorre uma falha no servidor + Quando acesso a página de templates + Então devo ver uma mensagem de erro ao carregar os templates + + Cenário: Acessar template que foi removido + Dado que o template foi excluído + Quando tento acessá-lo diretamente + Então devo ver uma mensagem informando que o template não existe \ No newline at end of file From 5faf12ee319208b746b2efed6feff44522a753c0 Mon Sep 17 00:00:00 2001 From: MatheusNevs Date: Tue, 26 May 2026 08:06:41 -0300 Subject: [PATCH 15/87] =?UTF-8?q?implementei=20a=20feature=20de=20cadastra?= =?UTF-8?q?r=20usu=C3=A1rios=20do=20sistema=20#3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/cadastrar_usuarios.feature | 55 +++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 features/cadastrar_usuarios.feature diff --git a/features/cadastrar_usuarios.feature b/features/cadastrar_usuarios.feature new file mode 100644 index 0000000000..98fe8c91b5 --- /dev/null +++ b/features/cadastrar_usuarios.feature @@ -0,0 +1,55 @@ +# language: pt + +Funcionalidade: Cadastrar usuários do sistema + Como Administrador + Quero cadastrar participantes de turmas do SIGAA ao importar dados de usuários novos para o sistema + A fim de que eles acessem o sistema CAMAAR + + Contexto: + Dado que estou logado como Administrador + E estou na página "Gerenciamento - Usuários" + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Solicitação de definição de senha para novo usuário importado + Dado que importei o arquivo "class_members.json" + E o sistema identificou o novo usuário com matrícula "190084006" + Quando o usuário "190084006" acessa a página de login + E preencho o campo "E-mail ou Matrícula" com "190084006" + E deixo o campo "Senha" em branco + E clico no botão "Entrar" + Então sou redirecionado para a página "Definição de Senha" + E vejo a mensagem "Este é seu primeiro acesso. Por favor, defina uma senha." + + Cenário: Efetivação do cadastro após definição de senha + Dado que o usuário "190084006" está na página "Definição de Senha" + Quando preencho o campo "Nova Senha" com "senhaSegura123" + E preencho o campo "Confirmar Senha" com "senhaSegura123" + E clico no botão "Salvar" + Então sou redirecionado para o painel principal + E vejo a mensagem de sucesso "Cadastro efetivado com sucesso!" + E meu status no sistema passa a ser "Ativo" + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Tentativa de definição de senha com campos divergentes + Dado que o usuário "190084006" está na página "Definição de Senha" + Quando preencho o campo "Nova Senha" com "senha123" + E preencho o campo "Confirmar Senha" com "senha456" + E clico no botão "Salvar" + Então permaneço na página "Definição de Senha" + E vejo a mensagem de erro "As senhas não coincidem" + + Cenário: Tentativa de definição de senha muito curta + Dado que o usuário "190084006" está na página "Definição de Senha" + Quando preencho o campo "Nova Senha" com "123" + E preencho o campo "Confirmar Senha" com "123" + E clico no botão "Salvar" + Então permaneço na página "Definição de Senha" + E vejo a mensagem de erro "A senha deve ter pelo menos 6 caracteres" + + Cenário: Tentativa de acesso sem definir senha após importação + Dado que o usuário "190084006" foi importado mas não definiu senha + Quando tento acessar a página "Avaliações" diretamente + Então sou redirecionado para a página "Definição de Senha" + E vejo o alerta "Você precisa definir uma senha antes de continuar" From 022ba4e252bcb50da52313e3abc7e1c3962f2234 Mon Sep 17 00:00:00 2001 From: MatheusNevs Date: Tue, 26 May 2026 08:06:41 -0300 Subject: [PATCH 16/87] =?UTF-8?q?implementei=20a=20feature=20de=20responde?= =?UTF-8?q?r=20formul=C3=A1rio=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/responder_formulario.feature | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 features/responder_formulario.feature diff --git a/features/responder_formulario.feature b/features/responder_formulario.feature new file mode 100644 index 0000000000..43acab6aae --- /dev/null +++ b/features/responder_formulario.feature @@ -0,0 +1,51 @@ +# language: pt + +Funcionalidade: Responder Formulário + 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 estou logado como Participante + E estou matriculado na turma "TA" da disciplina "ENGENHARIA DE SOFTWARE" + E existe um formulário pendente para esta turma + E estou na página de "Avaliações" + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Submissão de avaliação com sucesso + Quando clico no botão "Responder" do formulário da turma "TA" + E preencho todas as questões obrigatórias com valores válidos + E clico no botão "Enviar Avaliação" + Então vejo a mensagem de sucesso "Avaliação submetida com sucesso!" + E o card do formulário da turma "TA" exibe o status "Respondido" + + Cenário: Visualizar formulário já respondido + Dado que já respondi ao formulário da turma "TA" + Quando tento clicar no botão "Responder" novamente + Então o botão deve estar desabilitado + E vejo o texto "Você já respondeu esta avaliação" + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Tentativa de submissão de formulário incompleto + Quando clico no botão "Responder" do formulário da turma "TA" + E deixo a questão obrigatória "Avaliação do Professor" em branco + E clico no botão "Enviar Avaliação" + Então permaneço na página do formulário + E vejo o alerta "Por favor, responda todas as questões obrigatórias" + E a questão "Avaliação do Professor" é destacada em vermelho + + Cenário: Tentativa de responder formulário fora do prazo + Dado que o prazo para o formulário da turma "TA" expirou + Quando acesso a página de "Avaliações" + Então não vejo o botão "Responder" para a turma "TA" + E vejo a mensagem "Avaliação encerrada em 01/06/2026" + + Cenário: Erro ao enviar formulário por falha de conexão + Quando clico no botão "Responder" do formulário da turma "TA" + E preencho o formulário + E perco a conexão com a internet + E clico no botão "Enviar Avaliação" + Então vejo a mensagem de erro "Falha na conexão. Tente novamente." + E minhas respostas preenchidas devem ser preservadas localmente From 23f5b05dd044eb573d3515093470ed09ad0c7516 Mon Sep 17 00:00:00 2001 From: MatheusNevs Date: Tue, 26 May 2026 08:06:41 -0300 Subject: [PATCH 17/87] implementei a feature de importar dados do SIGAA #1 --- features/importar_dados_sigaa.feature | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 features/importar_dados_sigaa.feature diff --git a/features/importar_dados_sigaa.feature b/features/importar_dados_sigaa.feature new file mode 100644 index 0000000000..a46b493752 --- /dev/null +++ b/features/importar_dados_sigaa.feature @@ -0,0 +1,47 @@ +# language: pt + +Funcionalidade: Importar dados do SIGAA + Como Administrador + Quero importar dados de turmas, matérias e participantes do SIGAA + A fim de alimentar a base de dados do sistema + + Contexto: + Dado que estou logado como Administrador + E estou na página "Gerenciamento - Importar Dados" + + # ==================== CENÁRIOS FELIZES ==================== + + Cenário: Importação de dados de turmas inéditas com sucesso + Quando clico no botão "Selecionar Arquivo" + E seleciono o arquivo "classes.json" + E clico no botão "Confirmar Importação" + Então vejo a mensagem de sucesso "Dados de turmas importados com sucesso: 3 novas turmas adicionadas" + E a disciplina "ENGENHARIA DE SOFTWARE" passa a ser listada no sistema + + Cenário: Importação de participantes da turma com sucesso + Quando clico no botão "Selecionar Arquivo" + E seleciono o arquivo "class_members.json" + E clico no botão "Confirmar Importação" + Então vejo a mensagem de sucesso "Participantes importados com sucesso para a turma CIC0105-TA" + E a lista de discentes da turma exibe o aluno "Ana Clara Jordao Perna" + + # ==================== CENÁRIOS TRISTES ==================== + + Cenário: Tentativa de importar arquivo com formato inválido + Quando clico no botão "Selecionar Arquivo" + E seleciono o arquivo "relatorio.pdf" + E clico no botão "Confirmar Importação" + Então vejo a mensagem de erro "Formato de arquivo inválido. Por favor, envie um arquivo .json" + E o sistema não realiza nenhuma alteração na base de dados + + Cenário: Importar arquivo JSON com estrutura corrompida + Dado que possuo um arquivo "corrompido.json" com chaves faltando + Quando clico no botão "Selecionar Arquivo" + E seleciono o arquivo "corrompido.json" + E clico no botão "Confirmar Importação" + Then vejo a mensagem de erro "Erro na estrutura do arquivo: campos obrigatórios ausentes" + + Cenário: Tentativa de importação sem selecionar nenhum arquivo + Quando clico no botão "Confirmar Importação" sem selecionar um arquivo + Então vejo o alerta "Por favor, selecione um arquivo para importar" + E permaneço na página de importação From 9b16fb8c2c40b637ff9e52862395e47e5c2628cc Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Tue, 26 May 2026 13:51:11 -0300 Subject: [PATCH 18/87] =?UTF-8?q?=F0=9F=93=9D=20Documenta=C3=A7=C3=A3o=20s?= =?UTF-8?q?print=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wiki.md | 362 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 wiki.md diff --git a/wiki.md b/wiki.md new file mode 100644 index 0000000000..9dfbfa0635 --- /dev/null +++ b/wiki.md @@ -0,0 +1,362 @@ +# 📚 Wiki — Sprint 1 +## TrabalhoFinalEngSoft | CAMAAR — Sistema de Avaliação Acadêmica + +--- + +## 👥 Integrantes do Grupo + +| Nome | Matrícula | +|------|-----------| +| Jéssica Leal de Melo | 21/1038253 | +| Júlia Paulo Amorim | 24/1039270 | +| Matheus das Neves Fernandes | 23/1013672 | +| Vinicius Resplandes Caetano | 23/1025234 | +| Vitor Alencar Ribeiro | 23/1036292 | + +--- + +## 📌 Sobre o Projeto + +**Nome do Projeto:** CAMAAR — Sistema de Controle e Avaliação de Atividades Acadêmicas + +**Repositório:** [jessica8melo/TrabalhoFinalEngSoft](https://github.com/jessica8melo/TrabalhoFinalEngSoft) + +**Disciplina:** Engenharia de Software — UnB 2026.1 + +### Escopo do Projeto + +O CAMAAR é um sistema web desenvolvido em Ruby on Rails para a gestão de avaliações acadêmicas do Departamento de Ciência da Computação da UnB. O sistema permite que administradores importem dados de turmas, docentes e discentes, gerenciem templates de formulários de avaliação, e que usuários respondam e visualizem os resultados dessas avaliações. + +--- + +## 🏅 Papéis do Time + +| Papel | Responsável | +|-------|-------------| +| **Scrum Master** | Jéssica Leal de Melo | +| **Product Owner** | Matheus das Neves Fernandes | + +--- + +## ⚙️ Funcionalidades, Regras de Negócio e Responsáveis + +### 🎲 #1 — Importar dados do SIGAA +**Responsável:** Matheus das Neves Fernandes +**Story Points:** 5 + +**História de Usuário:** +> Como um administrador, quero importar os dados de turmas, docentes e discentes a partir de um arquivo JSON do SIGAA, a fim de manter a base de dados do sistema atualizada. + +**Regras de Negócio:** +- Apenas usuários com perfil **admin** podem realizar a importação +- O arquivo de importação deve estar no formato **JSON** seguindo a estrutura do SIGAA +- Em caso de erro no arquivo, o sistema deve exibir mensagem indicando o problema + +--- + +### 🖊️ #2 — Responder Formulário +**Responsável:** Matheus das Neves Fernandes +**Story Points:** 5 + +**História de Usuário:** +> 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 + +**Regras de Negócio:** +- Participantes da turma podem tanto responder um formulário novo quanto visualizar um formulário já respondido +- Em caso de erro no processo, o sistema deve exibir mensagem indicando o problema +- As respostas do usuário devem ser preservada localmente + +--- + +### 🖥️ #3 — Cadastrar usuários do sistema +**Responsável:** Matheus das Neves Fernandes +**Story Points:** 3 + +**História de Usuário:** +> Como um 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 + +**Regras de Negócio:** +- Apenas usuários com perfil **admin** podem realizar o cadastro de participantes de turmas +- Em caso de erro no processo, o sistema deve exibir mensagem indicando o problema +- Para criar um usuário válido, deve-se colocar o email ou matrícula e uma senha +- A senha do usuário deve ter pelo menos 6 caracteres + +--- + +### 📎 #4 — Gerar relatório do administrador +**Responsável:** Júlia Paulo Amorim +**Story Points:** 3 + +**História de Usuário:** +> Como um administrador, quero baixar um arquivo csv contendo os resultados de um formulário a fim de avaliar o desempenho das turmas + +**Regras de Negócio:** +- Apenas usuários com perfil **admin** podem baixar o arquivo dos resultados das avaliações +- O arquivo de importação deve estar no formato **csv** seguindo a estrutura do SIGAA +- Em caso de erro no arquivo, o sistema deve exibir mensagem indicando o problema + +--- + +### 📝 #5 — Gerar template de formulário +**Responsável:** Júlia Paulo Amorim +**Story Points:** 8 + +**História de Usuário:** +> Como um 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 + +**Regras de Negócio:** +- Apenas usuários com perfil **admin** podem gerar template de formulário +- Em caso de erro no processo, o sistema deve exibir mensagem indicando o problema +- Um template deve ter pelo menos um campo/pergunta +- Os tipos de pergunta suportados são: texto livre, múltipla escolha e escala (1 a 5) +- Templates podem ser salvos como rascunho ou publicados + +--- + +### 📋 #6 — Gerar formulário de avaliação +**Responsável:** Júlia Paulo Amorim +**Story Points:** 5 + +**História de Usuário:** +> Como um 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 + +**Regras de Negócio:** +- Apenas usuários com perfil **admin** podem gerar formulário de avaliação +- Usuários com perfil **discente** da respectiva turma podem visualizar e responder formulário enviado pelo administrador +- Em caso de erro no processo, o sistema deve exibir mensagem indicando o problema +- O formulário deve ser feito a partir de um template e deve se referir a uma turma específica + +--- + +### 🔐 #7 — Sistema de Login +**Responsável**: Jéssica Leal de Melo +**Story Points**: 3 + +**História de Usuário:** +> Como um 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. + +**Regras de Negócio:** +- O usuário pode se autenticar com e-mail ou matrícula + senha +- Credenciais inválidas devem exibir mensagem de erro genérica (sem indicar qual campo está errado) +- Usuários com perfil admin visualizam a opção "Gerenciamento" no menu lateral +- Usuários com perfil discente/docente não visualizam o menu de gerenciamento + +--- + +### 🔑 #8 — Sistema de Definição de Senha +**Responsável**: Jéssica Leal de Melo +**Story Points**: 3 + +**História de Usuário:** +> Como um 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. + +**Regras de Negócio:** + +- O link de definição de senha é enviado por e-mail após o cadastro +- O link expira em 24 horas +- O link só pode ser utilizado uma única vez +- A senha deve ter no mínimo 6 caracteres +- Os campos "Nova Senha" e "Confirmar Senha" devem ser idênticos + +--- + +### 🏛️ #9 — Sistema de Gerenciamento por Departamento +**Responsável:** Vitor Alencar Ribeiro +**Story Points:** 8 + +**História de Usuário:** +> Como um administrador, quero gerenciar usuários e turmas organizados por departamento, a fim de ter controle segmentado sobre as avaliações de cada área. + +**Regras de Negócio:** + +- Apenas usuários com perfil **admin** podem gerenciar por departamento +- Cada turma pertence a um departamento +- Admins podem ser restritos a gerenciar apenas seu próprio departamento +- Relatórios e formulários podem ser filtrados por departamento +- Informações de uma turma do departamento podem ser editadas +- Pode-se visualizar lista de discente ou docentes e estatísticas de respostas de uma turma +- Pode-se filtrar turmas por docente, por status ou por semestre + +--- + +### 🔄 #10 — Redefinição de Senha +**Responsável**: Jéssica Leal de Melo +**Story Points**: 3 + +**História de Usuário:** +> Como um usuário cadastrado, quero redefinir minha senha caso a esqueça, a fim de recuperar o acesso ao sistema. + +**Regras de Negócio:** + +- O usuário solicita a redefinição informando seu e-mail ou matrícula +- Um link de redefinição é enviado para o e-mail cadastrado +- O link expira em 24 horas +- A nova senha não pode ser igual à senha anterior + +--- + +### 🗄️ #11 — Atualizar Base de Dados com Dados do SIGAA +**Responsável**: Vinicius Resplandes Caetano +**Story Points**: 8 + +**História de Usuário:** +> Como um administrador, quero importar os dados de turmas, docentes e discentes a partir de um arquivo JSON do SIGAA, a fim de manter a base de dados do sistema atualizada. + +**Regras de Negócio:** + +- Apenas usuários com perfil admin podem realizar a importação +- O arquivo de importação deve estar no formato JSON seguindo a estrutura do SIGAA +- Em caso de erro no arquivo, o sistema deve exibir mensagem indicando o problema + +--- + +### 📄 #12 — Visualização de Formulários para Responder +**Responsável:** Vitor Alencar Ribeiro +**Story Points:** 3 + +**História de Usuário:** +> Como um discente ou docente, quero visualizar os formulários de avaliação disponíveis para mim, a fim de respondê-los. + +**Regras de Negócio:** + +- O usuário só visualiza formulários vinculados às suas turmas +- Formulários já respondidos devem ser marcados como concluídos +- Formulários com prazo encerrado devem aparecer como expirados e não podem ser acessados +- Formulários podem ser filtrados por turma ou por data de término +- Formulários devem mostrar o prazo de vencimento e quanto tempo falta para esse prazo ser concluído +- Informações de formulário podem ser salvas e vistas posteriormente mesmo que não esteja concluído + +--- + +### 📊 #13 — Visualização de Resultados dos Formulários +**Responsável:** Vitor Alencar Ribeiro +**Story Points:** 3 + +**História de Usuário:** +> Como um administrador ou docente, quero visualizar os resultados consolidados das avaliações respondidas, a fim de analisar o desempenho da turma. + +Regras de Negócio: + +- Admins visualizam resultados de todas as turmas +- Docentes visualizam apenas os resultados das suas próprias turmas +- Os resultados devem ser apresentados de forma anonimizada +- Só é possível visualizar resultados após o encerramento do formulário +- É possível filtrar formulários por tuma, por tipo de destinatário ou por período + +--- + +### 👁️ #14 — Visualização dos Templates Criados +**Responsável:** Vinicius Resplandes Caetano +**Story Points:** 2 + +**História de Usuário:** +> Como um administrador, quero visualizar todos os templates de formulário criados, a fim de gerenciá-los. + +**Regras de Negócio:** + +- Apenas **admins** têm acesso à listagem de templates +- A listagem deve exibir: nome, status (rascunho/publicado) e data de criação +- Templates publicados podem ser visualizados, editados ou excluídos + +--- + +### ✏️ #15 — Edição e Deleção de Templates +**Responsável:** Vinicius Resplandes Caetano +**Story Points:** 2 + +**História de Usuário:** +> Como um administrador, quero visualizar todos os templates de formulário criados, a fim de gerenciá-los. + +**Regras de Negócio:** + +- Apenas **admins** podem editar e/ou deletar templates +- A exclusão de um template não afeta formulários já gerados a partir dele +- O sistema deve solicitar confirmação antes de deletar + +--- + +### 🆕 #16 — Criação de Formulário para Docentes ou Discentes +**Responsável:** Vitor Alencar Ribeiro +**Story Points:** 5 + +**História de Usuário:** +> Como um administrador, quero criar formulários direcionados especificamente a docentes ou discentes, a fim de obter avaliações distintas para cada perfil. + +**Regras de Negócio:** + +- O formulário deve ter um público-alvo definido: docentes, discentes ou ambos +- Discentes e docentes só visualizam formulários do seu perfil +- O formulário deve ser criado a partir de um template +- O formulário deve ser atríbuido a uma ou várias turmas de um mesmo departamento + + + +## 📈 Velocity — Story Points da Sprint 1 + +| Issue | Funcionalidade | Story Points | Status | +|-------|---------------|:------------:|--------| +| #1 | Importar dados do SIGAA | 5 | 🔲 A fazer | +| #2 | Responder Formulário | 5 | 🔲 A fazer | +| #3 | Cadastrar usuários do sistema | 3 | 🔲 A fazer | +| #4 | Gerar relatório do administrador | 3 | 🔲 A fazer | +| #5 | Gerar Template de Formulário | 8 | 🔲 A fazer | +| #6 | Gerar Formulário de Avaliação | 5 | 🔲 A fazer | +| #7 | Sistema de Login | 3 | 🔲 A fazer | +| #8 | Sistema de Definição de Senha | 3 | 🔲 A fazer | +| #9 | Gerenciamento por Departamento | 8 | 🔲 A fazer | +| #10 | Redefinição de Senha | 2 | 🔲 A fazer | +| #11 | Atualizar Base com SIGAA | 8 | 🔲 A fazer | +| #12 | Visualização de Formulários | 3 | 🔲 A fazer | +| #13 | Visualização de Resultados | 3 | 🔲 A fazer | +| #14 | Visualização dos Templates | 2 | 🔲 A fazer | +| #15 | Edição e Deleção de Templates | 3 | 🔲 A fazer | +| #16 | Criação de Formulário Docente/Discente | 5 | 🔲 A fazer | +| | **Total da Sprint** | **69** | | +| | **Velocity (concluídos)** | **0** | _atualizar ao fim da sprint_ | + +> 💡 **O que é Velocity?** É a soma dos story points das histórias **concluídas** ao final da sprint. Ao fim da Sprint 2, atualize a coluna "Status" e some apenas os pontos das histórias entregues. Esse número será a velocity de referência para planejar a Sprint 3. + +--- + +## 🌿 Política de Branching + +O grupo adota o seguinte fluxo de branches: + +``` +main +└── sprint-1 + ├── feature/#7-login + ├── feature/#8-definicao-senha + ├── feature/#10-redefinicao-senha + └── ... +``` + +### Regras + +- **`main`** — branch de produção, recebe merges apenas via Pull Request revisada +- **`sprint-N`** — branch da sprint atual, base para todas as features da sprint +- **`feature/#N-nome-curto`** — uma branch por issue, criada a partir de `sprint-N` + +### Nomenclatura de commits + +Seguir o padrão com referência à issue: + +``` +git commit -m "Descrição da mudança #7" +``` + +Para fechar a issue automaticamente ao fazer merge na main: + +``` +git commit -m "Implementa sistema de login closes #7" +``` + +### Pull Requests + +- PRs de `feature/*` vão para `sprint-1` +- PRs de `sprint-1` vão para `main` ao fim da sprint +- Toda PR precisa de pelo menos **1 aprovação** de outro integrante antes do merge + +--- + +_Última atualização: Sprint 1 — 2026.1_ \ No newline at end of file From 17f46b9eab56b674ba09c17a47d46c856583d03f Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Wed, 27 May 2026 10:45:45 -0300 Subject: [PATCH 19/87] =?UTF-8?q?=F0=9F=94=A7=20Come=C3=A7a=20Sprint=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wiki.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wiki.md b/wiki.md index 9dfbfa0635..5adfd43b01 100644 --- a/wiki.md +++ b/wiki.md @@ -1,4 +1,4 @@ -# 📚 Wiki — Sprint 1 +# 📚 Wiki — Sprint 2 ## TrabalhoFinalEngSoft | CAMAAR — Sistema de Avaliação Acadêmica --- From 7a9b037c3d6496150d24bf639aef3843b7447ef1 Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Wed, 27 May 2026 10:59:16 -0300 Subject: [PATCH 20/87] =?UTF-8?q?=E2=9C=A8=20Criada=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/discente.rb | 3 +++ app/models/disciplina.rb | 2 ++ app/models/docente.rb | 3 +++ app/models/turma.rb | 3 +++ .../20260527135330_create_disciplinas.rb | 10 +++++++++ db/migrate/20260527135638_create_turmas.rb | 12 +++++++++++ db/migrate/20260527135740_create_discentes.rb | 16 ++++++++++++++ db/migrate/20260527135834_create_docentes.rb | 15 +++++++++++++ test/fixtures/discentes.yml | 21 +++++++++++++++++++ test/fixtures/disciplinas.yml | 9 ++++++++ test/fixtures/docentes.yml | 19 +++++++++++++++++ test/fixtures/turmas.yml | 13 ++++++++++++ test/models/discente_test.rb | 7 +++++++ test/models/disciplina_test.rb | 7 +++++++ test/models/docente_test.rb | 7 +++++++ test/models/turma_test.rb | 7 +++++++ 16 files changed, 154 insertions(+) create mode 100644 app/models/discente.rb create mode 100644 app/models/disciplina.rb create mode 100644 app/models/docente.rb create mode 100644 app/models/turma.rb create mode 100644 db/migrate/20260527135330_create_disciplinas.rb create mode 100644 db/migrate/20260527135638_create_turmas.rb create mode 100644 db/migrate/20260527135740_create_discentes.rb create mode 100644 db/migrate/20260527135834_create_docentes.rb create mode 100644 test/fixtures/discentes.yml create mode 100644 test/fixtures/disciplinas.yml create mode 100644 test/fixtures/docentes.yml create mode 100644 test/fixtures/turmas.yml create mode 100644 test/models/discente_test.rb create mode 100644 test/models/disciplina_test.rb create mode 100644 test/models/docente_test.rb create mode 100644 test/models/turma_test.rb diff --git a/app/models/discente.rb b/app/models/discente.rb new file mode 100644 index 0000000000..3fa5993446 --- /dev/null +++ b/app/models/discente.rb @@ -0,0 +1,3 @@ +class Discente < ApplicationRecord + belongs_to :turma +end diff --git a/app/models/disciplina.rb b/app/models/disciplina.rb new file mode 100644 index 0000000000..fd20a53250 --- /dev/null +++ b/app/models/disciplina.rb @@ -0,0 +1,2 @@ +class Disciplina < ApplicationRecord +end diff --git a/app/models/docente.rb b/app/models/docente.rb new file mode 100644 index 0000000000..227fe9a625 --- /dev/null +++ b/app/models/docente.rb @@ -0,0 +1,3 @@ +class Docente < ApplicationRecord + belongs_to :turma +end diff --git a/app/models/turma.rb b/app/models/turma.rb new file mode 100644 index 0000000000..f9d314c4be --- /dev/null +++ b/app/models/turma.rb @@ -0,0 +1,3 @@ +class Turma < ApplicationRecord + belongs_to :disciplina +end diff --git a/db/migrate/20260527135330_create_disciplinas.rb b/db/migrate/20260527135330_create_disciplinas.rb new file mode 100644 index 0000000000..47ea504471 --- /dev/null +++ b/db/migrate/20260527135330_create_disciplinas.rb @@ -0,0 +1,10 @@ +class CreateDisciplinas < ActiveRecord::Migration[8.1] + def change + create_table :disciplinas do |t| + t.string :code + t.string :name + + t.timestamps + end + end +end diff --git a/db/migrate/20260527135638_create_turmas.rb b/db/migrate/20260527135638_create_turmas.rb new file mode 100644 index 0000000000..535298f173 --- /dev/null +++ b/db/migrate/20260527135638_create_turmas.rb @@ -0,0 +1,12 @@ +class CreateTurmas < ActiveRecord::Migration[8.1] + def change + create_table :turmas do |t| + t.string :classCode + t.string :semester + t.string :time + t.references :disciplina, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20260527135740_create_discentes.rb b/db/migrate/20260527135740_create_discentes.rb new file mode 100644 index 0000000000..ab22a43f00 --- /dev/null +++ b/db/migrate/20260527135740_create_discentes.rb @@ -0,0 +1,16 @@ +class CreateDiscentes < ActiveRecord::Migration[8.1] + def change + create_table :discentes do |t| + t.string :nome + t.string :curso + t.string :matricula + t.string :usuario + t.string :formacao + t.string :ocupacao + t.string :email + t.references :turma, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20260527135834_create_docentes.rb b/db/migrate/20260527135834_create_docentes.rb new file mode 100644 index 0000000000..49b54a80aa --- /dev/null +++ b/db/migrate/20260527135834_create_docentes.rb @@ -0,0 +1,15 @@ +class CreateDocentes < ActiveRecord::Migration[8.1] + def change + create_table :docentes do |t| + t.string :nome + t.string :departamento + t.string :formacao + t.string :usuario + t.string :ocupacao + t.string :email + t.references :turma, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/test/fixtures/discentes.yml b/test/fixtures/discentes.yml new file mode 100644 index 0000000000..3654c91e05 --- /dev/null +++ b/test/fixtures/discentes.yml @@ -0,0 +1,21 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + nome: MyString + curso: MyString + matricula: MyString + usuario: MyString + formacao: MyString + ocupacao: MyString + email: MyString + turma: one + +two: + nome: MyString + curso: MyString + matricula: MyString + usuario: MyString + formacao: MyString + ocupacao: MyString + email: MyString + turma: two diff --git a/test/fixtures/disciplinas.yml b/test/fixtures/disciplinas.yml new file mode 100644 index 0000000000..59387ea1a2 --- /dev/null +++ b/test/fixtures/disciplinas.yml @@ -0,0 +1,9 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + code: MyString + name: MyString + +two: + code: MyString + name: MyString diff --git a/test/fixtures/docentes.yml b/test/fixtures/docentes.yml new file mode 100644 index 0000000000..5a51b069b4 --- /dev/null +++ b/test/fixtures/docentes.yml @@ -0,0 +1,19 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + nome: MyString + departamento: MyString + formacao: MyString + usuario: MyString + ocupacao: MyString + email: MyString + turma: one + +two: + nome: MyString + departamento: MyString + formacao: MyString + usuario: MyString + ocupacao: MyString + email: MyString + turma: two diff --git a/test/fixtures/turmas.yml b/test/fixtures/turmas.yml new file mode 100644 index 0000000000..bb27b76d32 --- /dev/null +++ b/test/fixtures/turmas.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + classCode: MyString + semester: MyString + time: MyString + disciplina: one + +two: + classCode: MyString + semester: MyString + time: MyString + disciplina: two diff --git a/test/models/discente_test.rb b/test/models/discente_test.rb new file mode 100644 index 0000000000..51b9aaf264 --- /dev/null +++ b/test/models/discente_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class DiscenteTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/disciplina_test.rb b/test/models/disciplina_test.rb new file mode 100644 index 0000000000..5416145dba --- /dev/null +++ b/test/models/disciplina_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class DisciplinaTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/docente_test.rb b/test/models/docente_test.rb new file mode 100644 index 0000000000..ad967b3cb2 --- /dev/null +++ b/test/models/docente_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class DocenteTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/turma_test.rb b/test/models/turma_test.rb new file mode 100644 index 0000000000..21806827d6 --- /dev/null +++ b/test/models/turma_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TurmaTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end From 5130e2cb30cadd252c1df5911bb584679835f29a Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Wed, 27 May 2026 11:01:12 -0300 Subject: [PATCH 21/87] =?UTF-8?q?=E2=9C=A8=20Criada=20schema=20das=20model?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/schema.rb | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 db/schema.rb diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000000..95128c06d0 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,61 @@ +# 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.1].define(version: 2026_05_27_135834) do + create_table "discentes", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "curso" + t.string "email" + t.string "formacao" + t.string "matricula" + t.string "nome" + t.string "ocupacao" + t.integer "turma_id", null: false + t.datetime "updated_at", null: false + t.string "usuario" + t.index ["turma_id"], name: "index_discentes_on_turma_id" + end + + create_table "disciplinas", force: :cascade do |t| + t.string "code" + t.datetime "created_at", null: false + t.string "name" + t.datetime "updated_at", null: false + end + + create_table "docentes", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "departamento" + t.string "email" + t.string "formacao" + t.string "nome" + t.string "ocupacao" + t.integer "turma_id", null: false + t.datetime "updated_at", null: false + t.string "usuario" + t.index ["turma_id"], name: "index_docentes_on_turma_id" + end + + create_table "turmas", force: :cascade do |t| + t.string "classCode" + t.datetime "created_at", null: false + t.integer "disciplina_id", null: false + t.string "semester" + t.string "time" + t.datetime "updated_at", null: false + t.index ["disciplina_id"], name: "index_turmas_on_disciplina_id" + end + + add_foreign_key "discentes", "turmas" + add_foreign_key "docentes", "turmas" + add_foreign_key "turmas", "disciplinas" +end From 31ec3b08c6579d4cffa7cf68e1f7df2056cb655e Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Wed, 27 May 2026 11:13:07 -0300 Subject: [PATCH 22/87] =?UTF-8?q?=E2=9C=A8=20Criada=20model=20User?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 2 +- Gemfile.lock | 2 ++ app/models/user.rb | 2 ++ db/migrate/20260527141115_create_users.rb | 12 ++++++++++++ db/schema.rb | 11 ++++++++++- test/fixtures/users.yml | 13 +++++++++++++ test/models/user_test.rb | 7 +++++++ 7 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 app/models/user.rb create mode 100644 db/migrate/20260527141115_create_users.rb create mode 100644 test/fixtures/users.yml create mode 100644 test/models/user_test.rb diff --git a/Gemfile b/Gemfile index 933e364877..65db7a2d24 100644 --- a/Gemfile +++ b/Gemfile @@ -18,7 +18,7 @@ gem "stimulus-rails" gem "jbuilder" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] -# gem "bcrypt", "~> 3.1.7" +gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ windows jruby ] diff --git a/Gemfile.lock b/Gemfile.lock index 9473bbd233..26b8419d26 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,6 +79,7 @@ GEM public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) base64 (0.3.0) + bcrypt (3.1.22) bcrypt_pbkdf (1.1.2) bigdecimal (4.1.2) bindex (0.8.1) @@ -430,6 +431,7 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + bcrypt (~> 3.1.7) bootsnap brakeman bundler-audit diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000000..379658a509 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,2 @@ +class User < ApplicationRecord +end diff --git a/db/migrate/20260527141115_create_users.rb b/db/migrate/20260527141115_create_users.rb new file mode 100644 index 0000000000..4072d68b91 --- /dev/null +++ b/db/migrate/20260527141115_create_users.rb @@ -0,0 +1,12 @@ +class CreateUsers < ActiveRecord::Migration[8.1] + def change + create_table :users do |t| + t.string :email + t.string :matricula + t.string :password_digest + t.string :role + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 95128c06d0..ac6511002f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_05_27_135834) do +ActiveRecord::Schema[8.1].define(version: 2026_05_27_141115) do create_table "discentes", force: :cascade do |t| t.datetime "created_at", null: false t.string "curso" @@ -55,6 +55,15 @@ t.index ["disciplina_id"], name: "index_turmas_on_disciplina_id" end + create_table "users", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "email" + t.string "matricula" + t.string "password_digest" + t.string "role" + t.datetime "updated_at", null: false + end + add_foreign_key "discentes", "turmas" add_foreign_key "docentes", "turmas" add_foreign_key "turmas", "disciplinas" diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 0000000000..d962f4d872 --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + email: MyString + matricula: MyString + password_digest: MyString + role: MyString + +two: + email: MyString + matricula: MyString + password_digest: MyString + role: MyString diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 0000000000..5c07f49007 --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class UserTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end From 470f178e187b9b2d14dd12e760961e03c8bc8a4c Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Wed, 27 May 2026 11:51:41 -0300 Subject: [PATCH 23/87] =?UTF-8?q?=E2=9C=A8Implementado=20sistema=20de=20lo?= =?UTF-8?q?gin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/sessions.css | 100 +++++++++++++++++++ app/controllers/application_controller.rb | 21 +++- app/controllers/sessions_controller.rb | 24 +++++ app/helpers/sessions_helper.rb | 2 + app/models/user.rb | 10 ++ app/views/sessions/create.html.erb | 2 + app/views/sessions/destroy.html.erb | 2 + app/views/sessions/new.html.erb | 29 ++++++ config/routes.rb | 16 +-- test/controllers/sessions_controller_test.rb | 18 ++++ 10 files changed, 210 insertions(+), 14 deletions(-) create mode 100644 app/assets/stylesheets/sessions.css create mode 100644 app/controllers/sessions_controller.rb create mode 100644 app/helpers/sessions_helper.rb create mode 100644 app/views/sessions/create.html.erb create mode 100644 app/views/sessions/destroy.html.erb create mode 100644 app/views/sessions/new.html.erb create mode 100644 test/controllers/sessions_controller_test.rb diff --git a/app/assets/stylesheets/sessions.css b/app/assets/stylesheets/sessions.css new file mode 100644 index 0000000000..efad885b2a --- /dev/null +++ b/app/assets/stylesheets/sessions.css @@ -0,0 +1,100 @@ +body { + background-color: #e0e0e0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; +} + +.login-wrapper { + display: flex; + width: 750px; + border-radius: 12px; + overflow: hidden; + background: white; + box-shadow: 0 2px 12px rgba(0,0,0,0.1); +} + +.login-left { + flex: 1; + padding: 60px 40px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.login-left h1 { + font-size: 1.8rem; + font-weight: normal; + text-align: center; + letter-spacing: 2px; +} + +.form { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 20px; +} + +.field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.field label { + font-size: 0.95rem; +} + +.field input { + border: 1px solid #ccc; + border-radius: 8px; + padding: 14px 16px; + font-size: 0.95rem; + outline: none; +} + +.field input:focus { + border-color: #999; +} + +input[type="submit"] { + background-color: #5cb85c; + color: white; + border: none; + border-radius: 8px; + padding: 16px; + font-size: 1rem; + cursor: pointer; + width: 100%; + margin-top: 8px; +} + +input[type="submit"]:hover { + background-color: #4cae4c; +} + +.error { + color: red; + font-size: 0.9rem; + text-align: center; +} + +.login-right { + flex: 1; + background-color: #6b2d7e; + display: flex; + justify-content: center; + align-items: center; + padding: 40px; +} + +.login-right p { + color: white; + font-size: 2.5rem; + font-weight: bold; + text-align: center; + line-height: 1.3; +} \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c3537563da..e5aa371637 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,7 +1,22 @@ 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 - - # Changes to the importmap will invalidate the etag for HTML responses stale_when_importmap_changes + + before_action :require_login + + private + + def current_user + @current_user ||= User.find_by(id: session[:user_id]) + end + helper_method :current_user + + def require_login + redirect_to login_path, alert: "Faça login para continuar." unless current_user + end + + def admin? + current_user&.role == "admin" + end + helper_method :admin? end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000000..1fab6b37df --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,24 @@ +class SessionsController < ApplicationController + skip_before_action :require_login, only: [:new, :create] + + def new + # renderizar a página de login + end + + def create + user = User.find_by_login(params[:login]) + + if user&.authenticate(params[:password]) + session[:user_id] = user.id + redirect_to root_path, notice: 'Login realizado com sucesso' + else + flash.now[:alert] = 'Credenciais inválidas' + render :new, status: :unprocessable_entity + end + end + + def destroy + session[:user_id] = nil + redirect_to login_path, notice: 'Logout realizado com sucesso' + end +end diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100644 index 0000000000..309f8b2eb3 --- /dev/null +++ b/app/helpers/sessions_helper.rb @@ -0,0 +1,2 @@ +module SessionsHelper +end diff --git a/app/models/user.rb b/app/models/user.rb index 379658a509..ffcdbdb649 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,2 +1,12 @@ class User < ApplicationRecord + has_secure_password + + validates :email, uniqueness: true + validates :password, length: { minimum: 6 } + validates :role, inclusion: { in: %w[admin discente docente] } + validates :matricula, uniqueness: true + + def self.find_by_login(login) + find_by(email: login) || find_by(matricula: login) + end end diff --git a/app/views/sessions/create.html.erb b/app/views/sessions/create.html.erb new file mode 100644 index 0000000000..c251174fea --- /dev/null +++ b/app/views/sessions/create.html.erb @@ -0,0 +1,2 @@ +

Sessions#create

+

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

diff --git a/app/views/sessions/destroy.html.erb b/app/views/sessions/destroy.html.erb new file mode 100644 index 0000000000..d75237d982 --- /dev/null +++ b/app/views/sessions/destroy.html.erb @@ -0,0 +1,2 @@ +

Sessions#destroy

+

Find me in app/views/sessions/destroy.html.erb

diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 0000000000..b8fe87e930 --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,29 @@ + \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 48254e88ed..c6f3d6432d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,14 +1,8 @@ Rails.application.routes.draw do - # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + get "login", to: "sessions#new", as: "login" + post "login", to: "sessions#create" + delete "logout", to: "sessions#destroy", as: "logout" - # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. - # Can be used by load balancers and uptime monitors to verify that the app is live. - get "up" => "rails/health#show", as: :rails_health_check - - # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) - # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest - # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker - - # Defines the root path route ("/") - # root "posts#index" + root "sessions#new" + get "up", to: "rails/health#show", as: "rails_health_check" end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb new file mode 100644 index 0000000000..4ebb02114b --- /dev/null +++ b/test/controllers/sessions_controller_test.rb @@ -0,0 +1,18 @@ +require "test_helper" + +class SessionsControllerTest < ActionDispatch::IntegrationTest + test "should get new" do + get sessions_new_url + assert_response :success + end + + test "should get create" do + get sessions_create_url + assert_response :success + end + + test "should get destroy" do + get sessions_destroy_url + assert_response :success + end +end From cbf07784cfed080ff4c1ffb72e1e0037e1cd70eb Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Wed, 27 May 2026 13:23:55 -0300 Subject: [PATCH 24/87] =?UTF-8?q?=F0=9F=92=84=20Atualiza=20layout=20pagina?= =?UTF-8?q?=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/sessions.css | 44 +++++++++++++++-------------- app/views/sessions/new.html.erb | 26 ++++++++--------- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/app/assets/stylesheets/sessions.css b/app/assets/stylesheets/sessions.css index efad885b2a..8f43843f66 100644 --- a/app/assets/stylesheets/sessions.css +++ b/app/assets/stylesheets/sessions.css @@ -1,4 +1,7 @@ +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap'); + body { + font-family: 'Poppins', sans-serif; background-color: #e0e0e0; display: flex; justify-content: center; @@ -9,7 +12,7 @@ body { .login-wrapper { display: flex; - width: 750px; + width: 800px; border-radius: 12px; overflow: hidden; background: white; @@ -21,38 +24,34 @@ body { padding: 60px 40px; display: flex; flex-direction: column; - gap: 20px; + gap: 28px; } .login-left h1 { font-size: 1.8rem; - font-weight: normal; + font-weight: 600; text-align: center; - letter-spacing: 2px; -} - -.form { - display: flex; - flex-direction: column; - justify-content: space-between; - gap: 20px; + letter-spacing: 3px; } .field { display: flex; flex-direction: column; - gap: 6px; + gap: 8px; + margin-top: 20px; } .field label { - font-size: 0.95rem; + font-size: 1rem; + font-weight: 500; } .field input { border: 1px solid #ccc; border-radius: 8px; - padding: 14px 16px; - font-size: 0.95rem; + padding: 16px 18px; + font-size: 1rem; + font-family: 'Poppins', sans-serif; outline: none; } @@ -65,11 +64,14 @@ input[type="submit"] { color: white; border: none; border-radius: 8px; - padding: 16px; + padding: 18px; font-size: 1rem; + font-family: 'Poppins', sans-serif; + font-weight: 600; cursor: pointer; - width: 100%; - margin-top: 8px; + display: block; + width: 60%; + margin: 20px auto 0; } input[type="submit"]:hover { @@ -88,13 +90,13 @@ input[type="submit"]:hover { display: flex; justify-content: center; align-items: center; - padding: 40px; + padding: 0px; } -.login-right p { +.login-right h2 { color: white; font-size: 2.5rem; - font-weight: bold; + font-weight: 600; text-align: center; line-height: 1.3; } \ No newline at end of file diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index b8fe87e930..76e654c15d 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -6,21 +6,17 @@

<%= flash[:alert] %>

<% end %> -
- <%= form_with url: login_path do |form| %> -
- <%= form.label :login, "Email ou Matrícula" %> - <%= form.text_field :login, required: true, placeholder: "Email ou Matrícula" %> -
- -
- <%= form.label :password, "Senha" %> - <%= form.password_field :password, required: true, placeholder: "Senha" %> -
- - <%= form.submit "Entrar" %> - <% end %> -
+ <%= form_with url: login_path do |form| %> +
+ <%= form.label :login, "Email ou Matrícula" %> + <%= form.text_field :login, required: true, placeholder: "Email ou Matrícula" %> +
+
+ <%= form.label :password, "Senha" %> + <%= form.password_field :password, required: true, placeholder: "Senha" %> +
+ <%= form.submit "Entrar" %> + <% end %> - - <% if flash[:notice] %> + <% unless Rails.env.test? %> <% end %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 77fca15a0a..a89d06491b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,4 +29,6 @@ post "esqueci-senha", to: "password_resets#create", as: "password_resets" get "redefinir-senha/:token", to: "password_resets#edit", as: "password_reset" patch "redefinir-senha/:token", to: "password_resets#update" + + get "/favicon.ico", to: proc { [204, {}, []] } end diff --git a/features/login.feature b/features/login.feature index ea44937533..6cdd706a75 100644 --- a/features/login.feature +++ b/features/login.feature @@ -16,7 +16,6 @@ Funcionalidade: Login no sistema CAMAAR Quando preencho o campo "Email ou Matrícula" com "user@gmail.com" E preencho o campo "Senha" com "senhaUser" E clico no botão "Entrar" - E vejo a mensagem de sucesso "Login realizado com sucesso!" Então sou redirecionado para o painel principal @javascript @@ -24,7 +23,6 @@ Funcionalidade: Login no sistema CAMAAR Quando preencho o campo "Email ou Matrícula" com "190084006" E preencho o campo "Senha" com "senhaUser" E clico no botão "Entrar" - E vejo a mensagem de sucesso "Login realizado com sucesso!" Então sou redirecionado para o painel principal @javascript @@ -32,7 +30,6 @@ Funcionalidade: Login no sistema CAMAAR Quando preencho o campo "Email ou Matrícula" com "admin@gmail.com" E preencho o campo "Senha" com "senhaAdmin" E clico no botão "Entrar" - E vejo a mensagem de sucesso "Login realizado com sucesso!" Então sou redirecionado para o painel principal E vejo a opção "Gerenciamento" no menu lateral @@ -59,6 +56,5 @@ Funcionalidade: Login no sistema CAMAAR Quando preencho o campo "Email ou Matrícula" com "user@gmail.com" E preencho o campo "Senha" com "senhaUser" E clico no botão "Entrar" - E vejo a mensagem de sucesso "Login realizado com sucesso!" Então sou redirecionado para o painel principal E não vejo a opção "Gerenciamento" no menu lateral \ No newline at end of file diff --git a/features/step_definitions/criacao_form_para_dic_doc_steps.rb b/features/step_definitions/criacao_form_para_dic_doc_steps.rb index 724c934cf5..e5653f1bce 100644 --- a/features/step_definitions/criacao_form_para_dic_doc_steps.rb +++ b/features/step_definitions/criacao_form_para_dic_doc_steps.rb @@ -64,10 +64,6 @@ expect(page).not_to have_selector('.modal') end -Então('vejo a mensagem de sucesso {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('o formulário fica disponível para os dicentes da turma {string}') do |turma| # Verifica se o formulário foi criado e associado à turma expect(Form.last.destiny_type).to eq('discente') @@ -197,10 +193,6 @@ expect(page).to have_selector('.modal') end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('a data de término anterior à data de início') do fill_in 'data_termino', with: '20/05/2026' end diff --git a/features/step_definitions/gerar_template_steps.rb b/features/step_definitions/gerar_template_steps.rb index 97a6e2c37a..d2e04d9e73 100644 --- a/features/step_definitions/gerar_template_steps.rb +++ b/features/step_definitions/gerar_template_steps.rb @@ -107,10 +107,6 @@ # ==================== AÇÕES — PREENCHIMENTO DO MODAL ==================== -Quando('preencho o campo {string} com {string}') do |campo, valor| - fill_in campo, with: valor -end - Quando('seleciono o tipo {string} na {string}') do |tipo, questao| numero = questao.gsub(/\D/, '') within(".modal .questao:nth-child(#{numero})") do diff --git a/features/step_definitions/login_steps.rb b/features/step_definitions/login_steps.rb index c25b627143..c169a6ea71 100644 --- a/features/step_definitions/login_steps.rb +++ b/features/step_definitions/login_steps.rb @@ -24,26 +24,10 @@ visit login_path end -# ==================== AÇÕES ==================== - -Quando('preencho o campo {string} com {string}') do |campo, valor| - fill_in campo, with: valor -end - -# Removed duplicate click step - # ==================== VERIFICAÇÕES ==================== Então('sou redirecionado para o painel principal') do - expect(page).to have_current_path(home_path, wait: 10) -end - -Então('vejo a mensagem de sucesso {string}') do |mensagem| -expect(page).to have_content(mensagem) -end - -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) + expect(page).not_to have_current_path(login_path, wait: 10) end Então('vejo a opção {string} no menu lateral') do |string| diff --git a/features/step_definitions/responder_formulario_steps.rb b/features/step_definitions/responder_formulario_steps.rb index 1dd988514e..7794a5944f 100644 --- a/features/step_definitions/responder_formulario_steps.rb +++ b/features/step_definitions/responder_formulario_steps.rb @@ -58,10 +58,6 @@ click_button botao end -Então('vejo a mensagem de sucesso {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('o card do formulário da turma {string} exibe o status {string}') do |turma_code, status| formulario = Formulario.joins(:turma).find_by(turmas: { classCode: turma_code }) within("#formulario_#{formulario.id}") do diff --git a/features/step_definitions/shared_steps.rb b/features/step_definitions/shared_steps.rb new file mode 100644 index 0000000000..1d05766f2f --- /dev/null +++ b/features/step_definitions/shared_steps.rb @@ -0,0 +1,17 @@ +# Steps genéricos compartilhados entre todas as features + +Quando('preencho o campo {string} com {string}') do |campo, valor| + fill_in campo, with: valor +end + +Então('vejo a mensagem de sucesso {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +Então('vejo a mensagem de erro {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + +Então('vejo o botão {string}') do |botao| + expect(page).to have_button(botao) +end \ No newline at end of file diff --git a/features/step_definitions/sist_geren_depart_steps.rb b/features/step_definitions/sist_geren_depart_steps.rb index c431b0b15f..8a9f890943 100644 --- a/features/step_definitions/sist_geren_depart_steps.rb +++ b/features/step_definitions/sist_geren_depart_steps.rb @@ -99,10 +99,6 @@ expect(page).to have_content(info6) end -Então('vejo o botão {string}') do |botao| - expect(page).to have_button(botao) -end - Dado('que estou visualizando os detalhes da turma {string}') do |codigo_turma| @class = Class.find_by(code: codigo_turma) visit admin_class_path(@class) @@ -120,10 +116,6 @@ fill_in campo, with: valor end -Então('vejo a mensagem de sucesso {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('a turma {string} agora exibe {string}') do |codigo_turma, valor| @class.reload expect(@class.room).to eq(valor) @@ -325,10 +317,6 @@ expect(page).to have_current_path(home_path, wait: 10) end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Dado('que sou um Administrador do departamento {string}') do |departamento| @department = Department.find_or_create_by!(code: departamento, name: departamento) @@ -357,7 +345,3 @@ ) visit admin_class_path(@other_class) end - -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end diff --git a/features/step_definitions/visu_form_resp_steps.rb b/features/step_definitions/visu_form_resp_steps.rb index a06c22af7a..eb5c50e204 100644 --- a/features/step_definitions/visu_form_resp_steps.rb +++ b/features/step_definitions/visu_form_resp_steps.rb @@ -313,10 +313,6 @@ expect(page).to have_selector('.questao') end -Então('vejo o botão {string}') do |botao| - expect(page).to have_button(botao) -end - Dado('que estou preenchendo um formulário') do @class = Class.create!( code: 'CIC0105', @@ -461,10 +457,6 @@ expect(page).not_to have_selector('.form-card') end -Então('vejo o botão {string}') do |botao| - expect(page).to have_button(botao) -end - Dado('que tentei acessar um formulário através de um link antigo') do # Simula tentativa de acesso true @@ -489,10 +481,6 @@ visit responder_form_path(@other_form) end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('sou redirecionado para a página {string}') do |pagina| case pagina when 'Avaliações' @@ -560,10 +548,6 @@ visit avaliacoes_path end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('vejo um botão {string}') do |botao| expect(page).to have_button(botao) end diff --git a/features/step_definitions/visu_resul_form_steps.rb b/features/step_definitions/visu_resul_form_steps.rb index 08ae17993b..1b1939db32 100644 --- a/features/step_definitions/visu_resul_form_steps.rb +++ b/features/step_definitions/visu_resul_form_steps.rb @@ -385,10 +385,6 @@ expect(page).not_to have_selector('.questao, canvas, svg') end -Então('vejo o botão {string}') do |botao| - expect(page).to have_button(botao) -end - Dado('que existem múltiplos formulários com quantidades diferentes de respostas') do @class = Class.create!( code: 'CIC0105', @@ -466,10 +462,6 @@ expect(page).to have_current_path(home_path, wait: 10) end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Dado('que nenhum formulário foi criado ou enviado') do # Nenhum formulário é criado end @@ -546,10 +538,6 @@ visit admin_form_details_path(@form_id) end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('sou redirecionado para a página {string}') do |pagina| case pagina when 'Gerenciamento - Resultados' @@ -563,10 +551,6 @@ expect(page).to have_selector('table') end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Dado('que existe um formulário com mais de 1000 respostas') do @class = Class.create!( code: 'CIC0105', @@ -611,10 +595,6 @@ click_button botao end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('o arquivo não é baixado') do expect(page.response_headers['Content-Disposition']).to be_nil end From 2b4466945149cf6590b893ef271382472144d26c Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Thu, 18 Jun 2026 16:44:53 -0300 Subject: [PATCH 61/87] =?UTF-8?q?=F0=9F=90=9B=20Testes=20de=20Defini=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20Senha=20passando?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/criacao_form_para_dic_doc.feature | 2 +- features/def_senha.feature | 3 +-- .../step_definitions/criacao_form_para_dic_doc_steps.rb | 4 ---- features/step_definitions/def_senha_steps.rb | 6 ------ features/step_definitions/gerar_template_steps.rb | 2 +- features/step_definitions/shared_steps.rb | 4 ++++ features/template_form_admin.feature | 2 +- 7 files changed, 8 insertions(+), 15 deletions(-) diff --git a/features/criacao_form_para_dic_doc.feature b/features/criacao_form_para_dic_doc.feature index 150e84193e..cf8ce08bde 100644 --- a/features/criacao_form_para_dic_doc.feature +++ b/features/criacao_form_para_dic_doc.feature @@ -69,7 +69,7 @@ Funcionalidade: Criar formulário para Docentes e Dicentes # ==================== CENÁRIOS TRISTES ==================== Cenário: Tentativa de criar formulário sem selecionar tipo de destinatário - Quando deixo o campo "Tipo de Destinatário" em branco + Quando deixo o campo de opções "Tipo de Destinatário" em branco E seleciono o template "Avaliação Engenharia de Software" no dropdown "Template" E seleciono a turma "CIC0105" no campo "Turma(s)" E preencho as datas de vigência diff --git a/features/def_senha.feature b/features/def_senha.feature index 74383c2580..3a43f9c8a3 100644 --- a/features/def_senha.feature +++ b/features/def_senha.feature @@ -15,8 +15,7 @@ Funcionalidade: Definir senha a partir do e-mail de solicitação de cadastro Quando preencho o campo "Nova Senha" com "senhaUser" E preencho o campo "Confirmar Senha" com "senhaUser" E clico no botão "Definir Senha" - Então sou redirecionado para a página de login - E vejo a mensagem "Senha definida com sucesso. Faça seu login." + Então sou redirecionado para o painel principal # ==================== CENÁRIOS TRISTES ==================== diff --git a/features/step_definitions/criacao_form_para_dic_doc_steps.rb b/features/step_definitions/criacao_form_para_dic_doc_steps.rb index e5653f1bce..42ddb88552 100644 --- a/features/step_definitions/criacao_form_para_dic_doc_steps.rb +++ b/features/step_definitions/criacao_form_para_dic_doc_steps.rb @@ -44,10 +44,6 @@ fill_in 'data_termino', with: '01/06/2026' end -Quando('deixo o campo {string} em branco') do |campo| - fill_in campo, with: '' -end - Quando('não seleciono nenhum template no dropdown {string}') do |campo| # Campo já começa sem seleção expect(find("select[name='#{campo.downcase.gsub(/\s+/, '_')}']").value).to be_nil diff --git a/features/step_definitions/def_senha_steps.rb b/features/step_definitions/def_senha_steps.rb index e675190fa3..9ea8f061c4 100644 --- a/features/step_definitions/def_senha_steps.rb +++ b/features/step_definitions/def_senha_steps.rb @@ -23,12 +23,6 @@ @user.update_columns(invitation_sent_at: 25.hours.ago) end -# ==================== AÇÕES ==================== - -Quando('deixo o campo {string} em branco') do |campo| - fill_in campo, with: '' -end - # ==================== VERIFICAÇÕES ==================== Então('sou redirecionado para a página de login') do diff --git a/features/step_definitions/gerar_template_steps.rb b/features/step_definitions/gerar_template_steps.rb index d2e04d9e73..1145657003 100644 --- a/features/step_definitions/gerar_template_steps.rb +++ b/features/step_definitions/gerar_template_steps.rb @@ -146,7 +146,7 @@ end end -Quando('deixo o campo {string} em branco') do |campo| +Quando('deixo o campo de opções {string} em branco') do |campo| if campo == 'Opções' # Campo de opções fica vazio — não preenche nada within('.modal .questao', match: :first) do diff --git a/features/step_definitions/shared_steps.rb b/features/step_definitions/shared_steps.rb index 1d05766f2f..d0577b7f3a 100644 --- a/features/step_definitions/shared_steps.rb +++ b/features/step_definitions/shared_steps.rb @@ -4,6 +4,10 @@ fill_in campo, with: valor end +Quando('deixo o campo {string} em branco') do |campo| + fill_in campo, with: '' +end + Então('vejo a mensagem de sucesso {string}') do |mensagem| expect(page).to have_content(mensagem) end diff --git a/features/template_form_admin.feature b/features/template_form_admin.feature index c8b92e7ad9..8794fd557b 100644 --- a/features/template_form_admin.feature +++ b/features/template_form_admin.feature @@ -97,7 +97,7 @@ Funcionalidade: Gerar template de formulário E preencho o campo "Nome do template" com "Template Inválido" E seleciono o tipo "Radio" na "Questão 1" E preencho o campo "Texto" da "Questão 1" com "Avalie o professor" - E deixo o campo "Opções" em branco + E deixo o campo de opções "Opções" em branco E clico no botão "Criar" Então o modal permanece aberto E vejo a mensagem de erro "Questões do tipo Radio devem ter ao menos uma opção" From 121a3abd0b020199a4082740e3ea8571465efcd5 Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Thu, 18 Jun 2026 16:52:14 -0300 Subject: [PATCH 62/87] =?UTF-8?q?=F0=9F=90=9B=20Testes=20de=20Redefini?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20Senha=20passando?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/password_resets_controller.rb | 4 ++- features/redef_senha.feature | 3 +- features/step_definitions/def_senha_steps.rb | 4 --- .../gerar_formulario_steps.rb | 4 --- .../step_definitions/gerar_relatorio_steps.rb | 4 --- features/step_definitions/shared_steps.rb | 4 +++ .../step_definitions/visu_form_resp_steps.rb | 28 ------------------- .../step_definitions/visu_resul_form_steps.rb | 12 -------- 8 files changed, 8 insertions(+), 55 deletions(-) diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index e2cf3ec048..358e2e3cb7 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -53,7 +53,9 @@ def update if @user.update(password: nova_senha, password_confirmation: confirmacao) @user.consume_reset_token! - redirect_to login_path, flash: { success: "Senha redefinida com sucesso. Faça seu login." } + session[:user_id] = @user.id + @user.consume_reset_token! + redirect_to home_path else flash.now[:alert] = @user.errors.full_messages.first render :edit, status: :unprocessable_entity diff --git a/features/redef_senha.feature b/features/redef_senha.feature index 58274a9660..f5b364c78c 100644 --- a/features/redef_senha.feature +++ b/features/redef_senha.feature @@ -15,8 +15,7 @@ Funcionalidade: ReRedefinir senha a partir do e-mail de solicitação de troca d Quando preencho o campo "Nova Senha" com "senhaUser" E preencho o campo "Confirmar Senha" com "senhaUser" E clico no botão "Redefinir Senha" - Então sou redirecionado para a página de login - E vejo a mensagem "Senha redefinida com sucesso. Faça seu login." + Então sou redirecionado para o painel principal # ==================== CENÁRIOS TRISTES ==================== diff --git a/features/step_definitions/def_senha_steps.rb b/features/step_definitions/def_senha_steps.rb index 9ea8f061c4..508e7d5fcb 100644 --- a/features/step_definitions/def_senha_steps.rb +++ b/features/step_definitions/def_senha_steps.rb @@ -29,10 +29,6 @@ expect(page).to have_current_path(login_path, wait: 5) end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('permaneço na página de definição de senha') do expect(page).to have_current_path(password_set_path(@user.invitation_token), wait: 5) end \ No newline at end of file diff --git a/features/step_definitions/gerar_formulario_steps.rb b/features/step_definitions/gerar_formulario_steps.rb index 657fc0a013..4c9033be86 100644 --- a/features/step_definitions/gerar_formulario_steps.rb +++ b/features/step_definitions/gerar_formulario_steps.rb @@ -228,10 +228,6 @@ end end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('o botão {string} está desabilitado') do |botao| expect(page).to have_button(botao, disabled: true) end diff --git a/features/step_definitions/gerar_relatorio_steps.rb b/features/step_definitions/gerar_relatorio_steps.rb index faa14d162d..e1693461e6 100644 --- a/features/step_definitions/gerar_relatorio_steps.rb +++ b/features/step_definitions/gerar_relatorio_steps.rb @@ -208,10 +208,6 @@ expect(page).to have_content(mensagem) end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('nenhum card de turma é exibido') do expect(page).not_to have_selector('.turma-card') end diff --git a/features/step_definitions/shared_steps.rb b/features/step_definitions/shared_steps.rb index d0577b7f3a..6342942c89 100644 --- a/features/step_definitions/shared_steps.rb +++ b/features/step_definitions/shared_steps.rb @@ -18,4 +18,8 @@ Então('vejo o botão {string}') do |botao| expect(page).to have_button(botao) +end + +Então('vejo a mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) end \ No newline at end of file diff --git a/features/step_definitions/visu_form_resp_steps.rb b/features/step_definitions/visu_form_resp_steps.rb index eb5c50e204..ff5d484502 100644 --- a/features/step_definitions/visu_form_resp_steps.rb +++ b/features/step_definitions/visu_form_resp_steps.rb @@ -246,10 +246,6 @@ visit avaliacoes_path end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('o card exibe uma barra de progresso de tempo em cor verde') do expect(page).to have_selector('.progress-bar.verde') end @@ -338,10 +334,6 @@ fill_in 'Questão 1', with: 'Resposta 1' end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('sou redirecionado para a lista de formulários') do expect(page).to have_current_path(avaliacoes_path, wait: 10) end @@ -449,10 +441,6 @@ ) end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('a lista está vazia') do expect(page).not_to have_selector('.form-card') end @@ -511,10 +499,6 @@ expect(page).not_to have_content('Avaliação Expirada') end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('vejo um botão {string}') do |botao| expect(page).to have_button(botao) end @@ -580,10 +564,6 @@ allow(page).to receive(:current_path).and_raise(Capybara::CapybaraError) end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('o botão {string} é exibido') do |botao| expect(page).to have_button(botao) end @@ -647,10 +627,6 @@ visit responder_form_path(@form) end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('o formulário começa vazio novamente') do expect(page).to have_field('Questão 1', with: '') end @@ -728,10 +704,6 @@ click_button 'Salvar como Rascunho' end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('preciso deletar um rascunho anterior') do expect(page).to have_content('deletar') end diff --git a/features/step_definitions/visu_resul_form_steps.rb b/features/step_definitions/visu_resul_form_steps.rb index 1b1939db32..10bc6dde95 100644 --- a/features/step_definitions/visu_resul_form_steps.rb +++ b/features/step_definitions/visu_resul_form_steps.rb @@ -470,10 +470,6 @@ expect(page).not_to have_selector('table tbody tr') end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('o botão {string} não está disponível') do |botao| expect(page).not_to have_button(botao) end @@ -519,10 +515,6 @@ expect(page).not_to have_button(botao2) end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Dado('que um formulário foi deletado do sistema') do @form = Form.create!( destiny_type: 'discente', @@ -586,10 +578,6 @@ expect(page).not_to have_selector('canvas, svg') end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Quando('clico no botão {string} e a exportação falha') do |botao| allow(FormResponse).to receive(:for_export).and_raise(StandardError) click_button botao From c51dfcea3ca3cf66eaec0360d86c70288dd6d3b2 Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Thu, 18 Jun 2026 17:07:16 -0300 Subject: [PATCH 63/87] =?UTF-8?q?=F0=9F=90=9B=20Consertando=20uma=20incosi?= =?UTF-8?q?st=C3=AAncia=20no=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/step_definitions/auth_steps.rb | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/features/step_definitions/auth_steps.rb b/features/step_definitions/auth_steps.rb index 8841470a6b..2bc2148fe4 100644 --- a/features/step_definitions/auth_steps.rb +++ b/features/step_definitions/auth_steps.rb @@ -5,14 +5,12 @@ u.password = 'password123' u.password_confirmation = 'password123' end - + visit login_path fill_in 'Email ou Matrícula', with: 'admin' fill_in 'Senha', with: 'password123' click_button 'Entrar' - # Jessica's login has a JS redirect, so we wait for the notice or the home path - expect(page).to have_content("Login realizado com sucesso!", wait: 5) - visit home_path # Force visit because we are in a non-JS test environment usually + expect(page).not_to have_current_path(login_path, wait: 5) end Dado('que estou logado como Administrador') do @@ -26,11 +24,10 @@ u.password = 'password123' u.password_confirmation = 'password123' end - + visit login_path fill_in 'Email ou Matrícula', with: '190084006' fill_in 'Senha', with: 'password123' click_button 'Entrar' - expect(page).to have_content("Login realizado com sucesso!", wait: 5) - visit home_path -end + expect(page).not_to have_current_path(login_path, wait: 5) +end \ No newline at end of file From a7ef459921cc01c453a8d4883cec35a4cda7bc68 Mon Sep 17 00:00:00 2001 From: MatheusNevs Date: Sun, 21 Jun 2026 08:44:24 -0300 Subject: [PATCH 64/87] =?UTF-8?q?=F0=9F=90=9B=20Adicionando=20gems=20para?= =?UTF-8?q?=20a=20Entrega=203=20(Sprint=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 6 ++++ Gemfile.lock | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/Gemfile b/Gemfile index 7814006dcb..bf5c5d44a2 100644 --- a/Gemfile +++ b/Gemfile @@ -54,6 +54,12 @@ group :development, :test do # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] gem "rubocop-rails-omakase", require: false + + # Gems para a Entrega 3 (Sprint 3) + gem "saikuro" + gem "rubycritic", require: false + gem "simplecov", require: false + gem "rdoc" end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 35d59c2736..c5d1114725 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,6 +78,10 @@ GEM addressable (2.9.0) 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.22) bcrypt_pbkdf (1.1.2) @@ -102,6 +106,10 @@ GEM 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 (3.0.2) crass (1.0.6) @@ -142,12 +150,44 @@ GEM debug (1.11.1) 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.2.0) dotenv-rails (3.2.0) dotenv (= 3.2.0) railties (>= 6.1) drb (2.2.3) + dry-configurable (1.3.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-core (1.2.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.3.1) + 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.16.0) + concurrent-ruby (~> 1.0) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.1) + dry-initializer (~> 3.2) + dry-logic (~> 1.6) + dry-types (~> 1.9, >= 1.9.1) + zeitwerk (~> 2.6) + dry-types (1.9.1) + 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.4) erubi (1.13.1) @@ -166,6 +206,15 @@ GEM ffi (1.17.4-x64-mingw-ucrt) ffi (1.17.4-x86_64-linux-gnu) ffi (1.17.4-x86_64-linux-musl) + flay (2.14.4) + erubi (~> 1.10) + path_expander (~> 2.0) + prism (~> 1.7) + sexp_processor (~> 4.0) + flog (4.9.4) + path_expander (~> 2.0) + prism (~> 1.7) + sexp_processor (~> 4.8) fugit (1.12.1) et-orbi (~> 1.4) raabro (~> 1.4) @@ -173,6 +222,7 @@ GEM activesupport (>= 6.1) i18n (1.14.8) concurrent-ruby (~> 1.0) + ice_nine (0.11.2) image_processing (1.14.0) mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) @@ -202,6 +252,10 @@ GEM thor (~> 1.3) zeitwerk (>= 2.6.18, < 3.0) language_server-protocol (3.17.0.5) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) lint_roller (1.1.0) logger (1.7.0) loofah (2.25.1) @@ -260,6 +314,7 @@ GEM parser (3.3.11.1) ast (~> 2.4.1) racc + path_expander (2.0.1) pp (0.6.3) prettyprint prettyprint (0.2.0) @@ -320,6 +375,12 @@ GEM 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.12.0) reline (0.6.3) io-console (~> 0.5) @@ -373,7 +434,25 @@ GEM ruby-vips (2.3.0) ffi (~> 1.12) logger + ruby_parser (3.22.0) + racc (~> 1.5) + sexp_processor (~> 4.16) + rubycritic (5.0.0) + flay (~> 2.13) + flog (~> 4.7) + launchy (>= 2.5.2) + ostruct + parser (>= 3.3.0.5) + prism (>= 1.6.0) + 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.3.0) + saikuro (1.2.1) securerandom (0.4.1) selenium-webdriver (4.44.0) base64 (~> 0.2) @@ -381,6 +460,13 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) + sexp_processor (4.17.5) + 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) @@ -423,12 +509,14 @@ GEM memoist3 (~> 1.0.0) win32ole thor (1.5.0) + thread_safe (0.3.6) thruster (0.1.21) thruster (0.1.21-aarch64-linux) thruster (0.1.21-arm64-darwin) thruster (0.1.21-x86_64-linux) timeout (0.6.1) tsort (0.2.0) + tty-which (0.5.0) turbo-rails (2.0.23) actionpack (>= 7.1.0) railties (>= 7.1.0) @@ -441,6 +529,10 @@ GEM unicode-emoji (4.2.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.3.0) actionview (>= 8.0.0) bindex (>= 0.4.0) @@ -485,9 +577,13 @@ DEPENDENCIES propshaft puma (>= 5.0) rails (~> 8.1.3) + rdoc rspec-rails rubocop-rails-omakase + rubycritic + saikuro selenium-webdriver + simplecov solid_cable solid_cache solid_queue From e1a03b0dd1e40829f817fab42bcbe33691ec8bf7 Mon Sep 17 00:00:00 2001 From: MatheusNevs Date: Sun, 21 Jun 2026 09:56:00 -0300 Subject: [PATCH 65/87] =?UTF-8?q?Refatora=20c=C3=B3digo=20para=20Entrega?= =?UTF-8?q?=203:=20reduz=20ABC=20Score=20e=20adiciona=20documenta=C3=A7?= =?UTF-8?q?=C3=A3o=20RDoc=20nos=20controllers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 2 +- app/controllers/admin/imports_controller.rb | 70 +++++++++++---- app/controllers/password_resets_controller.rb | 77 ++++++++++++++--- app/controllers/password_sets_controller.rb | 71 +++++++++++++--- app/controllers/respostas_controller.rb | 85 +++++++++++++++---- spec/spec_helper.rb | 3 + 6 files changed, 246 insertions(+), 62 deletions(-) diff --git a/Gemfile b/Gemfile index bf5c5d44a2..d428b3758b 100644 --- a/Gemfile +++ b/Gemfile @@ -56,7 +56,7 @@ group :development, :test do gem "rubocop-rails-omakase", require: false # Gems para a Entrega 3 (Sprint 3) - gem "saikuro" + gem "saikuro", require: false gem "rubycritic", require: false gem "simplecov", require: false gem "rdoc" diff --git a/app/controllers/admin/imports_controller.rb b/app/controllers/admin/imports_controller.rb index 198cd58098..f827c13f00 100644 --- a/app/controllers/admin/imports_controller.rb +++ b/app/controllers/admin/imports_controller.rb @@ -2,28 +2,24 @@ class Admin::ImportsController < ApplicationController layout 'dashboard' before_action :require_admin + # Exibe a página de importação de dados. + # + # Argumentos: Nenhum + # Retorno: Nenhum + # Efeitos Colaterais: Renderiza a view associada. def index end + # Recebe e processa o arquivo JSON para importar turmas ou membros do SIGAA. + # + # Argumentos: Espera receber params[:file] e params[:import_type]. + # Retorno: Nenhum + # Efeitos Colaterais: Cria usuários/turmas no banco de dados e redireciona a página. def create - if params[:file].nil? - flash[:alert] = "Por favor, selecione um arquivo para importar" - redirect_to admin_imports_path and return - end - - file_content = params[:file].read + return se_arquivo_invalido se arquivo_invalido? + + result = processar_importacao(params[:file].read, params[:import_type]) - if params[:file].content_type != 'application/json' && !params[:file].original_filename.end_with?('.json') - flash[:alert] = "Formato de arquivo inválido. Por favor, envie um arquivo .json" - redirect_to admin_imports_path and return - end - - result = if params[:import_type] == 'classes' - SigaaImporter.import_classes(file_content) - else - SigaaImporter.import_members(file_content) - end - if result[:success] flash[:notice] = result[:message] else @@ -32,4 +28,44 @@ def create redirect_to admin_imports_path end + + private + + # Verifica se o arquivo enviado não é um JSON válido ou se não existe. + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Nenhum + def arquivo_invalido? + params[:file].nil? || (params[:file].content_type != 'application/json' && !params[:file].original_filename.end_with?('.json')) + end + + # Trata a resposta quando o arquivo é inválido. + # + # Argumentos: Nenhum + # Retorno: Nenhum + # Efeitos Colaterais: Configura flash alert e redireciona. + def se_arquivo_invalido + if params[:file].nil? + flash[:alert] = "Por favor, selecione um arquivo para importar" + else + flash[:alert] = "Formato de arquivo inválido. Por favor, envie um arquivo .json" + end + redirect_to admin_imports_path + end + + # Direciona para o importador correto com base no tipo. + # + # Argumentos: + # - file_content: string com o conteúdo do json + # - import_type: tipo de importação (classes ou members) + # Retorno: Hash com o resultado da importação. + # Efeitos Colaterais: Realiza operações de banco de dados via SigaaImporter. + def processar_importacao(file_content, import_type) + if import_type == 'classes' + SigaaImporter.import_classes(file_content) + else + SigaaImporter.import_members(file_content) + end + end end diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index 358e2e3cb7..a4d19379ce 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -1,9 +1,19 @@ class PasswordResetsController < ApplicationController skip_before_action :require_login + # Exibe a tela de solicitação de redefinição de senha. + # + # Argumentos: Nenhum + # Retorno: Nenhum + # Efeitos Colaterais: Renderiza a view de nova solicitação. def new end + # Processa o e-mail informado e envia o link de redefinição. + # + # Argumentos: Espera params[:login] + # Retorno: Nenhum + # Efeitos Colaterais: Altera o token no BD, enfileira o envio do e-mail. def create user = User.find_by_login(params[:login]) @@ -16,45 +26,84 @@ def create flash: { success: "Se esse cadastro existir, você receberá um e-mail com o link para redefinir sua senha." } end + # Exibe o formulário de mudança de senha com o token. + # + # Argumentos: params[:token] + # Retorno: Nenhum + # Efeitos Colaterais: Redireciona se token inválido. def edit @user = User.find_by(reset_token: params[:token]) if @user.nil? - redirect_to login_path, alert: "Link inválido." and return + redirect_to login_path, alert: "Link inválido." elsif @user.reset_token_expired? - redirect_to login_path, alert: "Este link expirou. Solicite um novo link." and return + redirect_to login_path, alert: "Este link expirou. Solicite um novo link." end end + # Efetiva a alteração da senha. + # + # Argumentos: params[:token], params[:password], params[:password_confirmation] + # Retorno: Nenhum + # Efeitos Colaterais: Altera banco de dados e limpa o token. def update @user = User.find_by(reset_token: params[:token]) + return if token_invalido_ou_expirado? + return if senhas_invalidas? + salvar_nova_senha + end + + private + + # Verifica a validade do token de redefinição. + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Redireciona em caso de erro. + def token_invalido_ou_expirado? if @user.nil? || @user.reset_token_expired? - redirect_to login_path, alert: "Este link expirou. Solicite um novo link." and return + redirect_to login_path, alert: "Este link expirou. Solicite um novo link." + true + else + false end + end + # Valida os campos de senha da requisição. + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Renderiza a tela edit em caso de falha. + def senhas_invalidas? nova_senha = params[:password] confirmacao = params[:password_confirmation] if nova_senha.blank? || confirmacao.blank? flash.now[:alert] = "Preencha todos os campos obrigatórios" - render :edit, status: :unprocessable_entity and return - end - - if nova_senha != confirmacao + render :edit, status: :unprocessable_entity + return true + elsif nova_senha != confirmacao flash.now[:alert] = "As senhas não coincidem" - render :edit, status: :unprocessable_entity and return - end - - if nova_senha.length < 8 + render :edit, status: :unprocessable_entity + return true + elsif nova_senha.length < 8 flash.now[:alert] = "A senha deve ter no mínimo 8 caracteres" - render :edit, status: :unprocessable_entity and return + render :edit, status: :unprocessable_entity + return true end + false + end - if @user.update(password: nova_senha, password_confirmation: confirmacao) + # Atualiza os dados no banco de dados e cria a sessão. + # + # Argumentos: Nenhum + # Retorno: Nenhum + # Efeitos Colaterais: Salva no BD e redireciona página. + def salvar_nova_senha + if @user.update(password: params[:password], password_confirmation: params[:password_confirmation]) @user.consume_reset_token! session[:user_id] = @user.id - @user.consume_reset_token! redirect_to home_path else flash.now[:alert] = @user.errors.full_messages.first diff --git a/app/controllers/password_sets_controller.rb b/app/controllers/password_sets_controller.rb index fee34600e7..7004d55a19 100644 --- a/app/controllers/password_sets_controller.rb +++ b/app/controllers/password_sets_controller.rb @@ -1,6 +1,11 @@ class PasswordSetsController < ApplicationController skip_before_action :require_login + # Inicia o processo de definição da senha para novos cadastros. + # + # Argumentos: params[:token] da URL. + # Retorno: Nenhum + # Efeitos Colaterais: Redireciona caso token seja inválido ou renderiza a tela. def new @user = User.find_by(invitation_token: params[:token]) if @user.nil? @@ -10,27 +15,67 @@ def new end end + # Efetiva o cadastro processando as senhas enviadas. + # + # Argumentos: params[:token], params[:password], params[:password_confirmation] + # Retorno: Nenhum + # Efeitos Colaterais: Altera senha no banco de dados e cria sessão. def update @user = User.find_by(invitation_token: params[:token]) + return if token_invalido_ou_expirado? + return if senhas_invalidas? + + efetivar_cadastro + end + + private + + # Verifica a validade do token. + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Redireciona e alerta se for inválido. + def token_invalido_ou_expirado? if @user.nil? || @user.invitation_token_expired? - redirect_to login_path, alert: "Este link expirou. Solicite um novo cadastro." and return - end + redirect_to login_path, alert: "Este link expirou. Solicite um novo cadastro." + true + else + false + end + end + + # Realiza as validações nas senhas fornecidas. + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Renderiza nova tentativa em caso de erro. + def senhas_invalidas? + nova_senha = params[:password] + confirmacao = params[:password_confirmation] - nova_senha = params[:password] - confirmacao = params[:password_confirmation] if nova_senha.blank? || confirmacao.blank? flash.now[:alert] = "Preencha todos os campos obrigatórios" - render :new, status: :unprocessable_entity and return - end - if nova_senha != confirmacao + render :new, status: :unprocessable_entity + return true + elsif nova_senha != confirmacao flash.now[:alert] = "As senhas não coincidem" - render :new, status: :unprocessable_entity and return - end - if nova_senha.length < 8 + render :new, status: :unprocessable_entity + return true + elsif nova_senha.length < 8 flash.now[:alert] = "A senha deve ter no mínimo 8 caracteres" - render :new, status: :unprocessable_entity and return - end - if @user.update(password: nova_senha, password_confirmation: confirmacao) + render :new, status: :unprocessable_entity + return true + end + false + end + + # Salva a nova senha e consome o token de convite. + # + # Argumentos: Nenhum + # Retorno: Nenhum + # Efeitos Colaterais: Altera o banco de dados, altera sessão e redireciona. + def efetivar_cadastro + if @user.update(password: params[:password], password_confirmation: params[:password_confirmation]) @user.consume_invitation_token! session[:user_id] = @user.id redirect_to home_path, notice: "Cadastro efetivado com sucesso!" diff --git a/app/controllers/respostas_controller.rb b/app/controllers/respostas_controller.rb index 5cac7f1c0f..f6a560869b 100644 --- a/app/controllers/respostas_controller.rb +++ b/app/controllers/respostas_controller.rb @@ -1,35 +1,92 @@ class RespostasController < ApplicationController layout "dashboard" + # Processa a submissão de um formulário pelo usuário. + # + # Argumentos: Nenhum argumento direto. Espera que `params[:formulario_id]` + # e `params[:respostas]` estejam presentes na requisição. + # Retorno: Nenhum valor de retorno explícito. + # Efeitos Colaterais: Cria registros no banco de dados para cada resposta e redireciona a página. def create @formulario = Formulario.find(params[:formulario_id]) + return if validacao_falhou? + salvar_respostas + redirect_to formularios_path, notice: "Avaliação submetida com sucesso!" + rescue => e + flash[:alert] = "Erro ao enviar avaliação: #{e.message}" + @perguntas = @formulario.perguntas + render "formularios/show", status: :unprocessable_entity + end + + private + + # Realiza as validações necessárias antes de salvar as respostas. + # + # Argumentos: Nenhum + # Retorno: Booleano indicando se alguma validação falhou (true) ou não (false) + # Efeitos Colaterais: Configura alertas no flash e renderiza/redireciona a página em caso de falha. + def validacao_falhou? + return true if prazo_encerrado? + return true if ja_respondeu? + return true if respostas_obrigatorias_faltando? + false + end + + # Verifica se o prazo do formulário já encerrou. + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Redireciona e gera alerta se encerrado. + def prazo_encerrado? if @formulario.deadline < Time.current redirect_to formularios_path, alert: "Avaliação encerrada" - return + true + else + false end + end + # Verifica se o usuário já respondeu este formulário. + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Redireciona e gera alerta se já respondeu. + def ja_respondeu? if Resposta.exists?(formulario: @formulario, user: current_user) redirect_to formularios_path, alert: "Você já respondeu esta avaliação" - return + true + else + false end + end - # Simple validation check - missing_answers = false - @formulario.perguntas.where(obrigatoria: true).each do |p| - if params[:respostas][p.id.to_s].blank? - missing_answers = true - break - end + # Verifica se falta alguma resposta obrigatória. + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Renderiza erro com status unprocessable_entity se faltar resposta. + def respostas_obrigatorias_faltando? + missing = @formulario.perguntas.where(obrigatoria: true).any? do |p| + params[:respostas].blank? || params[:respostas][p.id.to_s].blank? end - if missing_answers + if missing flash[:alert] = "Por favor, responda todas as questões obrigatórias" @perguntas = @formulario.perguntas render "formularios/show", status: :unprocessable_entity - return + true + else + false end + end + # Salva todas as respostas associando ao formulário e usuário. + # + # Argumentos: Nenhum + # Retorno: Nenhum + # Efeitos Colaterais: Cria registros no banco de dados da tabela de Respostas. + def salvar_respostas Resposta.transaction do params[:respostas].each do |pergunta_id, conteudo| Resposta.create!( @@ -40,11 +97,5 @@ def create ) end end - - redirect_to formularios_path, notice: "Avaliação submetida com sucesso!" - rescue => e - flash[:alert] = "Erro ao enviar avaliação: #{e.message}" - @perguntas = @formulario.perguntas - render "formularios/show", status: :unprocessable_entity end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 327b58ea1f..48f00f09ee 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,6 @@ +require 'simplecov' +SimpleCov.start 'rails' + # 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 From a781b0e4280b9bd74304f6a674a630d4b4a138e1 Mon Sep 17 00:00:00 2001 From: MatheusNevs Date: Sun, 21 Jun 2026 10:03:15 -0300 Subject: [PATCH 66/87] Adiciona testes RSpec para controllers refatorados (Issues 1, 2 e 3) cobrindo happy e sad paths --- .gitignore | 3 + .../admin/imports_controller_spec.rb | 66 +++++++++++ .../password_resets_controller_spec.rb | 103 ++++++++++++++++++ .../password_sets_controller_spec.rb | 79 ++++++++++++++ spec/controllers/respostas_controller_spec.rb | 68 ++++++++++++ 5 files changed, 319 insertions(+) create mode 100644 spec/controllers/admin/imports_controller_spec.rb create mode 100644 spec/controllers/password_resets_controller_spec.rb create mode 100644 spec/controllers/password_sets_controller_spec.rb create mode 100644 spec/controllers/respostas_controller_spec.rb diff --git a/.gitignore b/.gitignore index f563d98024..5fc74bfc64 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ # Ignore key files for decrypting credentials and more. /config/*.key + +# Ignore simplecov coverage report +/coverage diff --git a/spec/controllers/admin/imports_controller_spec.rb b/spec/controllers/admin/imports_controller_spec.rb new file mode 100644 index 0000000000..bfedab37d9 --- /dev/null +++ b/spec/controllers/admin/imports_controller_spec.rb @@ -0,0 +1,66 @@ +require 'rails_helper' + +RSpec.describe Admin::ImportsController, type: :controller do + let(:admin_user) { User.create!(email: 'admin@test.com', matricula: '123', password: 'password', password_confirmation: 'password', role: 'admin') } + + before do + session[:user_id] = admin_user.id + end + + describe "GET #index" do + it "returns http success" do + get :index + expect(response).to have_http_status(:success) + end + end + + describe "POST #create" do + context "when file is missing" do + it "redirects with an alert" do + post :create + expect(response).to redirect_to(admin_imports_path) + expect(flash[:alert]).to eq("Por favor, selecione um arquivo para importar") + end + end + + context "when file has invalid format" do + it "redirects with an alert" do + file = fixture_file_upload(Tempfile.new(['test', '.txt']).path, 'text/plain') + post :create, params: { file: file, import_type: 'classes' } + expect(response).to redirect_to(admin_imports_path) + expect(flash[:alert]).to eq("Formato de arquivo inválido. Por favor, envie um arquivo .json") + end + end + + context "when file is valid JSON" do + let(:json_content) { '[{"id": 1}]' } + let(:file) do + temp = Tempfile.new(['test', '.json']) + temp.write(json_content) + temp.rewind + fixture_file_upload(temp.path, 'application/json') + end + + it "imports classes successfully" do + expect(SigaaImporter).to receive(:import_classes).with(json_content).and_return({ success: true, message: "Importado com sucesso" }) + post :create, params: { file: file, import_type: 'classes' } + expect(response).to redirect_to(admin_imports_path) + expect(flash[:notice]).to eq("Importado com sucesso") + end + + it "handles import classes failure" do + expect(SigaaImporter).to receive(:import_classes).with(json_content).and_return({ success: false, message: "Erro na importação" }) + post :create, params: { file: file, import_type: 'classes' } + expect(response).to redirect_to(admin_imports_path) + expect(flash[:alert]).to eq("Erro na importação") + end + + it "imports members successfully" do + expect(SigaaImporter).to receive(:import_members).with(json_content).and_return({ success: true, message: "Importado com sucesso" }) + post :create, params: { file: file, import_type: 'members' } + expect(response).to redirect_to(admin_imports_path) + expect(flash[:notice]).to eq("Importado com sucesso") + end + end + end +end diff --git a/spec/controllers/password_resets_controller_spec.rb b/spec/controllers/password_resets_controller_spec.rb new file mode 100644 index 0000000000..f01f9f1411 --- /dev/null +++ b/spec/controllers/password_resets_controller_spec.rb @@ -0,0 +1,103 @@ +require 'rails_helper' + +RSpec.describe PasswordResetsController, type: :controller do + let(:user) do + User.create!( + email: 'user@test.com', + matricula: '123', + password: 'password', + password_confirmation: 'password', + role: 'discente' + ) + end + + describe "GET #new" do + it "renders new" do + get :new + expect(response).to have_http_status(:success) + end + end + + describe "POST #create" do + it "generates reset token and sends email if user exists" do + expect_any_instance_of(User).to receive(:generate_reset_token!).and_call_original + mailer_mock = double("mailer") + expect(UserMailer).to receive(:reset_password).with(user).and_return(mailer_mock) + expect(mailer_mock).to receive(:deliver_later) + + post :create, params: { login: user.email } + expect(response).to redirect_to(login_path) + expect(flash[:success]).to be_present + end + + it "redirects without error if user does not exist" do + post :create, params: { login: 'inexistente@test.com' } + expect(response).to redirect_to(login_path) + expect(flash[:success]).to be_present + end + end + + describe "GET #edit" do + before { user.generate_reset_token! } + + it "renders edit if token is valid" do + get :edit, params: { token: user.reset_token } + expect(response).to have_http_status(:success) + end + + it "redirects if token is invalid" do + get :edit, params: { token: 'invalid' } + expect(response).to redirect_to(login_path) + end + + it "redirects if token is expired" do + user.update!(reset_sent_at: 1.year.ago) + get :edit, params: { token: user.reset_token } + expect(response).to redirect_to(login_path) + end + end + + describe "PATCH #update" do + before { user.generate_reset_token! } + + it "updates password successfully" do + patch :update, params: { token: user.reset_token, password: 'new_password123', password_confirmation: 'new_password123' } + expect(response).to redirect_to(home_path) + expect(user.reload.reset_token).to be_nil + end + + it "fails if fields are missing" do + patch :update, params: { token: user.reset_token, password: '', password_confirmation: '' } + expect(response).to have_http_status(:unprocessable_entity) + expect(flash.now[:alert]).to eq("Preencha todos os campos obrigatórios") + end + + it "fails if passwords do not match" do + patch :update, params: { token: user.reset_token, password: 'new_password123', password_confirmation: 'wrong' } + expect(response).to have_http_status(:unprocessable_entity) + expect(flash.now[:alert]).to eq("As senhas não coincidem") + end + + it "fails if password is short" do + patch :update, params: { token: user.reset_token, password: 'short', password_confirmation: 'short' } + expect(response).to have_http_status(:unprocessable_entity) + expect(flash.now[:alert]).to eq("A senha deve ter no mínimo 8 caracteres") + end + + it "fails if model validation fails" do + allow_any_instance_of(User).to receive(:update) do |user, args| + user.errors.add(:base, "Erro interno") + false + end + + patch :update, params: { token: user.reset_token, password: 'new_password123', password_confirmation: 'new_password123' } + expect(response).to have_http_status(:unprocessable_entity) + end + + it "fails if token is expired" do + user.update!(reset_sent_at: 1.year.ago) + patch :update, params: { token: user.reset_token, password: 'new_password123', password_confirmation: 'new_password123' } + expect(response).to redirect_to(login_path) + end + end +end diff --git a/spec/controllers/password_sets_controller_spec.rb b/spec/controllers/password_sets_controller_spec.rb new file mode 100644 index 0000000000..3779fba2c2 --- /dev/null +++ b/spec/controllers/password_sets_controller_spec.rb @@ -0,0 +1,79 @@ +require 'rails_helper' + +RSpec.describe PasswordSetsController, type: :controller do + let(:user) do + User.create!( + email: 'user@test.com', + matricula: '123', + password: 'password', + password_confirmation: 'password', + role: 'discente', + invitation_token: 'valid_token', + invitation_sent_at: Time.current + ) + end + + describe "GET #new" do + it "renders new if token is valid" do + get :new, params: { token: user.invitation_token } + expect(response).to have_http_status(:success) + end + + it "redirects to login if token is invalid" do + get :new, params: { token: 'invalid' } + expect(response).to redirect_to(login_path) + expect(flash[:alert]).to eq("Link inválido.") + end + + it "redirects if token is expired" do + user.update!(invitation_sent_at: 1.year.ago) + get :new, params: { token: user.invitation_token } + expect(response).to redirect_to(login_path) + expect(flash[:alert]).to eq("Este link expirou. Solicite um novo cadastro.") + end + end + + describe "PATCH #update" do + it "updates password successfully" do + patch :update, params: { token: user.invitation_token, password: 'new_password123', password_confirmation: 'new_password123' } + expect(response).to redirect_to(home_path) + expect(flash[:notice]).to eq("Cadastro efetivado com sucesso!") + expect(user.reload.invitation_token).to be_nil + end + + it "fails if token is expired on update" do + user.update!(invitation_sent_at: 1.year.ago) + patch :update, params: { token: user.invitation_token, password: 'new_password123', password_confirmation: 'new_password123' } + expect(response).to redirect_to(login_path) + end + + it "fails if passwords do not match" do + patch :update, params: { token: user.invitation_token, password: 'new_password123', password_confirmation: 'wrong' } + expect(response).to have_http_status(:unprocessable_entity) + expect(flash.now[:alert]).to eq("As senhas não coincidem") + end + + it "fails if password is too short" do + patch :update, params: { token: user.invitation_token, password: 'short', password_confirmation: 'short' } + expect(response).to have_http_status(:unprocessable_entity) + expect(flash.now[:alert]).to eq("A senha deve ter no mínimo 8 caracteres") + end + + it "fails if fields are blank" do + patch :update, params: { token: user.invitation_token, password: '', password_confirmation: '' } + expect(response).to have_http_status(:unprocessable_entity) + expect(flash.now[:alert]).to eq("Preencha todos os campos obrigatórios") + end + + it "fails to save if user model validation fails" do + allow_any_instance_of(User).to receive(:update) do |user, args| + user.errors.add(:base, "Erro no banco de dados") + false + end + + patch :update, params: { token: user.invitation_token, password: 'new_password123', password_confirmation: 'new_password123' } + expect(response).to have_http_status(:unprocessable_entity) + expect(flash.now[:alert]).to eq("Erro no banco de dados") + end + end +end diff --git a/spec/controllers/respostas_controller_spec.rb b/spec/controllers/respostas_controller_spec.rb new file mode 100644 index 0000000000..8b9fb4ea8d --- /dev/null +++ b/spec/controllers/respostas_controller_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +RSpec.describe RespostasController, type: :controller do + let(:admin_user) { User.create!(email: 'admin@test.com', matricula: '123', password: 'password', password_confirmation: 'password', role: 'admin') } + + let(:disciplina) { Disciplina.create!(name: 'Eng Soft', code: 'ES01') } + let(:turma) { Turma.create!(classCode: 'T01', semester: '2024.1', time: '14h', disciplina_id: disciplina.id) } + let(:formulario) { Formulario.create!(titulo: 'Avaliação', descricao: 'Desc', deadline: 1.day.from_now, turma_id: turma.id) } + let(:pergunta_obrigatoria) { Pergunta.create!(enunciado: 'Q1', formulario_id: formulario.id, tipo_pergunta: 'texto', obrigatoria: true) } + let(:pergunta_opcional) { Pergunta.create!(enunciado: 'Q2', formulario_id: formulario.id, tipo_pergunta: 'texto', obrigatoria: false) } + + before do + session[:user_id] = admin_user.id + pergunta_obrigatoria + pergunta_opcional + end + + describe "POST #create" do + it "saves respostas successfully" do + post :create, params: { + formulario_id: formulario.id, + respostas: { pergunta_obrigatoria.id.to_s => "Resposta Q1" } + } + expect(response).to redirect_to(formularios_path) + expect(flash[:notice]).to eq("Avaliação submetida com sucesso!") + expect(Resposta.count).to eq(1) + end + + it "fails if deadline is past" do + formulario.update!(deadline: 1.day.ago) + post :create, params: { + formulario_id: formulario.id, + respostas: { pergunta_obrigatoria.id.to_s => "Resposta Q1" } + } + expect(response).to redirect_to(formularios_path) + expect(flash[:alert]).to eq("Avaliação encerrada") + end + + it "fails if already answered" do + Resposta.create!(formulario: formulario, user: admin_user, pergunta: pergunta_obrigatoria, conteudo: "Antiga") + post :create, params: { + formulario_id: formulario.id, + respostas: { pergunta_obrigatoria.id.to_s => "Resposta Q1" } + } + expect(response).to redirect_to(formularios_path) + expect(flash[:alert]).to eq("Você já respondeu esta avaliação") + end + + it "fails if missing mandatory answers" do + post :create, params: { + formulario_id: formulario.id, + respostas: { pergunta_obrigatoria.id.to_s => "" } + } + expect(response).to have_http_status(:unprocessable_entity) + expect(flash[:alert]).to eq("Por favor, responda todas as questões obrigatórias") + end + + it "handles unexpected errors" do + allow(Resposta).to receive(:transaction).and_raise(StandardError.new("Database error")) + post :create, params: { + formulario_id: formulario.id, + respostas: { pergunta_obrigatoria.id.to_s => "Resposta Q1" } + } + expect(response).to have_http_status(:unprocessable_entity) + expect(flash[:alert]).to eq("Erro ao enviar avaliação: Database error") + end + end +end From d36de0c43180fc96982f63d7191c09ea303519d9 Mon Sep 17 00:00:00 2001 From: MatheusNevs Date: Sun, 21 Jun 2026 10:11:08 -0300 Subject: [PATCH 67/87] =?UTF-8?q?Corrige=20typo=20na=20refatora=C3=A7?= =?UTF-8?q?=C3=A3o:=20substitui=20'se'=20por=20'if'=20em=20imports=5Fcontr?= =?UTF-8?q?oller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/admin/imports_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/imports_controller.rb b/app/controllers/admin/imports_controller.rb index f827c13f00..a284f0f194 100644 --- a/app/controllers/admin/imports_controller.rb +++ b/app/controllers/admin/imports_controller.rb @@ -16,7 +16,7 @@ def index # Retorno: Nenhum # Efeitos Colaterais: Cria usuários/turmas no banco de dados e redireciona a página. def create - return se_arquivo_invalido se arquivo_invalido? + return se_arquivo_invalido if arquivo_invalido? result = processar_importacao(params[:file].read, params[:import_type]) From 09fcf7f8c3035e71000a05008e34ab11559b279c Mon Sep 17 00:00:00 2001 From: MatheusNevs Date: Sun, 21 Jun 2026 11:31:08 -0300 Subject: [PATCH 68/87] Refatora SigaaImporter para reduzir complexidade (Issue #1) --- app/services/sigaa_importer.rb | 223 +++++++++++++++++++++++---------- 1 file changed, 155 insertions(+), 68 deletions(-) diff --git a/app/services/sigaa_importer.rb b/app/services/sigaa_importer.rb index 9dbcf7ee8e..6a3af6bb57 100644 --- a/app/services/sigaa_importer.rb +++ b/app/services/sigaa_importer.rb @@ -1,91 +1,178 @@ require 'json' +# Serviço para importar dados do SIGAA. class SigaaImporter - def self.import_classes(json_content) - begin + class << self + # Importa turmas do JSON para o banco de dados. + # + # Argumentos: json_content (String) + # Retorno: Hash com status e mensagem + # Efeitos Colaterais: Cria disciplinas e turmas no banco. + def import_classes(json_content) + begin + data = parse_json_data(json_content) + process_classes_data(data) + { success: true, message: "Dados de turmas importados com sucesso: #{data.size} novas turmas processadas" } + rescue JSON::ParserError + { success: false, message: "Erro na estrutura do arquivo: campos obrigatórios ausentes ou JSON inválido" } + rescue => e + { success: false, message: "Erro na estrutura do arquivo: #{e.message}" } + end + end + + # Importa participantes (docentes e discentes) do JSON. + # + # Argumentos: json_content (String) + # Retorno: Hash com status e mensagem + # Efeitos Colaterais: Cria usuários, docentes e discentes no banco. + def import_members(json_content) + begin + data = parse_json_data(json_content) + process_members_data(data) + { success: true, message: "Participantes importados com sucesso" } + rescue JSON::ParserError + { success: false, message: "Erro na estrutura do arquivo: campos obrigatórios ausentes ou JSON inválido" } + rescue => e + { success: false, message: "Erro na estrutura do arquivo: #{e.message}" } + end + end + + private + + # Transforma string em array de hash. + # + # Argumentos: json_content (String) + # Retorno: Array de Hashes + # Efeitos Colaterais: Nenhum + def parse_json_data(json_content) data = JSON.parse(json_content) - data = [data] unless data.is_a?(Array) + data.is_a?(Array) ? data : [data] + end + # Itera sobre os dados para importar as classes. + # + # Argumentos: data (Array) + # Retorno: Nenhum + # Efeitos Colaterais: Chama persistencia no BD. + def process_classes_data(data) data.each do |item| next unless item.is_a?(Hash) - - # Validate structure - raise "campos obrigatórios ausentes" unless item['code'] && item['class'] && item['class']['classCode'] + validate_class_structure!(item) + import_single_class(item) + end + end - disciplina = Disciplina.find_or_create_by!(code: item['code']) do |d| - d.name = item['name'] - end + # Valida estrutura da classe. + # + # Argumentos: item (Hash) + # Retorno: Nenhum + # Efeitos Colaterais: Lança exceção se faltarem campos. + def validate_class_structure!(item) + raise "campos obrigatórios ausentes" unless item['code'] && item['class'] && item['class']['classCode'] + end - Turma.find_or_create_by!( - classCode: item['class']['classCode'], - semester: item['class']['semester'], - disciplina: disciplina - ) do |t| - t.time = item['class']['time'] - end + # Importa uma disciplina e turma individualmente. + # + # Argumentos: item (Hash) + # Retorno: Nenhum + # Efeitos Colaterais: Cria no BD. + def import_single_class(item) + disciplina = Disciplina.find_or_create_by!(code: item['code']) do |d| + d.name = item['name'] + end + + Turma.find_or_create_by!( + classCode: item['class']['classCode'], + semester: item['class']['semester'], + disciplina: disciplina + ) do |t| + t.time = item['class']['time'] end - { success: true, message: "Dados de turmas importados com sucesso: #{data.size} novas turmas processadas" } - rescue JSON::ParserError - { success: false, message: "Erro na estrutura do arquivo: campos obrigatórios ausentes ou JSON inválido" } - rescue => e - { success: false, message: "Erro na estrutura do arquivo: #{e.message}" } end - end - def self.import_members(json_content) - begin - data = JSON.parse(json_content) - data = [data] unless data.is_a?(Array) - + # Processa dados dos membros. + # + # Argumentos: data (Array) + # Retorno: Nenhum + # Efeitos Colaterais: Busca e persiste. + def process_members_data(data) data.each do |item| next unless item.is_a?(Hash) + turma = find_turma(item) + next unless turma - disciplina = Disciplina.find_by(code: item['code']) - next unless disciplina + import_discentes(item['dicente'] || [], turma) + import_docente(item['docente'], turma) if item['docente'] + end + end - turma = Turma.find_by(classCode: item['classCode'], semester: item['semester'], disciplina: disciplina) - next unless turma + # Localiza turma pelo hash. + # + # Argumentos: item (Hash) + # Retorno: Turma ou nil + # Efeitos Colaterais: Busca no BD. + def find_turma(item) + disciplina = Disciplina.find_by(code: item['code']) + return nil unless disciplina + Turma.find_by(classCode: item['classCode'], semester: item['semester'], disciplina: disciplina) + end + + # Importa discentes. + # + # Argumentos: dicentes (Array), turma (Turma) + # Retorno: Nenhum + # Efeitos Colaterais: Persiste discentes. + def import_discentes(dicentes, turma) + dicentes.each do |d_data| + create_discente_record(d_data, turma) + create_or_invite_user(d_data['matricula'], d_data['email'], 'discente') + end + end - # Import Discentes - (item['dicente'] || []).each do |d_data| - Discente.find_or_create_by!(matricula: d_data['matricula'], turma: turma) do |d| - d.nome = d_data['nome'] - d.curso = d_data['curso'] - d.usuario = d_data['usuario'] - d.formacao = d_data['formacao'] - d.ocupacao = d_data['ocupacao'] - d.email = d_data['email'] - end - - User.find_or_create_by!(matricula: d_data['matricula']) do |u| - u.email = d_data['email'] - u.role = 'discente' - u.generate_invitation_token! - end - end + # Cria ou busca o discente + # + # Argumentos: d_data (Hash), turma (Turma) + # Retorno: Nenhum + # Efeitos Colaterais: Cria discente no banco + def create_discente_record(d_data, turma) + Discente.find_or_create_by!(matricula: d_data['matricula'], turma: turma) do |d| + d.nome = d_data['nome'] + d.curso = d_data['curso'] + d.usuario = d_data['usuario'] + d.formacao = d_data['formacao'] + d.ocupacao = d_data['ocupacao'] + d.email = d_data['email'] + end + end - # Import Docente - if doc_data = item['docente'] - Docente.find_or_create_by!(usuario: doc_data['usuario'], turma: turma) do |doc| - doc.nome = doc_data['nome'] - doc.departamento = doc_data['departamento'] - doc.formacao = doc_data['formacao'] - doc.email = doc_data['email'] - doc.ocupacao = doc_data['ocupacao'] - end + # Importa docente. + # + # Argumentos: doc_data (Hash), turma (Turma) + # Retorno: Nenhum + # Efeitos Colaterais: Persiste docentes. + def import_docente(doc_data, turma) + Docente.find_or_create_by!(usuario: doc_data['usuario'], turma: turma) do |doc| + doc.nome = doc_data['nome'] + doc.departamento = doc_data['departamento'] + doc.formacao = doc_data['formacao'] + doc.email = doc_data['email'] + doc.ocupacao = doc_data['ocupacao'] + end + + create_or_invite_user(doc_data['usuario'], doc_data['email'], 'docente') + end - User.find_or_create_by!(matricula: doc_data['usuario']) do |u| - u.email = doc_data['email'] - u.role = 'docente' - u.generate_invitation_token! - end - end + # Cria usuário e gera convite caso não exista. + # + # Argumentos: matricula, email, role + # Retorno: Nenhum + # Efeitos Colaterais: Cria usuário + def create_or_invite_user(matricula, email, role) + User.find_or_create_by!(matricula: matricula) do |u| + u.email = email + u.role = role + u.generate_invitation_token! end - { success: true, message: "Participantes importados com sucesso" } - rescue JSON::ParserError - { success: false, message: "Erro na estrutura do arquivo: campos obrigatórios ausentes ou JSON inválido" } - rescue => e - { success: false, message: "Erro na estrutura do arquivo: #{e.message}" } end end end From 89a6f161e7269314d47487671a87d91aff3321ab Mon Sep 17 00:00:00 2001 From: MatheusNevs Date: Sun, 21 Jun 2026 11:37:16 -0300 Subject: [PATCH 69/87] Refatora SigaaSyncService para reduzir complexidade e fixa testes falhos (Issue #1) --- app/services/sigaa_sync_service.rb | 86 ++++++++++++++++------ spec/services/sigaa_update_service_spec.rb | 4 +- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/app/services/sigaa_sync_service.rb b/app/services/sigaa_sync_service.rb index 554c0969af..505a1cbbec 100644 --- a/app/services/sigaa_sync_service.rb +++ b/app/services/sigaa_sync_service.rb @@ -1,52 +1,92 @@ +# Serviço para sincronizar dados do SIGAA class SigaaSyncService + # Inicializa o servico + # + # Argumentos: user (User) + # Retorno: Nenhum + # Efeitos Colaterais: Salva user no estado def initialize(user) @user = user end + # Inicia o processo de importacao + # + # Argumentos: Nenhum + # Retorno: Hash com status e message + # Efeitos Colaterais: Cria locks, importa turmas e membros, registra logs. def call return failure("Acesso negado") unless @user.admin? return failure("Já existe uma atualização em andamento") if SyncLock.active? SyncLock.start! + execute_sync + ensure + SyncLock.release! + end + + private + # Executa o sincronismo real + # + # Argumentos: Nenhum + # Retorno: Hash com sucesso ou falha + # Efeitos Colaterais: Importa turmas ou captura exceções + def execute_sync begin json = SigaaApi.fetch_data - return failure("SIGAA não retornou dados") if json.nil? || json.strip == "[]" - classes = SigaaImporter.import_classes(json) - members = SigaaImporter.import_members(json) - - return failure("SIGAA retornou dados inconsistentes") unless classes[:success] && members[:success] - - SigaaLog.create!( - user: @user, - status: "success", - message: "Atualização SIGAA concluída" - ) - - success("Atualização concluída com sucesso") - + import_data_from_json(json) rescue => e - SigaaLog.create!( - user: @user, - status: "error", - message: e.message - ) - + log_sync_event("error", e.message) failure(e.message) + end + end - ensure - SyncLock.release! + # Coordena as chamadas para o importador + # + # Argumentos: json (String) + # Retorno: Hash + # Efeitos Colaterais: Persistencia no DB + def import_data_from_json(json) + classes = SigaaImporter.import_classes(json) + members = SigaaImporter.import_members(json) + + if classes[:success] && members[:success] + log_sync_event("success", "Atualização SIGAA concluída") + success("Atualização concluída com sucesso") + else + failure("SIGAA retornou dados inconsistentes") end end - private + # Salva log da operacao + # + # Argumentos: status, message + # Retorno: Nenhum + # Efeitos Colaterais: Salva SigaaLog no DB + def log_sync_event(status, message) + SigaaLog.create!( + user: @user, + status: status, + message: message + ) + end + # Auxiliar de sucesso + # + # Argumentos: msg + # Retorno: Hash + # Efeitos Colaterais: Nenhum def success(msg) { success: true, message: msg } end + # Auxiliar de falha + # + # Argumentos: msg + # Retorno: Hash + # Efeitos Colaterais: Nenhum def failure(msg) { success: false, message: msg } end diff --git a/spec/services/sigaa_update_service_spec.rb b/spec/services/sigaa_update_service_spec.rb index cea3820228..b2406d3a32 100644 --- a/spec/services/sigaa_update_service_spec.rb +++ b/spec/services/sigaa_update_service_spec.rb @@ -1,11 +1,11 @@ require "rails_helper" RSpec.describe SigaaSyncService do - let(:user) { double("User", admin?: true) } + let(:user) { User.create!(email: 'test@unb.br', matricula: '9999', role: 'admin', password: 'password123', password_confirmation: 'password123') } before do allow(SigaaApi).to receive(:fetch_data) - .and_return(File.read(Rails.root.join("spec/fixtures/sigaa.json"))) + .and_return('[{"id":1}]') allow(SigaaImporter).to receive(:import_classes) .and_return(success: true) From b78531a06a56b253496a05ed21ce80801d52430d Mon Sep 17 00:00:00 2001 From: MatheusNevs Date: Sun, 21 Jun 2026 11:39:35 -0300 Subject: [PATCH 70/87] =?UTF-8?q?Adiciona=20cabe=C3=A7alhos=20RDoc=20aos?= =?UTF-8?q?=20controllers=20Sessions=20e=20Sigaa=20(Issues=201=20e=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/sessions_controller.rb | 16 ++++++++++++++++ app/controllers/sigaa_controller.rb | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index aef073fdff..9bbb164db1 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,10 +1,21 @@ +# Controller responsável por gerenciar a sessão dos usuários (login e logout) class SessionsController < ApplicationController skip_before_action :require_login, only: [:new, :create] + # Exibe a página de login + # + # Argumentos: Nenhum + # Retorno: Nenhum + # Efeitos Colaterais: Renderiza a view de login def new # renderizar a página de login end + # Autentica o usuário e cria a sessão + # + # Argumentos: params[:login], params[:password] + # Retorno: Nenhum + # Efeitos Colaterais: Cria sessão, redireciona ou renderiza erro de credenciais. def create user = User.find_by_login(params[:login]) @@ -19,6 +30,11 @@ def create end end + # Destrói a sessão do usuário (Logout) + # + # Argumentos: Nenhum + # Retorno: Nenhum + # Efeitos Colaterais: Limpa sessão e redireciona para a tela de login. def destroy session[:user_id] = nil redirect_to login_path, flash: { logout: "Logout realizado com sucesso!" } diff --git a/app/controllers/sigaa_controller.rb b/app/controllers/sigaa_controller.rb index 9a93a7c1d3..d979b839bd 100644 --- a/app/controllers/sigaa_controller.rb +++ b/app/controllers/sigaa_controller.rb @@ -1,6 +1,12 @@ +# Controller responsável pela sincronização de dados via API do SIGAA class SigaaController < ApplicationController before_action :authenticate_user! + # Inicia o processo de atualização do banco de dados chamando o serviço de sincronização + # + # Argumentos: Nenhum + # Retorno: Nenhum + # Efeitos Colaterais: Dispara o job de importação e redireciona com flash message. def update_database resultado = SigaaSyncService.new(current_user).call From 7e08d4817d28a8ec291fe33daacf73e8e4a730d3 Mon Sep 17 00:00:00 2001 From: MatheusNevs Date: Sun, 21 Jun 2026 11:41:09 -0300 Subject: [PATCH 71/87] =?UTF-8?q?Adiciona=20coment=C3=A1rios=20em=20n?= =?UTF-8?q?=C3=ADvel=20de=20classe=20(RDoc)=20para=20remover=20alertas=20d?= =?UTF-8?q?o=20Reek?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/admin/imports_controller.rb | 1 + app/controllers/password_resets_controller.rb | 1 + app/controllers/password_sets_controller.rb | 1 + app/controllers/respostas_controller.rb | 1 + app/models/user.rb | 66 +++++++++++++++++++ app/services/sigaa_api.rb | 11 ++++ 6 files changed, 81 insertions(+) diff --git a/app/controllers/admin/imports_controller.rb b/app/controllers/admin/imports_controller.rb index a284f0f194..3f02023c34 100644 --- a/app/controllers/admin/imports_controller.rb +++ b/app/controllers/admin/imports_controller.rb @@ -1,3 +1,4 @@ +# Controller responsável por gerenciar a importação de turmas e discentes do SIGAA. class Admin::ImportsController < ApplicationController layout 'dashboard' before_action :require_admin diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index a4d19379ce..55fe1625ec 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -1,3 +1,4 @@ +# Controller responsável por gerenciar a redefinição de senhas esquecidas. class PasswordResetsController < ApplicationController skip_before_action :require_login diff --git a/app/controllers/password_sets_controller.rb b/app/controllers/password_sets_controller.rb index 7004d55a19..85c4252842 100644 --- a/app/controllers/password_sets_controller.rb +++ b/app/controllers/password_sets_controller.rb @@ -1,3 +1,4 @@ +# Controller responsável pelo primeiro cadastro de senha via convite. class PasswordSetsController < ApplicationController skip_before_action :require_login diff --git a/app/controllers/respostas_controller.rb b/app/controllers/respostas_controller.rb index f6a560869b..5639c77afa 100644 --- a/app/controllers/respostas_controller.rb +++ b/app/controllers/respostas_controller.rb @@ -1,3 +1,4 @@ +# Controller responsável por receber e processar as respostas dos discentes às avaliações. class RespostasController < ApplicationController layout "dashboard" diff --git a/app/models/user.rb b/app/models/user.rb index b660b32618..d1b6d6808a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,4 @@ +# Modelo de Usuário do sistema class User < ApplicationRecord has_secure_password validations: false @@ -9,22 +10,47 @@ class User < ApplicationRecord validates :password, length: { minimum: 8 }, if: :password_being_set? validates :password, confirmation: true, if: :password_being_set? + # Busca um usuário pelo email ou matricula + # + # Argumentos: login (String) + # Retorno: User ou nil + # Efeitos Colaterais: Busca no BD def self.find_by_login(login) find_by(email: login) || find_by(matricula: login) end + # Verifica se é admin + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Nenhum def admin? role == "admin" end + # Verifica se é discente + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Nenhum def discente? role == "discente" end + # Verifica se é docente + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Nenhum def docente? role == "docente" end + # Retorna o perfil vinculado (Discente ou Docente) + # + # Argumentos: Nenhum + # Retorno: Collection + # Efeitos Colaterais: Consulta no BD def profile @profile ||= if discente? Discente.where(matricula: matricula) @@ -33,6 +59,11 @@ def profile end end + # Retorna turmas vinculadas ao usuário + # + # Argumentos: Nenhum + # Retorno: Collection + # Efeitos Colaterais: Consulta no BD def turmas if discente? Turma.where(id: Discente.where(matricula: matricula).select(:turma_id)) @@ -43,36 +74,71 @@ def turmas end end + # Gera token de convite e salva + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Modifica BD def generate_invitation_token! self.invitation_token = SecureRandom.urlsafe_base64(32) self.invitation_sent_at = Time.current save!(validate: false) end + # Checa se o convite expirou (24h) + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Nenhum def invitation_token_expired? invitation_sent_at.nil? || invitation_sent_at < 24.hours.ago end + # Consome token de convite + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Atualiza colunas no BD def consume_invitation_token! update_columns(invitation_token: nil, invitation_sent_at: nil) end + # Gera token de redefinição de senha + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Modifica BD def generate_reset_token! self.reset_token = SecureRandom.urlsafe_base64(32) self.reset_sent_at = Time.current save!(validate: false) end + # Checa se o token de senha expirou (24h) + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Nenhum def reset_token_expired? reset_sent_at.nil? || reset_sent_at < 24.hours.ago end + # Consome token de redefinição + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Atualiza colunas no BD def consume_reset_token! update_columns(reset_token: nil, reset_sent_at: nil) end private + # Verifica se a senha está sendo cadastrada + # + # Argumentos: Nenhum + # Retorno: Booleano + # Efeitos Colaterais: Nenhum def password_being_set? password.present? || password_confirmation.present? end diff --git a/app/services/sigaa_api.rb b/app/services/sigaa_api.rb index affa9ecb58..18cae44a15 100644 --- a/app/services/sigaa_api.rb +++ b/app/services/sigaa_api.rb @@ -1,4 +1,10 @@ +# API Simulada (Mock) para comunicação com o SIGAA. class SigaaApi + # Busca os dados baseados no cenário de teste configurado. + # + # Argumentos: Nenhum + # Retorno: JSON String + # Efeitos Colaterais: Lê arquivo local ou lança exceção. def self.fetch_data case scenario when :success @@ -18,6 +24,11 @@ def self.fetch_data end end + # Recupera o cenário atual do ambiente. + # + # Argumentos: Nenhum + # Retorno: Symbol + # Efeitos Colaterais: Acessa ENV def self.scenario ENV["SIGAA_SCENARIO"]&.to_sym || :success end From 58d6d0cb11152d59db26b5b36779e22525e71026 Mon Sep 17 00:00:00 2001 From: Vitor Alencar Date: Thu, 25 Jun 2026 23:12:12 -0300 Subject: [PATCH 72/87] Concertei os meus erros da Branch 2 --- .../criacao_form_para_dic_doc_steps.rb | 17 +- .../step_definitions/visu_form_resp_steps.rb | 472 +++++++++--------- .../step_definitions/visu_resul_form_steps.rb | 41 +- 3 files changed, 269 insertions(+), 261 deletions(-) diff --git a/features/step_definitions/criacao_form_para_dic_doc_steps.rb b/features/step_definitions/criacao_form_para_dic_doc_steps.rb index 42ddb88552..b2996d8752 100644 --- a/features/step_definitions/criacao_form_para_dic_doc_steps.rb +++ b/features/step_definitions/criacao_form_para_dic_doc_steps.rb @@ -2,9 +2,6 @@ # ==================== CONTEXTO ==================== -# Removed duplicate navigation step - - Então('um modal é exibido com os campos {string}, {string}, {string} e data de disponibilidade') do |campo1, campo2, campo3| expect(page).to have_selector('.modal') expect(page).to have_field(campo1) @@ -44,6 +41,10 @@ fill_in 'data_termino', with: '01/06/2026' end +Quando('deixo o campo {string} em branco') do |campo| + fill_in campo, with: '' +end + Quando('não seleciono nenhum template no dropdown {string}') do |campo| # Campo já começa sem seleção expect(find("select[name='#{campo.downcase.gsub(/\s+/, '_')}']").value).to be_nil @@ -60,6 +61,10 @@ expect(page).not_to have_selector('.modal') end +Então('vejo a mensagem de sucesso {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + Então('o formulário fica disponível para os dicentes da turma {string}') do |turma| # Verifica se o formulário foi criado e associado à turma expect(Form.last.destiny_type).to eq('discente') @@ -189,6 +194,10 @@ expect(page).to have_selector('.modal') end +Então('vejo a mensagem de erro {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + Então('a data de término anterior à data de início') do fill_in 'data_termino', with: '20/05/2026' -end +end \ No newline at end of file diff --git a/features/step_definitions/visu_form_resp_steps.rb b/features/step_definitions/visu_form_resp_steps.rb index ff5d484502..5d6bbc57a7 100644 --- a/features/step_definitions/visu_form_resp_steps.rb +++ b/features/step_definitions/visu_form_resp_steps.rb @@ -2,19 +2,20 @@ # ==================== CONTEXTO ==================== -Dado('que estou logado como Participante') do - @user = User.create!( - email: 'participante@gmail.com', - matricula: '190084010', - password: 'senhaParticipante', - password_confirmation: 'senhaParticipante', - role: 'discente' - ) - - visit login_path - fill_in 'email', with: 'participante@gmail.com' - fill_in 'password', with: 'senhaParticipante' - click_button 'Entrar' +def assign_user_to_turma(user, turma) + if user.discente? + Discente.find_or_create_by!(matricula: user.matricula, turma: turma) do |discente| + discente.nome = user.nome || user.email + discente.email = user.email + end + elsif user.docente? + Docente.find_or_create_by!(usuario: user.matricula, turma: turma) do |docente| + docente.nome = user.nome || user.email + docente.email = user.email + end + else + raise "Usuário #{user.email} não pode ser associado à turma #{turma.classCode}" + end end Então('vejo uma seção com os formulários não respondidos das minhas turmas') do @@ -33,42 +34,52 @@ end Então('vejo uma lista com cards de formulários não respondidos') do - expect(page).to have_selector('.form-card') + expect(page).to have_selector('.card') +end + +Então('o formulário é removido da lista') do + expect(page).not_to have_selector("#formulario_#{@form.id}") end Então('cada card exibe {string}, {string}, {string} e um botão {string}') do |info1, info2, info3, botao| - card = find('.form-card') + card = find('.card') expect(card).to have_content(info1) # Turma expect(card).to have_content(info2) # Template expect(card).to have_content(info3) # Data de Término - expect(card).to have_button(botao) + expect(card.has_link?(botao) || card.has_button?(botao)).to be(true) end Então('os formulários são agrupados por turma') do - expect(page).to have_selector('.turma-group') + expect(page).to have_content('Turma:') end Dado('que existem formulários não respondidos') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação de Engenharia de Software', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form = Formulario.create!( + titulo: 'Avaliação de Engenharia de Software', + deadline: 1.day.from_now ) - @form.classes << @class + @form.update!(turma: @class) end Quando('clico no card de um formulário') do - click_link find('.form-card').text + within(first('.card')) do + click_link 'Responder' + end +end + +Quando('clico no card do formulário') do + within(first('.card')) do + click_link 'Responder' + end end Então('vejo os detalhes completos como {string}, {string}, {string}, {string}') do |info1, info2, info3, info4| @@ -87,34 +98,31 @@ end Dado('que estou matriculado em múltiplas turmas com formulários') do - @class1 = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class1 = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @class2 = Class.create!( - code: 'CIC0202', - name: 'Banco de Dados', + @class2 = Turma.create!( + classCode: 'CIC0202', + nome: 'Banco de Dados', semester: '2026.1' ) - @user.classes << [@class1, @class2] + assign_user_to_turma(@user, @class1) + assign_user_to_turma(@user, @class2) - @form1 = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação 1', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form1 = Formulario.create!( + titulo: 'Avaliação 1', + deadline: 1.day.from_now ) - @form1.classes << @class1 + @form1.update!(turma: @class1) - @form2 = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação 2', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form2 = Formulario.create!( + titulo: 'Avaliação 2', + deadline: 1.day.from_now ) - @form2.classes << @class2 + @form2.update!(turma: @class2) end Quando('seleciono a turma {string} no filtro') do |turma| @@ -126,7 +134,7 @@ end Então('os formulários de outras turmas deixam de aparecer') do - expect(page).not_to have_selector('.form-card', count: 2) + expect(page).not_to have_selector('.card', count: 2) end Dado('que sou um Dicente com formulários para dicentes e também Docente com formulários para docentes') do @@ -146,30 +154,26 @@ role: 'docente' ) - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user_dicente.classes << @class - @user_docente.classes << @class + assign_user_to_turma(@user_dicente, @class) + assign_user_to_turma(@user_docente, @class) - @form_dicente = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação para Dicentes', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form_dicente = Formulario.create!( + titulo: 'Avaliação para Dicentes', + deadline: 1.day.from_now ) - @form_dicente.classes << @class + @form_dicente.update!(turma: @class) - @form_docente = Form.create!( - destiny_type: 'docente', - template_name: 'Avaliação para Docentes', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form_docente = Formulario.create!( + titulo: 'Avaliação para Docentes', + deadline: 1.day.from_now ) - @form_docente.classes << @class + @form_docente.update!(turma: @class) end Quando('seleciono {string} no filtro {string}') do |opcao, filtro| @@ -185,29 +189,23 @@ end Dado('que existem múltiplos formulários com datas diferentes') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form1 = Form.create!( - destiny_type: 'discente', - template_name: 'Formulário 1', - start_date: 5.days.ago, - end_date: 5.days.from_now + @form1 = Formulario.create!( + titulo: 'Formulário 1', + deadline: 5.days.from_now ) - @form1.classes << @class - @form2 = Form.create!( - destiny_type: 'discente', - template_name: 'Formulário 2', - start_date: 2.days.ago, - end_date: 2.days.from_now + @form2 = Formulario.create!( + titulo: 'Formulário 2', + deadline: 2.days.from_now ) - @form2.classes << @class end Quando('clico no filtro {string}') do |filtro| @@ -218,28 +216,26 @@ click_link opcao end -Então('os formulários aparecem ordenados por data de término (mais próximos primeiro)') do - cards = all('.form-card') +Então(/os formulários aparecem ordenados por data de término/) do + cards = all('.card') expect(cards.first).to have_content('2 dias') # Mais próximo end Dado('que existe um formulário com deadline em 2 dias') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação', - start_date: 1.day.ago, - end_date: 2.days.from_now + @form = Formulario.create!( + titulo: 'Avaliação', + deadline: 2.days.from_now ) - @form.classes << @class + @form.update!(turma: @class) end Quando('visualizo o card do formulário') do @@ -251,22 +247,20 @@ end Dado('que existe um formulário vencendo em 1 hora') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação Urgente', - start_date: 1.day.ago, - end_date: 1.hour.from_now + @form = Formulario.create!( + titulo: 'Avaliação Urgente', + deadline: 1.hour.from_now ) - @form.classes << @class + @form.update!(turma: @class) end Então('vejo a mensagem de alerta {string}') do |mensagem| @@ -278,22 +272,20 @@ end Dado('que estou visualizando um formulário não respondido') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form = Formulario.create!( + titulo: 'Avaliação', + deadline: 1.day.from_now ) - @form.classes << @class + @form.update!(turma: @class) visit avaliacoes_path click_button 'Responder' @@ -310,22 +302,20 @@ end Dado('que estou preenchendo um formulário') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form = Formulario.create!( + titulo: 'Avaliação', + deadline: 1.day.from_now ) - @form.classes << @class + @form.update!(turma: @class) visit responder_form_path(@form) end @@ -343,25 +333,23 @@ end Dado('que salvei um formulário como rascunho anteriormente') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form = Formulario.create!( + titulo: 'Avaliação', + deadline: 1.day.from_now ) - @form.classes << @class + @form.update!(turma: @class) - @draft = FormResponse.create!( - form: @form, + @draft = Resposta.create!( + formulario: @form, user: @user, status: 'draft', answers: { 'q1' => 'Resposta anterior' } @@ -416,26 +404,24 @@ # ==================== CENÁRIOS TRISTES ==================== Dado('que respondeu todos os formulários das suas turmas') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form = Formulario.create!( + titulo: 'Avaliação', + deadline: 1.day.from_now ) - @form.classes << @class + @form.update!(turma: @class) # Marca como respondido - FormResponse.create!( - form: @form, + Resposta.create!( + formulario: @form, user: @user, status: 'submitted' ) @@ -451,20 +437,18 @@ end Quando('abro o link de um formulário da turma {string} em que não estou matriculado') do |turma| - @other_class = Class.create!( - code: turma, - name: 'Outra Turma', + @other_class = Turma.create!( + classCode: turma, + nome: 'Outra Turma', semester: '2026.1' ) - @other_form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação', - start_date: 1.day.ago, - end_date: 1.day.from_now + @other_form = Formulario.create!( + titulo: 'Avaliação', + deadline: 1.day.from_now ) - @other_form.classes << @other_class + @other_form.update!(turma: @other_class) visit responder_form_path(@other_form) end @@ -477,22 +461,20 @@ end Dado('que existe um formulário com deadline já vencido há 1 dia') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @expired_form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação Expirada', - start_date: 3.days.ago, - end_date: 1.day.ago + @expired_form = Formulario.create!( + titulo: 'Avaliação Expirada', + deadline: 1.day.ago ) - @expired_form.classes << @class + @expired_form.update!(turma: @class) end Então('o formulário expirado não aparece na lista') do @@ -504,22 +486,20 @@ end Dado('que tentei abrir um formulário após seu deadline') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @expired_form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação Expirada', - start_date: 3.days.ago, - end_date: 1.day.ago + @expired_form = Formulario.create!( + titulo: 'Avaliação Expirada', + deadline: 1.day.ago ) - @expired_form.classes << @class + @expired_form.update!(turma: @class) end Então('não consigo acessar o formulário') do @@ -541,22 +521,20 @@ end Dado('que cliquei em um formulário para responder') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form = Formulario.create!( + titulo: 'Avaliação', + deadline: 1.day.from_now ) - @form.classes << @class + @form.update!(turma: @class) end Quando('a conexão com o servidor é perdida') do @@ -569,22 +547,20 @@ end Dado('que estava visualizando os detalhes de um formulário') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form = Formulario.create!( + titulo: 'Avaliação', + deadline: 1.day.from_now ) - @form.classes << @class + @form.update!(turma: @class) visit avaliacoes_path click_link @form.template_name end @@ -598,22 +574,20 @@ end Dado('que estava preenchendo um formulário') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form = Formulario.create!( + titulo: 'Avaliação', + deadline: 1.day.from_now ) - @form.classes << @class + @form.update!(turma: @class) visit responder_form_path(@form) fill_in 'Questão 1', with: 'Minha resposta' end @@ -632,28 +606,26 @@ end Dado('que estou preenchendo um formulário') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form = Formulario.create!( + titulo: 'Avaliação', + deadline: 1.day.from_now ) - @form.classes << @class + @form.update!(turma: @class) visit responder_form_path(@form) end Quando('clico em {string} e o servidor retorna erro') do |acao| # Simula erro no servidor ao salvar - allow(FormResponse).to receive(:create!).and_raise(StandardError) + allow(Resposta).to receive(:create!).and_raise(StandardError) click_button acao end @@ -666,39 +638,43 @@ end Dado('que salvei 10 formulários como rascunho') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) 10.times do |i| - form = Form.create!( - destiny_type: 'discente', - template_name: "Avaliação #{i}", - start_date: 1.day.ago, - end_date: 1.day.from_now + form = Formulario.create!( + titulo: "Avaliação #{i}", + deadline: 1.day.from_now, + turma: @class ) - form.classes << @class - - FormResponse.create!( - form: form, + + pergunta = Pergunta.create!( + formulario: form, + enunciado: "Questão #{i + 1}", + tipo_pergunta: 'texto', + obrigatoria: false + ) + + Resposta.create!( + formulario: form, user: @user, - status: 'draft' + pergunta: pergunta, + conteudo: 'Rascunho' ) end end Quando('tento salvar outro formulário como rascunho') do - @new_form = Form.create!( - destiny_type: 'discente', - template_name: 'Avaliação Extra', - start_date: 1.day.ago, - end_date: 1.day.from_now + @new_form = Formulario.create!( + titulo: 'Avaliação Extra', + deadline: 1.day.from_now ) - @new_form.classes << @class + @new_form.update!(turma: @class) visit responder_form_path(@new_form) click_button 'Salvar como Rascunho' @@ -709,7 +685,7 @@ end Então('o formulário não é salvo') do - expect(FormResponse.where(form: @new_form).count).to eq(0) + expect(Resposta.where(formulario: @new_form).count).to eq(0) end Dado('que sou um Dicente') do @@ -728,22 +704,20 @@ end Quando('um formulário é enviado especificamente para Docentes') do - @class = Class.create!( - code: 'CIC0105', - name: 'Engenharia de Software', + @class = Turma.create!( + classCode: 'CIC0105', + nome: 'Engenharia de Software', semester: '2026.1' ) - @user.classes << @class + assign_user_to_turma(@user, @class) - @form_docente = Form.create!( - destiny_type: 'docente', - template_name: 'Avaliação para Docentes', - start_date: 1.day.ago, - end_date: 1.day.from_now + @form_docente = Formulario.create!( + titulo: 'Avaliação para Docentes', + deadline: 1.day.from_now ) - @form_docente.classes << @class + @form_docente.update!(turma: @class) end Então('esse formulário não aparece na minha lista de não respondidos') do @@ -754,4 +728,4 @@ Então('não consigo acessá-lo mesmo tendo o link direto') do visit responder_form_path(@form_docente) expect(page).to have_content('Acesso não autorizado') -end +end \ No newline at end of file diff --git a/features/step_definitions/visu_resul_form_steps.rb b/features/step_definitions/visu_resul_form_steps.rb index 10bc6dde95..344fb558ab 100644 --- a/features/step_definitions/visu_resul_form_steps.rb +++ b/features/step_definitions/visu_resul_form_steps.rb @@ -2,13 +2,6 @@ # ==================== CONTEXTO ==================== -# Removed duplicate auth step - -# Removed duplicate navigation step - -# Removed duplicate click step - - Então('sou redirecionado para a página {string}') do |pagina| case pagina when 'Gerenciamento - Resultados' @@ -385,6 +378,10 @@ expect(page).not_to have_selector('.questao, canvas, svg') end +Então('vejo o botão {string}') do |botao| + expect(page).to have_button(botao) +end + Dado('que existem múltiplos formulários com quantidades diferentes de respostas') do @class = Class.create!( code: 'CIC0105', @@ -462,6 +459,10 @@ expect(page).to have_current_path(home_path, wait: 10) end +Então('vejo a mensagem de erro {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + Dado('que nenhum formulário foi criado ou enviado') do # Nenhum formulário é criado end @@ -470,6 +471,10 @@ expect(page).not_to have_selector('table tbody tr') end +Então('vejo a mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + Então('o botão {string} não está disponível') do |botao| expect(page).not_to have_button(botao) end @@ -515,6 +520,10 @@ expect(page).not_to have_button(botao2) end +Então('vejo a mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + Dado('que um formulário foi deletado do sistema') do @form = Form.create!( destiny_type: 'discente', @@ -530,6 +539,10 @@ visit admin_form_details_path(@form_id) end +Então('vejo a mensagem de erro {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + Então('sou redirecionado para a página {string}') do |pagina| case pagina when 'Gerenciamento - Resultados' @@ -543,6 +556,10 @@ expect(page).to have_selector('table') end +Então('vejo a mensagem de erro {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + Dado('que existe um formulário com mais de 1000 respostas') do @class = Class.create!( code: 'CIC0105', @@ -578,11 +595,19 @@ expect(page).not_to have_selector('canvas, svg') end +Então('vejo a mensagem {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + Quando('clico no botão {string} e a exportação falha') do |botao| allow(FormResponse).to receive(:for_export).and_raise(StandardError) click_button botao end +Então('vejo a mensagem de erro {string}') do |mensagem| + expect(page).to have_content(mensagem) +end + Então('o arquivo não é baixado') do expect(page.response_headers['Content-Disposition']).to be_nil -end +end \ No newline at end of file From fbcc9ef814decd1bebb1888ee2a3f6224fb9ce18 Mon Sep 17 00:00:00 2001 From: diamondxorb Date: Mon, 29 Jun 2026 10:51:21 -0300 Subject: [PATCH 73/87] =?UTF-8?q?refatora=C3=A7=C3=A3o=20para=20a=20sprint?= =?UTF-8?q?=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/admin/forms_controller.rb | 54 +++++++ .../admin/management_controller.rb | 15 ++ app/controllers/admin/results_controller.rb | 99 ++++++++++++ app/controllers/admin/templates_controller.rb | 124 +++++++++++++++ app/controllers/application_controller.rb | 2 +- app/controllers/avaliacoes_controller.rb | 60 +++++++ app/controllers/formularios_controller.rb | 2 +- .../controllers/template_modal_controller.js | 148 ++++++++++++++++++ app/models/form.rb | 16 ++ app/models/form_response.rb | 10 ++ app/models/question.rb | 7 + app/models/template.rb | 14 +- app/models/turma.rb | 21 ++- app/models/turma_membership.rb | 5 + app/models/user.rb | 11 +- app/views/admin/management/index.html.erb | 110 +++++++++++++ app/views/admin/results/index.html.erb | 27 ++++ app/views/admin/results/show.html.erb | 29 ++++ app/views/admin/templates/index.html.erb | 103 ++++++++++++ app/views/avaliacoes/index.html.erb | 20 +++ app/views/avaliacoes/show.html.erb | 29 ++++ app/views/sessions/new.html.erb | 4 +- config/routes.rb | 11 ++ .../20260617000001_add_nome_to_users.rb | 6 + ...617000002_add_nome_and_fields_to_turmas.rb | 5 + ...0260617000003_add_semester_to_templates.rb | 6 + ...20260617000004_create_turma_memberships.rb | 10 ++ db/migrate/20260617000005_create_questions.rb | 10 ++ db/migrate/20260617000006_create_forms.rb | 10 ++ .../20260617000007_create_forms_turmas.rb | 8 + .../20260617000008_create_form_responses.rb | 11 ++ db/schema.rb | 62 +++++++- .../gerar_formulario_steps.rb | 12 ++ spec/rails_helper.rb | 11 +- 34 files changed, 1053 insertions(+), 19 deletions(-) create mode 100644 app/controllers/admin/forms_controller.rb create mode 100644 app/controllers/admin/management_controller.rb create mode 100644 app/controllers/admin/results_controller.rb create mode 100644 app/controllers/admin/templates_controller.rb create mode 100644 app/controllers/avaliacoes_controller.rb create mode 100644 app/javascript/controllers/template_modal_controller.js create mode 100644 app/models/form.rb create mode 100644 app/models/form_response.rb create mode 100644 app/models/question.rb create mode 100644 app/models/turma_membership.rb create mode 100644 app/views/admin/management/index.html.erb create mode 100644 app/views/admin/results/index.html.erb create mode 100644 app/views/admin/results/show.html.erb create mode 100644 app/views/admin/templates/index.html.erb create mode 100644 app/views/avaliacoes/index.html.erb create mode 100644 app/views/avaliacoes/show.html.erb create mode 100644 db/migrate/20260617000001_add_nome_to_users.rb create mode 100644 db/migrate/20260617000002_add_nome_and_fields_to_turmas.rb create mode 100644 db/migrate/20260617000003_add_semester_to_templates.rb create mode 100644 db/migrate/20260617000004_create_turma_memberships.rb create mode 100644 db/migrate/20260617000005_create_questions.rb create mode 100644 db/migrate/20260617000006_create_forms.rb create mode 100644 db/migrate/20260617000007_create_forms_turmas.rb create mode 100644 db/migrate/20260617000008_create_form_responses.rb diff --git a/app/controllers/admin/forms_controller.rb b/app/controllers/admin/forms_controller.rb new file mode 100644 index 0000000000..5ba230b419 --- /dev/null +++ b/app/controllers/admin/forms_controller.rb @@ -0,0 +1,54 @@ +# Namespace para os controllers do painel administrativo do sistema +module Admin +end + +# Controller para criação de formulários de avaliação pelo administrador +class Admin::FormsController < ApplicationController + before_action :require_admin + + # Cria um formulário baseado em template e o envia para as turmas selecionadas + # + # Argumentos: params[:template_id], params[:turma_ids] (array) + # Retorno: Nenhum (redireciona) + # Efeitos colaterais: Cria Form e associações com turmas no banco + def create + return redirect_missing_template if params[:template_id].blank? + + turma_ids = Array(params[:turma_ids]).reject(&:blank?) + return redirect_missing_turmas if turma_ids.empty? + + deliver_form(params[:template_id], turma_ids) + end + + private + + # Redireciona com erro quando template não foi selecionado + # + # Argumentos: Nenhum + # Retorno: ActionDispatch::Response (redirect) + # Efeitos colaterais: Nenhum + def redirect_missing_template + redirect_to admin_management_path, alert: "Selecione um template para o formulário" + end + + # Redireciona com erro quando nenhuma turma foi selecionada + # + # Argumentos: Nenhum + # Retorno: ActionDispatch::Response (redirect) + # Efeitos colaterais: Nenhum + def redirect_missing_turmas + redirect_to admin_management_path, alert: "Selecione ao menos uma turma" + end + + # Cria o formulário e associa às turmas selecionadas + # + # Argumentos: template_id (String), turma_ids (Array) + # Retorno: ActionDispatch::Response (redirect) + # Efeitos colaterais: Cria Form e registros em forms_turmas + def deliver_form(template_id, turma_ids) + template = Template.find_by(id: template_id) + form = Form.create!(template: template, start_date: Time.current, end_date: 1.month.from_now) + form.turmas << Turma.where(id: turma_ids) + redirect_to admin_management_path, notice: "Formulário enviado com sucesso" + end +end diff --git a/app/controllers/admin/management_controller.rb b/app/controllers/admin/management_controller.rb new file mode 100644 index 0000000000..a91c6c5551 --- /dev/null +++ b/app/controllers/admin/management_controller.rb @@ -0,0 +1,15 @@ +# Controller da página principal de gerenciamento do administrador +class Admin::ManagementController < ApplicationController + before_action :require_admin + layout "dashboard" + + # Exibe o painel de gerenciamento com botões de ação + # + # Argumentos: Nenhum + # Retorno: Nenhum (renderiza view) + # Efeitos colaterais: Nenhum + def index + @turmas = Turma.includes(:disciplina).all + @templates = Template.order(:nome) + end +end diff --git a/app/controllers/admin/results_controller.rb b/app/controllers/admin/results_controller.rb new file mode 100644 index 0000000000..6dc16129ca --- /dev/null +++ b/app/controllers/admin/results_controller.rb @@ -0,0 +1,99 @@ +require "csv" + +# Controller para visualização e download de resultados de formulários +class Admin::ResultsController < ApplicationController + before_action :require_admin + layout "dashboard" + before_action :set_turma, only: [:show, :csv] + + # Lista turmas que possuem formulários com respostas + # + # Argumentos: Nenhum + # Retorno: Nenhum (renderiza view) + # Efeitos colaterais: Nenhum + def index + turma_ids = FormResponse.distinct.pluck(:turma_id) + @turmas = Turma.includes(:disciplina, :docentes).where(id: turma_ids) + end + + # Exibe detalhes de resultados de uma turma específica + # + # Argumentos: params[:id] — id da Turma + # Retorno: Nenhum (renderiza view) + # Efeitos colaterais: Nenhum + def show + @responses = FormResponse.where(turma: @turma).includes(:user, form: { template: :questions }) + end + + # Gera e faz download de CSV com as respostas da turma + # + # Argumentos: params[:id] — id da Turma + # Retorno: Arquivo CSV + # Efeitos colaterais: Envia arquivo ao cliente + def csv + responses = FormResponse.where(turma: @turma).includes(:user, form: { template: :questions }) + send_data build_csv(responses), + type: "text/csv; charset=utf-8", + filename: "resultados_turma_#{@turma.id}.csv", + disposition: "attachment" + end + + private + + def set_turma + @turma = Turma.includes(:disciplina, :docentes).find(params[:id]) + end + + # Gera CSV com colunas Turma, Discente, Questão, Resposta + # + # Argumentos: responses (ActiveRecord::Relation) + # Retorno: String com conteúdo CSV + # Efeitos colaterais: Nenhum + def build_csv(responses) + CSV.generate(headers: true) do |csv| + csv << %w[Turma Discente Questão Resposta] + responses.each { |fr| append_response_rows(csv, fr) } + end + end + + # Adiciona linhas de um FormResponse ao CSV + # + # Argumentos: csv (CSV), fr (FormResponse) + # Retorno: Nenhum + # Efeitos colaterais: Modifica csv in-place + def append_response_rows(csv, fr) + answers = Array(fr.answers) + return csv << empty_row(fr) if answers.empty? + + questions = questions_for(fr) + answers.each { |ans| csv << answer_row(fr, questions, ans) } + end + + # Retorna linha vazia para respostas sem dados + # + # Argumentos: fr (FormResponse) + # Retorno: Array com campos da linha + # Efeitos colaterais: Nenhum + def empty_row(fr) + [@turma.classCode, fr.user&.nome.to_s, "", ""] + end + + # Retorna linha preenchida para uma resposta + # + # Argumentos: fr (FormResponse), questions (Array), ans (Hash) + # Retorno: Array com campos da linha + # Efeitos colaterais: Nenhum + def answer_row(fr, questions, ans) + question = questions.find { |q| q.id == ans["question_id"] } + [@turma.classCode, fr.user&.nome.to_s, question&.text.to_s, ans["value"].to_s] + end + + # Retorna as questões associadas ao FormResponse + # + # Argumentos: fr (FormResponse) + # Retorno: Array de Question + # Efeitos colaterais: Nenhum + def questions_for(fr) + fr.form&.template&.questions || [] + end +end diff --git a/app/controllers/admin/templates_controller.rb b/app/controllers/admin/templates_controller.rb new file mode 100644 index 0000000000..61146eb810 --- /dev/null +++ b/app/controllers/admin/templates_controller.rb @@ -0,0 +1,124 @@ +# Controller para CRUD de templates de formulário no painel admin +class Admin::TemplatesController < ApplicationController + before_action :require_admin + layout "dashboard" + before_action :set_template, only: [:update, :destroy] + + # Lista todos os templates + # + # Argumentos: Nenhum + # Retorno: Nenhum (renderiza view) + # Efeitos colaterais: Nenhum + def index + @templates = Template.order(:nome) + end + + # Cria um novo template com suas questões + # + # Argumentos: params[:template] — nome; params[:questions] — array de questões + # Retorno: Nenhum (redireciona) + # Efeitos colaterais: Cria registros de Template e Question no banco + def create + name = extract_template_name + return redirect_to(admin_templates_path, alert: "O nome do template é obrigatório") if name.blank? + + @template = Template.new(nome: name, semester: template_semester) + persist_template("Template criado com sucesso") + end + + # Atualiza um template existente + # + # Argumentos: params[:id], params[:template] + # Retorno: Nenhum (redireciona) + # Efeitos colaterais: Atualiza registro no banco + def update + if @template.update(nome: resolve_nome, semester: template_semester) + build_questions(@template, replace: true) + flash[:notice] = "Template atualizado com sucesso" + else + flash[:alert] = @template.errors.full_messages.join(", ") + end + redirect_to admin_templates_path + end + + # Exclui um template + # + # Argumentos: params[:id] + # Retorno: Nenhum (redireciona) + # Efeitos colaterais: Remove o template do banco + def destroy + @template.destroy + redirect_to admin_templates_path, notice: "Template removido com sucesso" + end + + private + + def set_template + @template = Template.find(params[:id]) + rescue ActiveRecord::RecordNotFound + redirect_to admin_templates_path, alert: "Template não encontrado" + end + + # Extrai o nome do template dos params (suporta :name e :nome) + # + # Argumentos: Nenhum + # Retorno: String ou nil + # Efeitos colaterais: Nenhum + def extract_template_name + params.dig(:template, :name).presence || params.dig(:template, :nome) + end + + # Extrai o semestre do template dos params + # + # Argumentos: Nenhum + # Retorno: String ou nil + # Efeitos colaterais: Nenhum + def template_semester + params.dig(:template, :semester) + end + + # Resolve o nome a usar no update (prioridade: name > nome > nome atual) + # + # Argumentos: Nenhum + # Retorno: String + # Efeitos colaterais: Nenhum + def resolve_nome + extract_template_name.presence || @template.nome + end + + # Salva o template e redireciona com mensagem de sucesso ou erro + # + # Argumentos: success_msg (String) + # Retorno: ActionDispatch::Response (redirect) + # Efeitos colaterais: Persiste Template e Questions no banco + def persist_template(success_msg) + if @template.save + build_questions(@template) + flash[:notice] = success_msg + else + flash[:alert] = @template.errors.full_messages.join(", ") + end + redirect_to admin_templates_path + end + + # Constrói questões a partir dos params + # + # Argumentos: template (Template), replace (Boolean) + # Retorno: Nenhum + # Efeitos colaterais: Cria/deleta registros de Question + def build_questions(template, replace: false) + return unless params[:questions].present? + template.questions.destroy_all if replace + params[:questions].each_value { |q| persist_question(template, q) } + end + + # Persiste uma questão individual se o texto não for vazio + # + # Argumentos: template (Template), q (Hash com :text e :kind) + # Retorno: Nenhum + # Efeitos colaterais: Cria registro de Question no banco + def persist_question(template, q) + return if q[:text].blank? + template.questions.create!(kind: q[:kind].presence || "text", text: q[:text]) + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4ba1d28ddc..d9890c4848 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -26,7 +26,7 @@ def admin? def require_admin unless admin? - redirect_to root_path, alert: "Acesso restrito a administradores." + redirect_to avaliacoes_path, alert: "Acesso não autorizado" end end end diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb new file mode 100644 index 0000000000..7853710943 --- /dev/null +++ b/app/controllers/avaliacoes_controller.rb @@ -0,0 +1,60 @@ +# Controller para o painel de avaliações do discente +class AvaliacoesController < ApplicationController + layout "dashboard" + before_action :set_turma_and_form, only: [:show, :responder] + + # Lista as turmas com formulários disponíveis para o usuário logado + # + # Argumentos: Nenhum + # Retorno: Nenhum (renderiza view) + # Efeitos colaterais: Nenhum + def index + turmas_ids = current_user.turmas.pluck(:id) + @turmas_com_form = Turma + .joins(:forms) + .where(id: turmas_ids) + .where("forms.end_date > ?", Time.current) + .includes(:disciplina) + .distinct + end + + # Exibe o formulário de avaliação de uma turma para o discente responder + # + # Argumentos: params[:id] — id da Turma + # Retorno: Nenhum (renderiza view) + # Efeitos colaterais: Nenhum + def show + @questions = @form.template.questions + end + + # Processa a submissão das respostas do discente + # + # Argumentos: params[:id] — id da Turma; params[:answers] — hash de respostas + # Retorno: Nenhum (redireciona) + # Efeitos colaterais: Cria FormResponse no banco + def responder + answers = build_answers + + FormResponse.create!( + form: @form, + user: current_user, + turma: @turma, + answers: answers + ) + + redirect_to avaliacoes_path, notice: "Avaliação enviada com sucesso" + end + + private + + def set_turma_and_form + @turma = Turma.find(params[:id]) + @form = @turma.forms.where("end_date > ?", Time.current).first + redirect_to avaliacoes_path, alert: "Formulário não disponível" unless @form + end + + def build_answers + return [] unless params[:answers].is_a?(ActionController::Parameters) + params[:answers].values.map { |a| a.is_a?(String) ? a : a.to_unsafe_h } + end +end diff --git a/app/controllers/formularios_controller.rb b/app/controllers/formularios_controller.rb index 7a076fe182..964a880ae3 100644 --- a/app/controllers/formularios_controller.rb +++ b/app/controllers/formularios_controller.rb @@ -2,7 +2,7 @@ class FormulariosController < ApplicationController layout "dashboard" def index - @turmas = current_user.turmas + @turmas = current_user.sigaa_turmas @formularios = Formulario.where(turma_id: @turmas.pluck(:id)) end diff --git a/app/javascript/controllers/template_modal_controller.js b/app/javascript/controllers/template_modal_controller.js new file mode 100644 index 0000000000..21e749537d --- /dev/null +++ b/app/javascript/controllers/template_modal_controller.js @@ -0,0 +1,148 @@ +import { Controller } from "@hotwired/stimulus" + +// Gerencia os modais de criação, edição e exclusão de templates de formulário +export default class extends Controller { + + openNew() { + const modal = document.getElementById("templateModal") + const form = document.getElementById("templateForm") + const methodField = document.getElementById("templateFormMethod") + const submitBtn = document.getElementById("templateSubmitBtn") + + form.action = form.dataset.createUrl || "/admin/templates" + methodField.value = "post" + document.getElementById("templateName").value = "" + document.getElementById("questoesContainer").innerHTML = this._questaoHtml(0) + submitBtn.textContent = "Criar" + this._hideError() + + modal.style.display = "block" + } + + openEdit(event) { + const btn = event.currentTarget + const id = btn.dataset.templateId + const name = btn.dataset.templateName || "" + const modal = document.getElementById("templateModal") + const form = document.getElementById("templateForm") + const submitBtn = document.getElementById("templateSubmitBtn") + + form.action = `/admin/templates/${id}` + document.getElementById("templateFormMethod").value = "patch" + document.getElementById("templateName").value = name + // Limpa questões ao editar — servidor mantém as questões existentes + document.getElementById("questoesContainer").innerHTML = "" + submitBtn.textContent = "Criar" + this._hideError() + + modal.style.display = "block" + } + + openDelete(event) { + const btn = event.currentTarget + const id = btn.dataset.templateId + const name = btn.dataset.templateName || "" + const modal = document.getElementById("deleteModal") + + document.getElementById("deleteMessage").textContent = + `Deseja excluir o template ${name}?` + document.getElementById("deleteForm").action = `/admin/templates/${id}` + + modal.style.display = "block" + } + + close() { + document.getElementById("templateModal").style.display = "none" + } + + closeDelete() { + document.getElementById("deleteModal").style.display = "none" + } + + handleSubmit(event) { + const name = document.getElementById("templateName").value.trim() + + if (!name) { + event.preventDefault() + this._showError("O nome do template é obrigatório") + return + } + + const questoes = document.querySelectorAll("#questoesContainer .questao") + let hasTextError = false + + questoes.forEach(q => { + const texto = q.querySelector(".questao-texto").value.trim() + if (!texto) hasTextError = true + }) + + if (hasTextError) { + event.preventDefault() + this._showError("O texto da questão é obrigatório") + return + } + } + + addQuestao() { + const container = document.getElementById("questoesContainer") + const idx = container.querySelectorAll(".questao").length + container.insertAdjacentHTML("beforeend", this._questaoHtml(idx)) + } + + addOpcao(event) { + const questao = event.currentTarget.closest(".questao") + const idx = parseInt(questao.dataset.idx, 10) || 0 + const opcoes = questao.querySelector(".opcoes-container") + const count = opcoes.querySelectorAll(".campo-opcao").length + const newId = `questao_${idx}_opcao_${count}` + + const input = document.createElement("input") + input.type = "text" + input.id = newId + input.name = `questions[${idx}][options][]` + input.className = "campo-opcao" + input.placeholder = `Opção ${count + 1}` + + opcoes.insertBefore(input, event.currentTarget) + } + + _showError(msg) { + const errDiv = document.getElementById("templateModalError") + if (errDiv) { + errDiv.textContent = msg + errDiv.style.display = "block" + } + } + + _hideError() { + const errDiv = document.getElementById("templateModalError") + if (errDiv) errDiv.style.display = "none" + } + + _questaoHtml(idx) { + return ` +
+
+ + +
+
+ + +
+
+ + + +
+
` + } +} diff --git a/app/models/form.rb b/app/models/form.rb new file mode 100644 index 0000000000..0f8384b814 --- /dev/null +++ b/app/models/form.rb @@ -0,0 +1,16 @@ +# Formulário de avaliação baseado em um Template, enviado para turmas +class Form < ApplicationRecord + belongs_to :template + has_and_belongs_to_many :turmas + + validates :template, presence: true + + # Retorna true se o formulário ainda está disponível para resposta + # + # Argumentos: Nenhum + # Retorno: Boolean + # Efeitos colaterais: Nenhum + def active? + end_date.nil? || end_date > Time.current + end +end diff --git a/app/models/form_response.rb b/app/models/form_response.rb new file mode 100644 index 0000000000..75c479f412 --- /dev/null +++ b/app/models/form_response.rb @@ -0,0 +1,10 @@ +# Resposta de um discente a um formulário enviado para sua turma +class FormResponse < ApplicationRecord + belongs_to :form + belongs_to :user + belongs_to :turma + + serialize :answers, coder: JSON + + validates :form, :user, :turma, presence: true +end diff --git a/app/models/question.rb b/app/models/question.rb new file mode 100644 index 0000000000..29ba97b1d6 --- /dev/null +++ b/app/models/question.rb @@ -0,0 +1,7 @@ +# Questão pertencente a um Template de formulário +class Question < ApplicationRecord + belongs_to :template + + validates :kind, presence: true, inclusion: { in: %w[radio text] } + validates :text, presence: true +end diff --git a/app/models/template.rb b/app/models/template.rb index 0af99d8c93..ebf255ec05 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -1,12 +1,14 @@ +# Template de formulário, contendo questões reutilizáveis class Template < ApplicationRecord belongs_to :user, optional: true + belongs_to :parent_template, class_name: "Template", optional: true + + has_many :versions, class_name: "Template", foreign_key: "parent_template_id" + has_many :questions, dependent: :destroy + has_many :forms, dependent: :destroy validates :nome, presence: true - has_many :versions, - class_name: "Template", - foreign_key: "parent_template_id" - belongs_to :parent_template, - class_name: "Template", - optional: true + # Permite usar :name como alias de :nome para compatibilidade com specs + alias_attribute :name, :nome end \ No newline at end of file diff --git a/app/models/turma.rb b/app/models/turma.rb index f988c31a35..2ad8a0dea1 100644 --- a/app/models/turma.rb +++ b/app/models/turma.rb @@ -1,6 +1,23 @@ +# Turma associada a uma disciplina class Turma < ApplicationRecord belongs_to :disciplina - has_many :discentes, dependent: :destroy - has_many :docentes, dependent: :destroy + has_many :docente_records, class_name: "Docente", dependent: :destroy + has_many :discente_records, class_name: "Discente", dependent: :destroy has_many :formularios, dependent: :destroy + has_and_belongs_to_many :forms + + has_many :turma_memberships, dependent: :destroy + has_many :member_users, through: :turma_memberships, source: :user + + # Users com role 'docente' vinculados via TurmaMembership + has_many :docentes, + -> { where(role: "docente") }, + through: :turma_memberships, + source: :user + + # Users com role 'discente' vinculados via TurmaMembership + has_many :discente_users, + -> { where(role: "discente") }, + through: :turma_memberships, + source: :user end diff --git a/app/models/turma_membership.rb b/app/models/turma_membership.rb new file mode 100644 index 0000000000..f1c9e3ce75 --- /dev/null +++ b/app/models/turma_membership.rb @@ -0,0 +1,5 @@ +# Associação entre Turma e User (discentes/docentes cadastrados via User) +class TurmaMembership < ApplicationRecord + belongs_to :turma + belongs_to :user +end diff --git a/app/models/user.rb b/app/models/user.rb index d1b6d6808a..9666ba50d5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,6 +3,9 @@ class User < ApplicationRecord has_secure_password validations: false has_many :respostas, dependent: :destroy + has_many :turma_memberships, dependent: :destroy + has_many :turmas, through: :turma_memberships + has_many :form_responses, dependent: :destroy validates :email, uniqueness: true, allow_blank: false validates :matricula, uniqueness: true, allow_blank: false @@ -59,18 +62,18 @@ def profile end end - # Retorna turmas vinculadas ao usuário + # Retorna turmas vinculadas ao usuário via importação SIGAA (legado) # # Argumentos: Nenhum - # Retorno: Collection + # Retorno: ActiveRecord::Relation ou Array vazio # Efeitos Colaterais: Consulta no BD - def turmas + def sigaa_turmas if discente? Turma.where(id: Discente.where(matricula: matricula).select(:turma_id)) elsif docente? Turma.where(id: Docente.where(usuario: matricula).select(:turma_id)) else - [] + Turma.none end end diff --git a/app/views/admin/management/index.html.erb b/app/views/admin/management/index.html.erb new file mode 100644 index 0000000000..9f659da5e2 --- /dev/null +++ b/app/views/admin/management/index.html.erb @@ -0,0 +1,110 @@ +

Gerenciamento

+ +
+ + + + + <%= link_to "Resultados", admin_results_path, class: "btn btn-info" %> +
+ + + + + diff --git a/app/views/admin/results/index.html.erb b/app/views/admin/results/index.html.erb new file mode 100644 index 0000000000..6511f285ac --- /dev/null +++ b/app/views/admin/results/index.html.erb @@ -0,0 +1,27 @@ +<% if flash[:notice] %> +
<%= flash[:notice] %>
+<% end %> +<% if flash[:alert] %> +
<%= flash[:alert] %>
+<% end %> + +

Resultados

+ +<% if @turmas.empty? %> +

Nenhum resultado disponível

+<% else %> +
+ <% @turmas.each do |turma| %> +
+ + <%= link_to turma.disciplina&.name || turma.classCode, + admin_result_path(turma) %> + + <%= turma.semester %> + + <%= turma.docentes.map(&:nome).reject(&:blank?).join(", ") %> + +
+ <% end %> +
+<% end %> diff --git a/app/views/admin/results/show.html.erb b/app/views/admin/results/show.html.erb new file mode 100644 index 0000000000..142ca15297 --- /dev/null +++ b/app/views/admin/results/show.html.erb @@ -0,0 +1,29 @@ +

Resultados — <%= @turma.disciplina&.name || @turma.classCode %>

+ +<%= form_with url: csv_admin_result_path(@turma), method: :get, local: true do %> + <%= submit_tag "Baixar CSV", class: "btn btn-primary" %> +<% end %> + +<% if @responses.empty? %> +

Esta turma ainda não possui respostas registradas

+<% else %> + + + + + + <% @responses.each do |fr| %> + <% answers = Array(fr.answers) %> + <% questions = fr.form&.template&.questions || [] %> + <% answers.each do |a| %> + <% q = questions.find { |q| q.id == a["question_id"] } %> + + + + + + <% end %> + <% end %> + +
DiscenteQuestãoResposta
<%= fr.user&.nome %><%= q&.text %><%= a["value"] %>
+<% end %> diff --git a/app/views/admin/templates/index.html.erb b/app/views/admin/templates/index.html.erb new file mode 100644 index 0000000000..aa38f6e704 --- /dev/null +++ b/app/views/admin/templates/index.html.erb @@ -0,0 +1,103 @@ +
+ +

Templates

+ +
+
+
+ + <% @templates.each do |template| %> +
+ <%= template.nome %> + <%= template.semester %> +
+ + +
+
+ <% end %> +
+ + + + + + + +
diff --git a/app/views/avaliacoes/index.html.erb b/app/views/avaliacoes/index.html.erb new file mode 100644 index 0000000000..dd32d277d7 --- /dev/null +++ b/app/views/avaliacoes/index.html.erb @@ -0,0 +1,20 @@ +<% if flash[:notice] %> +
<%= flash[:notice] %>
+<% end %> +<% if flash[:alert] %> +
<%= flash[:alert] %>
+<% end %> + +

Minhas Avaliações

+ +<% if @turmas_com_form.empty? %> +

Nenhuma avaliação disponível no momento.

+<% else %> +
+ <% @turmas_com_form.each do |turma| %> +
+ <%= link_to turma.classCode, avaliacao_path(turma) %> +
+ <% end %> +
+<% end %> diff --git a/app/views/avaliacoes/show.html.erb b/app/views/avaliacoes/show.html.erb new file mode 100644 index 0000000000..00e9a74926 --- /dev/null +++ b/app/views/avaliacoes/show.html.erb @@ -0,0 +1,29 @@ +<% if flash[:notice] %> +
<%= flash[:notice] %>
+<% end %> +<% if flash[:alert] %> +
<%= flash[:alert] %>
+<% end %> + +

Avaliação — <%= @turma.classCode %>

+ +<%= form_with url: responder_avaliacao_path(@turma), method: :post, local: true do |f| %> + <% @questions.each_with_index do |question, idx| %> +
+

<%= question.text %>

+ + <% if question.kind == "radio" %> + <% ["Sim", "Não", "Parcialmente"].each do |opcao| %> + + <% end %> + <% else %> + + <% end %> +
+ <% end %> + + +<% end %> diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index 6fecda5baf..3819c586be 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -4,8 +4,8 @@ <%= form_with url: login_path do |form| %>
- <%= form.label :login, "Email ou Matrícula" %> - <%= form.text_field :login, required: true, placeholder: "Email ou Matrícula" %> + <%= form.label :login, "Email ou Matrícula", for: "email" %> + <%= form.text_field :login, id: "email", required: true, placeholder: "Email ou Matrícula" %>
<%= form.label :password, "Senha" %> diff --git a/config/routes.rb b/config/routes.rb index a89d06491b..cf63b83982 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,8 +23,19 @@ namespace :admin do resources :imports, only: [:index, :create] + get "management", to: "management#index", as: "management" + get "results", to: "results#index", as: "results" + resources :results, only: [:show] do + member { get :csv } + end + resources :templates, only: [:index, :create, :update, :destroy] + resources :forms, only: [:create] end + get "avaliacoes", to: "avaliacoes#index", as: "avaliacoes" + get "avaliacoes/:id", to: "avaliacoes#show", as: "avaliacao" + post "avaliacoes/:id/responder", to: "avaliacoes#responder", as: "responder_avaliacao" + get "esqueci-senha", to: "password_resets#new", as: "new_password_reset" post "esqueci-senha", to: "password_resets#create", as: "password_resets" get "redefinir-senha/:token", to: "password_resets#edit", as: "password_reset" diff --git a/db/migrate/20260617000001_add_nome_to_users.rb b/db/migrate/20260617000001_add_nome_to_users.rb new file mode 100644 index 0000000000..d08d1ee06e --- /dev/null +++ b/db/migrate/20260617000001_add_nome_to_users.rb @@ -0,0 +1,6 @@ +class AddNomeToUsers < ActiveRecord::Migration[8.1] + def change + add_column :users, :nome, :string + add_column :users, :departamento, :string + end +end diff --git a/db/migrate/20260617000002_add_nome_and_fields_to_turmas.rb b/db/migrate/20260617000002_add_nome_and_fields_to_turmas.rb new file mode 100644 index 0000000000..e0a8e29203 --- /dev/null +++ b/db/migrate/20260617000002_add_nome_and_fields_to_turmas.rb @@ -0,0 +1,5 @@ +class AddNomeAndFieldsToTurmas < ActiveRecord::Migration[8.1] + def change + add_column :turmas, :nome, :string + end +end diff --git a/db/migrate/20260617000003_add_semester_to_templates.rb b/db/migrate/20260617000003_add_semester_to_templates.rb new file mode 100644 index 0000000000..14815482ef --- /dev/null +++ b/db/migrate/20260617000003_add_semester_to_templates.rb @@ -0,0 +1,6 @@ +class AddSemesterToTemplates < ActiveRecord::Migration[8.1] + def change + add_column :templates, :semester, :string + change_column_null :templates, :user_id, true + end +end diff --git a/db/migrate/20260617000004_create_turma_memberships.rb b/db/migrate/20260617000004_create_turma_memberships.rb new file mode 100644 index 0000000000..4ffcda47ad --- /dev/null +++ b/db/migrate/20260617000004_create_turma_memberships.rb @@ -0,0 +1,10 @@ +class CreateTurmaMemberships < ActiveRecord::Migration[8.1] + def change + create_table :turma_memberships do |t| + t.references :turma, null: false, foreign_key: true + t.references :user, null: false, foreign_key: true + t.timestamps + end + add_index :turma_memberships, [:turma_id, :user_id], unique: true + end +end diff --git a/db/migrate/20260617000005_create_questions.rb b/db/migrate/20260617000005_create_questions.rb new file mode 100644 index 0000000000..9365bcd29e --- /dev/null +++ b/db/migrate/20260617000005_create_questions.rb @@ -0,0 +1,10 @@ +class CreateQuestions < ActiveRecord::Migration[8.1] + def change + create_table :questions do |t| + t.references :template, null: false, foreign_key: true + t.string :kind, null: false + t.text :text, null: false + t.timestamps + end + end +end diff --git a/db/migrate/20260617000006_create_forms.rb b/db/migrate/20260617000006_create_forms.rb new file mode 100644 index 0000000000..fde48d6b15 --- /dev/null +++ b/db/migrate/20260617000006_create_forms.rb @@ -0,0 +1,10 @@ +class CreateForms < ActiveRecord::Migration[8.1] + def change + create_table :forms do |t| + t.references :template, null: false, foreign_key: true + t.datetime :start_date + t.datetime :end_date + t.timestamps + end + end +end diff --git a/db/migrate/20260617000007_create_forms_turmas.rb b/db/migrate/20260617000007_create_forms_turmas.rb new file mode 100644 index 0000000000..e3e36b2ff7 --- /dev/null +++ b/db/migrate/20260617000007_create_forms_turmas.rb @@ -0,0 +1,8 @@ +class CreateFormsTurmas < ActiveRecord::Migration[8.1] + def change + create_join_table :forms, :turmas do |t| + t.index [:form_id, :turma_id], unique: true + t.index [:turma_id, :form_id] + end + end +end diff --git a/db/migrate/20260617000008_create_form_responses.rb b/db/migrate/20260617000008_create_form_responses.rb new file mode 100644 index 0000000000..c84d02a2fb --- /dev/null +++ b/db/migrate/20260617000008_create_form_responses.rb @@ -0,0 +1,11 @@ +class CreateFormResponses < ActiveRecord::Migration[8.1] + def change + create_table :form_responses do |t| + t.references :form, null: false, foreign_key: true + t.references :user, null: false, foreign_key: true + t.references :turma, null: false, foreign_key: true + t.text :answers + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2e2aa82b8c..2135cfee8d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_06_16_200000) do +ActiveRecord::Schema[8.1].define(version: 2026_06_17_000008) do create_table "discentes", force: :cascade do |t| t.datetime "created_at", null: false t.string "curso" @@ -45,6 +45,34 @@ t.index ["turma_id"], name: "index_docentes_on_turma_id" end + create_table "form_responses", force: :cascade do |t| + t.text "answers" + t.datetime "created_at", null: false + t.integer "form_id", null: false + t.integer "turma_id", null: false + t.datetime "updated_at", null: false + t.integer "user_id", null: false + t.index ["form_id"], name: "index_form_responses_on_form_id" + t.index ["turma_id"], name: "index_form_responses_on_turma_id" + t.index ["user_id"], name: "index_form_responses_on_user_id" + end + + create_table "forms", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "end_date" + t.datetime "start_date" + t.integer "template_id", null: false + t.datetime "updated_at", null: false + t.index ["template_id"], name: "index_forms_on_template_id" + end + + create_table "forms_turmas", id: false, force: :cascade do |t| + t.integer "form_id", null: false + t.integer "turma_id", null: false + t.index ["form_id", "turma_id"], name: "index_forms_turmas_on_form_id_and_turma_id", unique: true + t.index ["turma_id", "form_id"], name: "index_forms_turmas_on_turma_id_and_form_id" + end + create_table "formularios", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "deadline" @@ -65,6 +93,15 @@ t.index ["formulario_id"], name: "index_perguntas_on_formulario_id" end + create_table "questions", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "kind", null: false + t.integer "template_id", null: false + t.text "text", null: false + t.datetime "updated_at", null: false + t.index ["template_id"], name: "index_questions_on_template_id" + end + create_table "respostas", force: :cascade do |t| t.text "conteudo" t.datetime "created_at", null: false @@ -91,17 +128,29 @@ t.text "descricao" t.string "nome", null: false t.integer "parent_template_id" + t.string "semester" t.datetime "updated_at", null: false - t.integer "user_id", null: false + t.integer "user_id" t.integer "version", default: 1, null: false t.index ["parent_template_id"], name: "index_templates_on_parent_template_id" t.index ["user_id"], name: "index_templates_on_user_id" end + create_table "turma_memberships", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "turma_id", null: false + t.datetime "updated_at", null: false + t.integer "user_id", null: false + t.index ["turma_id", "user_id"], name: "index_turma_memberships_on_turma_id_and_user_id", unique: true + t.index ["turma_id"], name: "index_turma_memberships_on_turma_id" + t.index ["user_id"], name: "index_turma_memberships_on_user_id" + end + create_table "turmas", force: :cascade do |t| t.string "classCode" t.datetime "created_at", null: false t.integer "disciplina_id", null: false + t.string "nome" t.string "semester" t.string "time" t.datetime "updated_at", null: false @@ -110,10 +159,12 @@ create_table "users", force: :cascade do |t| t.datetime "created_at", null: false + t.string "departamento" t.string "email" t.datetime "invitation_sent_at" t.string "invitation_token" t.string "matricula" + t.string "nome" t.string "password_digest" t.datetime "reset_sent_at" t.string "reset_token" @@ -123,13 +174,20 @@ add_foreign_key "discentes", "turmas" add_foreign_key "docentes", "turmas" + add_foreign_key "form_responses", "forms" + add_foreign_key "form_responses", "turmas" + add_foreign_key "form_responses", "users" + add_foreign_key "forms", "templates" add_foreign_key "formularios", "turmas" add_foreign_key "perguntas", "formularios" + add_foreign_key "questions", "templates" add_foreign_key "respostas", "formularios" add_foreign_key "respostas", "perguntas" add_foreign_key "respostas", "users" add_foreign_key "sigaa_logs", "users" add_foreign_key "templates", "templates", column: "parent_template_id" add_foreign_key "templates", "users" + add_foreign_key "turma_memberships", "turmas" + add_foreign_key "turma_memberships", "users" add_foreign_key "turmas", "disciplinas" end diff --git a/features/step_definitions/gerar_formulario_steps.rb b/features/step_definitions/gerar_formulario_steps.rb index 4c9033be86..fbe34e5d65 100644 --- a/features/step_definitions/gerar_formulario_steps.rb +++ b/features/step_definitions/gerar_formulario_steps.rb @@ -148,6 +148,18 @@ end end +Quando('marco o checkbox de uma turma na tabela') do + within('.modal table tbody') do + first("input[type='checkbox']").check + end +end + +Quando('o modal de {string} é aberto') do |_nome_modal| + within('.modal') do + expect(page).to have_selector('table') + end +end + # ==================== VERIFICAÇÕES — CENÁRIOS FELIZES ==================== Então('o formulário fica disponível para os discentes da turma {string}') do |codigo_turma| diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 2365f641cc..ee8e05c1c1 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -70,7 +70,16 @@ # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") Capybara.default_driver = :rack_test - Capybara.javascript_driver = :selenium_chrome_headless + + Capybara.register_driver :selenium_firefox_headless do |app| + options = Selenium::WebDriver::Firefox::Options.new + options.add_argument("--headless") + options.binary = "/snap/firefox/current/usr/lib/firefox/firefox" + Capybara::Selenium::Driver.new(app, browser: :firefox, options: options) + end + + Capybara.javascript_driver = :selenium_firefox_headless + Capybara.server = :puma, { Silent: true } config.before(:suite) do DatabaseCleaner.strategy = :truncation From c94f45b27ca5788c38293fe47400fd0904d3c4f2 Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Mon, 29 Jun 2026 20:23:27 -0300 Subject: [PATCH 74/87] =?UTF-8?q?=F0=9F=90=9B=20Refatora=C3=A7=C3=A3o=20c?= =?UTF-8?q?=C3=B3digos=20repetidos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 2 +- .../criacao_form_para_dic_doc_steps.rb | 12 ------- .../step_definitions/visu_form_resp_steps.rb | 16 ---------- .../step_definitions/visu_resul_form_steps.rb | 31 ------------------- 4 files changed, 1 insertion(+), 60 deletions(-) diff --git a/Gemfile b/Gemfile index bf5c5d44a2..d428b3758b 100644 --- a/Gemfile +++ b/Gemfile @@ -56,7 +56,7 @@ group :development, :test do gem "rubocop-rails-omakase", require: false # Gems para a Entrega 3 (Sprint 3) - gem "saikuro" + gem "saikuro", require: false gem "rubycritic", require: false gem "simplecov", require: false gem "rdoc" diff --git a/features/step_definitions/criacao_form_para_dic_doc_steps.rb b/features/step_definitions/criacao_form_para_dic_doc_steps.rb index b2996d8752..49229ec28d 100644 --- a/features/step_definitions/criacao_form_para_dic_doc_steps.rb +++ b/features/step_definitions/criacao_form_para_dic_doc_steps.rb @@ -41,10 +41,6 @@ fill_in 'data_termino', with: '01/06/2026' end -Quando('deixo o campo {string} em branco') do |campo| - fill_in campo, with: '' -end - Quando('não seleciono nenhum template no dropdown {string}') do |campo| # Campo já começa sem seleção expect(find("select[name='#{campo.downcase.gsub(/\s+/, '_')}']").value).to be_nil @@ -61,10 +57,6 @@ expect(page).not_to have_selector('.modal') end -Então('vejo a mensagem de sucesso {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('o formulário fica disponível para os dicentes da turma {string}') do |turma| # Verifica se o formulário foi criado e associado à turma expect(Form.last.destiny_type).to eq('discente') @@ -194,10 +186,6 @@ expect(page).to have_selector('.modal') end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('a data de término anterior à data de início') do fill_in 'data_termino', with: '20/05/2026' end \ No newline at end of file diff --git a/features/step_definitions/visu_form_resp_steps.rb b/features/step_definitions/visu_form_resp_steps.rb index 5d6bbc57a7..16f00c98a8 100644 --- a/features/step_definitions/visu_form_resp_steps.rb +++ b/features/step_definitions/visu_form_resp_steps.rb @@ -89,10 +89,6 @@ def assign_user_to_turma(user, turma) expect(page).to have_content(info4) # Data de Término end -Então('vejo um botão {string}') do |botao| - expect(page).to have_button(botao) -end - Então('vejo a quantidade de questões do formulário') do expect(page).to have_content('questões') end @@ -360,10 +356,6 @@ def assign_user_to_turma(user, turma) expect(page).to have_field(with: 'Resposta anterior') end -Então('vejo um botão {string}') do |botao| - expect(page).to have_button(botao) -end - Então('as respostas anteriores estão mantidas') do expect(page).to have_field(with: 'Resposta anterior') end @@ -481,10 +473,6 @@ def assign_user_to_turma(user, turma) expect(page).not_to have_content('Avaliação Expirada') end -Então('vejo um botão {string}') do |botao| - expect(page).to have_button(botao) -end - Dado('que tentei abrir um formulário após seu deadline') do @class = Turma.create!( classCode: 'CIC0105', @@ -512,10 +500,6 @@ def assign_user_to_turma(user, turma) visit avaliacoes_path end -Então('vejo um botão {string}') do |botao| - expect(page).to have_button(botao) -end - Então('nenhum formulário é exibido') do expect(page).not_to have_selector('.form-card') end diff --git a/features/step_definitions/visu_resul_form_steps.rb b/features/step_definitions/visu_resul_form_steps.rb index 344fb558ab..2a3575ddbd 100644 --- a/features/step_definitions/visu_resul_form_steps.rb +++ b/features/step_definitions/visu_resul_form_steps.rb @@ -378,10 +378,6 @@ expect(page).not_to have_selector('.questao, canvas, svg') end -Então('vejo o botão {string}') do |botao| - expect(page).to have_button(botao) -end - Dado('que existem múltiplos formulários com quantidades diferentes de respostas') do @class = Class.create!( code: 'CIC0105', @@ -459,10 +455,6 @@ expect(page).to have_current_path(home_path, wait: 10) end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Dado('que nenhum formulário foi criado ou enviado') do # Nenhum formulário é criado end @@ -471,10 +463,6 @@ expect(page).not_to have_selector('table tbody tr') end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('o botão {string} não está disponível') do |botao| expect(page).not_to have_button(botao) end @@ -520,10 +508,6 @@ expect(page).not_to have_button(botao2) end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Dado('que um formulário foi deletado do sistema') do @form = Form.create!( destiny_type: 'discente', @@ -539,10 +523,6 @@ visit admin_form_details_path(@form_id) end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('sou redirecionado para a página {string}') do |pagina| case pagina when 'Gerenciamento - Resultados' @@ -556,9 +536,6 @@ expect(page).to have_selector('table') end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end Dado('que existe um formulário com mais de 1000 respostas') do @class = Class.create!( @@ -595,19 +572,11 @@ expect(page).not_to have_selector('canvas, svg') end -Então('vejo a mensagem {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Quando('clico no botão {string} e a exportação falha') do |botao| allow(FormResponse).to receive(:for_export).and_raise(StandardError) click_button botao end -Então('vejo a mensagem de erro {string}') do |mensagem| - expect(page).to have_content(mensagem) -end - Então('o arquivo não é baixado') do expect(page.response_headers['Content-Disposition']).to be_nil end \ No newline at end of file From 0199406c8acbfadd01c8aa822aa4bc41abd72cfa Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Mon, 29 Jun 2026 20:56:56 -0300 Subject: [PATCH 75/87] =?UTF-8?q?=F0=9F=90=9B=20Refatora=C3=A7=C3=A3o=20c?= =?UTF-8?q?=C3=B3digos=20repetidos=20colocados=20no=20shared=5Fsteps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 8196 -> 8196 bytes features/.DS_Store | Bin 0 -> 8196 bytes .../criacao_form_para_dic_doc_steps.rb | 25 +--- .../gerar_formulario_steps.rb | 35 +---- .../step_definitions/gerar_relatorio_steps.rb | 24 ---- .../step_definitions/gerar_template_steps.rb | 48 ------- .../step_definitions/redef_senha_steps.rb | 8 +- .../responder_formulario_steps.rb | 4 +- features/step_definitions/shared_steps.rb | 132 +++++++++++++++++- .../sist_geren_depart_steps.rb | 61 +------- .../step_definitions/visu_form_resp_steps.rb | 50 +------ .../step_definitions/visu_resul_form_steps.rb | 74 +--------- 12 files changed, 154 insertions(+), 307 deletions(-) create mode 100644 features/.DS_Store diff --git a/.DS_Store b/.DS_Store index 2f7be4769c48087a790d48d001ae4777b99803b8..a06eaf135ee83c8dd9aa1bc9d7d07f34f4dd65b7 100644 GIT binary patch delta 416 zcmZp1XmOa}&nUk!U^hRb{AM13(@c|>3kgoP5oq8rH8IptFf=fjd_=%rmV+UUA(bJK zp@gB7p$Nz-E($LorY~m7#zko*@NDr!izQH**k47%p3t7>_uiW zm2Z+sDTB^xn=l1T0aL&fFa=D3T~Gk;Y_@|t-uwQnji!Jp@E{f7^C3YerU4s|`sl!* zB>-?A)7o&1IzVlbfN8+Sqnd)#R}U_jx=>;`Z;tZ`mjly)jYn@T&YO!1JG)S!INcp~ zi7pqHdbH6LFa^2_uy#*Su~$-KjO+IyelH|PB|nlv=D-E2`w3<Pq*wrZcW`f7I~>6}2P>~TmuvX~)_#WXErtYk-h&Lj(VH9!=Sz<(9M9zj zb5t_FPXqU>#eW)Ib0%NG!uYKeq2_bUT=)CEH|g+B)i3H zuC8ixwV?eP>kH&3ud0qT8Y~s;R|v|laUDlo%nP#4BHn&B=)K1FcFDGS{_%Om+}QFC z<(;&~jnG;%1$I<{N8Ya!*8d;x{{FwC-kV#dfGO}V6ma_&pDxb96l&`ZCu{8l{WCfn r*EJs11cR32KrP3C%RdZpoM0(!(twRe%%J`E4*~Z6SKV!R*b4js&yjsy literal 0 HcmV?d00001 diff --git a/features/step_definitions/criacao_form_para_dic_doc_steps.rb b/features/step_definitions/criacao_form_para_dic_doc_steps.rb index 49229ec28d..fa1fb2b584 100644 --- a/features/step_definitions/criacao_form_para_dic_doc_steps.rb +++ b/features/step_definitions/criacao_form_para_dic_doc_steps.rb @@ -15,10 +15,6 @@ select opcao, from: campo end -Quando('seleciono o template {string} no dropdown {string}') do |template, campo| - select template, from: campo -end - Quando('seleciono a turma {string} no campo {string}') do |turma, campo| select turma, from: campo end @@ -41,11 +37,6 @@ fill_in 'data_termino', with: '01/06/2026' end -Quando('não seleciono nenhum template no dropdown {string}') do |campo| - # Campo já começa sem seleção - expect(find("select[name='#{campo.downcase.gsub(/\s+/, '_')}']").value).to be_nil -end - Quando('não seleciono nenhuma turma no campo {string}') do |campo| # Campo já começa sem seleção expect(find("select[name='#{campo.downcase.gsub(/\s+/, '_')}']").value).to be_nil @@ -53,10 +44,6 @@ # ==================== CENÁRIOS FELIZES - VERIFICAÇÕES ==================== -Então('o modal é fechado') do - expect(page).not_to have_selector('.modal') -end - Então('o formulário fica disponível para os dicentes da turma {string}') do |turma| # Verifica se o formulário foi criado e associado à turma expect(Form.last.destiny_type).to eq('discente') @@ -136,8 +123,8 @@ # Login como dicente visit login_path - fill_in 'email', with: 'dicente@gmail.com' - fill_in 'password', with: 'senhaDicente' + fill_in 'Email ou Matrícula', with: 'dicente@gmail.com' + fill_in 'Senha', with: 'senhaDicente' click_button 'Entrar' # Acessa painel de Avaliações @@ -167,8 +154,8 @@ # Login como docente visit login_path - fill_in 'email', with: 'docente@gmail.com' - fill_in 'password', with: 'senhaDocente' + fill_in 'Email ou Matrícula', with: 'docente@gmail.com' + fill_in 'Senha', with: 'senhaDocente' click_button 'Entrar' # Acessa painel de Avaliações @@ -182,10 +169,6 @@ # ==================== CENÁRIOS TRISTES - VERIFICAÇÕES ==================== -Então('o modal permanece aberto') do - expect(page).to have_selector('.modal') -end - Então('a data de término anterior à data de início') do fill_in 'data_termino', with: '20/05/2026' end \ No newline at end of file diff --git a/features/step_definitions/gerar_formulario_steps.rb b/features/step_definitions/gerar_formulario_steps.rb index fbe34e5d65..490fa8146b 100644 --- a/features/step_definitions/gerar_formulario_steps.rb +++ b/features/step_definitions/gerar_formulario_steps.rb @@ -104,24 +104,8 @@ end end -Então('vejo o botão {string}') do |botao| - expect(page).to have_button(botao) -end - # ==================== AÇÕES — PREENCHIMENTO DO MODAL ==================== -Quando('seleciono o template {string} no dropdown {string}') do |template, campo| - within('.modal') do - select template, from: campo - end -end - -Quando('não seleciono nenhum template no dropdown {string}') do |campo| - within('.modal') do - expect(find("select[name='template']").value).to be_blank - end -end - Quando('marco o checkbox da turma {string} com semestre {string} e código {string}') do |nome, semestre, codigo| within('.modal table tbody') do row = find('tr', text: codigo) @@ -187,8 +171,8 @@ @discente.turmas << turma visit login_path - fill_in 'email', with: 'discente.form@gmail.com' - fill_in 'password', with: 'senhaDicente' + fill_in 'Email ou Matrícula', with: 'discente.form@gmail.com' + fill_in 'Senha', with: 'senhaDicente' click_button 'Entrar' visit avaliacoes_path @@ -224,8 +208,8 @@ # Faz logout do discente e login como admin para verificar click_on 'Logout' if page.has_link?('Logout') visit login_path - fill_in 'email', with: 'admin@gmail.com' - fill_in 'password', with: 'senhaAdmin' + fill_in 'Email ou Matrícula', with: 'admin@gmail.com' + fill_in 'Senha', with: 'senhaAdmin' click_button 'Entrar' visit admin_results_path @@ -243,14 +227,3 @@ Então('o botão {string} está desabilitado') do |botao| expect(page).to have_button(botao, disabled: true) end - -Quando('tento acessar a página {string}') do |pagina| - case pagina - when 'Gerenciamento' - visit admin_management_path - when 'Gerenciamento - Templates' - visit admin_templates_path - when 'Gerenciamento - Resultados' - visit admin_results_path - end -end \ No newline at end of file diff --git a/features/step_definitions/gerar_relatorio_steps.rb b/features/step_definitions/gerar_relatorio_steps.rb index e1693461e6..36a70ee95d 100644 --- a/features/step_definitions/gerar_relatorio_steps.rb +++ b/features/step_definitions/gerar_relatorio_steps.rb @@ -105,20 +105,6 @@ Turma.destroy_all end -Dado('que estou logado como Discente') do - @discente_user = User.create!( - email: 'discente.teste@gmail.com', - matricula: '190099999', - password: 'senhaDicente', - password_confirmation: 'senhaDicente', - role: 'discente' - ) - visit login_path - fill_in 'email', with: 'discente.teste@gmail.com' - fill_in 'password', with: 'senhaDicente' - click_button 'Entrar' -end - # ==================== AÇÕES ==================== Quando('clico no card da turma {string}') do |nome_turma| @@ -134,12 +120,6 @@ end end -Quando('tento acessar a página {string}') do |pagina| - case pagina - when 'Gerenciamento - Resultados' - visit admin_results_path - end -end # ==================== VERIFICAÇÕES - CENÁRIOS FELIZES ==================== @@ -211,7 +191,3 @@ Então('nenhum card de turma é exibido') do expect(page).not_to have_selector('.turma-card') end - -Então('sou redirecionado para o painel do usuário') do - expect(current_path).to eq(avaliacoes_path) -end \ No newline at end of file diff --git a/features/step_definitions/gerar_template_steps.rb b/features/step_definitions/gerar_template_steps.rb index 1145657003..76ea93abe2 100644 --- a/features/step_definitions/gerar_template_steps.rb +++ b/features/step_definitions/gerar_template_steps.rb @@ -42,29 +42,8 @@ visit admin_templates_path end -Dado('que estou logado como Discente') do - @discente_user = User.create!( - email: 'discente.teste@gmail.com', - matricula: '190099999', - password: 'senhaDicente', - password_confirmation: 'senhaDicente', - role: 'discente' - ) - visit login_path - fill_in 'email', with: 'discente.teste@gmail.com' - fill_in 'password', with: 'senhaDicente' - click_button 'Entrar' -end - # ==================== AÇÕES — NAVEGAÇÃO E MODAL ==================== -Então('sou redirecionado para a página {string}') do |pagina| - case pagina - when 'Gerenciamento - Templates' - expect(current_path).to eq(admin_templates_path) - end -end - Então('vejo os templates existentes exibidos em cards com nome e semestre') do expect(page).to have_selector('.template-card') within('.template-card', match: :first) do @@ -101,10 +80,6 @@ end end -Então('vejo o botão {string}') do |botao| - expect(page).to have_button(botao) -end - # ==================== AÇÕES — PREENCHIMENTO DO MODAL ==================== Quando('seleciono o tipo {string} na {string}') do |tipo, questao| @@ -157,10 +132,6 @@ end end -Quando('não seleciono nenhum template no dropdown {string}') do |campo| - expect(find("select[name='#{campo.downcase.gsub(/\s+/, '_')}']").value).to be_nil -end - # ==================== AÇÕES — EDITAR E EXCLUIR ==================== Quando('clico no ícone de editar do template {string}') do |nome_template| @@ -196,10 +167,6 @@ # ==================== VERIFICAÇÕES — CENÁRIOS FELIZES ==================== -Então('o modal é fechado') do - expect(page).not_to have_selector('.modal') -end - Então('o template {string} aparece na listagem de cards') do |nome_template| expect(page).to have_selector('.template-card', text: nome_template) end @@ -222,18 +189,3 @@ Então('o card {string} é removido da listagem') do |nome_template| expect(page).not_to have_selector('.template-card', text: nome_template) end - -# ==================== VERIFICAÇÕES — CENÁRIOS TRISTES ==================== - -Então('o modal permanece aberto') do - expect(page).to have_selector('.modal') -end - -Quando('tento acessar a página {string}') do |pagina| - case pagina - when 'Gerenciamento - Templates' - visit admin_templates_path - when 'Gerenciamento' - visit admin_management_path - end -end \ No newline at end of file diff --git a/features/step_definitions/redef_senha_steps.rb b/features/step_definitions/redef_senha_steps.rb index 266a2c8a4c..1049977cb7 100644 --- a/features/step_definitions/redef_senha_steps.rb +++ b/features/step_definitions/redef_senha_steps.rb @@ -4,9 +4,11 @@ Dado('que recebi um e-mail de solicitação de troca de senha no endereço {string}') do |email| @user = User.create!( - email: email, - matricula: '190084006', - role: 'discente' + email: email, + matricula: '190084006', + role: 'discente', + password: 'placeholder_temp', + password_confirmation: 'placeholder_temp' ) @user.generate_reset_token! end diff --git a/features/step_definitions/responder_formulario_steps.rb b/features/step_definitions/responder_formulario_steps.rb index 7794a5944f..ee96fb6854 100644 --- a/features/step_definitions/responder_formulario_steps.rb +++ b/features/step_definitions/responder_formulario_steps.rb @@ -12,8 +12,8 @@ ) end visit login_path - fill_in 'login', with: 'participante@gmail.com' - fill_in 'password', with: 'password123' + fill_in 'Email ou Matrícula', with: 'participante@gmail.com' + fill_in 'Senha', with: 'password123' click_button 'Entrar' end diff --git a/features/step_definitions/shared_steps.rb b/features/step_definitions/shared_steps.rb index 6342942c89..6d4a9aa1a8 100644 --- a/features/step_definitions/shared_steps.rb +++ b/features/step_definitions/shared_steps.rb @@ -1,5 +1,8 @@ +# encoding: utf-8 # Steps genéricos compartilhados entre todas as features +# ==================== CAMPOS E BOTÕES ==================== + Quando('preencho o campo {string} com {string}') do |campo, valor| fill_in campo, with: valor end @@ -22,4 +25,131 @@ Então('vejo a mensagem {string}') do |mensagem| expect(page).to have_content(mensagem) -end \ No newline at end of file +end + +# ==================== MODAL ==================== + +Então('o modal é fechado') do + expect(page).not_to have_selector('.modal') +end + +Então('o modal permanece aberto') do + expect(page).to have_selector('.modal') +end + +# ==================== AUTENTICAÇÃO (roles reutilizáveis) ==================== + +Dado('que estou logado como Discente') do + @discente_user = User.create!( + email: 'discente.teste@gmail.com', + matricula: '190099999', + password: 'senhaDicente', + password_confirmation: 'senhaDicente', + role: 'discente' + ) + visit login_path + fill_in 'Email ou Matrícula', with: 'discente.teste@gmail.com' + fill_in 'Senha', with: 'senhaDicente' + click_button 'Entrar' +end + +Dado('que estou logado como Dicente') do + @dicente = User.create!( + email: 'dicente@gmail.com', + matricula: '190084009', + password: 'senhaDicente', + password_confirmation: 'senhaDicente', + role: 'discente' + ) + visit login_path + fill_in 'Email ou Matrícula', with: 'dicente@gmail.com' + fill_in 'Senha', with: 'senhaDicente' + click_button 'Entrar' +end + +# ==================== NAVEGAÇÃO GENÉRICA ==================== + +Quando('acesso a página {string}') do |pagina| + case pagina + when 'Avaliações' + visit avaliacoes_path + when 'Gerenciamento - Turmas' + visit admin_classes_path + when 'Gerenciamento - Resultados' + visit admin_results_path + when 'Gerenciamento - Templates' + visit admin_templates_path + when 'Gerenciamento - Formulários Ativos' + visit admin_forms_path if respond_to?(:admin_forms_path) + else + visit '/' + end +end + +Quando('tento acessar a página {string}') do |pagina| + case pagina + when 'Gerenciamento' + visit admin_management_path + when 'Gerenciamento - Templates' + visit admin_templates_path + when 'Gerenciamento - Resultados' + visit admin_results_path + when 'Gerenciamento - Turmas' + visit admin_classes_path + else + visit '/' + end +end + +# ==================== REDIRECIONAMENTOS ==================== + +Então('sou redirecionado para o painel do usuário') do + expect(page).to have_current_path(home_path, wait: 10) +end + +Então('sou redirecionado para a página {string}') do |pagina| + case pagina + when 'Gerenciamento - Templates' + expect(page).to have_current_path(admin_templates_path, wait: 10) + when 'Gerenciamento - Turmas' + expect(page).to have_current_path(admin_classes_path, wait: 10) + when 'Gerenciamento - Resultados' + expect(page).to have_current_path(admin_results_path, wait: 10) + when 'Avaliações' + expect(page).to have_current_path(avaliacoes_path, wait: 10) + else + expect(page).to have_current_path('/', wait: 10) + end +end + +# ==================== FILTROS GENÉRICOS ==================== + +Quando('seleciono a turma {string} no filtro') do |turma| + select turma, from: 'filtro_turma' +end + +Quando('seleciono {string} no filtro {string}') do |opcao, filtro| + select opcao, from: filtro +end + +Quando('seleciono o template {string} no dropdown {string}') do |template, campo| + within('.modal') do + select template, from: campo + end +end + +Quando('não seleciono nenhum template no dropdown {string}') do |campo| + within('.modal') do + expect(find("select[name='template']").value).to be_blank + end +end + +Quando('clico no cabeçalho da coluna {string}') do |coluna| + find('th', text: coluna).click +end + +# ==================== FORMULÁRIOS PARA DOCENTES ==================== + +Então('os formulários para docentes deixam de aparecer') do + expect(page).not_to have_content('Docentes') +end diff --git a/features/step_definitions/sist_geren_depart_steps.rb b/features/step_definitions/sist_geren_depart_steps.rb index 8a9f890943..3acd60bb67 100644 --- a/features/step_definitions/sist_geren_depart_steps.rb +++ b/features/step_definitions/sist_geren_depart_steps.rb @@ -15,38 +15,16 @@ ) visit login_path - fill_in 'email', with: @admin_user.email - fill_in 'password', with: 'senhaAdmin' + fill_in 'Email ou Matrícula', with: @admin_user.email + fill_in 'Senha', with: 'senhaAdmin' click_button 'Entrar' end -# Removed duplicate click step - -Então('sou redirecionado para a página {string}') do |pagina| - case pagina - when 'Gerenciamento - Turmas' - expect(page).to have_current_path(admin_classes_path, wait: 10) - else - expect(page).to have_current_path('/') - end -end - Então('vejo uma tabela com as turmas do departamento {string}') do |departamento| expect(page).to have_selector('table') expect(page).to have_content(departamento) end -# ==================== CENÁRIOS FELIZES - AÇÕES E VERIFICAÇÕES ==================== - -Quando('acesso a página {string}') do |pagina| - case pagina - when 'Gerenciamento - Turmas' - visit admin_classes_path - else - visit '/' - end -end - Então('vejo a tabela com colunas {string}, {string}, {string}, {string} e {string}') do |col1, col2, col3, col4, col5| expect(page).to have_selector('table th', text: col1) expect(page).to have_selector('table th', text: col2) @@ -258,10 +236,6 @@ ) end -Quando('seleciono {string} no filtro {string}') do |opcao, filtro| - select opcao, from: filtro -end - Então('a tabela exibe apenas as turmas ativas') do expect(page).to have_content('Ativa') end @@ -270,10 +244,6 @@ expect(page).not_to have_content('Encerrada') end -Quando('clico no cabeçalho da coluna {string}') do |coluna| - find("th", text: coluna).click -end - Então('a tabela é ordenada alfabeticamente por nome') do expect(page).to have_selector('table tbody tr:first-child') end @@ -294,29 +264,6 @@ # ==================== CENÁRIOS TRISTES ==================== -Dado('que estou logado como Dicente') do - @dicente = User.create!( - email: 'dicente@gmail.com', - matricula: '190084009', - password: 'senhaDicente', - password_confirmation: 'senhaDicente', - role: 'discente' - ) - - visit login_path - fill_in 'email', with: 'dicente@gmail.com' - fill_in 'password', with: 'senhaDicente' - click_button 'Entrar' -end - -Quando('tento acessar a página {string}') do |pagina| - visit admin_classes_path -end - -Então('sou redirecionado para o painel do usuário') do - expect(page).to have_current_path(home_path, wait: 10) -end - Dado('que sou um Administrador do departamento {string}') do |departamento| @department = Department.find_or_create_by!(code: departamento, name: departamento) @@ -330,8 +277,8 @@ ) visit login_path - fill_in 'email', with: @admin_user.email - fill_in 'password', with: 'senhaAdmin' + fill_in 'Email ou Matrícula', with: @admin_user.email + fill_in 'Senha', with: 'senhaAdmin' click_button 'Entrar' end diff --git a/features/step_definitions/visu_form_resp_steps.rb b/features/step_definitions/visu_form_resp_steps.rb index 16f00c98a8..bd527e8e01 100644 --- a/features/step_definitions/visu_form_resp_steps.rb +++ b/features/step_definitions/visu_form_resp_steps.rb @@ -24,15 +24,6 @@ def assign_user_to_turma(user, turma) # ==================== CENÁRIOS FELIZES - AÇÕES ==================== -Quando('acesso a página {string}') do |pagina| - case pagina - when 'Avaliações' - visit avaliacoes_path - else - visit '/' - end -end - Então('vejo uma lista com cards de formulários não respondidos') do expect(page).to have_selector('.card') end @@ -121,10 +112,6 @@ def assign_user_to_turma(user, turma) @form2.update!(turma: @class2) end -Quando('seleciono a turma {string} no filtro') do |turma| - select turma, from: 'filtro_turma' -end - Então('a lista exibe apenas os formulários da turma {string}') do |turma| expect(page).to have_content(turma) end @@ -172,18 +159,10 @@ def assign_user_to_turma(user, turma) @form_docente.update!(turma: @class) end -Quando('seleciono {string} no filtro {string}') do |opcao, filtro| - select opcao, from: filtro -end - Então('a lista exibe apenas os formulários destinados a dicentes') do expect(page).to have_content('Avaliação para Dicentes') end -Então('os formulários para docentes deixam de aparecer') do - expect(page).not_to have_content('Avaliação para Docentes') -end - Dado('que existem múltiplos formulários com datas diferentes') do @class = Turma.create!( classCode: 'CIC0105', @@ -445,13 +424,6 @@ def assign_user_to_turma(user, turma) visit responder_form_path(@other_form) end -Então('sou redirecionado para a página {string}') do |pagina| - case pagina - when 'Avaliações' - expect(page).to have_current_path(avaliacoes_path, wait: 10) - end -end - Dado('que existe um formulário com deadline já vencido há 1 dia') do @class = Turma.create!( classCode: 'CIC0105', @@ -589,24 +561,6 @@ def assign_user_to_turma(user, turma) expect(page).to have_field('Questão 1', with: '') end -Dado('que estou preenchendo um formulário') do - @class = Turma.create!( - classCode: 'CIC0105', - nome: 'Engenharia de Software', - semester: '2026.1' - ) - - assign_user_to_turma(@user, @class) - - @form = Formulario.create!( - titulo: 'Avaliação', - deadline: 1.day.from_now - ) - - @form.update!(turma: @class) - visit responder_form_path(@form) -end - Quando('clico em {string} e o servidor retorna erro') do |acao| # Simula erro no servidor ao salvar allow(Resposta).to receive(:create!).and_raise(StandardError) @@ -682,8 +636,8 @@ def assign_user_to_turma(user, turma) ) visit login_path - fill_in 'email', with: 'dicente@gmail.com' - fill_in 'password', with: 'senha' + fill_in 'Email ou Matrícula', with: 'dicente@gmail.com' + fill_in 'Senha', with: 'senha' click_button 'Entrar' end diff --git a/features/step_definitions/visu_resul_form_steps.rb b/features/step_definitions/visu_resul_form_steps.rb index 2a3575ddbd..dbe1443499 100644 --- a/features/step_definitions/visu_resul_form_steps.rb +++ b/features/step_definitions/visu_resul_form_steps.rb @@ -1,31 +1,11 @@ # encoding: utf-8 # ==================== CONTEXTO ==================== - -Então('sou redirecionado para a página {string}') do |pagina| - case pagina - when 'Gerenciamento - Resultados' - expect(page).to have_current_path(admin_results_path, wait: 10) - else - expect(page).to have_current_path('/') - end -end - Então('vejo uma tabela com os formulários enviados') do expect(page).to have_selector('table') end # ==================== CENÁRIOS FELIZES - AÇÕES ==================== - -Quando('acesso a página {string}') do |pagina| - case pagina - when 'Gerenciamento - Resultados' - visit admin_results_path - else - visit '/' - end -end - Então('vejo uma tabela com as colunas {string}, {string}, {string}, {string} e {string}') do |col1, col2, col3, col4, col5| expect(page).to have_selector('table th', text: col1) expect(page).to have_selector('table th', text: col2) @@ -237,10 +217,6 @@ @form2.classes << @class2 end -Quando('seleciono a turma {string} no filtro') do |turma| - select turma, from: 'filtro_turma' -end - Então('a tabela exibe apenas os formulários da turma {string}') do |turma| expect(page).to have_content(turma) end @@ -273,18 +249,10 @@ @form_docente.classes << @class end -Quando('seleciono {string} no filtro {string}') do |opcao, filtro| - select opcao, from: filtro -end - Então('a tabela exibe apenas os formulários destinados a dicentes') do expect(page).to have_content('Dicentes') end -Então('os formulários para docentes deixam de aparecer') do - expect(page).not_to have_content('Docentes') -end - Dado('que existem formulários de períodos diferentes') do @form1 = Form.create!( destiny_type: 'discente', @@ -332,8 +300,6 @@ click_button 'Ver Detalhes' end -# Removed duplicate click step - Então('um arquivo com as respostas do formulário é baixado') do expect(page.response_headers['Content-Disposition']).to include('attachment') end @@ -412,10 +378,6 @@ FormResponse.create!(form: @form2, user: @dicente1) end -Quando('clico no cabeçalho da coluna {string}') do |coluna| - find("th", text: coluna).click -end - Então('a tabela é ordenada em ordem decrescente de respostas') do rows = all('table tbody tr') expect(rows.first).to have_content('3') # Maior quantidade @@ -432,29 +394,6 @@ # ==================== CENÁRIOS TRISTES ==================== -Dado('que estou logado como Dicente') do - @dicente = User.create!( - email: 'dicente@gmail.com', - matricula: '190084015', - password: 'senha', - password_confirmation: 'senha', - role: 'discente' - ) - - visit login_path - fill_in 'email', with: 'dicente@gmail.com' - fill_in 'password', with: 'senha' - click_button 'Entrar' -end - -Quando('tento acessar a página {string}') do |pagina| - visit admin_results_path -end - -Então('sou redirecionado para o painel do usuário') do - expect(page).to have_current_path(home_path, wait: 10) -end - Dado('que nenhum formulário foi criado ou enviado') do # Nenhum formulário é criado end @@ -477,8 +416,8 @@ ) visit login_path - fill_in 'email', with: 'docente@gmail.com' - fill_in 'password', with: 'senha' + fill_in 'Email ou Matrícula', with: 'docente@gmail.com' + fill_in 'Senha', with: 'senha' click_button 'Entrar' end @@ -523,15 +462,6 @@ visit admin_form_details_path(@form_id) end -Então('sou redirecionado para a página {string}') do |pagina| - case pagina - when 'Gerenciamento - Resultados' - expect(page).to have_current_path(admin_results_path, wait: 10) - end -end - -# Removed duplicate click step - Então('a tabela permanece sem aplicar o filtro') do expect(page).to have_selector('table') end From 68e6bc7c7f9b04db9e55662efe2383afc0525115 Mon Sep 17 00:00:00 2001 From: jessicamelo Date: Mon, 29 Jun 2026 21:47:53 -0300 Subject: [PATCH 76/87] =?UTF-8?q?=E2=9C=85=20Testes=20de=20Atualizar=20bas?= =?UTF-8?q?e=20de=20dados=20com=20os=20dados=20do=20SIGAA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/sigaa_controller.rb | 3 +- app/views/admin/management/index.html.erb | 1 + config/environments/test.rb | 2 +- .../atualizar_base_com_sigaa_steps.rb | 182 ++++++++++++++++++ .../atualizar_base_sigaa_steps.rb | 13 -- spec/fixtures/sigaa.json | 11 ++ 6 files changed, 197 insertions(+), 15 deletions(-) create mode 100644 features/step_definitions/atualizar_base_com_sigaa_steps.rb delete mode 100644 features/step_definitions/atualizar_base_sigaa_steps.rb create mode 100644 spec/fixtures/sigaa.json diff --git a/app/controllers/sigaa_controller.rb b/app/controllers/sigaa_controller.rb index d979b839bd..2f6bc0b81b 100644 --- a/app/controllers/sigaa_controller.rb +++ b/app/controllers/sigaa_controller.rb @@ -1,6 +1,7 @@ # Controller responsável pela sincronização de dados via API do SIGAA class SigaaController < ApplicationController - before_action :authenticate_user! + before_action :require_login + before_action :require_admin # Inicia o processo de atualização do banco de dados chamando o serviço de sincronização # diff --git a/app/views/admin/management/index.html.erb b/app/views/admin/management/index.html.erb index 9f659da5e2..7ea80dc9a2 100644 --- a/app/views/admin/management/index.html.erb +++ b/app/views/admin/management/index.html.erb @@ -1,6 +1,7 @@

Gerenciamento

+ <%= button_to "Atualizar Base SIGAA", "/sigaa/update", method: :post, class: "btn btn-warning", data: { turbo: false } %> -

Você já respondeu esta avaliação

- <% elsif formulario.deadline < Time.current %> - Encerrado -

Avaliação encerrada em <%= formulario.deadline.strftime('%d/%m/%Y') %>

- <% else %> - <%= link_to "Responder", formulario_path(formulario), class: "btn" %> - <% end %> -
- <% end %> -
diff --git a/app/views/formularios/show.html.erb b/app/views/formularios/show.html.erb deleted file mode 100644 index 0aabefbdeb..0000000000 --- a/app/views/formularios/show.html.erb +++ /dev/null @@ -1,26 +0,0 @@ -

<%= @formulario.titulo %>

- -<% if flash[:alert] %> -
<%= flash[:alert] %>
-<% end %> - -<%= form_with url: formulario_respostas_path(@formulario), method: :post, local: true do |f| %> - <% @perguntas.each do |pergunta| %> -
- - - <% if pergunta.tipo_pergunta == 'texto' %> - <%= text_area_tag "respostas[#{pergunta.id}]", params.dig(:respostas, pergunta.id.to_s), id: "pergunta_#{pergunta.id}" %> - <% end %> -
- <% end %> - - <%= f.submit "Enviar Avaliação" %> -<% end %> - - diff --git a/app/views/layouts/.DS_Store b/app/views/layouts/.DS_Store deleted file mode 100644 index 69b7915cbb4375362d1598a63861e1565e43058e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!AiqG5Peg7sCcl52QNatAmAT_QU%ZcfJq}*h(@%6$K3rGKT+T843##O+(hh5 znSGnd&g|}+lGz0yv)B3pm;#tk6-S3`c8I=L?a9b3W1^%OODs@gg(ceF=rH_61~hl4 z%-Iuv4fFP$zenV_XD%OT&vA!mRM$iF1OsaZ z^!t!e6${7CFnv1Mq$>bXkC92(*Q25OWR8VnXUI1cF;}9wieE9r+-c8zT;bRmnmfcV zAL28M-%upY&iGjx4k-+y4F-aNO$H9^ajNzI^5^^irc*ox1Hr(*VnD|8oB51e^4i+l voYvZmdP`N&ah>7Xg`HH2IaW*Y88r#*nRbYUV`oSUB@P0KhG>F;UuED6sNhbQ diff --git a/app/views/layouts/dashboard.html.erb b/app/views/layouts/dashboard.html.erb index bb674fbd31..036967da81 100644 --- a/app/views/layouts/dashboard.html.erb +++ b/app/views/layouts/dashboard.html.erb @@ -66,7 +66,7 @@